diff options
| author | Paul Buetow <paul@buetow.org> | 2026-03-23 07:41:07 +0200 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-03-23 07:41:07 +0200 |
| commit | 77a38b42f47e8842e5c60673f9b25e3871cf8d8e (patch) | |
| tree | e886cb0e4cd333c62f1fe9ab00661bacf719e611 /internal | |
| parent | 987c08b9bd86f4e6afabfb3dcb0efaf98f1ccb38 (diff) | |
ask add: always emit UUID, never the numeric task ID
Use rc.verbose=new-uuid so taskwarrior prints "Created task <uuid>."
directly on stdout. Parse the UUID from that line instead of doing
a two-step numeric-ID lookup or falling back to an export call.
Removes ExtractUUIDFromOutput (which could leak numeric IDs) and
fetchUUIDByNumericID (the export fallback). Integration tests now
get the UUID straight from ask add output without any extra calls.
Also fixes TestMain_WiresDispatcher which expected "export" first
in args, but list now prepends status:pending filter.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/askcli/command_info_add.go | 26 | ||||
| -rw-r--r-- | internal/askcli/command_info_add_test.go | 79 | ||||
| -rw-r--r-- | internal/askcli/taskexport.go | 34 | ||||
| -rw-r--r-- | internal/askcli/taskexport_test.go | 39 |
4 files changed, 70 insertions, 108 deletions
diff --git a/internal/askcli/command_info_add.go b/internal/askcli/command_info_add.go index 477ad62..5b76b2b 100644 --- a/internal/askcli/command_info_add.go +++ b/internal/askcli/command_info_add.go @@ -38,22 +38,38 @@ func (d Dispatcher) handleAdd(ctx context.Context, args []string, stdout, stderr } modifiers, description := parseAddArgs(args[1:]) var outBuf bytes.Buffer - taskArgs := []string{"add"} + // rc.verbose=new-uuid instructs taskwarrior to emit "Created task <uuid>." + // so we get the UUID directly from the add output without a follow-up export. + taskArgs := []string{"add", "rc.verbose=new-uuid"} taskArgs = append(taskArgs, modifiers...) taskArgs = append(taskArgs, description) code, err := d.runner.Run(ctx, taskArgs, 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") + uuid := extractUUIDFromAddOutput(outBuf.String()) + if uuid == "" { + io.WriteString(stderr, "error: could not parse UUID from task creation output\n") return 1, nil } - io.WriteString(stdout, createdUUID+"\n") + io.WriteString(stdout, uuid+"\n") return 0, nil } +// extractUUIDFromAddOutput parses the UUID from taskwarrior's +// "Created task <uuid>." output (produced when rc.verbose=new-uuid is set). +func extractUUIDFromAddOutput(output string) string { + for _, line := range strings.Split(strings.TrimSpace(output), "\n") { + if strings.HasPrefix(line, "Created task ") { + parts := strings.Fields(line) + if len(parts) >= 3 { + return strings.TrimSuffix(parts[2], ".") + } + } + } + return "" +} + func parseAddArgs(args []string) (modifiers []string, description string) { for i, arg := range args { if strings.HasPrefix(arg, "priority:") || strings.HasPrefix(arg, "+") || strings.HasPrefix(arg, "-") { diff --git a/internal/askcli/command_info_add_test.go b/internal/askcli/command_info_add_test.go index 47cd790..b809097 100644 --- a/internal/askcli/command_info_add_test.go +++ b/internal/askcli/command_info_add_test.go @@ -53,8 +53,9 @@ func TestHandleInfo_MissingUUID(t *testing.T) { } func TestHandleAdd_Success(t *testing.T) { + // With rc.verbose=new-uuid, task add outputs "Created task <uuid>." directly. 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") + io.WriteString(stdout, "Created task abc-123-def.") return 0, nil }}) var stdout, stderr bytes.Buffer @@ -62,9 +63,8 @@ func TestHandleAdd_Success(t *testing.T) { 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) + if !strings.Contains(stdout.String(), "abc-123-def") { + t.Fatalf("output missing UUID: %s", stdout.String()) } } @@ -79,37 +79,44 @@ func TestHandleAdd_MissingDescription(t *testing.T) { } } +func makeAddRunner(onAdd func(args []string, stdout io.Writer)) *spyRunner { + return &spyRunner{runFn: func(ctx context.Context, args []string, stdin io.Reader, stdout, stderr io.Writer) (int, error) { + onAdd(args, stdout) + return 0, nil + }} +} + 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) { + d := NewDispatcher(makeAddRunner(func(args []string, stdout io.Writer) { capturedArgs = args - io.WriteString(stdout, "Created task 123.\nUUID: xyz-789") - return 0, nil - }}) + io.WriteString(stdout, "Created task test-uuid.") + })) 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) + // args[0]="add", args[1]="rc.verbose=new-uuid", then description + if len(capturedArgs) < 3 || capturedArgs[0] != "add" || capturedArgs[1] != "rc.verbose=new-uuid" { + t.Fatalf("capturedArgs = %v, want [add, rc.verbose=new-uuid, ...]", capturedArgs) } } func TestHandleAdd_WithPriority(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) { + d := NewDispatcher(makeAddRunner(func(args []string, stdout io.Writer) { capturedArgs = args - io.WriteString(stdout, "Created task 123.\nUUID: uuid-priority") - return 0, nil - }}) + io.WriteString(stdout, "Created task test-uuid.") + })) var stdout, stderr bytes.Buffer code, _ := d.Dispatch(context.Background(), []string{"add", "priority:H", "Fix critical bug"}, nil, &stdout, &stderr) if code != 0 { t.Fatalf("add code = %d, want 0", code) } - if len(capturedArgs) < 3 { - t.Fatalf("capturedArgs = %v, want at least [add, priority:H, Fix critical bug]", capturedArgs) + // args: [add, rc.verbose=new-uuid, priority:H, Fix critical bug] + if len(capturedArgs) < 4 { + t.Fatalf("capturedArgs = %v, want at least 4 elements", capturedArgs) } - if capturedArgs[1] != "priority:H" { - t.Errorf("capturedArgs[1] = %s, want priority:H", capturedArgs[1]) + if capturedArgs[2] != "priority:H" { + t.Errorf("capturedArgs[2] = %s, want priority:H", capturedArgs[2]) } if capturedArgs[len(capturedArgs)-1] != "Fix critical bug" { t.Errorf("last arg = %s, want 'Fix critical bug'", capturedArgs[len(capturedArgs)-1]) @@ -118,35 +125,47 @@ func TestHandleAdd_WithPriority(t *testing.T) { func TestHandleAdd_WithTag(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) { + d := NewDispatcher(makeAddRunner(func(args []string, stdout io.Writer) { capturedArgs = args - io.WriteString(stdout, "Created task 123.\nUUID: uuid-tag") - return 0, nil - }}) + io.WriteString(stdout, "Created task test-uuid.") + })) var stdout, stderr bytes.Buffer code, _ := d.Dispatch(context.Background(), []string{"add", "+cli", "New feature"}, nil, &stdout, &stderr) if code != 0 { t.Fatalf("add code = %d, want 0", code) } - if capturedArgs[1] != "+cli" { - t.Errorf("capturedArgs[1] = %s, want +cli", capturedArgs[1]) + // args: [add, rc.verbose=new-uuid, +cli, New feature] + if capturedArgs[2] != "+cli" { + t.Errorf("capturedArgs[2] = %s, want +cli", capturedArgs[2]) } } func TestHandleAdd_WithPriorityAndTag(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) { + d := NewDispatcher(makeAddRunner(func(args []string, stdout io.Writer) { capturedArgs = args - io.WriteString(stdout, "Created task 123.\nUUID: uuid-combined") - return 0, nil - }}) + io.WriteString(stdout, "Created task test-uuid.") + })) var stdout, stderr bytes.Buffer code, _ := d.Dispatch(context.Background(), []string{"add", "priority:H", "+cli", "Complex task"}, nil, &stdout, &stderr) if code != 0 { t.Fatalf("add code = %d, want 0", code) } - if capturedArgs[1] != "priority:H" || capturedArgs[2] != "+cli" { - t.Errorf("capturedArgs = %v, want [add, priority:H, +cli, Complex task]", capturedArgs) + // args: [add, rc.verbose=new-uuid, priority:H, +cli, Complex task] + if capturedArgs[2] != "priority:H" || capturedArgs[3] != "+cli" { + t.Errorf("capturedArgs = %v, want [add, rc.verbose=new-uuid, priority:H, +cli, Complex task]", capturedArgs) + } +} + +func TestExtractUUIDFromAddOutput(t *testing.T) { + if uuid := extractUUIDFromAddOutput("Created task abc-123-def."); uuid != "abc-123-def" { + t.Fatalf("got %q, want abc-123-def", uuid) + } + if uuid := extractUUIDFromAddOutput("Created task abc-123-def.\nsome other line"); uuid != "abc-123-def" { + t.Fatalf("got %q, want abc-123-def", uuid) + } + if uuid := extractUUIDFromAddOutput("no match here"); uuid != "" { + t.Fatalf("got %q, want empty", uuid) } } diff --git a/internal/askcli/taskexport.go b/internal/askcli/taskexport.go index c18cd4e..9841821 100644 --- a/internal/askcli/taskexport.go +++ b/internal/askcli/taskexport.go @@ -4,7 +4,6 @@ import ( "encoding/json" "fmt" "io" - "strings" ) type TaskExport struct { @@ -41,36 +40,3 @@ func MustParseTaskExport(data []byte) []TaskExport { } return tasks } - -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 { - return strings.TrimSuffix(parts[2], ".") - } - } - } - fields := strings.Fields(output) - for i, f := range fields { - if f == "uuid" && i+1 < len(fields) { - return fields[i+1] - } - if strings.HasPrefix(f, "Created task") { - parts := strings.Split(f, " ") - if len(parts) >= 2 { - return strings.TrimSuffix(parts[1], ".") - } - } - } - return strings.TrimSpace(output) -} diff --git a/internal/askcli/taskexport_test.go b/internal/askcli/taskexport_test.go index af468e2..e7779aa 100644 --- a/internal/askcli/taskexport_test.go +++ b/internal/askcli/taskexport_test.go @@ -51,38 +51,6 @@ 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") - } -} - -func TestExtractUUIDFromOutput_UUIDField(t *testing.T) { - output := "Some text\nuuid abc-123-def\nmore text" - uuid := ExtractUUIDFromOutput(output) - if uuid != "abc-123-def" { - t.Fatalf("ExtractUUIDFromOutput = %q, want %q", uuid, "abc-123-def") - } -} - -func TestExtractUUIDFromOutput_PlainText(t *testing.T) { - output := "abc-456-xyz" - uuid := ExtractUUIDFromOutput(output) - if uuid != output { - t.Fatalf("ExtractUUIDFromOutput = %q, want %q", uuid, output) - } -} - func TestTaskExport_JSONRoundTrip(t *testing.T) { original := TaskExport{ UUID: "test-uuid", @@ -133,13 +101,6 @@ func TestParseTaskExport_MultipleTasks(t *testing.T) { } } -func TestExtractUUIDFromOutput_NilOutput(t *testing.T) { - uuid := ExtractUUIDFromOutput("") - if uuid != "" { - t.Fatalf("ExtractUUIDFromOutput = %q, want empty string", uuid) - } -} - func TestParseTaskExport_ReadError(t *testing.T) { _, err := ParseTaskExport(&errReader{}) if err == nil { |
