feat(hyperguild): brain write subcommand

Reads markdown from stdin, POSTs to the brain's /write endpoint with
type + slug, prints the resulting path. Pairs with 'brain query' for
shell-friendly read/write access to the brain HTTP REST API.

Tests cover success, missing args, backend error propagation, and
empty stdin (which produces an empty content payload — the brain
server's responsibility to validate).
This commit is contained in:
Mathias Bergqvist
2026-05-03 21:42:36 +02:00
parent cd5f3c0175
commit 8f9642df69
2 changed files with 87 additions and 4 deletions

View File

@@ -52,8 +52,22 @@ func runBrainQuery(ctx context.Context, args []string, _ io.Reader, stdout, stde
return nil
}
// runBrainWrite is implemented in Task 5; stub now returns an explicit error
// so the router compiles and tests for runBrainQuery can run.
func runBrainWrite(_ context.Context, _ []string, _ io.Reader, _, _ io.Writer) error {
return errors.New("brain write: not implemented (Task 5)")
func runBrainWrite(ctx context.Context, args []string, stdin io.Reader, stdout, stderr io.Writer) error {
fs := flag.NewFlagSet("brain write", flag.ContinueOnError)
fs.SetOutput(stderr)
if err := fs.Parse(args); err != nil {
return fmt.Errorf("parse flags: %w", err)
}
if fs.NArg() < 2 {
return errors.New("brain write: type and slug required (e.g. brain write knowledge my-slug)")
}
kind := fs.Arg(0)
slug := fs.Arg(1)
res, err := newBrainClient().Write(ctx, kind, slug, stdin)
if err != nil {
return err
}
fmt.Fprintln(stdout, res.Path) //nolint:errcheck
return nil
}

View File

@@ -3,6 +3,8 @@ package main
import (
"bytes"
"context"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"strings"
@@ -79,3 +81,70 @@ func TestRunBrain_UnknownSubsubcommand(t *testing.T) {
err := runBrain(context.Background(), []string{"bogus"}, strings.NewReader(""), &out, &errBuf)
assert.Error(t, err)
}
func TestRunBrainWrite_Success(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, http.MethodPost, r.Method)
assert.Equal(t, "/write", r.URL.Path)
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"path":"knowledge/test-slug.md"}`))
}))
defer srv.Close()
t.Setenv("BRAIN_URL", srv.URL)
var out, errBuf bytes.Buffer
err := runBrain(
context.Background(),
[]string{"write", "knowledge", "test-slug"},
strings.NewReader("# Test\n\nSome body content.\n"),
&out, &errBuf,
)
require.NoError(t, err)
assert.Contains(t, out.String(), "knowledge/test-slug.md")
}
func TestRunBrainWrite_MissingArgs(t *testing.T) {
var out, errBuf bytes.Buffer
err := runBrain(context.Background(), []string{"write", "knowledge"}, strings.NewReader("x"), &out, &errBuf)
assert.Error(t, err)
assert.Contains(t, err.Error(), "type and slug required")
}
func TestRunBrainWrite_BackendError(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusBadRequest)
_, _ = w.Write([]byte("invalid slug"))
}))
defer srv.Close()
t.Setenv("BRAIN_URL", srv.URL)
var out, errBuf bytes.Buffer
err := runBrain(
context.Background(),
[]string{"write", "knowledge", "bad slug"},
strings.NewReader("body"),
&out, &errBuf,
)
assert.Error(t, err)
assert.Contains(t, err.Error(), "400")
}
func TestRunBrainWrite_EmptyStdin(t *testing.T) {
gotLen := -1
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
var p struct {
Content string `json:"content"`
}
_ = json.Unmarshal(body, &p)
gotLen = len(p.Content)
_, _ = w.Write([]byte(`{"path":"x.md"}`))
}))
defer srv.Close()
t.Setenv("BRAIN_URL", srv.URL)
var out, errBuf bytes.Buffer
err := runBrain(context.Background(), []string{"write", "knowledge", "empty"}, strings.NewReader(""), &out, &errBuf)
require.NoError(t, err)
assert.Equal(t, 0, gotLen, "empty stdin should produce empty content payload")
}