/** * v0.30.x follow-up to PR #707 — performFullSync source_id threading regression test. * * Pre-fix bug: * - PR #707 fixed source_id routing for sync's incremental loop (sync.ts:581 + 641), * but `performFullSync` (the path `--full` invokes) at sync.ts:892 called * `runImport(engine, importArgs, { commit: headCommit })` without threading sourceId. * - Result: `gbrain sync --source X --full` updated `sources.last_sync_at` to look * like binding worked, but actual page rows landed in source_id='default'. * - The 19 tests at test/source-id-tx-regression.test.ts validate the engine-layer * transaction surface (putPage / addTag / etc.) but do NOT exercise performFullSync. * Confirmed via: grep -c 'performFullSync' test/source-id-tx-regression.test.ts → 0. * * Fix (this PR-E follow-up to PR #707): * - runImport accepts opts.sourceId (programmatic-only — no CLI flag, preserves * PR #707's design intent of `gbrain import` being default-only). * - runImport threads sourceId to importFile + importImageFile. * - performFullSync passes opts.sourceId to runImport. * - ImportImageOptions type accepts sourceId (TS-only fix; image-import body * wiring deferred — out of scope here, marked as a separate PR-C-style follow-up). * * This test verifies the sync-command-layer fix end-to-end on PGLite. * * Discovered: 2026-05-08 PRISM Round 2 Performance review on * `~/atlas/agents/terminal/docs/atlas-needs-from-gbrain-spec-v2.1-2026-05-08.md` * by Atlas Terminal agent. Test required as PR-E acceptance criterion. */ import { describe, test, expect, beforeAll, afterAll, beforeEach, afterEach } from 'bun:test'; import { mkdtempSync, writeFileSync, rmSync, mkdirSync } from 'fs'; import { execSync } from 'child_process'; import { tmpdir } from 'os'; import { join } from 'path'; import { PGLiteEngine } from '../src/core/pglite-engine.ts'; import { runSources } from '../src/commands/sources.ts'; import { resetPgliteState } from './helpers/reset-pglite.ts'; let engine: PGLiteEngine; let repoPath: string; async function pageCountBySource(): Promise> { const rows = await engine.executeRaw<{ source_id: string; n: number }>( `SELECT source_id, COUNT(*)::int AS n FROM pages GROUP BY source_id`, ); const out: Record = {}; for (const r of rows) out[r.source_id] = r.n; return out; } describe('performFullSync threads sourceId end-to-end', () => { beforeAll(async () => { engine = new PGLiteEngine(); await engine.connect({}); await engine.initSchema(); await runSources(engine, ['add', 'testsrc-pfs', '--no-federated']); }, 60_000); afterAll(async () => { if (engine) await engine.disconnect(); }, 60_000); beforeEach(async () => { await resetPgliteState(engine); // resetPgliteState clears pages but doesn't drop the source row; re-add only if missing const sources = await engine.executeRaw<{ id: string }>(`SELECT id FROM sources WHERE id = 'testsrc-pfs'`); if (sources.length === 0) { await runSources(engine, ['add', 'testsrc-pfs', '--no-federated']); } repoPath = mkdtempSync(join(tmpdir(), 'gbrain-pfs-')); execSync('git init', { cwd: repoPath, stdio: 'pipe' }); execSync('git config user.email "test@test.com"', { cwd: repoPath, stdio: 'pipe' }); execSync('git config user.name "Test"', { cwd: repoPath, stdio: 'pipe' }); mkdirSync(join(repoPath, 'topics'), { recursive: true }); writeFileSync(join(repoPath, 'topics/foo.md'), [ '---', 'type: concept', 'title: Foo Topic', '---', '', 'Test content for performFullSync source binding.', ].join('\n')); writeFileSync(join(repoPath, 'topics/bar.md'), [ '---', 'type: concept', 'title: Bar Topic', '---', '', 'Second test page to verify multi-page routing.', ].join('\n')); execSync('git add -A && git commit -m "initial"', { cwd: repoPath, stdio: 'pipe' }); }); afterEach(() => { if (repoPath) rmSync(repoPath, { recursive: true, force: true }); }); test('performFullSync with --source routes pages to named source (not default)', async () => { const { performSync } = await import('../src/commands/sync.ts'); const result = await performSync(engine, { repoPath, full: true, sourceId: 'testsrc-pfs', noPull: true, noEmbed: true, }); // status is 'first_sync' for fresh imports, 'synced' for incremental — accept both expect(['first_sync', 'synced']).toContain(result.status); expect(result.added).toBeGreaterThan(0); const counts = await pageCountBySource(); // Pre-fix bug: pages would land in 'default' (sources.last_sync_at would still // update on testsrc-pfs, making the gap silent at the sources-list level). // Post-fix: pages land in 'testsrc-pfs'. expect(counts['testsrc-pfs']).toBeGreaterThan(0); expect(counts['default'] ?? 0).toBe(0); }); test('performFullSync WITHOUT --source still targets default (back-compat preserved)', async () => { const { performSync } = await import('../src/commands/sync.ts'); const result = await performSync(engine, { repoPath, full: true, // no sourceId — expect default-source behavior noPull: true, noEmbed: true, }); // status is 'first_sync' for fresh imports, 'synced' for incremental — accept both expect(['first_sync', 'synced']).toContain(result.status); expect(result.added).toBeGreaterThan(0); const counts = await pageCountBySource(); // Back-compat: callers that omit sourceId continue to target source 'default'. expect(counts['default']).toBeGreaterThan(0); expect(counts['testsrc-pfs'] ?? 0).toBe(0); }); });