Enhance lookup mechanics and priority propagation (`spec/lookup.go`) Remove the requirement of typing a leading `-` when searching for options, allowing flag suggestions to surface naturally during input Accurately forward the `Priority` field from registry specs into the `Suggestion` struct so the scoring engine evaluates relevance properly Integrate scoring into the main suggestion pipeline (`root/suggestions.go`) Collect context signals using `scoring.CollectSignals(cwd, query, rootCmd, store)` with the live working directory retrieved from `spec.GetCWD()` Pass the entire deduplicated list across spec, history and AI items through `scoring.Score(deduped, signals)` to compute scores and sort by descending order before rendering on screen Record execution history in Frecency Store (`root/wrapper.go`) Save the last executed command to `lastSubmittedCommand` whenever Enter is pressed or a suggestion is selected Trigger `store.Record(...)` asynchronously within the `IRIS_CMD_STOP` hook using a dedicated goroutine guarded by panic recovery and timeout limits, preventing blocks on the main shell thread Optimize workspace identification for AI (`internal/ai/context_provider.go`) Use `workspace.DetectCached(cwd)` to instantly fetch ecosystem context across Git, Node, Go, Rust, Python and Docker alongside active git branch state This caching mechanism avoids redundant subprocess execution when outside git repositories, significantly speeding up prompt context generation
159 lines
4.7 KiB
Go
159 lines
4.7 KiB
Go
package scoring
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/versenilvis/iris/internal/workspace"
|
|
"github.com/versenilvis/iris/spec"
|
|
)
|
|
|
|
func TestScore_GitInitAndStatusInGitRepo(t *testing.T) {
|
|
suggestions := []spec.Suggestion{
|
|
{Cmd: "git init", Source: "spec"},
|
|
{Cmd: "git status -s", Source: "spec"},
|
|
}
|
|
signals := SignalSet{
|
|
Workspace: workspace.WorkspaceInfo{HasGit: true},
|
|
Query: "git",
|
|
}
|
|
|
|
scored := Score(suggestions, signals)
|
|
if len(scored) != 2 {
|
|
t.Fatalf("expected 2 scored suggestions, got %d", len(scored))
|
|
}
|
|
if scored[0].Cmd != "git status -s" {
|
|
t.Errorf("expected 'git status -s' at top when inside git repo, got %s", scored[0].Cmd)
|
|
}
|
|
if scored[1].Cmd != "git init" {
|
|
t.Errorf("expected 'git init' at bottom, got %s", scored[1].Cmd)
|
|
}
|
|
if scored[1].Breakdown.ContextBonus != -50 {
|
|
t.Errorf("expected -50 penalty for git init, got %d", scored[1].Breakdown.ContextBonus)
|
|
}
|
|
}
|
|
|
|
func TestScore_NormalizedFrecency(t *testing.T) {
|
|
suggestions := []spec.Suggestion{
|
|
{Cmd: "ls -la", Source: "history"},
|
|
{Cmd: "git push", Source: "history"},
|
|
}
|
|
|
|
now := time.Now()
|
|
signals := SignalSet{
|
|
LocalFrecency: []FrecencyEntry{
|
|
{Cmd: "ls -la", RawScore: 25000.0, LastUsed: now.Add(-30 * 24 * time.Hour)},
|
|
{Cmd: "git push", RawScore: 500.0, LastUsed: now},
|
|
},
|
|
}
|
|
|
|
scored := Score(suggestions, signals)
|
|
var lsBreakdown, pushBreakdown ScoreBreakdown
|
|
for _, s := range scored {
|
|
switch s.Cmd {
|
|
case "ls -la":
|
|
lsBreakdown = s.Breakdown
|
|
case "git push":
|
|
pushBreakdown = s.Breakdown
|
|
}
|
|
}
|
|
|
|
if lsBreakdown.Frecency != 100 {
|
|
t.Errorf("expected max raw score to normalize to 100, got %d", lsBreakdown.Frecency)
|
|
}
|
|
if pushBreakdown.Frecency <= 0 || pushBreakdown.Frecency > 100 {
|
|
t.Errorf("expected normalized frecency in (0, 100], got %d", pushBreakdown.Frecency)
|
|
}
|
|
}
|
|
|
|
func TestScore_PrefixOverFuzzyMatch(t *testing.T) {
|
|
suggestions := []spec.Suggestion{
|
|
{Cmd: "make build", Source: "spec"}, // fuzzy/contains for 'bl'
|
|
{Cmd: "block", Source: "spec"}, // prefix exact for 'bl'
|
|
}
|
|
signals := SignalSet{Query: "bl"}
|
|
|
|
scored := Score(suggestions, signals)
|
|
if len(scored) != 2 {
|
|
t.Fatalf("expected 2 scored suggestions, got %d", len(scored))
|
|
}
|
|
if scored[0].Cmd != "block" {
|
|
t.Errorf("expected prefix match 'block' to outscore fuzzy 'make build', got %s", scored[0].Cmd)
|
|
}
|
|
}
|
|
|
|
func TestScore_AISuggestionConfidence(t *testing.T) {
|
|
suggestions := []spec.Suggestion{
|
|
{Cmd: "npm run custom-script", Source: "ai", Confidence: 85},
|
|
{Cmd: "npm help", Source: "history"},
|
|
}
|
|
signals := SignalSet{
|
|
Workspace: workspace.WorkspaceInfo{HasNodeProject: true},
|
|
Query: "npm",
|
|
}
|
|
|
|
scored := Score(suggestions, signals)
|
|
if len(scored) < 1 {
|
|
t.Fatalf("expected scored suggestions")
|
|
}
|
|
if scored[0].Cmd != "npm run custom-script" {
|
|
t.Errorf("expected high-confidence AI suggestion with context bonus at top, got %s", scored[0].Cmd)
|
|
}
|
|
}
|
|
|
|
func TestScore_UnsortedHistorySorting(t *testing.T) {
|
|
suggestions := []spec.Suggestion{
|
|
{Cmd: "cmdA", Source: "history"},
|
|
{Cmd: "cmdB", Source: "history"},
|
|
{Cmd: "cmdC", Source: "history"},
|
|
}
|
|
signals := SignalSet{
|
|
LocalFrecency: []FrecencyEntry{
|
|
{Cmd: "cmdC", RawScore: 100.0},
|
|
{Cmd: "cmdA", RawScore: 50.0},
|
|
{Cmd: "cmdB", RawScore: 10.0},
|
|
},
|
|
}
|
|
|
|
scored := Score(suggestions, signals)
|
|
if len(scored) != 3 {
|
|
t.Fatalf("expected 3 items, got %d", len(scored))
|
|
}
|
|
if scored[0].Cmd != "cmdC" || scored[1].Cmd != "cmdA" || scored[2].Cmd != "cmdB" {
|
|
t.Errorf("expected cmdC > cmdA > cmdB based on frecency, got %s, %s, %s", scored[0].Cmd, scored[1].Cmd, scored[2].Cmd)
|
|
}
|
|
}
|
|
|
|
func TestBasePriorityFor_HistoryWithConfidence(t *testing.T) {
|
|
s1 := spec.Suggestion{Source: "history"}
|
|
if p := basePriorityFor(s1); p != 40 {
|
|
t.Errorf("expected default history priority 40 when confidence unset, got %d", p)
|
|
}
|
|
|
|
s2 := spec.Suggestion{Source: "history", Confidence: 85}
|
|
if p := basePriorityFor(s2); p != 85 {
|
|
t.Errorf("expected history priority 85 when confidence is 85, got %d", p)
|
|
}
|
|
|
|
s3 := spec.Suggestion{Source: "history", Confidence: 150}
|
|
if p := basePriorityFor(s3); p != 100 {
|
|
t.Errorf("expected capped history priority 100 when confidence is > 100, got %d", p)
|
|
}
|
|
}
|
|
|
|
func TestIsSubsequence_UTF8(t *testing.T) {
|
|
_ = isSubsequence("gố", "gõ tiếng việt")
|
|
if !isSubsequence("việt", "tiếng việt") {
|
|
t.Error("expected 'việt' to be subsequence of 'tiếng việt'")
|
|
}
|
|
if !isSubsequence("tệt", "tiếng việt") {
|
|
t.Error("expected 'tệt' to be subsequence of 'tiếng việt'")
|
|
}
|
|
if isSubsequence("xyz", "tiếng việt") {
|
|
t.Error("expected 'xyz' NOT to be subsequence of 'tiếng việt'")
|
|
}
|
|
if !isSubsequence("αγ", "αβγδε") {
|
|
t.Error("expected multi-byte rune 'αγ' to be subsequence of 'αβγδε'")
|
|
}
|
|
}
|