summaryrefslogtreecommitdiff
path: root/internal/askcli
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-05-31 10:07:41 +0300
committerPaul Buetow <paul@buetow.org>2026-05-31 10:07:41 +0300
commit0dfcb0f3fe6db144f02506df95e68707d9b5091c (patch)
treeed6c0b976acca95b403a7939dd49c49de160003b /internal/askcli
parent168d3a1009066ba38a37fe8a2f8f40a649344eae (diff)
ask: add completed sub-command to list completed tasks
Diffstat (limited to 'internal/askcli')
-rw-r--r--internal/askcli/command_list.go4
-rw-r--r--internal/askcli/command_list_test.go38
-rw-r--r--internal/askcli/command_watch.go2
-rw-r--r--internal/askcli/commands_registry.go7
-rw-r--r--internal/askcli/completion_test.go2
-rw-r--r--internal/askcli/dispatch.go1
-rw-r--r--internal/askcli/dispatch_test.go9
7 files changed, 59 insertions, 4 deletions
diff --git a/internal/askcli/command_list.go b/internal/askcli/command_list.go
index 99ffcc8..ebe26da 100644
--- a/internal/askcli/command_list.go
+++ b/internal/askcli/command_list.go
@@ -21,6 +21,10 @@ func (d *Dispatcher) handleReady(ctx context.Context, args []string, stdout, std
return d.handleListWithFilters(ctx, []string{"+READY"}, args[1:], stdout, stderr)
}
+func (d *Dispatcher) handleCompleted(ctx context.Context, args []string, stdout, stderr io.Writer) (int, error) {
+ return d.handleListWithFilters(ctx, []string{"status:completed"}, args[1:], stdout, stderr)
+}
+
// handleListWithFilters is the shared implementation for list/all/ready.
// initialFilters seeds the taskwarrior filter; extraArgs are user-supplied
// filter modifiers (limit:, sort:, +tag, started).
diff --git a/internal/askcli/command_list_test.go b/internal/askcli/command_list_test.go
index e25293e..c7c7b8a 100644
--- a/internal/askcli/command_list_test.go
+++ b/internal/askcli/command_list_test.go
@@ -188,6 +188,44 @@ func TestHandleReady_Success(t *testing.T) {
}
}
+func TestHandleCompleted_Success(t *testing.T) {
+ dir := t.TempDir()
+ oldRoot := taskAliasCacheRoot
+ oldNow := nowTaskAliasCache
+ taskAliasCacheRoot = func() (string, error) { return filepath.Join(dir, "hexai"), nil }
+ nowTaskAliasCache = func() time.Time { return time.Date(2026, 3, 26, 12, 0, 0, 0, time.UTC) }
+ defer func() {
+ taskAliasCacheRoot = oldRoot
+ nowTaskAliasCache = oldNow
+ }()
+
+ writeTaskAliasCacheForTest(t, taskAliasCache{
+ NextID: 1,
+ Entries: []taskAliasCacheEntry{
+ {UUID: "uuid-done", Alias: "0", CreatedAt: nowTaskAliasCache()},
+ },
+ })
+
+ jsonData := `[{"uuid":"uuid-done","description":"Done task","status":"completed","priority":"M","tags":[],"urgency":0.0,"depends":[]}]`
+ d := NewDispatcher(&spyRunner{runFn: func(ctx context.Context, args []string, stdin io.Reader, stdout, stderr io.Writer) (int, error) {
+ for _, arg := range args {
+ if arg == "export" {
+ _, _ = io.WriteString(stdout, jsonData)
+ return 0, nil
+ }
+ }
+ return 0, nil
+ }})
+ var stdout, stderr bytes.Buffer
+ code, _ := d.Dispatch(context.Background(), []string{"completed"}, nil, &stdout, &stderr)
+ if code != 0 {
+ t.Fatalf("completed code = %d, want 0", code)
+ }
+ if !strings.Contains(stdout.String(), "0") || strings.Contains(stdout.String(), "uuid-done") {
+ t.Fatalf("output should show alias only: %s", stdout.String())
+ }
+}
+
func TestHandleList_PassesFilters(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) {
diff --git a/internal/askcli/command_watch.go b/internal/askcli/command_watch.go
index 33b3bbd..0e326ef 100644
--- a/internal/askcli/command_watch.go
+++ b/internal/askcli/command_watch.go
@@ -40,7 +40,7 @@ func (d *Dispatcher) handleWatch(ctx context.Context, args []string, stdout, std
watchArgs = []string{"list"}
}
if !watchCommandAllowed(watchArgs) {
- _, _ = io.WriteString(stderr, "error: ask watch supports read-only subcommands: list, all, ready, info, dep list, urgency, help\n")
+ _, _ = io.WriteString(stderr, "error: ask watch supports read-only subcommands: list, all, ready, completed, info, dep list, urgency, help\n")
return 1, nil
}
diff --git a/internal/askcli/commands_registry.go b/internal/askcli/commands_registry.go
index b288fd4..c755560 100644
--- a/internal/askcli/commands_registry.go
+++ b/internal/askcli/commands_registry.go
@@ -100,6 +100,13 @@ var commandRegistry = newCommandTable([]commandEntry{
readOnly: true,
},
{
+ name: "completed",
+ description: "List completed tasks",
+ handler: wrapSimpleCommand((*Dispatcher).handleCompleted),
+ includeInCompletion: true,
+ readOnly: true,
+ },
+ {
name: "info",
description: "Show task details",
handler: wrapSimpleCommand((*Dispatcher).handleInfo),
diff --git a/internal/askcli/completion_test.go b/internal/askcli/completion_test.go
index 015c7d5..3cc3a24 100644
--- a/internal/askcli/completion_test.go
+++ b/internal/askcli/completion_test.go
@@ -12,7 +12,7 @@ func TestFishCompletion_IncludesCommandsAndExcludesExport(t *testing.T) {
t.Fatalf("script missing scope completion for %q", name)
}
}
- for _, name := range []string{"add", "list", "all", "ready", "info", "annotate", "start", "stop", "done", "priority", "tag", "dep", "urgency", "modify", "denotate", "delete", "fish", "help"} {
+ for _, name := range []string{"add", "list", "all", "ready", "completed", "info", "annotate", "start", "stop", "done", "priority", "tag", "dep", "urgency", "modify", "denotate", "delete", "fish", "help"} {
if !strings.Contains(script, " -a '"+name+"' ") {
t.Fatalf("script missing root completion for %q", name)
}
diff --git a/internal/askcli/dispatch.go b/internal/askcli/dispatch.go
index f6c503b..81b8944 100644
--- a/internal/askcli/dispatch.go
+++ b/internal/askcli/dispatch.go
@@ -82,6 +82,7 @@ func (d *Dispatcher) help(w io.Writer) (int, error) {
_, _ = io.WriteString(w, " ask add [mods...] [depends:<id|uuid>,...] <description...> Create a new task and print created task <id>\n")
_, _ = io.WriteString(w, " ask list [filters] List active tasks (default)\n")
_, _ = io.WriteString(w, " ask ready List READY tasks (not blocked)\n")
+ _, _ = io.WriteString(w, " ask completed List completed tasks\n")
_, _ = io.WriteString(w, " ask all [filters] List all tasks including completed/deleted\n")
_, _ = io.WriteString(w, " ask info [id|uuid] Show task details or current started task\n")
_, _ = io.WriteString(w, " ask annotate <id|uuid> \"note\" Add annotation to task\n")
diff --git a/internal/askcli/dispatch_test.go b/internal/askcli/dispatch_test.go
index c7c7086..2b00a0e 100644
--- a/internal/askcli/dispatch_test.go
+++ b/internal/askcli/dispatch_test.go
@@ -203,7 +203,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", "watch", "projects", "modify", "denotate", "delete", "fish"} {
+ for _, sub := range []string{"add", "list", "all", "ready", "completed", "info", "annotate", "start", "stop", "done", "priority", "tag", "dep", "urgency", "watch", "projects", "modify", "denotate", "delete", "fish"} {
if !strings.Contains(output, "ask "+sub) {
t.Errorf("help missing subcommand: ask %s", sub)
}
@@ -482,6 +482,11 @@ func TestDispatcher_AllSubcommandsReachExecutor(t *testing.T) {
wantCalls: [][]string{{"+READY", "export"}},
},
{
+ name: "completed",
+ args: []string{"completed"},
+ wantCalls: [][]string{{"status:completed", "export"}},
+ },
+ {
name: "urgency",
args: []string{"urgency"},
wantCalls: [][]string{{"export"}},
@@ -574,7 +579,7 @@ func TestDispatcher_AllSubcommandsReachExecutor(t *testing.T) {
d := NewDispatcher(&spyRunner{runFn: func(ctx context.Context, args []string, stdin io.Reader, stdout, stderr io.Writer) (int, error) {
calls = append(calls, append([]string(nil), args...))
switch strings.Join(args, " ") {
- case "export", "status:pending export", "+READY export":
+ case "export", "status:pending export", "+READY export", "status:completed export":
_, _ = io.WriteString(stdout, taskJSONFor("test-uuid"))
case "uuid:test-uuid export":
_, _ = io.WriteString(stdout, taskJSONFor("test-uuid"))