a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
156 lines
4.7 KiB
Go
156 lines
4.7 KiB
Go
package indexer
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"go.uber.org/zap"
|
|
|
|
"github.com/zzet/gortex/internal/config"
|
|
"github.com/zzet/gortex/internal/embedding"
|
|
"github.com/zzet/gortex/internal/graph"
|
|
"github.com/zzet/gortex/internal/parser"
|
|
"github.com/zzet/gortex/internal/parser/languages"
|
|
"github.com/zzet/gortex/internal/search"
|
|
)
|
|
|
|
// indexedVectorBackend indexes dir with the given chunk options and
|
|
// returns the vector backend buildSearchIndex produced.
|
|
func indexedVectorBackend(t *testing.T, dir string, opts embedding.ChunkOptions) *search.VectorBackend {
|
|
t.Helper()
|
|
g := graph.New()
|
|
reg := parser.NewRegistry()
|
|
reg.Register(languages.NewGoExtractor())
|
|
|
|
cfg := config.Default().Index
|
|
cfg.Workers = 1
|
|
|
|
idx := New(g, reg, cfg, zap.NewNop())
|
|
idx.SetEmbedder(stubEmbedder{})
|
|
idx.SetEmbeddingChunkOptions(opts)
|
|
|
|
_, err := idx.Index(dir)
|
|
require.NoError(t, err)
|
|
|
|
sw, ok := idx.Search().(*search.Swappable)
|
|
require.True(t, ok)
|
|
hyb, ok := sw.Inner().(*search.HybridBackend)
|
|
require.True(t, ok, "an embedder-equipped index must produce a HybridBackend")
|
|
return hyb.VectorIndex()
|
|
}
|
|
|
|
// TestBuildSearchIndex_LongSymbolYieldsMultipleChunks proves the
|
|
// pipeline change: a function whose source span exceeds the chunk
|
|
// threshold is split into several chunk vectors, and the chunk →
|
|
// parent mapping is recorded on the vector backend.
|
|
func TestBuildSearchIndex_LongSymbolYieldsMultipleChunks(t *testing.T) {
|
|
dir := t.TempDir()
|
|
var b strings.Builder
|
|
b.WriteString("package main\n\nfunc BigFunc() {\n")
|
|
for i := 0; i < 80; i++ {
|
|
b.WriteString("\tprintln(\"line\")\n")
|
|
}
|
|
b.WriteString("}\n")
|
|
require.NoError(t, os.WriteFile(filepath.Join(dir, "big.go"), []byte(b.String()), 0o644))
|
|
|
|
// Low threshold + small window so BigFunc is forced to split.
|
|
vec := indexedVectorBackend(t, dir, embedding.ChunkOptions{ThresholdLines: 20, WindowLines: 15})
|
|
|
|
require.True(t, vec.HasChunks(),
|
|
"a long function past the threshold must produce chunk vectors")
|
|
|
|
// At least two of BigFunc's chunk vectors must resolve back to it.
|
|
chunkCount := 0
|
|
for i := 0; i < 20; i++ {
|
|
id := bigFuncChunkID(i)
|
|
if parent, isChunk := vec.ResolveChunk(id); isChunk {
|
|
assert.Equal(t, "big.go::BigFunc", parent)
|
|
chunkCount++
|
|
}
|
|
}
|
|
assert.GreaterOrEqual(t, chunkCount, 2,
|
|
"BigFunc must be split into at least two chunk vectors")
|
|
}
|
|
|
|
// bigFuncChunkID builds the synthetic chunk ID buildSearchIndex stamps
|
|
// on BigFunc's i-th window.
|
|
func bigFuncChunkID(i int) string {
|
|
return "big.go::BigFunc#chunk" + itoa(i)
|
|
}
|
|
|
|
func itoa(i int) string {
|
|
if i == 0 {
|
|
return "0"
|
|
}
|
|
var digits []byte
|
|
for i > 0 {
|
|
digits = append([]byte{byte('0' + i%10)}, digits...)
|
|
i /= 10
|
|
}
|
|
return string(digits)
|
|
}
|
|
|
|
// TestBuildSearchIndex_ShortSymbolStaysWhole proves the inverse: a file
|
|
// of only small functions produces no chunk vectors — every symbol is
|
|
// embedded under its own ID.
|
|
func TestBuildSearchIndex_ShortSymbolStaysWhole(t *testing.T) {
|
|
dir := t.TempDir()
|
|
src := `package main
|
|
|
|
func Tiny() {
|
|
println("a")
|
|
}
|
|
|
|
func AlsoTiny() {
|
|
println("b")
|
|
}
|
|
`
|
|
require.NoError(t, os.WriteFile(filepath.Join(dir, "small.go"), []byte(src), 0o644))
|
|
|
|
vec := indexedVectorBackend(t, dir, embedding.ChunkOptions{ThresholdLines: 60, WindowLines: 40})
|
|
assert.False(t, vec.HasChunks(),
|
|
"a file of only short functions must produce no chunk vectors")
|
|
assert.Greater(t, vec.Count(), 0, "the symbols must still be embedded")
|
|
}
|
|
|
|
// TestBuildSearchIndex_ChunkedSymbolNotDuplicatedInSearch proves the
|
|
// end-to-end de-chunk contract through a real index: searching for a
|
|
// long symbol returns it once, and no synthetic chunk ID surfaces.
|
|
func TestBuildSearchIndex_ChunkedSymbolNotDuplicatedInSearch(t *testing.T) {
|
|
dir := t.TempDir()
|
|
var b strings.Builder
|
|
b.WriteString("package main\n\nfunc ValidateRequestPayload() {\n")
|
|
for i := 0; i < 80; i++ {
|
|
b.WriteString("\tcheckField()\n")
|
|
}
|
|
b.WriteString("}\n\nfunc checkField() {}\n")
|
|
require.NoError(t, os.WriteFile(filepath.Join(dir, "v.go"), []byte(b.String()), 0o644))
|
|
|
|
g := graph.New()
|
|
reg := parser.NewRegistry()
|
|
reg.Register(languages.NewGoExtractor())
|
|
cfg := config.Default().Index
|
|
cfg.Workers = 1
|
|
idx := New(g, reg, cfg, zap.NewNop())
|
|
idx.SetEmbedder(stubEmbedder{})
|
|
idx.SetEmbeddingChunkOptions(embedding.ChunkOptions{ThresholdLines: 20, WindowLines: 15})
|
|
_, err := idx.Index(dir)
|
|
require.NoError(t, err)
|
|
|
|
results := idx.Search().Search("ValidateRequestPayload", 20)
|
|
require.NotEmpty(t, results)
|
|
|
|
seen := map[string]int{}
|
|
for _, r := range results {
|
|
assert.NotContains(t, r.ID, "#chunk",
|
|
"a chunk ID must never appear in search output")
|
|
seen[r.ID]++
|
|
}
|
|
assert.LessOrEqual(t, seen["v.go::ValidateRequestPayload"], 1,
|
|
"the chunked symbol must not be returned more than once")
|
|
}
|