diff --git a/cmd/hyperguild/brain.go b/cmd/hyperguild/brain.go index f224dc5..0dbf76d 100644 --- a/cmd/hyperguild/brain.go +++ b/cmd/hyperguild/brain.go @@ -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 } diff --git a/cmd/hyperguild/brain_test.go b/cmd/hyperguild/brain_test.go index 4c7f063..8cb3aa8 100644 --- a/cmd/hyperguild/brain_test.go +++ b/cmd/hyperguild/brain_test.go @@ -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") +}