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
+7 -4
View File
@@ -55,18 +55,21 @@ Flags:
- `--limit N` — max results (default 5)
- `--json` — emit the raw response envelope
### `hyperguild brain write <type> <slug>`
### `hyperguild brain write <wing> <hall> <slug>`
Reads markdown from stdin, writes a knowledge entry.
Reads markdown from stdin, writes it to `brain/wiki/<wing>/<hall>/<slug>.md`
with `wing`/`hall`/`created_at` frontmatter injected (there is no legacy
`brain/knowledge/` fallback — ADR-0012 / #79, both wing and hall are
required).
```bash
$ cat <<EOF | hyperguild brain write knowledge example-lesson
$ cat <<EOF | hyperguild brain write homelab facts example-lesson
# Example lesson
## Lesson
...
EOF
knowledge/example-lesson.md
wiki/homelab/facts/example-lesson.md
```
### `hyperguild brain pass-rate <skill>`
+6 -5
View File
@@ -60,13 +60,14 @@ func runBrainWrite(ctx context.Context, args []string, stdin io.Reader, stdout,
if err := fs.Parse(args); err != nil {
return fmt.Errorf("parse flags: %w", err)
}
if fs.NArg() < 2 {
return errors.New("type and slug required (e.g. brain write knowledge my-slug)")
if fs.NArg() < 3 {
return errors.New("wing, hall and slug required (e.g. brain write homelab facts my-slug)")
}
kind := fs.Arg(0)
slug := fs.Arg(1)
wing := fs.Arg(0)
hall := fs.Arg(1)
slug := fs.Arg(2)
res, err := newBrainClient().Write(ctx, kind, slug, stdin)
res, err := newBrainClient().Write(ctx, wing, hall, slug, stdin)
if err != nil {
return err
}
+5 -5
View File
@@ -101,7 +101,7 @@ func TestRunBrainWrite_Success(t *testing.T) {
var out, errBuf bytes.Buffer
err := runBrain(
context.Background(),
[]string{"write", "knowledge", "test-slug"},
[]string{"write", "homelab", "facts", "test-slug"},
strings.NewReader("# Test\n\nSome body content.\n"),
&out, &errBuf,
)
@@ -111,9 +111,9 @@ func TestRunBrainWrite_Success(t *testing.T) {
func TestRunBrainWrite_MissingArgs(t *testing.T) {
var out, errBuf bytes.Buffer
err := runBrain(context.Background(), []string{"write", "knowledge"}, strings.NewReader("x"), &out, &errBuf)
err := runBrain(context.Background(), []string{"write", "homelab"}, strings.NewReader("x"), &out, &errBuf)
assert.Error(t, err)
assert.Contains(t, err.Error(), "type and slug required")
assert.Contains(t, err.Error(), "wing, hall and slug required")
}
func TestRunBrainWrite_BackendError(t *testing.T) {
@@ -127,7 +127,7 @@ func TestRunBrainWrite_BackendError(t *testing.T) {
var out, errBuf bytes.Buffer
err := runBrain(
context.Background(),
[]string{"write", "knowledge", "bad slug"},
[]string{"write", "homelab", "facts", "bad slug"},
strings.NewReader("body"),
&out, &errBuf,
)
@@ -150,7 +150,7 @@ func TestRunBrainWrite_EmptyStdin(t *testing.T) {
t.Setenv("BRAIN_URL", srv.URL)
var out, errBuf bytes.Buffer
err := runBrain(context.Background(), []string{"write", "knowledge", "empty"}, strings.NewReader(""), &out, &errBuf)
err := runBrain(context.Background(), []string{"write", "homelab", "facts", "empty"}, strings.NewReader(""), &out, &errBuf)
require.NoError(t, err)
assert.Equal(t, 0, gotLen, "empty stdin should produce empty content payload")
}
+6 -5
View File
@@ -84,16 +84,17 @@ type WriteResult struct {
Path string `json:"path"`
}
func (c *brainClient) Write(ctx context.Context, kind, slug string, content io.Reader) (*WriteResult, error) {
func (c *brainClient) Write(ctx context.Context, wing, hall, filename string, content io.Reader) (*WriteResult, error) {
body, err := io.ReadAll(content)
if err != nil {
return nil, fmt.Errorf("read content: %w", err)
}
payload, err := json.Marshal(struct {
Type string `json:"type"`
Slug string `json:"slug"`
Content string `json:"content"`
}{Type: kind, Slug: slug, Content: string(body)})
Wing string `json:"wing"`
Hall string `json:"hall"`
Filename string `json:"filename"`
Content string `json:"content"`
}{Wing: wing, Hall: hall, Filename: filename, Content: string(body)})
if err != nil {
return nil, fmt.Errorf("marshal payload: %w", err)
}
+10 -8
View File
@@ -65,23 +65,25 @@ func TestBrainClient_Write_Success(t *testing.T) {
assert.Equal(t, http.MethodPost, r.Method)
body, _ := io.ReadAll(r.Body)
var got struct {
Type string `json:"type"`
Slug string `json:"slug"`
Content string `json:"content"`
Wing string `json:"wing"`
Hall string `json:"hall"`
Filename string `json:"filename"`
Content string `json:"content"`
}
require.NoError(t, json.Unmarshal(body, &got))
assert.Equal(t, "knowledge", got.Type)
assert.Equal(t, "find-h", got.Slug)
assert.Equal(t, "homelab", got.Wing)
assert.Equal(t, "facts", got.Hall)
assert.Equal(t, "find-h", got.Filename)
assert.Equal(t, "# body\n", got.Content)
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"path":"knowledge/find-h.md"}`))
_, _ = w.Write([]byte(`{"path":"wiki/homelab/facts/find-h.md"}`))
}))
defer srv.Close()
c := &brainClient{baseURL: srv.URL, http: srv.Client()}
res, err := c.Write(context.Background(), "knowledge", "find-h", strings.NewReader("# body\n"))
res, err := c.Write(context.Background(), "homelab", "facts", "find-h", strings.NewReader("# body\n"))
require.NoError(t, err)
assert.Equal(t, "knowledge/find-h.md", res.Path)
assert.Equal(t, "wiki/homelab/facts/find-h.md", res.Path)
}
func TestBrainClient_PassRate_Success(t *testing.T) {