/** * v0.18.0 Step 9 — multi-source integration test against real PGLite. * * Exercises the full Step-1-through-Step-7 surface: * - migration v16 seeds the default source with federated=true * - migration v17 adds pages.source_id + composite UNIQUE * - migration v18 adds links.resolution_type column * - putPage implicitly targets the default source via the * schema DEFAULT 'default' clause * - raw INSERT can write pages to a non-default source and the * composite UNIQUE allows same-slug pages across sources * - sources CLI add/list/federate operations are reflected in DB * - federated flag distinguishes unqualified-search-visibility * * PGLite-only (fast + zero deps). Real Postgres parity lives in * test/e2e/mechanical.test.ts when DATABASE_URL is set. */ import { describe, test, expect, beforeAll, afterAll } from 'bun:test'; import { PGLiteEngine } from '../src/core/pglite-engine.ts'; import { runSources } from '../src/commands/sources.ts'; import { resolveSourceId } from '../src/core/source-resolver.ts'; let engine: PGLiteEngine; beforeAll(async () => { engine = new PGLiteEngine(); await engine.connect({ type: 'pglite' } as never); await engine.initSchema(); }, 60_000); // OAuth v25 + full migration chain needs breathing room afterAll(async () => { if (engine) await engine.disconnect(); }, 60_000); describe('v0.18.0 — sources table seeded with default row on fresh PGLite', () => { test("sources('default') exists after initSchema + migration", async () => { const rows = await engine.executeRaw<{ id: string; name: string; config: string | Record }>( `SELECT id, name, config FROM sources WHERE id = 'default'`, ); expect(rows.length).toBe(1); expect(rows[0].name).toBe('default'); const config = typeof rows[0].config === 'string' ? JSON.parse(rows[0].config) : rows[0].config; expect(config.federated).toBe(true); }); test('pages.source_id column exists with DEFAULT default', async () => { const rows = await engine.executeRaw<{ column_default: string | null }>( `SELECT column_default FROM information_schema.columns WHERE table_name = 'pages' AND column_name = 'source_id'`, ); expect(rows.length).toBe(1); // PGLite normalizes the default literal. expect(rows[0].column_default).toContain('default'); }); test('composite UNIQUE (source_id, slug) is installed', async () => { const rows = await engine.executeRaw<{ conname: string }>( `SELECT conname FROM pg_constraint WHERE conname = 'pages_source_slug_key'`, ); expect(rows.length).toBe(1); }); }); describe('v0.18.0 — putPage implicitly writes to default source', () => { test('putPage without explicit source → source_id = default', async () => { await engine.putPage('topics/step9-auto', { type: 'concept', title: 'Step 9 Auto', compiled_truth: 'Auto-defaulted to default source.', }); const rows = await engine.executeRaw<{ source_id: string; slug: string }>( `SELECT source_id, slug FROM pages WHERE slug = 'topics/step9-auto'`, ); expect(rows.length).toBe(1); expect(rows[0].source_id).toBe('default'); }); }); describe('v0.18.0 — composite UNIQUE allows same-slug across sources', () => { test('same slug in two different sources coexists (regression: Codex critical)', async () => { // Insert a second source via sources CLI. await runSources(engine, ['add', 'testsrc', '--no-federated']); // Sanity: default already has this slug from the previous test. // Now write the same slug under testsrc via raw INSERT (putPage only // targets default until a later step surfaces sourceId; raw INSERT is // the "source-aware write" Step 5 continuation will add). await engine.executeRaw( `INSERT INTO pages (source_id, slug, type, title, compiled_truth, timeline, frontmatter, content_hash) VALUES ('testsrc', 'topics/step9-auto', 'concept', 'Step 9 Auto (testsrc variant)', 'A different page with the same slug in a different source.', '', '{}'::jsonb, 'hash2')`, ); // Both rows must exist under the composite unique. const rows = await engine.executeRaw<{ source_id: string; slug: string; title: string }>( `SELECT source_id, slug, title FROM pages WHERE slug = 'topics/step9-auto' ORDER BY source_id`, ); expect(rows.length).toBe(2); expect(rows.map(r => r.source_id).sort()).toEqual(['default', 'testsrc']); }); test('inserting THIRD row with same (source_id, slug) hits composite UNIQUE', async () => { let err: Error | null = null; try { await engine.executeRaw( `INSERT INTO pages (source_id, slug, type, title, compiled_truth, timeline, frontmatter, content_hash) VALUES ('testsrc', 'topics/step9-auto', 'concept', 'Dup attempt', 'Should fail', '', '{}'::jsonb, 'hash3')`, ); } catch (e) { err = e as Error; } expect(err).not.toBeNull(); expect(err!.message.toLowerCase()).toMatch(/unique|duplicate/); }); }); describe('v0.18.0 — sources CLI manipulates the sources table', () => { test('sources federate flips config.federated true', async () => { await runSources(engine, ['federate', 'testsrc']); const rows = await engine.executeRaw<{ config: string | Record }>( `SELECT config FROM sources WHERE id = 'testsrc'`, ); const config = typeof rows[0].config === 'string' ? JSON.parse(rows[0].config) : rows[0].config; expect(config.federated).toBe(true); }); test('sources unfederate flips config.federated false', async () => { await runSources(engine, ['unfederate', 'testsrc']); const rows = await engine.executeRaw<{ config: string | Record }>( `SELECT config FROM sources WHERE id = 'testsrc'`, ); const config = typeof rows[0].config === 'string' ? JSON.parse(rows[0].config) : rows[0].config; expect(config.federated).toBe(false); }); test('sources rename changes name but keeps id immutable', async () => { await runSources(engine, ['rename', 'testsrc', 'Test Source']); const rows = await engine.executeRaw<{ id: string; name: string }>( `SELECT id, name FROM sources WHERE id = 'testsrc'`, ); expect(rows[0].id).toBe('testsrc'); expect(rows[0].name).toBe('Test Source'); }); }); describe('v0.18.0 — source resolution priority (integration)', () => { test('explicit --source flag wins when the source exists', async () => { const id = await resolveSourceId(engine, 'testsrc'); expect(id).toBe('testsrc'); }); test('GBRAIN_SOURCE env wins when no flag', async () => { process.env.GBRAIN_SOURCE = 'testsrc'; try { const id = await resolveSourceId(engine, null); expect(id).toBe('testsrc'); } finally { delete process.env.GBRAIN_SOURCE; } }); test('fallback to default when nothing is set', async () => { const id = await resolveSourceId(engine, null, '/nowhere-registered'); expect(id).toBe('default'); }); test('rejects unregistered explicit source with an actionable error', async () => { await expect(resolveSourceId(engine, 'ghost-source')).rejects.toThrow(/not found/); }); }); describe('v0.18.0 — sources remove cascades to pages', () => { test('removing a source cascade-deletes its pages', async () => { const before = await engine.executeRaw<{ n: number }>( `SELECT COUNT(*)::int AS n FROM pages WHERE source_id = 'testsrc'`, ); expect(before[0].n).toBeGreaterThan(0); // v0.26.5: populated sources require --confirm-destructive; --yes alone is rejected. await runSources(engine, ['remove', 'testsrc', '--confirm-destructive']); const after = await engine.executeRaw<{ n: number }>( `SELECT COUNT(*)::int AS n FROM pages WHERE source_id = 'testsrc'`, ); expect(after[0].n).toBe(0); const src = await engine.executeRaw<{ id: string }>( `SELECT id FROM sources WHERE id = 'testsrc'`, ); expect(src.length).toBe(0); // Default source is untouched. const defaultPages = await engine.executeRaw<{ n: number }>( `SELECT COUNT(*)::int AS n FROM pages WHERE source_id = 'default'`, ); expect(defaultPages[0].n).toBeGreaterThan(0); }); }); describe('v0.18.0 — links.resolution_type column exists (Step 4)', () => { test('links table accepts qualified/unqualified resolution_type', async () => { // Create two pages, insert a link with resolution_type='qualified'. await engine.putPage('topics/qf-a', { type: 'concept', title: 'QA', compiled_truth: 'a', }); await engine.putPage('topics/qf-b', { type: 'concept', title: 'QB', compiled_truth: 'b', }); await engine.executeRaw( `INSERT INTO links (from_page_id, to_page_id, link_type, context, link_source, resolution_type) SELECT a.id, b.id, 'ref', '', 'markdown', 'qualified' FROM pages a, pages b WHERE a.slug = 'topics/qf-a' AND b.slug = 'topics/qf-b' AND a.source_id = 'default' AND b.source_id = 'default'`, ); const rows = await engine.executeRaw<{ resolution_type: string }>( `SELECT l.resolution_type FROM links l JOIN pages a ON a.id = l.from_page_id WHERE a.slug = 'topics/qf-a'`, ); expect(rows.length).toBe(1); expect(rows[0].resolution_type).toBe('qualified'); }); test('links CHECK constraint rejects invalid resolution_type values', async () => { let err: Error | null = null; try { await engine.executeRaw( `INSERT INTO links (from_page_id, to_page_id, link_type, resolution_type) SELECT a.id, a.id, 'self', 'bogus-value' FROM pages a WHERE a.slug = 'topics/qf-a' AND a.source_id = 'default'`, ); } catch (e) { err = e as Error; } expect(err).not.toBeNull(); expect(err!.message.toLowerCase()).toMatch(/check|constraint/); }); });