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>
353 lines
11 KiB
C#
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);
|
|
}
|
|
}
|