Add tasks port and Amazing Marvin adapter
The tasks.Provider port answers "what should I be doing?". The Marvin adapter shells out to ampy's `am --json` and parses today's open tasks, dropping done/empty-title entries. Leaf package mirroring ai. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
package tasks
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user