diff options
| -rwxr-xr-x | cmd/ask/ask | bin | 4321591 -> 4322459 bytes | |||
| -rw-r--r-- | internal/askcli/dispatch_test.go | 12 | ||||
| -rw-r--r-- | internal/askcli/task_alias_cache.go | 33 | ||||
| -rw-r--r-- | internal/askcli/task_alias_cache_test.go | 18 | ||||
| -rw-r--r-- | internal/version.go | 2 |
5 files changed, 53 insertions, 12 deletions
diff --git a/cmd/ask/ask b/cmd/ask/ask Binary files differindex a7c597a..cffc885 100755 --- a/cmd/ask/ask +++ b/cmd/ask/ask diff --git a/internal/askcli/dispatch_test.go b/internal/askcli/dispatch_test.go index ebd7273..079d156 100644 --- a/internal/askcli/dispatch_test.go +++ b/internal/askcli/dispatch_test.go @@ -57,6 +57,18 @@ func TestDispatcher_UnknownSubcommand(t *testing.T) { } 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 + }() + 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" { t.Fatalf("args = %v, want pending export", args) diff --git a/internal/askcli/task_alias_cache.go b/internal/askcli/task_alias_cache.go index 3ea5dff..21967d8 100644 --- a/internal/askcli/task_alias_cache.go +++ b/internal/askcli/task_alias_cache.go @@ -107,7 +107,10 @@ func taskAliasCachePath() (string, error) { if err != nil { return "", fmt.Errorf("resolve cache dir: %w", err) } - return filepath.Join(dir, "ask", "task-aliases-v1.json"), nil + // v2 uses reversed alias strings (e.g. "10" instead of "01") so that the + // first character varies more often, improving shell auto-completion. The + // old v1 file is intentionally abandoned so the mapping starts fresh. + return filepath.Join(dir, "ask", "task-aliases-v2.json"), nil } func (c *taskAliasCache) validate() error { @@ -273,6 +276,21 @@ func (e taskAliasCacheEntry) lastTouchedAt() time.Time { return e.CreatedAt } +// reverseString returns s with its bytes in reverse order. Alias strings only +// ever contain ASCII characters so byte-level reversal is correct. +func reverseString(s string) string { + b := []byte(s) + for i, j := 0, len(b)-1; i < j; i, j = i+1, j-1 { + b[i], b[j] = b[j], b[i] + } + return string(b) +} + +// encodeTaskAliasID converts a monotonically-increasing counter to a short +// alphanumeric string and then reverses it. The reversal ensures that the +// first character of the alias varies as quickly as possible, which makes +// shell tab-completion more effective (e.g. "1", "2", ... "z", "00" becomes +// "1", "2", ..., "z", "00"; then "10", "20", ... instead of "00", "10", ...). func encodeTaskAliasID(id uint64) string { const alphabet = "0123456789abcdefghijklmnopqrstuvwxyz" @@ -290,9 +308,14 @@ func encodeTaskAliasID(id uint64) string { buf[i] = alphabet[remaining%uint64(len(alphabet))] remaining /= uint64(len(alphabet)) } - return string(buf) + // Reverse so that the least-significant digit comes first, keeping the + // leading character diverse across consecutive IDs. + return reverseString(string(buf)) } +// decodeTaskAliasID is the inverse of encodeTaskAliasID. It reverses the alias +// string to restore the canonical (most-significant-digit-first) form before +// decoding. func decodeTaskAliasID(alias string) (uint64, bool) { const alphabet = "0123456789abcdefghijklmnopqrstuvwxyz" @@ -300,7 +323,9 @@ func decodeTaskAliasID(alias string) (uint64, bool) { return 0, false } - width := len(alias) + // Reverse the alias back to the canonical form before decoding. + canonical := reverseString(alias) + width := len(canonical) var id uint64 blockSize := uint64(len(alphabet)) for i := 1; i < width; i++ { @@ -309,7 +334,7 @@ func decodeTaskAliasID(alias string) (uint64, bool) { } var value uint64 - for _, r := range alias { + for _, r := range canonical { index := int64(-1) for i, candidate := range alphabet { if r == candidate { diff --git a/internal/askcli/task_alias_cache_test.go b/internal/askcli/task_alias_cache_test.go index f792585..92d528b 100644 --- a/internal/askcli/task_alias_cache_test.go +++ b/internal/askcli/task_alias_cache_test.go @@ -11,6 +11,8 @@ import ( func TestEncodeTaskAliasID(t *testing.T) { t.Parallel() + // Aliases are stored in reversed form so that the first character varies + // quickly across consecutive IDs, improving shell tab-completion. tests := []struct { id uint64 want string @@ -20,12 +22,12 @@ func TestEncodeTaskAliasID(t *testing.T) { {id: 10, want: "a"}, {id: 35, want: "z"}, {id: 36, want: "00"}, - {id: 37, want: "01"}, - {id: 71, want: "0z"}, - {id: 72, want: "10"}, + {id: 37, want: "10"}, + {id: 71, want: "z0"}, + {id: 72, want: "01"}, {id: 1331, want: "zz"}, {id: 1332, want: "000"}, - {id: 1333, want: "001"}, + {id: 1333, want: "100"}, } for _, tc := range tests { @@ -38,6 +40,8 @@ func TestEncodeTaskAliasID(t *testing.T) { func TestDecodeTaskAliasID(t *testing.T) { t.Parallel() + // Aliases are in reversed form (matching encodeTaskAliasID output), so + // decodeTaskAliasID must reverse them back before decoding. tests := []struct { alias string want uint64 @@ -46,7 +50,7 @@ func TestDecodeTaskAliasID(t *testing.T) { {alias: "0", want: 0, ok: true}, {alias: "z", want: 35, ok: true}, {alias: "00", want: 36, ok: true}, - {alias: "01", want: 37, ok: true}, + {alias: "10", want: 37, ok: true}, {alias: "zz", want: 1331, ok: true}, {alias: "000", want: 1332, ok: true}, {alias: "", ok: false}, @@ -163,8 +167,8 @@ func TestEnsureTaskAliases_PrunesExpiredEntriesWithoutReusingIDs(t *testing.T) { if aliases["fresh"] != "00" { t.Fatalf("fresh alias = %q, want 00", aliases["fresh"]) } - if aliases["new-task"] != "01" { - t.Fatalf("new-task alias = %q, want 01", aliases["new-task"]) + if aliases["new-task"] != "10" { + t.Fatalf("new-task alias = %q, want 10", aliases["new-task"]) } cache = readTaskAliasCacheForTest(t, path) diff --git a/internal/version.go b/internal/version.go index c2ffb03..b9f06e0 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.28.1" +const Version = "0.28.2" |
