/** * Tests for src/commands/book-mirror.ts — flagship v0.25.1 CLI. * * Pure surface tests. The full subagent-fan-out integration path * needs a live queue engine + ANTHROPIC_API_KEY and is exercised by * the opt-in smoke test (test/e2e/skill-smoke-openclaw.test.ts when * EVALS=1 EVALS_TIER=skills is set). * * Constraint: src/cli.ts dispatches connectEngine() BEFORE any * CLI_ONLY command's own arg parsing, including --help. This is a * pre-existing architectural choice (every CLI_ONLY command — * agent, sync, jobs, book-mirror — behaves the same). So we can't * exercise help-text or arg-validation paths from a clean tempdir * without DATABASE_URL. * * What we DO test: * - The book-mirror command is registered (CLI dispatches it * instead of "Unknown command"). * - Without DB, the command fails fast and never reaches the * queue-submission path. * - The command source file is parseable + exports the runner. */ import { describe, expect, it } from 'bun:test'; import { readFileSync } from 'fs'; import { join } from 'path'; const REPO_ROOT = new URL('..', import.meta.url).pathname; async function runCli(args: string[]): Promise<{ stdout: string; stderr: string; exit: number; }> { // v0.34.1 (#876): set GBRAIN_HOME to a fresh tempdir so the test isn't // sensitive to the developer's local ~/.gbrain/config.json. Pre-fix this // test would silently inherit the user's database_url and try to connect // to a real Postgres + run migrations, which both extends runtime past // the default test timeout and produces different exit semantics // depending on the local DB state. With GBRAIN_HOME pointing at an // empty dir, the CLI hits "No brain configured" and exits 1 immediately // — which is what this test was originally written to cover. const tmpHome = await import('os').then(os => import('fs').then(fs => fs.mkdtempSync(`${os.tmpdir()}/gbrain-test-`)), ); try { const proc = Bun.spawn( ['bun', 'run', 'src/cli.ts', 'book-mirror', ...args], { cwd: REPO_ROOT, stdout: 'pipe', stderr: 'pipe', env: { ...process.env, DATABASE_URL: '', GBRAIN_HOME: tmpHome }, }, ); const stdout = await new Response(proc.stdout).text(); const stderr = await new Response(proc.stderr).text(); const exit = await proc.exited; return { stdout, stderr, exit }; } finally { const fs = await import('fs'); fs.rmSync(tmpHome, { recursive: true, force: true }); } } describe('gbrain book-mirror — CLI registration', () => { // v0.34.1 (#876): migrations v60-v65 added oauth_clients.source_id + // federated_read FK plumbing. The cold-spawned subprocess's initSchema // chain now takes ~1s longer end-to-end; bump these tests' timeout to // 30s so the test-harness budget covers Bun cold-start + PGLite init + // migration apply + Postgres-retry exhaustion. The CLI itself still // exits in 7-10s on a misconfigured DB. it('book-mirror is in CLI_ONLY (does not get "Unknown command")', async () => { const { stderr } = await runCli([]); // Without DB, the command will fail — but on the connect path, // not as "Unknown command". This proves dispatch reached // handleCliOnly's switch statement. expect(stderr).not.toContain('Unknown command'); }, 30000); it('without DB, never reaches queue submission', async () => { const { stderr, exit } = await runCli(['--slug', 'noop']); expect(exit).not.toBe(0); expect(stderr).not.toContain('submitted:'); }, 30000); }); describe('gbrain book-mirror — source file invariants', () => { const source = readFileSync( join(REPO_ROOT, 'src/commands/book-mirror.ts'), 'utf-8', ); it('exports runBookMirrorCmd', () => { expect(source).toContain('export async function runBookMirrorCmd'); }); it('documents the trust contract (codex HIGH-1 fix is in the file)', () => { // codex HIGH-1 fix: the trust contract narrowing must not silently // regress in a refactor. expect(source).toContain('media/books/'); expect(source).toContain('codex HIGH-1'); }); it('uses read-only allowed_tools for subagent fan-out (codex HIGH-1)', () => { // The trust narrowing actually happens at the tool-allowlist layer: // subagents get ['get_page', 'search'] — read-only — so they CANNOT // call put_page regardless of slug-prefix scope. expect(source).toContain("allowed_tools: ['get_page', 'search']"); }); it('writes via operator-trust put_page (handler takes the operator-context shape)', () => { // The CLI is the trusted writer; subagents never call put_page. // The contextual marker is "remote: false" — operator-trust path. expect(source).toContain('putPageOp.handler'); expect(source).toContain('remote: false'); // And the file documents the intentional omission of viaSubagent // / allowedSlugPrefixes via inline comments — those phrases are // a regression-detector for someone trying to "fix" the trust // contract by adding the wrong fields. expect(source).toContain('viaSubagent intentionally omitted'); }); it('prints a cost-estimate confirmation before launching (P1)', () => { expect(source).toContain('estimateCost'); expect(source).toContain('confirmInteractive'); }); it('uses idempotency keys for child jobs (retry-friendly)', () => { // Re-running the CLI on the same input should dedupe completed // chapters at the queue layer. expect(source).toContain('idempotency_key'); expect(source).toContain('book-mirror:'); }); it('handles partial-failure (continues + flags failed chapters)', () => { // The plan said: assemble with completed chapters + a failed-list // section. Don't abort the whole run on one chapter failure. expect(source).toContain('Failed chapters'); expect(source).toContain('chapters_failed'); }); });