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:
+2
-1
@@ -6,4 +6,5 @@ docs/guide.md
|
||||
task
|
||||
docs/code
|
||||
docs/plan
|
||||
todo.txt
|
||||
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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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") {
|
||||
@@ -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"
|
||||
@@ -1,9 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/versenilvis/iris/root"
|
||||
)
|
||||
|
||||
func main() {
|
||||
root.Execute()
|
||||
}
|
||||
+1
-1
@@ -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{
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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"
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user