Files
gbrain/test/autopilot-resolve-cli.test.ts
Garry Tan 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

84 lines
3.4 KiB
TypeScript

/**
* Tests for resolveGbrainCliPath() — picks the right executable to supervise
* as the Minions worker child.
*
* Iron rule (regression guard for Bug 4, v0.14.0 upgrade night): the resolver
* must NEVER return a `.ts` path. TypeScript source files are not executable;
* spawning them fails with EACCES and autopilot silently loses its worker.
* Earlier versions short-circuited on `argv[1].endsWith('/cli.ts')`, which
* caused the bug. The canonical resolution is the `gbrain` shim on PATH.
*/
import { describe, test, expect } from 'bun:test';
import { resolveGbrainCliPath } from '../src/commands/autopilot.ts';
describe('resolveGbrainCliPath', () => {
test('returns a non-empty string or throws with a clear install hint', () => {
let path: string;
try {
path = resolveGbrainCliPath();
} catch (e) {
// Machine without gbrain on PATH and no compiled binary: throw is
// expected. The error message must point the user at the install step.
expect((e as Error).message).toMatch(/PATH|resolve/i);
return;
}
expect(typeof path).toBe('string');
expect(path.length).toBeGreaterThan(0);
});
test('NEVER returns a path ending in .ts (regression guard — Bug 4)', () => {
// Simulate the exact production break: bun-source install puts
// `/path/to/src/cli.ts` in argv[1]. The resolver must not hand that back.
const origArg1 = process.argv[1];
const origExec = (process as { execPath?: string }).execPath;
process.argv[1] = '/some/project/src/cli.ts';
try {
const path = resolveGbrainCliPath();
// Either we got a real executable (shim on PATH from the test machine)
// or the throw path fires. Either way, the return value is never .ts.
expect(path.endsWith('.ts')).toBe(false);
expect(path.endsWith('.tsx')).toBe(false);
} catch (e) {
expect((e as Error).message).toMatch(/PATH|resolve/i);
} finally {
process.argv[1] = origArg1;
if (origExec) (process as { execPath?: string }).execPath = origExec;
}
});
test('shim on PATH wins over argv[1]=cli.ts', () => {
// If `which gbrain` resolves (most dev machines), the resolver should
// return that shim path, not argv[1]=cli.ts. This is the canonical
// install shape.
const origArg1 = process.argv[1];
process.argv[1] = '/some/project/src/cli.ts';
try {
const path = resolveGbrainCliPath();
// On a machine where `which gbrain` resolves, path ends in /gbrain.
// On a machine without, we throw. Both outcomes prove the resolver
// did not short-circuit on the .ts suffix.
expect(path.endsWith('/cli.ts')).toBe(false);
} catch (e) {
expect((e as Error).message).toMatch(/PATH|resolve/i);
} finally {
process.argv[1] = origArg1;
}
});
test('accepts argv[1]=/gbrain when shim is absent (compiled binary)', () => {
// If the machine has neither shim nor compiled exec, but argv[1]
// happens to be a literal /gbrain path (direct invocation), accept it.
const origArg1 = process.argv[1];
process.argv[1] = '/usr/local/bin/gbrain';
try {
const path = resolveGbrainCliPath();
// On a machine with `which gbrain`, we get the shim. On a machine
// without, argv[1] fallback fires. Either way the result is valid.
expect(path.endsWith('/gbrain') || path.endsWith('\\gbrain.exe')).toBe(true);
} finally {
process.argv[1] = origArg1;
}
});
});