Files
stack/agents/filbert/work/queue-95/mut/mutants.py
T
jason.woltjeandClaude Opus 5.5 27eda72309 filbert: row 95 build packet (#1589), manifest 246c0557
Candidate on base a3f6e9e6, not committed here. Gate green 21:27:35Z-21:30:19Z;
mutants 30/35 killed, 5 equivalent guards explained in build.md.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-12 16:31:43 -05:00

80 lines
7.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
# Filbert's row 95 mutants (builder's own). Each edit must match exactly once. The candidate is
# uncommitted in the worktree, so each file is restored from the bytes read before the edit, and
# every touched file's sha256 is checked against its start at the end.
# Usage: mutants.py <worktree> <out.txt>
import hashlib, os, signal, subprocess, sys
WT, OUT = sys.argv[1], sys.argv[2]
A = 'packages/webui/src/public/app.js'
B = 'packages/webui/src/public/bus.js'
TESTS = ['tests/board-held.test.mjs', 'tests/send-browser.test.mjs', 'tests/browser-edge.test.mjs', 'tests/browser.test.mjs']
RD = "if (res.type === 'opaqueredirect') { signOut(); throw signedOut(); }"
M = [
('app getJSON without manual', A, "const res = await fetch(path, { cache: 'no-store', redirect: 'manual' });", "const res = await fetch(path, { cache: 'no-store' });"),
('app api GET without manual', A, "value === undefined ? { cache: 'no-store', redirect: 'manual' }", "value === undefined ? { cache: 'no-store' }"),
('app api POST without manual', A, "body: JSON.stringify(value), redirect: 'manual' }", "body: JSON.stringify(value) }"),
('app api ignores a redirect', A, "{ method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(value), redirect: 'manual' });\n " + RD, "{ method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(value), redirect: 'manual' });\n"),
('bus fetchRead without manual', B, "=${enc(subject)}`}`, { cache: 'no-store', redirect: 'manual' }); }", "=${enc(subject)}`}`, { cache: 'no-store' }); }"),
('bus fetchRead ignores a redirect', B, "catch { throw new ReadError('unreachable', 'The Console server did not answer.', 0); }\n " + RD, "catch { throw new ReadError('unreachable', 'The Console server did not answer.', 0); }\n"),
('bus fetchApi without manual', B, "res = await fetch(`/api/${path}`, { cache: 'no-store', redirect: 'manual' });", "res = await fetch(`/api/${path}`, { cache: 'no-store' });"),
('bus send without manual', B, "body: JSON.stringify({ to, body: text }), cache: 'no-store', redirect: 'manual' });", "body: JSON.stringify({ to, body: text }), cache: 'no-store' });"),
('bus config without manual', B, "fetch('/api/config', { cache: 'no-store', redirect: 'manual' })", "fetch('/api/config', { cache: 'no-store' })"),
('accept: no signed-out guard', A, " if (out()) return; // a read that was in flight when the page signed out\n", ''),
('error: no signed-out guard', A, ' if (err.signedOut || out()) return;\n', ''),
('refresh reads with the toggle off', A, 'if (busy || !machine || out()) return;', 'if (busy || out()) return;'),
('schedule polls with the toggle off', A, 'if (!paused && machine && !out()) timer', 'if (!paused && !out()) timer'),
('refresh: no signed-out guard', A, 'if (busy || !machine || out()) return;', 'if (busy || !machine) return;'),
('refresh re-enables after sign-out', A, "busy = false; $('refresh').disabled = out(); schedule();", "busy = false; $('refresh').disabled = false; schedule();"),
('load: no hideMachine when off', A, ' if (!machine) hideMachine();\n', ''),
('setting not saved', A, " try { localStorage.setItem('mosaic-console-board', JSON.stringify({ machine })); } catch {}\n", ''),
('setting defaults on', A, "?.machine === true; } catch {}", "?.machine !== false; } catch {}"),
('toggle on reads nothing', A, "fresh(); refresh(); } else hideMachine();", "fresh(); } else hideMachine();"),
('hideMachine keeps the rows', A, " for (const id of ['waiting', 'seen', 'sessions']) $(id).replaceChildren();\n for (const id of ['waiting-count'", " for (const id of ['waiting-count'"),
('held: no address shows blank', B, '''${c.address ? txt(c.address) : '<span class="muted">no address</span>'}''', '''${c.address ? txt(c.address) : ''}'''),
('held: claims with no address dropped', B, 'const claims = r.rows;', 'const claims = r.rows.filter(c => c.address);'),
('Message without send', B, "</td>${canSend ? `<td>${typeof c.role", "</td>${true ? `<td>${typeof c.role"),
('Message keeps the old role', B, 'if (message) { draft.to = message.dataset.messageRole; focusSend = true;', 'if (message) { focusSend = true;'),
('Message: no focus in the form', B, 'if (message) { draft.to = message.dataset.messageRole; focusSend = true;', 'if (message) { draft.to = message.dataset.messageRole;'),
('bus render: no signed-out guard', B, " clearTimeout(timer);\n if (out()) return;\n", " clearTimeout(timer);\n"),
('bus schedule polls signed out', B, "if (!paused() && !out()) timer", "if (!paused()) timer"),
('board render draws after a signed-out read', B, " if (failed.some(err => err.code === 'signed-out')) return;\n", ''),
('bus view render draws a signed-out read', B, " if (err.code === 'signed-out') return;\n", ''),
('send after sign-out fetches', B, " if (out()) { receipt = { kind: 'refused', to: to || 'no role', code: 'sign-in'", " if (false) { receipt = { kind: 'refused', to: to || 'no role', code: 'sign-in'"),
('send redirect does not sign out', B, " if (res?.type === 'opaqueredirect') signOut();\n", ''),
('bus listener keeps the held rows', B, " $('held').replaceChildren(); $('held-count').textContent = '–'; heldHtml = null;\n", " $('held-count').textContent = '–'; heldHtml = null;\n"),
('bus listener keeps the bus page', B, " for (const el of [...$('bus-view').children]) if (el.id !== 'send-section') el.remove();\n", ''),
('app listener keeps the sessions', A, " for (const id of ['projects', 'waiting', 'seen', 'sessions']) $(id).replaceChildren();\n", ''),
('app listener: refresh stays enabled', A, " $('refresh').disabled = $('pause').disabled = true;\n", ''),
]
env = dict(os.environ, TMPDIR=os.path.expanduser('~/filbert-scratch/tmp'), DOCKER_HOST='unix:///nonexistent.sock')
start = {p: hashlib.sha256(open(os.path.join(WT, p), 'rb').read()).hexdigest() for p in (A, B)}
out = open(OUT, 'w')
killed = total = 0
for name, path, old, new in M:
full = os.path.join(WT, path)
orig = open(full, 'rb').read()
src = orig.decode()
if src.count(old) != 1:
out.write(f'BADEDIT {name}: {src.count(old)} matches\n'); out.flush(); continue
open(full, 'w').write(src.replace(old, new))
try:
total += 1
# Its own process group: a timeout kills node --test's file runners and their browsers too.
p = subprocess.Popen(['node', '--test', *TESTS], cwd=os.path.join(WT, 'packages/webui'), env=env, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, start_new_session=True)
try:
stdout = p.communicate(timeout=900)[0]
dead = p.returncode != 0
fails = [l.strip() for l in stdout.splitlines() if l.startswith('not ok')][:3]
except subprocess.TimeoutExpired:
os.killpg(p.pid, signal.SIGKILL); p.communicate()
dead, fails = True, ['timeout']
killed += dead
out.write(f"{'KILLED ' if dead else 'SURVIVED'} {name}{' <- ' + ' | '.join(fails) if fails else ''}\n"); out.flush()
finally:
open(full, 'wb').write(orig)
end = {p: hashlib.sha256(open(os.path.join(WT, p), 'rb').read()).hexdigest() for p in (A, B)}
out.write(f'{killed}/{total} killed; files restored: {start == end}\n')