summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-03-22 20:22:43 +0200
committerPaul Buetow <paul@buetow.org>2026-03-22 20:22:43 +0200
commit95081a322e45dc77c091767ed552c3d7df65e8c9 (patch)
treee964d95769e4266486cf0a613e6c41b828150a43
parentd4c02a0a1d320d38858e6ff0fb607237bcf42cdd (diff)
Implement 'ask info' and 'ask add' subcommands
-rw-r--r--internal/askcli/command_info_add.go52
-rw-r--r--internal/askcli/command_info_add_test.go94
-rw-r--r--internal/askcli/dispatch.go6
-rw-r--r--internal/askcli/dispatch_test.go14
-rw-r--r--internal/askcli/taskexport.go8
-rw-r--r--internal/askcli/taskexport_test.go8
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")
}