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
270 lines
9.4 KiB
Go
270 lines
9.4 KiB
Go
//go:build pgtest
|
|
|
|
package postgres
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
const vectorSchemaTestSchema = "agentsview_vector_schema_test"
|
|
|
|
func cleanVectorSchemaTestPG(t *testing.T, pgURL string) {
|
|
t.Helper()
|
|
pg, err := sql.Open("pgx", pgURL)
|
|
require.NoError(t, err, "connecting to pg")
|
|
defer pg.Close()
|
|
_, _ = pg.Exec(
|
|
"DROP SCHEMA IF EXISTS " + vectorSchemaTestSchema + " CASCADE",
|
|
)
|
|
}
|
|
|
|
// TestEnsureVectorBaseSchema verifies that ensureVectorBaseSchemaPG installs
|
|
// pgvector and the base vector tables idempotently, and that generation
|
|
// registration and chunk table creation round-trip through
|
|
// ensureVectorGeneration, ensureVectorChunkTable, and
|
|
// LookupVectorGeneration.
|
|
func TestEnsureVectorBaseSchema(t *testing.T) {
|
|
pgURL := testPGURL(t)
|
|
cleanVectorSchemaTestPG(t, pgURL)
|
|
t.Cleanup(func() { cleanVectorSchemaTestPG(t, pgURL) })
|
|
|
|
pg, err := Open(pgURL, vectorSchemaTestSchema, true)
|
|
require.NoError(t, err, "connecting to pg")
|
|
defer pg.Close()
|
|
|
|
ctx := context.Background()
|
|
|
|
// EnsureSchema creates the schema itself (Open only sets search_path;
|
|
// it does not create the target schema), and also exercises the
|
|
// ensureVectorBaseSchemaPG wiring inside EnsureSchema.
|
|
require.NoError(t, EnsureSchema(ctx, pg, vectorSchemaTestSchema))
|
|
|
|
unavailable, err := ensureVectorBaseSchemaPG(ctx, pg)
|
|
require.NoError(t, err)
|
|
if unavailable != "" {
|
|
t.Skip(unavailable)
|
|
}
|
|
|
|
// Idempotent.
|
|
_, err = ensureVectorBaseSchemaPG(ctx, pg)
|
|
require.NoError(t, err)
|
|
|
|
id, err := ensureVectorGeneration(ctx, pg, "fp-1", "test-model", 4)
|
|
require.NoError(t, err)
|
|
id2, err := ensureVectorGeneration(ctx, pg, "fp-1", "test-model", 4)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, id, id2)
|
|
|
|
require.NoError(t, ensureVectorChunkTable(ctx, pg, id, 4))
|
|
require.NoError(t, ensureVectorChunkTable(ctx, pg, id, 4)) // idempotent
|
|
|
|
gotID, dim, ok, err := LookupVectorGeneration(ctx, pg, "fp-1")
|
|
require.NoError(t, err)
|
|
require.True(t, ok)
|
|
assert.Equal(t, id, gotID)
|
|
assert.Equal(t, 4, dim)
|
|
|
|
_, _, ok, err = LookupVectorGeneration(ctx, pg, "fp-missing")
|
|
require.NoError(t, err)
|
|
assert.False(t, ok)
|
|
}
|
|
|
|
// TestVectorChunkTableExists pins the second half of the read-side startup
|
|
// gate: a generation row registered without its chunk table (a push
|
|
// interrupted between ensureVectorGeneration and ensureVectorChunkTable)
|
|
// reports the table absent, so serve degrades to unavailable instead of
|
|
// wiring a searcher that fails every query with a missing-relation error.
|
|
func TestVectorChunkTableExists(t *testing.T) {
|
|
pgURL := testPGURL(t)
|
|
cleanVectorSchemaTestPG(t, pgURL)
|
|
t.Cleanup(func() { cleanVectorSchemaTestPG(t, pgURL) })
|
|
|
|
pg, err := Open(pgURL, vectorSchemaTestSchema, true)
|
|
require.NoError(t, err, "connecting to pg")
|
|
defer pg.Close()
|
|
|
|
ctx := context.Background()
|
|
require.NoError(t, EnsureSchema(ctx, pg, vectorSchemaTestSchema))
|
|
unavailable, err := ensureVectorBaseSchemaPG(ctx, pg)
|
|
require.NoError(t, err)
|
|
if unavailable != "" {
|
|
t.Skip(unavailable)
|
|
}
|
|
|
|
// Generation registered, chunk table not yet created.
|
|
id, err := ensureVectorGeneration(ctx, pg, "fp-partial", "test-model", 4)
|
|
require.NoError(t, err)
|
|
exists, err := VectorChunkTableExists(ctx, pg, id)
|
|
require.NoError(t, err)
|
|
assert.False(t, exists, "generation row without chunk table")
|
|
|
|
require.NoError(t, ensureVectorChunkTable(ctx, pg, id, 4))
|
|
exists, err = VectorChunkTableExists(ctx, pg, id)
|
|
require.NoError(t, err)
|
|
assert.True(t, exists, "chunk table created")
|
|
}
|
|
|
|
// TestVectorGenerationLookupTolerateMissingTable verifies that the read-side
|
|
// gate degrades cleanly when the vector_generations table never existed
|
|
// (pgvector never installed): LookupVectorGeneration reports not-found and
|
|
// ListVectorGenerationFingerprints returns empty, both without error, so pg
|
|
// serve records a "semantic unavailable" reason instead of aborting startup.
|
|
func TestVectorGenerationLookupTolerateMissingTable(t *testing.T) {
|
|
pgURL := testPGURL(t)
|
|
cleanVectorSchemaTestPG(t, pgURL)
|
|
t.Cleanup(func() { cleanVectorSchemaTestPG(t, pgURL) })
|
|
|
|
pg, err := Open(pgURL, vectorSchemaTestSchema, true)
|
|
require.NoError(t, err, "connecting to pg")
|
|
defer pg.Close()
|
|
|
|
ctx := context.Background()
|
|
// Create the schema but deliberately do not install the vector tables,
|
|
// so vector_generations does not exist (SQLSTATE 42P01 on query).
|
|
_, err = pg.ExecContext(ctx,
|
|
"CREATE SCHEMA IF NOT EXISTS "+vectorSchemaTestSchema)
|
|
require.NoError(t, err)
|
|
require.False(t, tableExistsPG(t, pg, "vector_generations"),
|
|
"vector_generations must be absent for the 42P01 tolerance check")
|
|
|
|
_, _, ok, err := LookupVectorGeneration(ctx, pg, "fp-any")
|
|
require.NoError(t, err, "missing table must be tolerated as not-found")
|
|
assert.False(t, ok)
|
|
|
|
fingerprints, err := ListVectorGenerationFingerprints(ctx, pg)
|
|
require.NoError(t, err, "missing table must yield an empty list, not an error")
|
|
assert.Empty(t, fingerprints)
|
|
}
|
|
|
|
// TestListVectorGenerationFingerprints verifies fingerprints come back in id
|
|
// order (oldest generation first), the order pg serve's startup notice lists
|
|
// the generations PG already has.
|
|
func TestListVectorGenerationFingerprints(t *testing.T) {
|
|
pgURL := testPGURL(t)
|
|
cleanVectorSchemaTestPG(t, pgURL)
|
|
t.Cleanup(func() { cleanVectorSchemaTestPG(t, pgURL) })
|
|
|
|
pg, err := Open(pgURL, vectorSchemaTestSchema, true)
|
|
require.NoError(t, err, "connecting to pg")
|
|
defer pg.Close()
|
|
|
|
ctx := context.Background()
|
|
require.NoError(t, EnsureSchema(ctx, pg, vectorSchemaTestSchema))
|
|
unavailable, err := ensureVectorBaseSchemaPG(ctx, pg)
|
|
require.NoError(t, err)
|
|
if unavailable != "" {
|
|
t.Skip(unavailable)
|
|
}
|
|
|
|
_, err = ensureVectorGeneration(ctx, pg, "fp-alpha", "model-a", 4)
|
|
require.NoError(t, err)
|
|
_, err = ensureVectorGeneration(ctx, pg, "fp-beta", "model-b", 8)
|
|
require.NoError(t, err)
|
|
|
|
fingerprints, err := ListVectorGenerationFingerprints(ctx, pg)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, []string{"fp-alpha", "fp-beta"}, fingerprints)
|
|
}
|
|
|
|
// tableExistsPG reports whether a base table exists in the connection's
|
|
// current schema.
|
|
func tableExistsPG(t *testing.T, pg *sql.DB, table string) bool {
|
|
t.Helper()
|
|
var one int
|
|
err := pg.QueryRow(
|
|
`SELECT 1 FROM information_schema.tables
|
|
WHERE table_schema = current_schema() AND table_name = $1`, table,
|
|
).Scan(&one)
|
|
if err == sql.ErrNoRows {
|
|
return false
|
|
}
|
|
require.NoError(t, err, "checking table "+table)
|
|
return true
|
|
}
|
|
|
|
// TestEnsureSchemaFastPathCreatesVectorTables simulates upgrading an existing
|
|
// deployment: a full schema is already current (so EnsureSchema takes the
|
|
// pushSchemaCurrent fast path), but predates the vector tables. A plain
|
|
// post-upgrade pg push must still create them via the fast path's best-effort
|
|
// vector setup, otherwise semantic search never becomes available.
|
|
func TestEnsureSchemaFastPathCreatesVectorTables(t *testing.T) {
|
|
pgURL := testPGURL(t)
|
|
cleanVectorSchemaTestPG(t, pgURL)
|
|
t.Cleanup(func() { cleanVectorSchemaTestPG(t, pgURL) })
|
|
|
|
pg, err := Open(pgURL, vectorSchemaTestSchema, true)
|
|
require.NoError(t, err, "connecting to pg")
|
|
defer pg.Close()
|
|
|
|
ctx := context.Background()
|
|
require.NoError(t, EnsureSchema(ctx, pg, vectorSchemaTestSchema))
|
|
|
|
if available, err := VectorExtensionAvailable(ctx, pg); err != nil {
|
|
require.NoError(t, err)
|
|
} else if !available {
|
|
t.Skip("pgvector extension unavailable")
|
|
}
|
|
require.True(t, pushSchemaCurrent(ctx, pg),
|
|
"a freshly ensured schema must read as current so the fast path is exercised")
|
|
|
|
// Simulate a pre-vector deployment: drop the four vector tables while the
|
|
// rest of the schema stays current.
|
|
for _, table := range []string{
|
|
"vector_push_state", "vector_documents",
|
|
"vector_generation_machines", "vector_generations",
|
|
} {
|
|
_, err := pg.ExecContext(ctx, `DROP TABLE IF EXISTS `+table+` CASCADE`)
|
|
require.NoError(t, err, "dropping "+table)
|
|
}
|
|
require.False(t, tableExistsPG(t, pg, "vector_generations"),
|
|
"vector_generations must be gone before the fast-path re-ensure")
|
|
|
|
sync := &Sync{
|
|
pg: pg,
|
|
machine: "test-machine",
|
|
schema: vectorSchemaTestSchema,
|
|
}
|
|
require.NoError(t, sync.EnsureSchema(ctx))
|
|
|
|
assert.True(t, tableExistsPG(t, pg, "vector_generations"),
|
|
"the fast path must recreate the vector tables on an up-to-date schema")
|
|
assert.True(t, tableExistsPG(t, pg, "vector_documents"))
|
|
assert.True(t, tableExistsPG(t, pg, "vector_push_state"))
|
|
}
|
|
|
|
// TestVectorHalfvecGate pins the availability gate's two observable shapes:
|
|
// on a server with pgvector >= 0.7 the gate reports available (empty reason)
|
|
// and the halfvec probe returns the type plus a version; on a server without
|
|
// pgvector the gate degrades to a reason instead of an error, which is what
|
|
// keeps an old or missing extension from hard-failing `pg push`.
|
|
func TestVectorHalfvecGate(t *testing.T) {
|
|
pgURL := testPGURL(t)
|
|
cleanVectorSchemaTestPG(t, pgURL)
|
|
t.Cleanup(func() { cleanVectorSchemaTestPG(t, pgURL) })
|
|
|
|
pg, err := Open(pgURL, vectorSchemaTestSchema, true)
|
|
require.NoError(t, err, "connecting to pg")
|
|
defer pg.Close()
|
|
|
|
ctx := context.Background()
|
|
require.NoError(t, EnsureSchema(ctx, pg, vectorSchemaTestSchema))
|
|
|
|
unavailable, err := ensureVectorBaseSchemaPG(ctx, pg)
|
|
require.NoError(t, err, "gate must degrade, never error")
|
|
hasHalfvec, version, err := vectorHalfvecAvailable(ctx, pg)
|
|
require.NoError(t, err)
|
|
|
|
if unavailable == "" {
|
|
assert.True(t, hasHalfvec, "available gate implies halfvec exists")
|
|
assert.NotEmpty(t, version, "installed extension reports a version")
|
|
return
|
|
}
|
|
assert.False(t, hasHalfvec,
|
|
"unavailable reason %q must mean halfvec is missing", unavailable)
|
|
}
|