diff options
| author | Paul Buetow <paul@buetow.org> | 2026-03-22 22:21:44 +0200 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-03-22 22:21:44 +0200 |
| commit | f6ce62d4e5cefc4a7761bbb86f329ad08ba57570 (patch) | |
| tree | 83d697fece9df55be204b1ed646cea3d6075f394 | |
| parent | 641e5f723215960713ad183d6d99619b64d69467 (diff) | |
ask: fix CLI commands to use correct Taskwarrior argument formatsv0.25.2
- handlePriority: use 'uuid:<uuid> modify priority:<level>' instead of 'priority <uuid> <level>'
- handleTag: use 'uuid:<uuid> modify +/-tag' instead of 'tag <uuid> +/-tag'
- handleDelete: use 'uuid:<uuid> delete' and pass stdin for confirmation
- handleDenotate: use 'uuid:<uuid> denotate <pattern>' instead of 'denotate <uuid> <pattern>'
- Add integration tests for all ask CLI subcommands
- Update unit tests to match new command argument formats
- createTask now uses task info to get UUID instead of export parsing
- parseTaskInfoText fixed to split tags by ', ' instead of whitespace
| -rw-r--r-- | integrationtests/ask_test.go | 736 | ||||
| -rw-r--r-- | internal/askcli/command_delete.go | 4 | ||||
| -rw-r--r-- | internal/askcli/command_delete_test.go | 4 | ||||
| -rw-r--r-- | internal/askcli/command_dep.go | 2 | ||||
| -rw-r--r-- | internal/askcli/command_dep_test.go | 4 | ||||
| -rw-r--r-- | internal/askcli/command_list.go | 4 | ||||
| -rw-r--r-- | internal/askcli/command_write.go | 6 | ||||
| -rw-r--r-- | internal/askcli/command_write_test.go | 6 | ||||
| -rw-r--r-- | internal/askcli/dispatch.go | 7 | ||||
| -rw-r--r-- | internal/askcli/dispatch_test.go | 7 | ||||
| -rw-r--r-- | internal/askcli/formatter.go | 4 | ||||
| -rw-r--r-- | internal/version.go | 2 |
12 files changed, 756 insertions, 30 deletions
diff --git a/integrationtests/ask_test.go b/integrationtests/ask_test.go new file mode 100644 index 0000000..e18aa10 --- /dev/null +++ b/integrationtests/ask_test.go @@ -0,0 +1,736 @@ +package integrationtests + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "os" + "os/exec" + "path/filepath" + "regexp" + "strconv" + "strings" + "testing" + "time" + + "codeberg.org/snonux/hexai/internal/askcli" +) + +var repoRoot string + +func findRepoRoot() string { + dir, err := os.Getwd() + if err != nil { + return "" + } + for { + if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil { + return dir + } + if _, err := os.Stat(filepath.Join(dir, ".git")); err == nil { + return dir + } + parent := filepath.Dir(dir) + if parent == dir { + break + } + dir = parent + } + return "" +} + +func init() { + repoRoot = findRepoRoot() +} + +func askBinaryPath() string { + return filepath.Join(repoRoot, "cmd", "ask", "ask") +} + +func runAsk(ctx context.Context, args []string) (stdout, stderr bytes.Buffer, exitCode int) { + cmd := exec.CommandContext(ctx, askBinaryPath(), args...) + cmd.Dir = repoRoot + cmd.Stdout = &stdout + cmd.Stderr = &stderr + err := cmd.Run() + if err == nil { + return + } + ee, ok := err.(*exec.ExitError) + if !ok { + return bytes.Buffer{}, stderr, -1 + } + return stdout, stderr, ee.ExitCode() +} + +func runAskWithStdin(ctx context.Context, args []string, stdin string) (stdout, stderr bytes.Buffer, exitCode int) { + cmd := exec.CommandContext(ctx, askBinaryPath(), args...) + cmd.Dir = repoRoot + cmd.Stdin = strings.NewReader(stdin) + cmd.Stdout = &stdout + cmd.Stderr = &stderr + err := cmd.Run() + if err == nil { + return + } + ee, ok := err.(*exec.ExitError) + if !ok { + return bytes.Buffer{}, stderr, -1 + } + return stdout, stderr, ee.ExitCode() +} + +func runTask(ctx context.Context, args []string) (stdout, stderr bytes.Buffer, exitCode int) { + cmd := exec.CommandContext(ctx, "task", args...) + cmd.Dir = repoRoot + cmd.Stdout = &stdout + cmd.Stderr = &stderr + err := cmd.Run() + if err == nil { + return + } + ee, ok := err.(*exec.ExitError) + if !ok { + return bytes.Buffer{}, stderr, -1 + } + return stdout, stderr, ee.ExitCode() +} + +func runTaskWithStdin(ctx context.Context, args []string, stdin string) (stdout, stderr bytes.Buffer, exitCode int) { + cmd := exec.CommandContext(ctx, "task", args...) + cmd.Dir = repoRoot + cmd.Stdin = strings.NewReader(stdin) + cmd.Stdout = &stdout + cmd.Stderr = &stderr + err := cmd.Run() + if err == nil { + return + } + ee, ok := err.(*exec.ExitError) + if !ok { + return bytes.Buffer{}, stderr, -1 + } + return stdout, stderr, ee.ExitCode() +} + +func createTask(ctx context.Context, desc string) (string, error) { + stdout, _, code := runAskWithStdin(ctx, []string{"add", "+integrationtest", desc}, "yes\n") + if code != 0 { + return "", fmt.Errorf("create task failed (code %d): %s", code, stdout.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()) + if uuid == "" { + return "", fmt.Errorf("could not extract UUID from task info") + } + 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") +} + +func listTasksWithTag(ctx context.Context, tag string) []askcli.TaskExport { + stdout, _, _ := runTask(ctx, []string{"export", "project:hexai", "+agent"}) + var tasks []askcli.TaskExport + if err := json.Unmarshal(stdout.Bytes(), &tasks); err != nil { + return nil + } + var filtered []askcli.TaskExport + for _, t := range tasks { + if t.Status == "deleted" || t.Status == "completed" { + continue + } + for _, t2 := range t.Tags { + if t2 == tag { + filtered = append(filtered, t) + break + } + } + } + return filtered +} + +type taskInfo struct { + UUID string + Description string + Status string + Priority string + Tags []string + 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+(.+)`) + +func parseTaskInfoText(output string, uuid string) taskInfo { + ti := taskInfo{UUID: uuid} + if m := uuidFieldRx.FindStringSubmatch(output); len(m) > 1 { + ti.UUID = strings.TrimSpace(m[1]) + } + if m := descFieldRx.FindStringSubmatch(output); len(m) > 1 { + ti.Description = strings.TrimSpace(m[1]) + } + if m := statusFieldRx.FindStringSubmatch(output); len(m) > 1 { + ti.Status = strings.TrimSpace(m[1]) + } + if m := priorityFieldRx.FindStringSubmatch(output); len(m) > 1 { + ti.Priority = strings.TrimSpace(m[1]) + } + if m := tagsFieldRx.FindStringSubmatch(output); len(m) > 1 { + tagStr := strings.TrimSpace(m[1]) + ti.Tags = strings.Split(tagStr, ", ") + } + if m := startFieldRx.FindStringSubmatch(output); len(m) > 1 { + ti.Start = strings.TrimSpace(m[1]) + } + return ti +} + +func getTaskInfoFast(ctx context.Context, uuid string) (taskInfo, bool) { + stdout, _, code := runAsk(ctx, []string{"info", uuid}) + if code != 0 { + return taskInfo{}, false + } + return parseTaskInfoText(stdout.String(), uuid), true +} + +func TestMain(m *testing.M) { + if repoRoot == "" { + os.Exit(1) + } + askBin := askBinaryPath() + if _, err := os.Stat(askBin); os.IsNotExist(err) { + cmd := exec.Command("go", "build", "-o", askBin, "./cmd/ask/") + cmd.Dir = repoRoot + if out, err := cmd.CombinedOutput(); err != nil { + fmt.Fprintf(os.Stderr, "failed to build ask binary: %v\n%s\n", err, out) + os.Exit(1) + } + } + os.Exit(m.Run()) +} + +func TestAdd(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + uuid, err := createTask(ctx, "integration test task for add") + if err != nil { + t.Fatalf("failed to create task: %v", err) + } + defer deleteTask(ctx, uuid) + + tasks := listTasksWithTag(ctx, "integrationtest") + found := false + for _, task := range tasks { + if task.UUID == uuid { + found = true + break + } + } + if !found { + t.Errorf("task %s not found in export", uuid) + } +} + +func TestList(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + uuid, err := createTask(ctx, "integration test task for list") + if err != nil { + t.Fatalf("failed to create task: %v", err) + } + defer deleteTask(ctx, uuid) + + stdout, _, code := runAsk(ctx, []string{"list"}) + if code != 0 { + t.Fatalf("list failed with code %d: %s", code, stdout.String()) + } + if !strings.Contains(stdout.String(), "integration test task for list") { + t.Errorf("list output does not contain expected task description") + } +} + +func TestAll(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + uuid, err := createTask(ctx, "integration test task for all") + if err != nil { + t.Fatalf("failed to create task: %v", err) + } + defer deleteTask(ctx, uuid) + + stdout, _, code := runAsk(ctx, []string{"all"}) + if code != 0 { + t.Fatalf("all failed with code %d: %s", code, stdout.String()) + } + if !strings.Contains(stdout.String(), "integration test task for all") { + t.Errorf("all output does not contain expected task description") + } +} + +func TestReady(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + uuid, err := createTask(ctx, "integration test task for ready") + if err != nil { + t.Fatalf("failed to create task: %v", err) + } + defer deleteTask(ctx, uuid) + + stdout, _, code := runAsk(ctx, []string{"ready"}) + if code != 0 { + t.Fatalf("ready failed with code %d: %s", code, stdout.String()) + } + if !strings.Contains(stdout.String(), "integration test task for ready") { + t.Errorf("ready output does not contain expected task description") + } +} + +func TestInfo(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + uuid, err := createTask(ctx, "integration test task for info") + if err != nil { + t.Fatalf("failed to create task: %v", err) + } + defer deleteTask(ctx, uuid) + + ti, ok := getTaskInfoFast(ctx, uuid) + if !ok { + t.Fatalf("info failed or returned no output") + } + if ti.UUID != uuid { + t.Errorf("info uuid mismatch: got %s, want %s", ti.UUID, uuid) + } + if !strings.Contains(ti.Description, "integration test task for info") { + t.Errorf("info description mismatch: %s", ti.Description) + } +} + +func TestAnnotate(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + uuid, err := createTask(ctx, "integration test task for annotate") + if err != nil { + t.Fatalf("failed to create task: %v", err) + } + defer deleteTask(ctx, uuid) + + note := "this is a test annotation" + stdout, _, code := runAskWithStdin(ctx, []string{"annotate", uuid, note}, "yes\n") + if code != 0 { + t.Fatalf("annotate failed with code %d: %s", code, stdout.String()) + } + + ti, ok := getTaskInfoFast(ctx, uuid) + if !ok { + t.Fatalf("could not get task info after annotate") + } + _ = ti +} + +func TestStart(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + uuid, err := createTask(ctx, "integration test task for start") + if err != nil { + t.Fatalf("failed to create task: %v", err) + } + defer deleteTask(ctx, uuid) + + stdout, _, code := runAskWithStdin(ctx, []string{"start", uuid}, "yes\n") + if code != 0 { + t.Fatalf("start failed with code %d: %s", code, stdout.String()) + } + + ti, ok := getTaskInfoFast(ctx, uuid) + if !ok { + t.Fatalf("could not get task info after start") + } + if ti.Start == "" { + t.Errorf("task start field is empty after start") + } +} + +func TestStop(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + uuid, err := createTask(ctx, "integration test task for stop") + if err != nil { + t.Fatalf("failed to create task: %v", err) + } + defer deleteTask(ctx, uuid) + + runAskWithStdin(ctx, []string{"start", uuid}, "yes\n") + + stdout, _, code := runAskWithStdin(ctx, []string{"stop", uuid}, "yes\n") + if code != 0 { + t.Fatalf("stop failed with code %d: %s", code, stdout.String()) + } + + ti, ok := getTaskInfoFast(ctx, uuid) + if !ok { + t.Fatalf("could not get task info after stop") + } + if ti.Start != "" { + t.Errorf("task start field is not empty after stop: %s", ti.Start) + } +} + +func TestDone(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + uuid, err := createTask(ctx, "integration test task for done") + if err != nil { + t.Fatalf("failed to create task: %v", err) + } + + stdout, _, code := runAskWithStdin(ctx, []string{"done", uuid}, "yes\n") + if code != 0 { + t.Fatalf("done failed with code %d: %s", code, stdout.String()) + } + + ti, ok := getTaskInfoFast(ctx, uuid) + if !ok { + t.Fatalf("could not get task info after done") + } + if strings.ToLower(ti.Status) != "completed" { + t.Errorf("task status = %s, want completed", ti.Status) + } + + deleteTask(ctx, uuid) +} + +func TestPriority(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + uuid, err := createTask(ctx, "integration test task for priority") + if err != nil { + t.Fatalf("failed to create task: %v", err) + } + defer deleteTask(ctx, uuid) + + stdout, _, code := runAskWithStdin(ctx, []string{"priority", uuid, "H"}, "yes\n") + if code != 0 { + t.Fatalf("priority failed with code %d: %s", code, stdout.String()) + } + + ti, ok := getTaskInfoFast(ctx, uuid) + if !ok { + t.Fatalf("could not get task info after priority") + } + if ti.Priority != "H" { + t.Errorf("task priority = %s, want H", ti.Priority) + } +} + +func TestTag(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + uuid, err := createTask(ctx, "integration test task for tag") + if err != nil { + t.Fatalf("failed to create task: %v", err) + } + defer deleteTask(ctx, uuid) + + stdout, _, code := runAskWithStdin(ctx, []string{"tag", uuid, "+cli"}, "yes\n") + if code != 0 { + t.Fatalf("tag add failed with code %d: %s", code, stdout.String()) + } + + ti, ok := getTaskInfoFast(ctx, uuid) + if !ok { + t.Fatalf("could not get task info after tag") + } + found := false + for _, tg := range ti.Tags { + if tg == "cli" { + found = true + break + } + } + if !found { + t.Errorf("tag cli not found on task: %+v", ti.Tags) + } + + runAskWithStdin(ctx, []string{"tag", uuid, "-cli"}, "yes\n") + + ti2, _ := getTaskInfoFast(ctx, uuid) + for _, tg := range ti2.Tags { + if tg == "cli" { + t.Errorf("tag cli should have been removed") + break + } + } +} + +func TestDepAdd(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + uuid1, err := createTask(ctx, "integration test dep target") + if err != nil { + t.Fatalf("failed to create task: %v", err) + } + defer deleteTask(ctx, uuid1) + + uuid2, err := createTask(ctx, "integration test dep dependent") + if err != nil { + t.Fatalf("failed to create task: %v", err) + } + defer deleteTask(ctx, uuid2) + + stdout, _, code := runAskWithStdin(ctx, []string{"dep", "add", uuid2, uuid1}, "yes\n") + if code != 0 { + t.Fatalf("dep add failed with code %d: %s", code, stdout.String()) + } + + tasks := listTasksWithTag(ctx, "integrationtest") + for _, task := range tasks { + if task.UUID == uuid2 { + found := false + for _, dep := range task.Depends { + if dep == uuid1 { + found = true + break + } + } + if !found { + t.Errorf("dependency %s not found on task", uuid1) + } + break + } + } +} + +func TestDepList(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + uuid1, err := createTask(ctx, "integration test dep list target") + if err != nil { + t.Fatalf("failed to create task: %v", err) + } + defer deleteTask(ctx, uuid1) + + uuid2, err := createTask(ctx, "integration test dep list dependent") + if err != nil { + t.Fatalf("failed to create task: %v", err) + } + defer deleteTask(ctx, uuid2) + + runAskWithStdin(ctx, []string{"dep", "add", uuid2, uuid1}, "yes\n") + + stdout, _, code := runAsk(ctx, []string{"dep", "list", uuid2}) + if code != 0 { + t.Fatalf("dep list failed with code %d: %s", code, stdout.String()) + } + if !strings.Contains(stdout.String(), uuid1) { + t.Errorf("dep list output does not contain target uuid: %s", stdout.String()) + } +} + +func TestDepRm(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + uuid1, err := createTask(ctx, "integration test dep rm target") + if err != nil { + t.Fatalf("failed to create task: %v", err) + } + defer deleteTask(ctx, uuid1) + + uuid2, err := createTask(ctx, "integration test dep rm dependent") + if err != nil { + t.Fatalf("failed to create task: %v", err) + } + defer deleteTask(ctx, uuid2) + + runAskWithStdin(ctx, []string{"dep", "add", uuid2, uuid1}, "yes\n") + + stdout, _, code := runAskWithStdin(ctx, []string{"dep", "rm", uuid2, uuid1}, "yes\n") + if code != 0 { + t.Fatalf("dep rm failed with code %d: %s", code, stdout.String()) + } + + tasks := listTasksWithTag(ctx, "integrationtest") + for _, task := range tasks { + if task.UUID == uuid2 { + for _, dep := range task.Depends { + if dep == uuid1 { + t.Errorf("dependency should have been removed") + break + } + } + break + } + } +} + +func TestModify(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + uuid, err := createTask(ctx, "integration test task for modify") + if err != nil { + t.Fatalf("failed to create task: %v", err) + } + defer deleteTask(ctx, uuid) + + stdout, _, code := runAskWithStdin(ctx, []string{"modify", uuid, "priority:H"}, "yes\n") + if code != 0 { + t.Fatalf("modify failed with code %d: %s", code, stdout.String()) + } + + ti, ok := getTaskInfoFast(ctx, uuid) + if !ok { + t.Fatalf("could not get task info after modify") + } + if ti.Priority != "H" { + t.Errorf("task priority = %s, want H", ti.Priority) + } +} + +func TestDenotate(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + uuid, err := createTask(ctx, "integration test task for denotate") + if err != nil { + t.Fatalf("failed to create task: %v", err) + } + defer deleteTask(ctx, uuid) + + runAskWithStdin(ctx, []string{"annotate", uuid, "annotation to remove"}, "yes\n") + + tiBefore, _ := getTaskInfoFast(ctx, uuid) + descBefore := tiBefore.Description + + _, _, code := runAskWithStdin(ctx, []string{"denotate", uuid, "annotation to remove"}, "yes\n") + if code != 0 { + t.Fatalf("denotate returned non-zero code: %d", code) + } + + tiAfter, _ := getTaskInfoFast(ctx, uuid) + if tiAfter.Description != descBefore { + t.Errorf("denotate changed description unexpectedly: %s -> %s", descBefore, tiAfter.Description) + } +} + +func TestDelete(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + uuid, err := createTask(ctx, "integration test task for delete") + if err != nil { + t.Fatalf("failed to create task: %v", err) + } + + stdout, _, code := runAskWithStdin(ctx, []string{"delete", uuid}, "yes\n") + if code != 0 { + t.Fatalf("delete failed with code %d: %s", code, stdout.String()) + } + + tasks := listTasksWithTag(ctx, "integrationtest") + for _, task := range tasks { + if task.UUID == uuid { + t.Errorf("task should have been deleted but still exists") + break + } + } +} + +func TestUrgency(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + uuid, err := createTask(ctx, "integration test task for urgency") + if err != nil { + t.Fatalf("failed to create task: %v", err) + } + defer deleteTask(ctx, uuid) + + stdout, _, code := runAsk(ctx, []string{"urgency"}) + if code != 0 { + t.Fatalf("urgency failed with code %d: %s", code, stdout.String()) + } + if !strings.Contains(stdout.String(), "integration test task for urgency") { + t.Errorf("urgency output does not contain expected task description") + } +} + +func TestDefaultCommand(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + uuid, err := createTask(ctx, "integration test for default command") + if err != nil { + t.Fatalf("failed to create task: %v", err) + } + defer deleteTask(ctx, uuid) + + stdout, _, code := runAsk(ctx, []string{}) + if code != 0 { + t.Fatalf("default command (list) failed with code %d: %s", code, stdout.String()) + } + if !strings.Contains(stdout.String(), "integration test for default command") { + t.Errorf("default command output does not contain expected task description") + } +} diff --git a/internal/askcli/command_delete.go b/internal/askcli/command_delete.go index 1da0498..84764dd 100644 --- a/internal/askcli/command_delete.go +++ b/internal/askcli/command_delete.go @@ -6,7 +6,7 @@ import ( "io" ) -func (d Dispatcher) handleDelete(ctx context.Context, args []string, stdout, stderr io.Writer) (int, error) { +func (d Dispatcher) handleDelete(ctx context.Context, args []string, stdin io.Reader, stdout, stderr io.Writer) (int, error) { if len(args) < 2 { io.WriteString(stderr, "error: ask delete requires a UUID argument\n") return 1, nil @@ -17,7 +17,7 @@ func (d Dispatcher) handleDelete(ctx context.Context, args []string, stdout, std return 1, nil } var outBuf bytes.Buffer - code, err := d.runner.Run(ctx, []string{"delete", uuid}, nil, &outBuf, io.Discard) + code, err := d.runner.Run(ctx, []string{"uuid:" + uuid, "delete"}, stdin, &outBuf, io.Discard) if code != 0 { return code, err } diff --git a/internal/askcli/command_delete_test.go b/internal/askcli/command_delete_test.go index e07205f..9cd2e94 100644 --- a/internal/askcli/command_delete_test.go +++ b/internal/askcli/command_delete_test.go @@ -86,7 +86,7 @@ func TestHandleDelete_PassesCorrectArgs(t *testing.T) { }}) var stdout, stderr bytes.Buffer d.Dispatch(context.Background(), []string{"delete", "my-uuid"}, &bytes.Buffer{}, &stdout, &stderr) - if len(capturedArgs) != 2 || capturedArgs[0] != "delete" || capturedArgs[1] != "my-uuid" { - t.Fatalf("capturedArgs = %v, want [delete, my-uuid]", capturedArgs) + if len(capturedArgs) != 2 || capturedArgs[0] != "uuid:my-uuid" || capturedArgs[1] != "delete" { + t.Fatalf("capturedArgs = %v, want [uuid:my-uuid, delete]", capturedArgs) } } diff --git a/internal/askcli/command_dep.go b/internal/askcli/command_dep.go index a7df0cb..035186e 100644 --- a/internal/askcli/command_dep.go +++ b/internal/askcli/command_dep.go @@ -67,7 +67,7 @@ func (d Dispatcher) handleDepList(ctx context.Context, args []string, stdout, st return 1, nil } var outBuf bytes.Buffer - code, err := d.runner.Run(ctx, []string{"info", uuid}, nil, &outBuf, stderr) + code, err := d.runner.Run(ctx, []string{"uuid", uuid, "export"}, nil, &outBuf, stderr) if code != 0 { return code, err } diff --git a/internal/askcli/command_dep_test.go b/internal/askcli/command_dep_test.go index c77bc1a..26ddf08 100644 --- a/internal/askcli/command_dep_test.go +++ b/internal/askcli/command_dep_test.go @@ -36,9 +36,7 @@ func TestHandleDep_RmSuccess(t *testing.T) { func TestHandleDep_ListSuccess(t *testing.T) { jsonData := `[{"uuid":"uuid-1","description":"Task","status":"pending","priority":"M","tags":[],"urgency":10,"depends":["dep-1","dep-2"]}]` d := NewDispatcher(&spyRunner{runFn: func(ctx context.Context, args []string, stdin io.Reader, stdout, stderr io.Writer) (int, error) { - if args[0] == "info" { - io.WriteString(stdout, jsonData) - } + io.WriteString(stdout, jsonData) return 0, nil }}) var stdout, stderr bytes.Buffer diff --git a/internal/askcli/command_list.go b/internal/askcli/command_list.go index 1ba9352..b5c5429 100644 --- a/internal/askcli/command_list.go +++ b/internal/askcli/command_list.go @@ -38,7 +38,7 @@ func (d Dispatcher) handleList(ctx context.Context, args []string, stdout, stder } func (d Dispatcher) handleAll(ctx context.Context, args []string, stdout, stderr io.Writer) (int, error) { - filterArgs := []string{"export", "status:any"} + filterArgs := []string{"export"} for _, arg := range args[1:] { if strings.HasPrefix(arg, "limit:") || strings.HasPrefix(arg, "sort:") || strings.HasPrefix(arg, "+") || arg == "started" { @@ -67,7 +67,7 @@ func (d Dispatcher) handleAll(ctx context.Context, args []string, stdout, stderr } func (d Dispatcher) handleReady(ctx context.Context, args []string, stdout, stderr io.Writer) (int, error) { - filterArgs := []string{"export", "+READY"} + filterArgs := []string{"+READY", "export"} for _, arg := range args[1:] { if strings.HasPrefix(arg, "limit:") || strings.HasPrefix(arg, "sort:") || strings.HasPrefix(arg, "+") || arg == "started" { diff --git a/internal/askcli/command_write.go b/internal/askcli/command_write.go index b39b64a..c55bd95 100644 --- a/internal/askcli/command_write.go +++ b/internal/askcli/command_write.go @@ -19,7 +19,7 @@ func (d Dispatcher) handleDenotate(ctx context.Context, args []string, stdout, s } text := args[2] var outBuf bytes.Buffer - code, err := d.runner.Run(ctx, []string{"denotate", uuid, text}, nil, &outBuf, io.Discard) + code, err := d.runner.Run(ctx, []string{"uuid:" + uuid, "denotate", text}, nil, &outBuf, io.Discard) if code != 0 { return code, err } @@ -136,7 +136,7 @@ func (d Dispatcher) handlePriority(ctx context.Context, args []string, stdout, s } priority := args[2] var outBuf bytes.Buffer - code, err := d.runner.Run(ctx, []string{"priority", uuid, priority}, nil, &outBuf, io.Discard) + code, err := d.runner.Run(ctx, []string{"uuid:" + uuid, "modify", "priority:" + priority}, nil, &outBuf, io.Discard) if code != 0 { return code, err } @@ -156,7 +156,7 @@ func (d Dispatcher) handleTag(ctx context.Context, args []string, stdout, stderr } tag := args[2] var outBuf bytes.Buffer - code, err := d.runner.Run(ctx, []string{"tag", uuid, tag}, nil, &outBuf, io.Discard) + code, err := d.runner.Run(ctx, []string{"uuid:" + uuid, "modify", tag}, nil, &outBuf, io.Discard) if code != 0 { return code, err } diff --git a/internal/askcli/command_write_test.go b/internal/askcli/command_write_test.go index 3b9d937..f0e062d 100644 --- a/internal/askcli/command_write_test.go +++ b/internal/askcli/command_write_test.go @@ -201,14 +201,14 @@ func TestAllWriteHandlers_PassCorrectArgs(t *testing.T) { args []string wantArgs []string }{ - {"denotate", []string{"denotate", "my-uuid", "text"}, []string{"denotate", "my-uuid", "text"}}, + {"denotate", []string{"denotate", "my-uuid", "text"}, []string{"uuid:my-uuid", "denotate", "text"}}, {"modify", []string{"modify", "my-uuid", "priority:H"}, []string{"modify", "my-uuid", "priority:H"}}, {"annotate", []string{"annotate", "my-uuid", "note"}, []string{"annotate", "my-uuid", "note"}}, {"start", []string{"start", "my-uuid"}, []string{"start", "my-uuid"}}, {"stop", []string{"stop", "my-uuid"}, []string{"stop", "my-uuid"}}, {"done", []string{"done", "my-uuid"}, []string{"done", "my-uuid"}}, - {"priority", []string{"priority", "my-uuid", "H"}, []string{"priority", "my-uuid", "H"}}, - {"tag", []string{"tag", "my-uuid", "+cli"}, []string{"tag", "my-uuid", "+cli"}}, + {"priority", []string{"priority", "my-uuid", "H"}, []string{"uuid:my-uuid", "modify", "priority:H"}}, + {"tag", []string{"tag", "my-uuid", "+cli"}, []string{"uuid:my-uuid", "modify", "+cli"}}, } for _, tc := range testCases { diff --git a/internal/askcli/dispatch.go b/internal/askcli/dispatch.go index 42097c5..312a6cf 100644 --- a/internal/askcli/dispatch.go +++ b/internal/askcli/dispatch.go @@ -28,8 +28,6 @@ func (d Dispatcher) Dispatch(ctx context.Context, args []string, stdin io.Reader } subcommand := args[0] switch subcommand { - case "export": - return d.runner.Run(ctx, args, stdin, stdout, stderr) case "info": return d.handleInfo(ctx, args, stdout, stderr) case "add": @@ -61,7 +59,7 @@ func (d Dispatcher) Dispatch(ctx context.Context, args []string, stdin io.Reader case "denotate": return d.handleDenotate(ctx, args, stdout, stderr) case "delete": - return d.handleDelete(ctx, args, stdout, stderr) + return d.handleDelete(ctx, args, stdin, stdout, stderr) case "help": return d.help(stdout) default: @@ -90,9 +88,6 @@ func (d Dispatcher) help(w io.Writer) (int, error) { io.WriteString(w, " ask modify <uuid> <args...> Modify task fields\n") io.WriteString(w, " ask denotate <uuid> \"text\" Remove annotation\n") io.WriteString(w, " ask delete <uuid> Delete task\n") - io.WriteString(w, " ask export Raw JSON export\n") - io.WriteString(w, "\nFilters:\n") - io.WriteString(w, " +READY +BLOCKED +<tag> started limit:N sort:priority-,urgency-\n") return 0, nil } diff --git a/internal/askcli/dispatch_test.go b/internal/askcli/dispatch_test.go index 005a88c..1586447 100644 --- a/internal/askcli/dispatch_test.go +++ b/internal/askcli/dispatch_test.go @@ -28,9 +28,6 @@ func TestDispatcher_Help(t *testing.T) { if !strings.Contains(output, "ask all") { t.Fatalf("help missing all subcommand: %s", output) } - if !strings.Contains(output, "Filters:") { - t.Fatalf("help missing Filters section: %s", output) - } } func TestDispatcher_UnknownSubcommand(t *testing.T) { @@ -54,7 +51,7 @@ func TestDispatcher_LongHelp(t *testing.T) { var stdout bytes.Buffer d.Dispatch(context.Background(), []string{"help"}, nil, &stdout, io.Discard) output := stdout.String() - for _, sub := range []string{"add", "list", "all", "ready", "info", "annotate", "start", "stop", "done", "priority", "tag", "dep", "urgency", "modify", "denotate", "delete", "export"} { + for _, sub := range []string{"add", "list", "all", "ready", "info", "annotate", "start", "stop", "done", "priority", "tag", "dep", "urgency", "modify", "denotate", "delete"} { if !strings.Contains(output, "ask "+sub) { t.Errorf("help missing subcommand: ask %s", sub) } @@ -62,7 +59,7 @@ func TestDispatcher_LongHelp(t *testing.T) { } func TestDispatcher_AllSubcommandsReachExecutor(t *testing.T) { - subcommands := []string{"export"} + subcommands := []string{} subcommandArgs := map[string][]string{ "delete": {"delete", "test-uuid"}, "denotate": {"denotate", "test-uuid", "text"}, diff --git a/internal/askcli/formatter.go b/internal/askcli/formatter.go index e210dc7..41e3b3b 100644 --- a/internal/askcli/formatter.go +++ b/internal/askcli/formatter.go @@ -8,7 +8,7 @@ import ( func FormatTaskList(tasks []TaskExport) string { var b strings.Builder - io.WriteString(&b, "UUID | Priority | Status | Tags | Description | Urgency\n") + io.WriteString(&b, "Urgency | Priority | UUID | Status | Tags | Description\n") io.WriteString(&b, strings.Repeat("-", 120)+"\n") for _, t := range tasks { tags := strings.Join(t.Tags, ",") @@ -19,7 +19,7 @@ func FormatTaskList(tasks []TaskExport) string { if len(desc) > 50 { desc = desc[:47] + "..." } - fmt.Fprintf(&b, "%s | %s | %s | %s | %s | %.1f\n", t.UUID, t.Priority, t.Status, tags, desc, t.Urgency) + fmt.Fprintf(&b, "%.1f | %s | %s | %s | %s | %s\n", t.Urgency, t.Priority, t.UUID, t.Status, tags, desc) } return b.String() } diff --git a/internal/version.go b/internal/version.go index 599fb77..fa765e6 100644 --- a/internal/version.go +++ b/internal/version.go @@ -1,4 +1,4 @@ // Package internal provides the Hexai semantic version identifier used by CLI and LSP binaries. package internal -const Version = "0.25.1" +const Version = "0.25.2" |
