refactor: better project structure (#35)

This project is starting to grow, and I've seen flaws in the initial
project structure design
I think I'll restructure the entire project directory for better
scalability and also cleaner
This commit is contained in:
VERSE
2026-07-11 16:15:05 +07:00
committed by GitHub
parent 94c8af7b1d
commit 1e04949242
52 changed files with 339 additions and 445 deletions
+1
View File
@@ -7,3 +7,4 @@ task
docs/code
docs/plan
todo.txt
scratch/
@@ -1,9 +1,7 @@
package tests
package integration
import (
"testing"
"github.com/versenilvis/iris/integration"
)
func TestComputeCursorCol(t *testing.T) {
@@ -41,7 +39,7 @@ func TestComputeCursorCol(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := integration.ComputeCursorCol(tt.data)
got := ComputeCursorCol(tt.data)
if got != tt.want {
t.Errorf("ComputeCursorCol(%q) = %d, want %d", tt.data, got, tt.want)
}
+2 -3
View File
@@ -7,11 +7,10 @@ import (
"strings"
"sync"
"unicode/utf8"
"github.com/charmbracelet/lipgloss"
"github.com/versenilvis/iris/internal/config"
"github.com/versenilvis/iris/internal/logger"
"github.com/versenilvis/iris/spec"
"github.com/versenilvis/iris/config"
"github.com/versenilvis/iris/logger"
"golang.org/x/term"
)
@@ -1,15 +1,14 @@
package tests
package integration
import (
"strings"
"testing"
"github.com/versenilvis/iris/integration"
"github.com/versenilvis/iris/spec"
)
func TestRenderGhostText_CursorAtEnd(t *testing.T) {
o := integration.NewOverlay()
o := NewOverlay()
items := []spec.Suggestion{
{Cmd: "git checkout -b feature"},
}
@@ -35,7 +34,7 @@ func TestRenderGhostText_CursorAtEnd(t *testing.T) {
}
func TestGetGhostText(t *testing.T) {
o := integration.NewOverlay()
o := NewOverlay()
items := []spec.Suggestion{
{Cmd: "docker exec -it my-container bash"},
}
@@ -63,7 +62,7 @@ func TestGetGhostText(t *testing.T) {
}
func TestGhostText_MenuSync(t *testing.T) {
o := integration.NewOverlay()
o := NewOverlay()
items := []spec.Suggestion{
{Cmd: "git checkout -b first"},
{Cmd: "git checkout master"},
@@ -90,7 +89,7 @@ func TestGhostText_MenuSync(t *testing.T) {
}
func TestGhostText_Truncation(t *testing.T) {
o := integration.NewOverlay()
o := NewOverlay()
longCmd := "git commit -m '" + strings.Repeat("a", 150) + "'"
items := []spec.Suggestion{
{Cmd: longCmd},
@@ -107,7 +106,7 @@ func TestGhostText_Truncation(t *testing.T) {
}
func TestHideMenu_PreservesTypedQueryForAI(t *testing.T) {
o := integration.NewOverlay()
o := NewOverlay()
o.HideMenu("git commit")
if o.GetTypedQuery() != "git commit" {
@@ -1,10 +1,8 @@
package tests
package shell
import (
"reflect"
"testing"
"github.com/versenilvis/iris/integration/shell"
)
func TestScanPosixAliases(t *testing.T) {
@@ -22,7 +20,7 @@ alias l='ls' ll='ls -l'
"ll": "ls -l",
}
got := shell.ParseAliases(input)
got := ParseAliases(input)
if !reflect.DeepEqual(got, expected) {
t.Errorf("ScanPosixAliases() = %v; want %v", got, expected)
}
@@ -43,7 +41,7 @@ func TestSplitAliasTokens(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := shell.SplitAliasTokens(tt.input)
got := SplitAliasTokens(tt.input)
if !reflect.DeepEqual(got, tt.expected) {
t.Errorf("SplitAliasTokens(%q) = %v; want %v", tt.input, got, tt.expected)
}
+1 -1
View File
@@ -10,7 +10,7 @@ import (
"strings"
"time"
"github.com/versenilvis/iris/config"
"github.com/versenilvis/iris/internal/config"
"github.com/versenilvis/iris/spec"
)
@@ -1,4 +1,4 @@
package tests
package ai
import (
"context"
@@ -9,8 +9,7 @@ import (
"testing"
"time"
"github.com/versenilvis/iris/ai"
"github.com/versenilvis/iris/config"
"github.com/versenilvis/iris/internal/config"
)
func TestCleanSuggestion(t *testing.T) {
@@ -28,7 +27,7 @@ func TestCleanSuggestion(t *testing.T) {
}
for _, tt := range tests {
got := ai.CleanSuggestion(tt.input)
got := CleanSuggestion(tt.input)
if got != tt.expected {
t.Errorf("CleanSuggestion(%q) = %q, want %q", tt.input, got, tt.expected)
}
@@ -84,13 +83,13 @@ func TestOpenAIClient_Suggest(t *testing.T) {
},
}
client, err := ai.NewClient(cfg)
client, err := NewClient(cfg)
if err != nil {
t.Fatalf("failed to create client: %v", err)
}
ctx := context.Background()
env := ai.EnvSnapshot{Cwd: "/home/user", LastCmd: "kubectl get", LastExitCode: 0}
env := EnvSnapshot{Cwd: "/home/user", LastCmd: "kubectl get", LastExitCode: 0}
sugg, err := client.Suggest(ctx, "kubectl get p", env, "")
if err != nil {
t.Fatalf("suggest failed: %v", err)
@@ -119,9 +118,9 @@ func TestOpenAIClient_TimeoutAndCancel(t *testing.T) {
TimeoutMS: 50,
}
client := ai.NewOpenAIClient(cfg)
client := NewOpenAIClient(cfg)
ctx := context.Background()
env := ai.EnvSnapshot{}
env := EnvSnapshot{}
_, err := client.Suggest(ctx, "sleep", env, "")
if err == nil {
@@ -1,4 +1,4 @@
package tests
package ai
import (
"context"
@@ -9,7 +9,6 @@ import (
"testing"
"time"
"github.com/versenilvis/iris/ai"
"github.com/versenilvis/iris/spec"
)
@@ -34,7 +33,7 @@ func (m *mockProvider) Gather(ctx context.Context) (string, error) {
}
func TestProviderCache_TTL(t *testing.T) {
cache := ai.NewProviderCache(50 * time.Millisecond)
cache := NewProviderCache(50 * time.Millisecond)
provider := &mockProvider{
name: "test-prov",
matchPref: "test",
@@ -71,7 +70,7 @@ func TestAIEngine_DynamicContext(t *testing.T) {
gatherRet: "test-container\tnginx",
}
engine := ai.NewAIEngine(func(ctx context.Context, buf string, env ai.EnvSnapshot, dynamicCtx string) (*spec.Suggestion, error) {
engine := NewAIEngine(func(ctx context.Context, buf string, env EnvSnapshot, dynamicCtx string) (*spec.Suggestion, error) {
if dynamicCtx != "test-container\tnginx" {
t.Fatalf("expected dynamicCtx to be passed to handler, got: %q", dynamicCtx)
}
@@ -80,7 +79,7 @@ func TestAIEngine_DynamicContext(t *testing.T) {
engine.RegisterProvider(provider)
ctx := context.Background()
sugg, err := engine.Suggest(ctx, "docker exec ", ai.EnvSnapshot{}, "")
sugg, err := engine.Suggest(ctx, "docker exec ", EnvSnapshot{}, "")
if err != nil || sugg == nil {
t.Fatalf("expected suggestion, got err: %v, sugg: %+v", err, sugg)
}
@@ -91,10 +90,11 @@ func TestAIEngine_DynamicContext(t *testing.T) {
// Verify that cache evicts expired entries and resets when exceeding 50 items to prevent unbounded memory growth
func TestProviderCache_Eviction(t *testing.T) {
cache := ai.NewProviderCache(10 * time.Millisecond)
cache := NewProviderCache(15 * time.Millisecond)
ctx := context.Background()
for i := 0; i < 55; i++ {
// 1. Fill cache to capacity (50 items) rapidly before any TTL expires
for i := 0; i < 50; i++ {
p := &mockProvider{
name: fmt.Sprintf("prov-%d", i),
matchPref: "test",
@@ -103,19 +103,63 @@ func TestProviderCache_Eviction(t *testing.T) {
cache.GetOrGather(ctx, p)
}
time.Sleep(20 * time.Millisecond)
cache.mu.Lock()
if len(cache.entries) != 50 {
t.Fatalf("expected cache to hold exactly 50 entries, got %d", len(cache.entries))
}
cache.mu.Unlock()
// 2. Add the 51st item immediately when none are expired -> triggers map reset to bound memory
p51 := &mockProvider{
name: "prov-50",
matchPref: "test",
gatherRet: "data",
}
cache.GetOrGather(ctx, p51)
cache.mu.Lock()
if len(cache.entries) != 1 {
t.Fatalf("expected cache reset when exceeding 50 unexpired items (expected len 1, got %d)", len(cache.entries))
}
if _, ok := cache.entries["prov-50"]; !ok {
t.Fatalf("expected 'prov-50' to exist after reset")
}
cache.mu.Unlock()
// 3. Fill up to 50 items again
for i := 51; i < 100; i++ {
p := &mockProvider{
name: fmt.Sprintf("prov-%d", i),
matchPref: "test",
gatherRet: "data",
}
cache.GetOrGather(ctx, p)
}
// 4. Wait for all items to expire
time.Sleep(25 * time.Millisecond)
// 5. Add a new item when cache has 50 expired items -> should delete expired entries without complete map recreation
pNext := &mockProvider{
name: "prov-next",
name: "prov-100",
matchPref: "test",
gatherRet: "data",
}
cache.GetOrGather(ctx, pNext)
cache.mu.Lock()
if len(cache.entries) != 1 {
t.Fatalf("expected 50 expired items to be evicted before adding new entry (expected len 1, got %d)", len(cache.entries))
}
if _, ok := cache.entries["prov-100"]; !ok {
t.Fatalf("expected new entry 'prov-100' to exist after eviction")
}
cache.mu.Unlock()
}
// Verify that CommandContextProvider caps gathered output to 1000 characters to protect token budget
func TestCommandContextProvider_Truncation(t *testing.T) {
provider := &ai.CommandContextProvider{
provider := &CommandContextProvider{
NameStr: "test-trunc",
Prefixes: []string{"echo"},
GatherCmd: []string{"go", "env"},
@@ -136,7 +180,7 @@ func TestCommandContextProvider_Truncation(t *testing.T) {
// Verify that concurrent provider registration and context gathering do not cause data races
func TestAIEngine_ConcurrentRegistrationAndGather(t *testing.T) {
engine := ai.NewAIEngine(nil)
engine := NewAIEngine(nil)
ctx := context.Background()
var wg sync.WaitGroup
@@ -1,4 +1,4 @@
package tests
package ai
import (
"context"
@@ -9,7 +9,6 @@ import (
"testing"
"time"
"github.com/versenilvis/iris/ai"
"github.com/versenilvis/iris/spec"
)
@@ -18,43 +17,43 @@ type mockAISuggester struct {
ret *spec.Suggestion
}
func (m *mockAISuggester) SuggestOnEmpty(ctx context.Context, env ai.EnvSnapshot) (*spec.Suggestion, error) {
func (m *mockAISuggester) SuggestOnEmpty(ctx context.Context, env EnvSnapshot) (*spec.Suggestion, error) {
atomic.AddInt32(&m.calls, 1)
return m.ret, nil
}
func TestRuleBasedSuggester(t *testing.T) {
rule := ai.RuleBasedSuggester{}
ctx := context.Background()
rule := RuleBasedSuggester{}
// case 1: retry failed command
sugg1, _ := rule.SuggestOnEmpty(ctx, ai.EnvSnapshot{LastExitCode: 1, LastCmd: "make build"})
// Case 1: Last exit code != 0
ctx := context.Background()
sugg1, _ := rule.SuggestOnEmpty(ctx, EnvSnapshot{LastExitCode: 1, LastCmd: "make build"})
if sugg1 == nil || sugg1.Cmd != "make build" || sugg1.Confidence != 80 {
t.Fatalf("expected retry make build with conf 80, got: %+v", sugg1)
}
// case 2: git status after git status
sugg2, _ := rule.SuggestOnEmpty(ctx, ai.EnvSnapshot{LastCmd: "git status"})
sugg2, _ := rule.SuggestOnEmpty(ctx, EnvSnapshot{LastCmd: "git status"})
if sugg2 == nil || sugg2.Cmd != "git diff" || sugg2.Confidence != 75 {
t.Fatalf("expected git diff with conf 75, got: %+v", sugg2)
}
// case 3: modified git files
sugg3, _ := rule.SuggestOnEmpty(ctx, ai.EnvSnapshot{GitStatus: " M main.go"})
sugg3, _ := rule.SuggestOnEmpty(ctx, EnvSnapshot{GitStatus: " M main.go"})
if sugg3 == nil || sugg3.Cmd != "git status" || sugg3.Confidence != 70 {
t.Fatalf("expected git status with conf 70, got: %+v", sugg3)
}
// case 4: package.json signature (conf 65)
sugg4, _ := rule.SuggestOnEmpty(ctx, ai.EnvSnapshot{DirSignature: "package.json"})
sugg4, _ := rule.SuggestOnEmpty(ctx, EnvSnapshot{DirSignature: "package.json"})
if sugg4 == nil || sugg4.Cmd != "npm run dev" || sugg4.Confidence != 65 {
t.Fatalf("expected npm run dev with conf 65, got: %+v", sugg4)
}
}
func TestContextCache_ShouldCallAI(t *testing.T) {
cache := ai.NewContextCache()
snap := ai.EnvSnapshot{Cwd: "/test", GitStatus: "clean"}
cache := NewContextCache()
snap := EnvSnapshot{Cwd: "/test", GitStatus: "clean"}
// first call -> true
if !cache.ShouldCallAI(snap, 50*time.Millisecond) {
@@ -80,17 +79,16 @@ func TestContextCache_ShouldCallAI(t *testing.T) {
func TestEmptyLinePredictor_TwoTier(t *testing.T) {
ctx := context.Background()
mockAI := &mockAISuggester{ret: &spec.Suggestion{Cmd: "ai-suggested-cmd", Confidence: 85}}
predictor := ai.NewEmptyLinePredictor(nil, mockAI, 50*time.Millisecond)
predictor := NewEmptyLinePredictor(nil, mockAI, 50*time.Millisecond)
// case 1: rule based match >= 70 -> ai not called
env1 := ai.EnvSnapshot{LastExitCode: 1, LastCmd: "failed-cmd"}
env1 := EnvSnapshot{LastExitCode: 1, LastCmd: "failed-cmd"}
sugg1, _ := predictor.Predict(ctx, env1, true)
if sugg1 == nil || sugg1.Cmd != "failed-cmd" || atomic.LoadInt32(&mockAI.calls) != 0 {
t.Fatalf("expected rule based result and 0 ai calls, got sugg: %+v, calls: %d", sugg1, mockAI.calls)
}
// case 2: rule based conf < 70 -> ai called
env2 := ai.EnvSnapshot{DirSignature: "package.json"}
env2 := EnvSnapshot{DirSignature: "package.json"}
sugg2, _ := predictor.Predict(ctx, env2, true)
if sugg2 == nil || sugg2.Cmd != "ai-suggested-cmd" || atomic.LoadInt32(&mockAI.calls) != 1 {
t.Fatalf("expected ai result and 1 ai call, got sugg: %+v, calls: %d", sugg2, mockAI.calls)
@@ -110,7 +108,7 @@ func TestExtractScriptsAndTargets_Makefile(t *testing.T) {
_ = os.WriteFile(filepath.Join(tmp, "Makefile"), content, 0644)
var sb strings.Builder
ai.ExtractScriptsAndTargets(&sb, tmp, "")
ExtractScriptsAndTargets(&sb, tmp, "")
res := sb.String()
if !strings.Contains(res, "build") || !strings.Contains(res, "all") {
+2 -2
View File
@@ -6,8 +6,8 @@ import (
"sync"
"time"
"github.com/versenilvis/iris/config"
"github.com/versenilvis/iris/logger"
"github.com/versenilvis/iris/internal/config"
"github.com/versenilvis/iris/internal/logger"
"github.com/versenilvis/iris/spec"
)
@@ -1,4 +1,4 @@
package tests
package ai
import (
"context"
@@ -8,13 +8,12 @@ import (
"testing"
"time"
"github.com/versenilvis/iris/ai"
"github.com/versenilvis/iris/config"
"github.com/versenilvis/iris/internal/config"
"github.com/versenilvis/iris/spec"
)
func TestEnvSnapshot_Hash(t *testing.T) {
snap1 := ai.EnvSnapshot{
snap1 := EnvSnapshot{
Cwd: "/home/user",
LastCmd: "ls -l",
LastExitCode: 0,
@@ -33,8 +32,8 @@ func TestEnvSnapshot_Hash(t *testing.T) {
}
// Verify that fields containing delimiter characters do not collide
snapA := ai.EnvSnapshot{Cwd: "/home/user", LastCmd: "foo|bar"}
snapB := ai.EnvSnapshot{Cwd: "/home/user|foo", LastCmd: "bar"}
snapA := EnvSnapshot{Cwd: "/home/user", LastCmd: "foo|bar"}
snapB := EnvSnapshot{Cwd: "/home/user|foo", LastCmd: "bar"}
if snapA.Hash() == snapB.Hash() {
t.Fatalf("expected different hash for distinct snapshots containing delimiter characters")
}
@@ -65,21 +64,21 @@ func TestAIEngine_Suggest_Success(t *testing.T) {
},
}
engine := ai.NewAIEngine(nil)
engine := NewAIEngine(nil)
ctx := context.Background()
snap := ai.EnvSnapshot{GitStatus: "modified main.go"}
snap := EnvSnapshot{GitStatus: "modified main.go"}
sugg, err := engine.Suggest(ctx, "git commit -m \"", snap, "")
if err != nil {
t.Fatalf("expected success, got err: %v", err)
}
if sugg == nil || sugg.Confidence != 85 || sugg.Source != string(ai.SourceAI) {
if sugg == nil || sugg.Confidence != 85 || sugg.Source != string(SourceAI) {
t.Fatalf("unexpected suggestion: %+v", sugg)
}
}
func TestAIEngine_Suggest_Cancel(t *testing.T) {
engine := ai.NewAIEngine(func(ctx context.Context, buf string, env ai.EnvSnapshot, dynamicCtx string) (*spec.Suggestion, error) {
engine := NewAIEngine(func(ctx context.Context, buf string, env EnvSnapshot, dynamicCtx string) (*spec.Suggestion, error) {
time.Sleep(50 * time.Millisecond)
return &spec.Suggestion{Cmd: "test", Confidence: 90}, nil
})
@@ -87,7 +86,7 @@ func TestAIEngine_Suggest_Cancel(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
cancel() // cancel immediately before calling
sugg, err := engine.Suggest(ctx, "git commit -m \"", ai.EnvSnapshot{}, "")
sugg, err := engine.Suggest(ctx, "git commit -m \"", EnvSnapshot{}, "")
if err == nil || sugg != nil {
t.Fatalf("expected cancellation error and nil suggestion, got sugg: %+v, err: %v", sugg, err)
}
@@ -97,38 +96,38 @@ func TestShouldOverwrite(t *testing.T) {
newSugg := &spec.Suggestion{
Cmd: "git commit -m \"feat: new feature\"",
Confidence: 85,
Source: string(ai.SourceAI),
Source: string(SourceAI),
}
// case 1: exact match and higher confidence
if !ai.ShouldOverwrite("git commit -m \"", "git commit -m \"f", newSugg, 70) {
if !ShouldOverwrite("git commit -m \"", "git commit -m \"f", newSugg, 70) {
t.Fatalf("expected true for prefix match with higher confidence")
}
// case 2: user backspaced / typed different prefix
if ai.ShouldOverwrite("git commit -m \"", "git co", newSugg, 70) {
if ShouldOverwrite("git commit -m \"", "git co", newSugg, 70) {
t.Fatalf("expected false when current buf does not match original prefix")
}
// case 3: new suggestion does not match what user typed
if ai.ShouldOverwrite("git commit -m \"", "git commit -m \"fix", newSugg, 70) {
if ShouldOverwrite("git commit -m \"", "git commit -m \"fix", newSugg, 70) {
t.Fatalf("expected false when suggestion cmd does not match current buf")
}
// case 4: lower or equal confidence
if ai.ShouldOverwrite("git commit -m \"", "git commit -m \"", newSugg, 90) {
if ShouldOverwrite("git commit -m \"", "git commit -m \"", newSugg, 90) {
t.Fatalf("expected false when new confidence is lower")
}
}
// Verify that caller mutations on returned fresh suggestions do not corrupt the internal engine cache
func TestAIEngine_Suggest_Immutability(t *testing.T) {
engine := ai.NewAIEngine(func(ctx context.Context, buf string, env ai.EnvSnapshot, dynamicCtx string) (*spec.Suggestion, error) {
engine := NewAIEngine(func(ctx context.Context, buf string, env EnvSnapshot, dynamicCtx string) (*spec.Suggestion, error) {
return &spec.Suggestion{Cmd: "echo original", Confidence: 90}, nil
})
ctx := context.Background()
sugg1, err := engine.Suggest(ctx, "echo ", ai.EnvSnapshot{}, "")
sugg1, err := engine.Suggest(ctx, "echo ", EnvSnapshot{}, "")
if err != nil || sugg1 == nil {
t.Fatalf("expected suggestion, got err: %v, sugg: %v", err, sugg1)
}
@@ -137,7 +136,7 @@ func TestAIEngine_Suggest_Immutability(t *testing.T) {
sugg1.Cmd = "echo corrupted"
// Fetch from cache via prefix match
sugg2, err := engine.Suggest(ctx, "echo ", ai.EnvSnapshot{}, "")
sugg2, err := engine.Suggest(ctx, "echo ", EnvSnapshot{}, "")
if err != nil || sugg2 == nil {
t.Fatalf("expected cached suggestion, got err: %v, sugg: %v", err, sugg2)
}
@@ -1,18 +1,14 @@
package tests
package config
import (
"bytes"
"os"
"path/filepath"
"testing"
"time"
"github.com/versenilvis/iris/config"
"github.com/versenilvis/iris/root"
)
func TestDefaultConfigAndState(t *testing.T) {
cfg := config.DefaultConfig()
cfg := DefaultConfig()
if cfg.Core.Version != 1 {
t.Errorf("expected version 1, got %d", cfg.Core.Version)
}
@@ -31,7 +27,7 @@ func TestDefaultConfigAndState(t *testing.T) {
// test manual provider registration
cfg.AI.Provider = "custom"
cfg.AI.Providers = map[string]config.ProviderConfig{
cfg.AI.Providers = map[string]ProviderConfig{
"custom": {
InheritedFrom: "openai",
Endpoint: "https://custom-api.com/v1",
@@ -57,14 +53,14 @@ func TestDefaultConfigAndState(t *testing.T) {
t.Errorf("expected min interval 5000, got %d", cfg.AI.SuggestOnEmpty.MinIntervalMS)
}
state := config.DefaultState()
state := DefaultState()
if state.LastMode != "spec" {
t.Errorf("expected last mode spec, got %q", state.LastMode)
}
}
func TestCustomDuration(t *testing.T) {
var dur config.Duration
var dur Duration
err := dur.UnmarshalText([]byte("6h"))
if err != nil {
t.Fatalf("unexpected error unmarshalling duration: %v", err)
@@ -94,8 +90,7 @@ func TestValidationAndEnvironmentOverrides(t *testing.T) {
}
defer os.RemoveAll(tmpDir)
_ = os.Setenv("XDG_CONFIG_HOME", tmpDir)
defer func() { _ = os.Unsetenv("XDG_CONFIG_HOME") }()
t.Setenv("XDG_CONFIG_HOME", tmpDir)
configDir := filepath.Join(tmpDir, "iris")
if mkErr := os.MkdirAll(configDir, 0755); mkErr != nil {
@@ -117,33 +112,19 @@ model = "qwen-2.5-coder-32b"
t.Fatalf("failed to write config file: %v", wrErr)
}
_ = os.Setenv("IRIS_CORE_DEBUG", "true")
_ = os.Setenv("IRIS_CORE_SHELL", "fish")
_ = os.Setenv("IRIS_CORE_MODE", "history")
_ = os.Setenv("IRIS_UI_GHOST_TEXT", "false")
_ = os.Setenv("IRIS_UI_MAX_SUGGESTIONS", "250")
_ = os.Setenv("IRIS_UI_MAX_HEIGHT", "25")
_ = os.Setenv("IRIS_UPDATER_CHANNEL", "nightly")
_ = os.Setenv("IRIS_UPDATER_INTERVAL", "12h")
_ = os.Setenv("IRIS_UPDATER_CHECK_ON_STARTUP", "false")
_ = os.Setenv("IRIS_AI_PROVIDER", "ollama")
_ = os.Setenv("GROQ_API_KEY", "gsk_test_123")
t.Setenv("IRIS_CORE_DEBUG", "true")
t.Setenv("IRIS_CORE_SHELL", "fish")
t.Setenv("IRIS_CORE_MODE", "history")
t.Setenv("IRIS_UI_GHOST_TEXT", "false")
t.Setenv("IRIS_UI_MAX_SUGGESTIONS", "250")
t.Setenv("IRIS_UI_MAX_HEIGHT", "25")
t.Setenv("IRIS_UPDATER_CHANNEL", "nightly")
t.Setenv("IRIS_UPDATER_INTERVAL", "12h")
t.Setenv("IRIS_UPDATER_CHECK_ON_STARTUP", "false")
t.Setenv("IRIS_AI_PROVIDER", "ollama")
t.Setenv("GROQ_API_KEY", "gsk_test_123")
defer func() {
_ = os.Unsetenv("IRIS_CORE_DEBUG")
_ = os.Unsetenv("IRIS_CORE_SHELL")
_ = os.Unsetenv("IRIS_CORE_MODE")
_ = os.Unsetenv("IRIS_UI_GHOST_TEXT")
_ = os.Unsetenv("IRIS_UI_MAX_SUGGESTIONS")
_ = os.Unsetenv("IRIS_UI_MAX_HEIGHT")
_ = os.Unsetenv("IRIS_UPDATER_CHANNEL")
_ = os.Unsetenv("IRIS_UPDATER_INTERVAL")
_ = os.Unsetenv("IRIS_UPDATER_CHECK_ON_STARTUP")
_ = os.Unsetenv("IRIS_AI_PROVIDER")
_ = os.Unsetenv("GROQ_API_KEY")
}()
cfg, err := config.Load()
cfg, err := Load()
if err != nil {
t.Fatalf("failed to load config: %v", err)
}
@@ -183,8 +164,8 @@ model = "qwen-2.5-coder-32b"
t.Errorf("expected groq api key gsk_test_123 from env, got %q", groqCfg.GetAPIKey())
}
_ = os.Setenv("IRIS_CORE_MODE", "invalid")
_, err = config.Load()
t.Setenv("IRIS_CORE_MODE", "invalid")
_, err = Load()
if err == nil {
t.Errorf("expected validation error for invalid mode in env")
}
@@ -197,10 +178,9 @@ func TestLoadSave(t *testing.T) {
}
defer os.RemoveAll(tmpDir)
_ = os.Setenv("XDG_CONFIG_HOME", tmpDir)
defer func() { _ = os.Unsetenv("XDG_CONFIG_HOME") }()
t.Setenv("XDG_CONFIG_HOME", tmpDir)
cfg, err := config.Load()
cfg, err := Load()
if err != nil {
t.Fatalf("failed to load: %v", err)
}
@@ -208,12 +188,12 @@ func TestLoadSave(t *testing.T) {
cfg.Core.Shell = "zsh"
cfg.UI.MaxHeight = 20
err = config.Save(cfg)
err = Save(cfg)
if err != nil {
t.Fatalf("failed to save: %v", err)
}
loaded, err := config.Load()
loaded, err := Load()
if err != nil {
t.Fatalf("failed to load after save: %v", err)
}
@@ -233,12 +213,8 @@ func TestMigration(t *testing.T) {
}
defer os.RemoveAll(tmpDir)
_ = os.Setenv("HOME", tmpDir)
_ = os.Setenv("XDG_DATA_HOME", filepath.Join(tmpDir, ".local", "share"))
defer func() {
_ = os.Unsetenv("HOME")
_ = os.Unsetenv("XDG_DATA_HOME")
}()
t.Setenv("HOME", tmpDir)
t.Setenv("XDG_DATA_HOME", filepath.Join(tmpDir, ".local", "share"))
legacyDir := filepath.Join(tmpDir, ".iris")
if errMkdir := os.MkdirAll(legacyDir, 0755); errMkdir != nil {
@@ -251,12 +227,12 @@ func TestMigration(t *testing.T) {
legacyUpdateJson := `{"seen_version": "v1.2.3", "last_check": 1234567890}`
_ = os.WriteFile(filepath.Join(legacyDir, "update_state.json"), []byte(legacyUpdateJson), 0644)
err = config.MigrateFromLegacyJSON()
err = MigrateFromLegacyJSON()
if err != nil {
t.Fatalf("migration failed: %v", err)
}
state := config.LoadState()
state := LoadState()
if state.LastMode != "history" {
t.Errorf("expected migrated last mode 'history', got %q", state.LastMode)
}
@@ -275,30 +251,4 @@ func TestMigration(t *testing.T) {
}
}
func TestConfigCommands(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "iris-config-cmd-test")
if err != nil {
t.Fatalf("failed to create temp dir: %v", err)
}
defer os.RemoveAll(tmpDir)
origConfigHome := os.Getenv("XDG_CONFIG_HOME")
defer func() {
_ = os.Setenv("XDG_CONFIG_HOME", origConfigHome)
}()
_ = os.Setenv("XDG_CONFIG_HOME", tmpDir)
root.ConfigInitCmd.Run(root.ConfigInitCmd, []string{})
configPath := filepath.Join(tmpDir, "iris", "config.toml")
if _, err := os.Stat(configPath); err != nil {
t.Errorf("expected config file to be created at %s, but it was not", configPath)
}
buf := new(bytes.Buffer)
root.ConfigShowCmd.SetOut(buf)
root.ConfigShowCmd.Run(root.ConfigShowCmd, []string{})
if buf.Len() == 0 {
t.Errorf("expected show command to output configuration")
}
}
@@ -1,12 +1,10 @@
package tests
package logger
import (
"os"
"path/filepath"
"strings"
"testing"
"github.com/versenilvis/iris/logger"
)
func TestLogger(t *testing.T) {
@@ -19,10 +17,10 @@ func TestLogger(t *testing.T) {
logFilePath := filepath.Join(tempDir, "test.log")
// test 1: default init sets level to info
logger.Init(logFilePath, false)
logger.Debugf("this debug msg should not be logged")
logger.Infof("this info msg should be logged")
logger.Close()
Init(logFilePath, false)
Debugf("this debug msg should not be logged")
Infof("this info msg should be logged")
Close()
data, err := os.ReadFile(logFilePath)
if err != nil {
@@ -45,9 +43,9 @@ func TestLogger(t *testing.T) {
// test 2: override init with debug = true
_ = os.Remove(logFilePath)
logger.Init(logFilePath, true)
logger.Debugf("this debug msg should now be logged")
logger.Close()
Init(logFilePath, true)
Debugf("this debug msg should now be logged")
Close()
data, err = os.ReadFile(logFilePath)
if err != nil {
@@ -70,9 +68,9 @@ func TestLogger(t *testing.T) {
t.Fatalf("failed to write large file: %v", err)
}
logger.Init(logFilePath, false)
logger.Infof("new log after rotation")
logger.Close()
Init(logFilePath, false)
Infof("new log after rotation")
Close()
// check if old file exists and is rotated
oldPath := logFilePath + ".old"
-9
View File
@@ -1,9 +0,0 @@
package main
import (
"github.com/versenilvis/iris/root"
)
func main() {
root.Execute()
}
+1 -1
View File
@@ -7,7 +7,7 @@ import (
"github.com/BurntSushi/toml"
"github.com/spf13/cobra"
"github.com/versenilvis/iris/config"
"github.com/versenilvis/iris/internal/config"
)
var ConfigCmd = &cobra.Command{
+32
View File
@@ -0,0 +1,32 @@
package root
import (
"bytes"
"os"
"path/filepath"
"testing"
)
func TestConfigCommands(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "iris-config-cmd-test")
if err != nil {
t.Fatalf("failed to create temp dir: %v", err)
}
defer os.RemoveAll(tmpDir)
t.Setenv("XDG_CONFIG_HOME", tmpDir)
ConfigInitCmd.Run(ConfigInitCmd, []string{})
configPath := filepath.Join(tmpDir, "iris", "config.toml")
if _, err := os.Stat(configPath); err != nil {
t.Errorf("expected config file to be created at %s, but it was not", configPath)
}
buf := new(bytes.Buffer)
ConfigShowCmd.SetOut(buf)
ConfigShowCmd.Run(ConfigShowCmd, []string{})
if buf.Len() == 0 {
t.Errorf("expected show command to output configuration")
}
}
+1 -1
View File
@@ -11,7 +11,7 @@ import (
"time"
"github.com/spf13/cobra"
"github.com/versenilvis/iris/config"
"github.com/versenilvis/iris/internal/config"
)
// startRescueShell starts a fallback shell if the application crashes to keep the terminal open
+14 -28
View File
@@ -1,4 +1,4 @@
package tests
package root
import (
"bytes"
@@ -6,8 +6,6 @@ import (
"path/filepath"
"strings"
"testing"
"github.com/versenilvis/iris/root"
)
func TestWriteCrashLog(t *testing.T) {
@@ -17,17 +15,11 @@ func TestWriteCrashLog(t *testing.T) {
}
defer os.RemoveAll(tmpDir)
origHome := os.Getenv("HOME")
origCache := os.Getenv("XDG_CACHE_HOME")
defer func() {
_ = os.Setenv("HOME", origHome)
_ = os.Setenv("XDG_CACHE_HOME", origCache)
}()
_ = os.Setenv("HOME", tmpDir)
_ = os.Setenv("XDG_CACHE_HOME", filepath.Join(tmpDir, ".cache"))
t.Setenv("HOME", tmpDir)
t.Setenv("XDG_CACHE_HOME", filepath.Join(tmpDir, ".cache"))
testErr := "test panic message"
root.WriteCrashLog(testErr)
WriteCrashLog(testErr)
dir := filepath.Join(tmpDir, ".cache", "iris", "crashes")
files, err := os.ReadDir(dir)
@@ -63,35 +55,29 @@ func TestCrashLogCommand(t *testing.T) {
}
defer os.RemoveAll(tmpDir)
origHome := os.Getenv("HOME")
origCache := os.Getenv("XDG_CACHE_HOME")
defer func() {
_ = os.Setenv("HOME", origHome)
_ = os.Setenv("XDG_CACHE_HOME", origCache)
}()
_ = os.Setenv("HOME", tmpDir)
_ = os.Setenv("XDG_CACHE_HOME", filepath.Join(tmpDir, ".cache"))
t.Setenv("HOME", tmpDir)
t.Setenv("XDG_CACHE_HOME", filepath.Join(tmpDir, ".cache"))
var buf bytes.Buffer
root.CrashCmd.SetOut(&buf)
root.CrashCmd.SetArgs([]string{})
root.ClearLog = false
CrashCmd.SetOut(&buf)
CrashCmd.SetArgs([]string{})
ClearLog = false
root.CrashCmd.Run(root.CrashCmd, []string{})
CrashCmd.Run(CrashCmd, []string{})
if !strings.Contains(buf.String(), "no crash log found") {
t.Errorf("expected 'no crash log found', got: %q", buf.String())
}
root.WriteCrashLog("mock error")
WriteCrashLog("mock error")
buf.Reset()
root.CrashCmd.Run(root.CrashCmd, []string{})
CrashCmd.Run(CrashCmd, []string{})
if !strings.Contains(buf.String(), "crash_") || !strings.Contains(buf.String(), ".log") {
t.Errorf("expected crash log path, got: %q", buf.String())
}
buf.Reset()
root.ClearLog = true
root.CrashCmd.Run(root.CrashCmd, []string{})
ClearLog = true
CrashCmd.Run(CrashCmd, []string{})
if !strings.Contains(buf.String(), "crash log cleared") {
t.Errorf("expected 'crash log cleared', got: %q", buf.String())
}
+1 -1
View File
@@ -7,7 +7,7 @@ import (
"strings"
"github.com/spf13/cobra"
"github.com/versenilvis/iris/config"
"github.com/versenilvis/iris/internal/config"
)
var initCmd = &cobra.Command{
+2 -2
View File
@@ -15,8 +15,8 @@ import (
"github.com/spf13/cobra"
_ "github.com/versenilvis/iris/commands"
"github.com/versenilvis/iris/config"
"github.com/versenilvis/iris/logger"
"github.com/versenilvis/iris/internal/config"
"github.com/versenilvis/iris/internal/logger"
"golang.org/x/term"
)
+3 -3
View File
@@ -4,10 +4,10 @@ import (
"strings"
"sync"
"github.com/versenilvis/iris/ai"
"github.com/versenilvis/iris/config"
"github.com/versenilvis/iris/integration"
"github.com/versenilvis/iris/logger"
"github.com/versenilvis/iris/internal/ai"
"github.com/versenilvis/iris/internal/config"
"github.com/versenilvis/iris/internal/logger"
"github.com/versenilvis/iris/spec"
)
@@ -1,9 +1,8 @@
package tests
package root
import (
"testing"
"github.com/versenilvis/iris/root"
"github.com/versenilvis/iris/spec"
)
@@ -11,7 +10,7 @@ func TestMergeResults(t *testing.T) {
t.Run("Dedup exact match", func(t *testing.T) {
// Mock history items that might conflict with specs
res := root.MergeResults("git", "spec")
res := MergeResults("git", "spec")
seen := make(map[string]bool)
for _, r := range res {
if seen[r.Cmd] {
@@ -23,7 +22,7 @@ func TestMergeResults(t *testing.T) {
t.Run("Limit 100", func(t *testing.T) {
res := root.MergeResults("a", "history")
res := MergeResults("a", "history")
if len(res) > 100 {
t.Errorf("Expected max 100 suggestions, got %d", len(res))
}
@@ -36,10 +35,10 @@ func TestMergeResults(t *testing.T) {
Source: "ai",
Confidence: 85,
}
root.SetCurrentAISuggestion(aiSugg)
defer root.SetCurrentAISuggestion(nil)
SetCurrentAISuggestion(aiSugg)
defer SetCurrentAISuggestion(nil)
res := root.MergeResults("git c", "history")
res := MergeResults("git c", "history")
if len(res) == 0 {
t.Fatalf("Expected suggestions, got 0")
}
+1 -1
View File
@@ -8,7 +8,7 @@ import (
"strings"
"github.com/spf13/cobra"
"github.com/versenilvis/iris/config"
"github.com/versenilvis/iris/internal/config"
)
func init() {
+1 -1
View File
@@ -13,7 +13,7 @@ import (
"time"
"github.com/spf13/cobra"
"github.com/versenilvis/iris/config"
"github.com/versenilvis/iris/internal/config"
)
// updateResult is passed from the async checker to the main loop
@@ -1,4 +1,4 @@
package tests
package root
import (
"os"
@@ -6,8 +6,7 @@ import (
"testing"
"time"
"github.com/versenilvis/iris/config"
"github.com/versenilvis/iris/root"
"github.com/versenilvis/iris/internal/config"
)
func TestIsNewer(t *testing.T) {
@@ -29,7 +28,7 @@ func TestIsNewer(t *testing.T) {
}
for _, tt := range tests {
if got := root.IsNewer(tt.current, tt.latest); got != tt.want {
if got := IsNewer(tt.current, tt.latest); got != tt.want {
t.Errorf("IsNewer(%q, %q) = %v; want %v", tt.current, tt.latest, got, tt.want)
}
}
@@ -43,20 +42,8 @@ func TestUpdateState(t *testing.T) {
}
defer func() { _ = os.RemoveAll(tmpDir) }()
// Override home dir for testing
homeBackup := os.Getenv("HOME")
err = os.Setenv("HOME", tmpDir)
if err != nil {
t.Fatal(err)
}
defer func() { _ = os.Setenv("HOME", homeBackup) }()
xdgBackup := os.Getenv("XDG_DATA_HOME")
err = os.Setenv("XDG_DATA_HOME", filepath.Join(tmpDir, ".local", "share"))
if err != nil {
t.Fatal(err)
}
defer func() { _ = os.Setenv("XDG_DATA_HOME", xdgBackup) }()
t.Setenv("HOME", tmpDir)
t.Setenv("XDG_DATA_HOME", filepath.Join(tmpDir, ".local", "share"))
state := config.LoadState()
state.Updater.SeenVersion = "v1.0.0"
+3 -3
View File
@@ -17,11 +17,11 @@ import (
"time"
"github.com/creack/pty"
"github.com/versenilvis/iris/ai"
"github.com/versenilvis/iris/config"
"github.com/versenilvis/iris/integration"
"github.com/versenilvis/iris/integration/shell"
"github.com/versenilvis/iris/logger"
"github.com/versenilvis/iris/internal/ai"
"github.com/versenilvis/iris/internal/config"
"github.com/versenilvis/iris/internal/logger"
"github.com/versenilvis/iris/spec"
"golang.org/x/sys/unix"
"golang.org/x/term"
-68
View File
@@ -1,68 +0,0 @@
package main
import (
"context"
"fmt"
"os"
"time"
"github.com/versenilvis/iris/ai"
"github.com/versenilvis/iris/config"
"github.com/versenilvis/iris/root"
)
func main() {
cfg, err := config.Load()
if err != nil {
fmt.Printf("Error loading config: %v\n", err)
os.Exit(1)
}
config.Init(cfg)
fmt.Printf("AI Enabled: %v\n", cfg.AI.Enabled)
fmt.Printf("Active Provider: %q\n", cfg.AI.Provider)
pCfg, ok := cfg.AI.GetActiveProvider()
if !ok {
fmt.Println("Error: active provider not found in config")
os.Exit(1)
}
fmt.Printf("Endpoint: %s\n", pCfg.Endpoint)
fmt.Printf("Model: %s\n", pCfg.Model)
fmt.Printf("API Key Present: %v (len %d)\n", pCfg.GetAPIKey() != "", len(pCfg.GetAPIKey()))
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
cwd, _ := os.Getwd()
env := ai.EnvSnapshot{
Cwd: cwd,
LastCmd: "git status",
LastExitCode: 0,
}
engine := root.GetAIEngine()
dynCtx := engine.GatherDynamicContext(ctx, "git commit -m \"", cwd)
fmt.Printf("\n--- Dynamic Context Gathered by UniversalProvider ---\n%s\n-----------------------------------------------------\n", dynCtx)
fmt.Println("\nSending live test completion request to Groq for: \"git commit -m \" ...")
start := time.Now()
sugg, err := engine.Suggest(ctx, "git commit -m \"", env, dynCtx)
duration := time.Since(start)
if err != nil {
fmt.Printf("AI Suggest Error: %v\n", err)
os.Exit(1)
}
if sugg == nil {
fmt.Println("AI returned no suggestion (nil)")
os.Exit(0)
}
fmt.Printf("SUCCESS! Response received in %v:\n", duration)
fmt.Printf(" Cmd: %s\n", sugg.Cmd)
fmt.Printf(" Confidence: %d\n", sugg.Confidence)
fmt.Printf(" Source: %s\n", sugg.Source)
}
@@ -1,12 +1,10 @@
package tests
package spec
import (
"os"
"path/filepath"
"strings"
"testing"
"github.com/versenilvis/iris/spec"
)
func TestFileGenerator(t *testing.T) {
@@ -24,7 +22,7 @@ func TestFileGenerator(t *testing.T) {
t.Run("dirOnly shows only dirs", func(t *testing.T) {
gen := spec.FileGenerator("/")
gen := FileGenerator("/")
results := gen([]string{"cd", ""}, "cd ", "")
for _, r := range results {
if !strings.HasSuffix(r.Cmd, "/") {
@@ -35,7 +33,7 @@ func TestFileGenerator(t *testing.T) {
t.Run("Filter extension", func(t *testing.T) {
gen := spec.FileGenerator(".go")
gen := FileGenerator(".go")
results := gen([]string{"ls", ""}, "ls ", "")
foundMain := false
for _, r := range results {
@@ -53,7 +51,7 @@ func TestFileGenerator(t *testing.T) {
t.Run("Nested path", func(t *testing.T) {
gen := spec.FileGenerator()
gen := FileGenerator()
results := gen([]string{"ls", "src/u"}, "ls src/u", "src/u")
foundUtils := false
for _, r := range results {
@@ -68,7 +66,7 @@ func TestFileGenerator(t *testing.T) {
t.Run("Deep scan 1 level", func(t *testing.T) {
gen := spec.FileGenerator()
gen := FileGenerator()
results := gen([]string{"ls", "src/"}, "ls src/", "src/")
foundUtils := false
for _, r := range results {
@@ -85,7 +83,7 @@ func TestFileGenerator(t *testing.T) {
t.Run("Hidden files are skipped", func(t *testing.T) {
gen := spec.FileGenerator()
gen := FileGenerator()
results := gen([]string{"ls", ""}, "ls ", "")
for _, r := range results {
if strings.HasPrefix(r.Cmd, ".") {
+1 -1
View File
@@ -7,7 +7,7 @@ import (
"sync"
"github.com/versenilvis/iris/integration/shell"
"github.com/versenilvis/iris/logger"
"github.com/versenilvis/iris/internal/logger"
)
var (
@@ -1,27 +1,25 @@
package tests
package spec
import (
"strings"
"sync"
"testing"
"github.com/versenilvis/iris/spec"
)
func TestLookup(t *testing.T) {
// Setup Registry
spec.Registry = make(map[string]*spec.Spec)
spec.Register(&spec.Spec{
Registry = make(map[string]*Spec)
Register(&Spec{
Name: "git",
Subcommands: []spec.Subcommand{
{Name: "commit", Options: []spec.Option{{Name: "--message"}}, MaxArgs: 1},
{Name: "remote", Subcommands: []spec.Subcommand{{Name: "add"}}},
Subcommands: []Subcommand{
{Name: "commit", Options: []Option{{Name: "--message"}}, MaxArgs: 1},
{Name: "remote", Subcommands: []Subcommand{{Name: "add"}}},
},
Options: []spec.Option{{Name: "--verbose"}},
Options: []Option{{Name: "--verbose"}},
})
// Setup Aliases
spec.ShellAliases = map[string]string{
ShellAliases = map[string]string{
"gca": "git commit -a",
"ta": "tmux a -t",
}
@@ -44,7 +42,7 @@ func TestLookup(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
results := spec.Lookup(tt.input)
results := Lookup(tt.input)
if len(results) < tt.minResults {
t.Errorf("Lookup(%q) got %d results; want at least %d", tt.input, len(results), tt.minResults)
}
@@ -65,15 +63,15 @@ func TestLookup(t *testing.T) {
}
func TestLookupConcurrent(t *testing.T) {
spec.Registry = make(map[string]*spec.Spec)
spec.Register(&spec.Spec{
Registry = make(map[string]*Spec)
Register(&Spec{
Name: "git",
Subcommands: []spec.Subcommand{
{Name: "commit", Options: []spec.Option{{Name: "--message"}}, MaxArgs: 1},
Subcommands: []Subcommand{
{Name: "commit", Options: []Option{{Name: "--message"}}, MaxArgs: 1},
},
})
spec.ShellAliases = map[string]string{
ShellAliases = map[string]string{
"gca": "git commit -a",
}
@@ -86,8 +84,8 @@ func TestLookupConcurrent(t *testing.T) {
go func() {
defer wg.Done()
for range iterations {
_ = spec.Lookup("gca")
_ = spec.Lookup("git ")
_ = Lookup("gca")
_ = Lookup("git ")
}
}()
}
@@ -1,10 +1,8 @@
package tests
package spec
import (
"reflect"
"testing"
"github.com/versenilvis/iris/spec"
)
func TestTokenize(t *testing.T) {
@@ -24,7 +22,7 @@ func TestTokenize(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := spec.Tokenize(tt.input)
got := Tokenize(tt.input)
if !reflect.DeepEqual(got, tt.expected) {
t.Errorf("Tokenize(%q) = %v; want %v", tt.input, got, tt.expected)
}
@@ -47,7 +45,7 @@ func TestHasPrefix(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := spec.HasPrefix(tt.s, tt.prefix); got != tt.want {
if got := HasPrefix(tt.s, tt.prefix); got != tt.want {
t.Errorf("HasPrefix(%q, %q) = %v; want %v", tt.s, tt.prefix, got, tt.want)
}
})
@@ -4,12 +4,10 @@ import (
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"
_ "github.com/versenilvis/iris/commands"
"github.com/versenilvis/iris/spec"
"github.com/versenilvis/iris/commands/js"
"github.com/versenilvis/iris/spec"
)
func TestNpmScriptGenerator(t *testing.T) {
@@ -102,93 +100,3 @@ func TestNpmScriptGenerator(t *testing.T) {
}
})
}
func TestSshHostGenerator(t *testing.T) {
tmp := t.TempDir()
sshDir := filepath.Join(tmp, ".ssh")
_ = os.MkdirAll(sshDir, 0700)
configContent := `
Host prod-server
HostName 10.0.0.1
User deploy
Host staging bastion
HostName staging.example.com
User ubuntu
Host *.internal
User admin
Host !forbidden wildcard-test
HostName test.internal
`
_ = os.WriteFile(filepath.Join(sshDir, "config"), []byte(configContent), 0600)
// temporarily replace home dir lookup by using a mock path
// we call the generator directly with a custom home dir
results := sshHostGeneratorFromPath(filepath.Join(sshDir, "config"))
found := make(map[string]bool)
for _, r := range results {
found[r.Cmd] = true
}
if !found["prod-server"] {
t.Error("expected prod-server in suggestions")
}
if !found["staging"] {
t.Error("expected staging in suggestions")
}
if !found["bastion"] {
t.Error("expected bastion in suggestions")
}
// wildcards should be excluded
if found["*.internal"] {
t.Error("wildcard *.internal should not be suggested")
}
if found["!forbidden"] {
t.Error("negated host !forbidden should not be suggested")
}
}
// sshHostGeneratorFromPath is a helper that reads a specific ssh config path
func sshHostGeneratorFromPath(configPath string) []spec.Suggestion {
import_bufio := func() {
// using bufio in the same style as ssh.go
}
_ = import_bufio
f, err := os.Open(configPath)
if err != nil {
return nil
}
defer func() { _ = f.Close() }()
seen := make(map[string]bool)
var results []spec.Suggestion
scanner := strings.NewReader("")
_ = scanner
data, _ := os.ReadFile(configPath)
for line := range strings.SplitSeq(string(data), "\n") {
line = strings.TrimSpace(line)
if !strings.HasPrefix(strings.ToLower(line), "host ") {
continue
}
parts := strings.Fields(line)
for _, host := range parts[1:] {
if strings.ContainsAny(host, "*?!") {
continue
}
if seen[host] {
continue
}
seen[host] = true
results = append(results, spec.Suggestion{Cmd: host, Desc: "ssh host"})
}
}
return results
}
+88
View File
@@ -0,0 +1,88 @@
package tests
import (
"bufio"
"os"
"path/filepath"
"strings"
"testing"
"github.com/versenilvis/iris/spec"
)
func TestSshHostGenerator(t *testing.T) {
tmp := t.TempDir()
sshDir := filepath.Join(tmp, ".ssh")
_ = os.MkdirAll(sshDir, 0700)
configContent := `
Host prod-server
HostName 10.0.0.1
User deploy
Host staging bastion
HostName staging.example.com
User ubuntu
Host *.internal
User admin
Host !forbidden wildcard-test
HostName test.internal
`
_ = os.WriteFile(filepath.Join(sshDir, "config"), []byte(configContent), 0600)
results := sshHostGeneratorFromPath(filepath.Join(sshDir, "config"))
found := make(map[string]bool)
for _, r := range results {
found[r.Cmd] = true
}
if !found["prod-server"] {
t.Error("expected prod-server in suggestions")
}
if !found["staging"] {
t.Error("expected staging in suggestions")
}
if !found["bastion"] {
t.Error("expected bastion in suggestions")
}
if !found["wildcard-test"] {
t.Error("expected wildcard-test in suggestions")
}
if found["*.internal"] || found["!forbidden"] {
t.Error("wildcards and negations should be ignored")
}
}
// sshHostGeneratorFromPath is a helper that reads a specific ssh config path
func sshHostGeneratorFromPath(configPath string) []spec.Suggestion {
f, err := os.Open(configPath)
if err != nil {
return nil
}
defer func() { _ = f.Close() }()
seen := make(map[string]bool)
var results []spec.Suggestion
scanner := bufio.NewScanner(f)
for scanner.Scan() {
parts := strings.Fields(scanner.Text())
if len(parts) < 2 || !strings.EqualFold(parts[0], "host") {
continue
}
for _, host := range parts[1:] {
if strings.ContainsAny(host, "*?!") {
continue
}
if seen[host] {
continue
}
seen[host] = true
results = append(results, spec.Suggestion{Cmd: host, Desc: "ssh host"})
}
}
return results
}
@@ -18,10 +18,7 @@ func TestZoxideGenerator(t *testing.T) {
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 func() { _ = os.Setenv("PATH", oldPath) }()
t.Setenv("PATH", tmp+string(os.PathListSeparator)+os.Getenv("PATH"))
gen := fs.ZoxideGenerator()
+1 -4
View File
@@ -27,10 +27,7 @@ func TestIntegration_ZoxideMultiWord(t *testing.T) {
// Create mock zoxide binary
script := "#!/bin/sh\necho \"" + targetDir + "\""
_ = os.WriteFile(mockZoxide, []byte(script), 0755)
// Add mock bin to PATH
oldPath := os.Getenv("PATH")
_ = os.Setenv("PATH", mockBinDir+string(os.PathListSeparator)+oldPath)
defer func() { _ = os.Setenv("PATH", oldPath) }()
t.Setenv("PATH", mockBinDir+string(os.PathListSeparator)+os.Getenv("PATH"))
t.Run("z matches multi-word folder without quotes", func(t *testing.T) {
// Simulating user typing "z My Awe"