Files
gbrain/test/autopilot-resolve-cli.test.ts
T
Garry TanandClaude Opus 4.7 ec0556a66e feat(autopilot): Minions dispatch + worker spawn supervisor + async shutdown
Autopilot now dispatches each cycle as a single `autopilot-cycle` Minion
job (with idempotency_key on the cycle slot) instead of running steps
inline. A forked `gbrain jobs work` child drains the queue durably,
supervised by autopilot. The user runs ONE install step
(`gbrain autopilot --install`) and gets sync + extract + embed + backlinks
+ durable job processing, with no separate worker daemon to manage.

Mode selection:
  - minion_mode=always OR pain_triggered (default), engine=postgres →
    Minions dispatch. Spawn child, submit autopilot-cycle each interval.
  - minion_mode=off, OR engine=pglite, OR `--inline` flag → run steps
    inline in-process, same as pre-v0.11.1. PGLite has an exclusive file
    lock that blocks a second worker process, so the inline path is the
    only path that works there.

Worker supervision:
  - spawn(resolveGbrainCliPath(), ['jobs', 'work'], { stdio: 'inherit' }).
    stdio:'inherit' avoids pipe-buffer blocking (Codex architecture #2).
  - On worker exit: 10s backoff + restart. Crash counter caps at 5 →
    autopilot stops with a clear error.
  - resolveGbrainCliPath() prefers argv[1] (cli.ts / /gbrain), then
    process.execPath (compiled binary suffix check), then `which gbrain`
    (installed to $PATH). NEVER blindly uses process.execPath, which on
    source installs is the Bun runtime, not `gbrain` (Codex architecture
    #1).

Shutdown:
  - Async SIGTERM/SIGINT handler: sends SIGTERM to worker, awaits its
    exit for up to 35s (the worker's own drain is 30s; we add buffer for
    signal-delivery latency), then SIGKILL if still alive.
  - Drops the old `process.on('exit')` lock-cleanup handler — its
    callback runs synchronously and can't wait for the worker drain.
    Lock file cleanup moved inside the async shutdown.

Lock-file mtime refresh every cycle (Codex C) so a long-lived autopilot
doesn't get declared "stale" by the next cron-fired invocation after 10
minutes.

Inline fallback path calls the new Core fns (runExtractCore, runEmbedCore)
instead of the CLI wrappers. That way a bad arg from inside the loop
can't process.exit() the autopilot itself (matches Codex #5).

test/autopilot-resolve-cli.test.ts: 3 tests covering argv[1]-as-gbrain,
argv[1]-as-cli.ts, and graceful error when no path resolves.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 13:35:51 +08:00

54 lines
1.9 KiB
TypeScript

/**
* Tests for resolveGbrainCliPath() — picks the right executable to supervise
* as the Minions worker child. Codex caught that the earlier plan's use of
* process.execPath is wrong on source installs (points at the Bun runtime,
* not `gbrain`).
*/
import { describe, test, expect } from 'bun:test';
import { resolveGbrainCliPath } from '../src/commands/autopilot.ts';
describe('resolveGbrainCliPath', () => {
test('returns a non-empty string', () => {
// Whatever the test environment is (bun run ...), the resolver should
// find *something* — either argv[1] (cli.ts entry), execPath (compiled
// binary), or `which gbrain`. If none of those work, it throws; in
// test, argv[1] is the test runner path which usually ends in .ts, so
// the first branch or the `which` fallback catches it.
let path: string;
try {
path = resolveGbrainCliPath();
} catch (e) {
// If we throw, that means neither argv[1] nor execPath nor $PATH has
// gbrain — on a machine without gbrain installed, this is expected.
expect((e as Error).message).toContain('resolve');
return;
}
expect(typeof path).toBe('string');
expect(path.length).toBeGreaterThan(0);
});
test('accepts /gbrain suffix (compiled binary)', () => {
// Simulate compiled-binary detection by setting argv[1] to /usr/local/bin/gbrain
const orig = process.argv[1];
process.argv[1] = '/usr/local/bin/gbrain';
try {
const path = resolveGbrainCliPath();
expect(path).toBe('/usr/local/bin/gbrain');
} finally {
process.argv[1] = orig;
}
});
test('accepts /cli.ts suffix (source install)', () => {
const orig = process.argv[1];
process.argv[1] = '/some/path/src/cli.ts';
try {
const path = resolveGbrainCliPath();
expect(path).toBe('/some/path/src/cli.ts');
} finally {
process.argv[1] = orig;
}
});
});