package mcp_test import ( "bytes" "context" "encoding/json" "errors" "net/http" "net/http/httptest" "os" "path/filepath" "testing" "github.com/mathiasbq/hyperguild/ingestion/internal/audit" "github.com/mathiasbq/hyperguild/ingestion/internal/brainstore" "github.com/mathiasbq/hyperguild/ingestion/internal/capture" "github.com/mathiasbq/hyperguild/ingestion/internal/capturehttp" "github.com/mathiasbq/hyperguild/ingestion/internal/classification" "github.com/mathiasbq/hyperguild/ingestion/internal/mcp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) const capStaticTok = "cap-static-tok" type capFakeTracker struct{} func (capFakeTracker) CreateIssue(context.Context, string, string, string) (capture.IssueRef, error) { return capture.IssueRef{Repo: "hyperguild", Number: 1, URL: "https://git/1"}, nil } func (capFakeTracker) CloseIssue(context.Context, string, int, string) (capture.IssueRef, error) { return capture.IssueRef{}, nil } func (capFakeTracker) CommentIssue(context.Context, string, int, string) (capture.IssueRef, error) { return capture.IssueRef{}, nil } type capFakeValidator struct { subject string err error } func (v capFakeValidator) Validate(context.Context, string) (string, error) { return v.subject, v.err } func captureServer(t *testing.T, validator capturehttp.Validator, sovereign []string) (*mcp.Server, string) { t.Helper() brainDir := t.TempDir() cfg, err := classification.Load(brainDir) require.NoError(t, err) svc := capture.NewService(brainstore.New(brainDir), capFakeTracker{}, nil, cfg, audit.NewSlogSink(nil)) srv := mcp.NewServer(brainDir, nil, nil, nil) srv.WithCapture(svc, validator, capStaticTok, "local-cli", capturehttp.NewOriginResolver(sovereign)) return srv, brainDir } func captureCall(t *testing.T, srv http.Handler, authz string, args map[string]any) map[string]any { t.Helper() body, _ := json.Marshal(map[string]any{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": map[string]any{"name": "capture", "arguments": args}, }) req := httptest.NewRequest(http.MethodPost, "/mcp", bytes.NewReader(body)) if authz != "" { req.Header.Set("Authorization", authz) } rr := httptest.NewRecorder() srv.ServeHTTP(rr, req) var resp map[string]any require.NoError(t, json.Unmarshal(rr.Body.Bytes(), &resp)) return resp } func TestCaptureToolListedWhenWired(t *testing.T) { srv, _ := captureServer(t, nil, nil) body, _ := json.Marshal(map[string]any{"jsonrpc": "2.0", "id": 1, "method": "tools/list"}) req := httptest.NewRequest(http.MethodPost, "/mcp", bytes.NewReader(body)) rr := httptest.NewRecorder() srv.ServeHTTP(rr, req) assert.Contains(t, rr.Body.String(), `"capture"`) } func TestCaptureToolNotListedByDefault(t *testing.T) { srv := mcp.NewServer(t.TempDir(), nil, nil, nil) // no WithCapture body, _ := json.Marshal(map[string]any{"jsonrpc": "2.0", "id": 1, "method": "tools/list"}) req := httptest.NewRequest(http.MethodPost, "/mcp", bytes.NewReader(body)) rr := httptest.NewRecorder() srv.ServeHTTP(rr, req) assert.NotContains(t, rr.Body.String(), `"capture"`) } func TestCaptureToolForwardsViaStaticPrincipal(t *testing.T) { srv, brainDir := captureServer(t, nil, nil) resp := captureCall(t, srv, "Bearer "+capStaticTok, map[string]any{ "context": map[string]any{"harness": "claude-code", "actor": "mathias", "classification": "internal"}, "insights": []map[string]any{{"text": "a fact", "wing": "hyperguild", "hall": "facts"}}, "tickets": []map[string]any{{"repo": "hyperguild", "action": "create", "title": "t"}}, }) require.Nil(t, resp["error"], "got error: %v", resp["error"]) text := resp["result"].(map[string]any)["content"].([]any)[0].(map[string]any)["text"].(string) var rec capture.CaptureReceipt require.NoError(t, json.Unmarshal([]byte(text), &rec)) assert.True(t, rec.Insights[0].OK) assert.True(t, rec.Tickets[0].OK) // Forwarded to the real brain store. _, statErr := os.Stat(filepath.Join(brainDir, "wiki/hyperguild/facts")) require.NoError(t, statErr) } func TestCaptureToolRefusesConfidentialViaUSNexus(t *testing.T) { // JWT principal not in the sovereign allowlist ⇒ us-nexus origin. srv, _ := captureServer(t, capFakeValidator{subject: "claudeai-oauth"}, nil) resp := captureCall(t, srv, "Bearer jwt-token", map[string]any{ "context": map[string]any{"harness": "claudeai-chat", "actor": "mathias", "classification": "confidential"}, "insights": []map[string]any{{"text": "secret", "wing": "client-seb", "hall": "facts"}}, }) require.NotNil(t, resp["error"]) assert.Contains(t, resp["error"].(map[string]any)["message"].(string), "sovereignty") } func TestCaptureToolAllowsConfidentialViaSovereignJWT(t *testing.T) { srv, _ := captureServer(t, capFakeValidator{subject: "koala-cli"}, []string{"koala-cli"}) resp := captureCall(t, srv, "Bearer jwt-token", map[string]any{ "context": map[string]any{"harness": "claude-code", "actor": "mathias", "classification": "confidential"}, "insights": []map[string]any{{"text": "secret", "wing": "client-seb", "hall": "facts"}}, }) assert.Nil(t, resp["error"], "sovereign JWT principal should be allowed: %v", resp["error"]) } func TestCaptureToolRejectsUnauthenticated(t *testing.T) { srv, _ := captureServer(t, capFakeValidator{err: errors.New("no jwt")}, nil) resp := captureCall(t, srv, "", map[string]any{ // no Authorization "context": map[string]any{"harness": "x", "classification": "internal"}, "insights": []map[string]any{{"text": "a", "wing": "hyperguild", "hall": "facts"}}, }) require.NotNil(t, resp["error"]) assert.Contains(t, resp["error"].(map[string]any)["message"].(string), "authenticated principal") } func TestCaptureToolCallerCannotForgeOrigin(t *testing.T) { // Body asserts sovereign harness, but the us-nexus JWT principal governs. srv, _ := captureServer(t, capFakeValidator{subject: "claudeai-oauth"}, nil) resp := captureCall(t, srv, "Bearer jwt", map[string]any{ "context": map[string]any{"harness": "sovereign-soil", "classification": "confidential"}, "insights": []map[string]any{{"text": "secret", "wing": "client-seb", "hall": "facts"}}, }) require.NotNil(t, resp["error"]) assert.Contains(t, resp["error"].(map[string]any)["message"].(string), "sovereignty") }