#!/usr/bin/env node // E2E fake `claude` CLI — the metered digest lane's engine. // // Unlike the ollama stub (an HTTP server, because ollama-direct talks HTTP), // claude-code IS a subprocess, so this follows the existing fake-binary idiom: // a .mjs in e2e/fixtures/bin/ wired in through an env var (CLAUDE_BIN). // // Mimics what digestApps.ts's claudeCode.run() actually invokes and parses: // claude -p --output-format json [--model ] with the prompt on STDIN // and expects on stdout the CLI's own JSON WRAPPER, whose `result` field is a // STRING containing the model's JSON body. Reproducing both layers matters — // the double parse is exactly where a real integration breaks. // // There is no constrained decoding over the CLI, so this deliberately emits the // contract by hand, the same way the real lane depends on the parser's guards // rather than on a schema. import { readFileSync } from "node:fs"; import { installFixtureWatchdog } from "./_watchdog.mjs"; // Never outlive the run that spawned us — see _watchdog.mjs. installFixtureWatchdog(); const argv = process.argv.slice(2); function flag(name) { const i = argv.indexOf(name); return i < 0 ? undefined : argv[i + 1]; } // The reachability probe digestBatch runs before touching 74k videos — it // shells out to `claude --version` and treats a non-zero exit as "engine down". // Answering it is not optional: without this the metered lane fails at the probe // and never reaches the code the rest of this fake exists to exercise. if (argv.includes("--version")) { process.stdout.write("1.0.0 (fake-claude)\n"); process.exit(0); } // Fail loudly if the invocation drifts: a fake that accepts anything stops // testing the thing it exists to test. if (!argv.includes("-p") || flag("--output-format") !== "json") { process.stderr.write( `[fake-claude] unexpected argv: ${JSON.stringify(argv)}\n`, ); process.exit(2); } let stdin = ""; try { stdin = readFileSync(0, "utf8"); } catch { stdin = ""; } function hms(total) { const n = Math.max(0, Math.floor(total)); return [Math.floor(n / 3600), Math.floor((n % 3600) / 60), n % 60] .map((v) => String(v).padStart(2, "0")) .join(":"); } function toSeconds(clock) { const m = /^(\d\d):(\d\d):(\d\d)$/.exec(clock); return m ? Number(m[1]) * 3600 + Number(m[2]) * 60 + Number(m[3]) : null; } const m = /This section covers (\d\d:\d\d:\d\d) to (\d\d:\d\d:\d\d)/.exec(stdin); const start = m ? (toSeconds(m[1]) ?? 0) : 0; const end = m ? (toSeconds(m[2]) ?? 600) : 600; const span = Math.max(1, end - start); const body = /"tags"/.test(stdin) ? { tags: ["metered lane", "court filings", "podcast"] } : { chapters: [ { start: hms(start), title: "Metered lane opening remarks" }, { start: hms(start + Math.floor(span / 3)), title: "Ruling analysis" }, ], }; process.stdout.write( JSON.stringify({ type: "result", is_error: false, // The inner body is a STRING, exactly as the real CLI reports it. result: JSON.stringify(body), total_cost_usd: 0.0123, usage: { input_tokens: 1000, output_tokens: 120 }, }), );