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 | |
| 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>
| -rw-r--r-- | cmd/ask/main_test.go | 4 | ||||
| -rw-r--r-- | integrationtests/ask_test.go | 66 | ||||
| -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 |
6 files changed, 86 insertions, 162 deletions
diff --git a/cmd/ask/main_test.go b/cmd/ask/main_test.go index f91afbd..db6b436 100644 --- a/cmd/ask/main_test.go +++ b/cmd/ask/main_test.go @@ -25,8 +25,8 @@ func TestMain_WiresDispatcher(t *testing.T) { if code != 0 { t.Fatalf("exitCode = %d, want 0", code) } - if len(gotArgs) < 2 || gotArgs[0] != "export" { - t.Fatalf("args = %v, want [export, ...]", gotArgs) + if len(gotArgs) < 1 || gotArgs[len(gotArgs)-1] != "export" { + t.Fatalf("args = %v, want [..., export]", gotArgs) } } diff --git a/integrationtests/ask_test.go b/integrationtests/ask_test.go index e18aa10..42825dd 100644 --- a/integrationtests/ask_test.go +++ b/integrationtests/ask_test.go @@ -9,7 +9,6 @@ import ( "os/exec" "path/filepath" "regexp" - "strconv" "strings" "testing" "time" @@ -114,59 +113,20 @@ func runTaskWithStdin(ctx context.Context, args []string, stdin string) (stdout, return stdout, stderr, ee.ExitCode() } +// createTask creates a new task via ask add and returns its UUID. +// ask add now outputs the UUID directly, so no follow-up lookup is needed. func createTask(ctx context.Context, desc string) (string, error) { - stdout, _, code := runAskWithStdin(ctx, []string{"add", "+integrationtest", desc}, "yes\n") + stdout, stderr, code := runAsk(ctx, []string{"add", "+integrationtest", desc}) if code != 0 { - return "", fmt.Errorf("create task failed (code %d): %s", code, stdout.String()) + return "", fmt.Errorf("create task failed (code %d): stdout=%s stderr=%s", code, stdout.String(), stderr.String()) } - - taskID := extractTaskID(stdout.String()) - if taskID == "" { - return "", fmt.Errorf("could not extract task ID from output: %s", stdout.String()) - } - - time.Sleep(100 * time.Millisecond) - - infoOut, _, _ := runTask(ctx, []string{taskID, "info"}) - uuid := extractUUIDFromTaskInfo(infoOut.String()) + uuid := strings.TrimSpace(stdout.String()) if uuid == "" { - return "", fmt.Errorf("could not extract UUID from task info") + return "", fmt.Errorf("could not extract UUID from ask add output: %s", stdout.String()) } return uuid, nil } -var taskUUIDRx = regexp.MustCompile(`UUID\s+(.+)`) - -func extractUUIDFromTaskInfo(output string) string { - if m := taskUUIDRx.FindStringSubmatch(output); len(m) > 1 { - return strings.TrimSpace(m[1]) - } - return "" -} - -func extractTaskID(output string) string { - output = strings.TrimSpace(output) - lines := strings.Split(output, "\n") - for _, line := range lines { - line = strings.TrimSpace(line) - if strings.Contains(line, "Created task") { - fields := strings.Fields(line) - for i, f := range fields { - if f == "task" && i+1 < len(fields) { - return strings.TrimSuffix(fields[i+1], ".") - } - } - } - } - for _, line := range lines { - line = strings.TrimSpace(line) - if _, err := strconv.Atoi(line); err == nil { - return line - } - } - return "" -} - func deleteTask(ctx context.Context, uuid string) { runTaskWithStdin(ctx, []string{"uuid:" + uuid, "delete"}, "yes\n") } @@ -201,12 +161,14 @@ type taskInfo struct { Start string } -var uuidFieldRx = regexp.MustCompile(`UUID:\s+(.+)`) -var descFieldRx = regexp.MustCompile(`Description:\s+(.+)`) -var statusFieldRx = regexp.MustCompile(`Status:\s+(.+)`) -var priorityFieldRx = regexp.MustCompile(`Priority:\s+(.+)`) -var tagsFieldRx = regexp.MustCompile(`Tags:\s+(.+)`) -var startFieldRx = regexp.MustCompile(`Started:\s+(.+)`) +var ( + uuidFieldRx = regexp.MustCompile(`UUID:\s+(.+)`) + descFieldRx = regexp.MustCompile(`Description:\s+(.+)`) + statusFieldRx = regexp.MustCompile(`Status:\s+(.+)`) + priorityFieldRx = regexp.MustCompile(`Priority:\s+(.+)`) + tagsFieldRx = regexp.MustCompile(`Tags:\s+(.+)`) + startFieldRx = regexp.MustCompile(`Started:\s+(.+)`) +) func parseTaskInfoText(output string, uuid string) taskInfo { ti := taskInfo{UUID: uuid} 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 { |
