summaryrefslogtreecommitdiff
path: root/internal/api/agentmcp_endpoint_test.go
blob: dbc742c04d4fcd23362fac5ea58bb1d4e4760690 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
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)")
	}
}