Files
hyperguild/ingestion/internal/mcp/audit_test.go
T
mathiasandClaude Opus 5.5 e615d6f144
CI / Lint / Test / Vet (pull_request) Successful in 45s
CI / Mirror to GitHub (pull_request) Skipped
fix(brain): provenance as a post-condition on every model-reachable write
The PR #147 review forged origin: static as a JWT principal through
three tools the first version never touched (brain_edit, brain_ingest_raw,
ingest_raw + brain_promote), and through YAML spellings a key-name match
missed (quoted, escaped, tagged, flow-mapped, merge keys, lone CR).

- api.StampProvenance removes every spelling of origin/written_by from
  the frontmatter and appends the real values. A line is dropped if
  EITHER yaml.v3 (decoding quotes, escapes, tags, flow maps) or the
  brain's line-oriented readers would read it as provenance; complex
  keys, orphan values and merge keys are dropped outright. Lone CRs in
  frontmatter become LF. Principal values that are not bare words are
  quoted.
- Applied as the last step of writeHallNote, UpdateNote, PromoteNote
  (re-attributed to the promoter), brain_edit (re-attributed to the
  editor) and both ingests (every page written or merged). REST
  /write and /promote carry no principal and stamp unknown.
- Ingest: newlines in scalar fields fold to spaces; a page type must be
  a plain lower-case name (../raw wrote outside wiki/, pre-existing).
- Audit: outcome is ok|error, never the error text, which echoed
  argument values.

Tests: every escape from the review is a table case asserting the
invariant on the file (parses as YAML, yaml.v3 and brain_get agree,
exactly the stamped lines). Plus REST /write, REST capture AuthPath,
body-line provenance in search, and quoted principals; two of the
review's surviving flips now go red. Three tests renamed so names claim
only what they assert.

Refs #145 #144

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 22:29:46 +02:00

84 lines
2.6 KiB
Go

package mcp_test
import (
"bytes"
"encoding/json"
"log/slog"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// captureToolCallLog routes slog to a buffer for fn and returns the decoded
// "mcp tool call" records.
func captureToolCallLog(t *testing.T, fn func()) []map[string]any {
t.Helper()
var buf bytes.Buffer
prev := slog.Default()
slog.SetDefault(slog.New(slog.NewJSONHandler(&buf, nil)))
t.Cleanup(func() { slog.SetDefault(prev) })
fn()
var out []map[string]any
for _, l := range strings.Split(strings.TrimSpace(buf.String()), "\n") {
var rec map[string]any
if json.Unmarshal([]byte(l), &rec) == nil && rec["msg"] == "mcp tool call" {
out = append(out, rec)
}
}
return out
}
// hyperguild#144: brain_query/brain_read left no record of who read what.
func TestToolsCallLogsOneRecordWithToolPrincipalOriginAndScope(t *testing.T) {
srv, _, _, _ := govServer(t)
recs := captureToolCallLog(t, func() {
authCall(t, srv, "Bearer "+capStaticTok, "brain_query",
map[string]any{"query": "anything", "wing": "homelab", "hall": "facts"})
})
require.Len(t, recs, 1)
r := recs[0]
assert.Equal(t, "brain_query", r["tool"])
assert.Equal(t, "local-cli", r["principal"])
assert.Equal(t, "static", r["origin"])
assert.Equal(t, "homelab", r["wing"])
assert.Equal(t, "facts", r["hall"])
assert.Equal(t, "ok", r["outcome"])
assert.Contains(t, r, "duration_ms")
}
func TestToolsCallWithoutDerivablePrincipalLogsOriginUnknown(t *testing.T) {
srv, _, _, _ := govServer(t)
recs := captureToolCallLog(t, func() {
authCall(t, srv, "", "brain_query", map[string]any{"query": "x"})
})
require.Len(t, recs, 1)
assert.Equal(t, "unknown", recs[0]["origin"])
assert.Equal(t, "", recs[0]["principal"])
}
func TestToolsCallLogsErrorOutcome(t *testing.T) {
srv, _, _, _ := govServer(t)
recs := captureToolCallLog(t, func() {
authCall(t, srv, "Bearer "+capStaticTok, "brain_query", map[string]any{})
})
require.Len(t, recs, 1)
assert.Equal(t, "error", recs[0]["outcome"])
}
// The audit line must not become a second copy of brain content.
func TestToolsCallLogOmitsContentButRecordsItsSize(t *testing.T) {
const sentinel = "SENTINEL-BRAIN-CONTENT-91c2"
srv, _, _, _ := govServer(t)
recs := captureToolCallLog(t, func() {
authCall(t, srv, "Bearer "+capStaticTok, "brain_write", map[string]any{
"content": "# t\n\n" + sentinel, "wing": "testing", "hall": "facts", "filename": "audit-sentinel",
})
})
require.Len(t, recs, 1)
raw, _ := json.Marshal(recs[0])
assert.NotContains(t, string(raw), sentinel)
assert.EqualValues(t, len("# t\n\n"+sentinel), recs[0]["content_bytes"])
}