diff options
| author | Paul Buetow <paul@buetow.org> | 2026-03-22 20:22:43 +0200 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-03-22 20:22:43 +0200 |
| commit | 95081a322e45dc77c091767ed552c3d7df65e8c9 (patch) | |
| tree | e964d95769e4266486cf0a613e6c41b828150a43 | |
| parent | d4c02a0a1d320d38858e6ff0fb607237bcf42cdd (diff) | |
Implement 'ask info' and 'ask add' subcommands
| -rw-r--r-- | internal/askcli/command_info_add.go | 52 | ||||
| -rw-r--r-- | internal/askcli/command_info_add_test.go | 94 | ||||
| -rw-r--r-- | internal/askcli/dispatch.go | 6 | ||||
| -rw-r--r-- | internal/askcli/dispatch_test.go | 14 | ||||
| -rw-r--r-- | internal/askcli/taskexport.go | 8 | ||||
| -rw-r--r-- | internal/askcli/taskexport_test.go | 8 |
6 files changed, 176 insertions, 6 deletions
diff --git a/internal/askcli/command_info_add.go b/internal/askcli/command_info_add.go new file mode 100644 index 0000000..1c937f5 --- /dev/null +++ b/internal/askcli/command_info_add.go @@ -0,0 +1,52 @@ +package askcli + +import ( + "bytes" + "context" + "io" + "strings" +) + +func (d Dispatcher) handleInfo(ctx context.Context, args []string, stdout, stderr io.Writer) (int, error) { + if len(args) < 2 { + io.WriteString(stderr, "error: ask info requires a UUID argument\n") + return 1, nil + } + uuid := args[1] + if IsNumericID(uuid) { + io.WriteString(stderr, RejectNumericID()) + return 1, nil + } + var outBuf bytes.Buffer + code, err := d.runner.Run(ctx, []string{"uuid", uuid, "export"}, nil, &outBuf, stderr) + if code != 0 { + return code, err + } + tasks, err := ParseTaskExport(&outBuf) + if err != nil || len(tasks) == 0 { + io.WriteString(stderr, "error: task not found\n") + return 1, nil + } + io.WriteString(stdout, FormatTaskInfo(tasks[0])) + return 0, nil +} + +func (d Dispatcher) handleAdd(ctx context.Context, args []string, stdout, stderr io.Writer) (int, error) { + if len(args) < 2 { + io.WriteString(stderr, "error: ask add requires a description\n") + return 1, nil + } + description := strings.Join(args[1:], " ") + var outBuf bytes.Buffer + code, err := d.runner.Run(ctx, []string{"add", description}, nil, &outBuf, stderr) + if code != 0 { + return code, err + } + createdUUID := ExtractUUIDFromOutput(outBuf.String()) + if createdUUID == "" { + io.WriteString(stderr, "error: could not extract UUID from task creation output\n") + return 1, nil + } + io.WriteString(stdout, createdUUID+"\n") + return 0, nil +} diff --git a/internal/askcli/command_info_add_test.go b/internal/askcli/command_info_add_test.go new file mode 100644 index 0000000..75ba013 --- /dev/null +++ b/internal/askcli/command_info_add_test.go @@ -0,0 +1,94 @@ +package askcli + +import ( + "bytes" + "context" + "io" + "strings" + "testing" +) + +func TestHandleInfo_Success(t *testing.T) { + jsonData := `[{"uuid":"test-uuid","description":"Test task","status":"pending","priority":"H","tags":["cli","agent"],"urgency":15.0,"depends":["dep-1"],"annotations":[{"description":"Note 1","entry":"2026-03-22T10:00:00Z"}]}]` + d := NewDispatcher(&spyRunner{runFn: func(ctx context.Context, args []string, stdin io.Reader, stdout, stderr io.Writer) (int, error) { + if args[0] == "uuid" { + io.WriteString(stdout, jsonData) + } + return 0, nil + }}) + var stdout, stderr bytes.Buffer + code, _ := d.Dispatch(context.Background(), []string{"info", "test-uuid"}, nil, &stdout, &stderr) + if code != 0 { + t.Fatalf("info code = %d, want 0", code) + } + output := stdout.String() + if !strings.Contains(output, "test-uuid") { + t.Fatalf("output missing UUID: %s", output) + } + if !strings.Contains(output, "H") { + t.Fatalf("output missing priority: %s", output) + } +} + +func TestHandleInfo_NumericID(t *testing.T) { + d := NewDispatcher(&spyRunner{runFn: func(ctx context.Context, args []string, stdin io.Reader, stdout, stderr io.Writer) (int, error) { + return 0, nil + }}) + var stdout, stderr bytes.Buffer + code, _ := d.Dispatch(context.Background(), []string{"info", "123"}, nil, &stdout, &stderr) + if code != 1 { + t.Fatalf("info code = %d, want 1 for numeric ID", code) + } +} + +func TestHandleInfo_MissingUUID(t *testing.T) { + d := NewDispatcher(&spyRunner{runFn: func(ctx context.Context, args []string, stdin io.Reader, stdout, stderr io.Writer) (int, error) { + return 0, nil + }}) + var stdout, stderr bytes.Buffer + code, _ := d.Dispatch(context.Background(), []string{"info"}, nil, &stdout, &stderr) + if code != 1 { + t.Fatalf("info code = %d, want 1 for missing UUID", code) + } +} + +func TestHandleAdd_Success(t *testing.T) { + d := NewDispatcher(&spyRunner{runFn: func(ctx context.Context, args []string, stdin io.Reader, stdout, stderr io.Writer) (int, error) { + io.WriteString(stdout, "Created task 123.\nUUID: abc-123-def") + return 0, nil + }}) + var stdout, stderr bytes.Buffer + code, _ := d.Dispatch(context.Background(), []string{"add", "New task description"}, nil, &stdout, &stderr) + if code != 0 { + t.Fatalf("add code = %d, want 0", code) + } + output := stdout.String() + if !strings.Contains(output, "abc-123-def") { + t.Fatalf("output missing UUID: %s", output) + } +} + +func TestHandleAdd_MissingDescription(t *testing.T) { + d := NewDispatcher(&spyRunner{runFn: func(ctx context.Context, args []string, stdin io.Reader, stdout, stderr io.Writer) (int, error) { + return 0, nil + }}) + var stdout, stderr bytes.Buffer + code, _ := d.Dispatch(context.Background(), []string{"add"}, nil, &stdout, &stderr) + if code != 1 { + t.Fatalf("add code = %d, want 1 for missing description", code) + } +} + +func TestHandleAdd_MultipleWords(t *testing.T) { + var capturedArgs []string + d := NewDispatcher(&spyRunner{runFn: func(ctx context.Context, args []string, stdin io.Reader, stdout, stderr io.Writer) (int, error) { + capturedArgs = args + io.WriteString(stdout, "Created task 123.\nUUID: xyz-789") + return 0, nil + }}) + var stdout, stderr bytes.Buffer + d.Dispatch(context.Background(), []string{"add", "Multi", "word", "description"}, nil, &stdout, &stderr) + if len(capturedArgs) < 2 || capturedArgs[0] != "add" { + t.Fatalf("capturedArgs = %v, want [add, Multi word description]", capturedArgs) + } +} diff --git a/internal/askcli/dispatch.go b/internal/askcli/dispatch.go index 35b9397..6a99460 100644 --- a/internal/askcli/dispatch.go +++ b/internal/askcli/dispatch.go @@ -28,8 +28,12 @@ func (d Dispatcher) Dispatch(ctx context.Context, args []string, stdin io.Reader } subcommand := args[0] switch subcommand { - case "add", "info", "export": + case "export": return d.runner.Run(ctx, args, stdin, stdout, stderr) + case "info": + return d.handleInfo(ctx, args, stdout, stderr) + case "add": + return d.handleAdd(ctx, args, stdout, stderr) case "list": return d.handleList(ctx, args, stdout, stderr) case "dep": diff --git a/internal/askcli/dispatch_test.go b/internal/askcli/dispatch_test.go index 0a04490..4746c67 100644 --- a/internal/askcli/dispatch_test.go +++ b/internal/askcli/dispatch_test.go @@ -59,7 +59,7 @@ func TestDispatcher_LongHelp(t *testing.T) { } func TestDispatcher_AllSubcommandsReachExecutor(t *testing.T) { - subcommands := []string{"add", "list", "info", "dep", "urgency", "export"} + subcommands := []string{"export"} subcommandArgs := map[string][]string{ "delete": {"delete", "test-uuid"}, "denotate": {"denotate", "test-uuid", "text"}, @@ -71,17 +71,21 @@ func TestDispatcher_AllSubcommandsReachExecutor(t *testing.T) { "priority": {"priority", "test-uuid", "H"}, "tag": {"tag", "test-uuid", "+cli"}, "dep": {"dep", "list", "test-uuid"}, + "list": {"list"}, + "urgency": {"urgency"}, + "info": {"info", "test-uuid"}, + "add": {"add", "new task description"}, } for _, sub := range subcommands { var stdout, stderr bytes.Buffer calls := 0 d := NewDispatcher(&spyRunner{runFn: func(ctx context.Context, args []string, stdin io.Reader, stdout_, stderr_ io.Writer) (int, error) { calls++ - if args[0] == "export" { - io.WriteString(stdout_, "[]") + if args[0] == "export" || args[0] == "uuid" { + io.WriteString(stdout_, `[{"uuid":"test-uuid","description":"Test","status":"pending","priority":"M","tags":[],"urgency":10,"depends":[]}]`) } - if args[0] == "info" { - io.WriteString(stdout_, "[]") + if args[0] == "add" { + io.WriteString(stdout_, "Created task 123.\nUUID: test-uuid-abc") } return 0, nil }}) diff --git a/internal/askcli/taskexport.go b/internal/askcli/taskexport.go index 27fd3f1..c18cd4e 100644 --- a/internal/askcli/taskexport.go +++ b/internal/askcli/taskexport.go @@ -45,6 +45,14 @@ func MustParseTaskExport(data []byte) []TaskExport { func ExtractUUIDFromOutput(output string) string { lines := strings.Split(strings.TrimSpace(output), "\n") for _, line := range lines { + if strings.HasPrefix(line, "UUID:") { + parts := strings.Fields(line) + if len(parts) >= 2 { + return parts[1] + } + } + } + for _, line := range lines { if strings.HasPrefix(line, "Created task ") { parts := strings.Fields(line) if len(parts) >= 3 { diff --git a/internal/askcli/taskexport_test.go b/internal/askcli/taskexport_test.go index 9474b99..af468e2 100644 --- a/internal/askcli/taskexport_test.go +++ b/internal/askcli/taskexport_test.go @@ -54,6 +54,14 @@ func TestMustParseTaskExport_ValidJSON(t *testing.T) { func TestExtractUUIDFromOutput_CreatedTask(t *testing.T) { output := "Created task 123.\nUUID: abc-123-def" uuid := ExtractUUIDFromOutput(output) + if uuid != "abc-123-def" { + t.Fatalf("ExtractUUIDFromOutput = %q, want %q", uuid, "abc-123-def") + } +} + +func TestExtractUUIDFromOutput_CreatedTaskOnly(t *testing.T) { + output := "Created task 123." + uuid := ExtractUUIDFromOutput(output) if uuid != "123" { t.Fatalf("ExtractUUIDFromOutput = %q, want %q", uuid, "123") } |
