gitoriaLog in with ident

antcolony

All repositories: gitoria

ReadmeCodePull requestsReleasesTicketsSettings
Main branchmain3a4d0324antcolony#37: a too-long report gets up to 3 fix tries, finished work is never thrown away for lengthmremain/tests/fake-claude.mjs

23.9 KB

  1. #!/usr/bin/env node
  2. // tests/fake-claude.mjs — stands in for `claude -p` in tests/e2e.mjs (COLONY_CLAUDE=<this file>), so the
  3. // `work` process is tested end to end without spending Claude quota. It behaves like Claude Code 2.1.280
  4. // with `--output-format json` + `verbose` (a LIST of messages, the `result` entry last).
  5. // It is the WORKER, or — when `--json-schema` is the verdict schema — the CONTROLLER (mission 019).
  6. // FAKE_CLAUDE_MODE worker: good (default) | nostructured | badreport | hang | falseclaim | longresult
  7. // (falseclaim: the report claims `missing.txt`, which is never written; mission 027 longresult:
  8. // a `result` of 124 characters + a test step on `.scratch/` → refused → the fix step)
  9. // FAKE_DECIDED worker: the report's `decided` (a JSON list; default [] — mission 027)
  10. // FAKE_QUESTIONS worker: the report's `questions` (a JSON list; default [] — mission 022)
  11. // FAKE_OPEN worker: the report's `open` (a JSON list; default [] — mission 023)
  12. // FAKE_WEEK_FILE `/usage`: the weekly percent (default 41; "none" = the answer has no weekly limit — mission 023)
  13. // FAKE_CLAUDE_LOG worker: file with argv, cwd, stdin (the brief), whether COLONY_TOKEN_FILE was visible
  14. // FAKE_CONTROLLER_MODE controller: pass (default) | fail | noevidence (says pass, one finding "no evidence")
  15. // | nostructured | hang
  16. // FAKE_QUESTION_RULINGS controller: comma list of rulings per report question (ask|decided|trivial|internal|packed; default ask)
  17. // FAKE_CONTROLLER_LOG controller: the same log for the controller call
  18. // Mission 020 (the `run` loop):
  19. // `-p /usage` the quota probe: answers like Claude 2.1.280's local /usage command (cost 0) with
  20. // `usage_report.rate_limits.limits[kind session].percent` = the number in FAKE_USAGE_FILE
  21. // (re-read on every call, so a test can change it while `run` runs) or FAKE_USAGE_PERCENT (5)
  22. // FAKE_CLAUDE_MODE=ratelimit worker: ends like a session evicted by the usage limit (rate_limit_event
  23. // `rejected` with resetsAt = now + FAKE_RATELIMIT_RESET s (default 3), error result
  24. // "You've hit your limit …", no structured output); `--resume <id>` then answers like `good`
  25. // FAKE_CLAUDE_SLEEP seconds every worker / controller call waits before answering (default 0)
  26. // FAKE_CLAUDE_SLEEP_DIR `<folder name>=<seconds>`: the calls run in that folder wait this long instead (antcolony#28)
  27. // FAKE_LOG_DIR one JSON file per call: kind (usage/worker/controller), argv, pid, start/end ms, the
  28. // COLONY_PORTS* env, cwd — to prove parallel sessions and their port ranges
  29. // Mission 025 (the finalize step): a `--resume` whose stdin carries `colony-finalize:` is the FINALIZE call — it
  30. // answers by FAKE_FINALIZE_MODE (good | nostructured | badreport | hang; default = the worker's own mode, so an old
  31. // test whose worker had no report gets none again), writes no file, logs kind `finalize` (FAKE_LOG_DIR) and its
  32. // argv/stdin to FAKE_FINALIZE_LOG. `hang` now writes the worker's file before it hangs (work was done, then time ran out).
  33. // antcolony#37: FAKE_FINALIZE_MODE may be a comma list ("longresult,longresult,good") — one mode per successive
  34. // fix/finalize call on the same resumed Claude session (tracked in FAKE_STATE_DIR); past the list's end it repeats
  35. // the last entry.
  36. // Mission 026:
  37. // FAKE_COMPLETE worker / finalize: the report's `complete` (default true; "false" = half done)
  38. // FAKE_LEAVE_SERVER comma list of call kinds (worker, finalize, controller) that LEAVE servers running on their port
  39. // range (detached, like a crashed test's dev servers): worker → COLONY_PORT_FROM (own session env),
  40. // +2 (env COLONY_SESSION=s-foreign-e2e: "another colony session"), +3 (ignores SIGTERM);
  41. // finalize → +5; controller → +4. Each started server is appended to FAKE_SERVER_LOG (JSON lines
  42. // { kind, role, port, pid }) — the e2e checks what the scheduler stopped and kills the rest itself.
  43. // prompt cache (simulated): a session remembers its tool list (`--disallowedTools`, '' when none) from its first call;
  44. // a `--resume` with the SAME tool list reads the cached prefix (cache read 20000, write 44, cost
  45. // 0.0123), a DIFFERENT one re-writes it (cache read 0, write 20000, cost 0.1107 = 9×) — the
  46. // effect seen on the first real finalize (mission 025), modelled, not measured.
  47. import { readFileSync, writeFileSync, existsSync, mkdirSync, appendFileSync } from 'node:fs';
  48. import { join } from 'node:path';
  49. import { spawn } from 'node:child_process';
  50. import { connect } from 'node:net';
  51. const argv = process.argv.slice(2);
  52. const opt = (n) => { const i = argv.indexOf(n); return i >= 0 ? argv[i + 1] : null; };
  53. const started = Date.now();
  54. const callLog = (kind, extra = {}) => {
  55. if (!process.env.FAKE_LOG_DIR) return;
  56. mkdirSync(process.env.FAKE_LOG_DIR, { recursive: true });
  57. const f = join(process.env.FAKE_LOG_DIR, `${started}-${process.pid}-${kind}.json`);
  58. writeFileSync(f, JSON.stringify({ kind, argv, pid: process.pid, start: started, end: extra.end || null, cwd: process.cwd(),
  59. ports: process.env.COLONY_PORTS || null, portFrom: process.env.COLONY_PORT_FROM || null, portTo: process.env.COLONY_PORT_TO || null,
  60. tokenFileVisible: 'COLONY_TOKEN_FILE' in process.env, stopFileVisible: 'COLONY_STOP_FILE' in process.env, ...extra }));
  61. };
  62. if (argv[0] === '-p' && argv[1] === '/usage') {
  63. // the quota probe: a local command in the real Claude Code (no model call, cost 0)
  64. let pct = Number(process.env.FAKE_USAGE_PERCENT || 5);
  65. if (process.env.FAKE_USAGE_FILE && existsSync(process.env.FAKE_USAGE_FILE)) pct = Number(readFileSync(process.env.FAKE_USAGE_FILE, 'utf8').trim());
  66. // mission 023: the weekly percent (default 41) from FAKE_WEEK_FILE (re-read per call); "none" = no weekly limit in the answer
  67. let week = '41';
  68. if (process.env.FAKE_WEEK_FILE && existsSync(process.env.FAKE_WEEK_FILE)) week = readFileSync(process.env.FAKE_WEEK_FILE, 'utf8').trim();
  69. const resets = new Date(Date.now() + 3 * 3600e3).toISOString().replace('Z', '000+00:00');
  70. const text = `You are currently using your subscription to power your Claude Code usage\n\nCurrent session: ${pct}% used · resets soon` + (week === 'none' ? '' : `\nCurrent week (all models): ${week}% used · resets later`);
  71. process.stdout.write(JSON.stringify([
  72. { type: 'system', subtype: 'init', permissionMode: opt('--permission-mode'), session_id: 'usage-probe' },
  73. { type: 'assistant', message: { model: '<synthetic>', content: [{ type: 'text', text }] }, usage_report: { session: { total_cost_usd: 0 },
  74. rate_limits: { limits: [{ kind: 'session', group: 'session', percent: pct, resets_at: resets }, ...(week === 'none' ? [] : [{ kind: 'weekly_all', group: 'weekly', percent: Number(week), resets_at: resets }])] } } },
  75. { type: 'result', subtype: 'success', is_error: false, num_turns: 0, total_cost_usd: 0, result: text, local_command: 'usage', session_id: 'usage-probe' },
  76. ]));
  77. callLog('usage', { percent: pct, week, end: Date.now() });
  78. process.exit(0);
  79. }
  80. const stdin = readFileSync(0, 'utf8');
  81. // Mission 029: the LIBRARIAN (its schema has "supersedes"): reads the JSON input of the prompt and answers by markers in
  82. // the creator's text — `#not` (or the text "test") → not a decision; `#global` → project "all"; `#project:<name>`;
  83. // `#topic:<word-with-dashes>`; `#excerpt:"…"` → that quote (verbatim); `#paraphrase` → a quote NOT in the text; a text saying
  84. // `i never said "X"` supersedes every current entry whose quote contains X. FAKE_LIBRARIAN_MODE good (default) |
  85. // nostructured | badproject (answers a project that is not allowed) | hang; FAKE_LIBRARIAN_LOG = file (argv + stdin) per call.
  86. // Mission 034: FAKE_LIBRARIAN_MODE_FILE (re-read per call, beats FAKE_LIBRARIAN_MODE) so a test can switch the mode while
  87. // `run` runs. `ratelimit` = the shape Claude Code 2.1.280 gave LIVE on 2026-09-25 (librarian/calls/0muha3gdyxbp-result.json):
  88. // rate_limit_event rejected (resetsAt = now + FAKE_RATELIMIT_RESET s, default 3), a synthetic assistant message with
  89. // error "rate_limit" + "You've hit your session limit · resets 8:50pm (Europe/Vienna)", a result with subtype "success",
  90. // is_error true, api_error_status 429, 0 tokens, $0, no structured output. `ratelimit-noreset` = the same WITHOUT the
  91. // rate_limit_event and WITHOUT api_error_status (no reset time known; only the synthetic message + the text say it).
  92. if (/"supersedes"/.test(opt('--json-schema') || '')) {
  93. let lmode = process.env.FAKE_LIBRARIAN_MODE || 'good';
  94. if (process.env.FAKE_LIBRARIAN_MODE_FILE && existsSync(process.env.FAKE_LIBRARIAN_MODE_FILE)) lmode = readFileSync(process.env.FAKE_LIBRARIAN_MODE_FILE, 'utf8').trim() || lmode;
  95. const at = stdin.indexOf('## Input (JSON)');
  96. const line = stdin.slice(at).split('\n').find(l => l.startsWith('{'));
  97. const input = JSON.parse(line);
  98. const text = input.creatorText.quotes.join('\n');
  99. const resetsAt = Math.floor(Date.now() / 1000) + Number(process.env.FAKE_RATELIMIT_RESET || 3);
  100. if (process.env.FAKE_LIBRARIAN_LOG) appendFileSync(process.env.FAKE_LIBRARIAN_LOG, JSON.stringify({ argv, cwd: process.cwd(), text, input, mode: lmode, at: Date.now(), resetsAt: lmode === 'ratelimit' ? resetsAt : null, tokenFileVisible: 'COLONY_TOKEN_FILE' in process.env }) + '\n');
  101. callLog('librarian', { mode: lmode, text });
  102. if (lmode === 'hang') { setInterval(() => {}, 1000); }
  103. else if (lmode === 'ratelimit' || lmode === 'ratelimit-noreset') {
  104. const sid = 'fake-librarian-limited';
  105. const zero = { input_tokens: 0, output_tokens: 0, cache_read_input_tokens: 0, cache_creation_input_tokens: 0 };
  106. const msg = "You've hit your session limit · resets 8:50pm (Europe/Vienna)";
  107. const out = [{ type: 'system', subtype: 'init', permissionMode: opt('--permission-mode'), session_id: sid, model: 'claude-sonnet-5' }];
  108. if (lmode === 'ratelimit') out.push({ type: 'rate_limit_event', rate_limit_info: { status: 'rejected', resetsAt, rateLimitType: 'five_hour', overageStatus: 'rejected', isUsingOverage: false, unifiedWindows: { five_hour: { utilization: 1, resetsAt }, seven_day: { utilization: 0.2, resetsAt: resetsAt + 86400 } } }, session_id: sid });
  109. out.push({ type: 'assistant', message: { model: '<synthetic>', role: 'assistant', stop_reason: 'stop_sequence', usage: zero, content: [{ type: 'text', text: msg }] }, session_id: sid, error: 'rate_limit', is_api_error_message: true });
  110. const result = { type: 'result', subtype: 'success', is_error: true, num_turns: 1, duration_ms: 488, duration_api_ms: 0, session_id: sid, total_cost_usd: 0, usage: zero, modelUsage: {}, permission_denials: [], terminal_reason: 'api_error', result: msg };
  111. if (lmode === 'ratelimit') result.api_error_status = 429;
  112. out.push(result);
  113. process.stdout.write(JSON.stringify(out));
  114. process.exitCode = 1;
  115. }
  116. else {
  117. const not = /#not\b/.test(text) || /^test$/i.test(text.trim());
  118. let project = /#global\b/.test(text) ? 'all' : ((text.match(/#project:(\S+)/) || [])[1] || (input.ticket ? input.ticket.project : 'all'));
  119. if (lmode === 'badproject') project = 'no-such-project';
  120. const topic = ((text.match(/#topic:([a-z-]+)/) || [])[1] || 'general').replace(/-/g, ' ');
  121. // #paraphrase: a quote that is NOT in the text (a model that rephrased) — the scheduler must store the whole text
  122. const quote = /#paraphrase\b/.test(text) ? 'the creator wants it teal' : ((text.match(/#excerpt:"([^"]+)"/) || [])[1] || '');
  123. const never = (text.match(/i never said "([^"]+)"/i) || [])[1];
  124. const supersedes = never ? input.currentEntries.filter(e => e.quote.includes(never)).map(e => e.id) : [];
  125. // #longmeans: a reading of 400 characters on two lines (the scheduler makes it one line and clips it to 300)
  126. const means = /#longmeans\b/.test(text) ? 'long reading line one ' + 'x'.repeat(180) + '\nline two ' + 'y'.repeat(200) : 'fake reading of: ' + text.replace(/\s+/g, ' ').slice(0, 60);
  127. const answer = { decision: !not, project, topic, quote, means: not ? '' : means, supersedes, why: not ? 'fake: marked #not' : 'fake: a decision' };
  128. const result = { type: 'result', subtype: lmode === 'nostructured' ? 'error_max_turns' : 'success', is_error: lmode === 'nostructured', num_turns: 1, duration_ms: 321, session_id: 'fake-librarian',
  129. total_cost_usd: 0.0031, usage: { input_tokens: 5, output_tokens: 6, cache_read_input_tokens: 7, cache_creation_input_tokens: 8 }, modelUsage: { ['fake-' + opt('--model')]: { costUSD: 0.0031 } }, permission_denials: [] };
  130. if (lmode !== 'nostructured') result.structured_output = answer;
  131. process.stdout.write(JSON.stringify([{ type: 'system', subtype: 'init', permissionMode: opt('--permission-mode'), session_id: 'fake-librarian' }, result]));
  132. process.exitCode = lmode === 'nostructured' ? 1 : 0;
  133. }
  134. } else {
  135. const controller = /"verdict"/.test(opt('--json-schema') || '');
  136. const resuming = opt('--resume');
  137. let mode = controller ? (process.env.FAKE_CONTROLLER_MODE || 'pass') : (process.env.FAKE_CLAUDE_MODE || 'good');
  138. if (resuming && mode === 'ratelimit') mode = 'good';
  139. const finalize = !controller && !!resuming && stdin.includes('colony-finalize:');
  140. // antcolony#37: FAKE_FINALIZE_MODE may be a comma list, one mode per successive finalize/fix call on the SAME
  141. // resumed session (e.g. "longresult,longresult,good" — the third fix try passes); a call past the list's end
  142. // repeats the last entry. A single mode (no comma) behaves as before.
  143. if (finalize && process.env.FAKE_FINALIZE_MODE) {
  144. const modes = process.env.FAKE_FINALIZE_MODE.split(',');
  145. if (modes.length > 1) {
  146. const dir = process.env.FAKE_STATE_DIR || join(process.cwd(), '.fake-claude-sessions');
  147. mkdirSync(dir, { recursive: true });
  148. const cf = join(dir, resuming + '.fixcalls');
  149. const idx = existsSync(cf) ? Number(readFileSync(cf, 'utf8')) : 0;
  150. writeFileSync(cf, String(idx + 1));
  151. mode = modes[Math.min(idx, modes.length - 1)];
  152. } else {
  153. mode = modes[0];
  154. }
  155. }
  156. const logFile = finalize ? (process.env.FAKE_FINALIZE_LOG || process.env.FAKE_CLAUDE_LOG) : controller ? process.env.FAKE_CONTROLLER_LOG : process.env.FAKE_CLAUDE_LOG;
  157. if (logFile) writeFileSync(logFile, JSON.stringify({ argv, cwd: process.cwd(), brief: stdin, tokenFileVisible: 'COLONY_TOKEN_FILE' in process.env }, null, 1));
  158. const callSession = controller ? (stdin.match(/`session` = `([^`]+)`/) || [])[1] : (stdin.match(/"session": "([^"]+)"/) || [])[1];
  159. const kind = controller ? 'controller' : finalize ? 'finalize' : 'worker';
  160. callLog(kind, { mode, resume: resuming, session: callSession || null });
  161. let usage = { input_tokens: 11, output_tokens: 22, cache_read_input_tokens: 33, cache_creation_input_tokens: 44 };
  162. let cost = controller ? 0.0045 : 0.0123;
  163. const pm = opt('--permission-mode');
  164. const sid = opt('--session-id') || resuming;
  165. const rateEvent = (status, resetsAt) => ({ type: 'rate_limit_event', rate_limit_info: { status, resetsAt, rateLimitType: 'five_hour', unifiedWindows: { five_hour: { utilization: status === 'rejected' ? 1 : 0.05, resetsAt } } } });
  166. // mission 025: like Claude Code 2.1.280, a RESUMED session reports the cost of the WHOLE session so far (cumulative
  167. // total_cost_usd / modelUsage) — the fake keeps each Claude session's total in FAKE_STATE_DIR (default: .fake-claude-sessions/ in the cwd)
  168. const tools = opt('--disallowedTools') || '';
  169. const sessionTotal = (sid, add) => {
  170. if (controller || !sid) return add;
  171. const dir = process.env.FAKE_STATE_DIR || join(process.cwd(), '.fake-claude-sessions');
  172. mkdirSync(dir, { recursive: true });
  173. const f = join(dir, sid + '.json');
  174. const st = resuming && existsSync(f) ? JSON.parse(readFileSync(f, 'utf8')) : { total: 0, tools };
  175. // mission 026: the simulated prompt cache — a resume with another tool list misses the cached prefix
  176. if (resuming) {
  177. const hit = (st.tools || '') === tools;
  178. usage = { ...usage, cache_read_input_tokens: hit ? 20000 : 0, cache_creation_input_tokens: hit ? 44 : 20000 };
  179. if (!hit) add = Math.round(add * 9 * 1e6) / 1e6;
  180. }
  181. const total = Math.round((st.total + add) * 1e6) / 1e6;
  182. writeFileSync(f, JSON.stringify({ total, tools: st.tools === undefined ? tools : st.tools }));
  183. return total;
  184. };
  185. const answer = (structured, messages = []) => {
  186. const limited = mode === 'ratelimit';
  187. const total = sessionTotal(sid, cost);
  188. const failed = mode === 'nostructured' || limited;
  189. const resetsAt = Math.floor(Date.now() / 1000) + Number(process.env.FAKE_RATELIMIT_RESET || 3);
  190. const result = { type: 'result', subtype: failed && !limited ? 'error_max_turns' : 'success', is_error: failed,
  191. num_turns: 2, duration_ms: 1234, session_id: sid, total_cost_usd: total, usage,
  192. modelUsage: { ['fake-' + opt('--model')]: { costUSD: total } }, permission_denials: [] };
  193. if (limited) { result.result = "You've hit your limit · resets 8:19pm (Europe/Vienna)"; result.api_error_status = 429; }
  194. if (!failed) result.structured_output = structured;
  195. process.stdout.write(JSON.stringify([
  196. { type: 'system', subtype: 'init', permissionMode: pm, session_id: sid },
  197. { type: 'system', subtype: 'status', permissionMode: pm },
  198. rateEvent(limited ? 'rejected' : 'allowed', resetsAt),
  199. ...messages,
  200. result,
  201. ]));
  202. process.stderr.write('fake claude: ' + kind + ' mode ' + mode + (resuming ? ' (resume ' + resuming + ')' : '') + '\n');
  203. process.exitCode = failed ? 1 : 0;
  204. callLog(kind + '-end', { mode, resume: resuming, end: Date.now(), limited, session: callSession || null });
  205. };
  206. // mission 026: servers a session leaves behind (detached; they outlive this fake) — waits until each one listens
  207. const serverCode = (stubborn) => `const s=require('http').createServer((q,r)=>r.end('fake'));s.listen(Number(process.env.FAKE_PORT),'127.0.0.1');${stubborn ? "process.on('SIGTERM',()=>{});" : ''}setInterval(()=>{},1000);`;
  208. const listening = (port) => new Promise(res => { const c = connect(port, '127.0.0.1'); c.on('connect', () => { c.destroy(); res(true); }); c.on('error', () => res(false)); });
  209. const leaveServers = async () => {
  210. const kinds = (process.env.FAKE_LEAVE_SERVER || '').split(',');
  211. if (!kinds.includes(kind)) return;
  212. const from = Number(process.env.COLONY_PORT_FROM);
  213. if (!from) return;
  214. // a real session starts its servers seconds after it began; the scheduler counts only processes started more than
  215. // 50 ms after the session (clock precision margin) — so the fake waits a little too
  216. await new Promise(r => setTimeout(r, 300));
  217. const plan = kind === 'worker' ? [['own', 0, {}, false], ['foreign', 2, { COLONY_SESSION: 's-foreign-e2e' }, false], ['stubborn', 3, {}, true]]
  218. : kind === 'finalize' ? [['own', 5, {}, false]] : [['own', 4, {}, false]];
  219. for (const [role, off, env, stubborn] of plan) {
  220. const port = from + off;
  221. const c = spawn(process.execPath, ['-e', serverCode(stubborn), 'fake-left-server-' + kind + '-' + role], { detached: true, stdio: 'ignore', env: { ...process.env, ...env, FAKE_PORT: String(port) } });
  222. c.unref();
  223. if (process.env.FAKE_SERVER_LOG) appendFileSync(process.env.FAKE_SERVER_LOG, JSON.stringify({ kind, role, port, pid: c.pid }) + '\n');
  224. for (let i = 0; i < 50 && !(await listening(port)); i++) await new Promise(r => setTimeout(r, 100));
  225. }
  226. };
  227. const main = async () => {
  228. await leaveServers();
  229. if (mode === 'hang') {
  230. // mission 025: the work was done, then time ran out (not in a finalize call: that one changes nothing)
  231. const session = (stdin.match(/"session": "([^"]+)"/) || [])[1];
  232. if (!controller && !finalize && session) writeFileSync('fake-worker-was-here.txt', session + '\n');
  233. setInterval(() => {}, 1000);
  234. }
  235. else if (!controller) {
  236. const ticket = (stdin.match(/"ticket": "([^"]+)"/) || [])[1];
  237. const session = (stdin.match(/"session": "([^"]+)"/) || [])[1];
  238. // "the work": a file in the cwd (= the project's dev folder) — the finalize step (mission 025) writes nothing
  239. if (!finalize) writeFileSync('fake-worker-was-here.txt', session + '\n');
  240. const report = {
  241. ticket, session,
  242. result: finalize ? 'The fake worker wrote its file; its report came from the finalize step.' : 'The fake worker wrote its file.',
  243. complete: process.env.FAKE_COMPLETE !== 'false',
  244. test: ['open the dev folder — `fake-worker-was-here.txt` is there', 'it holds the session id'],
  245. done: ['created `fake-worker-was-here.txt`'],
  246. verified: [{ claim: 'the file exists', command: 'cat fake-worker-was-here.txt', output: session }],
  247. open: process.env.FAKE_OPEN ? JSON.parse(process.env.FAKE_OPEN) : [], issues: [], questions: process.env.FAKE_QUESTIONS ? JSON.parse(process.env.FAKE_QUESTIONS) : [],
  248. decided: process.env.FAKE_DECIDED ? JSON.parse(process.env.FAKE_DECIDED) : [], running: [], refused: [],
  249. };
  250. if (mode === 'longresult') {
  251. report.result = 'The fake worker wrote its file and this sentence is deliberately far too long for the creator to read in one glance, really.';
  252. report.test = ['open .scratch/dev/fake-worker-was-here.txt', 'it holds the session id'];
  253. }
  254. if (mode === 'badreport') delete report.verified[0].output;
  255. if (mode === 'falseclaim') {
  256. report.done = ['created `missing.txt` with the content `hello`'];
  257. report.verified = [{ claim: '`missing.txt` exists and says hello', command: 'cat missing.txt', output: 'hello' }];
  258. report.issues = [{ project: 'hybriel', subject: 'fake issue of a false report', repro: 'r', observed: 'o', expected: 'e', blocks: false }];
  259. }
  260. // a transcript like Claude's verbose output: one tool call + its result
  261. answer(report, [
  262. { type: 'assistant', message: { content: [{ type: 'text', text: 'fake worker: writing the file' }, { type: 'tool_use', id: 't1', name: 'Bash', input: { command: 'cat fake-worker-was-here.txt' } }] } },
  263. { type: 'user', message: { content: [{ type: 'tool_result', tool_use_id: 't1', content: session }] } },
  264. ]);
  265. } else {
  266. // the controller: really look at the files the report names (like a real controller would)
  267. const ticket = (stdin.match(/`ticket` = `([^`]+)`/) || [])[1];
  268. const session = (stdin.match(/`session` = `([^`]+)`/) || [])[1];
  269. const fenceStart = stdin.indexOf('## The worker report (report.json)');
  270. const reportText = stdin.slice(fenceStart).split('\n').find(l => l.startsWith('{'));
  271. const report = JSON.parse(reportText);
  272. const findings = report.verified.map(v => {
  273. const file = (v.command.match(/^cat (\S+)$/) || [])[1];
  274. const there = file && existsSync(file);
  275. return { claim: v.claim, about: 'verified', status: there ? 'ok' : 'false', check: file ? `test -f ${file}: ${there ? 'exists' : 'no such file'}` : 'not a cat command' };
  276. });
  277. let verdict = findings.every(f => f.status === 'ok') ? 'pass' : 'fail';
  278. if (mode === 'fail') { verdict = 'fail'; findings.push({ claim: 'fake controller says: fail', about: 'other', status: 'false', check: 'FAKE_CONTROLLER_MODE=fail' }); }
  279. if (mode === 'noevidence') { verdict = 'pass'; findings.push({ claim: report.done[0], about: 'done', status: 'no evidence', check: 'nothing in the session shows it' }); }
  280. // antcolony#24: a ruling per question of the report; FAKE_QUESTION_RULINGS = comma list (default: ask for all)
  281. const rl = (process.env.FAKE_QUESTION_RULINGS || '').split(',');
  282. const questions = (report.questions || []).map((_, index) => { const ruling = rl[index] || 'ask'; return ruling === 'ask' || ruling === 'packed' ? { index, ruling, reason: 'fake ruling' } : { index, ruling, reason: 'fake ruling', answer: 'Use the obvious choice.' }; });
  283. answer({ ticket, session, verdict, findings, questions, summary: `fake controller (${mode}): ${verdict}, ${findings.length} finding(s)` },
  284. [{ type: 'assistant', message: { content: [{ type: 'tool_use', id: 'c1', name: 'Bash', input: { command: 'test -f …' } }] } }]);
  285. }
  286. };
  287. let pause = Number(process.env.FAKE_CLAUDE_SLEEP || 0);
  288. const byDir = (process.env.FAKE_CLAUDE_SLEEP_DIR || '').split('=');
  289. if (byDir.length === 2 && process.cwd().split('/').pop() === byDir[0]) pause = Number(byDir[1]);
  290. if (pause > 0) setTimeout(main, pause * 1000); else main();
  291. }

Branches

Latest commits

  • 3a4d0324antcolony#37: a too-long report gets up to 3 fix tries, finished work is never thrown away for lengthmre
  • a6af7883tracker: worker box sees calendar.worldapi.org (login to copy)mre
  • c613d26btemplates: bridges to external components (login.js for ident's selector) are allowed (creator 2026-09-27)mre
  • 9062978ctracker: worker box sees /media/STORAGE/projects/old-tracker read-only (tracker#2 source data)mre
  • 7f9660eeState of 2026-09-27, before the move to gitoriamre