mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-30 19:49:14 +00:00
Six test files spin up PGLite + 20 migrations + git repos in beforeEach/ beforeAll hooks. Under 136-way parallel test file execution, bun's default 5s hook timeout wasn't enough, producing 18 flaky failures that only reproduced under full-suite parallel load (all 6 files passed in isolation). Root cause: PGLite.create() + initSchema() takes ~3-5s under idle load, but under 136 concurrent WASM instantiations the OS thrashes and hooks stall well past 5s. The bunfig.toml `timeout = 60_000` applies to TESTS, not HOOKS — bun requires per-hook timeouts as the third beforeEach/beforeAll argument. Files touched (hook timeouts added, no test logic changed): - test/dream.test.ts — 5 describe blocks × before/afterEach - test/orphans.test.ts — 1 beforeEach + afterEach - test/core/cycle.test.ts — shared beforeAll + afterAll - test/brain-allowlist.test.ts — beforeAll + afterAll - test/extract-db.test.ts — beforeAll + afterAll - test/multi-source-integration.test.ts — beforeAll + afterAll Results: 2317 pass / 0 fail (was 2253 pass / 18 fail). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
245 lines
9.6 KiB
TypeScript
245 lines
9.6 KiB
TypeScript
/**
|
|
* 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);
|
|
|
|
afterAll(async () => {
|
|
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<string, unknown> }>(
|
|
`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<string, unknown> }>(
|
|
`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<string, unknown> }>(
|
|
`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);
|
|
|
|
await runSources(engine, ['remove', 'testsrc', '--yes']);
|
|
|
|
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/);
|
|
});
|
|
});
|