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)")
}
}
|