Files
antidrift/internal/tasks/marvin_test.go
T
felixm 7d69a1f320 Generalize the focus controller into a harness hosting swappable modes
Loosen AntiDrift's session controller into Keel's general collect→brain→act
loop. A new internal/harness runs at most one mode.Mode at a time, fanning
async completions out to the web SSE and status-bar surfaces.

- internal/mode: the Mode contract (Kind/Command/View/Active) plus optional
  EvidenceConsumer and Expirer ports, and the surfacing Envelope.
- internal/mode/focus: the former session/domain/statemachine packages moved
  under the mode, now satisfying the harness contracts unchanged.
- internal/mode/offscreen: a one-shot away-from-desk mode built on the new
  ai.Proposer, which turns a life-domain brief into one off-screen action.
- cmd/keeld replaces cmd/antidriftd; daemon wires focus + offscreen factories
  with per-mode persistence under ~/.keel/modes/<kind>.
- Finish the rename: KEEL_* env refs in the README, /keeld build artifact
  ignored, stale antidriftd binary removed.

Include the design + implementation plan this refactor was built from under
docs/superpowers/{specs,plans}/2026-06-04-controller-refactor*.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 18:00:49 -04:00

142 lines
4.0 KiB
Go

package tasks
import (
"context"
"errors"
"strings"
"testing"
)
func TestParse(t *testing.T) {
cases := []struct {
name string
in string
want []Task
wantErr bool
}{
{
name: "valid array",
in: `[{"id":"a","title":"Write spec","parentId":"p","day":"2026-05-31","done":false}]`,
want: []Task{{ID: "a", Title: "Write spec", Day: "2026-05-31"}},
},
{name: "empty array", in: `[]`, want: []Task{}},
{name: "json null", in: `null`, want: []Task{}},
{
name: "drops done and empty-title",
in: `[{"id":"a","title":"keep","day":"","done":false},{"id":"b","title":"done one","done":true},{"id":"c","title":" ","done":false}]`,
want: []Task{{ID: "a", Title: "keep", Day: ""}},
},
{name: "malformed", in: `not json`, wantErr: true},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got, err := parse([]byte(tc.in))
if tc.wantErr {
if err == nil {
t.Fatal("want error, got nil")
}
return
}
if err != nil {
t.Fatalf("parse: %v", err)
}
if len(got) != len(tc.want) {
t.Fatalf("len = %d, want %d (%+v)", len(got), len(tc.want), got)
}
for i := range got {
if got[i] != tc.want[i] {
t.Errorf("[%d] = %+v, want %+v", i, got[i], tc.want[i])
}
}
})
}
}
func TestNewMarvinCommandSplitting(t *testing.T) {
cases := []struct {
in string
wantCmd string
wantArgs []string
}{
{"", "am", []string{"--json"}},
{"am", "am", []string{"--json"}},
{"uv run am", "uv", []string{"run", "am", "--json"}},
}
for _, tc := range cases {
m := NewMarvin(tc.in)
if m.cmd != tc.wantCmd {
t.Errorf("NewMarvin(%q).cmd = %q, want %q", tc.in, m.cmd, tc.wantCmd)
}
if len(m.args) != len(tc.wantArgs) {
t.Fatalf("NewMarvin(%q).args = %v, want %v", tc.in, m.args, tc.wantArgs)
}
for i := range m.args {
if m.args[i] != tc.wantArgs[i] {
t.Errorf("NewMarvin(%q).args[%d] = %q, want %q", tc.in, i, m.args[i], tc.wantArgs[i])
}
}
}
}
func TestTodayUsesRunnerOutput(t *testing.T) {
m := NewMarvin("am")
var gotName string
var gotArgs []string
m.run = func(ctx context.Context, name string, args ...string) ([]byte, error) {
gotName, gotArgs = name, args
return []byte(`[{"id":"x","title":"Do thing","day":"2026-05-31","done":false}]`), nil
}
got, err := m.Today(context.Background())
if err != nil {
t.Fatalf("Today: %v", err)
}
if gotName != "am" || len(gotArgs) != 1 || gotArgs[0] != "--json" {
t.Fatalf("runner called with %q %v", gotName, gotArgs)
}
if len(got) != 1 || got[0].Title != "Do thing" {
t.Fatalf("Today result = %+v", got)
}
}
func TestTodayPropagatesError(t *testing.T) {
m := NewMarvin("am")
m.run = func(ctx context.Context, name string, args ...string) ([]byte, error) {
return nil, errors.New("am not found")
}
if _, err := m.Today(context.Background()); err == nil {
t.Fatal("want error from failing runner")
}
}
func TestCreateInvokesMarvinAddPreservingCRDT(t *testing.T) {
m := NewMarvin("uv run am")
var gotName string
var gotArgs []string
m.run = func(ctx context.Context, name string, args ...string) ([]byte, error) {
gotName, gotArgs = name, args
return []byte(`{"id":"task-1"}`), nil
}
if err := m.Create(context.Background(), NewTask("buy groceries")); err != nil {
t.Fatalf("Create: %v", err)
}
// Same prefix the read path shells out through, so add goes through the
// same ampy entrypoint and preserves its CRDT bookkeeping.
if gotName != "uv" {
t.Fatalf("runner name = %q, want %q", gotName, "uv")
}
joined := strings.Join(gotArgs, " ")
if !strings.Contains(joined, "add") || !strings.Contains(joined, "buy groceries") {
t.Fatalf("args = %v, want marvin add with title", gotArgs)
}
}
func TestCreatePropagatesError(t *testing.T) {
m := NewMarvin("am")
m.run = func(ctx context.Context, name string, args ...string) ([]byte, error) {
return nil, errors.New("am add failed")
}
if err := m.Create(context.Background(), NewTask("buy groceries")); err == nil {
t.Fatal("want error from failing runner")
}
}