svrnty-mcp-server/tests/CodexMcpServer.Tests/ListDocumentsToolTests.cs
Svrnty 516e1479c6 docs: comprehensive AI coding assistant research and MCP-first implementation plan
Research conducted on modern AI coding assistants (Cursor, GitHub Copilot, Cline,
Aider, Windsurf, Replit Agent) to understand architecture patterns, context management,
code editing workflows, and tool use protocols.

Key Decision: Pivoted from building full CLI (40-50h) to validation-driven MCP-first
approach (10-15h). Build 5 core CODEX MCP tools that work with ANY coding assistant,
validate adoption over 2-4 weeks, then decide on full CLI if demand proven.

Files:
- research/ai-systems/modern-coding-assistants-architecture.md (comprehensive research)
- research/ai-systems/codex-coding-assistant-implementation-plan.md (original CLI plan, preserved)
- research/ai-systems/codex-mcp-tools-implementation-plan.md (approved MCP-first plan)
- ideas/registry.json (updated with approved MCP tools proposal)

Architech Validation: APPROVED with pivot to MCP-first approach
Human Decision: Approved (pragmatic validation-driven development)

Next: Begin Phase 1 implementation (10-15 hours, 5 core MCP tools)

🤖 Generated with CODEX Research System

Co-Authored-By: The Archivist <archivist@codex.svrnty.io>
Co-Authored-By: The Architech <architech@codex.svrnty.io>
Co-Authored-By: Mathias Beaulieu-Duncan <mat@svrnty.io>
2025-10-22 21:00:34 -04:00

353 lines
11 KiB
C#

using Xunit;
using Moq;
using Moq.Protected;
using System.Net;
using System.Net.Http;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using OpenHarbor.MCP.Core;
using CodexMcpServer.Tools;
namespace CodexMcpServer.Tests;
/// <summary>
/// Unit tests for ListDocumentsTool following TDD approach.
/// Tests integration with CODEX API /api/documents endpoint with pagination.
/// </summary>
public class ListDocumentsToolTests
{
[Fact]
public void ListDocumentsTool_ShouldHaveCorrectName()
{
// Arrange
var httpClient = new HttpClient();
var tool = new ListDocumentsTool(httpClient);
// Act
var name = tool.Name;
// Assert
Assert.Equal("list_documents", name);
}
[Fact]
public void ListDocumentsTool_ShouldHaveDescription()
{
// Arrange
var httpClient = new HttpClient();
var tool = new ListDocumentsTool(httpClient);
// Act
var description = tool.Description;
// Assert
Assert.NotNull(description);
Assert.NotEmpty(description);
Assert.Contains("list", description.ToLower());
}
[Fact]
public void ListDocumentsTool_ShouldHaveValidSchema()
{
// Arrange
var httpClient = new HttpClient();
var tool = new ListDocumentsTool(httpClient);
// Act
var schema = tool.Schema;
// Assert
Assert.NotNull(schema);
var root = schema.RootElement;
Assert.Equal(JsonValueKind.Object, root.ValueKind);
// Schema should have optional pagination properties
Assert.True(root.TryGetProperty("properties", out var properties));
// All properties should be optional (no required array or empty required)
}
[Fact]
public async Task ListDocumentsTool_ExecuteAsync_WithNoArguments_ShouldCallCodexApi()
{
// Arrange
var mockResponse = new
{
documents = new[]
{
new { id = "doc1", title = "Document 1" },
new { id = "doc2", title = "Document 2" }
},
total = 2,
page = 1,
pageSize = 20
};
var mockHttpMessageHandler = new Mock<HttpMessageHandler>();
mockHttpMessageHandler
.Protected()
.Setup<Task<HttpResponseMessage>>(
"SendAsync",
ItExpr.Is<HttpRequestMessage>(req =>
req.Method == HttpMethod.Get &&
req.RequestUri.ToString().Contains("/api/documents")
),
ItExpr.IsAny<CancellationToken>()
)
.ReturnsAsync(new HttpResponseMessage
{
StatusCode = HttpStatusCode.OK,
Content = new StringContent(JsonSerializer.Serialize(mockResponse))
});
var httpClient = new HttpClient(mockHttpMessageHandler.Object)
{
BaseAddress = new System.Uri("http://localhost:5050")
};
var tool = new ListDocumentsTool(httpClient);
// Act
var result = await tool.ExecuteAsync(null);
// Assert
Assert.NotNull(result);
var root = result.RootElement;
Assert.True(root.TryGetProperty("documents", out _));
}
[Fact]
public async Task ListDocumentsTool_ExecuteAsync_WithPaginationParams_ShouldIncludeInRequest()
{
// Arrange
string capturedUri = null;
var mockHttpMessageHandler = new Mock<HttpMessageHandler>();
mockHttpMessageHandler
.Protected()
.Setup<Task<HttpResponseMessage>>(
"SendAsync",
ItExpr.IsAny<HttpRequestMessage>(),
ItExpr.IsAny<CancellationToken>()
)
.ReturnsAsync((HttpRequestMessage req, CancellationToken ct) =>
{
capturedUri = req.RequestUri?.ToString();
return new HttpResponseMessage
{
StatusCode = HttpStatusCode.OK,
Content = new StringContent("""{"documents": [], "total": 0}""")
};
});
var httpClient = new HttpClient(mockHttpMessageHandler.Object)
{
BaseAddress = new System.Uri("http://localhost:5050")
};
var tool = new ListDocumentsTool(httpClient);
var arguments = JsonDocument.Parse("""{"page": 2, "pageSize": 10}""");
// Act
await tool.ExecuteAsync(arguments);
// Assert
Assert.NotNull(capturedUri);
Assert.Contains("/api/documents", capturedUri);
Assert.Contains("page=2", capturedUri);
Assert.Contains("pageSize=10", capturedUri);
}
[Fact]
public async Task ListDocumentsTool_ExecuteAsync_WithHttpError_ShouldReturnErrorResponse()
{
// Arrange
var mockHttpMessageHandler = new Mock<HttpMessageHandler>();
mockHttpMessageHandler
.Protected()
.Setup<Task<HttpResponseMessage>>(
"SendAsync",
ItExpr.IsAny<HttpRequestMessage>(),
ItExpr.IsAny<CancellationToken>()
)
.ReturnsAsync(new HttpResponseMessage
{
StatusCode = HttpStatusCode.InternalServerError,
Content = new StringContent("Internal server error")
});
var httpClient = new HttpClient(mockHttpMessageHandler.Object)
{
BaseAddress = new System.Uri("http://localhost:5050")
};
var tool = new ListDocumentsTool(httpClient);
// Act
var result = await tool.ExecuteAsync(null);
// Assert
Assert.NotNull(result);
var root = result.RootElement;
Assert.True(root.TryGetProperty("error", out _));
}
[Fact]
public async Task ListDocumentsTool_ExecuteAsync_WithInvalidJson_ShouldReturnError()
{
// Arrange
var mockHttpMessageHandler = new Mock<HttpMessageHandler>();
mockHttpMessageHandler
.Protected()
.Setup<Task<HttpResponseMessage>>(
"SendAsync",
ItExpr.IsAny<HttpRequestMessage>(),
ItExpr.IsAny<CancellationToken>()
)
.ReturnsAsync(new HttpResponseMessage
{
StatusCode = HttpStatusCode.OK,
Content = new StringContent("invalid json{{{")
});
var httpClient = new HttpClient(mockHttpMessageHandler.Object)
{
BaseAddress = new System.Uri("http://localhost:5050")
};
var tool = new ListDocumentsTool(httpClient);
// Act
var result = await tool.ExecuteAsync(null);
// Assert
Assert.NotNull(result);
var root = result.RootElement;
Assert.True(root.TryGetProperty("error", out _));
}
[Fact]
public void ListDocumentsTool_ShouldImplementIMcpTool()
{
// Arrange
var httpClient = new HttpClient();
// Act
var tool = new ListDocumentsTool(httpClient);
// Assert
Assert.IsAssignableFrom<IMcpTool>(tool);
}
[Fact]
public async Task ListDocumentsTool_ExecuteAsync_WithLimit_ShouldIncludeInRequest()
{
// Arrange
string capturedUri = null;
var mockHttpMessageHandler = new Mock<HttpMessageHandler>();
mockHttpMessageHandler
.Protected()
.Setup<Task<HttpResponseMessage>>(
"SendAsync",
ItExpr.IsAny<HttpRequestMessage>(),
ItExpr.IsAny<CancellationToken>()
)
.ReturnsAsync((HttpRequestMessage req, CancellationToken ct) =>
{
capturedUri = req.RequestUri?.ToString();
return new HttpResponseMessage
{
StatusCode = HttpStatusCode.OK,
Content = new StringContent("""{"documents": [], "total": 0}""")
};
});
var httpClient = new HttpClient(mockHttpMessageHandler.Object)
{
BaseAddress = new System.Uri("http://localhost:5050")
};
var tool = new ListDocumentsTool(httpClient);
var arguments = JsonDocument.Parse("""{"limit": 5}""");
// Act
await tool.ExecuteAsync(arguments);
// Assert
Assert.NotNull(capturedUri);
Assert.Contains("limit=5", capturedUri);
}
[Fact]
public async Task ListDocumentsTool_ExecuteAsync_WithOffset_ShouldIncludeInRequest()
{
// Arrange
string capturedUri = null;
var mockHttpMessageHandler = new Mock<HttpMessageHandler>();
mockHttpMessageHandler
.Protected()
.Setup<Task<HttpResponseMessage>>(
"SendAsync",
ItExpr.IsAny<HttpRequestMessage>(),
ItExpr.IsAny<CancellationToken>()
)
.ReturnsAsync((HttpRequestMessage req, CancellationToken ct) =>
{
capturedUri = req.RequestUri?.ToString();
return new HttpResponseMessage
{
StatusCode = HttpStatusCode.OK,
Content = new StringContent("""{"documents": [], "total": 0}""")
};
});
var httpClient = new HttpClient(mockHttpMessageHandler.Object)
{
BaseAddress = new System.Uri("http://localhost:5050")
};
var tool = new ListDocumentsTool(httpClient);
var arguments = JsonDocument.Parse("""{"offset": 10}""");
// Act
await tool.ExecuteAsync(arguments);
// Assert
Assert.NotNull(capturedUri);
Assert.Contains("offset=10", capturedUri);
}
[Fact]
public async Task ListDocumentsTool_ExecuteAsync_WithMultipleParams_ShouldIncludeAllInRequest()
{
// Arrange
string capturedUri = null;
var mockHttpMessageHandler = new Mock<HttpMessageHandler>();
mockHttpMessageHandler
.Protected()
.Setup<Task<HttpResponseMessage>>(
"SendAsync",
ItExpr.IsAny<HttpRequestMessage>(),
ItExpr.IsAny<CancellationToken>()
)
.ReturnsAsync((HttpRequestMessage req, CancellationToken ct) =>
{
capturedUri = req.RequestUri?.ToString();
return new HttpResponseMessage
{
StatusCode = HttpStatusCode.OK,
Content = new StringContent("""{"documents": [], "total": 0}""")
};
});
var httpClient = new HttpClient(mockHttpMessageHandler.Object)
{
BaseAddress = new System.Uri("http://localhost:5050")
};
var tool = new ListDocumentsTool(httpClient);
var arguments = JsonDocument.Parse("""{"page": 3, "pageSize": 15, "limit": 100, "offset": 30}""");
// Act
await tool.ExecuteAsync(arguments);
// Assert
Assert.NotNull(capturedUri);
Assert.Contains("page=3", capturedUri);
Assert.Contains("pageSize=15", capturedUri);
Assert.Contains("limit=100", capturedUri);
Assert.Contains("offset=30", capturedUri);
}
}