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

1443 lines
53 KiB
Go

//go:build pgtest
package postgres
import (
"context"
"database/sql"
"fmt"
"net/url"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.kenn.io/agentsview/internal/db"
)
// fakeVectorSource is an in-memory VectorPushSource whose generation, aggregate
// hashes, and per-session docs are mutated between pushes to drive the delta,
// eviction, and cross-generation cases. genOverride, when set, replaces the
// nth Generation answer (1-based call count) so a test can change the source's
// state between the push's initial Generation call and its mid-push
// re-checks. exportHashes, when set, overrides the per-session hash SessionDocs
// returns (simulating a local index that changed between the delta scan and
// the export); by default SessionDocs echoes the delta-scan hash so pushes
// proceed.
type fakeVectorSource struct {
gen VectorGenerationInfo
hasGen bool
hashes map[string]string
docs map[string][]VectorPushDoc
exportHashes map[string]string
docsCalls map[string]int // per-session SessionDocs invocation count
genCalls int
genOverride func(call int) (VectorGenerationInfo, bool, error)
}
func (f *fakeVectorSource) Generation(
ctx context.Context,
) (VectorGenerationInfo, bool, error) {
f.genCalls++
if f.genOverride != nil {
return f.genOverride(f.genCalls)
}
return f.gen, f.hasGen, nil
}
func (f *fakeVectorSource) SessionDocHashes(
ctx context.Context,
) (map[string]string, error) {
return f.hashes, nil
}
func (f *fakeVectorSource) SessionDocs(
ctx context.Context, id string,
) ([]VectorPushDoc, string, error) {
if f.docsCalls == nil {
f.docsCalls = make(map[string]int)
}
f.docsCalls[id]++
if hash, ok := f.exportHashes[id]; ok {
return f.docs[id], hash, nil
}
return f.docs[id], f.hashes[id], nil
}
// newVectorPushTestSync creates a fresh schema with the base + vector tables
// and returns a Sync wired to a local SQLite DB. It skips the test when the
// pgvector extension is unavailable, since the whole phase no-ops there.
func newVectorPushTestSync(
t *testing.T, pgURL, schema string,
) (*Sync, *db.DB, *sql.DB) {
t.Helper()
pg, err := Open(pgURL, schema, true)
require.NoError(t, err, "Open")
t.Cleanup(func() { _ = pg.Close() })
ctx := context.Background()
_, err = pg.Exec(`DROP SCHEMA IF EXISTS ` + schema + ` CASCADE`)
require.NoError(t, err, "drop schema")
require.NoError(t, EnsureSchema(ctx, pg, schema), "EnsureSchema")
available, err := VectorExtensionAvailable(ctx, pg)
require.NoError(t, err, "VectorExtensionAvailable")
if !available {
t.Skip("pgvector extension unavailable")
}
localDB, err := db.Open(filepath.Join(t.TempDir(), "local.db"))
require.NoError(t, err, "db.Open")
t.Cleanup(func() { _ = localDB.Close() })
sync := &Sync{
pg: pg,
local: localDB,
machine: "test-machine",
schema: schema,
schemaDone: true,
}
return sync, localDB, pg
}
// seedVectorSession inserts a minimal session with one message so that a Push
// creates a PG sessions row carrying this pusher's owner marker.
func seedVectorSession(t *testing.T, localDB *db.DB, id string) {
t.Helper()
require.NoError(t, localDB.UpsertSession(db.Session{
ID: id,
Project: "proj",
Machine: "test-machine",
Agent: "claude",
MessageCount: 1,
UserMessageCount: 1,
CreatedAt: "2026-01-01T00:00:00Z",
}), "UpsertSession "+id)
require.NoError(t, localDB.InsertMessages([]db.Message{{
SessionID: id,
Ordinal: 0,
Role: "user",
Content: id,
ContentLength: len(id),
}}), "InsertMessages "+id)
}
// seedVectorSessionProject inserts a minimal session under the given project so
// the PG sessions row carries a project the vector push filter can scope on.
func seedVectorSessionProject(t *testing.T, localDB *db.DB, id, project string) {
t.Helper()
require.NoError(t, localDB.UpsertSession(db.Session{
ID: id,
Project: project,
Machine: "test-machine",
Agent: "claude",
MessageCount: 1,
UserMessageCount: 1,
CreatedAt: "2026-01-01T00:00:00Z",
}), "UpsertSession "+id)
require.NoError(t, localDB.InsertMessages([]db.Message{{
SessionID: id,
Ordinal: 0,
Role: "user",
Content: id,
ContentLength: len(id),
}}), "InsertMessages "+id)
}
// vdoc builds a VectorPushDoc with the supplied embeddings as consecutive
// chunks (chunk_index 0..n-1).
func vdoc(
sessionID, docKey string, ordinal int,
content, hash string, chunks ...[]float32,
) VectorPushDoc {
d := VectorPushDoc{
DocKey: docKey,
SessionID: sessionID,
Ordinal: ordinal,
OrdinalEnd: ordinal,
OffsetsJSON: "[]",
Content: content,
ContentHash: hash,
}
for i, emb := range chunks {
d.Chunks = append(d.Chunks, VectorPushChunk{ChunkIndex: i, Embedding: emb})
}
return d
}
func countRows(t *testing.T, pg *sql.DB, query string, args ...any) int {
t.Helper()
var n int
require.NoError(t, pg.QueryRow(query, args...).Scan(&n), "count: "+query)
return n
}
// docOrdinal returns the current ordinal of the doc_key's vector_documents row.
func docOrdinal(t *testing.T, pg *sql.DB, docKey string) int {
t.Helper()
var ord int
require.NoError(t, pg.QueryRow(
`SELECT ordinal FROM vector_documents WHERE doc_key = $1`, docKey,
).Scan(&ord), "ordinal of "+docKey)
return ord
}
func TestVectorPushRoundTrip(t *testing.T) {
pgURL := testPGURL(t)
sync, localDB, pg := newVectorPushTestSync(
t, pgURL, "agentsview_vector_push_roundtrip_test")
ctx := context.Background()
seedVectorSession(t, localDB, "A")
seedVectorSession(t, localDB, "B")
sync.vectorSource = &fakeVectorSource{
gen: VectorGenerationInfo{Fingerprint: "fp-rt", Model: "m", Dimension: 4},
hasGen: true,
hashes: map[string]string{"A": "ha", "B": "hb"},
docs: map[string][]VectorPushDoc{
"A": {
vdoc("A", "A#0", 0, "c1", "h1",
[]float32{1, 0, 0, 0}, []float32{0, 1, 0, 0}),
vdoc("A", "A#1", 1, "c2", "h2", []float32{0, 0, 1, 0}),
},
"B": {vdoc("B", "B#0", 0, "c3", "h3", []float32{0, 0, 0, 1})},
},
}
var vectorReports, prepareReports []PushProgress
res, err := sync.Push(ctx, false, func(p PushProgress) {
switch p.Phase {
case "vectors":
vectorReports = append(vectorReports, p)
case "preparing":
prepareReports = append(prepareReports, p)
}
})
require.NoError(t, err, "Push")
assert.False(t, res.Vectors.Skipped)
assert.Equal(t, 2, res.Vectors.SessionsPushed)
assert.Equal(t, 3, res.Vectors.DocsPushed)
assert.Equal(t, 4, res.Vectors.ChunksPushed)
require.NotEmpty(t, prepareReports, "fingerprint phase must report progress")
prepared := prepareReports[len(prepareReports)-1]
assert.Equal(t, 2, prepared.SessionsDone)
assert.Equal(t, 2, prepared.SessionsTotal)
require.NotEmpty(t, vectorReports, "vector phase must report progress")
final := vectorReports[len(vectorReports)-1]
assert.Equal(t, 2, final.VectorSessionsDone)
assert.Equal(t, 2, final.VectorSessionsTotal)
assert.Equal(t, 4, final.VectorChunksPushed)
genID, dim, ok, err := LookupVectorGeneration(ctx, pg, "fp-rt")
require.NoError(t, err, "LookupVectorGeneration")
require.True(t, ok, "generation registered")
assert.Equal(t, 4, dim)
assert.Equal(t, 3,
countRows(t, pg, `SELECT COUNT(*) FROM vector_documents`))
assert.Equal(t, 4,
countRows(t, pg, `SELECT COUNT(*) FROM `+vectorChunkTable(genID)))
assert.Equal(t, 2, countRows(t, pg,
`SELECT COUNT(*) FROM vector_push_state WHERE generation_id = $1`, genID))
assert.Equal(t, 1, countRows(t, pg,
`SELECT COUNT(*) FROM vector_generation_machines
WHERE generation_id = $1 AND machine = $2`, genID, "test-machine"))
}
func TestVectorPushDeltaNoop(t *testing.T) {
pgURL := testPGURL(t)
sync, localDB, _ := newVectorPushTestSync(
t, pgURL, "agentsview_vector_push_noop_test")
ctx := context.Background()
seedVectorSession(t, localDB, "A")
seedVectorSession(t, localDB, "B")
sync.vectorSource = &fakeVectorSource{
gen: VectorGenerationInfo{Fingerprint: "fp-noop", Model: "m", Dimension: 4},
hasGen: true,
hashes: map[string]string{"A": "ha", "B": "hb"},
docs: map[string][]VectorPushDoc{
"A": {vdoc("A", "A#0", 0, "c1", "h1", []float32{1, 0, 0, 0})},
"B": {vdoc("B", "B#0", 0, "c3", "h3", []float32{0, 0, 0, 1})},
},
}
_, err := sync.Push(ctx, false, nil)
require.NoError(t, err, "first Push")
res, err := sync.Push(ctx, false, nil)
require.NoError(t, err, "second Push")
assert.Equal(t, 2, res.Vectors.SessionsUnchanged)
assert.Equal(t, 0, res.Vectors.SessionsPushed)
assert.Equal(t, 0, res.Vectors.DocsPushed)
}
// TestVectorPushFullRepairsSilentCorruption pins --full's repair contract for
// the vector phase: a chunk row missing from PG while vector_push_state still
// reports the session current is invisible to a normal delta push, but a full
// push bypasses the unchanged-hash skip and restores it.
func TestVectorPushFullRepairsSilentCorruption(t *testing.T) {
pgURL := testPGURL(t)
sync, localDB, pg := newVectorPushTestSync(
t, pgURL, "agentsview_vector_push_full_test")
ctx := context.Background()
seedVectorSession(t, localDB, "A")
sync.vectorSource = &fakeVectorSource{
gen: VectorGenerationInfo{Fingerprint: "fp-full", Model: "m", Dimension: 4},
hasGen: true,
hashes: map[string]string{"A": "ha"},
docs: map[string][]VectorPushDoc{
"A": {vdoc("A", "A#0", 0, "c1", "h1", []float32{1, 0, 0, 0})},
},
}
_, err := sync.Push(ctx, false, nil)
require.NoError(t, err, "seed Push")
genID, _, ok, err := LookupVectorGeneration(ctx, pg, "fp-full")
require.NoError(t, err)
require.True(t, ok)
// Silently lose the chunk while push state still says "current".
_, err = pg.Exec(`DELETE FROM ` + vectorChunkTable(genID))
require.NoError(t, err, "corrupting chunk table")
res, err := sync.pushVectors(ctx, false, nil, nil)
require.NoError(t, err, "delta pushVectors")
assert.Equal(t, 1, res.SessionsUnchanged, "delta push cannot see the loss")
assert.Equal(t, 0,
countRows(t, pg, `SELECT COUNT(*) FROM `+vectorChunkTable(genID)))
res, err = sync.pushVectors(ctx, true, nil, nil)
require.NoError(t, err, "full pushVectors")
assert.Equal(t, 1, res.SessionsPushed)
assert.Equal(t, 0, res.SessionsUnchanged)
assert.Equal(t, 1,
countRows(t, pg, `SELECT COUNT(*) FROM `+vectorChunkTable(genID)),
"full push restores the missing chunk")
}
// TestVectorPushDeferredOnSessionError pins that a changed session named in
// failedSessions is deferred — its newer local vectors must not land ahead of
// sessions/messages rows its session-phase push failed to write — and that
// the untouched delta state sends it on the next successful push.
func TestVectorPushDeferredOnSessionError(t *testing.T) {
pgURL := testPGURL(t)
sync, localDB, pg := newVectorPushTestSync(
t, pgURL, "agentsview_vector_push_deferred_test")
ctx := context.Background()
seedVectorSession(t, localDB, "A")
seedVectorSession(t, localDB, "B")
fake := &fakeVectorSource{
gen: VectorGenerationInfo{Fingerprint: "fp-def", Model: "m", Dimension: 4},
hasGen: true,
hashes: map[string]string{"A": "ha1", "B": "hb1"},
docs: map[string][]VectorPushDoc{
"A": {vdoc("A", "A#0", 0, "old", "h-old", []float32{1, 0, 0, 0})},
"B": {vdoc("B", "B#0", 0, "cb", "hb", []float32{0, 0, 0, 1})},
},
}
sync.vectorSource = fake
_, err := sync.Push(ctx, false, nil)
require.NoError(t, err, "seed Push")
// A's docs change locally, but its session push failed this run.
fake.hashes["A"] = "ha2"
fake.docs["A"] = []VectorPushDoc{
vdoc("A", "A#0", 0, "new", "h-new", []float32{0, 1, 0, 0}),
}
res, err := sync.pushVectors(ctx, false, map[string]struct{}{"A": {}}, nil)
require.NoError(t, err, "deferred pushVectors")
assert.Equal(t, 1, res.SessionsDeferred)
assert.Equal(t, 0, res.SessionsPushed)
assert.Equal(t, 1, res.SessionsUnchanged, "B is unchanged")
var content string
require.NoError(t, pg.QueryRow(
`SELECT content FROM vector_documents WHERE doc_key = $1`, "A#0",
).Scan(&content))
assert.Equal(t, "old", content, "deferred session keeps its prior vectors")
// Next push with no failures sends the deferred session.
res, err = sync.pushVectors(ctx, false, nil, nil)
require.NoError(t, err, "healing pushVectors")
assert.Equal(t, 0, res.SessionsDeferred)
assert.Equal(t, 1, res.SessionsPushed)
require.NoError(t, pg.QueryRow(
`SELECT content FROM vector_documents WHERE doc_key = $1`, "A#0",
).Scan(&content))
assert.Equal(t, "new", content)
}
func TestVectorPushContentChange(t *testing.T) {
pgURL := testPGURL(t)
sync, localDB, pg := newVectorPushTestSync(
t, pgURL, "agentsview_vector_push_content_test")
ctx := context.Background()
seedVectorSession(t, localDB, "A")
seedVectorSession(t, localDB, "B")
fake := &fakeVectorSource{
gen: VectorGenerationInfo{Fingerprint: "fp-cc", Model: "m", Dimension: 4},
hasGen: true,
hashes: map[string]string{"A": "ha", "B": "hb"},
docs: map[string][]VectorPushDoc{
"A": {vdoc("A", "A#0", 0, "c1", "h1", []float32{1, 0, 0, 0})},
"B": {vdoc("B", "B#0", 0, "c3", "h3", []float32{0, 0, 0, 1})},
},
}
sync.vectorSource = fake
_, err := sync.Push(ctx, false, nil)
require.NoError(t, err, "first Push")
fake.hashes["A"] = "ha2"
fake.docs["A"] = []VectorPushDoc{
vdoc("A", "A#0", 0, "c1-updated", "h1b", []float32{0, 1, 0, 0}),
}
res, err := sync.Push(ctx, false, nil)
require.NoError(t, err, "second Push")
assert.Equal(t, 1, res.Vectors.SessionsPushed)
assert.Equal(t, 1, res.Vectors.SessionsUnchanged)
var content, hash string
require.NoError(t, pg.QueryRow(
`SELECT content, content_hash FROM vector_documents WHERE doc_key = $1`,
"A#0").Scan(&content, &hash), "read updated doc")
assert.Equal(t, "c1-updated", content)
assert.Equal(t, "h1b", hash)
}
func TestVectorPushEviction(t *testing.T) {
pgURL := testPGURL(t)
sync, localDB, pg := newVectorPushTestSync(
t, pgURL, "agentsview_vector_push_evict_test")
ctx := context.Background()
seedVectorSession(t, localDB, "A")
seedVectorSession(t, localDB, "B")
fake := &fakeVectorSource{
gen: VectorGenerationInfo{Fingerprint: "fp-ev", Model: "m", Dimension: 4},
hasGen: true,
hashes: map[string]string{"A": "ha", "B": "hb"},
docs: map[string][]VectorPushDoc{
"A": {vdoc("A", "A#0", 0, "c1", "h1", []float32{1, 0, 0, 0})},
"B": {vdoc("B", "B#0", 0, "c3", "h3", []float32{0, 0, 0, 1})},
},
}
sync.vectorSource = fake
_, err := sync.Push(ctx, false, nil)
require.NoError(t, err, "first Push")
genID, _, ok, err := LookupVectorGeneration(ctx, pg, "fp-ev")
require.NoError(t, err)
require.True(t, ok)
delete(fake.hashes, "B")
delete(fake.docs, "B")
res, err := sync.Push(ctx, false, nil)
require.NoError(t, err, "second Push")
assert.Equal(t, 1, res.Vectors.SessionsEvicted)
assert.Equal(t, 1, res.Vectors.DocsDeleted)
assert.Equal(t, 0, countRows(t, pg,
`SELECT COUNT(*) FROM `+vectorChunkTable(genID)+` WHERE doc_key = $1`,
"B#0"))
assert.Equal(t, 0, countRows(t, pg,
`SELECT COUNT(*) FROM vector_push_state
WHERE generation_id = $1 AND session_id = $2`, genID, "B"))
assert.Equal(t, 0, countRows(t, pg,
`SELECT COUNT(*) FROM vector_documents WHERE session_id = $1`, "B"))
assert.Equal(t, 1, countRows(t, pg,
`SELECT COUNT(*) FROM vector_documents WHERE session_id = $1`, "A"))
}
func TestVectorPushNoActiveGeneration(t *testing.T) {
pgURL := testPGURL(t)
sync, localDB, pg := newVectorPushTestSync(
t, pgURL, "agentsview_vector_push_nogen_test")
ctx := context.Background()
seedVectorSession(t, localDB, "A")
sync.vectorSource = &fakeVectorSource{hasGen: false}
res, err := sync.Push(ctx, false, nil)
require.NoError(t, err, "Push")
assert.True(t, res.Vectors.Skipped)
assert.NotEmpty(t, res.Vectors.SkippedReason)
assert.Equal(t, 0,
countRows(t, pg, `SELECT COUNT(*) FROM vector_generations`))
}
func TestVectorPushSharedDocAcrossGenerations(t *testing.T) {
pgURL := testPGURL(t)
sync, localDB, pg := newVectorPushTestSync(
t, pgURL, "agentsview_vector_push_sharedgen_test")
ctx := context.Background()
seedVectorSession(t, localDB, "X")
seedVectorSession(t, localDB, "Y")
fake := &fakeVectorSource{
gen: VectorGenerationInfo{Fingerprint: "fp-a", Model: "m", Dimension: 4},
hasGen: true,
hashes: map[string]string{"X": "hx", "Y": "hy"},
docs: map[string][]VectorPushDoc{
"X": {vdoc("X", "X#0", 0, "cx", "hx1", []float32{1, 0, 0, 0})},
"Y": {vdoc("Y", "Y#0", 0, "cy", "hy1", []float32{0, 1, 0, 0})},
},
}
sync.vectorSource = fake
_, err := sync.Push(ctx, false, nil)
require.NoError(t, err, "push gen A")
genA, _, ok, err := LookupVectorGeneration(ctx, pg, "fp-a")
require.NoError(t, err)
require.True(t, ok)
// Switch to a second generation (new fingerprint) sharing the same docs.
fake.gen.Fingerprint = "fp-b"
_, err = sync.Push(ctx, false, nil)
require.NoError(t, err, "push gen B")
genB, _, ok, err := LookupVectorGeneration(ctx, pg, "fp-b")
require.NoError(t, err)
require.True(t, ok)
// Drop Y from gen B's source, triggering a gen-B eviction of Y.
delete(fake.hashes, "Y")
delete(fake.docs, "Y")
res, err := sync.Push(ctx, false, nil)
require.NoError(t, err, "push gen B without Y")
assert.Equal(t, 1, res.Vectors.SessionsEvicted)
assert.Equal(t, 0, res.Vectors.DocsDeleted,
"Y's doc is still referenced by gen A, so it must survive")
assert.Equal(t, 1, countRows(t, pg,
`SELECT COUNT(*) FROM `+vectorChunkTable(genA)+` WHERE doc_key = $1`,
"Y#0"), "gen A chunks for Y survive")
assert.Equal(t, 0, countRows(t, pg,
`SELECT COUNT(*) FROM `+vectorChunkTable(genB)+` WHERE doc_key = $1`,
"Y#0"), "gen B chunks for Y evicted")
assert.Equal(t, 1, countRows(t, pg,
`SELECT COUNT(*) FROM vector_documents WHERE doc_key = $1`, "Y#0"),
"shared doc row survives")
assert.Equal(t, 0, countRows(t, pg,
`SELECT COUNT(*) FROM vector_push_state
WHERE generation_id = $1 AND session_id = $2`, genB, "Y"))
}
// TestVectorPushOrdinalShift pins the slot-safe replacement: stable doc_keys
// whose ordinals shift up (0/1 -> 1/2 with a new doc taking slot 0) re-push
// without a UNIQUE (session_id, ordinal) violation and land on their final
// ordinals.
func TestVectorPushOrdinalShift(t *testing.T) {
pgURL := testPGURL(t)
sync, localDB, pg := newVectorPushTestSync(
t, pgURL, "agentsview_vector_push_shift_test")
ctx := context.Background()
seedVectorSession(t, localDB, "A")
fake := &fakeVectorSource{
gen: VectorGenerationInfo{Fingerprint: "fp-sh", Model: "m", Dimension: 4},
hasGen: true,
hashes: map[string]string{"A": "h1"},
docs: map[string][]VectorPushDoc{
"A": {
vdoc("A", "k0", 0, "c0", "hc0", []float32{1, 0, 0, 0}),
vdoc("A", "k1", 1, "c1", "hc1", []float32{0, 1, 0, 0}),
},
},
}
sync.vectorSource = fake
_, err := sync.Push(ctx, false, nil)
require.NoError(t, err, "first Push")
// Shift k0/k1 up to ordinals 1/2 and insert kNew at ordinal 0.
fake.hashes["A"] = "h2"
fake.docs["A"] = []VectorPushDoc{
vdoc("A", "kNew", 0, "cn", "hcn", []float32{0, 0, 1, 0}),
vdoc("A", "k0", 1, "c0", "hc0", []float32{1, 0, 0, 0}),
vdoc("A", "k1", 2, "c1", "hc1", []float32{0, 1, 0, 0}),
}
res, err := sync.Push(ctx, false, nil)
require.NoError(t, err, "shift Push")
assert.Equal(t, 1, res.Vectors.SessionsPushed)
assert.Equal(t, 0, res.Vectors.DocsDeleted)
assert.Equal(t, 0, docOrdinal(t, pg, "kNew"))
assert.Equal(t, 1, docOrdinal(t, pg, "k0"))
assert.Equal(t, 2, docOrdinal(t, pg, "k1"))
assert.Equal(t, 3,
countRows(t, pg, `SELECT COUNT(*) FROM vector_documents WHERE session_id = $1`, "A"))
}
// TestVectorPushDocRemovedAndShared pins that a doc removed from a re-pushed
// session disappears from vector_documents in the same push, cascading its
// chunks out of every generation — including one that still embedded it.
// Regression: preserving the shared row parked at a negative ordinal left the
// older generation with chunks the read path could never hydrate (the
// ordinal >= 0 tombstone guard), silently shrinking its results. Local kit
// removal deletes a vanished doc's vectors from all generations, and PG must
// match.
func TestVectorPushDocRemovedAndShared(t *testing.T) {
pgURL := testPGURL(t)
sync, localDB, pg := newVectorPushTestSync(
t, pgURL, "agentsview_vector_push_shrink_test")
ctx := context.Background()
seedVectorSession(t, localDB, "A")
// Gen A embeds kShared + kDropShared (not kDropSolo).
fake := &fakeVectorSource{
gen: VectorGenerationInfo{Fingerprint: "fp-ga", Model: "m", Dimension: 4},
hasGen: true,
hashes: map[string]string{"A": "ha"},
docs: map[string][]VectorPushDoc{
"A": {
vdoc("A", "kShared", 0, "cs", "hcs", []float32{1, 0, 0, 0}),
vdoc("A", "kDropShared", 1, "cds", "hcds", []float32{0, 1, 0, 0}),
},
},
}
sync.vectorSource = fake
_, err := sync.Push(ctx, false, nil)
require.NoError(t, err, "push gen A")
genA, _, ok, err := LookupVectorGeneration(ctx, pg, "fp-ga")
require.NoError(t, err)
require.True(t, ok)
// Gen B embeds all three docs for A.
fake.gen.Fingerprint = "fp-gb"
fake.hashes["A"] = "hb1"
fake.docs["A"] = []VectorPushDoc{
vdoc("A", "kShared", 0, "cs", "hcs", []float32{1, 0, 0, 0}),
vdoc("A", "kDropShared", 1, "cds", "hcds", []float32{0, 1, 0, 0}),
vdoc("A", "kDropSolo", 2, "cdo", "hcdo", []float32{0, 0, 1, 0}),
}
_, err = sync.Push(ctx, false, nil)
require.NoError(t, err, "push gen B")
genB, _, ok, err := LookupVectorGeneration(ctx, pg, "fp-gb")
require.NoError(t, err)
require.True(t, ok)
// Shrink gen B's A to only kShared: kDropShared + kDropSolo vanish locally.
fake.hashes["A"] = "hb2"
fake.docs["A"] = []VectorPushDoc{
vdoc("A", "kShared", 0, "cs", "hcs", []float32{1, 0, 0, 0}),
}
res, err := sync.Push(ctx, false, nil)
require.NoError(t, err, "shrink push gen B")
assert.Equal(t, 1, res.Vectors.SessionsPushed)
assert.Equal(t, 2, res.Vectors.DocsDeleted,
"kDropSolo and kDropShared both vanished locally")
// Both vanished docs are gone, whether or not another generation embedded
// them.
assert.Equal(t, 0, countRows(t, pg,
`SELECT COUNT(*) FROM vector_documents WHERE doc_key = $1`, "kDropSolo"))
assert.Equal(t, 0, countRows(t, pg,
`SELECT COUNT(*) FROM vector_documents WHERE doc_key = $1`, "kDropShared"))
assert.Equal(t, 0, countRows(t, pg,
`SELECT COUNT(*) FROM `+vectorChunkTable(genA)+` WHERE doc_key = $1`,
"kDropShared"), "gen A chunks for kDropShared cascade-deleted")
assert.Equal(t, 0, countRows(t, pg,
`SELECT COUNT(*) FROM `+vectorChunkTable(genB)+` WHERE doc_key = $1`,
"kDropShared"), "gen B chunks for kDropShared cleared")
// No parked tombstones survive the push, and every remaining gen A chunk
// hydrates: the older generation must not retain chunks pointing at rows
// the read path's ordinal >= 0 guard hides.
assert.Equal(t, 0, countRows(t, pg,
`SELECT COUNT(*) FROM vector_documents WHERE ordinal < 0`))
assert.Equal(t, 0, countRows(t, pg, `
SELECT COUNT(*) FROM `+vectorChunkTable(genA)+` c
LEFT JOIN vector_documents d
ON d.doc_key = c.doc_key AND d.ordinal >= 0
WHERE d.doc_key IS NULL`),
"gen A has no chunks that cannot hydrate")
// kShared stays live at its positive ordinal in both generations.
assert.Equal(t, 0, docOrdinal(t, pg, "kShared"))
assert.Equal(t, 1, countRows(t, pg,
`SELECT COUNT(*) FROM `+vectorChunkTable(genA)+` WHERE doc_key = $1`,
"kShared"), "gen A keeps its chunks for the still-present doc")
}
// TestVectorPushConflictSkipped pins finding 3: a session whose PG owner marker
// names another machine is skipped on push (counted in Conflicts) and left
// untouched on evict (also counted).
func TestVectorPushConflictSkipped(t *testing.T) {
pgURL := testPGURL(t)
sync, localDB, pg := newVectorPushTestSync(
t, pgURL, "agentsview_vector_push_conflict_test")
ctx := context.Background()
seedVectorSession(t, localDB, "A")
seedVectorSession(t, localDB, "B")
fake := &fakeVectorSource{
gen: VectorGenerationInfo{Fingerprint: "fp-cf", Model: "m", Dimension: 4},
hasGen: true,
hashes: map[string]string{"A": "ha", "B": "hb"},
docs: map[string][]VectorPushDoc{
"A": {vdoc("A", "cA0", 0, "ca", "hca", []float32{1, 0, 0, 0})},
"B": {vdoc("B", "cB0", 0, "cb", "hcb", []float32{0, 0, 0, 1})},
},
}
sync.vectorSource = fake
_, err := sync.Push(ctx, false, nil)
require.NoError(t, err, "first Push")
// Hand A to another machine on the PG side.
_, err = pg.Exec(
`UPDATE sessions SET owner_marker = 'other-machine' WHERE id = $1`, "A")
require.NoError(t, err, "reassign owner")
// A changed locally: the vector phase must skip it as a conflict, leaving
// its pushed content intact.
fake.hashes["A"] = "ha2"
fake.docs["A"] = []VectorPushDoc{
vdoc("A", "cA0", 0, "ca-new", "hca2", []float32{0, 1, 0, 0}),
}
res, err := sync.pushVectors(ctx, false, nil, nil)
require.NoError(t, err, "push conflict")
assert.Equal(t, 1, res.Conflicts)
assert.Equal(t, 0, res.SessionsPushed)
var content string
require.NoError(t, pg.QueryRow(
`SELECT content FROM vector_documents WHERE doc_key = $1`, "cA0",
).Scan(&content))
assert.Equal(t, "ca", content, "conflicted doc left untouched")
// A now vanishes from local: the evict path must leave the other machine's
// session in place and count the conflict.
delete(fake.hashes, "A")
delete(fake.docs, "A")
res, err = sync.pushVectors(ctx, false, nil, nil)
require.NoError(t, err, "evict conflict")
assert.Equal(t, 1, res.Conflicts)
assert.Equal(t, 0, res.SessionsEvicted)
genID, _, ok, err := LookupVectorGeneration(ctx, pg, "fp-cf")
require.NoError(t, err)
require.True(t, ok)
assert.Equal(t, 1, countRows(t, pg,
`SELECT COUNT(*) FROM vector_push_state
WHERE generation_id = $1 AND session_id = $2`, genID, "A"))
assert.Equal(t, 1, countRows(t, pg,
`SELECT COUNT(*) FROM vector_documents WHERE session_id = $1`, "A"))
}
// TestVectorPushLegacyMachineOwnership pins machine-aware ownership for
// legacy PG rows whose owner_marker is empty: a foreign machine name blocks
// both the push and evict paths (counted as conflicts), while this pusher's
// own machine name self-heals — its vectors push and evict normally.
func TestVectorPushLegacyMachineOwnership(t *testing.T) {
pgURL := testPGURL(t)
sync, localDB, pg := newVectorPushTestSync(
t, pgURL, "agentsview_vector_push_legacy_test")
ctx := context.Background()
seedVectorSession(t, localDB, "A")
seedVectorSession(t, localDB, "B")
fake := &fakeVectorSource{
gen: VectorGenerationInfo{Fingerprint: "fp-lg", Model: "m", Dimension: 4},
hasGen: true,
hashes: map[string]string{"A": "ha", "B": "hb"},
docs: map[string][]VectorPushDoc{
"A": {vdoc("A", "lA0", 0, "ca", "hca", []float32{1, 0, 0, 0})},
"B": {vdoc("B", "lB0", 0, "cb", "hcb", []float32{0, 0, 0, 1})},
},
}
sync.vectorSource = fake
_, err := sync.Push(ctx, false, nil)
require.NoError(t, err, "first Push")
// Rewrite both PG rows as legacy (empty marker): A owned by a foreign
// machine, B carrying this pusher's own machine name.
_, err = pg.Exec(`UPDATE sessions
SET owner_marker = '', machine = 'other-machine' WHERE id = $1`, "A")
require.NoError(t, err, "make A a foreign legacy row")
_, err = pg.Exec(`UPDATE sessions
SET owner_marker = '', machine = 'test-machine' WHERE id = $1`, "B")
require.NoError(t, err, "make B an owned legacy row")
// Both change locally: only B may push.
fake.hashes["A"], fake.hashes["B"] = "ha2", "hb2"
fake.docs["A"] = []VectorPushDoc{
vdoc("A", "lA0", 0, "ca-new", "hca2", []float32{0, 1, 0, 0}),
}
fake.docs["B"] = []VectorPushDoc{
vdoc("B", "lB0", 0, "cb-new", "hcb2", []float32{0, 0, 1, 0}),
}
res, err := sync.pushVectors(ctx, false, nil, nil)
require.NoError(t, err, "legacy push")
assert.Equal(t, 1, res.Conflicts, "foreign legacy row is a conflict")
assert.Equal(t, 1, res.SessionsPushed, "owned legacy row self-heals")
var content string
require.NoError(t, pg.QueryRow(
`SELECT content FROM vector_documents WHERE doc_key = $1`, "lA0",
).Scan(&content))
assert.Equal(t, "ca", content, "foreign legacy doc left untouched")
require.NoError(t, pg.QueryRow(
`SELECT content FROM vector_documents WHERE doc_key = $1`, "lB0",
).Scan(&content))
assert.Equal(t, "cb-new", content)
// Both vanish locally: only B may be evicted.
fake.hashes = map[string]string{}
fake.docs = map[string][]VectorPushDoc{}
res, err = sync.pushVectors(ctx, false, nil, nil)
require.NoError(t, err, "legacy evict")
assert.Equal(t, 1, res.Conflicts, "foreign legacy row is not evicted")
assert.Equal(t, 1, res.SessionsEvicted)
assert.Equal(t, 1, countRows(t, pg,
`SELECT COUNT(*) FROM vector_documents WHERE session_id = $1`, "A"))
assert.Equal(t, 0, countRows(t, pg,
`SELECT COUNT(*) FROM vector_documents WHERE session_id = $1`, "B"))
}
// TestVectorPushEvictionSkippedWhenSourceTurnsUnready pins the pre-eviction
// re-check: when the local source becomes unready between the delta scan and
// the eviction pass (an embeddings rebuild started mid-push), the eviction
// list — computed from a now-partial local view — must be dropped, and the
// next healthy push re-derives and applies it.
func TestVectorPushEvictionSkippedWhenSourceTurnsUnready(t *testing.T) {
pgURL := testPGURL(t)
sync, localDB, pg := newVectorPushTestSync(
t, pgURL, "agentsview_vector_push_unready_test")
ctx := context.Background()
seedVectorSession(t, localDB, "A")
seedVectorSession(t, localDB, "B")
fake := &fakeVectorSource{
gen: VectorGenerationInfo{Fingerprint: "fp-ur", Model: "m", Dimension: 4},
hasGen: true,
hashes: map[string]string{"A": "ha", "B": "hb"},
docs: map[string][]VectorPushDoc{
"A": {vdoc("A", "uA0", 0, "ca", "hca", []float32{1, 0, 0, 0})},
"B": {vdoc("B", "uB0", 0, "cb", "hcb", []float32{0, 0, 0, 1})},
},
}
sync.vectorSource = fake
_, err := sync.Push(ctx, false, nil)
require.NoError(t, err, "first Push")
// B vanishes locally, but the source turns unready before the evict pass.
delete(fake.hashes, "B")
delete(fake.docs, "B")
fake.genCalls = 0
fake.genOverride = func(call int) (VectorGenerationInfo, bool, error) {
if call == 1 {
return fake.gen, true, nil
}
return VectorGenerationInfo{}, false, fmt.Errorf(
"%w: rebuild started", ErrVectorSourceNotReady)
}
res, err := sync.pushVectors(ctx, false, nil, nil)
require.NoError(t, err, "push with mid-push rebuild")
assert.Equal(t, 0, res.SessionsEvicted, "eviction must be dropped")
assert.Equal(t, 1, countRows(t, pg,
`SELECT COUNT(*) FROM vector_documents WHERE session_id = $1`, "B"),
"B's docs survive the aborted eviction")
// The next healthy push evicts B.
fake.genOverride = nil
res, err = sync.pushVectors(ctx, false, nil, nil)
require.NoError(t, err, "healthy push")
assert.Equal(t, 1, res.SessionsEvicted)
assert.Equal(t, 0, countRows(t, pg,
`SELECT COUNT(*) FROM vector_documents WHERE session_id = $1`, "B"))
}
// TestVectorPushProjectScope pins the project-scope gate: a filtered push
// (projects=[alpha]) must leave vector state for an out-of-scope beta session
// already in PG untouched, while still pushing the in-scope alpha session's
// changes. The vectors.db source carries every session regardless of scope, so
// without the gate the beta doc would be re-pushed under a filtered push.
func TestVectorPushProjectScope(t *testing.T) {
pgURL := testPGURL(t)
sync, localDB, pg := newVectorPushTestSync(
t, pgURL, "agentsview_vector_push_scope_test")
ctx := context.Background()
seedVectorSessionProject(t, localDB, "alpha", "alpha")
seedVectorSessionProject(t, localDB, "beta", "beta")
fake := &fakeVectorSource{
gen: VectorGenerationInfo{Fingerprint: "fp-scope", Model: "m", Dimension: 4},
hasGen: true,
hashes: map[string]string{"alpha": "ha", "beta": "hb"},
docs: map[string][]VectorPushDoc{
"alpha": {vdoc("alpha", "alpha#0", 0, "a-orig", "ha1", []float32{1, 0, 0, 0})},
"beta": {vdoc("beta", "beta#0", 0, "b-orig", "hb1", []float32{0, 0, 0, 1})},
},
}
sync.vectorSource = fake
// Unfiltered push lands both alpha and beta vectors in PG.
_, err := sync.Push(ctx, false, nil)
require.NoError(t, err, "unfiltered push")
// Restrict the push to project alpha and change both sessions locally.
sync.projects = []string{"alpha"}
fake.hashes["alpha"] = "ha2"
fake.docs["alpha"] = []VectorPushDoc{
vdoc("alpha", "alpha#0", 0, "a-updated", "ha2", []float32{0, 1, 0, 0}),
}
fake.hashes["beta"] = "hb2"
fake.docs["beta"] = []VectorPushDoc{
vdoc("beta", "beta#0", 0, "b-updated", "hb2", []float32{0, 1, 0, 0}),
}
res, err := sync.pushVectors(ctx, false, nil, nil)
require.NoError(t, err, "filtered vector push")
assert.Equal(t, 1, res.SessionsPushed, "only in-scope alpha pushed")
assert.Equal(t, 0, res.Conflicts, "beta is out of scope, not a conflict")
assert.Equal(t, 0, res.SessionsEvicted, "beta must not be evicted")
var alphaContent, betaContent string
require.NoError(t, pg.QueryRow(
`SELECT content FROM vector_documents WHERE doc_key = $1`, "alpha#0",
).Scan(&alphaContent))
require.NoError(t, pg.QueryRow(
`SELECT content FROM vector_documents WHERE doc_key = $1`, "beta#0",
).Scan(&betaContent))
assert.Equal(t, "a-updated", alphaContent, "in-scope alpha updated")
assert.Equal(t, "b-orig", betaContent, "out-of-scope beta untouched")
genID, _, ok, err := LookupVectorGeneration(ctx, pg, "fp-scope")
require.NoError(t, err)
require.True(t, ok)
var betaHash string
require.NoError(t, pg.QueryRow(
`SELECT doc_agg_hash FROM vector_push_state
WHERE generation_id = $1 AND session_id = $2`, genID, "beta",
).Scan(&betaHash))
assert.Equal(t, "hb", betaHash, "beta push state left at its original hash")
}
// TestVectorPushLocalProjectMoveOutOfScope pins that filtered pushes scope
// local candidates by their LIVE local project, not the (possibly stale) PG
// project. A session pushed in scope (project alpha) then moved out of scope
// locally (alpha->beta) keeps its old PG project because the filtered session
// push skips it; scoping by PG would let the next `pg push --projects alpha`
// re-push or evict its vectors. Scoping by the local project must leave them
// untouched.
func TestVectorPushLocalProjectMoveOutOfScope(t *testing.T) {
pgURL := testPGURL(t)
sync, localDB, pg := newVectorPushTestSync(
t, pgURL, "agentsview_vector_push_localmove_test")
ctx := context.Background()
seedVectorSessionProject(t, localDB, "mover", "alpha")
fake := &fakeVectorSource{
gen: VectorGenerationInfo{Fingerprint: "fp-move", Model: "m", Dimension: 4},
hasGen: true,
hashes: map[string]string{"mover": "h1"},
docs: map[string][]VectorPushDoc{
"mover": {vdoc("mover", "mover#0", 0, "orig", "hc1", []float32{1, 0, 0, 0})},
},
}
sync.vectorSource = fake
// Unfiltered push lands the alpha vector in PG (PG project = alpha).
_, err := sync.Push(ctx, false, nil)
require.NoError(t, err, "unfiltered push")
// The session moves to beta locally, but its PG sessions.project stays alpha
// (a filtered session push skips it). The local doc content also changes.
require.NoError(t, localDB.UpsertSession(db.Session{
ID: "mover",
Project: "beta",
Machine: "test-machine",
Agent: "claude",
MessageCount: 1,
UserMessageCount: 1,
CreatedAt: "2026-01-01T00:00:00Z",
}), "move mover to beta")
fake.hashes["mover"] = "h2"
fake.docs["mover"] = []VectorPushDoc{
vdoc("mover", "mover#0", 0, "updated", "hc2", []float32{0, 1, 0, 0}),
}
var pgProject string
require.NoError(t, pg.QueryRow(
`SELECT project FROM sessions WHERE id = $1`, "mover",
).Scan(&pgProject))
require.Equal(t, "alpha", pgProject, "PG project is still stale alpha")
// A push filtered to alpha must treat mover (now local beta) as out of scope.
sync.projects = []string{"alpha"}
res, err := sync.pushVectors(ctx, false, nil, nil)
require.NoError(t, err, "filtered vector push after local move")
assert.Equal(t, 0, res.SessionsPushed, "moved-out session not pushed")
assert.Equal(t, 0, res.SessionsEvicted, "moved-out session not evicted")
assert.Equal(t, 0, res.Conflicts, "moved-out session is not a conflict")
var content string
require.NoError(t, pg.QueryRow(
`SELECT content FROM vector_documents WHERE doc_key = $1`, "mover#0",
).Scan(&content))
assert.Equal(t, "orig", content, "out-of-scope vector left untouched")
genID, _, ok, err := LookupVectorGeneration(ctx, pg, "fp-move")
require.NoError(t, err)
require.True(t, ok)
var stateHash string
require.NoError(t, pg.QueryRow(
`SELECT doc_agg_hash FROM vector_push_state
WHERE generation_id = $1 AND session_id = $2`, genID, "mover",
).Scan(&stateHash))
assert.Equal(t, "h1", stateHash, "push state left at its original hash")
}
// TestVectorPushSkipsDocExportForMissingSession pins that a local-only session
// with no PG sessions row is skipped without exporting its docs. Doc export
// (content + chunk decode) must run only after the in-tx existence probe
// passes, so a session that always skips never re-exports docs each push.
func TestVectorPushSkipsDocExportForMissingSession(t *testing.T) {
pgURL := testPGURL(t)
sync, localDB, _ := newVectorPushTestSync(
t, pgURL, "agentsview_vector_push_skipexport_test")
ctx := context.Background()
// "A" is a real session (pushed to PG). "ghost" is local-only: it never gets
// a PG sessions row, so the vector push must skip it.
seedVectorSession(t, localDB, "A")
fake := &fakeVectorSource{
gen: VectorGenerationInfo{Fingerprint: "fp-skip", Model: "m", Dimension: 4},
hasGen: true,
hashes: map[string]string{"A": "ha", "ghost": "hg"},
docs: map[string][]VectorPushDoc{
"A": {vdoc("A", "A#0", 0, "ca", "hca", []float32{1, 0, 0, 0})},
"ghost": {vdoc("ghost", "g#0", 0, "cg", "hcg", []float32{0, 1, 0, 0})},
},
}
sync.vectorSource = fake
// Push seeds A's PG sessions row (session phase) then runs the vector phase.
// ghost is absent from localDB, so it never gets a PG sessions row.
res, err := sync.Push(ctx, false, nil)
require.NoError(t, err, "Push")
assert.Equal(t, 1, res.Vectors.SessionsPushed, "only A lands")
assert.Equal(t, 0, res.Vectors.Conflicts, "missing session is not a conflict")
assert.Equal(t, 1, fake.docsCalls["A"], "A's docs exported once")
assert.Zero(t, fake.docsCalls["ghost"],
"missing session's docs never exported")
}
// TestVectorPushOrphanStateEvicted pins finding-3 rule d: a vector_push_state
// row whose sessions row has vanished is evicted regardless of owner marker.
func TestVectorPushOrphanStateEvicted(t *testing.T) {
pgURL := testPGURL(t)
sync, localDB, pg := newVectorPushTestSync(
t, pgURL, "agentsview_vector_push_orphan_test")
ctx := context.Background()
seedVectorSession(t, localDB, "A")
seedVectorSession(t, localDB, "B")
fake := &fakeVectorSource{
gen: VectorGenerationInfo{Fingerprint: "fp-or", Model: "m", Dimension: 4},
hasGen: true,
hashes: map[string]string{"A": "ha", "B": "hb"},
docs: map[string][]VectorPushDoc{
"A": {vdoc("A", "oA0", 0, "ca", "hca", []float32{1, 0, 0, 0})},
"B": {vdoc("B", "oB0", 0, "cb", "hcb", []float32{0, 0, 0, 1})},
},
}
sync.vectorSource = fake
_, err := sync.Push(ctx, false, nil)
require.NoError(t, err, "first Push")
genID, _, ok, err := LookupVectorGeneration(ctx, pg, "fp-or")
require.NoError(t, err)
require.True(t, ok)
// Session A's row disappears (e.g. deleted) but its vector state lingers,
// and A leaves local so it is an eviction candidate.
_, err = pg.Exec(`DELETE FROM sessions WHERE id = $1`, "A")
require.NoError(t, err, "delete session row")
delete(fake.hashes, "A")
delete(fake.docs, "A")
res, err := sync.pushVectors(ctx, false, nil, nil)
require.NoError(t, err, "evict orphan")
assert.Equal(t, 1, res.SessionsEvicted)
assert.Equal(t, 0, res.Conflicts, "orphaned state is not a conflict")
assert.Equal(t, 0, countRows(t, pg,
`SELECT COUNT(*) FROM vector_push_state
WHERE generation_id = $1 AND session_id = $2`, genID, "A"))
assert.Equal(t, 0, countRows(t, pg,
`SELECT COUNT(*) FROM `+vectorChunkTable(genID)+` WHERE doc_key = $1`, "oA0"))
assert.Equal(t, 0, countRows(t, pg,
`SELECT COUNT(*) FROM vector_documents WHERE session_id = $1`, "A"))
}
// TestVectorPushDefersSessionWhenExportDiverges pins the replacement-path
// guard against concurrent local rebuilds: when a session's export hash no
// longer matches the hash the delta scan read (an embeddings build rewrote
// the local index between the two reads), the session is deferred with
// nothing written — the previously pushed PG chunks and push state survive
// untouched. Without the guard, the push would replace valid PG vectors with
// the partial view and record the stale scan hash as current, which a
// same-fingerprint rebuild (same content, same hash) would never repair.
func TestVectorPushDefersSessionWhenExportDiverges(t *testing.T) {
pgURL := testPGURL(t)
sync, localDB, pg := newVectorPushTestSync(
t, pgURL, "agentsview_vector_push_diverge_test")
ctx := context.Background()
seedVectorSession(t, localDB, "A")
source := &fakeVectorSource{
gen: VectorGenerationInfo{Fingerprint: "fp-div", Model: "m", Dimension: 4},
hasGen: true,
hashes: map[string]string{"A": "ha"},
docs: map[string][]VectorPushDoc{
"A": {vdoc("A", "A#0", 0, "c1", "h1",
[]float32{1, 0, 0, 0}, []float32{0, 1, 0, 0})},
},
}
sync.vectorSource = source
_, err := sync.Push(ctx, false, nil)
require.NoError(t, err, "seed Push")
genID, _, ok, err := LookupVectorGeneration(ctx, pg, "fp-div")
require.NoError(t, err)
require.True(t, ok)
require.Equal(t, 2,
countRows(t, pg, `SELECT COUNT(*) FROM `+vectorChunkTable(genID)))
// The delta scan sees a changed session, but by export time the local
// index has moved again: SessionDocs returns a shrunken doc set whose
// hash differs from the scan's.
source.hashes = map[string]string{"A": "ha-changed"}
source.exportHashes = map[string]string{"A": "ha-mid-rebuild"}
source.docs = map[string][]VectorPushDoc{
"A": {vdoc("A", "A#0", 0, "c1", "h1", []float32{9, 9, 9, 9})},
}
res, err := sync.pushVectors(ctx, false, nil, nil)
require.NoError(t, err, "diverged pushVectors")
assert.Equal(t, 1, res.SessionsDeferred, "diverged session must defer")
assert.Equal(t, 0, res.SessionsPushed)
assert.Equal(t, 2,
countRows(t, pg, `SELECT COUNT(*) FROM `+vectorChunkTable(genID)),
"previously pushed chunks must survive the deferral")
assert.Equal(t, 1, countRows(t, pg, `
SELECT COUNT(*) FROM vector_push_state
WHERE generation_id = $1 AND session_id = 'A' AND doc_agg_hash = 'ha'`,
genID), "push state must keep the last successfully exported hash")
// Once the local index settles (export hash matches the scan again), the
// deferred session is re-derived and pushed.
source.exportHashes = nil
res, err = sync.pushVectors(ctx, false, nil, nil)
require.NoError(t, err, "settled pushVectors")
assert.Equal(t, 1, res.SessionsPushed)
assert.Equal(t, 1,
countRows(t, pg, `SELECT COUNT(*) FROM `+vectorChunkTable(genID)))
}
// TestVectorEvictionRechecksOwnershipInTx pins the eviction-path TOCTOU
// guard: ownership read by the delta scan can be minutes stale, so each
// eviction transaction re-probes the sessions row FOR UPDATE. A session
// another pusher claimed after the scan is skipped as a conflict — its new
// owner's chunks and push state survive — while a session whose row vanished
// is still evicted.
func TestVectorEvictionRechecksOwnershipInTx(t *testing.T) {
pgURL := testPGURL(t)
sync, localDB, pg := newVectorPushTestSync(
t, pgURL, "agentsview_vector_evict_recheck_test")
ctx := context.Background()
seedVectorSession(t, localDB, "A")
seedVectorSession(t, localDB, "B")
sync.vectorSource = &fakeVectorSource{
gen: VectorGenerationInfo{Fingerprint: "fp-ev", Model: "m", Dimension: 4},
hasGen: true,
hashes: map[string]string{"A": "ha", "B": "hb"},
docs: map[string][]VectorPushDoc{
"A": {vdoc("A", "A#0", 0, "c1", "h1", []float32{1, 0, 0, 0})},
"B": {vdoc("B", "B#0", 0, "c2", "h2", []float32{0, 1, 0, 0})},
},
}
_, err := sync.Push(ctx, false, nil)
require.NoError(t, err, "seed Push")
genID, _, ok, err := LookupVectorGeneration(ctx, pg, "fp-ev")
require.NoError(t, err)
require.True(t, ok)
// Simulate a concurrent pusher claiming A after this push's delta scan
// (which would have seen it as ours), and B's session row vanishing.
_, err = pg.ExecContext(ctx, `
UPDATE sessions SET owner_marker = 'other-marker', machine = 'other-machine'
WHERE id = 'A'`)
require.NoError(t, err, "reassign A")
_, err = pg.ExecContext(ctx, `DELETE FROM sessions WHERE id = 'B'`)
require.NoError(t, err, "delete B")
owner, err := sync.vectorOwnerIdentity(ctx)
require.NoError(t, err)
allGenIDs, err := sync.allVectorGenerationIDs(ctx)
require.NoError(t, err)
genIDs, err := sync.existingChunkGenerations(ctx, allGenIDs)
require.NoError(t, err)
scope := vectorPushScope{
gen: vectorGeneration{id: genID},
fingerprint: "fp-ev",
genIDs: genIDs,
owner: owner,
}
var res VectorPushResult
require.NoError(t,
sync.evictVectorSessions(ctx, scope, []string{"A", "B"}, &res))
assert.Equal(t, 1, res.Conflicts, "reclaimed session is a conflict")
assert.Equal(t, 1, res.SessionsEvicted, "vanished session is evicted")
assert.Equal(t, 1, countRows(t, pg, `
SELECT COUNT(*) FROM vector_push_state
WHERE generation_id = $1 AND session_id = 'A'`, genID),
"the new owner's push state survives")
assert.Equal(t, 1, countRows(t, pg, `
SELECT COUNT(*) FROM `+vectorChunkTable(genID)+` c
WHERE c.doc_key IN (
SELECT doc_key FROM vector_documents WHERE session_id = 'A')`),
"the new owner's chunks survive")
assert.Equal(t, 0, countRows(t, pg, `
SELECT COUNT(*) FROM vector_push_state
WHERE generation_id = $1 AND session_id = 'B'`, genID))
}
// TestVectorPushEvictionDefersFailedSession pins that failedSessions gates
// eviction, not just replacement: a failed session with zero embedded docs is
// absent from the local hash map and would otherwise be evicted — vector
// state running ahead of the sessions/messages rows its session-phase push
// failed to write. It must defer instead, and evict on the next clean push.
func TestVectorPushEvictionDefersFailedSession(t *testing.T) {
pgURL := testPGURL(t)
sync, localDB, pg := newVectorPushTestSync(
t, pgURL, "agentsview_vector_evict_failed_test")
ctx := context.Background()
seedVectorSession(t, localDB, "A")
fake := &fakeVectorSource{
gen: VectorGenerationInfo{Fingerprint: "fp-evf", Model: "m", Dimension: 4},
hasGen: true,
hashes: map[string]string{"A": "ha"},
docs: map[string][]VectorPushDoc{
"A": {vdoc("A", "A#0", 0, "c1", "h1", []float32{1, 0, 0, 0})},
},
}
sync.vectorSource = fake
_, err := sync.Push(ctx, false, nil)
require.NoError(t, err, "seed Push")
genID, _, ok, err := LookupVectorGeneration(ctx, pg, "fp-evf")
require.NoError(t, err)
require.True(t, ok)
// A's embedded docs all vanish locally while its session push fails.
fake.hashes = map[string]string{}
fake.docs = map[string][]VectorPushDoc{}
res, err := sync.pushVectors(ctx, false, map[string]struct{}{"A": {}}, nil)
require.NoError(t, err, "failed-session pushVectors")
assert.Equal(t, 0, res.SessionsEvicted, "failed session must not be evicted")
assert.Equal(t, 1, res.SessionsDeferred)
assert.Equal(t, 1,
countRows(t, pg, `SELECT COUNT(*) FROM `+vectorChunkTable(genID)),
"failed session keeps its vectors")
assert.Equal(t, 1, countRows(t, pg, `
SELECT COUNT(*) FROM vector_push_state
WHERE generation_id = $1 AND session_id = 'A'`, genID))
res, err = sync.pushVectors(ctx, false, nil, nil)
require.NoError(t, err, "clean pushVectors")
assert.Equal(t, 1, res.SessionsEvicted, "next clean push evicts")
assert.Equal(t, 0,
countRows(t, pg, `SELECT COUNT(*) FROM `+vectorChunkTable(genID)))
}
// TestVectorPushFilteredEvictionScopesByLocalProject pins the eviction-scope
// partition for filtered pushes: an eviction candidate absent from the vector
// hash map can still have a live local sessions row (zero embedded docs), and
// that row's LIVE project — not the stale PG project — decides scope. Only
// candidates genuinely absent from the local sessions table fall back to PG
// project scoping.
func TestVectorPushFilteredEvictionScopesByLocalProject(t *testing.T) {
pgURL := testPGURL(t)
sync, localDB, pg := newVectorPushTestSync(
t, pgURL, "agentsview_vector_evict_scope_test")
ctx := context.Background()
seedVectorSessionProject(t, localDB, "moved", "alpha")
seedVectorSessionProject(t, localDB, "gone-out", "beta")
seedVectorSessionProject(t, localDB, "gone-in", "alpha")
fake := &fakeVectorSource{
gen: VectorGenerationInfo{Fingerprint: "fp-evs", Model: "m", Dimension: 4},
hasGen: true,
hashes: map[string]string{"moved": "hm", "gone-out": "ho", "gone-in": "hi"},
docs: map[string][]VectorPushDoc{
"moved": {vdoc("moved", "m#0", 0, "cm", "hm1", []float32{1, 0, 0, 0})},
"gone-out": {vdoc("gone-out", "o#0", 0, "co", "ho1", []float32{0, 1, 0, 0})},
"gone-in": {vdoc("gone-in", "i#0", 0, "ci", "hi1", []float32{0, 0, 1, 0})},
},
}
sync.vectorSource = fake
// Unfiltered push lands all three sessions' vectors and PG rows.
_, err := sync.Push(ctx, false, nil)
require.NoError(t, err, "unfiltered push")
genID, _, ok, err := LookupVectorGeneration(ctx, pg, "fp-evs")
require.NoError(t, err)
require.True(t, ok)
// "moved" changes project locally (alpha -> beta) — its PG project stays
// alpha because a filtered session push skips it. The others vanish from
// the local archive entirely. All three drop out of the embedded set.
require.NoError(t, localDB.UpsertSession(db.Session{
ID: "moved", Project: "beta", Machine: "test-machine",
Agent: "claude", MessageCount: 1, UserMessageCount: 1,
CreatedAt: "2026-01-01T00:00:00Z",
}), "move session to beta")
require.NoError(t, localDB.DeleteSession("gone-out"))
require.NoError(t, localDB.DeleteSession("gone-in"))
fake.hashes = map[string]string{}
fake.docs = map[string][]VectorPushDoc{}
sync.projects = []string{"alpha"}
res, err := sync.pushVectors(ctx, false, nil, nil)
require.NoError(t, err, "filtered pushVectors")
assert.Equal(t, 1, res.SessionsEvicted,
"only the locally-absent alpha session is evicted")
chunkCount := func(sessionID string) int {
return countRows(t, pg, `
SELECT COUNT(*) FROM `+vectorChunkTable(genID)+` c
WHERE c.doc_key IN (
SELECT doc_key FROM vector_documents WHERE session_id = $1)`,
sessionID)
}
assert.Equal(t, 1, chunkCount("moved"),
"session that moved out of the filter locally is untouched")
assert.Equal(t, 1, chunkCount("gone-out"),
"locally-absent session with out-of-filter PG project is untouched")
assert.Equal(t, 0, chunkCount("gone-in"),
"locally-absent session with in-filter PG project is evicted")
}
// TestVectorPushSkipsOnInsufficientPrivilege pins the restricted-role degrade
// path: a schema provisioned by a privileged role (core tables exist) whose
// push role cannot CREATE means the vector base DDL fails with SQLSTATE
// 42501. That must skip the vector phase with a reason — matching a database
// without pgvector — not fail a push whose session phase succeeded.
func TestVectorPushSkipsOnInsufficientPrivilege(t *testing.T) {
pgURL := testPGURL(t)
const schema = "agentsview_vector_privilege_test"
const role = "agentsview_vector_restricted"
const rolePassword = "agentsview_vector_restricted_pw"
admin, err := Open(pgURL, schema, true)
require.NoError(t, err, "Open admin")
t.Cleanup(func() { _ = admin.Close() })
ctx := context.Background()
_, err = admin.Exec(`DROP SCHEMA IF EXISTS ` + schema + ` CASCADE`)
require.NoError(t, err, "drop schema")
require.NoError(t, EnsureSchema(ctx, admin, schema))
unavailable, err := ensureVectorBaseSchemaPG(ctx, admin)
require.NoError(t, err)
if unavailable != "" {
t.Skip(unavailable)
}
// Simulate a schema provisioned by an older, pre-vector agentsview:
// core tables exist but the vector tables do not, so the restricted
// role's own setup must CREATE them — and cannot.
for _, tbl := range []string{
"vector_push_state", "vector_documents",
"vector_generation_machines", "vector_generations",
} {
_, err = admin.Exec(`DROP TABLE IF EXISTS ` + tbl + ` CASCADE`)
require.NoError(t, err, tbl)
}
_, _ = admin.Exec(`DROP OWNED BY ` + role) // clear grants from prior runs
_, _ = admin.Exec(`DROP ROLE IF EXISTS ` + role)
_, err = admin.Exec(
`CREATE ROLE ` + role + ` LOGIN PASSWORD '` + rolePassword + `'`)
require.NoError(t, err, "create restricted role")
t.Cleanup(func() {
_, _ = admin.Exec(`DROP SCHEMA IF EXISTS ` + schema + ` CASCADE`)
_, _ = admin.Exec(`DROP OWNED BY ` + role)
_, _ = admin.Exec(`DROP ROLE IF EXISTS ` + role)
})
for _, grant := range []string{
`GRANT USAGE ON SCHEMA ` + schema + ` TO ` + role,
`GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA ` +
schema + ` TO ` + role,
} {
_, err = admin.Exec(grant)
require.NoError(t, err, grant)
}
restrictedURL, err := url.Parse(pgURL)
require.NoError(t, err)
restrictedURL.User = url.UserPassword(role, rolePassword)
restricted, err := Open(restrictedURL.String(), schema, true)
require.NoError(t, err, "Open restricted")
t.Cleanup(func() { _ = restricted.Close() })
localDB, err := db.Open(filepath.Join(t.TempDir(), "local.db"))
require.NoError(t, err, "db.Open")
t.Cleanup(func() { _ = localDB.Close() })
sync := &Sync{
pg: restricted,
local: localDB,
machine: "test-machine",
schema: schema,
schemaDone: true,
}
sync.vectorSource = &fakeVectorSource{
gen: VectorGenerationInfo{
Fingerprint: "fp-priv", Model: "m", Dimension: 4,
},
hasGen: true,
hashes: map[string]string{},
}
res, err := sync.pushVectors(ctx, false, nil, nil)
require.NoError(t, err, "privilege failure must skip, not fail the push")
assert.True(t, res.Skipped)
assert.Contains(t, res.SkippedReason, "privileges")
}