mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-31 04:07:52 +00:00
Codex review finding (P3): the --once validation checked the derived `phase` value, but `phase` gets defaulted implicitly by --input (implies --phase synthesize) and --drain (implies --phase extract_atoms) BEFORE that check ran. So `gbrain dream --input <f> --once` and `gbrain dream --drain --once` both slipped past the "explicit --phase required" contract silently -- and --once became a true no-op in both cases: --drain returns from runDream before onceForPhase is ever read (the drain path doesn't call runCycle at all), and --input already bypasses the synthesize enabled-gate on its own via the existing opts.inputFile check, so onceForPhase would never even be consulted. Fix: capture `phaseWasExplicit = phaseIdx !== -1` at the very top of parseArgs, before the --input/--drain defaulting blocks run, and validate --once against that instead of the derived `phase`. Updated the usage-error message and --help text to say "an explicit --phase" so a user hitting this understands why `--input ... --once` doesn't count. Tests: 2 new pins in test/dream.test.ts exercising runDream directly (--input <file> --once exits 2; --drain --once exits 2), plus a structural test in dream-cli-flags.test.ts pinning that phaseWasExplicit is captured before both implicit-defaulting blocks. Updated the two existing structural/behavioral tests whose literal guard-condition / error-message assertions changed shape. Verified: dream-cli-flags.test.ts 27/27, dream.test.ts 31/31, typecheck clean. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
185 lines
7.6 KiB
TypeScript
185 lines
7.6 KiB
TypeScript
/**
|
|
* Structural tests for `gbrain dream` argv parsing (v0.21).
|
|
*
|
|
* Verifies the help text + parser source contains the new flags
|
|
* (--input, --date, --from, --to) and that conflict detection is wired.
|
|
* The actual parseArgs is internal; we exercise it via the source file
|
|
* structure to avoid spinning up a process per test.
|
|
*/
|
|
|
|
import { describe, test, expect } from 'bun:test';
|
|
import { readFileSync } from 'fs';
|
|
|
|
const dreamSrc = readFileSync(new URL('../src/commands/dream.ts', import.meta.url), 'utf-8');
|
|
|
|
describe('dream CLI flag wiring', () => {
|
|
test('declares --input flag with file argument', () => {
|
|
expect(dreamSrc).toContain("'--input'");
|
|
expect(dreamSrc).toContain('inputFile');
|
|
});
|
|
|
|
test('declares --date / --from / --to flags', () => {
|
|
expect(dreamSrc).toContain("'--date'");
|
|
expect(dreamSrc).toContain("'--from'");
|
|
expect(dreamSrc).toContain("'--to'");
|
|
});
|
|
|
|
test('validates ISO date format', () => {
|
|
expect(dreamSrc).toMatch(/ISO_DATE_RE/);
|
|
expect(dreamSrc).toContain('YYYY-MM-DD');
|
|
});
|
|
|
|
test('--input + --date conflict detection', () => {
|
|
expect(dreamSrc).toContain('--input cannot be combined with --date');
|
|
});
|
|
|
|
test('--input implies --phase synthesize', () => {
|
|
expect(dreamSrc).toContain("phase = 'synthesize'");
|
|
});
|
|
|
|
test('--from > --to range validation', () => {
|
|
expect(dreamSrc).toContain('empty range');
|
|
});
|
|
|
|
test('forwards synth fields to runCycle', () => {
|
|
expect(dreamSrc).toContain('synthInputFile');
|
|
expect(dreamSrc).toContain('synthDate');
|
|
expect(dreamSrc).toContain('synthFrom');
|
|
expect(dreamSrc).toContain('synthTo');
|
|
});
|
|
|
|
test('totals line includes synth + patterns counters', () => {
|
|
expect(dreamSrc).toContain('synth_transcripts');
|
|
expect(dreamSrc).toContain('synth_pages');
|
|
expect(dreamSrc).toContain('patterns=');
|
|
});
|
|
|
|
test('help text documents dry-run synthesis semantics (Codex finding #8)', () => {
|
|
expect(dreamSrc).toContain('skips the Sonnet');
|
|
expect(dreamSrc.toLowerCase()).toContain('zero llm calls');
|
|
});
|
|
|
|
// v0.41.13: --source / --source-id flag wiring (supersedes PR #1559).
|
|
// Structural-only tests; behavioral tests live in test/dream.test.ts.
|
|
describe('--source / --source-id wiring (v0.41.13)', () => {
|
|
test('declares --source flag in argv parsing', () => {
|
|
expect(dreamSrc).toContain("'--source'");
|
|
});
|
|
|
|
test('declares --source-id alias in argv parsing', () => {
|
|
expect(dreamSrc).toContain("'--source-id'");
|
|
});
|
|
|
|
test('forwards resolved sourceId to runCycle', () => {
|
|
// The runCycle call must pass sourceId; gate name "sourceId"
|
|
// not "source" because CycleOpts.sourceId is the contract.
|
|
expect(dreamSrc).toMatch(/sourceId:\s*resolvedSourceId/);
|
|
});
|
|
|
|
test('imports resolveSourceId from canonical source-resolver helper', () => {
|
|
expect(dreamSrc).toContain("from '../core/source-resolver.ts'");
|
|
expect(dreamSrc).toContain('resolveSourceId');
|
|
});
|
|
|
|
test('declares isResolverUserError predicate for typed-error catch (T3 from eng review)', () => {
|
|
expect(dreamSrc).toContain('function isResolverUserError');
|
|
});
|
|
|
|
test('documents --source in --help output', () => {
|
|
expect(dreamSrc).toContain('--source <id>');
|
|
expect(dreamSrc).toContain('--source-id <id>');
|
|
});
|
|
|
|
test('preserves --help short-circuit ordering comment (IRON RULE)', () => {
|
|
// The comment lives in runDream BEFORE the engine-null gate.
|
|
// Future refactors that reorder these blocks will trip this guard.
|
|
expect(dreamSrc).toContain('IRON RULE: --help short-circuits BEFORE');
|
|
});
|
|
|
|
test('declares engine-null guard for --source', () => {
|
|
expect(dreamSrc).toContain('requires a connected brain');
|
|
});
|
|
|
|
test('declares archived-source guard', () => {
|
|
expect(dreamSrc).toMatch(/source.*is archived/);
|
|
expect(dreamSrc).toContain('gbrain sources restore');
|
|
});
|
|
});
|
|
|
|
// issue #1678 — --drain bounded backlog drain wiring (structural).
|
|
describe('--drain wiring', () => {
|
|
test('declares --drain and --window flags', () => {
|
|
expect(dreamSrc).toContain("'--drain'");
|
|
expect(dreamSrc).toContain("'--window'");
|
|
expect(dreamSrc).toContain('windowSeconds');
|
|
});
|
|
|
|
test('--drain defaults to extract_atoms and rejects other phases', () => {
|
|
expect(dreamSrc).toContain("phase = 'extract_atoms'");
|
|
expect(dreamSrc).toContain('--drain currently supports only --phase extract_atoms');
|
|
});
|
|
|
|
test('drain routes through the shared helper with the resolved source (5A)', () => {
|
|
// v0.42.10.0 (#1685 GAP D / 5A): the lock+batch+count wiring moved into
|
|
// runExtractAtomsDrainForSource so the CLI, the Minion handler, and
|
|
// autopilot share ONE drain path. dream threads resolvedSourceId so the
|
|
// helper picks cycleLockIdFor(resolvedSourceId) — the same lock the routine
|
|
// cycle holds for that source. The lock-id contract is now pinned in
|
|
// test/extract-atoms-drain.test.ts ("shared wiring helper holds the cycle lock").
|
|
expect(dreamSrc).toContain('runExtractAtomsDrainForSource');
|
|
expect(dreamSrc).toContain('sourceId: resolvedSourceId');
|
|
});
|
|
|
|
test('drain reports remaining + exits non-zero when incomplete', () => {
|
|
expect(dreamSrc).toContain('EXIT_DRAIN_INCOMPLETE');
|
|
expect(dreamSrc).toContain('cycle_already_running');
|
|
});
|
|
});
|
|
|
|
// issue #2860 — --once one-shot phase-enabled-gate bypass (structural).
|
|
// Behavioral coverage: test/e2e/dream-patterns-pglite.test.ts (bypass +
|
|
// config-untouched) and test/core/cycle.serial.test.ts (non-leak across
|
|
// phases via CycleOpts.onceForPhase).
|
|
describe('--once wiring (issue #2860)', () => {
|
|
test('declares --once flag', () => {
|
|
expect(dreamSrc).toContain("'--once'");
|
|
});
|
|
|
|
test('rejects bare --once with no --phase (exit 2)', () => {
|
|
expect(dreamSrc).toContain('--once requires an explicit --phase <name>');
|
|
// --help must short-circuit this validation (Codex review finding) —
|
|
// see the "--help --once" test in test/dream.test.ts for the
|
|
// behavioral pin of this exact ordering.
|
|
expect(dreamSrc).toContain('if (once && !phaseWasExplicit && !wantsHelp)');
|
|
});
|
|
|
|
// Codex P3 finding: the derived `phase` value gets populated by
|
|
// --input/--drain BEFORE this validation used to run, so those two
|
|
// silently slipped past an `!phase`-based check. The fix validates
|
|
// against `phaseWasExplicit` (captured at `phaseIdx !== -1`, before
|
|
// any implicit defaulting) instead. Behavioral pins live in
|
|
// test/dream.test.ts.
|
|
test('validates against phaseWasExplicit, captured before --input/--drain defaulting', () => {
|
|
expect(dreamSrc).toContain('const phaseWasExplicit = phaseIdx !== -1;');
|
|
// Must be declared before the --input-implies-synthesize and
|
|
// --drain-implies-extract_atoms defaulting blocks so it captures
|
|
// presence prior to any implicit phase assignment.
|
|
const explicitIdx = dreamSrc.indexOf('const phaseWasExplicit = phaseIdx !== -1;');
|
|
const inputImpliesIdx = dreamSrc.indexOf("phase = 'synthesize'");
|
|
const drainImpliesIdx = dreamSrc.indexOf("phase = 'extract_atoms'");
|
|
expect(explicitIdx).toBeGreaterThan(-1);
|
|
expect(explicitIdx).toBeLessThan(inputImpliesIdx);
|
|
expect(explicitIdx).toBeLessThan(drainImpliesIdx);
|
|
});
|
|
|
|
test('threads onceForPhase to runCycle, gated on opts.once', () => {
|
|
expect(dreamSrc).toMatch(/onceForPhase:\s*opts\.once\s*\?\s*opts\.phase!\s*:\s*undefined/);
|
|
});
|
|
|
|
test('documents --once in --help output', () => {
|
|
expect(dreamSrc).toContain('--once');
|
|
expect(dreamSrc).toContain('Never reads or writes config');
|
|
});
|
|
});
|
|
});
|