diff --git a/pkg/brain/provider_test.go b/pkg/brain/provider_test.go index 0b19d99..b557bdb 100644 --- a/pkg/brain/provider_test.go +++ b/pkg/brain/provider_test.go @@ -4,22 +4,30 @@ package brain import ( "bytes" + "context" "net/http" "net/http/httptest" "testing" + "time" core "dappco.re/go/core" + "forge.lthn.ai/core/go-ws" "forge.lthn.ai/core/mcp/pkg/mcp/ide" "github.com/gin-gonic/gin" + "github.com/gorilla/websocket" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) +type bridgeCapture struct { + err error + msg ide.BridgeMessage +} + func init() { gin.SetMode(gin.TestMode) } -// setupRouter creates a gin engine with provider routes registered. func setupRouter(p *BrainProvider) *gin.Engine { r := gin.New() g := r.Group(p.BasePath()) @@ -27,7 +35,6 @@ func setupRouter(p *BrainProvider) *gin.Engine { return r } -// providerRequest performs an HTTP request against the provider router and returns the recorder. func providerRequest(t *testing.T, p *BrainProvider, method, path string, body []byte) *httptest.ResponseRecorder { t.Helper() r := setupRouter(p) @@ -43,111 +50,413 @@ func providerRequest(t *testing.T, p *BrainProvider, method, path string, body [ return w } -// --- Provider construction --- +func connectedBridge(t *testing.T) (*ide.Bridge, <-chan bridgeCapture, func()) { + t.Helper() + + captures := make(chan bridgeCapture, 4) + upgrader := websocket.Upgrader{CheckOrigin: func(*http.Request) bool { return true }} + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + conn, err := upgrader.Upgrade(w, r, nil) + if err != nil { + captures <- bridgeCapture{err: err} + return + } + defer conn.Close() + + for { + _, data, err := conn.ReadMessage() + if err != nil { + return + } + + var msg ide.BridgeMessage + if result := core.JSONUnmarshal(data, &msg); !result.OK { + parseErr, _ := result.Value.(error) + captures <- bridgeCapture{err: parseErr} + return + } + captures <- bridgeCapture{msg: msg} + } + })) + + bridge := ide.NewBridge(nil, ide.Config{ + LaravelWSURL: core.Replace(server.URL, "http://", "ws://"), + ReconnectInterval: 10 * time.Millisecond, + MaxReconnectInterval: 20 * time.Millisecond, + }) + + ctx, cancel := context.WithCancel(context.Background()) + bridge.Start(ctx) + require.Eventually(t, bridge.Connected, time.Second, 10*time.Millisecond) + + cleanup := func() { + cancel() + bridge.Shutdown() + server.Close() + } + + return bridge, captures, cleanup +} + +func providerResponseData(t *testing.T, w *httptest.ResponseRecorder) map[string]any { + t.Helper() + var response map[string]any + require.True(t, core.JSONUnmarshal(w.Body.Bytes(), &response).OK) + data, ok := response["data"].(map[string]any) + require.True(t, ok) + return data +} + +func receiveBridgeMessage(t *testing.T, captures <-chan bridgeCapture) ide.BridgeMessage { + t.Helper() + select { + case capture := <-captures: + require.NoError(t, capture.err) + return capture.msg + case <-time.After(time.Second): + t.Fatal("timed out waiting for bridge message") + return ide.BridgeMessage{} + } +} + +func providerRouteSignatures(r *gin.Engine) []string { + routes := r.Routes() + signatures := make([]string, 0, len(routes)) + for _, route := range routes { + signatures = append(signatures, core.Concat(route.Method, " ", route.Path)) + } + return signatures +} func TestProvider_NewProvider_Good(t *testing.T) { + bridge := ide.NewBridge(nil, ide.Config{}) + hub := ws.NewHub() + + p := NewProvider(bridge, hub) + + require.NotNil(t, p) + assert.Same(t, bridge, p.bridge) + assert.Same(t, hub, p.hub) +} + +func TestProvider_NewProvider_Bad_NilDependencies(t *testing.T) { p := NewProvider(nil, nil) - assert.NotNil(t, p) + + require.NotNil(t, p) assert.Nil(t, p.bridge) assert.Nil(t, p.hub) } -func TestProvider_BrainProvider_Good_Name(t *testing.T) { +func TestProvider_NewProvider_Ugly_MixedDependencies(t *testing.T) { + bridge := ide.NewBridge(nil, ide.Config{}) + + p := NewProvider(bridge, nil) + + require.NotNil(t, p) + assert.Same(t, bridge, p.bridge) + assert.Nil(t, p.hub) +} + +func TestProvider_Name_Good(t *testing.T) { assert.Equal(t, "brain", NewProvider(nil, nil).Name()) } -func TestProvider_BrainProvider_Good_BasePath(t *testing.T) { +func TestProvider_Name_Bad_ZeroValueReceiver(t *testing.T) { + assert.Equal(t, "brain", (&BrainProvider{}).Name()) +} + +func TestProvider_Name_Ugly_NilReceiver(t *testing.T) { + var p *BrainProvider + assert.Equal(t, "brain", p.Name()) +} + +func TestProvider_BasePath_Good(t *testing.T) { assert.Equal(t, "/api/brain", NewProvider(nil, nil).BasePath()) } -func TestProvider_BrainProvider_Good_Channels(t *testing.T) { +func TestProvider_BasePath_Bad_ZeroValueReceiver(t *testing.T) { + assert.Equal(t, "/api/brain", (&BrainProvider{}).BasePath()) +} + +func TestProvider_BasePath_Ugly_NilReceiver(t *testing.T) { + var p *BrainProvider + assert.Equal(t, "/api/brain", p.BasePath()) +} + +func TestProvider_Channels_Good(t *testing.T) { channels := NewProvider(nil, nil).Channels() + + assert.Equal(t, []string{ + "brain.remember.complete", + "brain.recall.complete", + "brain.forget.complete", + }, channels) +} + +func TestProvider_Channels_Bad_ZeroValueReceiver(t *testing.T) { + channels := (&BrainProvider{}).Channels() assert.Len(t, channels, 3) - assert.Contains(t, channels, "brain.remember.complete") - assert.Contains(t, channels, "brain.recall.complete") - assert.Contains(t, channels, "brain.forget.complete") } -func TestProvider_BrainProvider_Good_Element(t *testing.T) { - el := NewProvider(nil, nil).Element() - assert.Equal(t, "core-brain-panel", el.Tag) - assert.Equal(t, "/assets/brain-panel.js", el.Source) +func TestProvider_Channels_Ugly_ReturnSliceIsDetached(t *testing.T) { + channels := NewProvider(nil, nil).Channels() + channels[0] = "changed" + + assert.Equal(t, "brain.remember.complete", NewProvider(nil, nil).Channels()[0]) } -func TestProvider_BrainProvider_Good_Describe(t *testing.T) { - descs := NewProvider(nil, nil).Describe() - assert.Len(t, descs, 5) +func TestProvider_Element_Good(t *testing.T) { + element := NewProvider(nil, nil).Element() - paths := make([]string, len(descs)) - for i, d := range descs { - paths[i] = d.Method + " " + d.Path - } - assert.Contains(t, paths, "POST /remember") - assert.Contains(t, paths, "POST /recall") - assert.Contains(t, paths, "POST /forget") - assert.Contains(t, paths, "GET /list") - assert.Contains(t, paths, "GET /status") + assert.Equal(t, "core-brain-panel", element.Tag) + assert.Equal(t, "/assets/brain-panel.js", element.Source) } -// --- Handler: status --- +func TestProvider_Element_Bad_ZeroValueReceiver(t *testing.T) { + element := (&BrainProvider{}).Element() + + assert.Equal(t, "core-brain-panel", element.Tag) + assert.Equal(t, "/assets/brain-panel.js", element.Source) +} + +func TestProvider_Element_Ugly_ReturnValueIsDetached(t *testing.T) { + element := NewProvider(nil, nil).Element() + element.Tag = "changed" + + assert.Equal(t, "core-brain-panel", NewProvider(nil, nil).Element().Tag) +} + +func TestProvider_RegisterRoutes_Good(t *testing.T) { + signatures := providerRouteSignatures(setupRouter(NewProvider(nil, nil))) + + assert.ElementsMatch(t, []string{ + "POST /api/brain/remember", + "POST /api/brain/recall", + "POST /api/brain/forget", + "GET /api/brain/list", + "GET /api/brain/status", + }, signatures) +} + +func TestProvider_RegisterRoutes_Bad_ZeroValueProvider(t *testing.T) { + provider := &BrainProvider{} + + status := providerRequest(t, provider, "GET", "/api/brain/status", nil) + list := providerRequest(t, provider, "GET", "/api/brain/list", nil) + + assert.Equal(t, http.StatusOK, status.Code) + assert.Equal(t, http.StatusServiceUnavailable, list.Code) +} + +func TestProvider_RegisterRoutes_Ugly_CustomGroup(t *testing.T) { + r := gin.New() + NewProvider(nil, nil).RegisterRoutes(r.Group("/v1/brain")) + + assert.ElementsMatch(t, []string{ + "POST /v1/brain/remember", + "POST /v1/brain/recall", + "POST /v1/brain/forget", + "GET /v1/brain/list", + "GET /v1/brain/status", + }, providerRouteSignatures(r)) +} + +func TestProvider_Describe_Good(t *testing.T) { + descriptions := NewProvider(nil, nil).Describe() + + assert.Len(t, descriptions, 5) + assert.Equal(t, "POST", descriptions[0].Method) + assert.Equal(t, "/remember", descriptions[0].Path) + assert.Equal(t, "GET", descriptions[4].Method) + assert.Equal(t, "/status", descriptions[4].Path) +} + +func TestProvider_Describe_Bad_ZeroValueReceiver(t *testing.T) { + descriptions := (&BrainProvider{}).Describe() + + assert.Len(t, descriptions, 5) + assert.Equal(t, "/list", descriptions[3].Path) +} + +func TestProvider_Describe_Ugly_ReturnSliceIsDetached(t *testing.T) { + descriptions := NewProvider(nil, nil).Describe() + descriptions[0].Path = "/changed" + + assert.Equal(t, "/remember", NewProvider(nil, nil).Describe()[0].Path) +} func TestProvider_Status_Good(t *testing.T) { - p := NewProvider(nil, nil) - w := providerRequest(t, p, "GET", "/api/brain/status", nil) + bridge, _, cleanup := connectedBridge(t) + defer cleanup() + + w := providerRequest(t, NewProvider(bridge, nil), "GET", "/api/brain/status", nil) assert.Equal(t, http.StatusOK, w.Code) - var resp map[string]any - require.True(t, core.JSONUnmarshal(w.Body.Bytes(), &resp).OK) - data, _ := resp["data"].(map[string]any) - assert.Equal(t, false, data["connected"]) + assert.Equal(t, true, providerResponseData(t, w)["connected"]) } -// --- Nil bridge handlers return 503 --- +func TestProvider_Status_Bad_NoBridge(t *testing.T) { + w := providerRequest(t, NewProvider(nil, nil), "GET", "/api/brain/status", nil) -func TestProvider_RememberHandler_Bad(t *testing.T) { - body := []byte(core.JSONMarshalString(map[string]any{"content": "test memory", "type": "observation"})) - w := providerRequest(t, NewProvider(nil, nil), "POST", "/api/brain/remember", body) - assert.Equal(t, http.StatusServiceUnavailable, w.Code) + assert.Equal(t, http.StatusOK, w.Code) + assert.Equal(t, false, providerResponseData(t, w)["connected"]) } -func TestProvider_RememberHandlerInvalid_Bad(t *testing.T) { - // nil bridge returns 503 before JSON validation. - w := providerRequest(t, NewProvider(nil, nil), "POST", "/api/brain/remember", []byte("not json")) - assert.Equal(t, http.StatusServiceUnavailable, w.Code) +func TestProvider_Status_Ugly_DisconnectedBridge(t *testing.T) { + w := providerRequest(t, NewProvider(&ide.Bridge{}, nil), "GET", "/api/brain/status", nil) + + assert.Equal(t, http.StatusOK, w.Code) + assert.Equal(t, false, providerResponseData(t, w)["connected"]) } -func TestProvider_RecallHandler_Bad(t *testing.T) { - body := []byte(core.JSONMarshalString(map[string]any{"query": "test"})) - w := providerRequest(t, NewProvider(nil, nil), "POST", "/api/brain/recall", body) - assert.Equal(t, http.StatusServiceUnavailable, w.Code) +func TestProvider_Remember_Good(t *testing.T) { + bridge, captures, cleanup := connectedBridge(t) + defer cleanup() + + body := []byte(core.JSONMarshalString(map[string]any{ + "content": "Use core.Env for system paths.", + "type": "convention", + "project": "agent", + "confidence": 0.9, + "tags": []string{"ax", "paths"}, + })) + w := providerRequest(t, NewProvider(bridge, nil), "POST", "/api/brain/remember", body) + + assert.Equal(t, http.StatusOK, w.Code) + assert.Equal(t, true, providerResponseData(t, w)["success"]) + + msg := receiveBridgeMessage(t, captures) + data, ok := msg.Data.(map[string]any) + require.True(t, ok) + assert.Equal(t, "brain_remember", msg.Type) + assert.Equal(t, "Use core.Env for system paths.", data["content"]) + assert.Equal(t, "convention", data["type"]) + assert.Equal(t, "agent", data["project"]) } -func TestProvider_ForgetHandler_Bad(t *testing.T) { - body := []byte(core.JSONMarshalString(map[string]any{"id": "mem-123"})) - w := providerRequest(t, NewProvider(nil, nil), "POST", "/api/brain/forget", body) - assert.Equal(t, http.StatusServiceUnavailable, w.Code) -} - -func TestProvider_ListHandler_Bad(t *testing.T) { - w := providerRequest(t, NewProvider(nil, nil), "GET", "/api/brain/list", nil) - assert.Equal(t, http.StatusServiceUnavailable, w.Code) -} - -func TestProvider_RememberHandler_Bad_InvalidInput(t *testing.T) { +func TestProvider_Remember_Bad_InvalidInput(t *testing.T) { w := providerRequest(t, NewProvider(&ide.Bridge{}, nil), "POST", "/api/brain/remember", []byte("not json")) assert.Equal(t, http.StatusBadRequest, w.Code) } -func TestProvider_ListHandler_Bad_InvalidLimit(t *testing.T) { +func TestProvider_Remember_Ugly_DisconnectedBridge(t *testing.T) { + body := []byte(core.JSONMarshalString(map[string]any{"content": "test memory", "type": "observation"})) + w := providerRequest(t, NewProvider(&ide.Bridge{}, nil), "POST", "/api/brain/remember", body) + assert.Equal(t, http.StatusInternalServerError, w.Code) +} + +func TestProvider_Recall_Good(t *testing.T) { + bridge, captures, cleanup := connectedBridge(t) + defer cleanup() + + body := []byte(core.JSONMarshalString(map[string]any{ + "query": "workspace path helpers", + "top_k": 3, + "filter": map[string]any{ + "project": "agent", + "type": "convention", + }, + })) + w := providerRequest(t, NewProvider(bridge, nil), "POST", "/api/brain/recall", body) + + assert.Equal(t, http.StatusOK, w.Code) + data := providerResponseData(t, w) + assert.Equal(t, true, data["success"]) + assert.Equal(t, 0.0, data["count"]) + + msg := receiveBridgeMessage(t, captures) + payload, ok := msg.Data.(map[string]any) + require.True(t, ok) + filter, ok := payload["filter"].(map[string]any) + require.True(t, ok) + assert.Equal(t, "brain_recall", msg.Type) + assert.Equal(t, "workspace path helpers", payload["query"]) + assert.Equal(t, 3.0, payload["top_k"]) + assert.Equal(t, "agent", filter["project"]) + assert.Equal(t, "convention", filter["type"]) +} + +func TestProvider_Recall_Bad_InvalidInput(t *testing.T) { + w := providerRequest(t, NewProvider(&ide.Bridge{}, nil), "POST", "/api/brain/recall", []byte("not json")) + assert.Equal(t, http.StatusBadRequest, w.Code) +} + +func TestProvider_Recall_Ugly_DisconnectedBridge(t *testing.T) { + body := []byte(core.JSONMarshalString(map[string]any{"query": "test"})) + w := providerRequest(t, NewProvider(&ide.Bridge{}, nil), "POST", "/api/brain/recall", body) + assert.Equal(t, http.StatusInternalServerError, w.Code) +} + +func TestProvider_Forget_Good(t *testing.T) { + bridge, captures, cleanup := connectedBridge(t) + defer cleanup() + + body := []byte(core.JSONMarshalString(map[string]any{"id": "mem-123", "reason": "superseded"})) + w := providerRequest(t, NewProvider(bridge, nil), "POST", "/api/brain/forget", body) + + assert.Equal(t, http.StatusOK, w.Code) + assert.Equal(t, "mem-123", providerResponseData(t, w)["forgotten"]) + + msg := receiveBridgeMessage(t, captures) + data, ok := msg.Data.(map[string]any) + require.True(t, ok) + assert.Equal(t, "brain_forget", msg.Type) + assert.Equal(t, "mem-123", data["id"]) + assert.Equal(t, "superseded", data["reason"]) +} + +func TestProvider_Forget_Bad_InvalidInput(t *testing.T) { + w := providerRequest(t, NewProvider(&ide.Bridge{}, nil), "POST", "/api/brain/forget", []byte("not json")) + assert.Equal(t, http.StatusBadRequest, w.Code) +} + +func TestProvider_Forget_Ugly_DisconnectedBridge(t *testing.T) { + body := []byte(core.JSONMarshalString(map[string]any{"id": "mem-123"})) + w := providerRequest(t, NewProvider(&ide.Bridge{}, nil), "POST", "/api/brain/forget", body) + assert.Equal(t, http.StatusInternalServerError, w.Code) +} + +func TestProvider_List_Good(t *testing.T) { + bridge, captures, cleanup := connectedBridge(t) + defer cleanup() + + w := providerRequest(t, NewProvider(bridge, nil), "GET", "/api/brain/list?project=agent&type=convention&agent_id=codex&limit=2", nil) + + assert.Equal(t, http.StatusOK, w.Code) + data := providerResponseData(t, w) + assert.Equal(t, true, data["success"]) + assert.Equal(t, 0.0, data["count"]) + + msg := receiveBridgeMessage(t, captures) + payload, ok := msg.Data.(map[string]any) + require.True(t, ok) + assert.Equal(t, "brain_list", msg.Type) + assert.Equal(t, "agent", payload["project"]) + assert.Equal(t, "convention", payload["type"]) + assert.Equal(t, "codex", payload["agent_id"]) + assert.Equal(t, 2.0, payload["limit"]) +} + +func TestProvider_List_Bad_InvalidLimit(t *testing.T) { w := providerRequest(t, NewProvider(&ide.Bridge{}, nil), "GET", "/api/brain/list?limit=abc", nil) assert.Equal(t, http.StatusBadRequest, w.Code) } -// --- emitEvent --- +func TestProvider_List_Ugly_DisconnectedBridge(t *testing.T) { + w := providerRequest(t, NewProvider(&ide.Bridge{}, nil), "GET", "/api/brain/list?limit=2", nil) + assert.Equal(t, http.StatusInternalServerError, w.Code) +} func TestProvider_EmitEvent_Good(t *testing.T) { - p := NewProvider(nil, nil) - p.emitEvent("brain.test", map[string]any{"foo": "bar"}) + assert.NotPanics(t, func() { + NewProvider(nil, ws.NewHub()).emitEvent("brain.test", map[string]any{"foo": "bar"}) + }) +} + +func TestProvider_EmitEvent_Bad_EmptyChannel(t *testing.T) { + assert.NotPanics(t, func() { + NewProvider(nil, ws.NewHub()).emitEvent("", map[string]any{"foo": "bar"}) + }) } func TestProvider_EmitEvent_Ugly_NilHub(t *testing.T) { diff --git a/pkg/runner/paths_example_test.go b/pkg/runner/paths_example_test.go index 97eb9b5..7bc4ab5 100644 --- a/pkg/runner/paths_example_test.go +++ b/pkg/runner/paths_example_test.go @@ -18,6 +18,25 @@ func ExampleWorkspaceRoot() { // Output: true } +func ExampleReadStatus() { + fsys := (&core.Fs{}).NewUnrestricted() + dir := fsys.TempDir("runner-paths-read") + defer fsys.DeleteAll(dir) + + WriteStatus(dir, &WorkspaceStatus{ + Status: "completed", + Agent: "codex", + Repo: "go-io", + }) + + st, err := ReadStatus(dir) + core.Println(err == nil) + core.Println(st.Repo) + // Output: + // true + // go-io +} + func ExampleWriteStatus() { fsys := (&core.Fs{}).NewUnrestricted() dir := fsys.TempDir("runner-paths") diff --git a/pkg/runner/paths_test.go b/pkg/runner/paths_test.go index b8951cd..9534092 100644 --- a/pkg/runner/paths_test.go +++ b/pkg/runner/paths_test.go @@ -12,18 +12,18 @@ import ( "github.com/stretchr/testify/require" ) -func TestPaths_CoreRoot_Good_EnvVar(t *testing.T) { +func TestPaths_CoreRoot_Good(t *testing.T) { t.Setenv("CORE_WORKSPACE", "/tmp/core-root") assert.Equal(t, "/tmp/core-root", CoreRoot()) } -func TestPaths_CoreRoot_Bad_Fallback(t *testing.T) { +func TestPaths_CoreRoot_Bad(t *testing.T) { t.Setenv("CORE_WORKSPACE", "") home := core.Env("DIR_HOME") assert.Equal(t, home+"/Code/.core", CoreRoot()) } -func TestPaths_CoreRoot_Ugly_UnicodePath(t *testing.T) { +func TestPaths_CoreRoot_Ugly(t *testing.T) { t.Setenv("CORE_WORKSPACE", "/tmp/core-røot") assert.Equal(t, "/tmp/core-røot", CoreRoot()) } @@ -33,18 +33,18 @@ func TestPaths_WorkspaceRoot_Good(t *testing.T) { assert.Equal(t, "/tmp/core-root/workspace", WorkspaceRoot()) } -func TestPaths_WorkspaceRoot_Bad_EmptyEnv(t *testing.T) { +func TestPaths_WorkspaceRoot_Bad(t *testing.T) { t.Setenv("CORE_WORKSPACE", "") home := core.Env("DIR_HOME") assert.Equal(t, home+"/Code/.core/workspace", WorkspaceRoot()) } -func TestPaths_WorkspaceRoot_Ugly_NestedCoreRoot(t *testing.T) { +func TestPaths_WorkspaceRoot_Ugly(t *testing.T) { t.Setenv("CORE_WORKSPACE", "/srv/core/tenant-a") assert.Equal(t, "/srv/core/tenant-a/workspace", WorkspaceRoot()) } -func TestPaths_ReadStatus_Good_AgenticShape(t *testing.T) { +func TestPaths_ReadStatus_Good(t *testing.T) { wsDir := t.TempDir() status := &agentic.WorkspaceStatus{ Status: "completed", @@ -71,7 +71,7 @@ func TestPaths_ReadStatus_Good_AgenticShape(t *testing.T) { assert.Equal(t, 2, st.Runs) } -func TestPaths_ReadStatus_Bad_InvalidJSON(t *testing.T) { +func TestPaths_ReadStatus_Bad(t *testing.T) { wsDir := t.TempDir() require.True(t, agentic.LocalFs().WriteAtomic(agentic.WorkspaceStatusPath(wsDir), "{not-json").OK) @@ -79,7 +79,45 @@ func TestPaths_ReadStatus_Bad_InvalidJSON(t *testing.T) { assert.Error(t, err) } -func TestPaths_WriteStatus_Ugly_AtomicOverwrite(t *testing.T) { +func TestPaths_ReadStatus_Ugly(t *testing.T) { + _, err := ReadStatus(t.TempDir()) + assert.Error(t, err) +} + +func TestPaths_WriteStatus_Good(t *testing.T) { + wsDir := t.TempDir() + + WriteStatus(wsDir, &WorkspaceStatus{ + Status: "running", + Agent: "codex", + Repo: "go-io", + Task: "Track workspace", + Branch: "agent/ax-cleanup", + Runs: 1, + }) + + st, err := ReadStatus(wsDir) + require.NoError(t, err) + assert.Equal(t, "running", st.Status) + assert.Equal(t, "codex", st.Agent) + assert.Equal(t, "go-io", st.Repo) + assert.Equal(t, "agent/ax-cleanup", st.Branch) + assert.Equal(t, 1, st.Runs) + + agenticStatus, err := agentic.ReadStatus(wsDir) + require.NoError(t, err) + assert.False(t, agenticStatus.UpdatedAt.IsZero()) +} + +func TestPaths_WriteStatus_Bad(t *testing.T) { + wsDir := t.TempDir() + + WriteStatus(wsDir, nil) + + assert.False(t, agentic.LocalFs().Read(agentic.WorkspaceStatusPath(wsDir)).OK) +} + +func TestPaths_WriteStatus_Ugly(t *testing.T) { wsDir := t.TempDir() WriteStatus(wsDir, &WorkspaceStatus{