/** * E2E dream tests — Tier 1 (no API keys required). * * Drives the dream CLI entry point through a real Postgres engine with * a real git repo. Complements test/dream.test.ts (which exercises the * code paths via the library call) by testing the actual CLI output * shape and exit-code semantics against real DB state. * * Run: DATABASE_URL=... bun test test/e2e/dream.test.ts */ import { describe, test, expect, mock, beforeAll, afterAll } from 'bun:test'; import { mkdtempSync, writeFileSync, rmSync, mkdirSync } from 'fs'; import { join } from 'path'; import { execSync } from 'child_process'; import { tmpdir } from 'os'; import { hasDatabase, setupDB, teardownDB, getEngine, getConn } from './helpers.ts'; // Mock embedBatch so embed phase doesn't call OpenAI. mock.module('../../src/core/embedding.ts', () => ({ embedBatch: async (texts: string[]) => texts.map(() => new Float32Array(1536)), // v0.41.31: embed phase reads the current signature to stamp provenance. currentEmbeddingSignature: () => 'test:model:1536', })); const { runDream } = await import('../../src/commands/dream.ts'); const skip = !hasDatabase(); const describeE2E = skip ? describe.skip : describe; if (skip) { console.log('Skipping E2E dream tests (DATABASE_URL not set)'); } function makeGitRepo(): string { const dir = mkdtempSync(join(tmpdir(), 'gbrain-e2e-dream-')); execSync('git init', { cwd: dir, stdio: 'pipe' }); execSync('git config user.email test@test.co', { cwd: dir, stdio: 'pipe' }); execSync('git config user.name test', { cwd: dir, stdio: 'pipe' }); mkdirSync(join(dir, 'concepts'), { recursive: true }); writeFileSync( join(dir, 'concepts/testing.md'), '---\ntype: concept\ntitle: Testing Philosophy\n---\n\nEvery untested path is a path where bugs hide.\n', ); execSync('git add -A && git commit -m init', { cwd: dir, stdio: 'pipe' }); return dir; } function captureLog(fn: () => Promise): Promise<{ result: T; output: string }> { return new Promise(async (resolve, reject) => { const lines: string[] = []; const origLog = console.log; console.log = (...args: unknown[]) => { lines.push(args.map(String).join(' ')); }; try { const result = await fn(); resolve({ result, output: lines.join('\n') }); } catch (e) { reject(e); } finally { console.log = origLog; } }); } describeE2E('E2E: gbrain dream CLI against real Postgres', () => { let repo: string; beforeAll(async () => { await setupDB(); repo = makeGitRepo(); }, 30_000); afterAll(async () => { await teardownDB(); if (repo) rmSync(repo, { recursive: true, force: true }); }); test('dream --dry-run --json emits a valid CycleReport + DB stays empty', async () => { const conn = getConn(); const beforePages = await conn.unsafe(`SELECT count(*)::int AS n FROM pages`); const { output } = await captureLog(() => runDream(getEngine(), ['--dir', repo, '--dry-run', '--json']), ); // dream prints a CycleReport as pretty-printed JSON. It may be // preceded by inline phase-runner log lines (e.g. sync's // "Full-sync dry run: N files"). Extract the JSON object. const jsonStart = output.indexOf('{'); expect(jsonStart).toBeGreaterThanOrEqual(0); const parsed = JSON.parse(output.slice(jsonStart)); expect(parsed.schema_version).toBe('1'); expect(parsed).toHaveProperty('status'); expect(parsed).toHaveProperty('phases'); expect(parsed).toHaveProperty('totals'); expect(parsed.brain_dir).toBe(repo); // No pages were written. const afterPages = await conn.unsafe(`SELECT count(*)::int AS n FROM pages`); expect(afterPages[0].n).toBe(beforePages[0].n); }); test('dream (no --dry-run) syncs pages into the real DB', async () => { const conn = getConn(); await captureLog(() => runDream(getEngine(), ['--dir', repo, '--json'])); const pages = await conn.unsafe(`SELECT slug FROM pages ORDER BY slug`); const slugs = (pages as unknown as Array<{ slug: string }>).map(p => p.slug); expect(slugs).toContain('concepts/testing'); // sync.last_commit bookmark set. const sync = await conn.unsafe( `SELECT value FROM config WHERE key = 'sync.last_commit'`, ); expect(sync.length).toBe(1); }, 60_000); test('dream --phase orphans only reports orphans + no cycle-lock footprint', async () => { const conn = getConn(); const before = await conn.unsafe( `SELECT COUNT(*)::int AS n FROM gbrain_cycle_locks`, ); const { result } = await captureLog(() => runDream(getEngine(), ['--dir', repo, '--phase', 'orphans', '--json']), ); expect(result).toBeTruthy(); if (result) { expect(result.phases.length).toBe(1); expect(result.phases[0].phase).toBe('orphans'); } const after = await conn.unsafe( `SELECT COUNT(*)::int AS n FROM gbrain_cycle_locks`, ); // Read-only phase selection doesn't touch the lock table. expect(after[0].n).toBe(before[0].n); }); });