Closes the in-code Step 5 TODO at postgres-engine.ts:131 + pglite-engine.ts:127.
- types.ts: PageInput.source_id?: string
- {postgres,pglite}-engine.ts putPage: accept source_id, INSERT explicit col when set
- {postgres,pglite}-engine.ts getTags/addTag/removeTag/upsertChunks/deleteChunks/
createVersion: optional sourceId param scopes slug->page_id lookup (avoids
subquery uniqueness violations on multi-source same-slug)
- engine.ts interface: matching optional sourceId params
- import-file.ts importFromContent/importFromFile: opts.sourceId, source-aware
idempotency check, threads through entire transaction
- import.ts runImport: opts.sourceId
- sync.ts: thread opts.sourceId through 3 importFile call sites + unconditional
resolveSourceId with pre-v0.17 backward-compat safety net (drop literal
'default' to undefined when no explicit/env signal)
- operations.ts put_page handler: resolveSourceId chain, source_id param schema
Tests:
- test/multi-source-write-path.test.ts (new): putPage explicit/implicit, ON
CONFLICT upsert, cross-source same-slug isolation, importFromContent
threading, content_hash idempotency source-aware
- test/sync-resolveSourceId-unconditional-regression.test.ts (new, CRITICAL
REGRESSION): pre-v0.17 brain backwards-compat, dotfile/cwd-prefix branches
fire, sync.ts safety-net rule
bun test: 2182 pass / 0 fail / 250 skip (E2E DATABASE_URL gated). Baseline
preserved.
Strategy: full fork-local (no upstream PR sent), per /plan-eng-review T1
outside-voice tension reconsidered post-impl. Engine-method source-aware
expansion was discovered mid-impl when cross-source same-slug tests hit
SQL state 21000 (subquery uniqueness violation) on slug-only methods.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
233 lines
7.7 KiB
TypeScript
233 lines
7.7 KiB
TypeScript
/**
|
|
* v0.18.0 Step 5 — multi-source write path tests.
|
|
*
|
|
* Verifies that source_id threads end-to-end through every write surface:
|
|
*
|
|
* PageInput.source_id → putPage() INSERT (engine direct)
|
|
* importFromContent({sourceId}) → putPage() (parse + transaction)
|
|
* importFromFile({sourceId}) → importFromContent
|
|
* runImport({sourceId}) → importFile loop
|
|
*
|
|
* Both PGLite (this file) and Postgres (parity in test/e2e/mechanical.test.ts
|
|
* when DATABASE_URL is set) must agree on the per-row source_id outcome.
|
|
*
|
|
* Step-2-through-Step-4 schema invariants (default seed, composite UNIQUE,
|
|
* source_id col exists) are already covered in multi-source-integration.test.ts;
|
|
* this file focuses purely on the WRITE-THROUGH semantics that Step 5 introduces.
|
|
*/
|
|
|
|
import { describe, test, expect, beforeAll, afterAll } from 'bun:test';
|
|
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'fs';
|
|
import { join } from 'path';
|
|
import { tmpdir } from 'os';
|
|
import { PGLiteEngine } from '../src/core/pglite-engine.ts';
|
|
import { importFromContent, importFromFile } from '../src/core/import-file.ts';
|
|
|
|
let engine: PGLiteEngine;
|
|
let tmpRoot: string;
|
|
|
|
beforeAll(async () => {
|
|
engine = new PGLiteEngine();
|
|
await engine.connect({ type: 'pglite' } as never);
|
|
await engine.initSchema();
|
|
|
|
// Pre-seed the named sources we'll route writes to. The 'default' row is
|
|
// seeded by migration v16; the rest we add explicitly so resolveSourceId
|
|
// / explicit threading have valid FK targets.
|
|
await engine.executeRaw(
|
|
`INSERT INTO sources (id, name, config) VALUES
|
|
('memory-dashboard', 'memory-dashboard', '{"federated": true}'::jsonb),
|
|
('stock-dashboard', 'stock-dashboard', '{"federated": true}'::jsonb)
|
|
ON CONFLICT (id) DO NOTHING`,
|
|
);
|
|
|
|
tmpRoot = mkdtempSync(join(tmpdir(), 'gbrain-step5-'));
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await engine.disconnect();
|
|
rmSync(tmpRoot, { recursive: true, force: true });
|
|
});
|
|
|
|
describe('Step 5 — engine.putPage threading', () => {
|
|
test('putPage with explicit source_id writes to that source', async () => {
|
|
await engine.putPage('write-path/explicit-md', {
|
|
type: 'note',
|
|
title: 'Explicit',
|
|
compiled_truth: 'goes to memory-dashboard',
|
|
source_id: 'memory-dashboard',
|
|
});
|
|
const rows = await engine.executeRaw<{ source_id: string }>(
|
|
`SELECT source_id FROM pages WHERE slug = 'write-path/explicit-md'`,
|
|
);
|
|
expect(rows.length).toBe(1);
|
|
expect(rows[0].source_id).toBe('memory-dashboard');
|
|
});
|
|
|
|
test('putPage without source_id falls back to schema DEFAULT default', async () => {
|
|
await engine.putPage('write-path/implicit-md', {
|
|
type: 'note',
|
|
title: 'Implicit',
|
|
compiled_truth: 'no source_id passed',
|
|
});
|
|
const rows = await engine.executeRaw<{ source_id: string }>(
|
|
`SELECT source_id FROM pages WHERE slug = 'write-path/implicit-md'`,
|
|
);
|
|
expect(rows.length).toBe(1);
|
|
expect(rows[0].source_id).toBe('default');
|
|
});
|
|
|
|
test('putPage twice on same (source, slug) upserts in place', async () => {
|
|
await engine.putPage('write-path/upsert-key', {
|
|
type: 'note',
|
|
title: 'First',
|
|
compiled_truth: 'v1',
|
|
source_id: 'memory-dashboard',
|
|
});
|
|
await engine.putPage('write-path/upsert-key', {
|
|
type: 'note',
|
|
title: 'Second',
|
|
compiled_truth: 'v2',
|
|
source_id: 'memory-dashboard',
|
|
});
|
|
const rows = await engine.executeRaw<{ title: string; compiled_truth: string }>(
|
|
`SELECT title, compiled_truth FROM pages
|
|
WHERE source_id = 'memory-dashboard' AND slug = 'write-path/upsert-key'`,
|
|
);
|
|
expect(rows.length).toBe(1);
|
|
expect(rows[0].title).toBe('Second');
|
|
expect(rows[0].compiled_truth).toBe('v2');
|
|
});
|
|
|
|
test('putPage with same slug across two sources keeps both rows distinct', async () => {
|
|
await engine.putPage('write-path/same-slug', {
|
|
type: 'note',
|
|
title: 'In MD',
|
|
compiled_truth: 'memory-dashboard side',
|
|
source_id: 'memory-dashboard',
|
|
});
|
|
await engine.putPage('write-path/same-slug', {
|
|
type: 'note',
|
|
title: 'In SD',
|
|
compiled_truth: 'stock-dashboard side',
|
|
source_id: 'stock-dashboard',
|
|
});
|
|
const rows = await engine.executeRaw<{ source_id: string; title: string }>(
|
|
`SELECT source_id, title FROM pages
|
|
WHERE slug = 'write-path/same-slug'
|
|
ORDER BY source_id`,
|
|
);
|
|
expect(rows.length).toBe(2);
|
|
expect(rows[0].source_id).toBe('memory-dashboard');
|
|
expect(rows[1].source_id).toBe('stock-dashboard');
|
|
});
|
|
});
|
|
|
|
describe('Step 5 — importFromContent threading', () => {
|
|
test('importFromContent({sourceId}) writes via the threaded source', async () => {
|
|
const md = `---
|
|
title: From Content
|
|
type: note
|
|
---
|
|
# From Content
|
|
|
|
Hello world.
|
|
`;
|
|
const result = await importFromContent(engine, 'write-path/from-content', md, {
|
|
noEmbed: true,
|
|
sourceId: 'memory-dashboard',
|
|
});
|
|
expect(result.status).toBe('imported');
|
|
const rows = await engine.executeRaw<{ source_id: string }>(
|
|
`SELECT source_id FROM pages WHERE slug = 'write-path/from-content'`,
|
|
);
|
|
expect(rows[0].source_id).toBe('memory-dashboard');
|
|
});
|
|
|
|
test('importFromContent without sourceId opt → DEFAULT default', async () => {
|
|
const md = `---
|
|
title: From Content Default
|
|
type: note
|
|
---
|
|
Default-targeted body.
|
|
`;
|
|
const result = await importFromContent(engine, 'write-path/from-content-default', md, {
|
|
noEmbed: true,
|
|
});
|
|
expect(result.status).toBe('imported');
|
|
const rows = await engine.executeRaw<{ source_id: string }>(
|
|
`SELECT source_id FROM pages WHERE slug = 'write-path/from-content-default'`,
|
|
);
|
|
expect(rows[0].source_id).toBe('default');
|
|
});
|
|
});
|
|
|
|
describe('Step 5 — importFromFile threading', () => {
|
|
test('importFromFile({sourceId}) reads disk + writes to source', async () => {
|
|
const repoDir = join(tmpRoot, 'repo-a');
|
|
mkdirSync(repoDir, { recursive: true });
|
|
const filePath = join(repoDir, 'write-path-from-file.md');
|
|
writeFileSync(
|
|
filePath,
|
|
`---
|
|
title: From File
|
|
type: note
|
|
---
|
|
On-disk content routed to stock-dashboard.
|
|
`,
|
|
);
|
|
const result = await importFromFile(engine, filePath, 'write-path/from-file', {
|
|
noEmbed: true,
|
|
sourceId: 'stock-dashboard',
|
|
});
|
|
expect(result.status).toBe('imported');
|
|
const rows = await engine.executeRaw<{ source_id: string }>(
|
|
`SELECT source_id FROM pages WHERE slug = 'write-path/from-file'`,
|
|
);
|
|
expect(rows[0].source_id).toBe('stock-dashboard');
|
|
});
|
|
});
|
|
|
|
describe('Step 5 — content_hash idempotency unaffected by source_id', () => {
|
|
test('rewriting identical content to same source returns skipped', async () => {
|
|
const md = `---
|
|
title: Idempotent
|
|
type: note
|
|
---
|
|
Stable body.
|
|
`;
|
|
const r1 = await importFromContent(engine, 'write-path/idempotent', md, {
|
|
noEmbed: true,
|
|
sourceId: 'memory-dashboard',
|
|
});
|
|
expect(r1.status).toBe('imported');
|
|
|
|
const r2 = await importFromContent(engine, 'write-path/idempotent', md, {
|
|
noEmbed: true,
|
|
sourceId: 'memory-dashboard',
|
|
});
|
|
expect(r2.status).toBe('skipped');
|
|
});
|
|
|
|
test('same slug in different source counts as a separate page (not skip)', async () => {
|
|
const md = `---
|
|
title: Cross-Source Slug
|
|
type: note
|
|
---
|
|
Same body, different source.
|
|
`;
|
|
const r1 = await importFromContent(engine, 'write-path/cross-slug', md, {
|
|
noEmbed: true,
|
|
sourceId: 'memory-dashboard',
|
|
});
|
|
expect(r1.status).toBe('imported');
|
|
|
|
const r2 = await importFromContent(engine, 'write-path/cross-slug', md, {
|
|
noEmbed: true,
|
|
sourceId: 'stock-dashboard',
|
|
});
|
|
// Different (source_id, slug) row → must be a fresh import, not a skip.
|
|
expect(r2.status).toBe('imported');
|
|
});
|
|
});
|