/** * Tests for the v0.13.0 frontmatter relationship indexing migration. * * Iron rule (regression guard for Bug 1, v0.14.0 upgrade night): phase * handlers must shell out to the bare string `gbrain`, NOT to * `process.execPath`. On bun-installed trees execPath is the bun runtime; * `bun extract ...` gets interpreted as `bun run extract` and the upgrade * crashes mid-migration. The canonical shim on PATH is the right target. */ import { describe, test, expect } from 'bun:test'; import { readFileSync } from 'fs'; import { join } from 'path'; const SRC_PATH = join(__dirname, '..', 'src', 'commands', 'migrations', 'v0_13_0.ts'); describe('v0.13.0 — Frontmatter relationship indexing migration', () => { test('registered in the TS migration registry', async () => { const { migrations, getMigration } = await import('../src/commands/migrations/index.ts'); const versions = migrations.map(m => m.version); expect(versions).toContain('0.13.0'); const m = getMigration('0.13.0'); expect(m).not.toBeNull(); expect(typeof m!.orchestrator).toBe('function'); }); test('phase functions exported for unit testing', async () => { const { __testing } = await import('../src/commands/migrations/v0_13_0.ts'); expect(typeof __testing.phaseASchema).toBe('function'); expect(typeof __testing.phaseBBackfill).toBe('function'); expect(typeof __testing.phaseCVerify).toBe('function'); }); test('dry-run skips all side-effect phases', async () => { const { v0_13_0 } = await import('../src/commands/migrations/v0_13_0.ts'); const result = await v0_13_0.orchestrator({ yes: true, dryRun: true, noAutopilotInstall: true }); expect(result.version).toBe('0.13.0'); for (const phase of result.phases) { expect(phase.status).toBe('skipped'); expect(phase.detail).toBe('dry-run'); } }); // ── Regression guards (Bug 1) ────────────────────────────── test('source does NOT reference process.execPath (Bug 1 regression)', () => { // process.execPath on a bun install is the bun runtime itself, so // `${process.execPath} extract` becomes `bun run extract` and dies. // See v0.14.0 upgrade-night postmortem. const src = readFileSync(SRC_PATH, 'utf-8'); expect(src).not.toContain('process.execPath'); }); test('source does NOT build commands from a GBRAIN constant (Bug 1 regression)', () => { // Earlier revisions used `const GBRAIN = process.execPath` and built // commands as `${GBRAIN} extract ...`. The constant was the vector. const src = readFileSync(SRC_PATH, 'utf-8'); expect(src).not.toMatch(/const\s+GBRAIN\s*=/); expect(src).not.toMatch(/\$\{GBRAIN\}/); }); test('phases use in-process schema + bare `gbrain` subprocess (v0.41.37.0 #1605)', () => { const src = readFileSync(SRC_PATH, 'utf-8'); // Schema bring-up is now IN-PROCESS (was execSync('gbrain init // --migrate-only'), which died with getaddrinfo ENOTFOUND on Windows). expect(src).toContain('runMigrateOnlyCore()'); expect(src).not.toContain("execSync('gbrain init --migrate-only'"); // Backfill extract goes through the stderr-capturing wrapper (still bare // `gbrain` so the canonical shim on PATH wins). expect(src).toContain("runGbrainSubprocess('gbrain extract links --source db --include-frontmatter'"); // Stats readback still shells out (reads stdout); bare gbrain. expect(src).toContain("execSync('gbrain call get_stats'"); }); test('phase commands never reference `bun` or `.ts` paths (Bug 1 regression)', () => { // Belt-and-suspenders: even if someone reintroduces a runtime-path // helper, they must not produce `bun ...` or `.ts` as the spawn // target. const src = readFileSync(SRC_PATH, 'utf-8'); expect(src).not.toMatch(/execSync\([^)]*\bbun\b/); expect(src).not.toMatch(/execSync\([^)]*\.ts/); }); });