Files
wehub-resource-sync f99010fae1
Desktop Artifacts / Desktop Build (Linux) (push) Waiting to run
Desktop Artifacts / Desktop Build (Windows) (push) Waiting to run
Desktop Artifacts / Desktop Build (Linux (arm64)) (push) Waiting to run
Desktop Artifacts (macOS) / Desktop Build (macOS (aarch64)) (push) Waiting to run
Desktop Artifacts (macOS) / Desktop Build (macOS (x86_64)) (push) Waiting to run
CI / lint (push) Failing after 1s
CI / frontend (push) Failing after 1s
CI / scripts (push) Failing after 1s
CI / Go Test (ubuntu-latest) (push) Failing after 0s
CI / frontend-node-25 (push) Failing after 1s
CI / docs (push) Failing after 0s
CI / coverage (push) Failing after 0s
CI / e2e (push) Failing after 0s
Docker / build-and-push (push) Failing after 1s
CI / integration (push) Failing after 4m43s
CI / Go Test (windows-latest) (push) Has been cancelled
CI / Desktop Unit Tests (Windows) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:30:36 +08:00

879 lines
32 KiB
Go

package vector
import (
"context"
"encoding/json"
"math"
"net/http"
"net/http/httptest"
"path/filepath"
"strings"
"sync"
"testing"
"time"
"unicode/utf8"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.kenn.io/agentsview/internal/db"
kitvec "go.kenn.io/kit/vector"
"go.kenn.io/kit/vector/sqlitevec"
)
// fakeSearchEncoder maps distinct known texts to orthogonal 3-dimensional
// vectors so a query for one topic scores a perfect match against its own
// document and a clear non-match against the others.
func fakeSearchEncoder() func(_ context.Context, texts []string) ([][]float32, error) {
return func(_ context.Context, texts []string) ([][]float32, error) {
out := make([][]float32, len(texts))
for i, text := range texts {
switch {
case strings.Contains(text, "alpha"):
out[i] = []float32{1, 0, 0}
case strings.Contains(text, "beta"):
out[i] = []float32{0, 1, 0}
default:
out[i] = []float32{0, 0, 1}
}
}
return out, nil
}
}
// threeDocSearchSource returns three single-topic documents in one session,
// one each for "alpha", "beta", and a third ("gamma") topic.
func threeDocSearchSource() *fakeUnitSource {
return &fakeUnitSource{rows: []fakeUnit{
{
unit: userDoc("s1", "u1", 0, "this message mentions alpha topic"),
endedAt: "2024-01-01T00:00:00Z",
},
{
unit: userDoc("s1", "u2", 1, "this message mentions beta topic"),
endedAt: "2024-01-01T00:00:01Z",
},
{
unit: userDoc("s1", "u3", 2, "this message mentions gamma topic"),
endedAt: "2024-01-01T00:00:02Z",
},
}}
}
func TestSearchReturnsBestMatchFirstWithSnippet(t *testing.T) {
ix := openTestIndex(t)
ctx := context.Background()
src := threeDocSearchSource()
gen := fakeGeneration("fake-model")
_, err := ix.Build(ctx, src, fakeSearchEncoder(), gen, BuildOptions{})
require.NoError(t, err)
hits, err := ix.Search(ctx, fakeSearchEncoder(), "alpha", 10)
require.NoError(t, err)
require.NotEmpty(t, hits)
best := hits[0]
assert.Equal(t, "s1", best.SessionID)
assert.Equal(t, 0, best.Ordinal)
assert.InDelta(t, 1.0, best.Score, 0.01)
assert.Contains(t, best.Snippet, "alpha")
}
func TestSearchLimitCapsResults(t *testing.T) {
ix := openTestIndex(t)
ctx := context.Background()
src := threeDocSearchSource()
gen := fakeGeneration("fake-model")
_, err := ix.Build(ctx, src, fakeSearchEncoder(), gen, BuildOptions{})
require.NoError(t, err)
hits, err := ix.Search(ctx, fakeSearchEncoder(), "alpha", 1)
require.NoError(t, err)
assert.Len(t, hits, 1)
}
func TestSearchNoGenerationsReturnsErrNoActiveGeneration(t *testing.T) {
ix := openTestIndex(t)
ctx := context.Background()
_, err := ix.Search(ctx, fakeSearchEncoder(), "alpha", 10)
assert.ErrorIs(t, err, ErrNoActiveGeneration)
}
func TestSearchBuildingOnlyReturnsBuildingError(t *testing.T) {
ix := openTestIndex(t)
ctx := context.Background()
src := threeDocSearchSource()
_, err := ix.Refresh(ctx, src, true, true)
require.NoError(t, err)
gen := fakeGeneration("fake-model")
_, err = ix.EnsureGeneration(ctx, gen, sqlitevec.StateBuilding)
require.NoError(t, err)
_, err = ix.Search(ctx, fakeSearchEncoder(), "alpha", 10)
require.Error(t, err)
var buildingErr *BuildingError
require.ErrorAs(t, err, &buildingErr)
assert.Equal(t, 0, buildingErr.Percent, "nothing has been embedded yet")
}
// TestSearchIgnoresBuildingGenerationOfDifferentDimension covers Search's
// active-generation-only query path: while a model/dimension change is in
// progress, a building generation of a different dimension coexists with
// the active one. kitvec.Search's default (query every live generation with
// one caller-supplied encoder) would try to run the active encoder's
// 3-dimensional query vector against the building generation's 5-dimensional
// vec0 table and fail. Search must encode once for, and query only, the
// active generation, so the building generation's differing dimension never
// matters and only active-generation hits come back.
func TestSearchIgnoresBuildingGenerationOfDifferentDimension(t *testing.T) {
ix := openTestIndex(t)
ctx := context.Background()
src := threeDocSearchSource()
activeGen := fakeGeneration("active-model")
_, err := ix.Build(ctx, src, fakeSearchEncoder(), activeGen, BuildOptions{})
require.NoError(t, err)
buildingGen := kitvec.Generation{Model: "building-model", Dimensions: 5}
_, err = ix.EnsureGeneration(ctx, buildingGen, sqlitevec.StateBuilding)
require.NoError(t, err)
hits, err := ix.Search(ctx, fakeSearchEncoder(), "alpha", 10)
require.NoError(t, err, "a building generation of a different dimension must not break search")
require.NotEmpty(t, hits)
assert.Equal(t, "s1", hits[0].SessionID)
assert.Equal(t, 0, hits[0].Ordinal)
assert.InDelta(t, 1.0, hits[0].Score, 0.01)
}
func TestStaleActiveTrueWhenFingerprintsDiffer(t *testing.T) {
ix := openTestIndex(t)
ctx := context.Background()
src := threeDocSearchSource()
gen := fakeGeneration("fake-model")
_, err := ix.Build(ctx, src, fakeSearchEncoder(), gen, BuildOptions{})
require.NoError(t, err)
stale, err := ix.StaleActive(ctx, "some-other-fingerprint")
require.NoError(t, err)
assert.True(t, stale)
stale, err = ix.StaleActive(ctx, gen.Fingerprint())
require.NoError(t, err)
assert.False(t, stale, "matching fingerprint is not stale")
}
func TestStaleActiveFalseWhenNoActiveGeneration(t *testing.T) {
ix := openTestIndex(t)
ctx := context.Background()
stale, err := ix.StaleActive(ctx, "anything")
require.NoError(t, err)
assert.False(t, stale, "no active generation means nothing to compare")
}
// anchorOrdinal is a test-only convenience over the production anchor
// pipeline: it maps a matched chunk back to the run member ordinal whose
// rune span contains the chunk's center rune, composing chunkWindow and
// anchorMemberIndex exactly the way resolveRunHit does. offsets must be
// non-empty (run documents only).
func anchorOrdinal(offsets []db.UnitOffset, contentRunes, chunkIndex int, o kitvec.SplitOptions) int {
start, end := chunkWindow(contentRunes, chunkIndex, o)
return offsets[anchorMemberIndex(offsets, start, end)].Ordinal
}
// TestAnchorOrdinal pins the anchor policy: the anchor is the member message
// whose rune span contains the matched chunk's center rune, computed from
// the chunk's ACTUAL rune length (the final chunk is capped at the content's
// end, not at start+MaxRunes), with the earlier member winning when the
// center falls in the "\n\n" separator between two members.
func TestAnchorOrdinal(t *testing.T) {
// Three members joined with "\n\n": member 10 spans runes [0,5),
// separator [5,7), member 11 spans [7,12), separator [12,14),
// member 12 spans [14,19).
offsets := []db.UnitOffset{
{Ordinal: 10, RuneStart: 0},
{Ordinal: 11, RuneStart: 7},
{Ordinal: 12, RuneStart: 14},
}
tests := []struct {
name string
offsets []db.UnitOffset
contentRunes int
chunkIndex int
opts kitvec.SplitOptions
want int
}{
{
name: "chunk fully inside first member", offsets: offsets,
// Window [0,4), center 2, inside member 10's span.
contentRunes: 19, chunkIndex: 0,
opts: kitvec.SplitOptions{MaxRunes: 4, Overlap: 0}, want: 10,
},
{
name: "single chunk run centers whole content", offsets: offsets,
// Content fits one chunk: window [0,19), center 9, member 11.
contentRunes: 19, chunkIndex: 0,
opts: kitvec.SplitOptions{MaxRunes: 100, Overlap: 15}, want: 11,
},
{
name: "center in separator anchors earlier member", offsets: offsets,
// Window [4,8), center 6 falls in the separator [5,7): the
// earlier member 10 wins the boundary tie.
contentRunes: 19, chunkIndex: 1,
opts: kitvec.SplitOptions{MaxRunes: 4, Overlap: 0}, want: 10,
},
{
name: "center exactly at member start anchors that member", offsets: offsets,
// Window [0,14), center 7 == member 11's RuneStart: member 11's
// span contains rune 7.
contentRunes: 19, chunkIndex: 0,
opts: kitvec.SplitOptions{MaxRunes: 14, Overlap: 0}, want: 11,
},
{
name: "short final chunk centers on actual length",
// Member 12 starts at rune 13; content is 15 runes. Stride is
// 10-1=9, so chunk 1's window is [9,15): actual length 6,
// center 12 -> member 11. Centering on MaxRunes instead
// ([9,19), center 14) would wrongly anchor member 12.
offsets: []db.UnitOffset{
{Ordinal: 10, RuneStart: 0},
{Ordinal: 11, RuneStart: 7},
{Ordinal: 12, RuneStart: 13},
},
contentRunes: 15, chunkIndex: 1,
opts: kitvec.SplitOptions{MaxRunes: 10, Overlap: 1}, want: 11,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := anchorOrdinal(tt.offsets, tt.contentRunes, tt.chunkIndex, tt.opts)
assert.Equal(t, tt.want, got)
})
}
}
// TestChunkWindowMatchesKitSplit cross-checks the anchor computation's
// deterministic window math against kitvec.Split itself: for every chunk kit
// actually produces, chunkWindow's [start,end) rune window must select
// exactly that chunk's text. If kit's stride semantics ever change, this
// test breaks instead of anchors silently drifting.
func TestChunkWindowMatchesKitSplit(t *testing.T) {
tests := []struct {
name string
content string
opts kitvec.SplitOptions
}{
{"ascii multi chunk", strings.Repeat("abcde", 9),
kitvec.SplitOptions{MaxRunes: 10, Overlap: 2}},
{"multi byte multi chunk", strings.Repeat("é", 25),
kitvec.SplitOptions{MaxRunes: 7, Overlap: 1}},
{"single chunk", "short",
kitvec.SplitOptions{MaxRunes: 10, Overlap: 2}},
{"short final chunk", strings.Repeat("x", 23),
kitvec.SplitOptions{MaxRunes: 10, Overlap: 1}},
{"overlap exceeding max runes is clamped", strings.Repeat("y", 25),
kitvec.SplitOptions{MaxRunes: 10, Overlap: 50}},
{"production overlap shape", strings.Repeat("word ", 100),
kitvec.SplitOptions{MaxRunes: 40, Overlap: ChunkOverlap(40)}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
chunks := kitvec.Split(tt.content, tt.opts)
require.NotEmpty(t, chunks)
runes := []rune(tt.content)
for _, c := range chunks {
start, end := chunkWindow(len(runes), c.Index, tt.opts)
require.GreaterOrEqual(t, start, 0)
require.LessOrEqual(t, end, len(runes))
assert.Equal(t, string(runes[start:end]), c.Text,
"chunk %d window mismatch", c.Index)
}
})
}
}
// openSmallChunkIndex opens a test index whose split options force
// multi-chunk documents at tiny content sizes (MaxRunes = maxInputChars,
// Overlap = ChunkOverlap(maxInputChars)).
func openSmallChunkIndex(t *testing.T, maxInputChars int) *Index {
t.Helper()
ctx := context.Background()
path := filepath.Join(t.TempDir(), "vectors.db")
ix, err := Open(ctx, path, false, maxInputChars)
require.NoError(t, err)
t.Cleanup(func() { require.NoError(t, ix.Close()) })
return ix
}
// seedMirrorRow inserts one vector_messages row shaped like Refresh would
// write it for u, so hydrate-level tests can exercise anchoring without a
// full Build.
func seedMirrorRow(t *testing.T, ix *Index, docKey string, u db.EmbeddableUnit) {
t.Helper()
offsets, err := marshalOffsets(u.Offsets)
require.NoError(t, err)
_, err = ix.db.Exec(`
INSERT INTO vector_messages (doc_key, session_id, ordinal, ordinal_end,
subordinate, offsets, content, content_hash)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
docKey, u.SessionID, u.Ordinal, u.OrdinalEnd, u.Subordinate,
offsets, u.Content, contentHash(u.Content))
require.NoError(t, err)
}
// TestHydrateHitsAnchorsRunChunks pins the full hydrate path for a run
// document: each matched chunk anchors to the member containing its center
// rune, the hit carries the run's ordinal range and subordinate flag, and
// the snippet is the intersection of the matched chunk's window with the
// ANCHOR member's own span — a substring of the anchor message's text, not
// run-level text spanning members — so the db layer's snippet centering can
// locate it inside the anchor message's content.
func TestHydrateHitsAnchorsRunChunks(t *testing.T) {
ix := openSmallChunkIndex(t, 10) // stride 10-1=9
ctx := context.Background()
content := "aaaaa\n\nbbbbb\n\nccccc" // 19 runes, chunks [0,10) and [9,19)
seedMirrorRow(t, ix, "r1", db.EmbeddableUnit{
SessionID: "s1", Kind: "run", Ordinal: 5, OrdinalEnd: 7,
Subordinate: true, Content: content,
Offsets: []db.UnitOffset{
{Ordinal: 5, RuneStart: 0, ByteStart: 0},
{Ordinal: 6, RuneStart: 7, ByteStart: 7},
{Ordinal: 7, RuneStart: 14, ByteStart: 14},
},
})
hits, err := ix.hydrateHits(ctx, []kitvec.Hit[string]{
{Doc: "r1", ChunkIndex: 0, Score: 0.9},
})
require.NoError(t, err)
require.Len(t, hits, 1)
// Chunk 0's window [0,10) spans members 5 and 6 and centers on rune 5,
// in the separator after member 5: the earlier member anchors, and the
// snippet is clipped to member 5's own text rather than the whole
// cross-member chunk "aaaaa\n\nbbb".
assert.Equal(t, 5, hits[0].Ordinal, "anchor ordinal")
assert.Equal(t, 5, hits[0].OrdinalStart)
assert.Equal(t, 7, hits[0].OrdinalEnd)
assert.True(t, hits[0].Subordinate)
assert.Equal(t, "aaaaa", hits[0].Snippet,
"run snippet must be a substring of the anchor member's own text")
hits, err = ix.hydrateHits(ctx, []kitvec.Hit[string]{
{Doc: "r1", ChunkIndex: 1, Score: 0.8},
})
require.NoError(t, err)
require.Len(t, hits, 1)
// Chunk 1's window [9,19) centers on rune 14, member 7's first rune:
// the snippet is member 7's slice of the window, not "bbb\n\nccccc".
assert.Equal(t, 7, hits[0].Ordinal, "anchor ordinal")
assert.Equal(t, 5, hits[0].OrdinalStart)
assert.Equal(t, 7, hits[0].OrdinalEnd)
assert.Equal(t, "ccccc", hits[0].Snippet,
"run snippet must be a substring of the anchor member's own text")
}
// TestHydrateHitsDegenerateChunkIndexFallsBackToAnchorSpan pins the
// fallback for a stale/degenerate ChunkIndex whose re-split window misses
// the content entirely (content changed since embedding): the snippet falls
// back to the anchor member's own span text — never a panic, never text
// from a different member.
func TestHydrateHitsDegenerateChunkIndexFallsBackToAnchorSpan(t *testing.T) {
ix := openSmallChunkIndex(t, 10)
ctx := context.Background()
content := "aaaaa\n\nbbbbb\n\nccccc"
seedMirrorRow(t, ix, "r1", db.EmbeddableUnit{
SessionID: "s1", Kind: "run", Ordinal: 5, OrdinalEnd: 7,
Content: content,
Offsets: []db.UnitOffset{
{Ordinal: 5, RuneStart: 0, ByteStart: 0},
{Ordinal: 6, RuneStart: 7, ByteStart: 7},
{Ordinal: 7, RuneStart: 14, ByteStart: 14},
},
})
hits, err := ix.hydrateHits(ctx, []kitvec.Hit[string]{
{Doc: "r1", ChunkIndex: 99, Score: 0.9},
})
require.NoError(t, err)
require.Len(t, hits, 1)
assert.Equal(t, 7, hits[0].Ordinal)
assert.Equal(t, "ccccc", hits[0].Snippet,
"an out-of-range chunk window must fall back to the anchor member's span text")
}
// TestHydrateHitsCorruptOffsetsFailsWithDocKey seeds a mirror row whose
// offsets column holds invalid JSON and pins that hydration fails fast with
// the doc_key in the error rather than panicking or silently dropping the
// hit.
func TestHydrateHitsCorruptOffsetsFailsWithDocKey(t *testing.T) {
ix := openTestIndex(t)
ctx := context.Background()
_, err := ix.db.Exec(`
INSERT INTO vector_messages (doc_key, session_id, ordinal, ordinal_end,
subordinate, offsets, content, content_hash)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
"r-corrupt", "s1", 0, 1, false, `{"not": "an array"`, "some content", "h1")
require.NoError(t, err)
_, err = ix.hydrateHits(ctx, []kitvec.Hit[string]{
{Doc: "r-corrupt", ChunkIndex: 0, Score: 0.9},
})
require.Error(t, err)
assert.Contains(t, err.Error(), "r-corrupt",
"the error must name the doc_key whose offsets are corrupt")
}
// TestHydrateHitsUserDocPassthrough pins that a user document (offsets "[]")
// passes its mirror ordinal through unchanged: Ordinal, OrdinalStart, and
// OrdinalEnd all equal the mirror row's ordinal and the anchor helper is
// never consulted.
func TestHydrateHitsUserDocPassthrough(t *testing.T) {
ix := openTestIndex(t)
ctx := context.Background()
seedMirrorRow(t, ix, "u1", db.EmbeddableUnit{
SessionID: "s1", Kind: "user", Ordinal: 3, OrdinalEnd: 3,
Content: "a plain user question",
})
hits, err := ix.hydrateHits(ctx, []kitvec.Hit[string]{
{Doc: "u1", ChunkIndex: 0, Score: 0.7},
})
require.NoError(t, err)
require.Len(t, hits, 1)
assert.Equal(t, 3, hits[0].Ordinal)
assert.Equal(t, 3, hits[0].OrdinalStart)
assert.Equal(t, 3, hits[0].OrdinalEnd)
assert.False(t, hits[0].Subordinate)
assert.Equal(t, "a plain user question", hits[0].Snippet)
}
// TestHydrateHitsMultiByteSnippet pins that run snippets slice on rune
// boundaries: with every content rune multi-byte, byte-offset math would
// tear characters apart or select the wrong window.
func TestHydrateHitsMultiByteSnippet(t *testing.T) {
ix := openSmallChunkIndex(t, 10) // stride 9
ctx := context.Background()
content := strings.Repeat("é", 5) + "\n\n" + strings.Repeat("ü", 5) // 12 runes
seedMirrorRow(t, ix, "r1", db.EmbeddableUnit{
SessionID: "s1", Kind: "run", Ordinal: 1, OrdinalEnd: 2,
Content: content,
Offsets: []db.UnitOffset{
{Ordinal: 1, RuneStart: 0, ByteStart: 0},
{Ordinal: 2, RuneStart: 7, ByteStart: 12},
},
})
hits, err := ix.hydrateHits(ctx, []kitvec.Hit[string]{
{Doc: "r1", ChunkIndex: 1, Score: 0.9},
})
require.NoError(t, err)
require.Len(t, hits, 1)
// Chunk 1's window is [9,12): the last three ü runes, center rune 10
// inside member 2's span.
assert.Equal(t, "üüü", hits[0].Snippet,
"run snippet must be a rune-sliced substring of the anchor member's own text")
assert.True(t, utf8.ValidString(hits[0].Snippet))
assert.Equal(t, 2, hits[0].Ordinal)
}
// TestSearchRunDocReturnsAnchoredHit pins Search end to end over a mixed
// mirror: a run document's hit is anchored to the member containing the
// matched content and carries its ordinal range and subordinate flag, while
// a user document's hit passes its own ordinal through.
func TestSearchRunDocReturnsAnchoredHit(t *testing.T) {
ix := openTestIndex(t)
ctx := context.Background()
// "run first message" is 17 runes; member 2 starts at rune 19 after the
// "\n\n" separator. The whole run fits one chunk, whose center rune
// (39/2 = 19) is member 2's first rune.
run := db.EmbeddableUnit{
SessionID: "s1", Kind: "run", SourceUUID: "a1",
Ordinal: 1, OrdinalEnd: 2, Subordinate: true,
Content: "run first message\n\nmentions alpha topic",
Offsets: []db.UnitOffset{
{Ordinal: 1, RuneStart: 0, ByteStart: 0},
{Ordinal: 2, RuneStart: 19, ByteStart: 19},
},
}
src := &fakeUnitSource{rows: []fakeUnit{
{unit: userDoc("s1", "u1", 0, "this message mentions beta topic"),
endedAt: "2024-01-01T00:00:00Z"},
{unit: run, endedAt: "2024-01-01T00:00:01Z"},
}}
gen := fakeGeneration("fake-model")
_, err := ix.Build(ctx, src, fakeSearchEncoder(), gen, BuildOptions{})
require.NoError(t, err)
hits, err := ix.Search(ctx, fakeSearchEncoder(), "alpha", 10)
require.NoError(t, err)
require.NotEmpty(t, hits)
best := hits[0]
assert.Equal(t, "s1", best.SessionID)
assert.Equal(t, 2, best.Ordinal, "anchor: member containing the chunk center")
assert.Equal(t, 1, best.OrdinalStart)
assert.Equal(t, 2, best.OrdinalEnd)
assert.True(t, best.Subordinate)
assert.Equal(t, "mentions alpha topic", best.Snippet,
"snippet must be the anchor member's own slice of the chunk, not run-level text")
hits, err = ix.Search(ctx, fakeSearchEncoder(), "beta", 10)
require.NoError(t, err)
require.NotEmpty(t, hits)
best = hits[0]
assert.Equal(t, 0, best.Ordinal)
assert.Equal(t, 0, best.OrdinalStart)
assert.Equal(t, 0, best.OrdinalEnd)
assert.False(t, best.Subordinate)
}
// seedUnitRow inserts one vector_messages unit row directly, bypassing
// Build, so resolver tests can shape exact unit boundaries and gaps.
func seedUnitRow(
t *testing.T, ix *Index, docKey, sessionID string, start, end int, subordinate bool,
) {
t.Helper()
_, err := ix.db.Exec(`
INSERT INTO vector_messages
(doc_key, session_id, ordinal, ordinal_end, subordinate, content, content_hash)
VALUES (?, ?, ?, ?, ?, ?, ?)`,
docKey, sessionID, start, end, subordinate, "content "+docKey, "h-"+docKey)
require.NoError(t, err)
}
// TestResolveMessageUnitsPointLookup pins the resolver's containment
// semantics over a mirror with a user unit, a multi-message run, a gap of
// non-embeddable ordinals, and a subordinate run.
func TestResolveMessageUnitsPointLookup(t *testing.T) {
ix := openTestIndex(t)
ctx := context.Background()
seedUnitRow(t, ix, "u:s:0", "s", 0, 0, false)
seedUnitRow(t, ix, "r:s:a", "s", 1, 3, false)
// Ordinals 4-5 are a gap: no unit covers them.
seedUnitRow(t, ix, "r:s:b", "s", 6, 7, true)
// Session t's first unit starts above ordinal 0.
seedUnitRow(t, ix, "r:t:a", "t", 5, 6, false)
runA := db.UnitRef{DocKey: "r:s:a", SessionID: "s", OrdinalStart: 1, OrdinalEnd: 3}
runB := db.UnitRef{
DocKey: "r:s:b", SessionID: "s", OrdinalStart: 6, OrdinalEnd: 7, Subordinate: true,
}
tests := []struct {
name string
ref db.MessageRef
want db.UnitRef
}{
{"user unit own ordinal", db.MessageRef{SessionID: "s", Ordinal: 0},
db.UnitRef{DocKey: "u:s:0", SessionID: "s"}},
{"run first ordinal", db.MessageRef{SessionID: "s", Ordinal: 1}, runA},
{"run interior ordinal", db.MessageRef{SessionID: "s", Ordinal: 2}, runA},
{"run last ordinal", db.MessageRef{SessionID: "s", Ordinal: 3}, runA},
{"gap after run", db.MessageRef{SessionID: "s", Ordinal: 4}, db.UnitRef{}},
{"gap before next run", db.MessageRef{SessionID: "s", Ordinal: 5}, db.UnitRef{}},
{"subordinate run", db.MessageRef{SessionID: "s", Ordinal: 7}, runB},
{"past last unit", db.MessageRef{SessionID: "s", Ordinal: 99}, db.UnitRef{}},
{"before first unit", db.MessageRef{SessionID: "t", Ordinal: 2}, db.UnitRef{}},
{"unknown session", db.MessageRef{SessionID: "nope", Ordinal: 1}, db.UnitRef{}},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
got, err := ix.ResolveMessageUnits(ctx, []db.MessageRef{tc.ref})
require.NoError(t, err)
require.Len(t, got, 1)
assert.Equal(t, tc.want, got[0])
})
}
}
// TestResolveMessageUnitsIgnoresParkedRows pins the mid-refresh read
// contract: Refresh parks a displaced row at a negative sentinel ordinal
// non-transactionally (evictSlotOccupant), so a concurrent resolver call can
// see it. The point lookup's ordinal-DESC seek would otherwise land on the
// parked row (its old ordinal_end still covers the ref) and emit a negative
// OrdinalStart; parked rows must be invisible to readers.
func TestResolveMessageUnitsIgnoresParkedRows(t *testing.T) {
ix := openTestIndex(t)
// Parked mid-refresh: ordinal moved to the sentinel, ordinal_end still
// holds its old value, so containment (2 <= 3) would pass.
seedUnitRow(t, ix, "r:s:parked", "s", -2, 3, false)
seedUnitRow(t, ix, "r:s:valid", "s", 5, 6, false)
got, err := ix.ResolveMessageUnits(context.Background(), []db.MessageRef{
{SessionID: "s", Ordinal: 2},
{SessionID: "s", Ordinal: 5},
})
require.NoError(t, err)
require.Len(t, got, 2)
assert.Equal(t, db.UnitRef{}, got[0],
"a ref covered only by a parked row must stay unresolved, not surface a negative ordinal")
assert.Equal(t, db.UnitRef{
DocKey: "r:s:valid", SessionID: "s", OrdinalStart: 5, OrdinalEnd: 6,
}, got[1], "valid rows must keep resolving alongside a parked one")
}
// TestHydrateHitsIgnoresParkedRows pins the same mid-refresh contract on the
// hit-hydration path: a KNN hit whose doc_key points at a sentinel-parked
// mirror row must be dropped (like a vanished doc), never hydrated into a
// hit with a negative ordinal.
func TestHydrateHitsIgnoresParkedRows(t *testing.T) {
ix := openTestIndex(t)
ctx := context.Background()
seedMirrorRow(t, ix, "u-parked", db.EmbeddableUnit{
SessionID: "s1", Kind: "user", Ordinal: -1, OrdinalEnd: 4,
Content: "parked mid-refresh",
})
seedMirrorRow(t, ix, "u-valid", db.EmbeddableUnit{
SessionID: "s1", Kind: "user", Ordinal: 6, OrdinalEnd: 6,
Content: "still visible",
})
hits, err := ix.hydrateHits(ctx, []kitvec.Hit[string]{
{Doc: "u-parked", ChunkIndex: 0, Score: 0.9},
{Doc: "u-valid", ChunkIndex: 0, Score: 0.8},
})
require.NoError(t, err)
require.Len(t, hits, 1, "the parked row's hit must be dropped")
assert.Equal(t, 6, hits[0].Ordinal)
assert.Equal(t, "still visible", hits[0].Snippet)
}
// TestResolveMessageUnitsResultParallelToRefs pins that one call over a
// mixed batch keeps the result slice parallel to refs, with zero UnitRefs
// holding the positions of unresolvable refs.
func TestResolveMessageUnitsResultParallelToRefs(t *testing.T) {
ix := openTestIndex(t)
seedUnitRow(t, ix, "r:s:a", "s", 1, 3, true)
got, err := ix.ResolveMessageUnits(context.Background(), []db.MessageRef{
{SessionID: "s", Ordinal: 4},
{SessionID: "s", Ordinal: 2},
{SessionID: "missing", Ordinal: 2},
})
require.NoError(t, err)
require.Len(t, got, 3)
assert.Equal(t, db.UnitRef{}, got[0], "gap ref stays zero")
assert.Equal(t, db.UnitRef{
DocKey: "r:s:a", SessionID: "s", OrdinalStart: 1, OrdinalEnd: 3, Subordinate: true,
}, got[1])
assert.Equal(t, db.UnitRef{}, got[2], "unknown session stays zero")
}
// TestResolveMessageUnitsEmptyRefs pins the empty-input shape: an empty,
// non-nil result and no query error.
func TestResolveMessageUnitsEmptyRefs(t *testing.T) {
ix := openTestIndex(t)
got, err := ix.ResolveMessageUnits(context.Background(), nil)
require.NoError(t, err)
assert.Empty(t, got)
}
// TestResolveMessageUnitsManyRefs feeds well over SQLite's historical
// 999-bind-variable budget through the resolver in one call: the per-ref
// point-lookup implementation must handle any batch size without an
// unbounded IN list.
func TestResolveMessageUnitsManyRefs(t *testing.T) {
ix := openTestIndex(t)
const n = 1200
seedUnitRow(t, ix, "r:s:wide", "s", 0, n-1, false)
refs := make([]db.MessageRef, n)
for i := range refs {
refs[i] = db.MessageRef{SessionID: "s", Ordinal: i}
}
got, err := ix.ResolveMessageUnits(context.Background(), refs)
require.NoError(t, err)
require.Len(t, got, n)
for i, u := range got {
require.Equal(t, "r:s:wide", u.DocKey, "ref %d must resolve", i)
}
}
// TestResolveMessageUnitsVersionMismatchGate pins the read gate: a read-only
// Index over a vectors.db stamped by a different mirror schema version must
// fail closed with ErrMirrorVersionMismatch before touching any table, the
// same contract Search and StaleActive already honor.
func TestResolveMessageUnitsVersionMismatchGate(t *testing.T) {
ctx := context.Background()
path := filepath.Join(t.TempDir(), "vectors.db")
seedV2Mirror(t, path)
ro, err := Open(ctx, path, true, 4000)
require.NoError(t, err, "read-only Open must succeed against a mismatched mirror")
defer ro.Close()
_, err = ro.ResolveMessageUnits(ctx, []db.MessageRef{{SessionID: "s1", Ordinal: 0}})
require.Error(t, err)
assert.ErrorIs(t, err, ErrMirrorVersionMismatch)
}
// runDocAnchorFixture returns the three-member run content and offsets
// shared by TestDocAnchor's run-doc cases: "aaaaa" (ordinal 5), "bbbbb"
// (ordinal 6), "ccccc" (ordinal 7), joined with "\n\n" as db's runUnit does.
func runDocAnchorFixture() (string, []db.UnitOffset) {
content := "aaaaa\n\nbbbbb\n\nccccc"
offsets := []db.UnitOffset{
{Ordinal: 5, RuneStart: 0, ByteStart: 0},
{Ordinal: 6, RuneStart: 7, ByteStart: 7},
{Ordinal: 7, RuneStart: 14, ByteStart: 14},
}
return content, offsets
}
// TestDocAnchorRunDocReturnsMemberAnchorAndLocalSnippet pins DocAnchor's run
// branch: with maxInputChars large enough that the whole content is one
// chunk, the anchor is the member whose span contains the chunk's center
// rune, and the snippet is confined to that member's own text rather than
// spilling into a neighboring member.
func TestDocAnchorRunDocReturnsMemberAnchorAndLocalSnippet(t *testing.T) {
content, offsets := runDocAnchorFixture()
anchor, snippet := DocAnchor(content, offsets, 0, 0, 100)
assert.Equal(t, 6, anchor, "anchor must be the member containing the chunk's center rune")
assert.Equal(t, "bbbbb", snippet, "snippet must be the anchor member's own text")
}
// TestDocAnchorEmptyOffsetsReturnsDocOrdinalAndWholeSnippet pins DocAnchor's
// user-doc branch: empty offsets means the caller-supplied docOrdinal
// anchors as-is, with a snippet of the whole matched chunk.
func TestDocAnchorEmptyOffsetsReturnsDocOrdinalAndWholeSnippet(t *testing.T) {
anchor, snippet := DocAnchor("a plain user question", nil, 3, 0, 100)
assert.Equal(t, 3, anchor)
assert.Equal(t, "a plain user question", snippet)
}
// TestDocAnchorOutOfRangeChunkIndexFallsBack pins the documented fallback
// for a stale/degenerate ChunkIndex whose re-split window misses the
// content entirely: a user doc yields an empty snippet (see chunkSnippet),
// while a run doc falls back to the anchor member's own span text (see
// resolveRunAnchor) rather than a panic or cross-member text.
func TestDocAnchorOutOfRangeChunkIndexFallsBack(t *testing.T) {
t.Run("user doc", func(t *testing.T) {
anchor, snippet := DocAnchor("a plain user question", nil, 3, 99, 100)
assert.Equal(t, 3, anchor)
assert.Empty(t, snippet)
})
t.Run("run doc", func(t *testing.T) {
content, offsets := runDocAnchorFixture()
anchor, snippet := DocAnchor(content, offsets, 0, 99, 10)
assert.Equal(t, 7, anchor)
assert.Equal(t, "ccccc", snippet)
})
}
// TestSearchReducedDimensionsEndToEnd exercises the full reduced-dimension
// path against a Matryoshka-style stub server: every embeddings request —
// document builds and the search query alike — carries the configured
// dimensions field, the index stores vectors at the reduced length, and
// semantic search ranks in the reduced space. The stub serves native
// 4-dimensional vectors and honors the OpenAI-compatible dimensions field by
// slicing and renormalizing, the way Matryoshka-capable endpoints do.
func TestSearchReducedDimensionsEndToEnd(t *testing.T) {
const nativeDim, reducedDim = 4, 3
nativeVector := func(text string) []float32 {
switch {
case strings.Contains(text, "alpha"):
return []float32{1, 0, 0, 1}
case strings.Contains(text, "beta"):
return []float32{0, 1, 0, 1}
default:
return []float32{0, 0, 1, 1}
}
}
var mu sync.Mutex
var requestedDims []int
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var req struct {
Input []string `json:"input"`
Dimensions int `json:"dimensions"`
}
require.NoError(t, json.NewDecoder(r.Body).Decode(&req))
mu.Lock()
requestedDims = append(requestedDims, req.Dimensions)
mu.Unlock()
dim := nativeDim
if req.Dimensions > 0 {
dim = req.Dimensions
}
data := make([]map[string]any, len(req.Input))
for i, text := range req.Input {
sliced := nativeVector(text)[:dim]
var norm float64
for _, v := range sliced {
norm += float64(v) * float64(v)
}
norm = math.Sqrt(norm)
vec := make([]float32, dim)
for j, v := range sliced {
vec[j] = float32(float64(v) / norm)
}
data[i] = map[string]any{"index": i, "embedding": vec}
}
w.Header().Set("Content-Type", "application/json")
require.NoError(t, json.NewEncoder(w).Encode(map[string]any{"data": data}))
}))
defer srv.Close()
enc := NewEncoder(EncoderConfig{
Endpoint: srv.URL + "/v1",
Model: "matryoshka-model",
Dimension: reducedDim,
RequestDimensions: true,
Timeout: 5 * time.Second,
MaxRetries: 1,
})
ix := openTestIndex(t)
ctx := context.Background()
gen := kitvec.Generation{
Model: "matryoshka-model",
Dimensions: reducedDim,
Params: map[string]string{"request_dimensions": "true"},
}
result, err := ix.Build(ctx, threeDocSearchSource(), enc, gen, BuildOptions{})
require.NoError(t, err)
assert.True(t, result.Activated)
assert.Equal(t, 3, result.Fill.Documents)
gens, err := ix.Generations(ctx)
require.NoError(t, err)
require.Len(t, gens, 1)
assert.Equal(t, reducedDim, gens[0].Dimension,
"the generation stores the reduced dimension")
hits, err := ix.Search(ctx, enc, "alpha", 10)
require.NoError(t, err)
require.NotEmpty(t, hits)
assert.Equal(t, "s1", hits[0].SessionID)
assert.Equal(t, 0, hits[0].Ordinal)
assert.Contains(t, hits[0].Snippet, "alpha")
mu.Lock()
defer mu.Unlock()
require.GreaterOrEqual(t, len(requestedDims), 2,
"at least one build request and the query request reached the server")
for i, d := range requestedDims {
assert.Equal(t, reducedDim, d,
"request %d must ask for the reduced dimension", i)
}
}