summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
Diffstat (limited to 'internal')
-rw-r--r--internal/askcli/dispatch_test.go12
-rw-r--r--internal/askcli/task_alias_cache.go33
-rw-r--r--internal/askcli/task_alias_cache_test.go18
-rw-r--r--internal/version.go2
4 files changed, 53 insertions, 12 deletions
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"