mirror of
https://github.com/garrytan/gbrain.git
synced 2026-08-14 00:48:18 +00:00
* fix(cli): --help required a configured brain for five commands `gbrain models --help` on a machine that has not run `gbrain init` exits 1 with "No brain configured. Run: gbrain init". So do watch, skillopt, maintain and extract-conversation-facts. That is the state a reader is most likely to be in when they reach for --help. Mechanism: cli.ts's per-command --help block prints the generic usage stub only for CLI_ONLY commands NOT in CLI_ONLY_SELF_HELP. Members of that set are meant to print their own help, so they fall through to the normal dispatch -- which connects the engine before the handler runs. Each of these five already opens with a --help branch (models honours it first by explicit comment, watch and skillopt on line 1 of the handler); the connect gate made that branch unreachable. Dispatched pre-connect now, through a small table keyed by command. The engine is never read on the help path, so a placeholder is passed rather than widening five signatures -- skillopt already declares `BrainEngine | null` for this reason. Measured on130d321dwith an empty GBRAIN_HOME: before: models/watch/skillopt/maintain/extract-conversation-facts -> exit 1, 0 lines after: exit 0, 15 / 21 / 94 / 11 / 43 lines of their own help Seven other members of the set (brainstorm, config, embed, lsd, migrate, pages, retrieval-upgrade) still need a brain, because their handlers have no --help branch to reach. Fixing those means either writing help or dropping them from CLI_ONLY_SELF_HELP so the generic stub answers -- a call for the maintainer, not something to guess at here. They are pinned in the test as a tripwire rather than omitted, so the coverage list cannot drift in either direction: fixing one fails the test until its entry moves. The guard runs the CLI with an empty GBRAIN_HOME and asserts exit 0 plus real help output. Behaviour, not a declaration -- membership in CLI_ONLY_SELF_HELP was itself unverified, which is how five commands sat in a set whose whole meaning is "prints its own help" while printing none. Verification on130d321d(v0.42.76.0): - bun test cli + cli-flag-validation + cli-help-without-brain -> 54 pass / 0 fail - red check: with upstream/master's cli.ts -> 5 fail / 1 pass - bun run typecheck -> clean - bun run verify -> 34/34 green - bun run build:flag-registry -> no diff Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(cli): stop the no-brain guard inheriting a database URL Codex: an empty GBRAIN_HOME is not sufficient. loadConfig also honours GBRAIN_DATABASE_URL and DATABASE_URL (config.ts:550-551), so a developer or CI runner exporting either would let the CLI connect, the positive assertions would pass on master, and the guard would be inert. Both are now unset for the spawned process. Verified the guard still regresses under the leak it was vulnerable to: with DATABASE_URL exported and upstream/master's cli.ts, the run is 5 fail / 1 pass. Verification on130d321d: 6 pass / 0 fail, typecheck clean, verify 34/34. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(test): type the spawn env so the delete compiles process.env is narrowly typed here, so deleting GBRAIN_DATABASE_URL / DATABASE_URL off a spread of it fails tsc with TS2339. Declared as Record<string, string | undefined>. My previous commit was pushed with this error present — typecheck and verify were run after the push, not before. Verified now: - bun run typecheck -> clean - bun run verify -> 34/34 green - bun test cli + cli-flag-validation + cli-help-without-brain -> 54 pass / 0 fail Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(cli): spawn with --no-env-file so a local .env cannot re-inject the URL Codex: unsetting GBRAIN_DATABASE_URL / DATABASE_URL on the spawn env is not enough, because bun auto-loads .env from cwd and GBRAIN_DATABASE_URL is honored unconditionally (config.ts:550). A developer's local .env would put back exactly what the deletes removed, and the guard would go inert. Flag semantics confirmed from `bun --help`: '--no-env-file Disable automatic loading of .env files'. Verified: typecheck clean, verify 34/34, guard 6 pass / 0 fail, and still 5 fail / 1 pass against upstream/master's cli.ts. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(cli): run the tripwire spawns concurrently Seven sequential CLI spawns were most of this file's wall clock and each is independent (own temp GBRAIN_HOME). 4.05s -> 2.87s locally; the saving is larger on a loaded CI runner. Gates run BEFORE this push, unlike twice earlier on this branch: typecheck clean, verify 34/34, 6 pass / 0 fail, and still 5 fail / 1 pass against upstream/master's cli.ts. Note on the shard 9 failure on the previous run: it exited 123 with no reported test failure, the last output being an unrelated capture test hitting a placeholder API key. Shard 9 passes locally with this change, and passes on three sibling PRs off the same base, so I have not been able to attribute it. Pushing this change re-runs it; if it reproduces in the same place the cause is here and I will chase it rather than re-run again. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(cli): make the guard serial so it does not re-shard the suite Root cause of the shard 9 failure on the two previous runs, chased rather than re-run as promised. scripts/run-unit-shard.sh documents 'hash-disjoint' sharding but assigns by sorted-index modulo: `i % shard_m + 1`. Adding ONE test file shifts every index after it, so 933 of 1103 files change shard. Shard 9 kept capture-runcapture.test.ts but got a different set of neighbours, and that file does a real embed attempt — under CI's placeholder OPENAI_API_KEY it reached the network, failed, and took the shard process down with exit 123 and no reported test failure. Not my code's behaviour: capture-runcapture passes in isolation with my cli.ts AND with master's (26 pass each), and shard 9 passed on four sibling PRs off the same base that add no test file. Any PR adding a sharded test file can surface this. Serial files are excluded from the partition (`-not -name '*.serial.test.ts'`), so the sharded set is back to master's 1103 files and shard 9 to its original composition. Serial is also the honest classification: this guard spawns twelve CLI subprocesses. Gates before push: typecheck clean, verify 34/34, 6 pass / 0 fail, still 5 fail / 1 pass against upstream/master's cli.ts. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
100 lines
3.8 KiB
TypeScript
100 lines
3.8 KiB
TypeScript
/**
|
|
* `--help` must be answerable with no brain configured.
|
|
*
|
|
* A reader runs `--help` most often right after install, before `gbrain init`.
|
|
* CLI_ONLY_SELF_HELP members skip the dispatcher's generic usage stub (that is
|
|
* the point — they print their own), but they were then reached through the
|
|
* normal dispatch, which connects the engine first. So on a machine with no
|
|
* brain they exited 1 with "No brain configured" and their own help block was
|
|
* unreachable code.
|
|
*
|
|
* The oracle here is behaviour, not a declaration: each command is actually
|
|
* run with an empty GBRAIN_HOME.
|
|
*/
|
|
import { describe, test, expect } from 'bun:test';
|
|
import { mkdtempSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
|
|
const REPO = new URL('..', import.meta.url).pathname;
|
|
|
|
/** Answer `--help` before touching the engine. */
|
|
const HELP_WITHOUT_BRAIN = [
|
|
'models',
|
|
'watch',
|
|
'skillopt',
|
|
'maintain',
|
|
'extract-conversation-facts',
|
|
];
|
|
|
|
/**
|
|
* Self-help members that still need a brain for `--help`, because their handler
|
|
* has no `--help` branch to reach. Pinned rather than omitted so the list can
|
|
* only shrink deliberately: writing help for one of these, or dropping it from
|
|
* CLI_ONLY_SELF_HELP so the generic stub answers, fails this test until the
|
|
* entry moves.
|
|
*/
|
|
const STILL_NEEDS_A_BRAIN = [
|
|
'brainstorm',
|
|
'config',
|
|
'embed',
|
|
'lsd',
|
|
'migrate',
|
|
'pages',
|
|
'retrieval-upgrade',
|
|
];
|
|
|
|
async function runHelp(command: string): Promise<{ code: number; out: string }> {
|
|
const home = mkdtempSync(join(tmpdir(), 'gbrain-nobrain-'));
|
|
// An empty GBRAIN_HOME is not enough: loadConfig also honours
|
|
// GBRAIN_DATABASE_URL and DATABASE_URL (config.ts:550-551), so a developer
|
|
// or CI runner that exports either would let the CLI connect anyway — the
|
|
// positive assertions would pass on master and this guard would be inert.
|
|
const env: Record<string, string | undefined> = { ...process.env, GBRAIN_HOME: home };
|
|
delete env.GBRAIN_DATABASE_URL;
|
|
delete env.DATABASE_URL;
|
|
// --no-env-file: bun auto-loads .env from cwd, and GBRAIN_DATABASE_URL is
|
|
// honored unconditionally, so a developer's local .env would put back
|
|
// exactly what the deletes above removed.
|
|
const proc = Bun.spawn(['bun', '--no-env-file', 'run', 'src/cli.ts', command, '--help'], {
|
|
cwd: REPO,
|
|
env,
|
|
stdout: 'pipe',
|
|
stderr: 'pipe',
|
|
});
|
|
const [stdout, stderr] = await Promise.all([
|
|
new Response(proc.stdout).text(),
|
|
new Response(proc.stderr).text(),
|
|
]);
|
|
const code = await proc.exited;
|
|
return { code, out: stdout + stderr };
|
|
}
|
|
|
|
describe('--help without a configured brain', () => {
|
|
for (const command of HELP_WITHOUT_BRAIN) {
|
|
test(`${command} --help answers`, async () => {
|
|
const { code, out } = await runHelp(command);
|
|
expect(code).toBe(0);
|
|
expect(out).not.toContain('No brain configured');
|
|
// Real help, not a one-line stub or an empty exit.
|
|
expect(out.split('\n').filter(l => l.trim()).length).toBeGreaterThan(3);
|
|
expect(out.toLowerCase()).toContain(command.split('-')[0]);
|
|
}, 30_000);
|
|
}
|
|
|
|
test('the known-unfixed set is exactly what it claims', async () => {
|
|
// Concurrently: seven sequential CLI spawns is most of this file's wall
|
|
// clock, and each one is independent (its own temp GBRAIN_HOME).
|
|
const results = await Promise.all(
|
|
STILL_NEEDS_A_BRAIN.map(async command => ({ command, ...(await runHelp(command)) })),
|
|
);
|
|
const unexpectedlyWorking = results
|
|
.filter(r => !r.out.includes('No brain configured'))
|
|
.map(r => r.command);
|
|
// Not a wish that they stay broken — a tripwire. Fixing one is good and
|
|
// should move it to HELP_WITHOUT_BRAIN in the same change, so the coverage
|
|
// list never drifts away from reality in either direction.
|
|
expect(unexpectedlyWorking).toEqual([]);
|
|
}, 90_000);
|
|
});
|