From 1e049492421aa2b10cfcc584ac585d800d1883eb Mon Sep 17 00:00:00 2001 From: VERSE Date: Sat, 11 Jul 2026 16:15:05 +0700 Subject: [PATCH] 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 --- .gitignore | 3 +- {tests => integration}/cursor_test.go | 6 +- integration/overlay.go | 5 +- .../overlay_test.go | 13 +-- {tests => integration}/shell/adapter_test.go | 8 +- {ai => internal/ai}/cache.go | 0 {ai => internal/ai}/client.go | 2 +- {tests => internal}/ai/client_test.go | 15 ++- {ai => internal/ai}/context.go | 0 {ai => internal/ai}/context_provider.go | 0 .../ai/context_provider_test.go | 66 +++++++++-- {tests => internal}/ai/context_test.go | 32 +++--- {ai => internal/ai}/defaults.go | 0 {ai => internal/ai}/engine.go | 4 +- {tests => internal}/ai/engine_test.go | 37 +++--- {ai => internal/ai}/prompts.go | 0 {ai => internal/ai}/types.go | 0 {ai => internal/ai}/utils.go | 0 {config => internal/config}/config.go | 0 {tests => internal/config}/config_test.go | 106 +++++------------- {config => internal/config}/defaults.go | 0 {config => internal/config}/env.go | 0 {config => internal/config}/migrate.go | 0 {config => internal/config}/paths.go | 0 {config => internal/config}/state.go | 0 {ipc => internal/ipc}/server.go | 0 {logger => internal/logger}/logger.go | 0 {tests => internal/logger}/logger_test.go | 24 ++-- main.go | 9 -- root/config_cmd.go | 2 +- root/config_cmd_test.go | 32 ++++++ root/crash.go | 2 +- {tests => root}/crash_test.go | 42 +++---- root/init.go | 2 +- root/root.go | 4 +- root/suggestions.go | 6 +- {tests/root => root}/suggestions_test.go | 13 +-- root/uninstall.go | 2 +- root/update.go | 2 +- {tests/root => root}/update_test.go | 23 +--- root/wrapper.go | 6 +- scratch/test_groq.go | 68 ----------- {tests/spec => spec}/filegen_test.go | 14 +-- spec/lookup.go | 2 +- {tests/spec => spec}/lookup_test.go | 34 +++--- {tests/spec => spec}/utils_test.go | 8 +- tests/{dev => commands}/git_test.go | 0 tests/{spec => commands}/just_test.go | 0 .../npm_ssh_test.go => commands/npm_test.go} | 94 +--------------- tests/commands/ssh_test.go | 88 +++++++++++++++ tests/{fs => commands}/zoxide_test.go | 5 +- tests/integration_test.go | 5 +- 52 files changed, 339 insertions(+), 445 deletions(-) rename {tests => integration}/cursor_test.go (88%) rename {tests/integration => integration}/overlay_test.go (94%) rename {tests => integration}/shell/adapter_test.go (87%) rename {ai => internal/ai}/cache.go (100%) rename {ai => internal/ai}/client.go (98%) rename {tests => internal}/ai/client_test.go (91%) rename {ai => internal/ai}/context.go (100%) rename {ai => internal/ai}/context_provider.go (100%) rename {tests => internal}/ai/context_provider_test.go (67%) rename {tests => internal}/ai/context_test.go (80%) rename {ai => internal/ai}/defaults.go (100%) rename {ai => internal/ai}/engine.go (97%) rename {tests => internal}/ai/engine_test.go (75%) rename {ai => internal/ai}/prompts.go (100%) rename {ai => internal/ai}/types.go (100%) rename {ai => internal/ai}/utils.go (100%) rename {config => internal/config}/config.go (100%) rename {tests => internal/config}/config_test.go (70%) rename {config => internal/config}/defaults.go (100%) rename {config => internal/config}/env.go (100%) rename {config => internal/config}/migrate.go (100%) rename {config => internal/config}/paths.go (100%) rename {config => internal/config}/state.go (100%) rename {ipc => internal/ipc}/server.go (100%) rename {logger => internal/logger}/logger.go (100%) rename {tests => internal/logger}/logger_test.go (84%) delete mode 100644 main.go create mode 100644 root/config_cmd_test.go rename {tests => root}/crash_test.go (67%) rename {tests/root => root}/suggestions_test.go (78%) rename {tests/root => root}/update_test.go (73%) delete mode 100644 scratch/test_groq.go rename {tests/spec => spec}/filegen_test.go (90%) rename {tests/spec => spec}/lookup_test.go (68%) rename {tests/spec => spec}/utils_test.go (90%) rename tests/{dev => commands}/git_test.go (100%) rename tests/{spec => commands}/just_test.go (100%) rename tests/{dev/npm_ssh_test.go => commands/npm_test.go} (52%) create mode 100644 tests/commands/ssh_test.go rename tests/{fs => commands}/zoxide_test.go (88%) diff --git a/.gitignore b/.gitignore index 47cf814..41e3f0c 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,5 @@ docs/guide.md task docs/code docs/plan -todo.txt \ No newline at end of file +todo.txt +scratch/ diff --git a/tests/cursor_test.go b/integration/cursor_test.go similarity index 88% rename from tests/cursor_test.go rename to integration/cursor_test.go index 562c443..5886df3 100644 --- a/tests/cursor_test.go +++ b/integration/cursor_test.go @@ -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) } diff --git a/integration/overlay.go b/integration/overlay.go index 4d76498..95b334a 100644 --- a/integration/overlay.go +++ b/integration/overlay.go @@ -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" ) diff --git a/tests/integration/overlay_test.go b/integration/overlay_test.go similarity index 94% rename from tests/integration/overlay_test.go rename to integration/overlay_test.go index 498e945..a0f7fb6 100644 --- a/tests/integration/overlay_test.go +++ b/integration/overlay_test.go @@ -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" { diff --git a/tests/shell/adapter_test.go b/integration/shell/adapter_test.go similarity index 87% rename from tests/shell/adapter_test.go rename to integration/shell/adapter_test.go index 933ca53..1ad7826 100644 --- a/tests/shell/adapter_test.go +++ b/integration/shell/adapter_test.go @@ -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) } diff --git a/ai/cache.go b/internal/ai/cache.go similarity index 100% rename from ai/cache.go rename to internal/ai/cache.go diff --git a/ai/client.go b/internal/ai/client.go similarity index 98% rename from ai/client.go rename to internal/ai/client.go index d58c5a8..2a4ca03 100644 --- a/ai/client.go +++ b/internal/ai/client.go @@ -10,7 +10,7 @@ import ( "strings" "time" - "github.com/versenilvis/iris/config" + "github.com/versenilvis/iris/internal/config" "github.com/versenilvis/iris/spec" ) diff --git a/tests/ai/client_test.go b/internal/ai/client_test.go similarity index 91% rename from tests/ai/client_test.go rename to internal/ai/client_test.go index 8d5a2d0..389b1dc 100644 --- a/tests/ai/client_test.go +++ b/internal/ai/client_test.go @@ -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 { diff --git a/ai/context.go b/internal/ai/context.go similarity index 100% rename from ai/context.go rename to internal/ai/context.go diff --git a/ai/context_provider.go b/internal/ai/context_provider.go similarity index 100% rename from ai/context_provider.go rename to internal/ai/context_provider.go diff --git a/tests/ai/context_provider_test.go b/internal/ai/context_provider_test.go similarity index 67% rename from tests/ai/context_provider_test.go rename to internal/ai/context_provider_test.go index 3a92aca..0ee41ce 100644 --- a/tests/ai/context_provider_test.go +++ b/internal/ai/context_provider_test.go @@ -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 diff --git a/tests/ai/context_test.go b/internal/ai/context_test.go similarity index 80% rename from tests/ai/context_test.go rename to internal/ai/context_test.go index 7f05bd8..c794eef 100644 --- a/tests/ai/context_test.go +++ b/internal/ai/context_test.go @@ -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") { diff --git a/ai/defaults.go b/internal/ai/defaults.go similarity index 100% rename from ai/defaults.go rename to internal/ai/defaults.go diff --git a/ai/engine.go b/internal/ai/engine.go similarity index 97% rename from ai/engine.go rename to internal/ai/engine.go index d57c82d..3e64cb6 100644 --- a/ai/engine.go +++ b/internal/ai/engine.go @@ -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" ) diff --git a/tests/ai/engine_test.go b/internal/ai/engine_test.go similarity index 75% rename from tests/ai/engine_test.go rename to internal/ai/engine_test.go index a6b2319..f0061f2 100644 --- a/tests/ai/engine_test.go +++ b/internal/ai/engine_test.go @@ -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) } diff --git a/ai/prompts.go b/internal/ai/prompts.go similarity index 100% rename from ai/prompts.go rename to internal/ai/prompts.go diff --git a/ai/types.go b/internal/ai/types.go similarity index 100% rename from ai/types.go rename to internal/ai/types.go diff --git a/ai/utils.go b/internal/ai/utils.go similarity index 100% rename from ai/utils.go rename to internal/ai/utils.go diff --git a/config/config.go b/internal/config/config.go similarity index 100% rename from config/config.go rename to internal/config/config.go diff --git a/tests/config_test.go b/internal/config/config_test.go similarity index 70% rename from tests/config_test.go rename to internal/config/config_test.go index 4341f17..6987f8b 100644 --- a/tests/config_test.go +++ b/internal/config/config_test.go @@ -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") - } -} diff --git a/config/defaults.go b/internal/config/defaults.go similarity index 100% rename from config/defaults.go rename to internal/config/defaults.go diff --git a/config/env.go b/internal/config/env.go similarity index 100% rename from config/env.go rename to internal/config/env.go diff --git a/config/migrate.go b/internal/config/migrate.go similarity index 100% rename from config/migrate.go rename to internal/config/migrate.go diff --git a/config/paths.go b/internal/config/paths.go similarity index 100% rename from config/paths.go rename to internal/config/paths.go diff --git a/config/state.go b/internal/config/state.go similarity index 100% rename from config/state.go rename to internal/config/state.go diff --git a/ipc/server.go b/internal/ipc/server.go similarity index 100% rename from ipc/server.go rename to internal/ipc/server.go diff --git a/logger/logger.go b/internal/logger/logger.go similarity index 100% rename from logger/logger.go rename to internal/logger/logger.go diff --git a/tests/logger_test.go b/internal/logger/logger_test.go similarity index 84% rename from tests/logger_test.go rename to internal/logger/logger_test.go index 31cfd0a..18bbad1 100644 --- a/tests/logger_test.go +++ b/internal/logger/logger_test.go @@ -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" diff --git a/main.go b/main.go deleted file mode 100644 index 3d47cd3..0000000 --- a/main.go +++ /dev/null @@ -1,9 +0,0 @@ -package main - -import ( - "github.com/versenilvis/iris/root" -) - -func main() { - root.Execute() -} \ No newline at end of file diff --git a/root/config_cmd.go b/root/config_cmd.go index 7ba1d47..929021d 100644 --- a/root/config_cmd.go +++ b/root/config_cmd.go @@ -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{ diff --git a/root/config_cmd_test.go b/root/config_cmd_test.go new file mode 100644 index 0000000..53b79cd --- /dev/null +++ b/root/config_cmd_test.go @@ -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") + } +} diff --git a/root/crash.go b/root/crash.go index a94b4f0..cc68e40 100644 --- a/root/crash.go +++ b/root/crash.go @@ -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 diff --git a/tests/crash_test.go b/root/crash_test.go similarity index 67% rename from tests/crash_test.go rename to root/crash_test.go index 09c7cda..ceaa53c 100644 --- a/tests/crash_test.go +++ b/root/crash_test.go @@ -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()) } diff --git a/root/init.go b/root/init.go index 23ef7dd..881ddf9 100644 --- a/root/init.go +++ b/root/init.go @@ -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{ diff --git a/root/root.go b/root/root.go index 3dcc72e..23f87d4 100644 --- a/root/root.go +++ b/root/root.go @@ -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" ) diff --git a/root/suggestions.go b/root/suggestions.go index cad2e5b..c8bb80b 100644 --- a/root/suggestions.go +++ b/root/suggestions.go @@ -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" ) diff --git a/tests/root/suggestions_test.go b/root/suggestions_test.go similarity index 78% rename from tests/root/suggestions_test.go rename to root/suggestions_test.go index 1f3b7ed..847df2e 100644 --- a/tests/root/suggestions_test.go +++ b/root/suggestions_test.go @@ -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") } diff --git a/root/uninstall.go b/root/uninstall.go index 6539317..08bfe56 100644 --- a/root/uninstall.go +++ b/root/uninstall.go @@ -8,7 +8,7 @@ import ( "strings" "github.com/spf13/cobra" - "github.com/versenilvis/iris/config" + "github.com/versenilvis/iris/internal/config" ) func init() { diff --git a/root/update.go b/root/update.go index d2b0538..c402bb1 100644 --- a/root/update.go +++ b/root/update.go @@ -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 diff --git a/tests/root/update_test.go b/root/update_test.go similarity index 73% rename from tests/root/update_test.go rename to root/update_test.go index 9020b5b..29af684 100644 --- a/tests/root/update_test.go +++ b/root/update_test.go @@ -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" diff --git a/root/wrapper.go b/root/wrapper.go index 56b590d..a667527 100644 --- a/root/wrapper.go +++ b/root/wrapper.go @@ -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" diff --git a/scratch/test_groq.go b/scratch/test_groq.go deleted file mode 100644 index 53af701..0000000 --- a/scratch/test_groq.go +++ /dev/null @@ -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) -} diff --git a/tests/spec/filegen_test.go b/spec/filegen_test.go similarity index 90% rename from tests/spec/filegen_test.go rename to spec/filegen_test.go index 698a7d9..f7c5439 100644 --- a/tests/spec/filegen_test.go +++ b/spec/filegen_test.go @@ -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, ".") { diff --git a/spec/lookup.go b/spec/lookup.go index 1ca260f..10d70d0 100644 --- a/spec/lookup.go +++ b/spec/lookup.go @@ -7,7 +7,7 @@ import ( "sync" "github.com/versenilvis/iris/integration/shell" - "github.com/versenilvis/iris/logger" + "github.com/versenilvis/iris/internal/logger" ) var ( diff --git a/tests/spec/lookup_test.go b/spec/lookup_test.go similarity index 68% rename from tests/spec/lookup_test.go rename to spec/lookup_test.go index c1cf16e..bcad844 100644 --- a/tests/spec/lookup_test.go +++ b/spec/lookup_test.go @@ -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 ") } }() } diff --git a/tests/spec/utils_test.go b/spec/utils_test.go similarity index 90% rename from tests/spec/utils_test.go rename to spec/utils_test.go index 9e25f3a..052b0a6 100644 --- a/tests/spec/utils_test.go +++ b/spec/utils_test.go @@ -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) } }) diff --git a/tests/dev/git_test.go b/tests/commands/git_test.go similarity index 100% rename from tests/dev/git_test.go rename to tests/commands/git_test.go diff --git a/tests/spec/just_test.go b/tests/commands/just_test.go similarity index 100% rename from tests/spec/just_test.go rename to tests/commands/just_test.go diff --git a/tests/dev/npm_ssh_test.go b/tests/commands/npm_test.go similarity index 52% rename from tests/dev/npm_ssh_test.go rename to tests/commands/npm_test.go index 5d15e01..b50e543 100644 --- a/tests/dev/npm_ssh_test.go +++ b/tests/commands/npm_test.go @@ -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 -} diff --git a/tests/commands/ssh_test.go b/tests/commands/ssh_test.go new file mode 100644 index 0000000..cb01c2d --- /dev/null +++ b/tests/commands/ssh_test.go @@ -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 +} diff --git a/tests/fs/zoxide_test.go b/tests/commands/zoxide_test.go similarity index 88% rename from tests/fs/zoxide_test.go rename to tests/commands/zoxide_test.go index 2e2fd75..6428f5e 100644 --- a/tests/fs/zoxide_test.go +++ b/tests/commands/zoxide_test.go @@ -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() diff --git a/tests/integration_test.go b/tests/integration_test.go index 465c06b..48a1307 100644 --- a/tests/integration_test.go +++ b/tests/integration_test.go @@ -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"