feat(spec): support tool aliases (#107)

closes #57

IRIS now automatically read and suggest internal configuration aliases
for Git (`.gitconfig`, `.git/config`) and Cargo (`.cargo/config.toml`)

- Added the `Provider` interface and extended registry for CLI tools
that define aliases
- Git: parse aliases via `git config --show-scope`, supporting raw
tab/space separation and scope differentiation
- Cargo: traverse the directory tree upwards to root to read the
`.cargo/config.toml` configuration
- 0ms hot-path caching: key caching is calculated entirely via `os.Stat`
mtime (no subprocess spawn) combined with a 2-second TTL window to
ensure 0ms latency when typing
- Lookup integration: display alias suggestions at the subcommand level
and automatically expand aliases as the user enters further arguments
(ignore shell pipeline commands starting with `!`)
This commit is contained in:
VERSE
2026-08-02 14:30:46 +07:00
committed by GitHub
parent 476ca3946b
commit 6faa2e3274
10 changed files with 732 additions and 2 deletions
+159
View File
@@ -6,6 +6,8 @@ import (
"strings"
"sync"
"testing"
"github.com/versenilvis/iris/spec/alias"
)
func TestLookup(t *testing.T) {
@@ -231,3 +233,160 @@ func TestLookup_NestedDirectoryTrailingSpace(t *testing.T) {
t.Errorf("expected hello.txt in results, got %v", results)
}
}
type mockGitProvider struct{}
func (m *mockGitProvider) ToolName() string { return "git" }
func (m *mockGitProvider) GetAliases(cwd string) []alias.AliasEntry {
return []alias.AliasEntry{
{
Name: "co",
Expansion: "checkout",
Scope: "global",
},
{
Name: "recent",
Expansion: "!git for-each-ref --sort=committerdate --format='%(committerdate:relative) %(refname:short)' refs/heads/ | tail -10",
Scope: "global",
},
{
Name: "local-alias",
Expansion: "status",
Scope: "local",
},
}
}
func TestLookup_GitRecentAlias(t *testing.T) {
ResetRegistry()
Register(&Spec{
Name: "git",
Subcommands: []Subcommand{
{Name: "commit"},
{Name: "checkout", Options: []Option{{Name: "-b"}}},
},
})
alias.Register(&mockGitProvider{})
t.Cleanup(alias.Reset)
// Test Type 1: Standard subcommand alias 'git co' -> 'git checkout'
resultsCo := Lookup("git co ")
foundCo := false
for _, r := range resultsCo {
if strings.Contains(r.Cmd, "-b") {
foundCo = true
break
}
}
if !foundCo {
t.Errorf("expected '-b' option after expanding 'git co ', got %v", resultsCo)
}
// Test Type 2: Shell pipeline alias 'git rec' -> 'git recent'
resultsRec := Lookup("git rec")
foundRec := false
for _, r := range resultsRec {
if strings.Contains(r.Cmd, "git recent") {
foundRec = true
if r.Priority != 70 {
t.Errorf("expected global alias priority 70, got %d", r.Priority)
}
break
}
}
if !foundRec {
t.Errorf("expected 'git recent' suggestion for 'git rec', got %v", resultsRec)
}
// Test Local Scope Priority (Priority = 85)
resultsLocal := Lookup("git local")
foundLocal := false
for _, r := range resultsLocal {
if strings.Contains(r.Cmd, "git local-alias") {
foundLocal = true
if r.Priority != 85 {
t.Errorf("expected local alias priority 85, got %d", r.Priority)
}
break
}
}
if !foundLocal {
t.Errorf("expected 'git local-alias' suggestion, got %v", resultsLocal)
}
// Test Type 2 expansion with trailing space: 'git recent ' (shell pipeline alias starting with !) should be skipped and return nil
resultsRecentSpace := Lookup("git recent ")
if len(resultsRecentSpace) != 0 {
t.Errorf("expected 0 suggestions for shell pipeline alias 'git recent ', got %v", resultsRecentSpace)
}
}
func TestLookup_InvalidSubcommandAfterAliasExpansion(t *testing.T) {
ResetRegistry()
Register(&Spec{
Name: "git",
Subcommands: []Subcommand{
{Name: "commit"},
{Name: "status", Subcommands: []Subcommand{{Name: "porcelain"}}},
},
})
alias.Register(&mockGitProvider{})
// token beyond the alias expansion must be validated against the expanded subcommand
results := Lookup("git local-alias badcmd ")
if len(results) != 0 {
t.Errorf("expected 0 suggestions for invalid subcommand after alias expansion, got %v", results)
}
// expansion itself is still allowed even when it is not a registered subcommand
resultsOk := Lookup("git co ")
if len(resultsOk) == 0 {
t.Errorf("expected suggestions for expanded alias 'git co ', got none")
}
}
func TestLookup_RealGitProvider(t *testing.T) {
ResetRegistry()
Register(&Spec{
Name: "git",
})
alias.Reset()
// Isolate from the developer's real git configuration by pointing HOME at a temp dir
home := t.TempDir()
t.Setenv("HOME", home)
if err := os.WriteFile(filepath.Join(home, ".gitconfig"), []byte("[alias]\nrecent = for-each-ref --sort=committerdate refs/heads\n"), 0644); err != nil {
t.Fatal(err)
}
cwd, _ := os.Getwd()
provider := &alias.GitProvider{}
aliases := provider.GetAliases(cwd)
t.Logf("Real Git aliases: %v", aliases)
foundAlias := false
for _, a := range aliases {
if a.Name == "recent" {
foundAlias = true
break
}
}
if !foundAlias {
t.Skipf("git unavailable or could not read isolated config, got aliases: %v", aliases)
}
alias.Register(provider)
t.Cleanup(alias.Reset)
results := Lookup("git rec")
t.Logf("Lookup('git rec') results: %v", results)
found := false
for _, r := range results {
if strings.Contains(r.Cmd, "recent") {
found = true
break
}
}
if !found {
t.Errorf("expected 'git recent' in results for real GitProvider, got %v", results)
}
}