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
|
task
|
||||||
docs/code
|
docs/code
|
||||||
docs/plan
|
docs/plan
|
||||||
todo.txt
|
todo.txt
|
||||||
|
scratch/
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
package tests
|
package integration
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/versenilvis/iris/integration"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestComputeCursorCol(t *testing.T) {
|
func TestComputeCursorCol(t *testing.T) {
|
||||||
@@ -41,7 +39,7 @@ func TestComputeCursorCol(t *testing.T) {
|
|||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
got := integration.ComputeCursorCol(tt.data)
|
got := ComputeCursorCol(tt.data)
|
||||||
if got != tt.want {
|
if got != tt.want {
|
||||||
t.Errorf("ComputeCursorCol(%q) = %d, want %d", tt.data, got, tt.want)
|
t.Errorf("ComputeCursorCol(%q) = %d, want %d", tt.data, got, tt.want)
|
||||||
}
|
}
|
||||||
@@ -7,11 +7,10 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
|
|
||||||
"github.com/charmbracelet/lipgloss"
|
"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/spec"
|
||||||
"github.com/versenilvis/iris/config"
|
|
||||||
"github.com/versenilvis/iris/logger"
|
|
||||||
"golang.org/x/term"
|
"golang.org/x/term"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
package tests
|
package integration
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/versenilvis/iris/integration"
|
|
||||||
"github.com/versenilvis/iris/spec"
|
"github.com/versenilvis/iris/spec"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestRenderGhostText_CursorAtEnd(t *testing.T) {
|
func TestRenderGhostText_CursorAtEnd(t *testing.T) {
|
||||||
o := integration.NewOverlay()
|
o := NewOverlay()
|
||||||
items := []spec.Suggestion{
|
items := []spec.Suggestion{
|
||||||
{Cmd: "git checkout -b feature"},
|
{Cmd: "git checkout -b feature"},
|
||||||
}
|
}
|
||||||
@@ -35,7 +34,7 @@ func TestRenderGhostText_CursorAtEnd(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestGetGhostText(t *testing.T) {
|
func TestGetGhostText(t *testing.T) {
|
||||||
o := integration.NewOverlay()
|
o := NewOverlay()
|
||||||
items := []spec.Suggestion{
|
items := []spec.Suggestion{
|
||||||
{Cmd: "docker exec -it my-container bash"},
|
{Cmd: "docker exec -it my-container bash"},
|
||||||
}
|
}
|
||||||
@@ -63,7 +62,7 @@ func TestGetGhostText(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestGhostText_MenuSync(t *testing.T) {
|
func TestGhostText_MenuSync(t *testing.T) {
|
||||||
o := integration.NewOverlay()
|
o := NewOverlay()
|
||||||
items := []spec.Suggestion{
|
items := []spec.Suggestion{
|
||||||
{Cmd: "git checkout -b first"},
|
{Cmd: "git checkout -b first"},
|
||||||
{Cmd: "git checkout master"},
|
{Cmd: "git checkout master"},
|
||||||
@@ -90,7 +89,7 @@ func TestGhostText_MenuSync(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestGhostText_Truncation(t *testing.T) {
|
func TestGhostText_Truncation(t *testing.T) {
|
||||||
o := integration.NewOverlay()
|
o := NewOverlay()
|
||||||
longCmd := "git commit -m '" + strings.Repeat("a", 150) + "'"
|
longCmd := "git commit -m '" + strings.Repeat("a", 150) + "'"
|
||||||
items := []spec.Suggestion{
|
items := []spec.Suggestion{
|
||||||
{Cmd: longCmd},
|
{Cmd: longCmd},
|
||||||
@@ -107,7 +106,7 @@ func TestGhostText_Truncation(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestHideMenu_PreservesTypedQueryForAI(t *testing.T) {
|
func TestHideMenu_PreservesTypedQueryForAI(t *testing.T) {
|
||||||
o := integration.NewOverlay()
|
o := NewOverlay()
|
||||||
o.HideMenu("git commit")
|
o.HideMenu("git commit")
|
||||||
|
|
||||||
if o.GetTypedQuery() != "git commit" {
|
if o.GetTypedQuery() != "git commit" {
|
||||||
@@ -1,10 +1,8 @@
|
|||||||
package tests
|
package shell
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/versenilvis/iris/integration/shell"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestScanPosixAliases(t *testing.T) {
|
func TestScanPosixAliases(t *testing.T) {
|
||||||
@@ -22,7 +20,7 @@ alias l='ls' ll='ls -l'
|
|||||||
"ll": "ls -l",
|
"ll": "ls -l",
|
||||||
}
|
}
|
||||||
|
|
||||||
got := shell.ParseAliases(input)
|
got := ParseAliases(input)
|
||||||
if !reflect.DeepEqual(got, expected) {
|
if !reflect.DeepEqual(got, expected) {
|
||||||
t.Errorf("ScanPosixAliases() = %v; want %v", got, expected)
|
t.Errorf("ScanPosixAliases() = %v; want %v", got, expected)
|
||||||
}
|
}
|
||||||
@@ -43,7 +41,7 @@ func TestSplitAliasTokens(t *testing.T) {
|
|||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
got := shell.SplitAliasTokens(tt.input)
|
got := SplitAliasTokens(tt.input)
|
||||||
if !reflect.DeepEqual(got, tt.expected) {
|
if !reflect.DeepEqual(got, tt.expected) {
|
||||||
t.Errorf("SplitAliasTokens(%q) = %v; want %v", tt.input, got, tt.expected)
|
t.Errorf("SplitAliasTokens(%q) = %v; want %v", tt.input, got, tt.expected)
|
||||||
}
|
}
|
||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/versenilvis/iris/config"
|
"github.com/versenilvis/iris/internal/config"
|
||||||
"github.com/versenilvis/iris/spec"
|
"github.com/versenilvis/iris/spec"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package tests
|
package ai
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@@ -9,8 +9,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/versenilvis/iris/ai"
|
"github.com/versenilvis/iris/internal/config"
|
||||||
"github.com/versenilvis/iris/config"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestCleanSuggestion(t *testing.T) {
|
func TestCleanSuggestion(t *testing.T) {
|
||||||
@@ -28,7 +27,7 @@ func TestCleanSuggestion(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
got := ai.CleanSuggestion(tt.input)
|
got := CleanSuggestion(tt.input)
|
||||||
if got != tt.expected {
|
if got != tt.expected {
|
||||||
t.Errorf("CleanSuggestion(%q) = %q, want %q", tt.input, 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 {
|
if err != nil {
|
||||||
t.Fatalf("failed to create client: %v", err)
|
t.Fatalf("failed to create client: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx := context.Background()
|
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, "")
|
sugg, err := client.Suggest(ctx, "kubectl get p", env, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("suggest failed: %v", err)
|
t.Fatalf("suggest failed: %v", err)
|
||||||
@@ -119,9 +118,9 @@ func TestOpenAIClient_TimeoutAndCancel(t *testing.T) {
|
|||||||
TimeoutMS: 50,
|
TimeoutMS: 50,
|
||||||
}
|
}
|
||||||
|
|
||||||
client := ai.NewOpenAIClient(cfg)
|
client := NewOpenAIClient(cfg)
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
env := ai.EnvSnapshot{}
|
env := EnvSnapshot{}
|
||||||
|
|
||||||
_, err := client.Suggest(ctx, "sleep", env, "")
|
_, err := client.Suggest(ctx, "sleep", env, "")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package tests
|
package ai
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@@ -9,7 +9,6 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/versenilvis/iris/ai"
|
|
||||||
"github.com/versenilvis/iris/spec"
|
"github.com/versenilvis/iris/spec"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -34,7 +33,7 @@ func (m *mockProvider) Gather(ctx context.Context) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestProviderCache_TTL(t *testing.T) {
|
func TestProviderCache_TTL(t *testing.T) {
|
||||||
cache := ai.NewProviderCache(50 * time.Millisecond)
|
cache := NewProviderCache(50 * time.Millisecond)
|
||||||
provider := &mockProvider{
|
provider := &mockProvider{
|
||||||
name: "test-prov",
|
name: "test-prov",
|
||||||
matchPref: "test",
|
matchPref: "test",
|
||||||
@@ -71,7 +70,7 @@ func TestAIEngine_DynamicContext(t *testing.T) {
|
|||||||
gatherRet: "test-container\tnginx",
|
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" {
|
if dynamicCtx != "test-container\tnginx" {
|
||||||
t.Fatalf("expected dynamicCtx to be passed to handler, got: %q", dynamicCtx)
|
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)
|
engine.RegisterProvider(provider)
|
||||||
|
|
||||||
ctx := context.Background()
|
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 {
|
if err != nil || sugg == nil {
|
||||||
t.Fatalf("expected suggestion, got err: %v, sugg: %+v", err, sugg)
|
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
|
// Verify that cache evicts expired entries and resets when exceeding 50 items to prevent unbounded memory growth
|
||||||
func TestProviderCache_Eviction(t *testing.T) {
|
func TestProviderCache_Eviction(t *testing.T) {
|
||||||
cache := ai.NewProviderCache(10 * time.Millisecond)
|
cache := NewProviderCache(15 * time.Millisecond)
|
||||||
ctx := context.Background()
|
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{
|
p := &mockProvider{
|
||||||
name: fmt.Sprintf("prov-%d", i),
|
name: fmt.Sprintf("prov-%d", i),
|
||||||
matchPref: "test",
|
matchPref: "test",
|
||||||
@@ -103,19 +103,63 @@ func TestProviderCache_Eviction(t *testing.T) {
|
|||||||
cache.GetOrGather(ctx, p)
|
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{
|
pNext := &mockProvider{
|
||||||
name: "prov-next",
|
name: "prov-100",
|
||||||
matchPref: "test",
|
matchPref: "test",
|
||||||
gatherRet: "data",
|
gatherRet: "data",
|
||||||
}
|
}
|
||||||
cache.GetOrGather(ctx, pNext)
|
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
|
// Verify that CommandContextProvider caps gathered output to 1000 characters to protect token budget
|
||||||
func TestCommandContextProvider_Truncation(t *testing.T) {
|
func TestCommandContextProvider_Truncation(t *testing.T) {
|
||||||
provider := &ai.CommandContextProvider{
|
provider := &CommandContextProvider{
|
||||||
NameStr: "test-trunc",
|
NameStr: "test-trunc",
|
||||||
Prefixes: []string{"echo"},
|
Prefixes: []string{"echo"},
|
||||||
GatherCmd: []string{"go", "env"},
|
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
|
// Verify that concurrent provider registration and context gathering do not cause data races
|
||||||
func TestAIEngine_ConcurrentRegistrationAndGather(t *testing.T) {
|
func TestAIEngine_ConcurrentRegistrationAndGather(t *testing.T) {
|
||||||
engine := ai.NewAIEngine(nil)
|
engine := NewAIEngine(nil)
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package tests
|
package ai
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@@ -9,7 +9,6 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/versenilvis/iris/ai"
|
|
||||||
"github.com/versenilvis/iris/spec"
|
"github.com/versenilvis/iris/spec"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -18,43 +17,43 @@ type mockAISuggester struct {
|
|||||||
ret *spec.Suggestion
|
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)
|
atomic.AddInt32(&m.calls, 1)
|
||||||
return m.ret, nil
|
return m.ret, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRuleBasedSuggester(t *testing.T) {
|
func TestRuleBasedSuggester(t *testing.T) {
|
||||||
rule := ai.RuleBasedSuggester{}
|
rule := RuleBasedSuggester{}
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
// case 1: retry failed command
|
// Case 1: Last exit code != 0
|
||||||
sugg1, _ := rule.SuggestOnEmpty(ctx, ai.EnvSnapshot{LastExitCode: 1, LastCmd: "make build"})
|
ctx := context.Background()
|
||||||
|
sugg1, _ := rule.SuggestOnEmpty(ctx, EnvSnapshot{LastExitCode: 1, LastCmd: "make build"})
|
||||||
if sugg1 == nil || sugg1.Cmd != "make build" || sugg1.Confidence != 80 {
|
if sugg1 == nil || sugg1.Cmd != "make build" || sugg1.Confidence != 80 {
|
||||||
t.Fatalf("expected retry make build with conf 80, got: %+v", sugg1)
|
t.Fatalf("expected retry make build with conf 80, got: %+v", sugg1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// case 2: git status after git status
|
// 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 {
|
if sugg2 == nil || sugg2.Cmd != "git diff" || sugg2.Confidence != 75 {
|
||||||
t.Fatalf("expected git diff with conf 75, got: %+v", sugg2)
|
t.Fatalf("expected git diff with conf 75, got: %+v", sugg2)
|
||||||
}
|
}
|
||||||
|
|
||||||
// case 3: modified git files
|
// 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 {
|
if sugg3 == nil || sugg3.Cmd != "git status" || sugg3.Confidence != 70 {
|
||||||
t.Fatalf("expected git status with conf 70, got: %+v", sugg3)
|
t.Fatalf("expected git status with conf 70, got: %+v", sugg3)
|
||||||
}
|
}
|
||||||
|
|
||||||
// case 4: package.json signature (conf 65)
|
// 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 {
|
if sugg4 == nil || sugg4.Cmd != "npm run dev" || sugg4.Confidence != 65 {
|
||||||
t.Fatalf("expected npm run dev with conf 65, got: %+v", sugg4)
|
t.Fatalf("expected npm run dev with conf 65, got: %+v", sugg4)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestContextCache_ShouldCallAI(t *testing.T) {
|
func TestContextCache_ShouldCallAI(t *testing.T) {
|
||||||
cache := ai.NewContextCache()
|
cache := NewContextCache()
|
||||||
snap := ai.EnvSnapshot{Cwd: "/test", GitStatus: "clean"}
|
snap := EnvSnapshot{Cwd: "/test", GitStatus: "clean"}
|
||||||
|
|
||||||
// first call -> true
|
// first call -> true
|
||||||
if !cache.ShouldCallAI(snap, 50*time.Millisecond) {
|
if !cache.ShouldCallAI(snap, 50*time.Millisecond) {
|
||||||
@@ -80,17 +79,16 @@ func TestContextCache_ShouldCallAI(t *testing.T) {
|
|||||||
func TestEmptyLinePredictor_TwoTier(t *testing.T) {
|
func TestEmptyLinePredictor_TwoTier(t *testing.T) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
mockAI := &mockAISuggester{ret: &spec.Suggestion{Cmd: "ai-suggested-cmd", Confidence: 85}}
|
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 := EnvSnapshot{LastExitCode: 1, LastCmd: "failed-cmd"}
|
||||||
env1 := ai.EnvSnapshot{LastExitCode: 1, LastCmd: "failed-cmd"}
|
|
||||||
sugg1, _ := predictor.Predict(ctx, env1, true)
|
sugg1, _ := predictor.Predict(ctx, env1, true)
|
||||||
if sugg1 == nil || sugg1.Cmd != "failed-cmd" || atomic.LoadInt32(&mockAI.calls) != 0 {
|
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)
|
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
|
// 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)
|
sugg2, _ := predictor.Predict(ctx, env2, true)
|
||||||
if sugg2 == nil || sugg2.Cmd != "ai-suggested-cmd" || atomic.LoadInt32(&mockAI.calls) != 1 {
|
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)
|
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)
|
_ = os.WriteFile(filepath.Join(tmp, "Makefile"), content, 0644)
|
||||||
|
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
ai.ExtractScriptsAndTargets(&sb, tmp, "")
|
ExtractScriptsAndTargets(&sb, tmp, "")
|
||||||
res := sb.String()
|
res := sb.String()
|
||||||
|
|
||||||
if !strings.Contains(res, "build") || !strings.Contains(res, "all") {
|
if !strings.Contains(res, "build") || !strings.Contains(res, "all") {
|
||||||
@@ -6,8 +6,8 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/versenilvis/iris/config"
|
"github.com/versenilvis/iris/internal/config"
|
||||||
"github.com/versenilvis/iris/logger"
|
"github.com/versenilvis/iris/internal/logger"
|
||||||
"github.com/versenilvis/iris/spec"
|
"github.com/versenilvis/iris/spec"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package tests
|
package ai
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@@ -8,13 +8,12 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/versenilvis/iris/ai"
|
"github.com/versenilvis/iris/internal/config"
|
||||||
"github.com/versenilvis/iris/config"
|
|
||||||
"github.com/versenilvis/iris/spec"
|
"github.com/versenilvis/iris/spec"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestEnvSnapshot_Hash(t *testing.T) {
|
func TestEnvSnapshot_Hash(t *testing.T) {
|
||||||
snap1 := ai.EnvSnapshot{
|
snap1 := EnvSnapshot{
|
||||||
Cwd: "/home/user",
|
Cwd: "/home/user",
|
||||||
LastCmd: "ls -l",
|
LastCmd: "ls -l",
|
||||||
LastExitCode: 0,
|
LastExitCode: 0,
|
||||||
@@ -33,8 +32,8 @@ func TestEnvSnapshot_Hash(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Verify that fields containing delimiter characters do not collide
|
// Verify that fields containing delimiter characters do not collide
|
||||||
snapA := ai.EnvSnapshot{Cwd: "/home/user", LastCmd: "foo|bar"}
|
snapA := EnvSnapshot{Cwd: "/home/user", LastCmd: "foo|bar"}
|
||||||
snapB := ai.EnvSnapshot{Cwd: "/home/user|foo", LastCmd: "bar"}
|
snapB := EnvSnapshot{Cwd: "/home/user|foo", LastCmd: "bar"}
|
||||||
if snapA.Hash() == snapB.Hash() {
|
if snapA.Hash() == snapB.Hash() {
|
||||||
t.Fatalf("expected different hash for distinct snapshots containing delimiter characters")
|
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()
|
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, "")
|
sugg, err := engine.Suggest(ctx, "git commit -m \"", snap, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("expected success, got err: %v", err)
|
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)
|
t.Fatalf("unexpected suggestion: %+v", sugg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAIEngine_Suggest_Cancel(t *testing.T) {
|
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)
|
time.Sleep(50 * time.Millisecond)
|
||||||
return &spec.Suggestion{Cmd: "test", Confidence: 90}, nil
|
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())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
cancel() // cancel immediately before calling
|
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 {
|
if err == nil || sugg != nil {
|
||||||
t.Fatalf("expected cancellation error and nil suggestion, got sugg: %+v, err: %v", sugg, err)
|
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{
|
newSugg := &spec.Suggestion{
|
||||||
Cmd: "git commit -m \"feat: new feature\"",
|
Cmd: "git commit -m \"feat: new feature\"",
|
||||||
Confidence: 85,
|
Confidence: 85,
|
||||||
Source: string(ai.SourceAI),
|
Source: string(SourceAI),
|
||||||
}
|
}
|
||||||
|
|
||||||
// case 1: exact match and higher confidence
|
// 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")
|
t.Fatalf("expected true for prefix match with higher confidence")
|
||||||
}
|
}
|
||||||
|
|
||||||
// case 2: user backspaced / typed different prefix
|
// 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")
|
t.Fatalf("expected false when current buf does not match original prefix")
|
||||||
}
|
}
|
||||||
|
|
||||||
// case 3: new suggestion does not match what user typed
|
// 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")
|
t.Fatalf("expected false when suggestion cmd does not match current buf")
|
||||||
}
|
}
|
||||||
|
|
||||||
// case 4: lower or equal confidence
|
// 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")
|
t.Fatalf("expected false when new confidence is lower")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify that caller mutations on returned fresh suggestions do not corrupt the internal engine cache
|
// Verify that caller mutations on returned fresh suggestions do not corrupt the internal engine cache
|
||||||
func TestAIEngine_Suggest_Immutability(t *testing.T) {
|
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
|
return &spec.Suggestion{Cmd: "echo original", Confidence: 90}, nil
|
||||||
})
|
})
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
sugg1, err := engine.Suggest(ctx, "echo ", ai.EnvSnapshot{}, "")
|
sugg1, err := engine.Suggest(ctx, "echo ", EnvSnapshot{}, "")
|
||||||
if err != nil || sugg1 == nil {
|
if err != nil || sugg1 == nil {
|
||||||
t.Fatalf("expected suggestion, got err: %v, sugg: %v", err, sugg1)
|
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"
|
sugg1.Cmd = "echo corrupted"
|
||||||
|
|
||||||
// Fetch from cache via prefix match
|
// 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 {
|
if err != nil || sugg2 == nil {
|
||||||
t.Fatalf("expected cached suggestion, got err: %v, sugg: %v", err, sugg2)
|
t.Fatalf("expected cached suggestion, got err: %v, sugg: %v", err, sugg2)
|
||||||
}
|
}
|
||||||
@@ -1,18 +1,14 @@
|
|||||||
package tests
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/versenilvis/iris/config"
|
|
||||||
"github.com/versenilvis/iris/root"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestDefaultConfigAndState(t *testing.T) {
|
func TestDefaultConfigAndState(t *testing.T) {
|
||||||
cfg := config.DefaultConfig()
|
cfg := DefaultConfig()
|
||||||
if cfg.Core.Version != 1 {
|
if cfg.Core.Version != 1 {
|
||||||
t.Errorf("expected version 1, got %d", cfg.Core.Version)
|
t.Errorf("expected version 1, got %d", cfg.Core.Version)
|
||||||
}
|
}
|
||||||
@@ -31,7 +27,7 @@ func TestDefaultConfigAndState(t *testing.T) {
|
|||||||
|
|
||||||
// test manual provider registration
|
// test manual provider registration
|
||||||
cfg.AI.Provider = "custom"
|
cfg.AI.Provider = "custom"
|
||||||
cfg.AI.Providers = map[string]config.ProviderConfig{
|
cfg.AI.Providers = map[string]ProviderConfig{
|
||||||
"custom": {
|
"custom": {
|
||||||
InheritedFrom: "openai",
|
InheritedFrom: "openai",
|
||||||
Endpoint: "https://custom-api.com/v1",
|
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)
|
t.Errorf("expected min interval 5000, got %d", cfg.AI.SuggestOnEmpty.MinIntervalMS)
|
||||||
}
|
}
|
||||||
|
|
||||||
state := config.DefaultState()
|
state := DefaultState()
|
||||||
if state.LastMode != "spec" {
|
if state.LastMode != "spec" {
|
||||||
t.Errorf("expected last mode spec, got %q", state.LastMode)
|
t.Errorf("expected last mode spec, got %q", state.LastMode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCustomDuration(t *testing.T) {
|
func TestCustomDuration(t *testing.T) {
|
||||||
var dur config.Duration
|
var dur Duration
|
||||||
err := dur.UnmarshalText([]byte("6h"))
|
err := dur.UnmarshalText([]byte("6h"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unexpected error unmarshalling duration: %v", err)
|
t.Fatalf("unexpected error unmarshalling duration: %v", err)
|
||||||
@@ -94,8 +90,7 @@ func TestValidationAndEnvironmentOverrides(t *testing.T) {
|
|||||||
}
|
}
|
||||||
defer os.RemoveAll(tmpDir)
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
_ = os.Setenv("XDG_CONFIG_HOME", tmpDir)
|
t.Setenv("XDG_CONFIG_HOME", tmpDir)
|
||||||
defer func() { _ = os.Unsetenv("XDG_CONFIG_HOME") }()
|
|
||||||
|
|
||||||
configDir := filepath.Join(tmpDir, "iris")
|
configDir := filepath.Join(tmpDir, "iris")
|
||||||
if mkErr := os.MkdirAll(configDir, 0755); mkErr != nil {
|
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)
|
t.Fatalf("failed to write config file: %v", wrErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = os.Setenv("IRIS_CORE_DEBUG", "true")
|
t.Setenv("IRIS_CORE_DEBUG", "true")
|
||||||
_ = os.Setenv("IRIS_CORE_SHELL", "fish")
|
t.Setenv("IRIS_CORE_SHELL", "fish")
|
||||||
_ = os.Setenv("IRIS_CORE_MODE", "history")
|
t.Setenv("IRIS_CORE_MODE", "history")
|
||||||
_ = os.Setenv("IRIS_UI_GHOST_TEXT", "false")
|
t.Setenv("IRIS_UI_GHOST_TEXT", "false")
|
||||||
_ = os.Setenv("IRIS_UI_MAX_SUGGESTIONS", "250")
|
t.Setenv("IRIS_UI_MAX_SUGGESTIONS", "250")
|
||||||
_ = os.Setenv("IRIS_UI_MAX_HEIGHT", "25")
|
t.Setenv("IRIS_UI_MAX_HEIGHT", "25")
|
||||||
_ = os.Setenv("IRIS_UPDATER_CHANNEL", "nightly")
|
t.Setenv("IRIS_UPDATER_CHANNEL", "nightly")
|
||||||
_ = os.Setenv("IRIS_UPDATER_INTERVAL", "12h")
|
t.Setenv("IRIS_UPDATER_INTERVAL", "12h")
|
||||||
_ = os.Setenv("IRIS_UPDATER_CHECK_ON_STARTUP", "false")
|
t.Setenv("IRIS_UPDATER_CHECK_ON_STARTUP", "false")
|
||||||
_ = os.Setenv("IRIS_AI_PROVIDER", "ollama")
|
t.Setenv("IRIS_AI_PROVIDER", "ollama")
|
||||||
_ = os.Setenv("GROQ_API_KEY", "gsk_test_123")
|
t.Setenv("GROQ_API_KEY", "gsk_test_123")
|
||||||
|
|
||||||
defer func() {
|
cfg, err := Load()
|
||||||
_ = 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()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to load config: %v", err)
|
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())
|
t.Errorf("expected groq api key gsk_test_123 from env, got %q", groqCfg.GetAPIKey())
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = os.Setenv("IRIS_CORE_MODE", "invalid")
|
t.Setenv("IRIS_CORE_MODE", "invalid")
|
||||||
_, err = config.Load()
|
_, err = Load()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Errorf("expected validation error for invalid mode in env")
|
t.Errorf("expected validation error for invalid mode in env")
|
||||||
}
|
}
|
||||||
@@ -197,10 +178,9 @@ func TestLoadSave(t *testing.T) {
|
|||||||
}
|
}
|
||||||
defer os.RemoveAll(tmpDir)
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
_ = os.Setenv("XDG_CONFIG_HOME", tmpDir)
|
t.Setenv("XDG_CONFIG_HOME", tmpDir)
|
||||||
defer func() { _ = os.Unsetenv("XDG_CONFIG_HOME") }()
|
|
||||||
|
|
||||||
cfg, err := config.Load()
|
cfg, err := Load()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to load: %v", err)
|
t.Fatalf("failed to load: %v", err)
|
||||||
}
|
}
|
||||||
@@ -208,12 +188,12 @@ func TestLoadSave(t *testing.T) {
|
|||||||
cfg.Core.Shell = "zsh"
|
cfg.Core.Shell = "zsh"
|
||||||
cfg.UI.MaxHeight = 20
|
cfg.UI.MaxHeight = 20
|
||||||
|
|
||||||
err = config.Save(cfg)
|
err = Save(cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to save: %v", err)
|
t.Fatalf("failed to save: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
loaded, err := config.Load()
|
loaded, err := Load()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to load after save: %v", err)
|
t.Fatalf("failed to load after save: %v", err)
|
||||||
}
|
}
|
||||||
@@ -233,12 +213,8 @@ func TestMigration(t *testing.T) {
|
|||||||
}
|
}
|
||||||
defer os.RemoveAll(tmpDir)
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
_ = os.Setenv("HOME", tmpDir)
|
t.Setenv("HOME", tmpDir)
|
||||||
_ = os.Setenv("XDG_DATA_HOME", filepath.Join(tmpDir, ".local", "share"))
|
t.Setenv("XDG_DATA_HOME", filepath.Join(tmpDir, ".local", "share"))
|
||||||
defer func() {
|
|
||||||
_ = os.Unsetenv("HOME")
|
|
||||||
_ = os.Unsetenv("XDG_DATA_HOME")
|
|
||||||
}()
|
|
||||||
|
|
||||||
legacyDir := filepath.Join(tmpDir, ".iris")
|
legacyDir := filepath.Join(tmpDir, ".iris")
|
||||||
if errMkdir := os.MkdirAll(legacyDir, 0755); errMkdir != nil {
|
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}`
|
legacyUpdateJson := `{"seen_version": "v1.2.3", "last_check": 1234567890}`
|
||||||
_ = os.WriteFile(filepath.Join(legacyDir, "update_state.json"), []byte(legacyUpdateJson), 0644)
|
_ = os.WriteFile(filepath.Join(legacyDir, "update_state.json"), []byte(legacyUpdateJson), 0644)
|
||||||
|
|
||||||
err = config.MigrateFromLegacyJSON()
|
err = MigrateFromLegacyJSON()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("migration failed: %v", err)
|
t.Fatalf("migration failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
state := config.LoadState()
|
state := LoadState()
|
||||||
if state.LastMode != "history" {
|
if state.LastMode != "history" {
|
||||||
t.Errorf("expected migrated last mode 'history', got %q", state.LastMode)
|
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 (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/versenilvis/iris/logger"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestLogger(t *testing.T) {
|
func TestLogger(t *testing.T) {
|
||||||
@@ -19,10 +17,10 @@ func TestLogger(t *testing.T) {
|
|||||||
logFilePath := filepath.Join(tempDir, "test.log")
|
logFilePath := filepath.Join(tempDir, "test.log")
|
||||||
|
|
||||||
// test 1: default init sets level to info
|
// test 1: default init sets level to info
|
||||||
logger.Init(logFilePath, false)
|
Init(logFilePath, false)
|
||||||
logger.Debugf("this debug msg should not be logged")
|
Debugf("this debug msg should not be logged")
|
||||||
logger.Infof("this info msg should be logged")
|
Infof("this info msg should be logged")
|
||||||
logger.Close()
|
Close()
|
||||||
|
|
||||||
data, err := os.ReadFile(logFilePath)
|
data, err := os.ReadFile(logFilePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -45,9 +43,9 @@ func TestLogger(t *testing.T) {
|
|||||||
|
|
||||||
// test 2: override init with debug = true
|
// test 2: override init with debug = true
|
||||||
_ = os.Remove(logFilePath)
|
_ = os.Remove(logFilePath)
|
||||||
logger.Init(logFilePath, true)
|
Init(logFilePath, true)
|
||||||
logger.Debugf("this debug msg should now be logged")
|
Debugf("this debug msg should now be logged")
|
||||||
logger.Close()
|
Close()
|
||||||
|
|
||||||
data, err = os.ReadFile(logFilePath)
|
data, err = os.ReadFile(logFilePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -70,9 +68,9 @@ func TestLogger(t *testing.T) {
|
|||||||
t.Fatalf("failed to write large file: %v", err)
|
t.Fatalf("failed to write large file: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Init(logFilePath, false)
|
Init(logFilePath, false)
|
||||||
logger.Infof("new log after rotation")
|
Infof("new log after rotation")
|
||||||
logger.Close()
|
Close()
|
||||||
|
|
||||||
// check if old file exists and is rotated
|
// check if old file exists and is rotated
|
||||||
oldPath := logFilePath + ".old"
|
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/BurntSushi/toml"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/versenilvis/iris/config"
|
"github.com/versenilvis/iris/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
var ConfigCmd = &cobra.Command{
|
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"
|
"time"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"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
|
// startRescueShell starts a fallback shell if the application crashes to keep the terminal open
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package tests
|
package root
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
@@ -6,8 +6,6 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/versenilvis/iris/root"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestWriteCrashLog(t *testing.T) {
|
func TestWriteCrashLog(t *testing.T) {
|
||||||
@@ -17,17 +15,11 @@ func TestWriteCrashLog(t *testing.T) {
|
|||||||
}
|
}
|
||||||
defer os.RemoveAll(tmpDir)
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
origHome := os.Getenv("HOME")
|
t.Setenv("HOME", tmpDir)
|
||||||
origCache := os.Getenv("XDG_CACHE_HOME")
|
t.Setenv("XDG_CACHE_HOME", filepath.Join(tmpDir, ".cache"))
|
||||||
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"))
|
|
||||||
|
|
||||||
testErr := "test panic message"
|
testErr := "test panic message"
|
||||||
root.WriteCrashLog(testErr)
|
WriteCrashLog(testErr)
|
||||||
|
|
||||||
dir := filepath.Join(tmpDir, ".cache", "iris", "crashes")
|
dir := filepath.Join(tmpDir, ".cache", "iris", "crashes")
|
||||||
files, err := os.ReadDir(dir)
|
files, err := os.ReadDir(dir)
|
||||||
@@ -63,35 +55,29 @@ func TestCrashLogCommand(t *testing.T) {
|
|||||||
}
|
}
|
||||||
defer os.RemoveAll(tmpDir)
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
origHome := os.Getenv("HOME")
|
t.Setenv("HOME", tmpDir)
|
||||||
origCache := os.Getenv("XDG_CACHE_HOME")
|
t.Setenv("XDG_CACHE_HOME", filepath.Join(tmpDir, ".cache"))
|
||||||
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"))
|
|
||||||
|
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
root.CrashCmd.SetOut(&buf)
|
CrashCmd.SetOut(&buf)
|
||||||
root.CrashCmd.SetArgs([]string{})
|
CrashCmd.SetArgs([]string{})
|
||||||
root.ClearLog = false
|
ClearLog = false
|
||||||
|
|
||||||
root.CrashCmd.Run(root.CrashCmd, []string{})
|
CrashCmd.Run(CrashCmd, []string{})
|
||||||
if !strings.Contains(buf.String(), "no crash log found") {
|
if !strings.Contains(buf.String(), "no crash log found") {
|
||||||
t.Errorf("expected 'no crash log found', got: %q", buf.String())
|
t.Errorf("expected 'no crash log found', got: %q", buf.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
root.WriteCrashLog("mock error")
|
WriteCrashLog("mock error")
|
||||||
buf.Reset()
|
buf.Reset()
|
||||||
root.CrashCmd.Run(root.CrashCmd, []string{})
|
CrashCmd.Run(CrashCmd, []string{})
|
||||||
if !strings.Contains(buf.String(), "crash_") || !strings.Contains(buf.String(), ".log") {
|
if !strings.Contains(buf.String(), "crash_") || !strings.Contains(buf.String(), ".log") {
|
||||||
t.Errorf("expected crash log path, got: %q", buf.String())
|
t.Errorf("expected crash log path, got: %q", buf.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
buf.Reset()
|
buf.Reset()
|
||||||
root.ClearLog = true
|
ClearLog = true
|
||||||
root.CrashCmd.Run(root.CrashCmd, []string{})
|
CrashCmd.Run(CrashCmd, []string{})
|
||||||
if !strings.Contains(buf.String(), "crash log cleared") {
|
if !strings.Contains(buf.String(), "crash log cleared") {
|
||||||
t.Errorf("expected 'crash log cleared', got: %q", buf.String())
|
t.Errorf("expected 'crash log cleared', got: %q", buf.String())
|
||||||
}
|
}
|
||||||
+1
-1
@@ -7,7 +7,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/versenilvis/iris/config"
|
"github.com/versenilvis/iris/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
var initCmd = &cobra.Command{
|
var initCmd = &cobra.Command{
|
||||||
|
|||||||
+2
-2
@@ -15,8 +15,8 @@ import (
|
|||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
_ "github.com/versenilvis/iris/commands"
|
_ "github.com/versenilvis/iris/commands"
|
||||||
"github.com/versenilvis/iris/config"
|
"github.com/versenilvis/iris/internal/config"
|
||||||
"github.com/versenilvis/iris/logger"
|
"github.com/versenilvis/iris/internal/logger"
|
||||||
"golang.org/x/term"
|
"golang.org/x/term"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -4,10 +4,10 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/versenilvis/iris/ai"
|
|
||||||
"github.com/versenilvis/iris/config"
|
|
||||||
"github.com/versenilvis/iris/integration"
|
"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"
|
"github.com/versenilvis/iris/spec"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
package tests
|
package root
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/versenilvis/iris/root"
|
|
||||||
"github.com/versenilvis/iris/spec"
|
"github.com/versenilvis/iris/spec"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -11,7 +10,7 @@ func TestMergeResults(t *testing.T) {
|
|||||||
|
|
||||||
t.Run("Dedup exact match", func(t *testing.T) {
|
t.Run("Dedup exact match", func(t *testing.T) {
|
||||||
// Mock history items that might conflict with specs
|
// Mock history items that might conflict with specs
|
||||||
res := root.MergeResults("git", "spec")
|
res := MergeResults("git", "spec")
|
||||||
seen := make(map[string]bool)
|
seen := make(map[string]bool)
|
||||||
for _, r := range res {
|
for _, r := range res {
|
||||||
if seen[r.Cmd] {
|
if seen[r.Cmd] {
|
||||||
@@ -23,7 +22,7 @@ func TestMergeResults(t *testing.T) {
|
|||||||
|
|
||||||
|
|
||||||
t.Run("Limit 100", func(t *testing.T) {
|
t.Run("Limit 100", func(t *testing.T) {
|
||||||
res := root.MergeResults("a", "history")
|
res := MergeResults("a", "history")
|
||||||
if len(res) > 100 {
|
if len(res) > 100 {
|
||||||
t.Errorf("Expected max 100 suggestions, got %d", len(res))
|
t.Errorf("Expected max 100 suggestions, got %d", len(res))
|
||||||
}
|
}
|
||||||
@@ -36,10 +35,10 @@ func TestMergeResults(t *testing.T) {
|
|||||||
Source: "ai",
|
Source: "ai",
|
||||||
Confidence: 85,
|
Confidence: 85,
|
||||||
}
|
}
|
||||||
root.SetCurrentAISuggestion(aiSugg)
|
SetCurrentAISuggestion(aiSugg)
|
||||||
defer root.SetCurrentAISuggestion(nil)
|
defer SetCurrentAISuggestion(nil)
|
||||||
|
|
||||||
res := root.MergeResults("git c", "history")
|
res := MergeResults("git c", "history")
|
||||||
if len(res) == 0 {
|
if len(res) == 0 {
|
||||||
t.Fatalf("Expected suggestions, got 0")
|
t.Fatalf("Expected suggestions, got 0")
|
||||||
}
|
}
|
||||||
+1
-1
@@ -8,7 +8,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/versenilvis/iris/config"
|
"github.com/versenilvis/iris/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
|||||||
+1
-1
@@ -13,7 +13,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"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
|
// updateResult is passed from the async checker to the main loop
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package tests
|
package root
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
@@ -6,8 +6,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/versenilvis/iris/config"
|
"github.com/versenilvis/iris/internal/config"
|
||||||
"github.com/versenilvis/iris/root"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestIsNewer(t *testing.T) {
|
func TestIsNewer(t *testing.T) {
|
||||||
@@ -29,7 +28,7 @@ func TestIsNewer(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
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)
|
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) }()
|
defer func() { _ = os.RemoveAll(tmpDir) }()
|
||||||
|
|
||||||
// Override home dir for testing
|
t.Setenv("HOME", tmpDir)
|
||||||
homeBackup := os.Getenv("HOME")
|
t.Setenv("XDG_DATA_HOME", filepath.Join(tmpDir, ".local", "share"))
|
||||||
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) }()
|
|
||||||
|
|
||||||
state := config.LoadState()
|
state := config.LoadState()
|
||||||
state.Updater.SeenVersion = "v1.0.0"
|
state.Updater.SeenVersion = "v1.0.0"
|
||||||
+3
-3
@@ -17,11 +17,11 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/creack/pty"
|
"github.com/creack/pty"
|
||||||
"github.com/versenilvis/iris/ai"
|
|
||||||
"github.com/versenilvis/iris/config"
|
|
||||||
"github.com/versenilvis/iris/integration"
|
"github.com/versenilvis/iris/integration"
|
||||||
"github.com/versenilvis/iris/integration/shell"
|
"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"
|
"github.com/versenilvis/iris/spec"
|
||||||
"golang.org/x/sys/unix"
|
"golang.org/x/sys/unix"
|
||||||
"golang.org/x/term"
|
"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 (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/versenilvis/iris/spec"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestFileGenerator(t *testing.T) {
|
func TestFileGenerator(t *testing.T) {
|
||||||
@@ -24,7 +22,7 @@ func TestFileGenerator(t *testing.T) {
|
|||||||
|
|
||||||
|
|
||||||
t.Run("dirOnly shows only dirs", func(t *testing.T) {
|
t.Run("dirOnly shows only dirs", func(t *testing.T) {
|
||||||
gen := spec.FileGenerator("/")
|
gen := FileGenerator("/")
|
||||||
results := gen([]string{"cd", ""}, "cd ", "")
|
results := gen([]string{"cd", ""}, "cd ", "")
|
||||||
for _, r := range results {
|
for _, r := range results {
|
||||||
if !strings.HasSuffix(r.Cmd, "/") {
|
if !strings.HasSuffix(r.Cmd, "/") {
|
||||||
@@ -35,7 +33,7 @@ func TestFileGenerator(t *testing.T) {
|
|||||||
|
|
||||||
|
|
||||||
t.Run("Filter extension", func(t *testing.T) {
|
t.Run("Filter extension", func(t *testing.T) {
|
||||||
gen := spec.FileGenerator(".go")
|
gen := FileGenerator(".go")
|
||||||
results := gen([]string{"ls", ""}, "ls ", "")
|
results := gen([]string{"ls", ""}, "ls ", "")
|
||||||
foundMain := false
|
foundMain := false
|
||||||
for _, r := range results {
|
for _, r := range results {
|
||||||
@@ -53,7 +51,7 @@ func TestFileGenerator(t *testing.T) {
|
|||||||
|
|
||||||
|
|
||||||
t.Run("Nested path", func(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")
|
results := gen([]string{"ls", "src/u"}, "ls src/u", "src/u")
|
||||||
foundUtils := false
|
foundUtils := false
|
||||||
for _, r := range results {
|
for _, r := range results {
|
||||||
@@ -68,7 +66,7 @@ func TestFileGenerator(t *testing.T) {
|
|||||||
|
|
||||||
|
|
||||||
t.Run("Deep scan 1 level", func(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/")
|
results := gen([]string{"ls", "src/"}, "ls src/", "src/")
|
||||||
foundUtils := false
|
foundUtils := false
|
||||||
for _, r := range results {
|
for _, r := range results {
|
||||||
@@ -85,7 +83,7 @@ func TestFileGenerator(t *testing.T) {
|
|||||||
|
|
||||||
|
|
||||||
t.Run("Hidden files are skipped", func(t *testing.T) {
|
t.Run("Hidden files are skipped", func(t *testing.T) {
|
||||||
gen := spec.FileGenerator()
|
gen := FileGenerator()
|
||||||
results := gen([]string{"ls", ""}, "ls ", "")
|
results := gen([]string{"ls", ""}, "ls ", "")
|
||||||
for _, r := range results {
|
for _, r := range results {
|
||||||
if strings.HasPrefix(r.Cmd, ".") {
|
if strings.HasPrefix(r.Cmd, ".") {
|
||||||
+1
-1
@@ -7,7 +7,7 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/versenilvis/iris/integration/shell"
|
"github.com/versenilvis/iris/integration/shell"
|
||||||
"github.com/versenilvis/iris/logger"
|
"github.com/versenilvis/iris/internal/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|||||||
@@ -1,27 +1,25 @@
|
|||||||
package tests
|
package spec
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/versenilvis/iris/spec"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestLookup(t *testing.T) {
|
func TestLookup(t *testing.T) {
|
||||||
// Setup Registry
|
// Setup Registry
|
||||||
spec.Registry = make(map[string]*spec.Spec)
|
Registry = make(map[string]*Spec)
|
||||||
spec.Register(&spec.Spec{
|
Register(&Spec{
|
||||||
Name: "git",
|
Name: "git",
|
||||||
Subcommands: []spec.Subcommand{
|
Subcommands: []Subcommand{
|
||||||
{Name: "commit", Options: []spec.Option{{Name: "--message"}}, MaxArgs: 1},
|
{Name: "commit", Options: []Option{{Name: "--message"}}, MaxArgs: 1},
|
||||||
{Name: "remote", Subcommands: []spec.Subcommand{{Name: "add"}}},
|
{Name: "remote", Subcommands: []Subcommand{{Name: "add"}}},
|
||||||
},
|
},
|
||||||
Options: []spec.Option{{Name: "--verbose"}},
|
Options: []Option{{Name: "--verbose"}},
|
||||||
})
|
})
|
||||||
|
|
||||||
// Setup Aliases
|
// Setup Aliases
|
||||||
spec.ShellAliases = map[string]string{
|
ShellAliases = map[string]string{
|
||||||
"gca": "git commit -a",
|
"gca": "git commit -a",
|
||||||
"ta": "tmux a -t",
|
"ta": "tmux a -t",
|
||||||
}
|
}
|
||||||
@@ -44,7 +42,7 @@ func TestLookup(t *testing.T) {
|
|||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
results := spec.Lookup(tt.input)
|
results := Lookup(tt.input)
|
||||||
if len(results) < tt.minResults {
|
if len(results) < tt.minResults {
|
||||||
t.Errorf("Lookup(%q) got %d results; want at least %d", tt.input, 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) {
|
func TestLookupConcurrent(t *testing.T) {
|
||||||
spec.Registry = make(map[string]*spec.Spec)
|
Registry = make(map[string]*Spec)
|
||||||
spec.Register(&spec.Spec{
|
Register(&Spec{
|
||||||
Name: "git",
|
Name: "git",
|
||||||
Subcommands: []spec.Subcommand{
|
Subcommands: []Subcommand{
|
||||||
{Name: "commit", Options: []spec.Option{{Name: "--message"}}, MaxArgs: 1},
|
{Name: "commit", Options: []Option{{Name: "--message"}}, MaxArgs: 1},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
spec.ShellAliases = map[string]string{
|
ShellAliases = map[string]string{
|
||||||
"gca": "git commit -a",
|
"gca": "git commit -a",
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,8 +84,8 @@ func TestLookupConcurrent(t *testing.T) {
|
|||||||
go func() {
|
go func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
for range iterations {
|
for range iterations {
|
||||||
_ = spec.Lookup("gca")
|
_ = Lookup("gca")
|
||||||
_ = spec.Lookup("git ")
|
_ = Lookup("git ")
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,8 @@
|
|||||||
package tests
|
package spec
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/versenilvis/iris/spec"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestTokenize(t *testing.T) {
|
func TestTokenize(t *testing.T) {
|
||||||
@@ -24,7 +22,7 @@ func TestTokenize(t *testing.T) {
|
|||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
got := spec.Tokenize(tt.input)
|
got := Tokenize(tt.input)
|
||||||
if !reflect.DeepEqual(got, tt.expected) {
|
if !reflect.DeepEqual(got, tt.expected) {
|
||||||
t.Errorf("Tokenize(%q) = %v; want %v", tt.input, 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 {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
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)
|
t.Errorf("HasPrefix(%q, %q) = %v; want %v", tt.s, tt.prefix, got, tt.want)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -4,12 +4,10 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
_ "github.com/versenilvis/iris/commands"
|
|
||||||
"github.com/versenilvis/iris/spec"
|
|
||||||
"github.com/versenilvis/iris/commands/js"
|
"github.com/versenilvis/iris/commands/js"
|
||||||
|
"github.com/versenilvis/iris/spec"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestNpmScriptGenerator(t *testing.T) {
|
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\""
|
script := "#!/bin/sh\necho \"/home/verse/project1\n/home/verse/docs\n/home/verse/dev/iris\""
|
||||||
_ = os.WriteFile(mockZoxide, []byte(script), 0755)
|
_ = os.WriteFile(mockZoxide, []byte(script), 0755)
|
||||||
|
|
||||||
// Add tmp to PATH
|
t.Setenv("PATH", tmp+string(os.PathListSeparator)+os.Getenv("PATH"))
|
||||||
oldPath := os.Getenv("PATH")
|
|
||||||
_ = os.Setenv("PATH", tmp+string(os.PathListSeparator)+oldPath)
|
|
||||||
defer func() { _ = os.Setenv("PATH", oldPath) }()
|
|
||||||
|
|
||||||
gen := fs.ZoxideGenerator()
|
gen := fs.ZoxideGenerator()
|
||||||
|
|
||||||
@@ -27,10 +27,7 @@ func TestIntegration_ZoxideMultiWord(t *testing.T) {
|
|||||||
// Create mock zoxide binary
|
// Create mock zoxide binary
|
||||||
script := "#!/bin/sh\necho \"" + targetDir + "\""
|
script := "#!/bin/sh\necho \"" + targetDir + "\""
|
||||||
_ = os.WriteFile(mockZoxide, []byte(script), 0755)
|
_ = os.WriteFile(mockZoxide, []byte(script), 0755)
|
||||||
// Add mock bin to PATH
|
t.Setenv("PATH", mockBinDir+string(os.PathListSeparator)+os.Getenv("PATH"))
|
||||||
oldPath := os.Getenv("PATH")
|
|
||||||
_ = os.Setenv("PATH", mockBinDir+string(os.PathListSeparator)+oldPath)
|
|
||||||
defer func() { _ = os.Setenv("PATH", oldPath) }()
|
|
||||||
|
|
||||||
t.Run("z matches multi-word folder without quotes", func(t *testing.T) {
|
t.Run("z matches multi-word folder without quotes", func(t *testing.T) {
|
||||||
// Simulating user typing "z My Awe"
|
// Simulating user typing "z My Awe"
|
||||||
|
|||||||
Reference in New Issue
Block a user