#!/usr/bin/env python3 """One fault per acceptance check. Preflight is read-only; batches require Sage's slot.""" import json, os, pathlib, signal, subprocess, sys ROOT = pathlib.Path('/mnt/storage/src/mosaic-stack-rocko-124') OUT = pathlib.Path(__file__).resolve().parent / 'mutants' T = 'packages/plugins/usage-source/transcript/index.mjs' TT = 'packages/plugins/tests/transcript.test.mjs' U = 'packages/bus/src/usage.mjs' UT = 'packages/bus/tests/usage.test.mjs' B = 'packages/bus/src/broker.mjs' BT = 'packages/bus/tests/usage-broker.test.mjs' RT = 'packages/bus/tests/usage-runtime.test.mjs' M = 'packages/bus/tests/usage-migration.test.mjs' C = 'packages/cli/src/usage.mjs' CT = 'packages/cli/tests/usage.test.mjs' W = 'packages/webui/src/public/bus.js' WT = 'packages/webui/tests/usage-browser.test.mjs' cases = [] def add(name, path, old, new, test): cases.append(dict(check=name, path=path, old=old, new=new, test=test)) add('01-dir-no-follow', 'packages/plugins/usage-source/transcript/index.mjs', 'const DIR = constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW;', 'const DIR = constants.O_RDONLY | constants.O_DIRECTORY;', 'packages/plugins/tests/transcript.test.mjs') add('02-file-no-follow', 'packages/plugins/usage-source/transcript/index.mjs', 'const FILE = constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK;', 'const FILE = constants.O_RDONLY | constants.O_NONBLOCK;', 'packages/plugins/tests/transcript.test.mjs') add('03-regular-single-link', 'packages/plugins/usage-source/transcript/index.mjs', 'if (!before.isFile() || before.nlink !== 1)', 'if (false)', 'packages/plugins/tests/transcript.test.mjs') add('04-stat-size-before-read', 'packages/plugins/usage-source/transcript/index.mjs', 'if (before.size > MAX_BYTES - budget.bytes)', 'if (false)', 'packages/plugins/tests/transcript.test.mjs') add('05-stream-size-bound', 'packages/plugins/usage-source/transcript/index.mjs', 'if (budget.bytes > MAX_BYTES)', 'if (false)', 'packages/plugins/tests/transcript.test.mjs') add('06-snapshot-stability', 'packages/plugins/usage-source/transcript/index.mjs', 'if (before.size !== after.size || total !== after.size || before.mtimeMs !== after.mtimeMs ||\n before.ctimeMs !== after.ctimeMs)', 'if (false)', 'packages/plugins/tests/transcript.test.mjs') add('07-fatal-utf8', 'packages/plugins/usage-source/transcript/index.mjs', '{ fatal: true }', '{ fatal: false }', 'packages/plugins/tests/transcript.test.mjs') add('08-cut-line', 'packages/plugins/usage-source/transcript/index.mjs', "if (text && !text.endsWith('\\n'))", 'if (false)', 'packages/plugins/tests/transcript.test.mjs') add('09-line-byte-bound', 'packages/plugins/usage-source/transcript/index.mjs', 'if (Buffer.byteLength(line) > MAX_LINE)', 'if (false)', 'packages/plugins/tests/transcript.test.mjs') add('10-record-bound', 'packages/plugins/usage-source/transcript/index.mjs', 'if (++budget.records > MAX_RECORDS)', 'if (false)', 'packages/plugins/tests/transcript.test.mjs') add('11-directory-entry-bound', 'packages/plugins/usage-source/transcript/index.mjs', 'if (++budget.entries > MAX_ENTRIES)', 'if (false)', 'packages/plugins/tests/transcript.test.mjs') add('12-root-confinement', 'packages/plugins/usage-source/transcript/index.mjs', "if (typeof root !== 'string' || !isAbsolute(root) || root.split('/').includes('..'))", 'if (false)', 'packages/plugins/tests/transcript.test.mjs') add('13-harness-validation', 'packages/plugins/usage-source/transcript/index.mjs', "if (!['pi', 'claude-code'].includes(harness))", 'if (false)', 'packages/plugins/tests/transcript.test.mjs') add('14-json-object', 'packages/plugins/usage-source/transcript/index.mjs', "if (!record || typeof record !== 'object' || Array.isArray(record))", 'if (false)', 'packages/plugins/tests/transcript.test.mjs') add('15-assistant-id', 'packages/plugins/usage-source/transcript/index.mjs', "if (typeof id !== 'string' || !ID.test(id))", 'if (false)', 'packages/plugins/tests/transcript.test.mjs') add('16-usage-object', 'packages/plugins/usage-source/transcript/index.mjs', "if (!usage || typeof usage !== 'object' || Array.isArray(usage))", 'if (false)', 'packages/plugins/tests/transcript.test.mjs') add('17-nonnegative-safe-integers', 'packages/plugins/usage-source/transcript/index.mjs', 'if (values.some(n => !Number.isSafeInteger(n) || n < 0))', 'if (false)', 'packages/plugins/tests/transcript.test.mjs') add('18-conflicting-duplicates', 'packages/plugins/usage-source/transcript/index.mjs', 'if (Object.hasOwn(entries, key) && JSON.stringify(entries[key]) !== JSON.stringify(value))', 'if (false)', 'packages/plugins/tests/transcript.test.mjs') add('19-claude-kinds', 'packages/plugins/usage-source/transcript/index.mjs', "['input_tokens', 'output_tokens', 'cache_creation_input_tokens', 'cache_read_input_tokens']", "['output_tokens', 'input_tokens', 'cache_read_input_tokens', 'cache_creation_input_tokens']", 'packages/plugins/tests/transcript.test.mjs') add('20-pi-kinds', 'packages/plugins/usage-source/transcript/index.mjs', "['input', 'output', 'cacheWrite', 'cacheRead'] :", "['output', 'input', 'cacheRead', 'cacheWrite'] :", 'packages/plugins/tests/transcript.test.mjs') add('21-dedup-identical', 'packages/plugins/usage-source/transcript/index.mjs', 'entries[key] = value;', 'if (entries[key]) value.input += entries[key].input;\n entries[key] = value;', 'packages/plugins/tests/transcript.test.mjs') add('22-baseline-required', 'packages/bus/src/usage.mjs', "if (!before || before.status !== 'known') return unknown('baseline-unknown');", "if (!before || before.status !== 'known') return { status: 'known', ...zero() };", 'packages/bus/tests/usage.test.mjs') add('23-rewritten-history', 'packages/bus/src/usage.mjs', "if (!Object.hasOwn(after.entries, id) || KINDS.some(k => after.entries[id][k] !== v[k])) return unknown('transcript-rewritten');", '/* history check omitted */', 'packages/bus/tests/usage.test.mjs') add('24-no-new-usage-unknown', 'packages/bus/src/usage.mjs', "return added ? { status: 'known', ...sum } : unknown('no-new-usage');", "return { status: 'known', ...sum };", 'packages/bus/tests/usage.test.mjs') add('25-turn-idempotence', 'packages/bus/src/usage.mjs', 'if (existing.length) return { recorded: true };', '/* retry check omitted */', 'packages/bus/tests/usage.test.mjs') add('26-unknowns-beside-totals', 'packages/bus/src/usage.mjs', "target.unknown.add(row.run + ':' + row.turn)", 'void 0', 'packages/bus/tests/usage.test.mjs') add('27-log-unknown', 'packages/bus/src/usage.mjs', "log({ event: 'usage-unknown', business, run, instance, turn, source: name, code: result.code });", '/* unknown log omitted */', 'packages/bus/tests/usage.test.mjs') add('28-safe-parse-code', 'packages/bus/src/usage.mjs', "return unknown(/^transcript-(path|size|line|entries|records|changed|parse|usage|harness)$/.test(value.code) ? value.code : 'source-unknown');", "return unknown('source-unknown');", 'packages/bus/tests/usage.test.mjs') add('29-business-scoped-view', 'packages/bus/src/usage.mjs', 'SELECT * FROM usage_records WHERE business=? ORDER BY seq', 'SELECT * FROM usage_records WHERE business=? OR 1=1 ORDER BY seq', 'packages/bus/tests/usage.test.mjs') add('30-loader-only-source', 'packages/bus/src/usage.mjs', "const modules = loader.modules('usage-source'), names", 'const modules = [], names', 'packages/bus/tests/usage.test.mjs') add('31-run-grouping', 'packages/bus/src/usage.mjs', 'byRun: { run: row.run }', "byRun: { run: 'all' }", 'packages/bus/tests/usage.test.mjs') add('32-thread-transcript-root', 'packages/bus/src/usage.mjs', 'const root = session.thread', 'const root = false', 'packages/bus/tests/usage.test.mjs') add('33-exact-large-totals', 'packages/bus/src/usage.mjs', 'target[key] = BigInt(target[key]) + BigInt(row[column]);', 'target[key] = Number(target[key]) + row[column];', 'packages/bus/tests/usage.test.mjs') add('34-runner-role-check', 'packages/bus/src/broker.mjs', ' this.#store.transaction(() => {\n this.#agent(s);\n if (!this.#store.get("SELECT 1 FROM messages m JOIN deliveries', ' this.#store.transaction(() => {\n /* agent check omitted */\n if (!this.#store.get("SELECT 1 FROM messages m JOIN deliveries', 'packages/bus/tests/usage-broker.test.mjs') add('35-bound-delivered-turn', 'packages/bus/src/broker.mjs', 'if (!this.#store.get("SELECT 1 FROM messages m JOIN deliveries d ON d.message=m.id WHERE m.id=? AND m.business=? AND m.to_role=? AND d.op=\'delivered\' AND d.holder_run=?", args.turn, s.business, s.role, s.run))', 'if (false)', 'packages/bus/tests/usage-broker.test.mjs') add('36-no-path-in-request', 'packages/bus/src/broker.mjs', "keys(args, ['turn'], ['turn']);", "keys(args, ['turn', 'root'], ['turn']);", 'packages/bus/tests/usage-broker.test.mjs') add('37-reader-usage-allowed', 'packages/bus/src/broker.mjs', "'messages', 'state', 'usage'].includes(request.verb)", "'messages', 'state'].includes(request.verb)", 'packages/bus/tests/usage-broker.test.mjs') add('38-view-bound-business', 'packages/bus/src/broker.mjs', 'return this.#usage.view(s.business);', "return this.#usage.view('other');", 'packages/bus/tests/usage-broker.test.mjs') add('39-runtime-injection', 'packages/bus/src/runtime.mjs', ' usage,\n', '', 'packages/bus/tests/usage-runtime.test.mjs') add('40-await-usage', 'packages/bus/src/server.mjs', "r.verb === 'usage.begin' || r.verb === 'usage.end' ||", 'false ||', 'packages/bus/tests/usage-runtime.test.mjs') add('41-runner-begin', 'packages/harness/src/runner.mjs', 'await client.call("usage.begin", { turn: m.id });', 'await Promise.resolve();', 'packages/harness/tests/runner.test.mjs') add('42-runner-end', 'packages/harness/src/runner.mjs', 'await client.call("usage.end", { turn: m.id });', 'await Promise.resolve();', 'packages/harness/tests/runner.test.mjs') add('43-cli-unknown', 'packages/cli/src/usage.mjs', 'unknown=${row.unknownTurns}', 'unknown=0', 'packages/cli/tests/usage.test.mjs') add('44-cli-json', 'packages/cli/src/usage.mjs', "if (parsed.flags.has('--json'))", 'if (false)', 'packages/cli/tests/usage.test.mjs') add('45-dispatch', 'scripts/mosaic', 'usage|project|limits|inbox|', 'project|limits|inbox|', 'packages/cli/tests/usage.test.mjs') add('46-read-adapter', 'packages/bus/src/views.mjs', " usage: () => client.call('usage'),\n", '', 'packages/cli/tests/usage.test.mjs') add('47-settings-unknown', 'packages/webui/src/public/bus.js', '${txt(row[key])}', "${txt(key === 'unknownTurns' ? 0 : row[key])}", 'packages/webui/tests/usage-browser.test.mjs') add('48-settings-four-kinds', 'packages/webui/src/public/bus.js', "['input', 'output', 'cacheWrite', 'cacheRead', 'unknownTurns']", "['input', 'output', 'cacheWrite', 'unknownTurns']", 'packages/webui/tests/usage-browser.test.mjs') add('49-settings-section', 'packages/webui/src/public/bus.js', '${tokenUsage(usage)}', '', 'packages/webui/tests/usage-browser.test.mjs') add('50-settings-safe-labels', 'packages/webui/src/public/bus.js', '${label}: ${txt(name)}', '${label}: ${name}', 'packages/webui/tests/usage-browser.test.mjs') add('51-http-arguments', 'packages/webui/src/serve.mjs', "if (search) return json(res, 400, { error: 'invalid-request', message: 'usage takes no arguments' });", '/* query check omitted */', 'packages/webui/tests/usage.test.mjs') add('52-http-shape', 'packages/webui/src/serve.mjs', "if (!value || typeof value.total !== 'object' || !Array.isArray(value.byInstance)) throw new BusReadError('invalid-response');", '/* shape check omitted */', 'packages/webui/tests/usage.test.mjs') add('53-migration-step', 'packages/bus/src/store.mjs', ' this.#db.exec(step.sql);', " if (step.from !== '3g') this.#db.exec(step.sql);", 'packages/bus/tests/usage-migration.test.mjs') add('54-migration-chain', 'packages/bus/src/store.mjs', 'while (version !== SCHEMA_VERSION)', 'if (version !== SCHEMA_VERSION)', 'packages/bus/tests/comment-migration.test.mjs') add('55-nonblocking-open', T, 'const FILE = constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK;', 'const FILE = constants.O_RDONLY | constants.O_NOFOLLOW;', TT) add('56-entry-name-validation', T, "if (!NAME.test(entry.name) || entry.name === '.' || entry.name === '..')", 'if (false)', TT) add('57-source-output-validation', U, 'if (id.length > 512 || !v || Object.keys(v).length !== 4 ||\n KINDS.some(k => !Number.isSafeInteger(v[k]) || v[k] < 0))', 'if (false)', UT) add('58-turn-sum-precision', U, 'if (!Number.isSafeInteger(sum[k]))', 'if (false)', UT) add('59-source-failure-is-unknown', U, "catch { sample = unknown('source-failed'); }", 'catch (e) { throw e; }', UT) add('60-all-enabled-sources', U, 'modules.map((module, i)', 'modules.slice(0, 1).map((module, i)', UT) add('61-instance-group', U, 'byInstance: { instance: row.instance }', "byInstance: { instance: 'other' }", UT) add('62-model-group', U, 'model: row.model }', "model: 'other-model' }", UT) add('63-provider-group', U, 'byProvider: { provider: row.provider }', "byProvider: { provider: 'other-provider' }", UT) add('64-account-unassigned', U, 'byAccount: { account: row.account }', "byAccount: { account: 'assigned' }", UT) for c in cases: assert (ROOT / c['path']).read_text().count(c['old']) == 1, c['check'] if '--preflight' in sys.argv: print('PREFLIGHT_OK', len(cases)); sys.exit(0) OUT.mkdir(exist_ok=True) resume = next((int(a.split('=')[1]) for a in sys.argv if a.startswith('--resume=')), 1) rows = json.loads((OUT / 'results.json').read_text())[:resume - 1] if resume > 1 else [] def stop_tree(pid): # Git's own runner creates process groups too. Capture descendants before killing the parent. descendants = {pid} for _ in range(8): before = set(descendants) for entry in pathlib.Path('/proc').iterdir(): if not entry.name.isdigit(): continue try: fields = (entry / 'stat').read_text().split(') ', 1)[1].split() if int(fields[1]) in descendants: descendants.add(int(entry.name)) except (OSError, ValueError, IndexError): pass if before == descendants: break for child in descendants: try: os.killpg(child, signal.SIGKILL) except ProcessLookupError: pass for c in cases[resume - 1:]: file = ROOT / c['path']; original = file.read_bytes(); process = None try: file.write_text(original.decode().replace(c['old'], c['new'])) with (OUT / (c['check'] + '.log')).open('w') as log: process = subprocess.Popen(['node', '--test', *(['--test-name-pattern', 'REQ-LIMIT-4 runner brackets'] if c['check'] in ['41-runner-begin', '42-runner-end'] else []), c['test']], cwd=ROOT, env={**os.environ, 'DOCKER_HOST': 'unix:///nonexistent.sock', 'TMPDIR': '/mnt/storage/scratch/rocko-124/tmp'}, stdout=log, stderr=subprocess.STDOUT, start_new_session=True) try: rc = process.wait(timeout=180) except subprocess.TimeoutExpired: stop_tree(process.pid); process.wait(); rc = 'timeout' content = (OUT / (c['check'] + '.log')).read_text() checker = ['node', '--check', c['path']] if c['path'].endswith(('.mjs', '.js')) else ['bash', '-n', c['path']] syntax = subprocess.run(checker, cwd=ROOT, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, start_new_session=True, timeout=10) invalid = syntax.returncode != 0 or 'ERR_MODULE_NOT_FOUND' in content verdict = 'INVALID' if invalid else 'TIMEOUT' if rc == 'timeout' else 'SURVIVED' if rc == 0 else 'KILLED' rows.append({k: c[k] for k in ['check', 'path', 'test']} | {'result': verdict, 'exit': rc}) (OUT / 'results.json').write_text(json.dumps(rows, indent=2) + '\n') print(c['check'], verdict, rc, flush=True) finally: if process is not None and process.poll() is None: stop_tree(process.pid); process.wait() file.write_bytes(original) if verdict != 'KILLED': sys.exit(1)