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>
323 lines
9.9 KiB
C#
323 lines
9.9 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 GetDocumentTool following TDD approach.
|
|
/// Tests integration with CODEX API /api/documents/{id} endpoint.
|
|
/// </summary>
|
|
public class GetDocumentToolTests
|
|
{
|
|
[Fact]
|
|
public void GetDocumentTool_ShouldHaveCorrectName()
|
|
{
|
|
// Arrange
|
|
var httpClient = new HttpClient();
|
|
var tool = new GetDocumentTool(httpClient);
|
|
|
|
// Act
|
|
var name = tool.Name;
|
|
|
|
// Assert
|
|
Assert.Equal("get_document", name);
|
|
}
|
|
|
|
[Fact]
|
|
public void GetDocumentTool_ShouldHaveDescription()
|
|
{
|
|
// Arrange
|
|
var httpClient = new HttpClient();
|
|
var tool = new GetDocumentTool(httpClient);
|
|
|
|
// Act
|
|
var description = tool.Description;
|
|
|
|
// Assert
|
|
Assert.NotNull(description);
|
|
Assert.NotEmpty(description);
|
|
Assert.Contains("document", description.ToLower());
|
|
}
|
|
|
|
[Fact]
|
|
public void GetDocumentTool_ShouldHaveValidSchema()
|
|
{
|
|
// Arrange
|
|
var httpClient = new HttpClient();
|
|
var tool = new GetDocumentTool(httpClient);
|
|
|
|
// Act
|
|
var schema = tool.Schema;
|
|
|
|
// Assert
|
|
Assert.NotNull(schema);
|
|
var root = schema.RootElement;
|
|
Assert.Equal(JsonValueKind.Object, root.ValueKind);
|
|
|
|
// Schema should define required "id" parameter
|
|
Assert.True(root.TryGetProperty("properties", out var properties));
|
|
Assert.True(properties.TryGetProperty("id", out var idProp));
|
|
Assert.True(idProp.TryGetProperty("type", out var idType));
|
|
Assert.Equal("string", idType.GetString());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetDocumentTool_ExecuteAsync_WithValidId_ShouldCallCodexApi()
|
|
{
|
|
// Arrange
|
|
var mockResponse = new
|
|
{
|
|
id = "doc123",
|
|
title = "Test Document",
|
|
content = "Document content",
|
|
metadata = new { author = "Test Author" }
|
|
};
|
|
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/doc123")
|
|
),
|
|
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 GetDocumentTool(httpClient);
|
|
|
|
var arguments = JsonDocument.Parse("""{"id": "doc123"}""");
|
|
|
|
// Act
|
|
var result = await tool.ExecuteAsync(arguments);
|
|
|
|
// Assert
|
|
Assert.NotNull(result);
|
|
var root = result.RootElement;
|
|
Assert.True(root.TryGetProperty("id", out var id));
|
|
Assert.Equal("doc123", id.GetString());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetDocumentTool_ExecuteAsync_WithEmptyId_ShouldReturnError()
|
|
{
|
|
// Arrange
|
|
var httpClient = new HttpClient();
|
|
var tool = new GetDocumentTool(httpClient);
|
|
var arguments = JsonDocument.Parse("""{"id": ""}""");
|
|
|
|
// 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("id", error.GetString().ToLower());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetDocumentTool_ExecuteAsync_WithNullArguments_ShouldReturnError()
|
|
{
|
|
// Arrange
|
|
var httpClient = new HttpClient();
|
|
var tool = new GetDocumentTool(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 GetDocumentTool_ExecuteAsync_WithMissingIdProperty_ShouldReturnError()
|
|
{
|
|
// Arrange
|
|
var httpClient = new HttpClient();
|
|
var tool = new GetDocumentTool(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 GetDocumentTool_ExecuteAsync_WithNotFound_ShouldReturnError()
|
|
{
|
|
// Arrange
|
|
var mockHttpMessageHandler = new Mock<HttpMessageHandler>();
|
|
mockHttpMessageHandler
|
|
.Protected()
|
|
.Setup<Task<HttpResponseMessage>>(
|
|
"SendAsync",
|
|
ItExpr.IsAny<HttpRequestMessage>(),
|
|
ItExpr.IsAny<CancellationToken>()
|
|
)
|
|
.ReturnsAsync(new HttpResponseMessage
|
|
{
|
|
StatusCode = HttpStatusCode.NotFound,
|
|
Content = new StringContent("Document not found")
|
|
});
|
|
|
|
var httpClient = new HttpClient(mockHttpMessageHandler.Object)
|
|
{
|
|
BaseAddress = new System.Uri("http://localhost:5050")
|
|
};
|
|
var tool = new GetDocumentTool(httpClient);
|
|
var arguments = JsonDocument.Parse("""{"id": "nonexistent"}""");
|
|
|
|
// 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 GetDocumentTool_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 GetDocumentTool(httpClient);
|
|
var arguments = JsonDocument.Parse("""{"id": "doc123"}""");
|
|
|
|
// 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 GetDocumentTool_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 GetDocumentTool(httpClient);
|
|
var arguments = JsonDocument.Parse("""{"id": "doc123"}""");
|
|
|
|
// Act
|
|
var result = await tool.ExecuteAsync(arguments);
|
|
|
|
// Assert
|
|
Assert.NotNull(result);
|
|
var root = result.RootElement;
|
|
Assert.True(root.TryGetProperty("error", out _));
|
|
}
|
|
|
|
[Fact]
|
|
public void GetDocumentTool_ShouldImplementIMcpTool()
|
|
{
|
|
// Arrange
|
|
var httpClient = new HttpClient();
|
|
|
|
// Act
|
|
var tool = new GetDocumentTool(httpClient);
|
|
|
|
// Assert
|
|
Assert.IsAssignableFrom<IMcpTool>(tool);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetDocumentTool_ExecuteAsync_ShouldUseCorrectEndpoint()
|
|
{
|
|
// 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("""{"id": "test123"}""")
|
|
};
|
|
});
|
|
|
|
var httpClient = new HttpClient(mockHttpMessageHandler.Object)
|
|
{
|
|
BaseAddress = new System.Uri("http://localhost:5050")
|
|
};
|
|
var tool = new GetDocumentTool(httpClient);
|
|
var arguments = JsonDocument.Parse("""{"id": "test123"}""");
|
|
|
|
// Act
|
|
await tool.ExecuteAsync(arguments);
|
|
|
|
// Assert
|
|
Assert.NotNull(capturedUri);
|
|
Assert.Contains("/api/documents/test123", capturedUri);
|
|
Assert.DoesNotContain("POST", capturedUri); // Should be GET
|
|
}
|
|
}
|