Files
gbrain/test/migrations-v0_13_0.test.ts
T
fcf40a12fc fix: v0.15.4 — PgBouncer prepare:false for Supabase transaction pooler (closes #284, #286, #270) (#301)
* fix(migrate): v0_13_0 shells out to `gbrain` shim, not `process.execPath`

On bun-installed trees, process.execPath is the bun runtime itself.
`bun extract links ...` got reinterpreted as `bun run extract` and
crashed the upgrade mid-Phase B. The canonical shim on PATH already
wraps the right runtime+entrypoint; trust it.

Regression-guarded by test/migrations-v0_13_0.test.ts which greps
the source for `process.execPath` and `bun` invocations. This was
Bug 1 of tonight's v0.13 → v0.14 upgrade-night postmortem.

* fix(autopilot): resolveGbrainCliPath prefers shim, never returns .ts

argv[1] check used to short-circuit on /cli.ts, so bun-source installs
got a .ts path back. spawn() then failed EACCES because TypeScript
source isn't executable, and autopilot silently lost its worker.

Reordered probes: which gbrain (shim) first, then compiled execPath,
then argv[1] only if it ends in /gbrain. Deleted the .ts branch
entirely — no valid case exists.

Rewrote the existing test that enshrined the buggy .ts return.
Critical regression guard: resolver MUST NEVER return a .ts path
across any combination of argv[1] + execPath + shim availability.
This was Bug 4 of tonight's v0.13 → v0.14 upgrade-night postmortem.

* chore: bump version and changelog (v0.15.3)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(db): resolvePrepare() helper for PgBouncer transaction-mode pools

Adds port-6543 auto-detect with a 4-level precedence chain:
GBRAIN_PREPARE env var → ?prepare= URL param → port auto-detect → default.
Wires into the module-singleton connect() so the main CLI path no longer
hits "prepared statement does not exist" against Supabase transaction
pooler. Returns boolean | undefined; undefined means omit the option and
let postgres.js default (true) stand.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(postgres-engine): honor resolvePrepare in worker-instance pool

Without this, \`gbrain jobs work\` against a Supabase pooler URL hits
"prepared statement does not exist" under load even after the module
singleton was fixed in db.ts. Community PR #270 (@notjbg) caught this
second path that #284 had missed. Reuses the shared helper, no regex
duplication.

Co-Authored-By: Jonah Berg <jonah.berg.g@gmail.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(doctor): pgbouncer_prepare check

URL-only check (no DB roundtrip) that reads the configured URL via
loadConfig() and flags the footgun: port 6543 with prepared statements
still enabled. Warns with the exact env override (GBRAIN_PREPARE=false)
and URL-query alternative (?prepare=false). Works for both the module
singleton and worker-instance engines.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test: resolvePrepare precedence matrix + postgres-engine wiring guard

- test/resolve-prepare.test.ts: 11 cases covering env override, URL
  query param, port auto-detect, malformed URLs, postgres:// scheme,
  URL-encoded credentials. Uses bun:test — #284's original vitest file
  would never have run in this project.
- test/postgres-engine.test.ts: new source-level grep case asserting
  the worker-pool connect() branch calls db.resolvePrepare(url) and
  includes a typeof prepare === 'boolean' check. Mirrors the existing
  SET LOCAL regression guard. If anyone rips out the wiring, the build
  fails before shipping starts dropping rows.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: bump version and changelog (v0.15.4)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Jonah Berg <jonah.berg.g@gmail.com>
2026-04-21 18:35:45 -07:00

80 lines
3.6 KiB
TypeScript

/**
* 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 });
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('phase commands invoke bare `gbrain` shell-out (Bug 1 fix)', () => {
const src = readFileSync(SRC_PATH, 'utf-8');
// All three phases shell out to bare `gbrain` so the canonical shim
// on PATH wins. This is the shape v0_12_0 has always used.
expect(src).toContain("execSync('gbrain init --migrate-only'");
expect(src).toContain("execSync('gbrain extract links --source db --include-frontmatter'");
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 `<path>.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/);
});
});