package api import ( "context" "log/slog" "net/http" "net/http/httptest" "os" "path/filepath" "testing" "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/thepeterstone/claudomator/internal/executor" "github.com/thepeterstone/claudomator/internal/role" "github.com/thepeterstone/claudomator/internal/storage" ) type fakeAgentChannel struct{ progress []string } func (f *fakeAgentChannel) AskUser(context.Context, string) (string, error) { return "", executor.ErrAgentBlocked } func (f *fakeAgentChannel) ReportSummary(context.Context, string) error { return nil } func (f *fakeAgentChannel) SpawnSubtask(context.Context, executor.SubtaskSpec) (string, error) { return "sub", nil } func (f *fakeAgentChannel) RecordProgress(_ context.Context, m string) error { f.progress = append(f.progress, m) return nil } func (f *fakeAgentChannel) ProposeEpic(context.Context, executor.EpicProposal) (string, error) { return "epic-1", nil } func (f *fakeAgentChannel) ProposeRoleConfig(context.Context, role.RoleConfig) (int, error) { return 1, nil } func (f *fakeAgentChannel) ReportVerdict(context.Context, bool, string) error { return nil } type tokenRT struct { token string base http.RoundTripper } func (b tokenRT) RoundTrip(r *http.Request) (*http.Response, error) { r = r.Clone(r.Context()) r.Header.Set("Authorization", "Bearer "+b.token) return b.base.RoundTrip(r) } func TestServer_MountsAgentMCPEndpoint(t *testing.T) { store, err := storage.Open(filepath.Join(t.TempDir(), "test.db")) if err != nil { t.Fatal(err) } defer store.Close() logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError})) pool := executor.NewPool(1, map[string]executor.Runner{}, store, logger) reg := executor.NewRegistry() fc := &fakeAgentChannel{} token, _ := reg.Mint(fc) srv := NewServer(store, pool, reg, logger, "claude") ts := httptest.NewServer(srv.Handler()) defer ts.Close() client := mcp.NewClient(&mcp.Implementation{Name: "test", Version: "1"}, nil) cs, err := client.Connect(context.Background(), &mcp.StreamableClientTransport{ Endpoint: ts.URL + "/mcp", HTTPClient: &http.Client{Transport: tokenRT{token: token, base: http.DefaultTransport}}, }, nil) if err != nil { t.Fatalf("connect to mounted /mcp: %v", err) } defer cs.Close() if _, err := cs.CallTool(context.Background(), &mcp.CallToolParams{ Name: "record_progress", Arguments: map[string]any{"message": "via api server"}, }); err != nil { t.Fatalf("CallTool through api server: %v", err) } if len(fc.progress) != 1 || fc.progress[0] != "via api server" { t.Errorf("tool call did not reach channel: %+v", fc.progress) } } func TestServer_NoRegistry_NoMCPRoute(t *testing.T) { store, err := storage.Open(filepath.Join(t.TempDir(), "test.db")) if err != nil { t.Fatal(err) } defer store.Close() logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError})) pool := executor.NewPool(1, map[string]executor.Runner{}, store, logger) // nil registry → /mcp must not be mounted. srv := NewServer(store, pool, nil, logger, "claude") ts := httptest.NewServer(srv.Handler()) defer ts.Close() resp, err := http.Post(ts.URL+"/mcp", "application/json", http.NoBody) if err != nil { t.Fatal(err) } defer resp.Body.Close() // Unmounted: the catch-all "GET /" route rejects POST with 405. A mounted // MCP handler would instead respond (e.g. 400 for the missing token). if resp.StatusCode == http.StatusBadRequest { t.Errorf("expected /mcp to be absent without a registry, got 400 (handler ran)") } }