ai-sessions#13: capture's `summary` field wrote a frontmatter shape (title/harness/fidelity/captured_at/repos_touched) that ai-sessions' own extract/audit pipeline can't parse -- silently invisible to that repo's own audit tooling, and bypassing its redaction + Stage2 completeness gates by construction. Only 5 files ever landed this way over 3 weeks; the summary capability's whole value (speed) fights ai-sessions' whole value (redacted, audited, complete), so kill it rather than build a second parse branch. capture now persists insights -> brain and action items -> Gitea tickets only. Removes Summary/SummaryResult/SummaryWriter and all wiring (service, REST body, MCP tool schema); close-session updated to stop assembling a summary payload. specs/capture-*.md kept as historical record with a superseded note -- the feature shipped and is documented, just no longer current behaviour. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WPdSHbp9Utb2hPFm9wDG59
151 lines
6.1 KiB
Go
151 lines
6.1 KiB
Go
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{}, 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")
|
|
}
|