Ctrl K
ComunidadGuías…

agent/filefinder/query_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package filefinder_test

import (
	"slices"
	"testing"

	"github.com/coder/coder/v2/agent/filefinder"
)

func TestNewQueryPlan(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name       string
		query      string
		wantNorm   string
		wantShort  bool
		wantSlash  bool
		wantBase   string
		wantTokens []string
		wantDirTok []string
		wantTriCnt int // -1 to skip check
	}{
		{"Simple", "foo", "foo", false, false, "foo", []string{"foo"}, nil, 1},
		{"MultiToken", "foo bar", "foo bar", false, false, "bar", []string{"foo", "bar"}, []string{"foo"}, -1},
		{"Slash", "internal/foo", "internal/foo", false, true, "foo", []string{"internal", "foo"}, []string{"internal"}, -1},
		{"SingleChar", "a", "a", true, false, "a", []string{"a"}, nil, 0},
		{"TwoChars", "ab", "ab", true, false, "ab", []string{"ab"}, nil, -1},
		{"ThreeChars", "abc", "abc", false, false, "abc", []string{"abc"}, nil, 1},
		{"DotPrefix", ".go", ".go", false, false, ".go", []string{".go"}, nil, -1},
		{"UpperCase", "FOO", "foo", false, false, "foo", []string{"foo"}, nil, -1},
		{"Empty", "", "", true, false, "", nil, nil, -1},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()
			plan := filefinder.NewQueryPlanForTest(tt.query)
			if plan.Normalized != tt.wantNorm {
				t.Errorf("normalized = %q, want %q", plan.Normalized, tt.wantNorm)
			}
			if plan.IsShort != tt.wantShort {
				t.Errorf("isShort = %v, want %v", plan.IsShort, tt.wantShort)
			}
			if plan.HasSlash != tt.wantSlash {
				t.Errorf("hasSlash = %v, want %v", plan.HasSlash, tt.wantSlash)
			}
			if string(plan.BasenameQ) != tt.wantBase {
				t.Errorf("basenameQ = %q, want %q", plan.BasenameQ, tt.wantBase)
			}
			if tt.wantTokens == nil {
				if len(plan.Tokens) != 0 {
					t.Errorf("expected 0 tokens, got %d", len(plan.Tokens))
				}
			} else {
				if len(plan.Tokens) != len(tt.wantTokens) {
					t.Fatalf("tokens len = %d, want %d", len(plan.Tokens), len(tt.wantTokens))
				}
				for i, tok := range plan.Tokens {
					if string(tok) != tt.wantTokens[i] {
						t.Errorf("tokens[%d] = %q, want %q", i, tok, tt.wantTokens[i])
					}
				}
			}
			if tt.wantDirTok != nil {
				if len(plan.DirTokens) != len(tt.wantDirTok) {
					t.Fatalf("dirTokens len = %d, want %d", len(plan.DirTokens), len(tt.wantDirTok))
				}
				for i, tok := range plan.DirTokens {
					if string(tok) != tt.wantDirTok[i] {
						t.Errorf("dirTokens[%d] = %q, want %q", i, tok, tt.wantDirTok[i])
					}
				}
			}
			if tt.wantTriCnt >= 0 && len(plan.Trigrams) != tt.wantTriCnt {
				t.Errorf("trigram count = %d, want %d", len(plan.Trigrams), tt.wantTriCnt)
			}
		})
	}

	// ThreeChars: verify the actual trigram value.
	plan := filefinder.NewQueryPlanForTest("abc")
	if want := filefinder.PackTrigramForTest('a', 'b', 'c'); plan.Trigrams[0] != want {
		t.Errorf("trigram = %x, want %x", plan.Trigrams[0], want)
	}

	// ShortMultiToken: both tokens < 3 chars so isShort should be true.
	plan = filefinder.NewQueryPlanForTest("ab cd")
	if !plan.IsShort {
		t.Error("expected isShort=true when all tokens < 3 chars")
	}
	// One token >= 3 chars, so isShort should be false.
	plan = filefinder.NewQueryPlanForTest("ab cde")
	if plan.IsShort {
		t.Error("expected isShort=false when any token >= 3 chars")
	}
}

func requireCandHasPath(t *testing.T, cands []filefinder.CandidateForTest, path string) {
	t.Helper()
	for _, c := range cands {
		if c.Path == path {
			return
		}
	}
	t.Errorf("expected to find %q in candidates", path)
}

func TestSearchSnapshot_TrigramMatch(t *testing.T) {
	t.Parallel()
	snap := filefinder.MakeTestSnapshot([]string{"src/handler.go", "src/router.go", "lib/utils.go"})
	cands := filefinder.SearchSnapshotForTest(filefinder.NewQueryPlanForTest("handler"), snap, 100)
	if len(cands) == 0 {
		t.Fatal("expected at least 1 candidate for 'handler'")
	}
	requireCandHasPath(t, cands, "src/handler.go")
}

func TestSearchSnapshot_ShortQuery(t *testing.T) {
	t.Parallel()
	snap := filefinder.MakeTestSnapshot([]string{"foo.go", "bar.go", "fab.go"})
	cands := filefinder.SearchSnapshotForTest(filefinder.NewQueryPlanForTest("fo"), snap, 100)
	if len(cands) == 0 {
		t.Fatal("expected at least 1 candidate for 'fo'")
	}
	requireCandHasPath(t, cands, "foo.go")
}

func TestSearchSnapshot_FuzzyFallback(t *testing.T) {
	t.Parallel()
	snap := filefinder.MakeTestSnapshot([]string{"src/handler.go", "src/router.go", "lib/utils.go"})
	cands := filefinder.SearchSnapshotForTest(filefinder.NewQueryPlanForTest("hndlr"), snap, 100)
	if len(cands) == 0 {
		t.Fatal("expected fuzzy fallback to find 'handler.go' for query 'hndlr'")
	}
	requireCandHasPath(t, cands, "src/handler.go")
}

func TestSearchSnapshot_FuzzyFallbackNoFirstCharMatch(t *testing.T) {
	t.Parallel()
	snap := filefinder.MakeTestSnapshot([]string{"src/xylophone.go", "lib/extra.go"})
	cands := filefinder.SearchSnapshotForTest(filefinder.NewQueryPlanForTest("xylo"), snap, 100)
	if len(cands) == 0 {
		t.Fatal("expected at least 1 candidate for 'xylo'")
	}
	requireCandHasPath(t, cands, "src/xylophone.go")
}

func TestSearchSnapshot_NilSnapshot(t *testing.T) {
	t.Parallel()
	cands := filefinder.SearchSnapshotForTest(filefinder.NewQueryPlanForTest("foo"), nil, 100)
	if cands != nil {
		t.Errorf("expected nil for nil snapshot, got %v", cands)
	}
}

func TestSearchSnapshot_EmptyQuery(t *testing.T) {
	t.Parallel()
	snap := filefinder.MakeTestSnapshot([]string{"foo.go"})
	cands := filefinder.SearchSnapshotForTest(filefinder.NewQueryPlanForTest(""), snap, 100)
	if cands != nil {
		t.Errorf("expected nil for empty query, got %v", cands)
	}
}

func TestSearchSnapshot_DeletedDocsExcluded(t *testing.T) {
	t.Parallel()
	idx := filefinder.NewIndex()
	idx.Add("handler.go", 0)
	idx.Remove("handler.go")
	snap := idx.Snapshot()
	cands := filefinder.SearchSnapshotForTest(filefinder.NewQueryPlanForTest("handler"), snap, 100)
	for _, c := range cands {
		if c.Path == "handler.go" {
			t.Error("deleted doc should not appear in results")
		}
	}
}

func TestSearchSnapshot_Limit(t *testing.T) {
	t.Parallel()
	paths := make([]string, 50)
	for i := range paths {
		paths[i] = "handler" + string(rune('a'+i%26)) + ".go"
	}
	snap := filefinder.MakeTestSnapshot(paths)
	cands := filefinder.SearchSnapshotForTest(filefinder.NewQueryPlanForTest("handler"), snap, 3)
	if len(cands) > 3 {
		t.Errorf("expected at most 3 candidates, got %d", len(cands))
	}
}

func TestIntersectSorted(t *testing.T) {
	t.Parallel()
	tests := []struct {
		name string
		a, b []uint32
		want []uint32
	}{
		{"both empty", nil, nil, nil},
		{"a empty", nil, []uint32{1, 2}, nil},
		{"b empty", []uint32{1, 2}, nil, nil},
		{"no overlap", []uint32{1, 3, 5}, []uint32{2, 4, 6}, nil},
		{"full overlap", []uint32{1, 2, 3}, []uint32{1, 2, 3}, []uint32{1, 2, 3}},
		{"partial overlap", []uint32{1, 2, 3, 5}, []uint32{2, 4, 5}, []uint32{2, 5}},
		{"single match", []uint32{1, 2, 3}, []uint32{2}, []uint32{2}},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()
			got := filefinder.IntersectSortedForTest(tt.a, tt.b)
			if len(tt.want) == 0 {
				if len(got) != 0 {
					t.Errorf("got %v, want empty/nil", got)
				}
				return
			}
			if !slices.Equal(got, tt.want) {
				t.Errorf("got %v, want %v", got, tt.want)
			}
		})
	}
}

func TestIntersectAll(t *testing.T) {
	t.Parallel()
	t.Run("empty", func(t *testing.T) {
		t.Parallel()
		if got := filefinder.IntersectAllForTest(nil); got != nil {
			t.Errorf("got %v, want nil", got)
		}
	})
	t.Run("single", func(t *testing.T) {
		t.Parallel()
		if got := filefinder.IntersectAllForTest([][]uint32{{1, 2, 3}}); len(got) != 3 {
			t.Fatalf("len = %d, want 3", len(got))
		}
	})
	t.Run("multiple", func(t *testing.T) {
		t.Parallel()
		got := filefinder.IntersectAllForTest([][]uint32{{1, 2, 3, 4, 5}, {2, 3, 5}, {3, 5, 7}})
		if !slices.Equal(got, []uint32{3, 5}) {
			t.Errorf("got %v, want [3 5]", got)
		}
	})
	t.Run("no overlap", func(t *testing.T) {
		t.Parallel()
		if got := filefinder.IntersectAllForTest([][]uint32{{1, 2}, {3, 4}}); got != nil {
			t.Errorf("got %v, want nil", got)
		}
	})
}

func TestMergeAndScore_SortedDescending(t *testing.T) {
	t.Parallel()
	plan := filefinder.NewQueryPlanForTest("foo")
	params := filefinder.DefaultScoreParamsForTest()
	cands := []filefinder.CandidateForTest{
		{DocID: 0, Path: "a/b/c/d/e/foo", BaseOff: 10, BaseLen: 3, Depth: 5},
		{DocID: 1, Path: "src/foo", BaseOff: 4, BaseLen: 3, Depth: 1},
		{DocID: 2, Path: "foo", BaseOff: 0, BaseLen: 3, Depth: 0},
	}
	results := filefinder.MergeAndScoreForTest(cands, plan, params, 10)
	if len(results) == 0 {
		t.Fatal("expected non-empty results")
	}
	for i := 1; i < len(results); i++ {
		if results[i].Score > results[i-1].Score {
			t.Errorf("results not sorted: [%d].Score=%f > [%d].Score=%f",
				i, results[i].Score, i-1, results[i-1].Score)
		}
	}
}

func TestMergeAndScore_TopKLimit(t *testing.T) {
	t.Parallel()
	plan := filefinder.NewQueryPlanForTest("f")
	params := filefinder.DefaultScoreParamsForTest()
	var cands []filefinder.CandidateForTest
	for i := range 20 {
		p := "f" + string(rune('a'+i))
		cands = append(cands, filefinder.CandidateForTest{DocID: uint32(i), Path: p, BaseOff: 0, BaseLen: len(p), Depth: 0}) //nolint:gosec // test index is tiny
	}
	if results := filefinder.MergeAndScoreForTest(cands, plan, params, 5); len(results) != 5 {
		t.Errorf("expected 5 results, got %d", len(results))
	}
}

func TestMergeAndScore_ZeroTopK(t *testing.T) {
	t.Parallel()
	plan := filefinder.NewQueryPlanForTest("foo")
	cands := []filefinder.CandidateForTest{{DocID: 0, Path: "foo", BaseOff: 0, BaseLen: 3, Depth: 0}}
	if results := filefinder.MergeAndScoreForTest(cands, plan, filefinder.DefaultScoreParamsForTest(), 0); len(results) != 0 {
		t.Errorf("expected 0 results for topK=0, got %d", len(results))
	}
}

func TestMergeAndScore_NoMatchCandidatesDropped(t *testing.T) {
	t.Parallel()
	plan := filefinder.NewQueryPlanForTest("xyz")
	cands := []filefinder.CandidateForTest{
		{DocID: 0, Path: "abc", BaseOff: 0, BaseLen: 3, Depth: 0},
		{DocID: 1, Path: "def", BaseOff: 0, BaseLen: 3, Depth: 0},
	}
	if results := filefinder.MergeAndScoreForTest(cands, plan, filefinder.DefaultScoreParamsForTest(), 10); len(results) != 0 {
		t.Errorf("expected 0 results for non-matching candidates, got %d", len(results))
	}
}

func TestMergeAndScore_IsDirFlag(t *testing.T) {
	t.Parallel()
	plan := filefinder.NewQueryPlanForTest("foo")
	cands := []filefinder.CandidateForTest{
		{DocID: 0, Path: "foo", BaseOff: 0, BaseLen: 3, Depth: 0, Flags: uint16(filefinder.FlagDir)},
	}
	results := filefinder.MergeAndScoreForTest(cands, plan, filefinder.DefaultScoreParamsForTest(), 10)
	if len(results) != 1 {
		t.Fatalf("expected 1 result, got %d", len(results))
	}
	if !results[0].IsDir {
		t.Error("expected IsDir=true for FlagDir candidate")
	}
}

func TestMergeAndScore_EmptyCandidates(t *testing.T) {
	t.Parallel()
	if results := filefinder.MergeAndScoreForTest(nil, filefinder.NewQueryPlanForTest("foo"), filefinder.DefaultScoreParamsForTest(), 10); len(results) != 0 {
		t.Errorf("expected 0 results for nil candidates, got %d", len(results))
	}
}

func TestSearchSnapshot_FuzzyFallbackEndToEnd(t *testing.T) {
	t.Parallel()
	snap := filefinder.MakeTestSnapshot([]string{"src/handler.go", "src/middleware.go", "pkg/config.go"})
	plan := filefinder.NewQueryPlanForTest("hndlr")
	results := filefinder.MergeAndScoreForTest(filefinder.SearchSnapshotForTest(plan, snap, 100), plan, filefinder.DefaultScoreParamsForTest(), 10)
	if len(results) == 0 {
		t.Fatal("expected fuzzy fallback to produce scored results for 'hndlr'")
	}
	if results[0].Path != "src/handler.go" {
		t.Errorf("expected top result 'src/handler.go', got %q", results[0].Path)
	}
}