package fs import ( "context" "errors" "os" "path/filepath" "slices" "testing" "time" "github.com/versenilvis/iris/spec" ) func TestZoxideSourceCachesDirectoriesAndErrors(t *testing.T) { now := time.Date(2026, time.August, 14, 12, 0, 0, 0, time.UTC) calls := 0 source := newZoxideSource(func(context.Context) ([]byte, error) { calls++ return []byte("/tmp/Project One \n/tmp/Проект\r\n"), nil }) source.now = func() time.Time { return now } want := []string{"/tmp/Project One ", "/tmp/Проект"} for range 2 { got, err := source.directories() if err != nil { t.Fatalf("directories returned an error: %v", err) } if !slices.Equal(got, want) { t.Fatalf("directories = %q, want %q", got, want) } } if calls != 1 { t.Fatalf("query calls = %d, want 1", calls) } now = now.Add(zoxideCacheTTL) if _, err := source.directories(); err != nil { t.Fatalf("directories after cache expiry returned an error: %v", err) } if calls != 2 { t.Fatalf("query calls after cache expiry = %d, want 2", calls) } wantErr := errors.New("zoxide unavailable") errorCalls := 0 errorSource := newZoxideSource(func(context.Context) ([]byte, error) { errorCalls++ return nil, wantErr }) if _, err := errorSource.directories(); !errors.Is(err, wantErr) { t.Fatalf("directories error = %v, want %v", err, wantErr) } if _, err := errorSource.directories(); !errors.Is(err, wantErr) { t.Fatalf("cached directories error = %v, want %v", err, wantErr) } if errorCalls != 1 { t.Fatalf("failed query calls = %d, want 1", errorCalls) } } func TestZoxideGeneratorMergesValidDirectories(t *testing.T) { cwd := t.TempDir() local := filepath.Join(cwd, "project") external := filepath.Join(t.TempDir(), "My Проект") missing := filepath.Join(t.TempDir(), "deleted") mustMkdirAll(t, local) mustMkdirAll(t, external) previousCWD := spec.GetCWD() spec.SetCWD(cwd) t.Cleanup(func() { spec.SetCWD(previousCWD) }) gen := newZoxideGenerator(func() ([]string, error) { return []string{local, external, external, missing}, nil }) results := gen([]string{"z", ""}, "z ", "") if len(results) != 2 { t.Fatalf("suggestion count = %d, want 2: %#v", len(results), results) } if results[0].Cmd != "project/" { t.Fatalf("first suggestion = %q, want local project/", results[0].Cmd) } if results[1].Cmd != external { t.Fatalf("second suggestion = %q, want %q", results[1].Cmd, external) } if results[1].Priority != 40 { t.Fatalf("external priority = %d, want 40", results[1].Priority) } } func TestZoxideGeneratorFiltersAndFallsBack(t *testing.T) { cwd := t.TempDir() nested := filepath.Join(cwd, "nested", "child") mustMkdirAll(t, nested) previousCWD := spec.GetCWD() spec.SetCWD(cwd) t.Cleanup(func() { spec.SetCWD(previousCWD) }) calls := 0 gen := newZoxideGenerator(func() ([]string, error) { calls++ return nil, errors.New("zoxide unavailable") }) results := gen([]string{"z", "nested/ch"}, "z ", "nested/ch") if calls != 0 { t.Fatalf("source calls for explicit path = %d, want 0", calls) } if len(results) != 1 || results[0].Cmd != "nested/child/" { t.Fatalf("explicit path suggestions = %#v", results) } results = gen([]string{"z", "n"}, "z ", "n") if calls != 1 { t.Fatalf("source calls after name query = %d, want 1", calls) } if len(results) != 1 || results[0].Cmd != "nested/" { t.Fatalf("fallback suggestions = %#v", results) } empty := newZoxideGenerator(func() ([]string, error) { return nil, nil }) if results := empty([]string{"z", "missing"}, "z ", "missing"); len(results) != 0 { t.Fatalf("empty database suggestions = %#v, want none", results) } } func TestZoxideGeneratorFuzzyMatchesUnicodeAndLookupQuotesSpaces(t *testing.T) { cwd := t.TempDir() target := filepath.Join(t.TempDir(), "My Awesome $Проект") mustMkdirAll(t, target) previousCWD := spec.GetCWD() spec.SetCWD(cwd) t.Cleanup(func() { spec.SetCWD(previousCWD) }) const command = "zoxide-test-command" spec.Register(&spec.Spec{ Name: command, MaxArgs: 0, Generator: newZoxideGenerator(func() ([]string, error) { return []string{target}, nil }), }) t.Cleanup(func() { delete(spec.Registry, command) }) results := spec.Lookup(command + " My Awe") want := command + ` '` + target + `'` if len(results) != 1 || results[0].Cmd != want { t.Fatalf("lookup suggestions = %#v, want command %q", results, want) } } func TestDisplayDirectoryOnlyShortensHomePrefix(t *testing.T) { home := filepath.Join(string(os.PathSeparator), "Users", "example") inside := filepath.Join(home, "Developer", "Проект") outside := filepath.Join(string(os.PathSeparator), "tmp", "Users", "example", "data") if got := displayDirectory(home, home); got != "~" { t.Fatalf("displayDirectory(home) = %q, want ~", got) } if got := displayDirectory(inside, home); got != filepath.Join("~", "Developer", "Проект") { t.Fatalf("displayDirectory(inside) = %q", got) } if got := displayDirectory(outside, home); got != outside { t.Fatalf("displayDirectory(outside) = %q, want %q", got, outside) } } func mustMkdirAll(t *testing.T, path string) { t.Helper() if err := os.MkdirAll(path, 0o755); err != nil { t.Fatalf("create directory %q: %v", path, err) } }