Unclaimed files left in the canonical checkout. Inventory: canonical-cleanup-inventory.md (commit-as-evidence groups). detect-secrets scan clean on 2026-11-02: hits are sha256 pins, git object ids, test sentinels already in tracked tests and unit-file lines.
136 lines
16 KiB
Python
136 lines
16 KiB
Python
#!/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-r2')
|
|
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)
|
|
|
|
add('65-usage-timestamp', 'packages/bus/src/store.mjs', " 'usage_records',\n", '', 'packages/bus/tests/store.test.mjs')
|
|
add('66-claude-project-no-follow', T, 'const child = openSync(at(fd, entry.name), DIR);', 'const child = openSync(at(fd, entry.name), constants.O_RDONLY | constants.O_DIRECTORY);', TT)
|
|
|
|
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)
|