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
199 lines
7.8 KiB
Go
199 lines
7.8 KiB
Go
package capturehttp_test
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"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/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
const staticTok = "static-secret"
|
|
|
|
// fakeValidator stands in for the chassis JWT validator.
|
|
type fakeValidator struct {
|
|
subject string
|
|
err error
|
|
}
|
|
|
|
func (f fakeValidator) Validate(context.Context, string) (string, error) {
|
|
return f.subject, f.err
|
|
}
|
|
|
|
type fakeTracker struct{ failCreate bool }
|
|
|
|
func (f fakeTracker) CreateIssue(context.Context, string, string, string) (capture.IssueRef, error) {
|
|
if f.failCreate {
|
|
return capture.IssueRef{}, errors.New("gitea down")
|
|
}
|
|
return capture.IssueRef{Repo: "hyperguild", Number: 1, URL: "https://git/1"}, nil
|
|
}
|
|
func (fakeTracker) CloseIssue(context.Context, string, int, string) (capture.IssueRef, error) {
|
|
return capture.IssueRef{}, nil
|
|
}
|
|
func (fakeTracker) CommentIssue(context.Context, string, int, string) (capture.IssueRef, error) {
|
|
return capture.IssueRef{}, nil
|
|
}
|
|
|
|
func newHandler(t *testing.T, v capturehttp.Validator, tr capture.IssueTracker, sovereign []string) *capturehttp.Handler {
|
|
t.Helper()
|
|
cfg, err := classification.Load(t.TempDir())
|
|
require.NoError(t, err)
|
|
svc := capture.NewService(brainstore.New(t.TempDir()), tr, cfg, audit.NewSlogSink(nil))
|
|
return capturehttp.New(svc, v, staticTok, "local-cli", capturehttp.NewOriginResolver(sovereign))
|
|
}
|
|
|
|
func do(t *testing.T, h *capturehttp.Handler, authz string, body any) *httptest.ResponseRecorder {
|
|
t.Helper()
|
|
b, _ := json.Marshal(body)
|
|
req := httptest.NewRequest(http.MethodPost, "/capture", bytes.NewReader(b))
|
|
if authz != "" {
|
|
req.Header.Set("Authorization", authz)
|
|
}
|
|
rr := httptest.NewRecorder()
|
|
h.ServeHTTP(rr, req)
|
|
return rr
|
|
}
|
|
|
|
func internalReq() map[string]any {
|
|
return 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"}},
|
|
}
|
|
}
|
|
|
|
func TestUnauthorizedWithoutToken(t *testing.T) {
|
|
h := newHandler(t, fakeValidator{err: errors.New("no")}, fakeTracker{}, nil)
|
|
rr := do(t, h, "", internalReq())
|
|
assert.Equal(t, http.StatusUnauthorized, rr.Code)
|
|
}
|
|
|
|
func TestUnauthorizedBadToken(t *testing.T) {
|
|
h := newHandler(t, fakeValidator{err: errors.New("bad jwt")}, fakeTracker{}, nil)
|
|
rr := do(t, h, "Bearer wrong", internalReq())
|
|
assert.Equal(t, http.StatusUnauthorized, rr.Code)
|
|
}
|
|
|
|
func TestHappyPathStaticToken(t *testing.T) {
|
|
h := newHandler(t, nil, fakeTracker{}, nil)
|
|
rr := do(t, h, "Bearer "+staticTok, 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.Equal(t, http.StatusOK, rr.Code)
|
|
var rec capture.CaptureReceipt
|
|
require.NoError(t, json.Unmarshal(rr.Body.Bytes(), &rec))
|
|
assert.True(t, rec.Insights[0].OK)
|
|
assert.True(t, rec.Tickets[0].OK)
|
|
assert.Empty(t, rec.Errors)
|
|
}
|
|
|
|
func TestConfidentialViaUSNexusRefused(t *testing.T) {
|
|
// JWT principal not in the sovereign allowlist ⇒ us-nexus; confidential ⇒ 403.
|
|
h := newHandler(t, fakeValidator{subject: "claudeai-oauth-client"}, fakeTracker{}, nil)
|
|
rr := do(t, h, "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"}},
|
|
})
|
|
assert.Equal(t, http.StatusForbidden, rr.Code)
|
|
assert.Contains(t, rr.Body.String(), "sovereignty")
|
|
}
|
|
|
|
func TestConfidentialViaSovereignJWTAllowed(t *testing.T) {
|
|
// Same confidential payload, but the principal is allowlisted sovereign ⇒ allowed.
|
|
h := newHandler(t, fakeValidator{subject: "koala-cli"}, fakeTracker{}, []string{"koala-cli"})
|
|
rr := do(t, h, "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"}},
|
|
})
|
|
require.Equal(t, http.StatusOK, rr.Code)
|
|
}
|
|
|
|
func TestStaticTokenIsSovereignSoConfidentialAllowed(t *testing.T) {
|
|
h := newHandler(t, nil, fakeTracker{}, nil)
|
|
rr := do(t, h, "Bearer "+staticTok, 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.Equal(t, http.StatusOK, rr.Code)
|
|
}
|
|
|
|
func TestValidationRejectedBeforeWrite(t *testing.T) {
|
|
h := newHandler(t, nil, fakeTracker{}, nil)
|
|
rr := do(t, h, "Bearer "+staticTok, map[string]any{
|
|
"context": map[string]any{"actor": "mathias", "classification": "internal"},
|
|
"insights": []map[string]any{{"text": "x", "wing": "hyperguild", "hall": "not-a-hall"}},
|
|
})
|
|
assert.Equal(t, http.StatusBadRequest, rr.Code)
|
|
}
|
|
|
|
func TestPartialFailureIs207(t *testing.T) {
|
|
h := newHandler(t, nil, fakeTracker{failCreate: true}, nil)
|
|
rr := do(t, h, "Bearer "+staticTok, map[string]any{
|
|
"context": map[string]any{"harness": "claude-code", "actor": "mathias", "classification": "internal"},
|
|
"insights": []map[string]any{{"text": "ok insight", "wing": "hyperguild", "hall": "facts"}},
|
|
"tickets": []map[string]any{{"repo": "hyperguild", "action": "create", "title": "fails"}},
|
|
})
|
|
assert.Equal(t, http.StatusMultiStatus, rr.Code)
|
|
var rec capture.CaptureReceipt
|
|
require.NoError(t, json.Unmarshal(rr.Body.Bytes(), &rec))
|
|
assert.True(t, rec.Insights[0].OK)
|
|
assert.False(t, rec.Tickets[0].OK)
|
|
assert.Len(t, rec.Errors, 1)
|
|
}
|
|
|
|
func TestDryRunWritesNothing(t *testing.T) {
|
|
h := newHandler(t, nil, fakeTracker{}, nil)
|
|
rr := do(t, h, "Bearer "+staticTok, map[string]any{
|
|
"context": map[string]any{"harness": "claude-code", "actor": "mathias", "classification": "internal"},
|
|
"insights": []map[string]any{{"text": "a", "wing": "hyperguild", "hall": "facts"}},
|
|
"dry_run": true,
|
|
})
|
|
require.Equal(t, http.StatusOK, rr.Code)
|
|
var rec capture.CaptureReceipt
|
|
require.NoError(t, json.Unmarshal(rr.Body.Bytes(), &rec))
|
|
assert.True(t, rec.DryRun)
|
|
}
|
|
|
|
func TestCallerCannotForgeOrigin(t *testing.T) {
|
|
// Even if the body tried to assert a sovereign harness, a us-nexus JWT
|
|
// principal + confidential ⇒ refused. (Origin is server-derived.)
|
|
h := newHandler(t, fakeValidator{subject: "claudeai-oauth-client"}, fakeTracker{}, nil)
|
|
rr := do(t, h, "Bearer jwt", map[string]any{
|
|
"context": map[string]any{"harness": "sovereign-soil", "actor": "mathias", "classification": "confidential"},
|
|
"insights": []map[string]any{{"text": "secret", "wing": "client-seb", "hall": "facts"}},
|
|
})
|
|
assert.Equal(t, http.StatusForbidden, rr.Code)
|
|
}
|
|
|
|
// refusingAudit refuses at Reserve (e.g. confidential + loki down, or floor).
|
|
type refusingAudit struct{}
|
|
|
|
func (refusingAudit) Reserve(context.Context, classification.Level) (capture.AuditOutcome, error) {
|
|
return 0, errors.New("central audit sink unreachable")
|
|
}
|
|
func (refusingAudit) Record(context.Context, capture.AuditEntry, capture.AuditOutcome) error {
|
|
return nil
|
|
}
|
|
|
|
func TestAuditUnavailableIs503(t *testing.T) {
|
|
cfg, err := classification.Load(t.TempDir())
|
|
require.NoError(t, err)
|
|
svc := capture.NewService(brainstore.New(t.TempDir()), fakeTracker{}, cfg, refusingAudit{})
|
|
h := capturehttp.New(svc, nil, staticTok, "local-cli", capturehttp.NewOriginResolver(nil))
|
|
|
|
rr := do(t, h, "Bearer "+staticTok, internalReq())
|
|
assert.Equal(t, http.StatusServiceUnavailable, rr.Code)
|
|
}
|