diff options
| author | Paul Buetow <paul@buetow.org> | 2026-06-18 08:05:32 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-06-18 08:05:32 +0300 |
| commit | a80ad2b4691d0df63f68c6977ee18444e7bb752f (patch) | |
| tree | 6edc47fcc3c929856826432cf3df3394a14934e8 | |
| parent | 4ffb22e7f69f1c9c79b095d4e60bad3d97aac55b (diff) | |
ik0 replace remaining test seams with DI
42 files changed, 452 insertions, 597 deletions
diff --git a/cmd/hexai-mcp-server/main.go b/cmd/hexai-mcp-server/main.go index 03150bb..741e310 100644 --- a/cmd/hexai-mcp-server/main.go +++ b/cmd/hexai-mcp-server/main.go @@ -25,13 +25,6 @@ func buildOverrides(opts mcpOptions) hexaimcp.MCPOverrides { } } -// Seams for testing: override in tests to avoid launching real MCP server. -// Signatures match hexaimcp.Run and hexaimcp.RunBackfill respectively. -var ( - runMCP = hexaimcp.Run - runBackfill = hexaimcp.RunBackfill -) - // deprecationWarning is the notice runMain emits on every startup so users // see this binary is experimental. Kept as a constant (not printf'd) so // tests can assert on its contents directly. @@ -65,6 +58,18 @@ type mcpOptions struct { showVersion bool } +type mcpDeps struct { + runMCP func(context.Context, string, string, hexaimcp.MCPOverrides, io.Reader, io.Writer, io.Writer) error + runBackfill func(context.Context, string, string, hexaimcp.MCPOverrides) error +} + +func defaultMCPDeps() mcpDeps { + return mcpDeps{ + runMCP: hexaimcp.Run, + runBackfill: hexaimcp.RunBackfill, + } +} + func main() { os.Exit(runMain(os.Args[1:], os.Stdin, os.Stdout, os.Stderr)) } // runMain prints the deprecation warning, parses flags, and delegates to @@ -73,6 +78,10 @@ func main() { os.Exit(runMain(os.Args[1:], os.Stdin, os.Stdout, os.Stderr)) } // Pulling this out of main keeps it testable without touching package-level // flag state. func runMain(args []string, stdin io.Reader, stdout, stderr io.Writer) int { + return runMainWithDeps(args, stdin, stdout, stderr, defaultMCPDeps()) +} + +func runMainWithDeps(args []string, stdin io.Reader, stdout, stderr io.Writer, deps mcpDeps) int { fmt.Fprint(stderr, deprecationWarning) defaultLog, err := defaultLogPath() @@ -107,7 +116,7 @@ func runMain(args []string, stdin io.Reader, stdout, stderr io.Writer) int { ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer stop() - if err := run(ctx, opts, stdin, stdout, stderr); err != nil { + if err := runWithDeps(ctx, opts, stdin, stdout, stderr, deps); err != nil { fmt.Fprintf(stderr, "error: %v\n", err) return 1 } @@ -118,6 +127,10 @@ func runMain(args []string, stdin io.Reader, stdout, stderr io.Writer) int { // CLI flag values are passed via MCPOverrides instead of environment variables. // ctx is threaded into the server/backfill so they stop on shutdown signals. func run(ctx context.Context, opts mcpOptions, stdin io.Reader, stdout, stderr io.Writer) error { + return runWithDeps(ctx, opts, stdin, stdout, stderr, defaultMCPDeps()) +} + +func runWithDeps(ctx context.Context, opts mcpOptions, stdin io.Reader, stdout, stderr io.Writer, deps mcpDeps) error { if opts.showVersion { fmt.Fprintln(stdout, internal.Version) return nil @@ -127,10 +140,10 @@ func run(ctx context.Context, opts mcpOptions, stdin io.Reader, stdout, stderr i // Handle backfill operation if opts.syncAll { - return runBackfill(ctx, opts.logPath, opts.configPath, overrides) + return deps.runBackfill(ctx, opts.logPath, opts.configPath, overrides) } - return runMCP(ctx, opts.logPath, opts.configPath, overrides, stdin, stdout, stderr) + return deps.runMCP(ctx, opts.logPath, opts.configPath, overrides, stdin, stdout, stderr) } // defaultLogPath returns the default MCP log file path in the state directory. diff --git a/cmd/hexai-mcp-server/main_test.go b/cmd/hexai-mcp-server/main_test.go index b2a3895..ded0be7 100644 --- a/cmd/hexai-mcp-server/main_test.go +++ b/cmd/hexai-mcp-server/main_test.go @@ -66,12 +66,10 @@ func TestBuildOverrides(t *testing.T) { } func TestRun_SyncAll(t *testing.T) { - old := runBackfill - t.Cleanup(func() { runBackfill = old }) - var gotLog, gotConfig string var gotOverrides hexaimcp.MCPOverrides - runBackfill = func(_ context.Context, logPath, configPath string, overrides hexaimcp.MCPOverrides) error { + deps := defaultMCPDeps() + deps.runBackfill = func(_ context.Context, logPath, configPath string, overrides hexaimcp.MCPOverrides) error { gotLog = logPath gotConfig = configPath gotOverrides = overrides @@ -86,7 +84,7 @@ func TestRun_SyncAll(t *testing.T) { slashCommandSync: true, slashCommandDir: "/tmp/cmds", } - if err := run(context.Background(), opts, nil, nil, nil); err != nil { + if err := runWithDeps(context.Background(), opts, nil, nil, nil, deps); err != nil { t.Fatalf("run syncAll: %v", err) } if gotLog != "/tmp/test.log" { @@ -107,30 +105,26 @@ func TestRun_SyncAll(t *testing.T) { } func TestRun_SyncAllError(t *testing.T) { - old := runBackfill - t.Cleanup(func() { runBackfill = old }) - wantErr := errors.New("backfill failed") - runBackfill = func(_ context.Context, _, _ string, _ hexaimcp.MCPOverrides) error { return wantErr } + deps := defaultMCPDeps() + deps.runBackfill = func(_ context.Context, _, _ string, _ hexaimcp.MCPOverrides) error { return wantErr } opts := mcpOptions{syncAll: true} - if err := run(context.Background(), opts, nil, nil, nil); !errors.Is(err, wantErr) { + if err := runWithDeps(context.Background(), opts, nil, nil, nil, deps); !errors.Is(err, wantErr) { t.Fatalf("expected backfill error, got: %v", err) } } func TestRun_MCPServer(t *testing.T) { - old := runMCP - t.Cleanup(func() { runMCP = old }) - called := false - runMCP = func(_ context.Context, logPath, configPath string, overrides hexaimcp.MCPOverrides, stdin io.Reader, stdout, stderr io.Writer) error { + deps := defaultMCPDeps() + deps.runMCP = func(_ context.Context, logPath, configPath string, overrides hexaimcp.MCPOverrides, stdin io.Reader, stdout, stderr io.Writer) error { called = true return nil } opts := mcpOptions{logPath: "/tmp/mcp.log"} - if err := run(context.Background(), opts, nil, nil, nil); err != nil { + if err := runWithDeps(context.Background(), opts, nil, nil, nil, deps); err != nil { t.Fatalf("run MCP: %v", err) } if !called { @@ -139,15 +133,13 @@ func TestRun_MCPServer(t *testing.T) { } func TestRun_MCPServerError(t *testing.T) { - old := runMCP - t.Cleanup(func() { runMCP = old }) - wantErr := errors.New("server failed") - runMCP = func(_ context.Context, _, _ string, _ hexaimcp.MCPOverrides, _ io.Reader, _, _ io.Writer) error { + deps := defaultMCPDeps() + deps.runMCP = func(_ context.Context, _, _ string, _ hexaimcp.MCPOverrides, _ io.Reader, _, _ io.Writer) error { return wantErr } - if err := run(context.Background(), mcpOptions{}, nil, nil, nil); !errors.Is(err, wantErr) { + if err := runWithDeps(context.Background(), mcpOptions{}, nil, nil, nil, deps); !errors.Is(err, wantErr) { t.Fatalf("expected server error, got: %v", err) } } @@ -172,17 +164,15 @@ func TestRunMain_VersionFlag(t *testing.T) { // runMain --sync-all path: forwards parsed options to runBackfill and // returns 0 on success. func TestRunMain_SyncAllSuccess(t *testing.T) { - old := runBackfill - t.Cleanup(func() { runBackfill = old }) - var gotLog string - runBackfill = func(_ context.Context, logPath string, _ string, _ hexaimcp.MCPOverrides) error { + deps := defaultMCPDeps() + deps.runBackfill = func(_ context.Context, logPath string, _ string, _ hexaimcp.MCPOverrides) error { gotLog = logPath return nil } var stdout, stderr bytes.Buffer - code := runMain([]string{"-sync-all", "-log", "/tmp/sync.log"}, nil, &stdout, &stderr) + code := runMainWithDeps([]string{"-sync-all", "-log", "/tmp/sync.log"}, nil, &stdout, &stderr, deps) if code != 0 { t.Fatalf("runMain code = %d, want 0; stderr=%q", code, stderr.String()) } @@ -194,14 +184,13 @@ func TestRunMain_SyncAllSuccess(t *testing.T) { // runMain run-error path: when the underlying server fails, runMain must // return 1 (the production exit code) and write the error to stderr. func TestRunMain_ServerErrorReturnsOne(t *testing.T) { - old := runMCP - t.Cleanup(func() { runMCP = old }) - runMCP = func(context.Context, string, string, hexaimcp.MCPOverrides, io.Reader, io.Writer, io.Writer) error { + deps := defaultMCPDeps() + deps.runMCP = func(context.Context, string, string, hexaimcp.MCPOverrides, io.Reader, io.Writer, io.Writer) error { return errors.New("mcp boom") } var stdout, stderr bytes.Buffer - code := runMain(nil, nil, &stdout, &stderr) + code := runMainWithDeps(nil, nil, &stdout, &stderr, deps) if code != 1 { t.Fatalf("runMain code = %d, want 1", code) } @@ -212,15 +201,14 @@ func TestRunMain_ServerErrorReturnsOne(t *testing.T) { // Bad flag must yield exit 2 without ever invoking the server stub. func TestRunMain_BadFlagReturnsTwo(t *testing.T) { - old := runMCP - t.Cleanup(func() { runMCP = old }) called := false - runMCP = func(context.Context, string, string, hexaimcp.MCPOverrides, io.Reader, io.Writer, io.Writer) error { + deps := defaultMCPDeps() + deps.runMCP = func(context.Context, string, string, hexaimcp.MCPOverrides, io.Reader, io.Writer, io.Writer) error { called = true return nil } var stdout, stderr bytes.Buffer - code := runMain([]string{"--bogus"}, nil, &stdout, &stderr) + code := runMainWithDeps([]string{"--bogus"}, nil, &stdout, &stderr, deps) if code != 2 { t.Fatalf("runMain code = %d, want 2", code) } 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) { @@ |
