From a80ad2b4691d0df63f68c6977ee18444e7bb752f Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Thu, 18 Jun 2026 08:05:32 +0300 Subject: ik0 replace remaining test seams with DI --- internal/askcli/command_add.go | 2 +- internal/askcli/command_complete_uuids.go | 2 +- internal/askcli/command_complete_uuids_test.go | 31 +++------ internal/askcli/command_delete_test.go | 31 +++------ internal/askcli/command_dep.go | 2 +- internal/askcli/command_dep_test.go | 65 ++++++------------ internal/askcli/command_info.go | 2 +- internal/askcli/command_info_add_test.go | 94 ++++++++------------------ internal/askcli/command_list.go | 2 +- internal/askcli/command_list_test.go | 80 ++++++++-------------- internal/askcli/command_projects.go | 23 +++++-- internal/askcli/command_projects_test.go | 52 ++++---------- internal/askcli/command_start_deps.go | 2 +- internal/askcli/command_start_deps_test.go | 37 +++------- internal/askcli/command_urgency.go | 2 +- internal/askcli/command_urgency_test.go | 18 ++--- internal/askcli/command_watch_test.go | 19 +----- internal/askcli/command_write_test.go | 10 +-- internal/askcli/dispatch.go | 20 ++++-- internal/askcli/dispatch_test.go | 38 ++--------- internal/askcli/render_task_list.go | 8 ++- internal/askcli/render_task_list_test.go | 21 ++---- internal/askcli/task_alias_cache.go | 54 ++++++++++++--- internal/askcli/task_alias_cache_test.go | 84 ++++++++--------------- internal/askcli/task_selector.go | 15 ++-- internal/askcli/task_selector_test.go | 87 ++++++++++++------------ internal/hexaiaction/tui_custom.go | 2 +- internal/hexaiaction/tui_custom_test.go | 17 +++++ internal/hexaicli/cache_test.go | 30 ++++---- internal/hexaicli/run.go | 27 ++++++-- internal/hexaicli/run_model_override_test.go | 7 +- internal/hexaicli/run_output_test.go | 6 +- internal/hexaicli/run_test.go | 13 ++-- internal/hexaicli/run_timeout_test.go | 8 +-- internal/hexaicli/runner.go | 3 + internal/hexaicli/simulation_test.go | 12 ++-- internal/tmuxedit/agentutil.go | 12 +++- internal/tmuxedit/agentutil_test.go | 7 +- internal/tmuxedit/pane.go | 15 ++-- internal/tmuxedit/send_test.go | 2 +- 40 files changed, 408 insertions(+), 554 deletions(-) (limited to 'internal') diff --git a/internal/askcli/command_add.go b/internal/askcli/command_add.go index ccfb034..0793693 100644 --- a/internal/askcli/command_add.go +++ b/internal/askcli/command_add.go @@ -56,7 +56,7 @@ func (d *Dispatcher) createTask(ctx context.Context, modifiers []string, descrip // problem as a warning on stderr but still report success on stdout with // exit 0 so the user does not retry and create a duplicate task. The // displayed identifier falls back to the UUID when no alias is available. - aliases, aliasErr := ensureTaskAliasesForUUIDs([]string{uuid}) + aliases, aliasErr := d.aliasCache.withDefaults().ensureTaskAliasesForUUIDs([]string{uuid}) if aliasErr != nil { fmt.Fprintf(stderr, "warning: failed to assign task alias: %v\n", aliasErr) aliases = nil diff --git a/internal/askcli/command_complete_uuids.go b/internal/askcli/command_complete_uuids.go index 5c26649..567602d 100644 --- a/internal/askcli/command_complete_uuids.go +++ b/internal/askcli/command_complete_uuids.go @@ -36,7 +36,7 @@ func (d *Dispatcher) completeTaskSelectors(ctx context.Context, args []string, s fmt.Fprintf(stderr, "error: failed to parse task data: %v\n", err) return 1, nil } - aliases, err := ensureTaskAliases(tasks) + aliases, err := d.aliasCache.withDefaults().ensureTaskAliases(tasks) if err != nil { fmt.Fprintf(stderr, "warning: failed to update task alias cache: %v\n", err) aliases = nil diff --git a/internal/askcli/command_complete_uuids_test.go b/internal/askcli/command_complete_uuids_test.go index 2c1b5fd..b7a2e80 100644 --- a/internal/askcli/command_complete_uuids_test.go +++ b/internal/askcli/command_complete_uuids_test.go @@ -13,14 +13,8 @@ import ( func TestHandleCompleteUUIDs_PrintsPendingUUIDs(t *testing.T) { dir := t.TempDir() - oldNow := nowTaskAliasCache - oldRoot := taskAliasCacheRoot - nowTaskAliasCache = func() time.Time { return time.Date(2026, 3, 26, 12, 0, 0, 0, time.UTC) } - taskAliasCacheRoot = func() (string, error) { return filepath.Join(dir, "hexai"), nil } - defer func() { - nowTaskAliasCache = oldNow - taskAliasCacheRoot = oldRoot - }() + now := time.Date(2026, 3, 26, 12, 0, 0, 0, time.UTC) + deps := testTaskAliasCacheDeps(dir, &now) d := NewDispatcher(&spyRunner{runFn: func(ctx context.Context, args []string, stdin io.Reader, stdout, stderr io.Writer) (int, error) { want := []string{"status:pending", "export"} @@ -30,6 +24,7 @@ func TestHandleCompleteUUIDs_PrintsPendingUUIDs(t *testing.T) { _, _ = io.WriteString(stdout, `[{"uuid":"uuid-1","description":"First task"},{"uuid":"uuid-2","description":"Second task"},{"uuid":""}]`) return 0, nil }}) + d.aliasCache = deps var stdout, stderr bytes.Buffer code, err := d.handleCompleteUUIDs(context.Background(), nil, &stdout, &stderr) @@ -48,7 +43,7 @@ func TestHandleCompleteUUIDs_PrintsPendingUUIDs(t *testing.T) { t.Fatalf("stderr = %q, want empty", stderr.String()) } - path, err := taskAliasCachePath() + path, err := deps.taskAliasCachePath() if err != nil { t.Fatalf("taskAliasCachePath: %v", err) } @@ -82,11 +77,9 @@ func TestHandleCompleteUUIDs_ParseError(t *testing.T) { func TestHandleCompleteUUIDs_RecoverFromCorruptAliasCache(t *testing.T) { dir := t.TempDir() - oldRoot := taskAliasCacheRoot - taskAliasCacheRoot = func() (string, error) { return filepath.Join(dir, "hexai"), nil } - defer func() { taskAliasCacheRoot = oldRoot }() + deps := testTaskAliasCacheDeps(dir, nil) - path, err := taskAliasCachePath() + path, err := deps.taskAliasCachePath() if err != nil { t.Fatalf("taskAliasCachePath: %v", err) } @@ -104,6 +97,7 @@ func TestHandleCompleteUUIDs_RecoverFromCorruptAliasCache(t *testing.T) { _, _ = io.WriteString(stdout, `[{"uuid":"uuid-1","description":"Fallback task"}]`) return 0, nil }}) + d.aliasCache = deps var stdout, stderr bytes.Buffer code, err := d.handleCompleteUUIDs(context.Background(), nil, &stdout, &stderr) @@ -168,14 +162,8 @@ func TestTaskCompletionAliasItems_OnlyShortAliases(t *testing.T) { func TestHandleCompleteAliases_PrintsAliasesOnly(t *testing.T) { dir := t.TempDir() - oldNow := nowTaskAliasCache - oldRoot := taskAliasCacheRoot - nowTaskAliasCache = func() time.Time { return time.Date(2026, 3, 26, 12, 0, 0, 0, time.UTC) } - taskAliasCacheRoot = func() (string, error) { return filepath.Join(dir, "hexai"), nil } - defer func() { - nowTaskAliasCache = oldNow - taskAliasCacheRoot = oldRoot - }() + now := time.Date(2026, 3, 26, 12, 0, 0, 0, time.UTC) + deps := testTaskAliasCacheDeps(dir, &now) d := NewDispatcher(&spyRunner{runFn: func(ctx context.Context, args []string, stdin io.Reader, stdout, stderr io.Writer) (int, error) { want := []string{"status:pending", "export"} @@ -185,6 +173,7 @@ func TestHandleCompleteAliases_PrintsAliasesOnly(t *testing.T) { _, _ = io.WriteString(stdout, `[{"uuid":"uuid-1","description":"First task"},{"uuid":"uuid-2","description":"Second task"},{"uuid":""}]`) return 0, nil }}) + d.aliasCache = deps var stdout, stderr bytes.Buffer code, err := d.handleCompleteAliases(context.Background(), nil, &stdout, &stderr) diff --git a/internal/askcli/command_delete_test.go b/internal/askcli/command_delete_test.go index 62d3e48..d86ee0b 100644 --- a/internal/askcli/command_delete_test.go +++ b/internal/askcli/command_delete_test.go @@ -4,7 +4,6 @@ import ( "bytes" "context" "io" - "path/filepath" "strings" "testing" "time" @@ -12,21 +11,15 @@ import ( func TestHandleDelete_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 - }() + now := time.Date(2026, 3, 26, 12, 0, 0, 0, time.UTC) + deps := testTaskAliasCacheDeps(dir, &now) writeTaskAliasCacheForTest(t, taskAliasCache{ NextID: 1, Entries: []taskAliasCacheEntry{ - {UUID: "test-uuid-123", Alias: "0", CreatedAt: nowTaskAliasCache()}, + {UUID: "test-uuid-123", Alias: "0", CreatedAt: now}, }, - }) + }, deps) d := NewDispatcher(&spyRunner{runFn: func(ctx context.Context, args []string, stdin io.Reader, stdout, stderr io.Writer) (int, error) { if len(args) == 2 && args[0] == "uuid:test-uuid-123" && args[1] == "export" { @@ -35,6 +28,7 @@ func TestHandleDelete_Success(t *testing.T) { } return 0, nil }}) + d.aliasCache = deps var stdout, stderr bytes.Buffer code, err := d.Dispatch(context.Background(), []string{"delete", "test-uuid-123"}, &bytes.Buffer{}, &stdout, &stderr) if code != 0 { @@ -120,21 +114,15 @@ func TestHandleDelete_PassesCorrectArgs(t *testing.T) { func TestHandleDelete_AliasSelector(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 - }() + now := time.Date(2026, 3, 26, 12, 0, 0, 0, time.UTC) + deps := testTaskAliasCacheDeps(dir, &now) writeTaskAliasCacheForTest(t, taskAliasCache{ NextID: 1, Entries: []taskAliasCacheEntry{ - {UUID: "test-uuid-123", Alias: "0", CreatedAt: nowTaskAliasCache()}, + {UUID: "test-uuid-123", Alias: "0", CreatedAt: now}, }, - }) + }, deps) var capturedArgs []string d := NewDispatcher(&spyRunner{runFn: func(ctx context.Context, args []string, stdin io.Reader, stdout, stderr io.Writer) (int, error) { @@ -145,6 +133,7 @@ func TestHandleDelete_AliasSelector(t *testing.T) { capturedArgs = args return 0, nil }}) + d.aliasCache = deps var stdout, stderr bytes.Buffer code, _ := d.Dispatch(context.Background(), []string{"delete", "0"}, &bytes.Buffer{}, &stdout, &stderr) diff --git a/internal/askcli/command_dep.go b/internal/askcli/command_dep.go index aa28df8..ce65aa4 100644 --- a/internal/askcli/command_dep.go +++ b/internal/askcli/command_dep.go @@ -75,7 +75,7 @@ func (d *Dispatcher) handleDepList(ctx context.Context, args []string, stdout, s if len(task.Depends) == 0 { _, _ = io.WriteString(stdout, "no dependencies\n") } else { - aliases, err := ensureTaskAliasesForUUIDs(task.Depends) + aliases, err := d.aliasCache.withDefaults().ensureTaskAliasesForUUIDs(task.Depends) if err != nil { fmt.Fprintf(stderr, "error: failed to load task aliases: %v\n", err) return 1, nil diff --git a/internal/askcli/command_dep_test.go b/internal/askcli/command_dep_test.go index 4cbcd26..fe3ab70 100644 --- a/internal/askcli/command_dep_test.go +++ b/internal/askcli/command_dep_test.go @@ -4,7 +4,6 @@ import ( "bytes" "context" "io" - "path/filepath" "strings" "testing" "time" @@ -12,22 +11,16 @@ import ( func TestHandleDep_AddSuccess(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 - }() + now := time.Date(2026, 3, 26, 12, 0, 0, 0, time.UTC) + deps := testTaskAliasCacheDeps(dir, &now) writeTaskAliasCacheForTest(t, taskAliasCache{ NextID: 2, Entries: []taskAliasCacheEntry{ - {UUID: "uuid-1", Alias: "0", CreatedAt: nowTaskAliasCache()}, - {UUID: "uuid-2", Alias: "1", CreatedAt: nowTaskAliasCache()}, + {UUID: "uuid-1", Alias: "0", CreatedAt: now}, + {UUID: "uuid-2", Alias: "1", CreatedAt: now}, }, - }) + }, deps) var capturedArgs []string d := NewDispatcher(&spyRunner{runFn: func(ctx context.Context, args []string, stdin io.Reader, stdout, stderr io.Writer) (int, error) { @@ -43,6 +36,7 @@ func TestHandleDep_AddSuccess(t *testing.T) { capturedArgs = args return 0, nil }}) + d.aliasCache = deps var stdout, stderr bytes.Buffer code, _ := d.Dispatch(context.Background(), []string{"dep", "add", "uuid-1", "uuid-2"}, nil, &stdout, &stderr) if code != 0 { @@ -59,14 +53,8 @@ func TestHandleDep_AddSuccess(t *testing.T) { func TestHandleDep_RmSuccess(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 - }() + now := time.Date(2026, 3, 26, 12, 0, 0, 0, time.UTC) + deps := testTaskAliasCacheDeps(dir, &now) d := NewDispatcher(&spyRunner{runFn: func(ctx context.Context, args []string, stdin io.Reader, stdout, stderr io.Writer) (int, error) { if len(args) == 2 && args[1] == "export" { @@ -80,6 +68,7 @@ func TestHandleDep_RmSuccess(t *testing.T) { } return 0, nil }}) + d.aliasCache = deps var stdout, stderr bytes.Buffer code, _ := d.Dispatch(context.Background(), []string{"dep", "rm", "uuid-1", "uuid-2"}, nil, &stdout, &stderr) if code != 0 { @@ -89,29 +78,24 @@ func TestHandleDep_RmSuccess(t *testing.T) { func TestHandleDep_ListSuccess(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 - }() + now := time.Date(2026, 3, 26, 12, 0, 0, 0, time.UTC) + deps := testTaskAliasCacheDeps(dir, &now) writeTaskAliasCacheForTest(t, taskAliasCache{ NextID: 3, Entries: []taskAliasCacheEntry{ - {UUID: "dep-1", Alias: "1", CreatedAt: nowTaskAliasCache()}, - {UUID: "dep-2", Alias: "2", CreatedAt: nowTaskAliasCache()}, - {UUID: "uuid-1", Alias: "0", CreatedAt: nowTaskAliasCache()}, + {UUID: "dep-1", Alias: "1", CreatedAt: now}, + {UUID: "dep-2", Alias: "2", CreatedAt: now}, + {UUID: "uuid-1", Alias: "0", CreatedAt: now}, }, - }) + }, deps) 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) { _, _ = io.WriteString(stdout, jsonData) return 0, nil }}) + d.aliasCache = deps var stdout, stderr bytes.Buffer code, _ := d.Dispatch(context.Background(), []string{"dep", "list", "uuid-1"}, nil, &stdout, &stderr) if code != 0 { @@ -198,22 +182,16 @@ func TestHandleDep_AcceptUUIDPrefix(t *testing.T) { func TestHandleDep_AliasSelectors(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 - }() + now := time.Date(2026, 3, 26, 12, 0, 0, 0, time.UTC) + deps := testTaskAliasCacheDeps(dir, &now) writeTaskAliasCacheForTest(t, taskAliasCache{ NextID: 2, Entries: []taskAliasCacheEntry{ - {UUID: "uuid-1", Alias: "0", CreatedAt: nowTaskAliasCache()}, - {UUID: "uuid-2", Alias: "1", CreatedAt: nowTaskAliasCache()}, + {UUID: "uuid-1", Alias: "0", CreatedAt: now}, + {UUID: "uuid-2", Alias: "1", CreatedAt: now}, }, - }) + }, deps) var capturedArgs []string d := NewDispatcher(&spyRunner{runFn: func(ctx context.Context, args []string, stdin io.Reader, stdout, stderr io.Writer) (int, error) { @@ -229,6 +207,7 @@ func TestHandleDep_AliasSelectors(t *testing.T) { capturedArgs = args return 0, nil }}) + d.aliasCache = deps var stdout, stderr bytes.Buffer code, _ := d.Dispatch(context.Background(), []string{"dep", "add", "0", "1"}, nil, &stdout, &stderr) diff --git a/internal/askcli/command_info.go b/internal/askcli/command_info.go index ba2e37d..6c02583 100644 --- a/internal/askcli/command_info.go +++ b/internal/askcli/command_info.go @@ -16,7 +16,7 @@ func (d *Dispatcher) handleInfo(ctx context.Context, args []string, stdout, stde return code, nil } allUUIDs := append([]string{tasks[0].UUID}, tasks[0].Depends...) - aliases, err := ensureTaskAliasesForUUIDs(allUUIDs) + aliases, err := d.aliasCache.withDefaults().ensureTaskAliasesForUUIDs(allUUIDs) if err != nil { fmt.Fprintf(stderr, "error: failed to load task aliases: %v\n", err) return 1, nil diff --git a/internal/askcli/command_info_add_test.go b/internal/askcli/command_info_add_test.go index b918518..ad82606 100644 --- a/internal/askcli/command_info_add_test.go +++ b/internal/askcli/command_info_add_test.go @@ -5,7 +5,6 @@ import ( "context" "encoding/json" "io" - "path/filepath" "strings" "testing" "time" @@ -14,22 +13,16 @@ import ( func TestHandleInfo_Success(t *testing.T) { unsetTestEnv(t, "HEXAI_DEBUG") 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 - }() + now := time.Date(2026, 3, 26, 12, 0, 0, 0, time.UTC) + deps := testTaskAliasCacheDeps(dir, &now) writeTaskAliasCacheForTest(t, taskAliasCache{ NextID: 2, Entries: []taskAliasCacheEntry{ - {UUID: "dep-1", Alias: "1", CreatedAt: nowTaskAliasCache()}, - {UUID: "test-uuid", Alias: "0", CreatedAt: nowTaskAliasCache()}, + {UUID: "dep-1", Alias: "1", CreatedAt: now}, + {UUID: "test-uuid", Alias: "0", CreatedAt: now}, }, - }) + }, deps) jsonData := `[{"uuid":"test-uuid","description":"Test task","status":"pending","priority":"H","tags":["cli","agent"],"urgency":15.0,"depends":["dep-1"],"annotations":[{"description":"Note 1","entry":"2026-03-22T10:00:00Z"}]}]` d := NewDispatcher(&spyRunner{runFn: func(ctx context.Context, args []string, stdin io.Reader, stdout, stderr io.Writer) (int, error) { @@ -39,6 +32,7 @@ func TestHandleInfo_Success(t *testing.T) { } return 0, nil }}) + d.aliasCache = deps var stdout, stderr bytes.Buffer code, _ := d.Dispatch(context.Background(), []string{"info", "test-uuid"}, nil, &stdout, &stderr) if code != 0 { @@ -68,21 +62,15 @@ func TestHandleInfo_Success(t *testing.T) { func TestHandleInfo_Success_DebugShowsUUID(t *testing.T) { t.Setenv("HEXAI_DEBUG", "true") 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 - }() + now := time.Date(2026, 3, 26, 12, 0, 0, 0, time.UTC) + deps := testTaskAliasCacheDeps(dir, &now) writeTaskAliasCacheForTest(t, taskAliasCache{ NextID: 1, Entries: []taskAliasCacheEntry{ - {UUID: "test-uuid", Alias: "0", CreatedAt: nowTaskAliasCache()}, + {UUID: "test-uuid", Alias: "0", CreatedAt: now}, }, - }) + }, deps) jsonData := `[{"uuid":"test-uuid","description":"Test task","status":"pending","priority":"H","tags":["cli","agent"],"urgency":15.0,"depends":[],"annotations":[]}]` d := NewDispatcher(&spyRunner{runFn: func(ctx context.Context, args []string, stdin io.Reader, stdout, stderr io.Writer) (int, error) { @@ -91,6 +79,7 @@ func TestHandleInfo_Success_DebugShowsUUID(t *testing.T) { } return 0, nil }}) + d.aliasCache = deps var stdout, stderr bytes.Buffer code, _ := d.Dispatch(context.Background(), []string{"info", "test-uuid"}, nil, &stdout, &stderr) @@ -107,21 +96,15 @@ func TestHandleInfo_Success_DebugShowsUUID(t *testing.T) { func TestHandleInfo_AssignsDependencyAliasesFromInfo(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 - }() + now := time.Date(2026, 3, 26, 12, 0, 0, 0, time.UTC) + deps := testTaskAliasCacheDeps(dir, &now) writeTaskAliasCacheForTest(t, taskAliasCache{ NextID: 1, Entries: []taskAliasCacheEntry{ - {UUID: "test-uuid", Alias: "0", CreatedAt: nowTaskAliasCache()}, + {UUID: "test-uuid", Alias: "0", CreatedAt: now}, }, - }) + }, deps) jsonData := `[{"uuid":"test-uuid","description":"Test task","status":"pending","priority":"H","tags":["cli"],"urgency":15.0,"depends":["dep-b","dep-a"]}]` d := NewDispatcher(&spyRunner{runFn: func(ctx context.Context, args []string, stdin io.Reader, stdout, stderr io.Writer) (int, error) { @@ -130,6 +113,7 @@ func TestHandleInfo_AssignsDependencyAliasesFromInfo(t *testing.T) { } return 0, nil }}) + d.aliasCache = deps var stdout, stderr bytes.Buffer code, _ := d.Dispatch(context.Background(), []string{"info", "test-uuid"}, nil, &stdout, &stderr) @@ -149,21 +133,15 @@ func TestHandleInfo_AssignsDependencyAliasesFromInfo(t *testing.T) { func TestHandleInfo_AliasSelector(t *testing.T) { unsetTestEnv(t, "HEXAI_DEBUG") 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 - }() + now := time.Date(2026, 3, 26, 12, 0, 0, 0, time.UTC) + deps := testTaskAliasCacheDeps(dir, &now) writeTaskAliasCacheForTest(t, taskAliasCache{ NextID: 1, Entries: []taskAliasCacheEntry{ - {UUID: "test-uuid", Alias: "0", CreatedAt: nowTaskAliasCache()}, + {UUID: "test-uuid", Alias: "0", CreatedAt: now}, }, - }) + }, deps) jsonData := `[{"uuid":"test-uuid","description":"Test task","status":"pending","priority":"H","tags":["cli"],"urgency":15.0,"depends":[]}]` d := NewDispatcher(&spyRunner{runFn: func(ctx context.Context, args []string, stdin io.Reader, stdout, stderr io.Writer) (int, error) { @@ -172,6 +150,7 @@ func TestHandleInfo_AliasSelector(t *testing.T) { } return 0, nil }}) + d.aliasCache = deps var stdout, stderr bytes.Buffer code, _ := d.Dispatch(context.Background(), []string{"info", "0"}, nil, &stdout, &stderr) @@ -188,21 +167,15 @@ func TestHandleInfo_AliasSelector(t *testing.T) { func TestHandleInfo_JSONIncludesAliasID(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 - }() + now := time.Date(2026, 3, 26, 12, 0, 0, 0, time.UTC) + deps := testTaskAliasCacheDeps(dir, &now) writeTaskAliasCacheForTest(t, taskAliasCache{ NextID: 1, Entries: []taskAliasCacheEntry{ - {UUID: "test-uuid", Alias: "0", CreatedAt: nowTaskAliasCache()}, + {UUID: "test-uuid", Alias: "0", CreatedAt: now}, }, - }) + }, deps) jsonData := `[{"uuid":"test-uuid","description":"Test task","status":"pending","priority":"H","tags":["cli"],"urgency":15.0,"depends":[]}]` d := NewDispatcher(&spyRunner{runFn: func(ctx context.Context, args []string, stdin io.Reader, stdout, stderr io.Writer) (int, error) { @@ -211,6 +184,7 @@ func TestHandleInfo_JSONIncludesAliasID(t *testing.T) { } return 0, nil }}) + d.aliasCache = deps var stdout, stderr bytes.Buffer code, _ := d.Dispatch(context.Background(), []string{"--json", "info", "0"}, nil, &stdout, &stderr) @@ -247,14 +221,8 @@ func TestHandleInfo_NumericID(t *testing.T) { func TestHandleInfo_MissingUUID(t *testing.T) { unsetTestEnv(t, "HEXAI_DEBUG") 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 - }() + now := time.Date(2026, 3, 26, 12, 0, 0, 0, time.UTC) + deps := testTaskAliasCacheDeps(dir, &now) jsonData := `[{"uuid":"started-uuid","description":"Started task","status":"pending","priority":"M","start":"2026-03-26T10:00:00Z","urgency":5.0,"depends":[]}]` d := NewDispatcher(&spyRunner{runFn: func(ctx context.Context, args []string, stdin io.Reader, stdout, stderr io.Writer) (int, error) { @@ -263,6 +231,7 @@ func TestHandleInfo_MissingUUID(t *testing.T) { } return 0, nil }}) + d.aliasCache = deps var stdout, stderr bytes.Buffer code, _ := d.Dispatch(context.Background(), []string{"info"}, nil, &stdout, &stderr) if code != 0 { @@ -349,14 +318,11 @@ func TestHandleAdd_Success(t *testing.T) { // This prevents the previous bug where the user saw exit 1 and retried, // creating duplicate tasks. func TestHandleAdd_AliasAssignmentFailure(t *testing.T) { - oldRoot := taskAliasCacheRoot - taskAliasCacheRoot = func() (string, error) { return "", io.ErrUnexpectedEOF } - defer func() { taskAliasCacheRoot = oldRoot }() - d := NewDispatcher(&spyRunner{runFn: func(ctx context.Context, args []string, stdin io.Reader, stdout, stderr io.Writer) (int, error) { _, _ = io.WriteString(stdout, "Created task abc-123-def.") return 0, nil }}) + d.aliasCache = taskAliasCacheDeps{cacheRoot: func() (string, error) { return "", io.ErrUnexpectedEOF }} var stdout, stderr bytes.Buffer code, _ := d.Dispatch(context.Background(), []string{"add", "New task description"}, nil, &stdout, &stderr) diff --git a/internal/askcli/command_list.go b/internal/askcli/command_list.go index ebe26da..c1f37bb 100644 --- a/internal/askcli/command_list.go +++ b/internal/askcli/command_list.go @@ -55,7 +55,7 @@ func (d *Dispatcher) handleListWithFilters(ctx context.Context, initialFilters, } return tasks[i].Urgency > tasks[j].Urgency }) - return renderTaskList(tasks, stdout, stderr, d.jsonOutput) + return renderTaskListWithAliasLoader(tasks, stdout, stderr, d.jsonOutput, d.aliasCache.withDefaults().ensureTaskAliases) } func priorityOrder(p string) int { diff --git a/internal/askcli/command_list_test.go b/internal/askcli/command_list_test.go index c7c7b8a..82efe77 100644 --- a/internal/askcli/command_list_test.go +++ b/internal/askcli/command_list_test.go @@ -4,7 +4,6 @@ import ( "bytes" "context" "io" - "path/filepath" "strings" "testing" "time" @@ -12,22 +11,16 @@ import ( func TestHandleList_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 - }() + now := time.Date(2026, 3, 26, 12, 0, 0, 0, time.UTC) + deps := testTaskAliasCacheDeps(dir, &now) writeTaskAliasCacheForTest(t, taskAliasCache{ NextID: 2, Entries: []taskAliasCacheEntry{ - {UUID: "uuid-1", Alias: "0", CreatedAt: nowTaskAliasCache()}, - {UUID: "uuid-2", Alias: "1", CreatedAt: nowTaskAliasCache()}, + {UUID: "uuid-1", Alias: "0", CreatedAt: now}, + {UUID: "uuid-2", Alias: "1", CreatedAt: now}, }, - }) + }, deps) jsonData := `[{"uuid":"uuid-1","description":"Task 1","status":"pending","priority":"H","tags":["cli"],"start":"2026-03-26T10:00:00Z","urgency":15.0,"depends":[]},{"uuid":"uuid-2","description":"Task 2","status":"completed","priority":"M","tags":["agent"],"urgency":10.0,"depends":[]}]` d := NewDispatcher(&spyRunner{runFn: func(ctx context.Context, args []string, stdin io.Reader, stdout, stderr io.Writer) (int, error) { @@ -39,6 +32,7 @@ func TestHandleList_Success(t *testing.T) { } return 0, nil }}) + d.aliasCache = deps var stdout, stderr bytes.Buffer code, _ := d.Dispatch(context.Background(), []string{"list"}, nil, &stdout, &stderr) if code != 0 { @@ -58,22 +52,16 @@ func TestHandleList_Success(t *testing.T) { func TestHandleList_SortedByPriority(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 - }() + now := time.Date(2026, 3, 26, 12, 0, 0, 0, time.UTC) + deps := testTaskAliasCacheDeps(dir, &now) writeTaskAliasCacheForTest(t, taskAliasCache{ NextID: 2, Entries: []taskAliasCacheEntry{ - {UUID: "uuid-1", Alias: "0", CreatedAt: nowTaskAliasCache()}, - {UUID: "uuid-2", Alias: "1", CreatedAt: nowTaskAliasCache()}, + {UUID: "uuid-1", Alias: "0", CreatedAt: now}, + {UUID: "uuid-2", Alias: "1", CreatedAt: now}, }, - }) + }, deps) jsonData := `[{"uuid":"uuid-2","description":"Task 2","status":"pending","priority":"M","tags":[],"urgency":10.0,"depends":[]},{"uuid":"uuid-1","description":"Task 1","status":"pending","priority":"H","tags":[],"urgency":5.0,"depends":[]}]` d := NewDispatcher(&spyRunner{runFn: func(ctx context.Context, args []string, stdin io.Reader, stdout, stderr io.Writer) (int, error) { @@ -85,6 +73,7 @@ func TestHandleList_SortedByPriority(t *testing.T) { } return 0, nil }}) + d.aliasCache = deps var stdout bytes.Buffer d.Dispatch(context.Background(), []string{"list"}, nil, &stdout, &bytes.Buffer{}) output := stdout.String() @@ -114,21 +103,15 @@ func TestHandleList_EmptyList(t *testing.T) { func TestHandleAll_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 - }() + now := time.Date(2026, 3, 26, 12, 0, 0, 0, time.UTC) + deps := testTaskAliasCacheDeps(dir, &now) writeTaskAliasCacheForTest(t, taskAliasCache{ NextID: 1, Entries: []taskAliasCacheEntry{ - {UUID: "uuid-1", Alias: "0", CreatedAt: nowTaskAliasCache()}, + {UUID: "uuid-1", Alias: "0", CreatedAt: now}, }, - }) + }, deps) jsonData := `[{"uuid":"uuid-1","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) { @@ -140,6 +123,7 @@ func TestHandleAll_Success(t *testing.T) { } return 0, nil }}) + d.aliasCache = deps var stdout, stderr bytes.Buffer code, _ := d.Dispatch(context.Background(), []string{"all"}, nil, &stdout, &stderr) if code != 0 { @@ -152,21 +136,15 @@ func TestHandleAll_Success(t *testing.T) { func TestHandleReady_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 - }() + now := time.Date(2026, 3, 26, 12, 0, 0, 0, time.UTC) + deps := testTaskAliasCacheDeps(dir, &now) writeTaskAliasCacheForTest(t, taskAliasCache{ NextID: 1, Entries: []taskAliasCacheEntry{ - {UUID: "uuid-ready", Alias: "0", CreatedAt: nowTaskAliasCache()}, + {UUID: "uuid-ready", Alias: "0", CreatedAt: now}, }, - }) + }, deps) jsonData := `[{"uuid":"uuid-ready","description":"Ready task","status":"pending","priority":"H","tags":["READY"],"urgency":20.0,"depends":[]}]` d := NewDispatcher(&spyRunner{runFn: func(ctx context.Context, args []string, stdin io.Reader, stdout, stderr io.Writer) (int, error) { @@ -178,6 +156,7 @@ func TestHandleReady_Success(t *testing.T) { } return 0, nil }}) + d.aliasCache = deps var stdout, stderr bytes.Buffer code, _ := d.Dispatch(context.Background(), []string{"ready"}, nil, &stdout, &stderr) if code != 0 { @@ -190,21 +169,15 @@ 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 - }() + now := time.Date(2026, 3, 26, 12, 0, 0, 0, time.UTC) + deps := testTaskAliasCacheDeps(dir, &now) writeTaskAliasCacheForTest(t, taskAliasCache{ NextID: 1, Entries: []taskAliasCacheEntry{ - {UUID: "uuid-done", Alias: "0", CreatedAt: nowTaskAliasCache()}, + {UUID: "uuid-done", Alias: "0", CreatedAt: now}, }, - }) + }, deps) 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) { @@ -216,6 +189,7 @@ func TestHandleCompleted_Success(t *testing.T) { } return 0, nil }}) + d.aliasCache = deps var stdout, stderr bytes.Buffer code, _ := d.Dispatch(context.Background(), []string{"completed"}, nil, &stdout, &stderr) if code != 0 { diff --git a/internal/askcli/command_projects.go b/internal/askcli/command_projects.go index b370f45..26e407f 100644 --- a/internal/askcli/command_projects.go +++ b/internal/askcli/command_projects.go @@ -9,15 +9,12 @@ import ( "sort" ) -var ( - projectsFindTaskBinary = findTaskBinary - projectsRunTaskCommand = runTaskCommand -) +type taskCommandRunner func(context.Context, string, []string, io.Reader, io.Writer, io.Writer) error func (d *Dispatcher) handleProjects(ctx context.Context, args []string, stdout, stderr io.Writer) (int, error) { _ = args - taskPath, err := projectsFindTaskBinary() + taskPath, err := d.projectTaskBinary()() if err != nil { return 1, fmt.Errorf("ask projects: task binary lookup failed: %w", err) } @@ -25,7 +22,7 @@ func (d *Dispatcher) handleProjects(ctx context.Context, args []string, stdout, scopeFilter := taskScopeFilter(taskScopeFromContext(ctx)) cmdArgs := append([]string{"rc.verbose=nothing", "rc.confirmation=off", scopeFilter, "status:pending", "export"}, args[1:]...) var outBuf bytes.Buffer - err = projectsRunTaskCommand(ctx, taskPath, cmdArgs, nil, &outBuf, stderr) + err = d.projectTaskCommand()(ctx, taskPath, cmdArgs, nil, &outBuf, stderr) if err != nil { return exitCodeFor(err), fmt.Errorf("ask projects: task export failed: %w", err) } @@ -65,3 +62,17 @@ func (d *Dispatcher) handleProjects(ctx context.Context, args []string, stdout, } return 0, nil } + +func (d *Dispatcher) projectTaskBinary() func() (string, error) { + if d != nil && d.findTaskBinary != nil { + return d.findTaskBinary + } + return findTaskBinary +} + +func (d *Dispatcher) projectTaskCommand() taskCommandRunner { + if d != nil && d.runTaskCommand != nil { + return d.runTaskCommand + } + return runTaskCommand +} diff --git a/internal/askcli/command_projects_test.go b/internal/askcli/command_projects_test.go index 3059afb..144cfe5 100644 --- a/internal/askcli/command_projects_test.go +++ b/internal/askcli/command_projects_test.go @@ -11,15 +11,9 @@ import ( ) func TestHandleProjects_ListsUniqueProjects(t *testing.T) { - oldFind := projectsFindTaskBinary - oldRun := projectsRunTaskCommand - t.Cleanup(func() { - projectsFindTaskBinary = oldFind - projectsRunTaskCommand = oldRun - }) - - projectsFindTaskBinary = func() (string, error) { return "task", nil } - projectsRunTaskCommand = func(ctx context.Context, name string, args []string, stdin io.Reader, stdout, stderr io.Writer) error { + d := NewDispatcher(nil) + d.findTaskBinary = func() (string, error) { return "task", nil } + d.runTaskCommand = func(ctx context.Context, name string, args []string, stdin io.Reader, stdout, stderr io.Writer) error { tasks := []TaskExport{ {UUID: "1", Project: "hexai", Status: "pending", Urgency: 1}, {UUID: "2", Project: "dtail", Status: "pending", Urgency: 2, Start: "2026-01-01T00:00:00Z"}, @@ -31,7 +25,6 @@ func TestHandleProjects_ListsUniqueProjects(t *testing.T) { } ctx := context.Background() - d := NewDispatcher(nil) var stdout, stderr bytes.Buffer code, err := d.Dispatch(ctx, []string{"projects"}, nil, &stdout, &stderr) if err != nil { @@ -48,15 +41,9 @@ func TestHandleProjects_ListsUniqueProjects(t *testing.T) { } func TestHandleProjects_JSONOutput(t *testing.T) { - oldFind := projectsFindTaskBinary - oldRun := projectsRunTaskCommand - t.Cleanup(func() { - projectsFindTaskBinary = oldFind - projectsRunTaskCommand = oldRun - }) - - projectsFindTaskBinary = func() (string, error) { return "task", nil } - projectsRunTaskCommand = func(ctx context.Context, name string, args []string, stdin io.Reader, stdout, stderr io.Writer) error { + d := NewDispatcher(nil) + d.findTaskBinary = func() (string, error) { return "task", nil } + d.runTaskCommand = func(ctx context.Context, name string, args []string, stdin io.Reader, stdout, stderr io.Writer) error { tasks := []TaskExport{ {UUID: "1", Project: "hexai", Status: "pending", Urgency: 1}, {UUID: "2", Project: "dtail", Status: "pending", Urgency: 2}, @@ -66,7 +53,6 @@ func TestHandleProjects_JSONOutput(t *testing.T) { } ctx := context.Background() - d := NewDispatcher(nil) var stdout, stderr bytes.Buffer code, err := d.Dispatch(ctx, []string{"--json", "projects"}, nil, &stdout, &stderr) if err != nil { @@ -82,21 +68,14 @@ func TestHandleProjects_JSONOutput(t *testing.T) { } func TestHandleProjects_EmptyResult(t *testing.T) { - oldFind := projectsFindTaskBinary - oldRun := projectsRunTaskCommand - t.Cleanup(func() { - projectsFindTaskBinary = oldFind - projectsRunTaskCommand = oldRun - }) - - projectsFindTaskBinary = func() (string, error) { return "task", nil } - projectsRunTaskCommand = func(ctx context.Context, name string, args []string, stdin io.Reader, stdout, stderr io.Writer) error { + d := NewDispatcher(nil) + d.findTaskBinary = func() (string, error) { return "task", nil } + d.runTaskCommand = func(ctx context.Context, name string, args []string, stdin io.Reader, stdout, stderr io.Writer) error { _, _ = io.WriteString(stdout, "[]") return nil } ctx := context.Background() - d := NewDispatcher(nil) var stdout, stderr bytes.Buffer code, err := d.Dispatch(ctx, []string{"projects"}, nil, &stdout, &stderr) if err != nil { @@ -111,20 +90,13 @@ func TestHandleProjects_EmptyResult(t *testing.T) { } func TestHandleProjects_ForwardsTaskExportError(t *testing.T) { - oldFind := projectsFindTaskBinary - oldRun := projectsRunTaskCommand - t.Cleanup(func() { - projectsFindTaskBinary = oldFind - projectsRunTaskCommand = oldRun - }) - - projectsFindTaskBinary = func() (string, error) { return "task", nil } - projectsRunTaskCommand = func(ctx context.Context, name string, args []string, stdin io.Reader, stdout, stderr io.Writer) error { + d := NewDispatcher(nil) + d.findTaskBinary = func() (string, error) { return "task", nil } + d.runTaskCommand = func(ctx context.Context, name string, args []string, stdin io.Reader, stdout, stderr io.Writer) error { return fmt.Errorf("some error") } ctx := context.Background() - d := NewDispatcher(nil) var stdout, stderr bytes.Buffer code, err := d.Dispatch(ctx, []string{"projects"}, nil, &stdout, &stderr) if err == nil { diff --git a/internal/askcli/command_start_deps.go b/internal/askcli/command_start_deps.go index 94a6a9e..d20bd3f 100644 --- a/internal/askcli/command_start_deps.go +++ b/internal/askcli/command_start_deps.go @@ -13,7 +13,7 @@ func (d *Dispatcher) verifyDependenciesCompletedForStart(ctx context.Context, ta if len(task.Depends) == 0 { return 0 } - aliases, err := ensureTaskAliasesForUUIDs(task.Depends) + aliases, err := d.aliasCache.withDefaults().ensureTaskAliasesForUUIDs(task.Depends) if err != nil { fmt.Fprintf(stderr, "error: failed to load task aliases: %v\n", err) return 1 diff --git a/internal/askcli/command_start_deps_test.go b/internal/askcli/command_start_deps_test.go index 9ba197b..929eabc 100644 --- a/internal/askcli/command_start_deps_test.go +++ b/internal/askcli/command_start_deps_test.go @@ -4,7 +4,6 @@ import ( "bytes" "context" "io" - "path/filepath" "strings" "testing" "time" @@ -12,15 +11,8 @@ import ( func TestHandleStart_BlockedWhenDependencyNotCompleted(t *testing.T) { dir := t.TempDir() - oldRoot := taskAliasCacheRoot - oldNow := nowTaskAliasCache fixedNow := time.Date(2026, 3, 26, 12, 0, 0, 0, time.UTC) - taskAliasCacheRoot = func() (string, error) { return filepath.Join(dir, "hexai"), nil } - nowTaskAliasCache = func() time.Time { return fixedNow } - defer func() { - taskAliasCacheRoot = oldRoot - nowTaskAliasCache = oldNow - }() + deps := testTaskAliasCacheDeps(dir, &fixedNow) writeTaskAliasCacheForTest(t, taskAliasCache{ NextID: 2, @@ -28,7 +20,7 @@ func TestHandleStart_BlockedWhenDependencyNotCompleted(t *testing.T) { {UUID: "main-uuid", Alias: "0", CreatedAt: fixedNow}, {UUID: "dep-uuid", Alias: "1", CreatedAt: fixedNow}, }, - }) + }, deps) var startCalls int d := NewDispatcher(&spyRunner{runFn: func(ctx context.Context, args []string, stdin io.Reader, stdout, stderr io.Writer) (int, error) { @@ -47,6 +39,7 @@ func TestHandleStart_BlockedWhenDependencyNotCompleted(t *testing.T) { return 1, nil } }}) + d.aliasCache = deps var stdout, stderr bytes.Buffer code, _ := d.Dispatch(context.Background(), []string{"start", "main-uuid"}, &bytes.Buffer{}, &stdout, &stderr) @@ -66,15 +59,8 @@ func TestHandleStart_BlockedWhenDependencyNotCompleted(t *testing.T) { func TestHandleStart_AllowedWhenDependencyCompleted(t *testing.T) { dir := t.TempDir() - oldRoot := taskAliasCacheRoot - oldNow := nowTaskAliasCache fixedNow := time.Date(2026, 3, 26, 12, 0, 0, 0, time.UTC) - taskAliasCacheRoot = func() (string, error) { return filepath.Join(dir, "hexai"), nil } - nowTaskAliasCache = func() time.Time { return fixedNow } - defer func() { - taskAliasCacheRoot = oldRoot - nowTaskAliasCache = oldNow - }() + deps := testTaskAliasCacheDeps(dir, &fixedNow) writeTaskAliasCacheForTest(t, taskAliasCache{ NextID: 2, @@ -82,7 +68,7 @@ func TestHandleStart_AllowedWhenDependencyCompleted(t *testing.T) { {UUID: "main-uuid", Alias: "0", CreatedAt: fixedNow}, {UUID: "dep-uuid", Alias: "1", CreatedAt: fixedNow}, }, - }) + }, deps) d := NewDispatcher(&spyRunner{runFn: func(ctx context.Context, args []string, stdin io.Reader, stdout, stderr io.Writer) (int, error) { switch { @@ -99,6 +85,7 @@ func TestHandleStart_AllowedWhenDependencyCompleted(t *testing.T) { return 1, nil } }}) + d.aliasCache = deps var stdout, stderr bytes.Buffer code, _ := d.Dispatch(context.Background(), []string{"start", "main-uuid"}, &bytes.Buffer{}, &stdout, &stderr) @@ -112,15 +99,8 @@ func TestHandleStart_AllowedWhenDependencyCompleted(t *testing.T) { func TestHandleStart_CompletedStatusIsCaseInsensitive(t *testing.T) { dir := t.TempDir() - oldRoot := taskAliasCacheRoot - oldNow := nowTaskAliasCache fixedNow := time.Date(2026, 3, 26, 12, 0, 0, 0, time.UTC) - taskAliasCacheRoot = func() (string, error) { return filepath.Join(dir, "hexai"), nil } - nowTaskAliasCache = func() time.Time { return fixedNow } - defer func() { - taskAliasCacheRoot = oldRoot - nowTaskAliasCache = oldNow - }() + deps := testTaskAliasCacheDeps(dir, &fixedNow) writeTaskAliasCacheForTest(t, taskAliasCache{ NextID: 2, @@ -128,7 +108,7 @@ func TestHandleStart_CompletedStatusIsCaseInsensitive(t *testing.T) { {UUID: "main-uuid", Alias: "0", CreatedAt: fixedNow}, {UUID: "dep-uuid", Alias: "1", CreatedAt: fixedNow}, }, - }) + }, deps) d := NewDispatcher(&spyRunner{runFn: func(ctx context.Context, args []string, stdin io.Reader, stdout, stderr io.Writer) (int, error) { switch { @@ -145,6 +125,7 @@ func TestHandleStart_CompletedStatusIsCaseInsensitive(t *testing.T) { return 1, nil } }}) + d.aliasCache = deps var stdout, stderr bytes.Buffer code, _ := d.Dispatch(context.Background(), []string{"start", "main-uuid"}, &bytes.Buffer{}, &stdout, &stderr) diff --git a/internal/askcli/command_urgency.go b/internal/askcli/command_urgency.go index bc7d4fd..bf7940f 100644 --- a/internal/askcli/command_urgency.go +++ b/internal/askcli/command_urgency.go @@ -23,5 +23,5 @@ func (d *Dispatcher) handleUrgency(ctx context.Context, args []string, stdout, s sort.Slice(tasks, func(i, j int) bool { return tasks[i].Urgency > tasks[j].Urgency }) - return renderTaskList(tasks, stdout, stderr, d.jsonOutput) + return renderTaskListWithAliasLoader(tasks, stdout, stderr, d.jsonOutput, d.aliasCache.withDefaults().ensureTaskAliases) } diff --git a/internal/askcli/command_urgency_test.go b/internal/askcli/command_urgency_test.go index 2582e58..1ad323e 100644 --- a/internal/askcli/command_urgency_test.go +++ b/internal/askcli/command_urgency_test.go @@ -4,7 +4,6 @@ import ( "bytes" "context" "io" - "path/filepath" "strings" "testing" "time" @@ -12,28 +11,23 @@ import ( func TestHandleUrgency_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 - }() + now := time.Date(2026, 3, 26, 12, 0, 0, 0, time.UTC) + deps := testTaskAliasCacheDeps(dir, &now) writeTaskAliasCacheForTest(t, taskAliasCache{ NextID: 2, Entries: []taskAliasCacheEntry{ - {UUID: "uuid-1", Alias: "0", CreatedAt: nowTaskAliasCache()}, - {UUID: "uuid-2", Alias: "1", CreatedAt: nowTaskAliasCache()}, + {UUID: "uuid-1", Alias: "0", CreatedAt: now}, + {UUID: "uuid-2", Alias: "1", CreatedAt: now}, }, - }) + }, deps) jsonData := `[{"uuid":"uuid-2","description":"Task 2","status":"pending","priority":"M","tags":["agent"],"urgency":10.0,"depends":[]},{"uuid":"uuid-1","description":"Task 1","status":"pending","priority":"H","tags":["cli"],"urgency":15.0,"depends":[]}]` d := NewDispatcher(&spyRunner{runFn: func(ctx context.Context, args []string, stdin io.Reader, stdout, stderr io.Writer) (int, error) { _, _ = io.WriteString(stdout, jsonData) return 0, nil }}) + d.aliasCache = deps var stdout, stderr bytes.Buffer code, _ := d.Dispatch(context.Background(), []string{"urgency"}, nil, &stdout, &stderr) if code != 0 { diff --git a/internal/askcli/command_watch_test.go b/internal/askcli/command_watch_test.go index 7618060..d742a4b 100644 --- a/internal/askcli/command_watch_test.go +++ b/internal/askcli/command_watch_test.go @@ -5,7 +5,6 @@ import ( "context" "errors" "io" - "path/filepath" "reflect" "strings" "testing" @@ -100,14 +99,7 @@ func TestHandleWatch_DrawsStderrOnNonZero(t *testing.T) { func TestHandleWatch_DefaultsToListAndRedrawsOnChange(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, 5, 26, 12, 0, 0, 0, time.UTC) } - t.Cleanup(func() { - taskAliasCacheRoot = oldRoot - nowTaskAliasCache = oldNow - }) + t.Setenv("XDG_CACHE_HOME", dir) ticks := make(chan time.Time, 1) ticks <- time.Now() @@ -157,14 +149,7 @@ func TestHandleWatch_DefaultsToListAndRedrawsOnChange(t *testing.T) { func TestHandleWatch_DoesNotRedrawUnchangedOutput(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, 5, 26, 12, 0, 0, 0, time.UTC) } - t.Cleanup(func() { - taskAliasCacheRoot = oldRoot - nowTaskAliasCache = oldNow - }) + t.Setenv("XDG_CACHE_HOME", dir) ticks := make(chan time.Time, 1) ticks <- time.Now() diff --git a/internal/askcli/command_write_test.go b/internal/askcli/command_write_test.go index e922920..453b005 100644 --- a/internal/askcli/command_write_test.go +++ b/internal/askcli/command_write_test.go @@ -5,7 +5,6 @@ import ( "context" "errors" "io" - "path/filepath" "strings" "testing" "time" @@ -15,15 +14,8 @@ func useIsolatedTaskAliasCache(t *testing.T) time.Time { t.Helper() dir := t.TempDir() - oldRoot := taskAliasCacheRoot - oldNow := nowTaskAliasCache fixedNow := time.Date(2026, 3, 26, 12, 0, 0, 0, time.UTC) - taskAliasCacheRoot = func() (string, error) { return filepath.Join(dir, "hexai"), nil } - nowTaskAliasCache = func() time.Time { return fixedNow } - t.Cleanup(func() { - taskAliasCacheRoot = oldRoot - nowTaskAliasCache = oldNow - }) + t.Setenv("XDG_CACHE_HOME", dir) return fixedNow } diff --git a/internal/askcli/dispatch.go b/internal/askcli/dispatch.go index cdfc1d8..c1976f1 100644 --- a/internal/askcli/dispatch.go +++ b/internal/askcli/dispatch.go @@ -20,10 +20,13 @@ type Runner interface { // uses the real time.Ticker-backed factory installed by NewDispatcher; tests // inject a fake ticker so they can drive the watch loop without real delays. type Dispatcher struct { - runner Runner - jsonOutput bool - newTicker func(time.Duration) watchTicker - capture func(context.Context, []byte) (string, error) + runner Runner + jsonOutput bool + newTicker func(time.Duration) watchTicker + capture func(context.Context, []byte) (string, error) + aliasCache taskAliasCacheDeps + findTaskBinary func() (string, error) + runTaskCommand taskCommandRunner } // NewDispatcher creates a Dispatcher backed by the provided Runner or a default @@ -35,9 +38,12 @@ func NewDispatcher(runner Runner) *Dispatcher { runner = &e } return &Dispatcher{ - runner: runner, - newTicker: newRealWatchTicker, - capture: editorCapture, + runner: runner, + newTicker: newRealWatchTicker, + capture: editorCapture, + aliasCache: defaultTaskAliasCacheDeps(), + findTaskBinary: findTaskBinary, + runTaskCommand: runTaskCommand, } } diff --git a/internal/askcli/dispatch_test.go b/internal/askcli/dispatch_test.go index 2b00a0e..589e956 100644 --- a/internal/askcli/dispatch_test.go +++ b/internal/askcli/dispatch_test.go @@ -5,11 +5,9 @@ import ( "context" "io" "os" - "path/filepath" "reflect" "strings" "testing" - "time" ) func TestDispatcher_Help(t *testing.T) { @@ -45,14 +43,7 @@ func TestDispatcher_Help(t *testing.T) { func TestDispatcher_DefaultsInvalidSubcommandToAdd(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, 4, 22, 12, 0, 0, 0, time.UTC) } - defer func() { - taskAliasCacheRoot = oldRoot - nowTaskAliasCache = oldNow - }() + t.Setenv("XDG_CACHE_HOME", dir) tests := []struct { name string @@ -136,14 +127,7 @@ func TestDispatcher_CompleteUUIDsSubcommand(t *testing.T) { // Use a temp dir for the alias cache so this test is hermetic and does // not depend on cache state left by other tests. 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, 27, 12, 0, 0, 0, time.UTC) } - defer func() { - taskAliasCacheRoot = oldRoot - nowTaskAliasCache = oldNow - }() + t.Setenv("XDG_CACHE_HOME", dir) d := NewDispatcher(&spyRunner{runFn: func(ctx context.Context, args []string, stdin io.Reader, stdout, stderr io.Writer) (int, error) { if strings.Join(args, " ") != "status:pending export" { @@ -168,14 +152,7 @@ func TestDispatcher_CompleteUUIDsSubcommand(t *testing.T) { func TestDispatcher_CompleteAliasesSubcommand(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, 27, 12, 0, 0, 0, time.UTC) } - defer func() { - taskAliasCacheRoot = oldRoot - nowTaskAliasCache = oldNow - }() + t.Setenv("XDG_CACHE_HOME", dir) d := NewDispatcher(&spyRunner{runFn: func(ctx context.Context, args []string, stdin io.Reader, stdout, stderr io.Writer) (int, error) { if strings.Join(args, " ") != "status:pending export" { @@ -448,14 +425,7 @@ func TestDispatcher_NoAgentPrefix_StripsScopePrefix(t *testing.T) { func TestDispatcher_AllSubcommandsReachExecutor(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, 27, 12, 0, 0, 0, time.UTC) } - defer func() { - taskAliasCacheRoot = oldRoot - nowTaskAliasCache = oldNow - }() + t.Setenv("XDG_CACHE_HOME", dir) taskJSONFor := func(uuid string) string { return `[{"uuid":"` + uuid + `","description":"Test","status":"pending","priority":"M","tags":[],"urgency":10,"depends":[]}]` diff --git a/internal/askcli/render_task_list.go b/internal/askcli/render_task_list.go index 177bd81..65cae71 100644 --- a/internal/askcli/render_task_list.go +++ b/internal/askcli/render_task_list.go @@ -6,10 +6,14 @@ import ( "io" ) -var taskListAliasLoader = ensureTaskAliases +type taskListAliasLoader func([]TaskExport) (map[string]string, error) func renderTaskList(tasks []TaskExport, stdout, stderr io.Writer, jsonOutput bool) (int, error) { - aliases, err := taskListAliasLoader(tasks) + return renderTaskListWithAliasLoader(tasks, stdout, stderr, jsonOutput, ensureTaskAliases) +} + +func renderTaskListWithAliasLoader(tasks []TaskExport, stdout, stderr io.Writer, jsonOutput bool, loadAliases taskListAliasLoader) (int, error) { + aliases, err := loadAliases(tasks) if err != nil { fmt.Fprintf(stderr, "error: failed to load task aliases: %v\n", err) return 1, nil diff --git a/internal/askcli/render_task_list_test.go b/internal/askcli/render_task_list_test.go index ff47153..42a5e0d 100644 --- a/internal/askcli/render_task_list_test.go +++ b/internal/askcli/render_task_list_test.go @@ -10,10 +10,7 @@ import ( ) func TestRenderTaskList_JSONOutput(t *testing.T) { - oldLoader := taskListAliasLoader - defer func() { taskListAliasLoader = oldLoader }() - - taskListAliasLoader = func(tasks []TaskExport) (map[string]string, error) { + loadAliases := func(tasks []TaskExport) (map[string]string, error) { return map[string]string{"uuid-json": "sq"}, nil } @@ -26,7 +23,7 @@ func TestRenderTaskList_JSONOutput(t *testing.T) { Urgency: 12.5, }} var stdout, stderr bytes.Buffer - code, err := renderTaskList(tasks, &stdout, &stderr, true) + code, err := renderTaskListWithAliasLoader(tasks, &stdout, &stderr, true, loadAliases) if err != nil { t.Fatalf("renderTaskList returned error: %v", err) } @@ -50,10 +47,7 @@ func TestRenderTaskList_JSONOutput(t *testing.T) { } func TestRenderTaskList_TextOutputUsesAliasLoader(t *testing.T) { - oldLoader := taskListAliasLoader - defer func() { taskListAliasLoader = oldLoader }() - - taskListAliasLoader = func(tasks []TaskExport) (map[string]string, error) { + loadAliases := func(tasks []TaskExport) (map[string]string, error) { if len(tasks) != 1 || tasks[0].UUID != "uuid-text" { t.Fatalf("unexpected tasks passed to loader: %#v", tasks) } @@ -61,7 +55,7 @@ func TestRenderTaskList_TextOutputUsesAliasLoader(t *testing.T) { } var stdout, stderr bytes.Buffer - code, err := renderTaskList([]TaskExport{{UUID: "uuid-text", Description: "Text task", Priority: "L"}}, &stdout, &stderr, false) + code, err := renderTaskListWithAliasLoader([]TaskExport{{UUID: "uuid-text", Description: "Text task", Priority: "L"}}, &stdout, &stderr, false, loadAliases) if err != nil { t.Fatalf("renderTaskList returned error: %v", err) } @@ -78,14 +72,11 @@ func TestRenderTaskList_TextOutputUsesAliasLoader(t *testing.T) { } func TestRenderTaskList_AliasLoaderError(t *testing.T) { - oldLoader := taskListAliasLoader - defer func() { taskListAliasLoader = oldLoader }() - - taskListAliasLoader = func([]TaskExport) (map[string]string, error) { + loadAliases := func([]TaskExport) (map[string]string, error) { return nil, fmt.Errorf("boom") } var stdout, stderr bytes.Buffer - code, err := renderTaskList([]TaskExport{{UUID: "uuid-error"}}, &stdout, &stderr, false) + code, err := renderTaskListWithAliasLoader([]TaskExport{{UUID: "uuid-error"}}, &stdout, &stderr, false, loadAliases) if err != nil { t.Fatalf("renderTaskList returned error: %v", err) } diff --git a/internal/askcli/task_alias_cache.go b/internal/askcli/task_alias_cache.go index 8e3fcc4..e7702f2 100644 --- a/internal/askcli/task_alias_cache.go +++ b/internal/askcli/task_alias_cache.go @@ -20,10 +20,32 @@ const ( taskAliasCacheLockTimeout = 30 * time.Second ) -var ( - nowTaskAliasCache = time.Now - taskAliasCacheRoot = stats.CacheDir -) +type taskAliasCacheDeps struct { + now func() time.Time + cacheRoot func() (string, error) + lock func(string) (func() error, error) +} + +func defaultTaskAliasCacheDeps() taskAliasCacheDeps { + return taskAliasCacheDeps{ + now: time.Now, + cacheRoot: stats.CacheDir, + lock: acquireTaskAliasCacheLock, + } +} + +func (d taskAliasCacheDeps) withDefaults() taskAliasCacheDeps { + if d.now == nil { + d.now = time.Now + } + if d.cacheRoot == nil { + d.cacheRoot = stats.CacheDir + } + if d.lock == nil { + d.lock = acquireTaskAliasCacheLock + } + return d +} // taskAliasCache maps UUIDs to short human-readable aliases for display and // completion. The Entries slice is persisted as JSON; the byUUID and byAlias @@ -46,14 +68,19 @@ type taskAliasCacheEntry struct { } func ensureTaskAliases(tasks []TaskExport) (map[string]string, error) { - path, err := taskAliasCachePath() + return defaultTaskAliasCacheDeps().ensureTaskAliases(tasks) +} + +func (d taskAliasCacheDeps) ensureTaskAliases(tasks []TaskExport) (map[string]string, error) { + d = d.withDefaults() + path, err := d.taskAliasCachePath() if err != nil { return nil, err } // Hold an advisory lock for the full load/modify/save cycle so concurrent // ask invocations cannot race on the cache file (lost-update or // rename-against-missing-tempfile). - unlock, err := acquireTaskAliasCacheLock(filepath.Dir(path)) + unlock, err := d.lock(filepath.Dir(path)) if err != nil { return nil, err } @@ -64,7 +91,7 @@ func ensureTaskAliases(tasks []TaskExport) (map[string]string, error) { return nil, err } - now := nowTaskAliasCache().UTC() + now := d.now().UTC() changed := cache.prune(now) aliases := make(map[string]string, len(tasks)) for _, task := range tasks { @@ -86,6 +113,10 @@ func ensureTaskAliases(tasks []TaskExport) (map[string]string, error) { } func ensureTaskAliasesForUUIDs(uuids []string) (map[string]string, error) { + return defaultTaskAliasCacheDeps().ensureTaskAliasesForUUIDs(uuids) +} + +func (d taskAliasCacheDeps) ensureTaskAliasesForUUIDs(uuids []string) (map[string]string, error) { tasks := make([]TaskExport, 0, len(uuids)) for _, uuid := range uuids { if uuid == "" { @@ -93,7 +124,7 @@ func ensureTaskAliasesForUUIDs(uuids []string) (map[string]string, error) { } tasks = append(tasks, TaskExport{UUID: uuid}) } - return ensureTaskAliases(tasks) + return d.ensureTaskAliases(tasks) } func loadTaskAliasCacheAt(path string) (taskAliasCache, error) { @@ -150,7 +181,12 @@ func acquireTaskAliasCacheLock(dir string) (func() error, error) { } func taskAliasCachePath() (string, error) { - dir, err := taskAliasCacheRoot() + return defaultTaskAliasCacheDeps().taskAliasCachePath() +} + +func (d taskAliasCacheDeps) taskAliasCachePath() (string, error) { + d = d.withDefaults() + dir, err := d.cacheRoot() if err != nil { return "", fmt.Errorf("resolve cache dir: %w", err) } diff --git a/internal/askcli/task_alias_cache_test.go b/internal/askcli/task_alias_cache_test.go index 7cfa2a7..791450a 100644 --- a/internal/askcli/task_alias_cache_test.go +++ b/internal/askcli/task_alias_cache_test.go @@ -75,17 +75,11 @@ func TestEnsureTaskAliases_PersistsAliasesAndTracksAccess(t *testing.T) { dir := t.TempDir() t.Setenv("XDG_CACHE_HOME", dir) - oldNow := nowTaskAliasCache - oldRoot := taskAliasCacheRoot - nowTaskAliasCache = func() time.Time { return time.Date(2026, 3, 26, 12, 0, 0, 0, time.UTC) } - taskAliasCacheRoot = func() (string, error) { return filepath.Join(dir, "hexai"), nil } - defer func() { - nowTaskAliasCache = oldNow - taskAliasCacheRoot = oldRoot - }() + now := time.Date(2026, 3, 26, 12, 0, 0, 0, time.UTC) + deps := testTaskAliasCacheDeps(dir, &now) tasks := []TaskExport{{UUID: "uuid-1"}, {UUID: "uuid-2"}} - aliases, err := ensureTaskAliases(tasks) + aliases, err := deps.ensureTaskAliases(tasks) if err != nil { t.Fatalf("ensureTaskAliases returned error: %v", err) } @@ -93,7 +87,7 @@ func TestEnsureTaskAliases_PersistsAliasesAndTracksAccess(t *testing.T) { t.Fatalf("aliases = %#v, want sequential aliases", aliases) } - path, err := taskAliasCachePath() + path, err := deps.taskAliasCachePath() if err != nil { t.Fatalf("taskAliasCachePath: %v", err) } @@ -105,8 +99,8 @@ func TestEnsureTaskAliases_PersistsAliasesAndTracksAccess(t *testing.T) { t.Fatalf("len(Entries) = %d, want 2", len(cache.Entries)) } - nowTaskAliasCache = func() time.Time { return time.Date(2026, 3, 27, 12, 0, 0, 0, time.UTC) } - aliases, err = ensureTaskAliases([]TaskExport{{UUID: "uuid-2"}, {UUID: "uuid-3"}}) + now = time.Date(2026, 3, 27, 12, 0, 0, 0, time.UTC) + aliases, err = deps.ensureTaskAliases([]TaskExport{{UUID: "uuid-2"}, {UUID: "uuid-3"}}) if err != nil { t.Fatalf("ensureTaskAliases second call returned error: %v", err) } @@ -119,24 +113,18 @@ func TestEnsureTaskAliases_PersistsAliasesAndTracksAccess(t *testing.T) { t.Fatalf("NextID after second call = %d, want 3", cache.NextID) } entry := findTaskAliasEntry(t, cache, "uuid-2") - if got := entry.LastAccessedAt; !got.Equal(nowTaskAliasCache()) { - t.Fatalf("LastAccessedAt = %s, want %s", got, nowTaskAliasCache()) + if got := entry.LastAccessedAt; !got.Equal(now) { + t.Fatalf("LastAccessedAt = %s, want %s", got, now) } } func TestEnsureTaskAliases_PrunesExpiredEntriesWithoutReusingIDs(t *testing.T) { dir := t.TempDir() - oldNow := nowTaskAliasCache - oldRoot := taskAliasCacheRoot - nowTaskAliasCache = func() time.Time { return time.Date(2026, 3, 26, 12, 0, 0, 0, time.UTC) } - taskAliasCacheRoot = func() (string, error) { return filepath.Join(dir, "hexai"), nil } - defer func() { - nowTaskAliasCache = oldNow - taskA