/** * v0.31 Phase 6 — dream-cycle `consolidate` phase tests. * * Pins: * - Below-threshold buckets are skipped (count < 3 OR oldest < 24h) * - Cluster of >=2 same-vector facts produces 1 take, marks all facts consolidated * - Never DELETE — facts stay as audit trail * - dryRun honored */ import { describe, test, expect, beforeAll, afterAll, beforeEach } from 'bun:test'; import { PGLiteEngine } from '../src/core/pglite-engine.ts'; import { configureGateway } from '../src/core/ai/gateway.ts'; import { runPhaseConsolidate } from '../src/core/cycle/phases/consolidate.ts'; let engine: PGLiteEngine; beforeAll(async () => { engine = new PGLiteEngine(); await engine.connect({}); // initSchema() bakes the facts.embedding dim from the gateway's configured // embedding model; the default is now 1280-d (ZE). This file's fixtures are // 1536-d, so pin the legacy 1536-d OpenAI config (matching // test/helpers/legacy-embedding-preload.ts) right before initSchema. The // global preload sets this, but a co-sharded test that calls resetGateway() // in its teardown nulls it, leaving initSchema to fall back to the 1280-d // default and build a halfvec(1280) column the 1536-d inserts can't fill. // Re-pinning here makes the schema deterministic regardless of shard // neighbors (surfaced when #1972's new test files reshuffled the shards). configureGateway({ embedding_model: 'openai:text-embedding-3-large', embedding_dimensions: 1536, env: { ...process.env }, }); await engine.initSchema(); }); afterAll(async () => { await engine.disconnect(); }); beforeEach(async () => { // Clean facts + takes between tests for hermetic state. await engine.executeRaw(`DELETE FROM facts`); await engine.executeRaw(`DELETE FROM takes`); }); const oldDate = () => new Date(Date.now() - 30 * 60 * 60 * 1000).toISOString(); const recentDate = () => new Date(Date.now() - 60 * 1000).toISOString(); function unitVec(): string { const a = new Float32Array(1536); a[0] = 1.0; return '[' + Array.from(a).join(',') + ']'; } async function seedPage(slug: string): Promise { await engine.executeRaw( `INSERT INTO pages (slug, type, title) VALUES ($1, 'concept', 'Test') ON CONFLICT DO NOTHING`, [slug], ); const r = await engine.executeRaw<{ id: number }>( `SELECT id FROM pages WHERE slug = $1 AND source_id = 'default'`, [slug], ); return r[0].id; } describe('runPhaseConsolidate', () => { test('below threshold (count < 3) → skipped', async () => { await seedPage('cons-skip-count'); for (let i = 0; i < 2; i++) { await engine.executeRaw( `INSERT INTO facts (source_id, entity_slug, fact, kind, source, valid_from, embedding, embedded_at) VALUES ('default', 'cons-skip-count', $1, 'fact', 'test', $2::timestamptz, $3::vector, $2::timestamptz)`, [`fact ${i}`, oldDate(), unitVec()], ); } const r = await runPhaseConsolidate(engine, {}); expect(r.details.facts_consolidated).toBe(0); expect(r.details.takes_written).toBe(0); }); test('all facts too recent → bucket processed but skipped, 0 work', async () => { await seedPage('cons-skip-age'); for (let i = 0; i < 4; i++) { await engine.executeRaw( `INSERT INTO facts (source_id, entity_slug, fact, kind, source, valid_from, embedding, embedded_at) VALUES ('default', 'cons-skip-age', $1, 'fact', 'test', $2::timestamptz, $3::vector, $2::timestamptz)`, [`fact ${i}`, recentDate(), unitVec()], ); } const r = await runPhaseConsolidate(engine, {}); expect(r.details.facts_consolidated).toBe(0); expect(r.details.buckets_skipped).toBeGreaterThanOrEqual(1); }); test('happy path: 4 same-vector facts on a page → 1 take, all consolidated', async () => { const pageId = await seedPage('people/alice-example'); expect(pageId).toBeGreaterThan(0); for (let i = 0; i < 4; i++) { await engine.executeRaw( `INSERT INTO facts (source_id, entity_slug, fact, kind, source, valid_from, confidence, embedding, embedded_at) VALUES ('default', 'people/alice-example', $1, 'fact', 'test', $2::timestamptz, 0.9, $3::vector, $2::timestamptz)`, [`alice fact ${i}`, oldDate(), unitVec()], ); } const r = await runPhaseConsolidate(engine, {}); expect(r.details.facts_consolidated).toBe(4); expect(r.details.takes_written).toBe(1); // Take row created on the right page. const takes = await engine.executeRaw<{ page_id: number; kind: string; weight: number; holder: string }>( `SELECT page_id, kind, weight, holder FROM takes`, ); expect(takes.length).toBe(1); expect(takes[0].page_id).toBe(pageId); expect(takes[0].kind).toBe('fact'); expect(takes[0].holder).toBe('self'); expect(takes[0].weight).toBeCloseTo(0.9, 2); // Facts marked consolidated, NEVER deleted. const facts = await engine.executeRaw<{ id: number; consolidated_at: Date | null; consolidated_into: number | null }>( `SELECT id, consolidated_at, consolidated_into FROM facts ORDER BY id`, ); expect(facts.length).toBe(4); for (const f of facts) { expect(f.consolidated_at).not.toBeNull(); expect(f.consolidated_into).not.toBeNull(); } }); test('dryRun honored: counters tick but no rows written', async () => { await seedPage('cons-dryrun'); for (let i = 0; i < 3; i++) { await engine.executeRaw( `INSERT INTO facts (source_id, entity_slug, fact, kind, source, valid_from, embedding, embedded_at) VALUES ('default', 'cons-dryrun', $1, 'fact', 'test', $2::timestamptz, $3::vector, $2::timestamptz)`, [`dryrun fact ${i}`, oldDate(), unitVec()], ); } const r = await runPhaseConsolidate(engine, { dryRun: true }); expect(r.details.dryRun).toBe(true); expect(r.details.facts_consolidated).toBe(3); expect(r.details.takes_written).toBe(1); const takes = await engine.executeRaw<{ id: number }>(`SELECT id FROM takes`); expect(takes.length).toBe(0); const facts = await engine.executeRaw<{ id: number; consolidated_at: Date | null }>( `SELECT id, consolidated_at FROM facts ORDER BY id`, ); for (const f of facts) { expect(f.consolidated_at).toBeNull(); } }); test('skips bucket when no matching page exists in source', async () => { // Don't seed a page — entity_slug 'no-page' won't resolve. for (let i = 0; i < 4; i++) { await engine.executeRaw( `INSERT INTO facts (source_id, entity_slug, fact, kind, source, valid_from, embedding, embedded_at) VALUES ('default', 'no-page', $1, 'fact', 'test', $2::timestamptz, $3::vector, $2::timestamptz)`, [`orphan fact ${i}`, oldDate(), unitVec()], ); } const r = await runPhaseConsolidate(engine, {}); // Bucket processed (passes count + age gates) but cluster skipped (no page). expect(r.details.buckets_processed).toBeGreaterThanOrEqual(1); expect(r.details.facts_consolidated).toBe(0); expect(r.details.takes_written).toBe(0); }); });