summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-03-22 22:21:44 +0200
committerPaul Buetow <paul@buetow.org>2026-03-22 22:21:44 +0200
commitf6ce62d4e5cefc4a7761bbb86f329ad08ba57570 (patch)
tree83d697fece9df55be204b1ed646cea3d6075f394
parent641e5f723215960713ad183d6d99619b64d69467 (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.go736
-rw-r--r--internal/askcli/command_delete.go4
-rw-r--r--internal/askcli/command_delete_test.go4
-rw-r--r--internal/askcli/command_dep.go2
-rw-r--r--internal/askcli/command_dep_test.go4
-rw-r--r--internal/askcli/command_list.go4
-rw-r--r--internal/askcli/command_write.go6
-rw-r--r--internal/askcli/command_write_test.go6
-rw-r--r--internal/askcli/dispatch.go7
-rw-r--r--internal/askcli/dispatch_test.go7
-rw-r--r--internal/askcli/formatter.go4
-rw-r--r--internal/version.go2
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"