feat: all tests

This commit is contained in:
verse91
2026-05-09 23:18:08 +07:00
parent be3c08171b
commit 17f33ff480
16 changed files with 460 additions and 43 deletions
+1 -1
View File
@@ -87,7 +87,7 @@ func FileGenerator(filters ...string) GeneratorFunc {
continue
}
if filePrefix != "" && !hasPrefix(name, filePrefix) {
if filePrefix != "" && !HasPrefix(name, filePrefix) {
continue
}
+20 -20
View File
@@ -6,11 +6,11 @@ import (
"github.com/versenilvis/iris/integration/shell"
)
var shellAliases = make(map[string]string)
var ShellAliases = map[string]string{}
func GetAlias(name string) (string, bool) {
target, ok := shellAliases[name]
return target, ok
val, ok := ShellAliases[name]
return val, ok
}
// Lookup finds matching suggestions for your input by looking at how many words you typed
@@ -19,16 +19,16 @@ func GetAlias(name string) (string, bool) {
// e.g. Lookup("git checkout ") -> suggests branch names via generator
func Lookup(input string) []Suggestion {
if shell.Current != nil {
shellAliases = shell.Current.ScanAliases()
ShellAliases = shell.Current.ScanAliases()
} else {
shellAliases = make(map[string]string)
ShellAliases = make(map[string]string)
}
if input == "" {
return nil
}
tokens := tokenize(input)
tokens := Tokenize(input)
if len(tokens) == 1 && tokens[0] == "" {
return nil
@@ -43,8 +43,8 @@ func Lookup(input string) []Suggestion {
// if you have an alias in your shell config like: alias gca="git commit -a"
// if the first word match it, IRIS will suggest "git commit -a"
if len(tokens) > 1 {
if target, ok := shellAliases[tokens[0]]; ok {
aliasTokens := tokenize(target)
if target, ok := ShellAliases[tokens[0]]; ok {
aliasTokens := Tokenize(target)
if len(aliasTokens) > 0 && aliasTokens[len(aliasTokens)-1] == "" {
aliasTokens = aliasTokens[:len(aliasTokens)-1]
}
@@ -56,7 +56,7 @@ func Lookup(input string) []Suggestion {
query := tokens[0]
results := topLevelSuggestions(query)
if spec, exists := registry[query]; exists {
if spec, exists := Registry[query]; exists {
hasTrailingSpace := query != "" && query[len(query)-1] == ' '
if hasTrailingSpace {
@@ -82,7 +82,7 @@ func Lookup(input string) []Suggestion {
}
rootCmdName := tokens[0]
spec, exists := registry[rootCmdName]
spec, exists := Registry[rootCmdName]
if !exists {
return nil
}
@@ -174,7 +174,7 @@ func Lookup(input string) []Suggestion {
genResults := currentGen(tokens, prefix, partial)
for _, g := range genResults {
if partial != "" && !hasPrefix(g.Cmd, partial) && !strings.Contains(g.Cmd, partial) {
if partial != "" && !HasPrefix(g.Cmd, partial) && !strings.Contains(g.Cmd, partial) {
continue
}
@@ -193,7 +193,7 @@ func Lookup(input string) []Suggestion {
finalCmd = strings.TrimSpace(linePrefix) + " " + suggested
}
newTokens := tokenize(finalCmd)
newTokens := Tokenize(finalCmd)
if len(newTokens) > 0 {
lastToken := newTokens[len(newTokens)-1]
isDuplicate := false
@@ -218,7 +218,7 @@ func Lookup(input string) []Suggestion {
if allowMoreArgs {
for _, sub := range currentSubs {
if partial == "" || hasPrefix(sub.Name, partial) {
if partial == "" || HasPrefix(sub.Name, partial) {
results = append(results, Suggestion{
Cmd: prefix + " " + sub.Name, Desc: sub.Description, Icon: rootCmdName,
})
@@ -234,7 +234,7 @@ func Lookup(input string) []Suggestion {
}
}
for _, opt := range currentOpts {
if !usedOpts[opt.Name] && (partial == "" || hasPrefix(opt.Name, partial)) {
if !usedOpts[opt.Name] && (partial == "" || HasPrefix(opt.Name, partial)) {
results = append(results, Suggestion{
Cmd: prefix + " " + opt.Name, Desc: opt.Description, Icon: rootCmdName,
})
@@ -248,8 +248,8 @@ func Lookup(input string) []Suggestion {
func topLevelSuggestions(query string) []Suggestion {
results, seen := []Suggestion{}, make(map[string]bool)
for name, target := range shellAliases {
if !seen[name] && (query == "" || hasPrefix(name, query)) {
for name, target := range ShellAliases {
if !seen[name] && (query == "" || HasPrefix(name, query)) {
results = append(results, Suggestion{
Cmd: target, Desc: "alias: " + name, Icon: "root",
})
@@ -257,16 +257,16 @@ func topLevelSuggestions(query string) []Suggestion {
}
}
for name, spec := range registry {
for name, spec := range Registry {
if seen[name] {
continue
}
match := false
if query == "" || hasPrefix(name, query) {
if query == "" || HasPrefix(name, query) {
match = true
} else {
for _, a := range spec.Aliases {
if hasPrefix(a, query) {
if HasPrefix(a, query) {
match = true
break
}
@@ -279,7 +279,7 @@ func topLevelSuggestions(query string) []Suggestion {
}
for name := range pathCmds {
if !seen[name] && (query == "" || hasPrefix(name, query)) {
if !seen[name] && (query == "" || HasPrefix(name, query)) {
results = append(results, Suggestion{
Cmd: name, Desc: "system command", Icon: "root",
})
+3 -3
View File
@@ -37,10 +37,10 @@ type Suggestion struct {
Icon string
}
var registry = map[string]*Spec{}
var Registry = map[string]*Spec{}
// Register adds a new spec to the global registry
// Register adds a new spec to the global Registry
// example: Register(&Spec{Name: "git"})
func Register(s *Spec) {
registry[s.Name] = s
Registry[s.Name] = s
}
+6 -6
View File
@@ -14,9 +14,9 @@ func debugLog(format string, a ...interface{}) {
}
}
// tokenize parses the input string into shell-like tokens handling quotes
// example: tokenize("git commit -m \"hello world\"")
func tokenize(s string) []string {
// SplitAliasTokens parses the input string into shell-like tokens handling quotes
// example: SplitAliasTokens("git commit -m \"hello world\"")
func Tokenize(s string) []string {
tokens := []string{}
var current strings.Builder
inQuote := false
@@ -43,8 +43,8 @@ func tokenize(s string) []string {
return tokens
}
// hasPrefix checks if s starts with prefix using case-insensitive matching
func hasPrefix(s, prefix string) bool {
// HasPrefix checks if s starts with prefix using case-insensitive matching
func HasPrefix(s, prefix string) bool {
if len(prefix) > len(s) {
return false
}
@@ -65,5 +65,5 @@ func hasPrefix(s, prefix string) bool {
// CI = case insensitive
func HasPrefixCI(s, prefix string) bool {
return hasPrefix(s, prefix)
return HasPrefix(s, prefix)
}
+3 -3
View File
@@ -19,17 +19,17 @@ func init() {
Name: "z",
Description: "jump to directory",
MaxArgs: 0,
Generator: zoxideGenerator(),
Generator: ZoxideGenerator(),
})
core.Register(&core.Spec{
Name: "zi",
Description: "jump to directory interactively",
MaxArgs: 0,
Generator: zoxideGenerator(),
Generator: ZoxideGenerator(),
})
}
func zoxideGenerator() core.GeneratorFunc {
func ZoxideGenerator() core.GeneratorFunc {
return func(tokens []string, prefix string, partial string) []core.Suggestion {
fullQuery := strings.Join(tokens[1:], " ")
localSuggestions := core.FileGenerator("/")(tokens, prefix, fullQuery)
+6 -6
View File
@@ -43,7 +43,7 @@ func (b *BashAdapter) PrepareSelectSequence(selected string) []byte {
return append([]byte{0x15}, []byte(selected)...)
}
func (b *BashAdapter) ScanAliases() map[string]string {
return scanPosixAliases([]string{".bashrc", ".bash_profile", ".bash_aliases"})
return ScanPosixAliases([]string{".bashrc", ".bash_profile", ".bash_aliases"})
}
// ZshAdapter implementation
@@ -58,7 +58,7 @@ func (z *ZshAdapter) PrepareSelectSequence(selected string) []byte {
return append([]byte{0x15}, []byte(selected)...)
}
func (z *ZshAdapter) ScanAliases() map[string]string {
return scanPosixAliases([]string{".zshrc", ".zshenv", ".zprofile"})
return ScanPosixAliases([]string{".zshrc", ".zshenv", ".zprofile"})
}
// FishAdapter implementation
@@ -74,10 +74,10 @@ func (f *FishAdapter) PrepareSelectSequence(selected string) []byte {
}
func (f *FishAdapter) ScanAliases() map[string]string {
// fish uses 'alias' command in config.fish or separate function files
return scanPosixAliases([]string{filepath.Join(".config", "fish", "config.fish")})
return ScanPosixAliases([]string{filepath.Join(".config", "fish", "config.fish")})
}
func scanPosixAliases(files []string) map[string]string {
func ScanPosixAliases(files []string) map[string]string {
aliases := make(map[string]string)
home, err := os.UserHomeDir()
if err != nil {
@@ -105,7 +105,7 @@ func scanPosixAliases(files []string) map[string]string {
continue
}
pairs := splitAliasTokens(body)
pairs := SplitAliasTokens(body)
for _, pair := range pairs {
eqIdx := strings.IndexByte(pair, '=')
if eqIdx < 0 {
@@ -122,7 +122,7 @@ func scanPosixAliases(files []string) map[string]string {
return aliases
}
func splitAliasTokens(s string) []string {
func SplitAliasTokens(s string) []string {
var tokens []string
var cur strings.Builder
inQuote := false
+4
View File
@@ -33,3 +33,7 @@ debug:
copy:
@rm ~/.local/bin/iris
@cp ./iris ~/.local/bin/iris
# run all tests
[group('dev')]
test:
@go test ./... -v
+1 -1
View File
@@ -10,7 +10,7 @@ import (
// mergeResults collects and dedupes suggestions for a query and mode
// example: mergeResults("git ", "spec")
func mergeResults(query string, mode string) []core.Suggestion {
func MergeResults(query string, mode string) []core.Suggestion {
if query == "" && mode != "history" {
debugLog("[Merge] Query empty, returning nil")
return nil
+3 -3
View File
@@ -207,7 +207,7 @@ func runWrapper() {
continue
}
results := mergeResults(query, "spec")
results := MergeResults(query, "spec")
if len(results) == 0 {
os.Stdout.Write([]byte(overlay.ClearAndDisable()))
continue
@@ -263,7 +263,7 @@ func runWrapper() {
var b strings.Builder
if !navCopy {
debugLog("[Render] query: '%s', mode: %s", bufCopy, modeCopy)
results := mergeResults(bufCopy, modeCopy)
results := MergeResults(bufCopy, modeCopy)
debugLog("[Render] results found: %d", len(results))
if len(results) == 0 {
@@ -378,7 +378,7 @@ func runWrapper() {
mode = "history"
saveMode(mode)
results := mergeResults("", "history")
results := MergeResults("", "history")
if len(results) > 0 {
limit := 100
if len(results) < limit {
+61
View File
@@ -0,0 +1,61 @@
package core_test
import (
"os"
"path/filepath"
"strings"
"testing"
"github.com/versenilvis/iris/commands/core"
)
func TestFileGenerator(t *testing.T) {
// Create a temp directory structure
tmp := t.TempDir()
os.Mkdir(filepath.Join(tmp, ".git"), 0755)
os.Mkdir(filepath.Join(tmp, "src"), 0755)
os.Mkdir(filepath.Join(tmp, "docs"), 0755)
os.WriteFile(filepath.Join(tmp, "src", "main.go"), []byte("package main"), 0644)
os.WriteFile(filepath.Join(tmp, "src", "utils.go"), []byte("package core"), 0644)
os.WriteFile(filepath.Join(tmp, "README.md"), []byte("# Readme"), 0644)
// Helper to change CWD for test
oldWd, _ := os.Getwd()
os.Chdir(tmp)
defer os.Chdir(oldWd)
t.Run("dirOnly shows only dirs", func(t *testing.T) {
gen := core.FileGenerator("/")
results := gen([]string{"cd", ""}, "cd ", "")
for _, r := range results {
if r.Desc != "directory" {
t.Errorf("FileGenerator(\"/\") suggested a non-directory: %s", r.Cmd)
}
}
})
t.Run("Filter extension", func(t *testing.T) {
gen := core.FileGenerator(".go")
results := gen([]string{"go", "src/"}, "go src/", "src/")
foundMain := false
for _, r := range results {
if r.Cmd == "src/main.go" {
foundMain = true
}
}
if !foundMain {
t.Errorf("FileGenerator(\".go\") did not suggest src/main.go")
}
})
t.Run("Hidden files are skipped", func(t *testing.T) {
gen := core.FileGenerator()
results := gen([]string{"ls", ""}, "ls ", "")
for _, r := range results {
if strings.HasPrefix(filepath.Base(r.Cmd), ".") {
t.Errorf("FileGenerator suggested hidden file: %s", r.Cmd)
}
}
})
}
+73
View File
@@ -0,0 +1,73 @@
package core_test
import (
"testing"
"github.com/versenilvis/iris/commands/core"
"github.com/versenilvis/iris/integration/shell"
)
type mockAdapter struct {
shell.BashAdapter
}
func (m *mockAdapter) ScanAliases() map[string]string {
return map[string]string{
"gca": "git commit -a",
"ta": "tmux a -t",
}
}
func TestLookup(t *testing.T) {
// Use mock adapter
shell.Current = &mockAdapter{}
core.Register(&core.Spec{
Name: "git",
Description: "git command",
Subcommands: []core.Subcommand{
{Name: "commit", Description: "commit changes"},
{Name: "remote", Description: "manage remotes", Subcommands: []core.Subcommand{
{Name: "add", Description: "add remote"},
}},
},
Options: []core.Option{
{Name: "--verbose", Description: "verbose output"},
},
})
tests := []struct {
name string
input string
minCount int
checkCmd string
}{
{"Top-level suggestions", "gi", 1, "git"},
{"Subcommand suggestions", "git ", 2, "git commit"},
{"Alias expansion", "gca", 1, "git commit -a"},
{"Deep subcommand", "git remote ", 1, "git remote add"},
{"Option dedup", "git --verbose -", 0, ""},
{"Flag with value ignore", "git --output=json ", 2, "git --output=json commit"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
results := core.Lookup(tt.input)
if len(results) < tt.minCount {
t.Errorf("Lookup(%q) returned %d results; want at least %d", tt.input, len(results), tt.minCount)
}
if tt.checkCmd != "" {
found := false
for _, r := range results {
if r.Cmd == tt.checkCmd {
found = true
break
}
}
if !found {
t.Errorf("Lookup(%q) did not suggest %q", tt.input, tt.checkCmd)
}
}
})
}
}
+55
View File
@@ -0,0 +1,55 @@
package core_test
import (
"reflect"
"testing"
"github.com/versenilvis/iris/commands/core"
)
func TestTokenize(t *testing.T) {
tests := []struct {
input string
expected []string
}{
{"", []string{""}},
{"git ", []string{"git", ""}},
{"git add", []string{"git", "add"}},
{"git commit -m \"hello world\"", []string{"git", "commit", "-m", "hello world"}},
{"git commit -m \"hello", []string{"git", "commit", "-m", "hello"}},
{"git commit -m 'hello world'", []string{"git", "commit", "-m", "hello world"}},
{"git commit -m 'hello", []string{"git", "commit", "-m", "hello"}},
{"ls -l \"file name\"", []string{"ls", "-l", "file name"}},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
got := core.Tokenize(tt.input)
if !reflect.DeepEqual(got, tt.expected) {
t.Errorf("Tokenize(%q) = %v; want %v", tt.input, got, tt.expected)
}
})
}
}
func TestHasPrefix(t *testing.T) {
tests := []struct {
s string
prefix string
want bool
}{
{"Hello", "hel", true},
{"Thử nghiệm", "thử", true},
{"Iris", "Iris-Longer", false},
{"Iris", "", true},
{"", "a", false},
}
for _, tt := range tests {
t.Run(tt.s+"_"+tt.prefix, func(t *testing.T) {
if got := core.HasPrefix(tt.s, tt.prefix); got != tt.want {
t.Errorf("HasPrefix(%q, %q) = %v; want %v", tt.s, tt.prefix, got, tt.want)
}
})
}
}
+58
View File
@@ -0,0 +1,58 @@
package fs_test
import (
"os"
"path/filepath"
"strings"
"testing"
"github.com/versenilvis/iris/commands/fs"
)
func TestZoxideGenerator(t *testing.T) {
// Create a mock zoxide binary
tmp := t.TempDir()
mockZoxide := filepath.Join(tmp, "zoxide")
// Script that prints mock directories
script := "#!/bin/sh\necho \"/home/verse/project1\n/home/verse/docs\n/home/verse/dev/iris\""
os.WriteFile(mockZoxide, []byte(script), 0755)
// Add tmp to PATH
oldPath := os.Getenv("PATH")
os.Setenv("PATH", tmp+string(os.PathListSeparator)+oldPath)
defer os.Setenv("PATH", oldPath)
gen := fs.ZoxideGenerator()
t.Run("Query returns correct result when partial is empty", func(t *testing.T) {
results := gen([]string{"z", ""}, "z ", "")
if len(results) == 0 {
t.Errorf("Expected results from zoxide history, got 0")
}
})
t.Run("Path replaces home dir with ~", func(t *testing.T) {
home, _ := os.UserHomeDir()
results := gen([]string{"z", ""}, "z ", "")
foundHome := false
for _, r := range results {
if strings.HasPrefix(r.Desc, "~") {
foundHome = true
break
}
}
if !foundHome && home != "" {
t.Logf("Warning: Did not find ~ in descriptions, home is %s", home)
}
})
t.Run("Sort by descending score", func(t *testing.T) {
results := gen([]string{"z", "i"}, "z ", "i")
if len(results) >= 1 {
if results[0].Cmd == "" {
t.Errorf("Empty result command")
}
}
})
}
+54
View File
@@ -0,0 +1,54 @@
package tests
import (
"os"
"path/filepath"
"strings"
"testing"
"github.com/versenilvis/iris/commands/core"
_ "github.com/versenilvis/iris/commands/fs" // Register z command
)
func TestIntegration_ZoxideMultiWord(t *testing.T) {
// Create mock directory with spaces
tmp := t.TempDir()
targetDir := filepath.Join(tmp, "My Awesome Project")
os.MkdirAll(targetDir, 0755)
// Mock shell environment
oldWd, _ := os.Getwd()
os.Chdir(tmp)
defer os.Chdir(oldWd)
// Mock zoxide binary
mockBinDir := t.TempDir()
mockZoxide := filepath.Join(mockBinDir, "zoxide")
script := "#!/bin/sh\necho \"" + targetDir + "\""
os.WriteFile(mockZoxide, []byte(script), 0755)
oldPath := os.Getenv("PATH")
os.Setenv("PATH", mockBinDir+string(os.PathListSeparator)+oldPath)
defer os.Setenv("PATH", oldPath)
t.Run("z matches multi-word folder without quotes", func(t *testing.T) {
// Simulating user typing "z My Awe"
input := "z My Awe"
results := core.Lookup(input)
found := false
for _, r := range results {
// Expected result should be "z My Awesome Project/"
if strings.Contains(r.Cmd, "My Awesome Project") {
found = true
// Check for word duplication bug: "z My My Awesome..."
if strings.Contains(r.Cmd, "My My") {
t.Errorf("Word duplication detected in suggestion: %s", r.Cmd)
}
}
}
if !found {
t.Errorf("Could not find 'My Awesome Project' in suggestions for %q", input)
}
})
}
+50
View File
@@ -0,0 +1,50 @@
package root_test
import (
"os"
"path/filepath"
"testing"
"github.com/versenilvis/iris/commands/core"
"github.com/versenilvis/iris/root"
)
func TestMergeResults(t *testing.T) {
// Setup mock Registry
core.Registry = make(map[string]*core.Spec)
core.Register(&core.Spec{
Name: "ls",
Description: "list files",
})
// Setup mock history file
tmp := t.TempDir()
histFile := filepath.Join(tmp, ".bash_history")
os.WriteFile(histFile, []byte("ls -l\ncd /tmp\ngit status\n"), 0644)
oldHome := os.Getenv("HOME")
os.Setenv("HOME", tmp)
defer os.Setenv("HOME", oldHome)
t.Run("History mode returns history items", func(t *testing.T) {
results := root.MergeResults("", "history")
if len(results) == 0 {
t.Errorf("MergeResults history mode returned 0 items")
}
})
t.Run("Spec mode returns command results and dedups", func(t *testing.T) {
core.Register(&core.Spec{Name: "git", Description: "git"})
results := root.MergeResults("gi", "spec")
foundGit := false
for _, r := range results {
if r.Cmd == "git" {
foundGit = true
}
}
if !foundGit {
t.Errorf("MergeResults spec mode did not find 'git'")
}
})
}
+62
View File
@@ -0,0 +1,62 @@
package shell_test
import (
"os"
"path/filepath"
"reflect"
"testing"
"github.com/versenilvis/iris/integration/shell"
)
func TestSplitAliasTokens(t *testing.T) {
tests := []struct {
input string
expected []string
}{
{"g='git commit'", []string{"g='git commit'"}},
{"a=b c=d", []string{"a=b", "c=d"}},
{"ta='tmux a -t' l='ls -l'", []string{"ta='tmux a -t'", "l='ls -l'"}},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
got := shell.SplitAliasTokens(tt.input)
if !reflect.DeepEqual(got, tt.expected) {
t.Errorf("SplitAliasTokens(%q) = %v; want %v", tt.input, got, tt.expected)
}
})
}
}
func TestScanPosixAliases(t *testing.T) {
tmp := t.TempDir()
aliasFile := filepath.Join(tmp, ".bashrc")
content := `
# some comments
alias g='git'
alias gca='git commit -a'
alias multi="a" b="c"
`
os.WriteFile(aliasFile, []byte(content), 0644)
oldHome := os.Getenv("HOME")
os.Setenv("HOME", tmp)
defer os.Setenv("HOME", oldHome)
aliases := shell.ScanPosixAliases([]string{".bashrc"})
expected := map[string]string{
"g": "git",
"gca": "git commit -a",
"multi": "a",
"b": "c",
}
for k, v := range expected {
if aliases[k] != v {
t.Errorf("Expected alias %s=%s, got %s", k, v, aliases[k])
}
}
}