fix: hyperguild brain write requires wing/hall, not legacy type/slug (#79)
CI / Lint / Test / Vet (push) Successful in 34s
CI / Mirror to GitHub (push) Successful in 4s

Removes brain_write legacy domain/type/knowledge-path params (ADR-0012) and migrates the previously-missed cmd/hyperguild CLI caller (hyperguild brain write) from <type> <slug> to <wing> <hall> <slug>, fixing a 100%% regression the first review round caught. Adds TestWrite_CLIPayloadRoundTrips exercising the real Handler.Write over HTTP to pin the wire contract.

Closes #79.

Co-authored-by: Mathias <mthbqv@gmail.com>
This commit was merged in pull request #131.
This commit is contained in:
2026-09-23 05:43:28 +00:00
committed by mathias
parent cdafdf6e0f
commit f324f04944
15 changed files with 188 additions and 607 deletions
+35 -29
View File
@@ -51,25 +51,50 @@ func TestBrainQueryReturnsResults(t *testing.T) {
assert.Contains(t, text, "tdd.md")
}
func TestBrainWriteCreatesFile(t *testing.T) {
// TestBrainWriteIgnoresLegacyTypeAndDomainParams pins the post-#79 contract:
// domain/type were removed from the schema and the handler's arg struct, so
// a caller that still sends them (the old MCP surface) gets a normal wing+
// hall write with no error and no effect from the stale fields — silently
// dropped by json.Unmarshal, not rejected. Only content in its OWN
// frontmatter (unrelated to these params) can set type/domain, per
// TestWriteNote_HallRouteMergesExistingFrontmatterInsteadOfStacking in the
// api package.
func TestBrainWriteIgnoresLegacyTypeAndDomainParams(t *testing.T) {
brainDir := t.TempDir()
srv := mcp.NewServer(brainDir, nil, nil, nil)
resp := toolCall(t, srv, "brain_write", map[string]any{
"content": "# Test\n\nbody",
"filename": "test.md",
"filename": "test",
"wing": "homelab",
"hall": "facts",
"type": "note",
"domain": "personal",
})
require.Nil(t, resp["error"])
got, err := os.ReadFile(filepath.Join(brainDir, "knowledge", "test.md"))
got, err := os.ReadFile(filepath.Join(brainDir, "wiki", "homelab", "facts", "test.md"))
require.NoError(t, err)
assert.Contains(t, string(got), "type: note")
assert.Contains(t, string(got), "domain: personal")
assert.NotContains(t, string(got), "type: note")
assert.NotContains(t, string(got), "domain: personal")
assert.Contains(t, string(got), "# Test")
}
// TestBrainWriteRequiresWingAndHall is the MCP-tool-call counterpart of
// api.TestWriteNote_RequiresWingAndHall: the legacy brain/knowledge/
// fallback is gone, so a caller omitting either param gets a tool error,
// not a silent legacy write.
func TestBrainWriteRequiresWingAndHall(t *testing.T) {
brainDir := t.TempDir()
srv := mcp.NewServer(brainDir, nil, nil, nil)
resp := toolCall(t, srv, "brain_write", map[string]any{
"content": "x",
"filename": "no-destination",
})
require.NotNil(t, resp["error"])
}
func TestBrainWriteWingHallRoutesToWiki(t *testing.T) {
brainDir := t.TempDir()
srv := mcp.NewServer(brainDir, nil, nil, nil)
@@ -208,30 +233,11 @@ func TestBrainTunnelRejectsMissing(t *testing.T) {
require.NotNil(t, resp["error"])
}
func TestBrainWriteRejectsTraversal(t *testing.T) {
brainDir := t.TempDir()
srv := mcp.NewServer(brainDir, nil, nil, nil)
resp := toolCall(t, srv, "brain_write", map[string]any{
"content": "x",
"filename": "../escape.md",
})
require.NotNil(t, resp["error"])
}
func TestBrainWriteAcceptsDoubleDotInName(t *testing.T) {
brainDir := t.TempDir()
srv := mcp.NewServer(brainDir, nil, nil, nil)
resp := toolCall(t, srv, "brain_write", map[string]any{
"content": "x",
"filename": "notes..draft.md",
})
require.Nil(t, resp["error"])
_, err := os.Stat(filepath.Join(brainDir, "knowledge", "notes..draft.md"))
require.NoError(t, err, "filename with embedded .. should be allowed")
}
// Filename traversal/sanitisation on the wiki route is covered directly
// against brain.NotePath/Sanitise in internal/brain/path_test.go
// (TestNotePath_SanitisesWingAndSlug, TestSanitise) — those tests exercised
// the now-deleted brain/knowledge/ legacy path's own separate filename
// validation, which had no equivalent once that path was removed (#79).
func TestBrainIngestRawDryRun(t *testing.T) {
brainDir := t.TempDir()