Files
amcs/internal/mcpserver/streamable_integration_test.go
Hein 9e6d05e055
Some checks failed
CI / build-and-test (push) Failing after -31m24s
feat(ui): add content editor components for skills and thoughts
* Implement ContentEditorField for inline editing of content
* Create ContentEditorModal for editing content in a modal
* Introduce FormerShell for managing forms related to skills and thoughts
* Enhance SkillsPage and ThoughtsPage with new components for better content management
2026-05-02 19:35:27 +02:00

214 lines
7.0 KiB
Go

package mcpserver
import (
"context"
"net/http/httptest"
"testing"
"time"
"github.com/modelcontextprotocol/go-sdk/jsonrpc"
"github.com/modelcontextprotocol/go-sdk/mcp"
"git.warky.dev/wdevs/amcs/internal/buildinfo"
"git.warky.dev/wdevs/amcs/internal/config"
"git.warky.dev/wdevs/amcs/internal/mcperrors"
"git.warky.dev/wdevs/amcs/internal/tools"
)
func TestStreamableHTTPReturnsStructuredToolErrors(t *testing.T) {
handler, err := New(config.MCPConfig{
ServerName: "test",
Version: "0.0.1",
SessionTimeout: time.Minute,
}, nil, streamableTestToolSet(), nil)
if err != nil {
t.Fatalf("mcpserver.New() error = %v", err)
}
httpServer := httptest.NewServer(handler)
defer httpServer.Close()
client := mcp.NewClient(&mcp.Implementation{Name: "client", Version: "0.0.1"}, nil)
cs, err := client.Connect(context.Background(), &mcp.StreamableClientTransport{Endpoint: httpServer.URL}, nil)
if err != nil {
t.Fatalf("connect client: %v", err)
}
defer func() {
_ = cs.Close()
}()
t.Run("schema_validation", func(t *testing.T) {
_, err := cs.CallTool(context.Background(), &mcp.CallToolParams{
Name: "create_project",
Arguments: map[string]any{},
})
if err == nil {
t.Fatal("CallTool(create_project) error = nil, want error")
}
rpcErr, data := requireWireError(t, err)
if rpcErr.Code != jsonrpc.CodeInvalidParams {
t.Fatalf("create_project code = %d, want %d", rpcErr.Code, jsonrpc.CodeInvalidParams)
}
if data.Type != mcperrors.TypeInvalidArguments {
t.Fatalf("create_project data.type = %q, want %q", data.Type, mcperrors.TypeInvalidArguments)
}
if data.Field != "name" {
t.Fatalf("create_project data.field = %q, want %q", data.Field, "name")
}
})
t.Run("project_required", func(t *testing.T) {
_, err := cs.CallTool(context.Background(), &mcp.CallToolParams{
Name: "get_project_context",
Arguments: map[string]any{},
})
if err == nil {
t.Fatal("CallTool(get_project_context) error = nil, want error")
}
rpcErr, data := requireWireError(t, err)
if rpcErr.Code != mcperrors.CodeProjectRequired {
t.Fatalf("get_project_context code = %d, want %d", rpcErr.Code, mcperrors.CodeProjectRequired)
}
if data.Type != mcperrors.TypeProjectRequired {
t.Fatalf("get_project_context data.type = %q, want %q", data.Type, mcperrors.TypeProjectRequired)
}
if data.Field != "project" {
t.Fatalf("get_project_context data.field = %q, want %q", data.Field, "project")
}
})
t.Run("version_info", func(t *testing.T) {
result, err := cs.CallTool(context.Background(), &mcp.CallToolParams{
Name: "get_version_info",
Arguments: map[string]any{},
})
if err != nil {
t.Fatalf("CallTool(get_version_info) error = %v", err)
}
got, ok := result.StructuredContent.(map[string]any)
if !ok {
t.Fatalf("structured content type = %T, want map[string]any", result.StructuredContent)
}
if got["server_name"] != "test" {
t.Fatalf("server_name = %#v, want %q", got["server_name"], "test")
}
if got["version"] != "0.0.1" {
t.Fatalf("version = %#v, want %q", got["version"], "0.0.1")
}
if got["tag_name"] != "v0.0.1" {
t.Fatalf("tag_name = %#v, want %q", got["tag_name"], "v0.0.1")
}
if got["build_date"] != "2026-03-31T00:00:00Z" {
t.Fatalf("build_date = %#v, want %q", got["build_date"], "2026-03-31T00:00:00Z")
}
})
t.Run("add_learning_requires_summary", func(t *testing.T) {
_, err := cs.CallTool(context.Background(), &mcp.CallToolParams{
Name: "add_learning",
Arguments: map[string]any{},
})
if err == nil {
t.Fatal("CallTool(add_learning) error = nil, want error")
}
rpcErr, data := requireWireError(t, err)
if rpcErr.Code != jsonrpc.CodeInvalidParams {
t.Fatalf("add_learning code = %d, want %d", rpcErr.Code, jsonrpc.CodeInvalidParams)
}
if data.Type != mcperrors.TypeInvalidArguments {
t.Fatalf("add_learning data.type = %q, want %q", data.Type, mcperrors.TypeInvalidArguments)
}
if data.Field != "summary" {
t.Fatalf("add_learning data.field = %q, want %q", data.Field, "summary")
}
})
t.Run("get_learning_requires_id", func(t *testing.T) {
_, err := cs.CallTool(context.Background(), &mcp.CallToolParams{
Name: "get_learning",
Arguments: map[string]any{},
})
if err == nil {
t.Fatal("CallTool(get_learning) error = nil, want error")
}
rpcErr, data := requireWireError(t, err)
if rpcErr.Code != jsonrpc.CodeInvalidParams {
t.Fatalf("get_learning code = %d, want %d", rpcErr.Code, jsonrpc.CodeInvalidParams)
}
if data.Type != mcperrors.TypeInvalidArguments {
t.Fatalf("get_learning data.type = %q, want %q", data.Type, mcperrors.TypeInvalidArguments)
}
if data.Field != "id" {
t.Fatalf("get_learning data.field = %q, want %q", data.Field, "id")
}
})
t.Run("add_learning_unconfigured_returns_structured_error", func(t *testing.T) {
_, err := cs.CallTool(context.Background(), &mcp.CallToolParams{
Name: "add_learning",
Arguments: map[string]any{
"summary": "Learning with configured check",
},
})
if err == nil {
t.Fatal("CallTool(add_learning) error = nil, want error")
}
rpcErr, data := requireWireError(t, err)
if rpcErr.Code != jsonrpc.CodeInvalidParams {
t.Fatalf("add_learning code = %d, want %d", rpcErr.Code, jsonrpc.CodeInvalidParams)
}
if data.Type != mcperrors.TypeInvalidInput {
t.Fatalf("add_learning data.type = %q, want %q", data.Type, mcperrors.TypeInvalidInput)
}
})
t.Run("list_learnings_unconfigured_returns_structured_error", func(t *testing.T) {
_, err := cs.CallTool(context.Background(), &mcp.CallToolParams{
Name: "list_learnings",
Arguments: map[string]any{},
})
if err == nil {
t.Fatal("CallTool(list_learnings) error = nil, want error")
}
rpcErr, data := requireWireError(t, err)
if rpcErr.Code != jsonrpc.CodeInvalidParams {
t.Fatalf("list_learnings code = %d, want %d", rpcErr.Code, jsonrpc.CodeInvalidParams)
}
if data.Type != mcperrors.TypeInvalidInput {
t.Fatalf("list_learnings data.type = %q, want %q", data.Type, mcperrors.TypeInvalidInput)
}
})
}
func streamableTestToolSet() ToolSet {
return ToolSet{
Version: tools.NewVersionTool("test", buildinfo.Info{Version: "0.0.1", TagName: "v0.0.1", Commit: "test", BuildDate: "2026-03-31T00:00:00Z"}),
Capture: new(tools.CaptureTool),
Search: new(tools.SearchTool),
List: new(tools.ListTool),
Stats: new(tools.StatsTool),
Get: new(tools.GetTool),
Update: new(tools.UpdateTool),
Delete: new(tools.DeleteTool),
Archive: new(tools.ArchiveTool),
Projects: new(tools.ProjectsTool),
Context: new(tools.ContextTool),
Recall: new(tools.RecallTool),
Summarize: new(tools.SummarizeTool),
Links: new(tools.LinksTool),
Files: new(tools.FilesTool),
Backfill: new(tools.BackfillTool),
Reparse: new(tools.ReparseMetadataTool),
RetryMetadata: new(tools.RetryEnrichmentTool),
//Maintenance: new(tools.MaintenanceTool),
Skills: new(tools.SkillsTool),
}
}