mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-30 03:12:32 +00:00
* fix(doctor): honor pgvector HNSW dimension limits * fix(ci): stabilize local Docker verification * chore: bump version and changelog (v0.42.66.1) Co-Authored-By: OpenAI Codex <noreply@openai.com> --------- Co-authored-by: OpenAI Codex <noreply@openai.com>
232 lines
8.2 KiB
TypeScript
232 lines
8.2 KiB
TypeScript
import { describe, expect, test } from 'bun:test';
|
|
import {
|
|
chunkEmbeddingIndexSql,
|
|
applyChunkEmbeddingIndexPolicy,
|
|
PGVECTOR_HNSW_VECTOR_MAX_DIMS,
|
|
PGVECTOR_HNSW_HALFVEC_MAX_DIMS,
|
|
hnswIndexExpected,
|
|
checkActiveBuild,
|
|
dropZombieIndexes,
|
|
dropAndRebuild,
|
|
isSupabaseAutoMaintenance,
|
|
type ActiveBuildInfo,
|
|
type IndexSpec,
|
|
} from '../src/core/vector-index.ts';
|
|
|
|
describe('chunkEmbeddingIndexSql — pre-v0.30.1 contract', () => {
|
|
test('emits CREATE INDEX for dims ≤ 2000', () => {
|
|
const sql = chunkEmbeddingIndexSql(1536);
|
|
expect(sql).toContain('CREATE INDEX IF NOT EXISTS idx_chunks_embedding');
|
|
expect(sql).toContain('hnsw');
|
|
});
|
|
|
|
test('emits skip-comment for dims > 2000 (Voyage 3072)', () => {
|
|
const sql = chunkEmbeddingIndexSql(3072);
|
|
expect(sql).toContain('skipped');
|
|
expect(sql).not.toContain('CREATE INDEX');
|
|
});
|
|
|
|
test('boundary at exactly PGVECTOR_HNSW_VECTOR_MAX_DIMS (2000)', () => {
|
|
const at = chunkEmbeddingIndexSql(PGVECTOR_HNSW_VECTOR_MAX_DIMS);
|
|
expect(at).toContain('CREATE INDEX');
|
|
const above = chunkEmbeddingIndexSql(PGVECTOR_HNSW_VECTOR_MAX_DIMS + 1);
|
|
expect(above).toContain('skipped');
|
|
});
|
|
});
|
|
|
|
describe('hnswIndexExpected', () => {
|
|
test('matches pgvector caps for vector and halfvec columns', () => {
|
|
expect(hnswIndexExpected('vector', PGVECTOR_HNSW_VECTOR_MAX_DIMS)).toBe(true);
|
|
expect(hnswIndexExpected('vector', PGVECTOR_HNSW_VECTOR_MAX_DIMS + 1)).toBe(false);
|
|
expect(hnswIndexExpected('halfvec', PGVECTOR_HNSW_HALFVEC_MAX_DIMS)).toBe(true);
|
|
expect(hnswIndexExpected('halfvec', PGVECTOR_HNSW_HALFVEC_MAX_DIMS + 1)).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('applyChunkEmbeddingIndexPolicy', () => {
|
|
test('replaces the canonical index SQL', () => {
|
|
const input = `BEFORE\nCREATE INDEX IF NOT EXISTS idx_chunks_embedding ON content_chunks USING hnsw (embedding vector_cosine_ops);\nAFTER`;
|
|
const out = applyChunkEmbeddingIndexPolicy(input, 1536);
|
|
expect(out).toContain('idx_chunks_embedding');
|
|
const out2 = applyChunkEmbeddingIndexPolicy(input, 3072);
|
|
expect(out2).toContain('skipped');
|
|
});
|
|
});
|
|
|
|
describe('checkActiveBuild', () => {
|
|
test('PGLite returns active: false', async () => {
|
|
const fakeEngine = { kind: 'pglite' as const } as never;
|
|
const r = await checkActiveBuild(fakeEngine, 'idx_chunks_embedding');
|
|
expect(r.active).toBe(false);
|
|
});
|
|
|
|
test('Postgres with no active builds returns active: false', async () => {
|
|
const fakeEngine = {
|
|
kind: 'postgres' as const,
|
|
executeRaw: async () => [],
|
|
} as never;
|
|
const r = await checkActiveBuild(fakeEngine, 'idx_chunks_embedding');
|
|
expect(r.active).toBe(false);
|
|
});
|
|
|
|
test('Postgres with an active build returns the row', async () => {
|
|
const fakeEngine = {
|
|
kind: 'postgres' as const,
|
|
executeRaw: async () => [
|
|
{ pid: 12345, query: 'CREATE INDEX CONCURRENTLY idx_chunks_embedding ON ...', application_name: 'gbrain' },
|
|
],
|
|
} as never;
|
|
const r = await checkActiveBuild(fakeEngine, 'idx_chunks_embedding');
|
|
expect(r.active).toBe(true);
|
|
expect(r.pid).toBe(12345);
|
|
expect(r.application_name).toBe('gbrain');
|
|
});
|
|
|
|
test('query failure returns active: false (best-effort)', async () => {
|
|
const fakeEngine = {
|
|
kind: 'postgres' as const,
|
|
executeRaw: async () => { throw new Error('permission denied'); },
|
|
} as never;
|
|
const r = await checkActiveBuild(fakeEngine, 'idx_chunks_embedding');
|
|
expect(r.active).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('isSupabaseAutoMaintenance', () => {
|
|
test('true for application_name containing "supabase"', () => {
|
|
expect(isSupabaseAutoMaintenance({ active: true, application_name: 'supabase-cron' })).toBe(true);
|
|
expect(isSupabaseAutoMaintenance({ active: true, application_name: 'postgres-meta' })).toBe(true);
|
|
});
|
|
|
|
test('false for gbrain', () => {
|
|
expect(isSupabaseAutoMaintenance({ active: true, application_name: 'gbrain-worker' })).toBe(false);
|
|
});
|
|
|
|
test('false when not active', () => {
|
|
expect(isSupabaseAutoMaintenance({ active: false })).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('dropZombieIndexes', () => {
|
|
test('PGLite: no-op returns dropped: []', async () => {
|
|
const fakeEngine = { kind: 'pglite' as const } as never;
|
|
const r = await dropZombieIndexes(fakeEngine);
|
|
expect(r.dropped).toEqual([]);
|
|
});
|
|
|
|
test('Postgres: no zombies returns dropped: []', async () => {
|
|
const fakeEngine = {
|
|
kind: 'postgres' as const,
|
|
executeRaw: async () => [],
|
|
} as never;
|
|
const r = await dropZombieIndexes(fakeEngine);
|
|
expect(r.dropped).toEqual([]);
|
|
});
|
|
|
|
test('Postgres: drops invalid indexes, names them in result', async () => {
|
|
let dropCalls = 0;
|
|
const fakeEngine = {
|
|
kind: 'postgres' as const,
|
|
executeRaw: async (sql: string) => {
|
|
if (sql.includes('pg_stat_activity')) return []; // no active builds
|
|
if (sql.includes('pg_index')) {
|
|
return [
|
|
{ indexname: 'zombie_idx_a', tablename: 'content_chunks' },
|
|
{ indexname: 'zombie_idx_b', tablename: 'pages' },
|
|
];
|
|
}
|
|
if (sql.startsWith('DROP INDEX')) {
|
|
dropCalls++;
|
|
return [];
|
|
}
|
|
return [];
|
|
},
|
|
} as never;
|
|
const r = await dropZombieIndexes(fakeEngine);
|
|
expect(r.dropped).toEqual(['zombie_idx_a', 'zombie_idx_b']);
|
|
expect(dropCalls).toBe(2);
|
|
});
|
|
|
|
test('Postgres: skips zombie when active build present', async () => {
|
|
const fakeEngine = {
|
|
kind: 'postgres' as const,
|
|
executeRaw: async (sql: string) => {
|
|
if (sql.includes('pg_stat_activity')) {
|
|
return [{ pid: 555, query: 'CREATE INDEX zombie_idx_a ...', application_name: 'gbrain' }];
|
|
}
|
|
if (sql.includes('pg_index')) {
|
|
return [{ indexname: 'zombie_idx_a', tablename: 'content_chunks' }];
|
|
}
|
|
return [];
|
|
},
|
|
} as never;
|
|
const r = await dropZombieIndexes(fakeEngine);
|
|
expect(r.dropped).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe('dropAndRebuild — A3 atomic-swap', () => {
|
|
test('PGLite: no-op returns rebuilt: false', async () => {
|
|
const fakeEngine = { kind: 'pglite' as const } as never;
|
|
const spec: IndexSpec = {
|
|
name: 'idx_chunks_embedding',
|
|
table: 'content_chunks',
|
|
column: 'embedding',
|
|
using: 'hnsw (embedding vector_cosine_ops)',
|
|
};
|
|
const r = await dropAndRebuild(fakeEngine, spec, { reason: 'test' });
|
|
expect(r.rebuilt).toBe(false);
|
|
});
|
|
|
|
test('Postgres: bails when active build present (without --force)', async () => {
|
|
const fakeEngine = {
|
|
kind: 'postgres' as const,
|
|
executeRaw: async (sql: string) => {
|
|
if (sql.includes('pg_stat_activity')) {
|
|
return [{ pid: 555, query: 'CREATE INDEX idx_chunks_embedding...', application_name: 'supabase' }];
|
|
}
|
|
return [];
|
|
},
|
|
withReservedConnection: async () => { throw new Error('should not be called'); },
|
|
transaction: async () => { throw new Error('should not be called'); },
|
|
} as never;
|
|
const spec: IndexSpec = {
|
|
name: 'idx_chunks_embedding',
|
|
table: 'content_chunks',
|
|
column: 'embedding',
|
|
using: 'hnsw (embedding vector_cosine_ops)',
|
|
};
|
|
const r = await dropAndRebuild(fakeEngine, spec, { reason: 'auto' });
|
|
expect(r.rebuilt).toBe(false);
|
|
});
|
|
|
|
test('temp name format: <name>_rebuild_<unix-ms>', async () => {
|
|
let executedSql = '';
|
|
const fakeEngine = {
|
|
kind: 'postgres' as const,
|
|
executeRaw: async () => [], // no active build
|
|
withReservedConnection: async (fn: any) => fn({
|
|
executeRaw: async (sql: string) => {
|
|
executedSql = sql;
|
|
return [];
|
|
},
|
|
}),
|
|
transaction: async (fn: any) => {
|
|
// Provide a no-op tx with sql.unsafe.
|
|
await fn({ sql: { unsafe: async () => [] } });
|
|
},
|
|
} as never;
|
|
const spec: IndexSpec = {
|
|
name: 'idx_chunks_embedding',
|
|
table: 'content_chunks',
|
|
column: 'embedding',
|
|
using: 'hnsw (embedding vector_cosine_ops)',
|
|
};
|
|
const r = await dropAndRebuild(fakeEngine, spec, { reason: 'test' });
|
|
expect(r.rebuilt).toBe(true);
|
|
expect(r.tempName).toMatch(/^idx_chunks_embedding_rebuild_\d+$/);
|
|
expect(executedSql).toContain('CREATE INDEX CONCURRENTLY');
|
|
expect(executedSql).toContain(r.tempName);
|
|
});
|
|
});
|