* 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>
84 lines
3.4 KiB
TypeScript
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;
|
|
}
|
|
});
|
|
});
|