diff options
| -rw-r--r-- | integrationtests/ask_test.go | 2 | ||||
| -rw-r--r-- | internal/askcli/command_complete_uuids_test.go | 21 | ||||
| -rw-r--r-- | internal/askcli/task_alias_cache.go | 6 | ||||
| -rw-r--r-- | internal/askcli/task_alias_cache_test.go | 18 | ||||
| -rw-r--r-- | internal/version.go | 2 |
5 files changed, 37 insertions, 12 deletions
diff --git a/integrationtests/ask_test.go b/integrationtests/ask_test.go index 0ce36fe..0ebdb01 100644 --- a/integrationtests/ask_test.go +++ b/integrationtests/ask_test.go @@ -272,7 +272,7 @@ func mustTaskAlias(t *testing.T, ctx context.Context, uuid string) string { func aliasCachePath(t *testing.T, cacheRoot string) string { t.Helper() - return filepath.Join(cacheRoot, "hexai", "ask", "task-aliases-v1.json") + return filepath.Join(cacheRoot, "hexai", "ask", "task-aliases-v2.json") } // cleanupOrphanedIntegrationTasks deletes any tasks with the +integrationtest diff --git a/internal/askcli/command_complete_uuids_test.go b/internal/askcli/command_complete_uuids_test.go index 442e0a8..92bf244 100644 --- a/internal/askcli/command_complete_uuids_test.go +++ b/internal/askcli/command_complete_uuids_test.go @@ -80,7 +80,7 @@ func TestHandleCompleteUUIDs_ParseError(t *testing.T) { } } -func TestHandleCompleteUUIDs_WarnsOnInvalidAliasCache(t *testing.T) { +func TestHandleCompleteUUIDs_RecoverFromCorruptAliasCache(t *testing.T) { dir := t.TempDir() oldRoot := taskAliasCacheRoot taskAliasCacheRoot = func() (string, error) { return filepath.Join(dir, "hexai"), nil } @@ -93,6 +93,9 @@ func TestHandleCompleteUUIDs_WarnsOnInvalidAliasCache(t *testing.T) { if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { t.Fatalf("MkdirAll: %v", err) } + // Simulate a corrupted cache file (e.g. two JSON objects concatenated from a + // concurrent write race). The handler must recover by resetting the cache and + // assigning fresh aliases rather than erroring or degrading to UUID-only output. if err := os.WriteFile(path, []byte("{bad"), 0o600); err != nil { t.Fatalf("WriteFile: %v", err) } @@ -110,12 +113,18 @@ func TestHandleCompleteUUIDs_WarnsOnInvalidAliasCache(t *testing.T) { if code != 0 { t.Fatalf("handleCompleteUUIDs code = %d, want 0", code) } - // When alias cache is unavailable, output UUID with description (tab-separated). - if got := stdout.String(); got != "uuid-1\tFallback task\n" { - t.Fatalf("stdout = %q, want UUID-only fallback list with description", got) + // After recovery a fresh alias (e.g. "0") must be assigned, so the output + // includes both the short alias and the UUID (fish shows whichever the user + // types). No warning should appear on stderr. + got := stdout.String() + if !strings.Contains(got, "uuid-1\tFallback task") { + t.Fatalf("stdout = %q, want UUID with description", got) } - if !strings.Contains(stderr.String(), "failed to update task alias cache") { - t.Fatalf("stderr = %q, want cache warning", stderr.String()) + if !strings.Contains(got, "Fallback task") { + t.Fatalf("stdout = %q, want task description in output", got) + } + if stderr.Len() != 0 { + t.Fatalf("stderr = %q, want no warnings after graceful recovery", stderr.String()) } } diff --git a/internal/askcli/task_alias_cache.go b/internal/askcli/task_alias_cache.go index 21967d8..e89dbb5 100644 --- a/internal/askcli/task_alias_cache.go +++ b/internal/askcli/task_alias_cache.go @@ -91,7 +91,11 @@ func loadTaskAliasCache() (taskAliasCache, string, error) { var cache taskAliasCache if err := json.Unmarshal(data, &cache); err != nil { - return taskAliasCache{}, "", fmt.Errorf("parse task alias cache: %w", err) + // Cache file is unreadable (e.g. truncated write or duplicate-write + // corruption). Discard and start fresh — tasks will get new alias IDs on + // the next run, which is preferable to a hard failure. + _ = os.Remove(path) + return taskAliasCache{}, path, nil } if err := cache.validate(); err != nil { return taskAliasCache{}, "", fmt.Errorf("validate task alias cache: %w", err) diff --git a/internal/askcli/task_alias_cache_test.go b/internal/askcli/task_alias_cache_test.go index 92d528b..f93a762 100644 --- a/internal/askcli/task_alias_cache_test.go +++ b/internal/askcli/task_alias_cache_test.go @@ -230,7 +230,7 @@ func TestEnsureTaskAliases_DoesNotPruneEntriesAt120DayBoundary(t *testing.T) { } } -func TestEnsureTaskAliases_InvalidCacheReturnsError(t *testing.T) { +func TestEnsureTaskAliases_CorruptedCacheIsResetGracefully(t *testing.T) { dir := t.TempDir() oldRoot := taskAliasCacheRoot @@ -244,12 +244,24 @@ func TestEnsureTaskAliases_InvalidCacheReturnsError(t *testing.T) { if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { t.Fatalf("MkdirAll: %v", err) } + // Write a corrupted cache (e.g. two JSON objects concatenated due to a + // concurrent write race). The function must recover gracefully by resetting + // the cache instead of returning an error. if err := os.WriteFile(path, []byte("{not-json"), 0o600); err != nil { t.Fatalf("WriteFile: %v", err) } - if _, err := ensureTaskAliases([]TaskExport{{UUID: "uuid-1"}}); err == nil { - t.Fatal("expected error for invalid cache file") + aliases, err := ensureTaskAliases([]TaskExport{{UUID: "uuid-1"}}) + if err != nil { + t.Fatalf("expected graceful recovery from corrupted cache, got error: %v", err) + } + // A fresh alias should have been assigned after the corrupt file was discarded. + if aliases["uuid-1"] == "" { + t.Fatal("expected alias to be assigned after cache reset") + } + // The corrupt file should have been removed and replaced with a valid one. + if _, statErr := os.Stat(path); os.IsNotExist(statErr) { + t.Fatal("expected a new cache file to be written after reset") } } diff --git a/internal/version.go b/internal/version.go index f2804f2..56cd339 100644 --- a/internal/version.go +++ b/internal/version.go @@ -1,4 +1,4 @@ // Package internal provides the Hexai semantic version identifier used by CLI and LSP binaries. package internal -const Version = "0.29.0" +const Version = "0.29.1" |
