Files
wehub-resource-sync f99010fae1
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
Desktop Artifacts / Desktop Build (Linux (arm64)) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Windows) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (aarch64)) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (x86_64)) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:30:36 +08:00

282 lines
9.1 KiB
Go

package main
import (
"context"
"errors"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
kitvec "go.kenn.io/kit/vector"
"go.kenn.io/agentsview/internal/config"
"go.kenn.io/agentsview/internal/db"
"go.kenn.io/agentsview/internal/postgres"
"go.kenn.io/agentsview/internal/vector"
)
// enabledVectorConfig returns a minimal Config with [vector] enabled and a
// fresh DataDir, so the push source resolves a vectors.db path under a temp
// dir that no other test touches.
func enabledVectorConfig(t *testing.T) config.Config {
t.Helper()
return config.Config{
DataDir: t.TempDir(),
Vector: config.VectorConfig{
Enabled: true,
Embeddings: config.VectorEmbeddingsConfig{
Model: "fake-model",
Dimension: 4,
MaxInputChars: 8192,
},
},
}
}
// fakePushUnitSource is a vector.UnitSource that replays a fixed slice of
// units, enough to build a tiny active generation for the round-trip test.
type fakePushUnitSource struct {
units []db.EmbeddableUnit
}
func (f fakePushUnitSource) ScanEmbeddableUnits(
_ context.Context, _ string, _ bool, fn func(db.EmbeddableUnit) error,
) (string, error) {
for _, u := range f.units {
if err := fn(u); err != nil {
return "", err
}
}
return "2024-01-01T00:00:03Z", nil
}
// fakePushEncoder returns a deterministic 4-dimensional encoder matching the
// gen dimension the round-trip test builds with.
func fakePushEncoder() kitvec.EncodeFunc {
return func(_ context.Context, texts []string) ([][]float32, error) {
out := make([][]float32, len(texts))
for i := range texts {
out[i] = []float32{1, 0, 0, 0}
}
return out, nil
}
}
// testPushUnitSource returns the fixed unit set the push-source tests build
// their vectors.db from: two sessions, three documents.
func testPushUnitSource() fakePushUnitSource {
return fakePushUnitSource{units: []db.EmbeddableUnit{
{SessionID: "session-1", Kind: "user", SourceUUID: "u1", Content: "hello"},
{
SessionID: "session-1", Kind: "run", SourceUUID: "a1",
Ordinal: 1, OrdinalEnd: 2, Content: "first\n\nsecond",
Offsets: []db.UnitOffset{{Ordinal: 1}, {Ordinal: 2, RuneStart: 7, ByteStart: 7}},
},
{SessionID: "session-2", Kind: "user", SourceUUID: "u2", Content: "world"},
}}
}
// buildTestVectorsDB opens a read-write index at cfg's resolved vectors.db
// path, builds and activates one generation over two sessions, then closes
// it so the push source can reopen the file read-only.
func buildTestVectorsDB(t *testing.T, cfg config.Config) {
t.Helper()
ctx := context.Background()
ix, err := vector.Open(
ctx, cfg.Vector.ResolvedDBPath(cfg.DataDir), false,
cfg.Vector.Embeddings.MaxInputChars,
)
require.NoError(t, err)
defer ix.Close()
gen := kitvec.Generation{Model: "fake-model", Dimensions: 4}
result, err := ix.Build(
ctx, testPushUnitSource(), fakePushEncoder(), gen, vector.BuildOptions{},
)
require.NoError(t, err)
require.True(t, result.Activated)
}
// closePushSource registers a cleanup that closes the adapter's vectors.db
// handle. Required on Windows, where TempDir removal fails while the sqlite
// file is still open.
func closePushSource(t *testing.T, src postgres.VectorPushSource) {
t.Helper()
t.Cleanup(func() {
require.NoError(t, src.(*vectorPushSource).Close())
})
}
func TestNewVectorPushSourceDisabled(t *testing.T) {
cfg := config.Config{} // [vector] disabled
assert.Nil(t, newVectorPushSource(cfg))
}
func TestVectorPushSourceMissingFile(t *testing.T) {
cfg := enabledVectorConfig(t)
src := newVectorPushSource(cfg)
require.NotNil(t, src)
closePushSource(t, src)
_, ok, err := src.Generation(context.Background())
require.NoError(t, err)
assert.False(t, ok) // no vectors.db yet -> nothing to push, not an error
}
// TestVectorPushSourceMissingFileThenBuilt pins that a missing vectors.db is
// not memoized: the SAME adapter reports "nothing to push" before any build and
// then picks up a generation built at the same path afterward, as a daemon push
// that starts before embeddings exist must.
func TestVectorPushSourceMissingFileThenBuilt(t *testing.T) {
ctx := context.Background()
cfg := enabledVectorConfig(t)
src := newVectorPushSource(cfg)
require.NotNil(t, src)
closePushSource(t, src)
_, ok, err := src.Generation(ctx)
require.NoError(t, err)
require.False(t, ok, "no vectors.db yet -> nothing to push")
buildTestVectorsDB(t, cfg)
gen, ok, err := src.Generation(ctx)
require.NoError(t, err)
require.True(t, ok, "same adapter must pick up a later build")
assert.Equal(t, "fake-model", gen.Model)
hashes, err := src.SessionDocHashes(ctx)
require.NoError(t, err)
assert.Len(t, hashes, 2)
}
// TestVectorPushSourceNotReadyDuringRebuild pins the partial-coverage gate: a
// full rebuild clears the active generation's stamps in place before
// re-embedding, so an aborted (or still running) rebuild leaves the active
// generation with missing embeddings. Exporting that view would evict valid
// PG vectors, so Generation must refuse with ErrVectorSourceNotReady until a
// build completes.
func TestVectorPushSourceNotReadyDuringRebuild(t *testing.T) {
ctx := context.Background()
cfg := enabledVectorConfig(t)
buildTestVectorsDB(t, cfg)
// Start a full rebuild whose encoder fails: resetGeneration has already
// cleared the active generation's stamps, so coverage is now partial —
// exactly the state a concurrent push would observe mid-rebuild.
ix, err := vector.Open(
ctx, cfg.Vector.ResolvedDBPath(cfg.DataDir), false,
cfg.Vector.Embeddings.MaxInputChars,
)
require.NoError(t, err)
failingEncoder := func(_ context.Context, _ []string) ([][]float32, error) {
return nil, errors.New("embeddings endpoint down")
}
_, err = ix.Build(ctx, testPushUnitSource(), failingEncoder,
kitvec.Generation{Model: "fake-model", Dimensions: 4},
vector.BuildOptions{FullRebuild: true},
)
require.Error(t, err, "rebuild must abort on encoder failure")
require.NoError(t, ix.Close())
src := newVectorPushSource(cfg)
require.NotNil(t, src)
closePushSource(t, src)
_, ok, err := src.Generation(ctx)
require.Error(t, err)
assert.ErrorIs(t, err, postgres.ErrVectorSourceNotReady)
assert.False(t, ok)
}
func TestVectorPushSourceRoundTrip(t *testing.T) {
ctx := context.Background()
cfg := enabledVectorConfig(t)
buildTestVectorsDB(t, cfg)
src := newVectorPushSource(cfg)
require.NotNil(t, src)
closePushSource(t, src)
gen, ok, err := src.Generation(ctx)
require.NoError(t, err)
require.True(t, ok)
assert.NotEmpty(t, gen.Fingerprint)
assert.Equal(t, "fake-model", gen.Model)
assert.Equal(t, 4, gen.Dimension)
hashes, err := src.SessionDocHashes(ctx)
require.NoError(t, err)
require.Len(t, hashes, 2)
assert.Contains(t, hashes, "session-1")
assert.Contains(t, hashes, "session-2")
// Cross-check the adapter's mapping against the raw export API: every
// VectorPushDoc field must mirror its ExportDoc source exactly.
ix, err := vector.Open(
ctx, cfg.Vector.ResolvedDBPath(cfg.DataDir), true,
cfg.Vector.Embeddings.MaxInputChars,
)
require.NoError(t, err)
defer ix.Close()
exp, ok, err := ix.ActiveExport(ctx)
require.NoError(t, err)
require.True(t, ok)
want, wantHash, err := ix.ExportSessionDocs(ctx, exp.Ordinal, "session-1")
require.NoError(t, err)
require.NotEmpty(t, want)
assert.Equal(t, hashes["session-1"], wantHash,
"export hash must match the delta-scan aggregate for an unchanged index")
docs, gotHash, err := src.SessionDocs(ctx, "session-1")
require.NoError(t, err)
assert.Equal(t, wantHash, gotHash)
require.Len(t, docs, len(want))
for i, got := range docs {
w := want[i]
assert.Equal(t, w.DocKey, got.DocKey)
assert.Equal(t, w.SessionID, got.SessionID)
assert.Equal(t, w.SourceUUID, got.SourceUUID)
assert.Equal(t, w.Ordinal, got.Ordinal)
assert.Equal(t, w.OrdinalEnd, got.OrdinalEnd)
assert.Equal(t, w.Subordinate, got.Subordinate)
assert.Equal(t, w.OffsetsJSON, got.OffsetsJSON)
assert.Equal(t, w.Content, got.Content)
assert.Equal(t, w.ContentHash, got.ContentHash)
require.Len(t, got.Chunks, len(w.Chunks))
for j, gc := range got.Chunks {
assert.Equal(t, w.Chunks[j].ChunkIndex, gc.ChunkIndex)
assert.Equal(t, w.Chunks[j].Embedding, gc.Embedding)
}
}
}
// TestCloseVectorPushSource pins the creator-owned handle lifecycle: closing
// releases the memoized read-only vectors.db handle (so per-push and
// per-watch-loop sources do not leak SQLite handles), a nil source is a
// no-op, and a closed adapter transparently reopens on the next call.
func TestCloseVectorPushSource(t *testing.T) {
closeVectorPushSource(nil) // must not panic
cfg := enabledVectorConfig(t)
buildTestVectorsDB(t, cfg)
src := newVectorPushSource(cfg)
require.NotNil(t, src)
ctx := context.Background()
_, ok, err := src.Generation(ctx)
require.NoError(t, err)
require.True(t, ok)
adapter := src.(*vectorPushSource)
require.NotNil(t, adapter.ix, "Generation memoizes the open handle")
closeVectorPushSource(src)
assert.Nil(t, adapter.ix, "close releases the memoized handle")
closeVectorPushSource(src) // idempotent
_, ok, err = src.Generation(ctx)
require.NoError(t, err)
assert.True(t, ok, "a closed adapter reopens lazily")
closePushSource(t, src)
}