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>
288 lines
8.7 KiB
C#
288 lines
8.7 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 SearchCodexTool following TDD approach.
|
|
/// Tests integration with CODEX API /api/documents/search endpoint.
|
|
/// </summary>
|
|
public class SearchCodexToolTests
|
|
{
|
|
[Fact]
|
|
public void SearchCodexTool_ShouldHaveCorrectName()
|
|
{
|
|
// Arrange
|
|
var httpClient = new HttpClient();
|
|
var tool = new SearchCodexTool(httpClient);
|
|
|
|
// Act
|
|
var name = tool.Name;
|
|
|
|
// Assert
|
|
Assert.Equal("search_codex", name);
|
|
}
|
|
|
|
[Fact]
|
|
public void SearchCodexTool_ShouldHaveDescription()
|
|
{
|
|
// Arrange
|
|
var httpClient = new HttpClient();
|
|
var tool = new SearchCodexTool(httpClient);
|
|
|
|
// Act
|
|
var description = tool.Description;
|
|
|
|
// Assert
|
|
Assert.NotNull(description);
|
|
Assert.NotEmpty(description);
|
|
Assert.Contains("search", description.ToLower());
|
|
}
|
|
|
|
[Fact]
|
|
public void SearchCodexTool_ShouldHaveValidSchema()
|
|
{
|
|
// Arrange
|
|
var httpClient = new HttpClient();
|
|
var tool = new SearchCodexTool(httpClient);
|
|
|
|
// Act
|
|
var schema = tool.Schema;
|
|
|
|
// Assert
|
|
Assert.NotNull(schema);
|
|
var root = schema.RootElement;
|
|
Assert.Equal(JsonValueKind.Object, root.ValueKind);
|
|
|
|
// Schema should define required "query" parameter
|
|
Assert.True(root.TryGetProperty("properties", out var properties));
|
|
Assert.True(properties.TryGetProperty("query", out var queryProp));
|
|
Assert.True(queryProp.TryGetProperty("type", out var queryType));
|
|
Assert.Equal("string", queryType.GetString());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SearchCodexTool_ExecuteAsync_WithValidQuery_ShouldCallCodexApi()
|
|
{
|
|
// Arrange
|
|
var mockResponse = new
|
|
{
|
|
documents = new[]
|
|
{
|
|
new { id = "doc1", title = "Test Document", content = "Test content" }
|
|
},
|
|
total = 1
|
|
};
|
|
var mockHttpMessageHandler = new Mock<HttpMessageHandler>();
|
|
mockHttpMessageHandler
|
|
.Protected()
|
|
.Setup<Task<HttpResponseMessage>>(
|
|
"SendAsync",
|
|
ItExpr.Is<HttpRequestMessage>(req =>
|
|
req.Method == HttpMethod.Post &&
|
|
req.RequestUri.ToString().Contains("/api/documents/search")
|
|
),
|
|
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 SearchCodexTool(httpClient);
|
|
|
|
var arguments = JsonDocument.Parse("""{"query": "test search"}""");
|
|
|
|
// Act
|
|
var result = await tool.ExecuteAsync(arguments);
|
|
|
|
// Assert
|
|
Assert.NotNull(result);
|
|
var root = result.RootElement;
|
|
Assert.True(root.TryGetProperty("documents", out _));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SearchCodexTool_ExecuteAsync_WithEmptyQuery_ShouldReturnError()
|
|
{
|
|
// Arrange
|
|
var httpClient = new HttpClient();
|
|
var tool = new SearchCodexTool(httpClient);
|
|
var arguments = JsonDocument.Parse("""{"query": ""}""");
|
|
|
|
// Act
|
|
var result = await tool.ExecuteAsync(arguments);
|
|
|
|
// Assert
|
|
Assert.NotNull(result);
|
|
var root = result.RootElement;
|
|
Assert.True(root.TryGetProperty("error", out var error));
|
|
Assert.Contains("query", error.GetString().ToLower());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SearchCodexTool_ExecuteAsync_WithNullArguments_ShouldReturnError()
|
|
{
|
|
// Arrange
|
|
var httpClient = new HttpClient();
|
|
var tool = new SearchCodexTool(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 SearchCodexTool_ExecuteAsync_WithMissingQueryProperty_ShouldReturnError()
|
|
{
|
|
// Arrange
|
|
var httpClient = new HttpClient();
|
|
var tool = new SearchCodexTool(httpClient);
|
|
var arguments = JsonDocument.Parse("""{"other": "value"}""");
|
|
|
|
// Act
|
|
var result = await tool.ExecuteAsync(arguments);
|
|
|
|
// Assert
|
|
Assert.NotNull(result);
|
|
var root = result.RootElement;
|
|
Assert.True(root.TryGetProperty("error", out _));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SearchCodexTool_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 SearchCodexTool(httpClient);
|
|
var arguments = JsonDocument.Parse("""{"query": "test"}""");
|
|
|
|
// Act
|
|
var result = await tool.ExecuteAsync(arguments);
|
|
|
|
// Assert
|
|
Assert.NotNull(result);
|
|
var root = result.RootElement;
|
|
Assert.True(root.TryGetProperty("error", out _));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SearchCodexTool_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 SearchCodexTool(httpClient);
|
|
var arguments = JsonDocument.Parse("""{"query": "test"}""");
|
|
|
|
// Act
|
|
var result = await tool.ExecuteAsync(arguments);
|
|
|
|
// Assert
|
|
Assert.NotNull(result);
|
|
var root = result.RootElement;
|
|
Assert.True(root.TryGetProperty("error", out _));
|
|
}
|
|
|
|
[Fact]
|
|
public void SearchCodexTool_ShouldImplementIMcpTool()
|
|
{
|
|
// Arrange
|
|
var httpClient = new HttpClient();
|
|
|
|
// Act
|
|
var tool = new SearchCodexTool(httpClient);
|
|
|
|
// Assert
|
|
Assert.IsAssignableFrom<IMcpTool>(tool);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SearchCodexTool_ExecuteAsync_ShouldIncludeQueryInRequest()
|
|
{
|
|
// Arrange
|
|
string capturedRequestBody = null;
|
|
var mockHttpMessageHandler = new Mock<HttpMessageHandler>();
|
|
mockHttpMessageHandler
|
|
.Protected()
|
|
.Setup<Task<HttpResponseMessage>>(
|
|
"SendAsync",
|
|
ItExpr.IsAny<HttpRequestMessage>(),
|
|
ItExpr.IsAny<CancellationToken>()
|
|
)
|
|
.ReturnsAsync((HttpRequestMessage req, CancellationToken ct) =>
|
|
{
|
|
capturedRequestBody = req.Content?.ReadAsStringAsync().Result;
|
|
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 SearchCodexTool(httpClient);
|
|
var arguments = JsonDocument.Parse("""{"query": "test search query"}""");
|
|
|
|
// Act
|
|
await tool.ExecuteAsync(arguments);
|
|
|
|
// Assert
|
|
Assert.NotNull(capturedRequestBody);
|
|
Assert.Contains("test search query", capturedRequestBody);
|
|
}
|
|
}
|