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]>
This commit is contained in:
2026-10-12 16:31:43 -05:00
co-authored by Claude Opus 5.5
parent f9db19d7af
commit 27eda72309
31 changed files with 2237 additions and 0 deletions
@@ -0,0 +1,79 @@
#!/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')
@@ -0,0 +1,36 @@
SURVIVED app getJSON without manual
KILLED app api GET without manual
SURVIVED app api POST without manual
KILLED app api ignores a redirect
KILLED bus fetchRead without manual
KILLED bus fetchRead ignores a redirect
SURVIVED bus fetchApi without manual
KILLED bus send without manual
KILLED bus config without manual
KILLED accept: no signed-out guard
SURVIVED error: no signed-out guard
KILLED refresh reads with the toggle off
SURVIVED schedule polls with the toggle off
SURVIVED refresh: no signed-out guard
KILLED refresh re-enables after sign-out
KILLED load: no hideMachine when off
KILLED setting not saved
KILLED setting defaults on
KILLED toggle on reads nothing <- timeout
KILLED hideMachine keeps the rows
KILLED held: no address shows blank
KILLED held: claims with no address dropped
KILLED Message without send
KILLED Message keeps the old role
KILLED Message: no focus in the form
KILLED bus render: no signed-out guard
SURVIVED bus schedule polls signed out
SURVIVED board render draws after a signed-out read
SURVIVED bus view render draws a signed-out read
KILLED send after sign-out fetches
KILLED send redirect does not sign out
KILLED bus listener keeps the held rows
KILLED bus listener keeps the bus page
KILLED app listener keeps the sessions
KILLED app listener: refresh stays enabled
26/35 killed; files restored: True
+36
View File
@@ -0,0 +1,36 @@
KILLED app getJSON without manual
KILLED app api GET without manual
KILLED app api POST without manual
KILLED app api ignores a redirect
KILLED bus fetchRead without manual
KILLED bus fetchRead ignores a redirect
KILLED bus fetchApi without manual
KILLED bus send without manual
KILLED bus config without manual
KILLED accept: no signed-out guard
KILLED error: no signed-out guard
KILLED refresh reads with the toggle off
SURVIVED schedule polls with the toggle off
SURVIVED refresh: no signed-out guard
KILLED refresh re-enables after sign-out
KILLED load: no hideMachine when off
KILLED setting not saved
KILLED setting defaults on
KILLED toggle on reads nothing <- timeout
KILLED hideMachine keeps the rows
KILLED held: no address shows blank
KILLED held: claims with no address dropped
KILLED Message without send
KILLED Message keeps the old role
KILLED Message: no focus in the form
KILLED bus render: no signed-out guard
SURVIVED bus schedule polls signed out
SURVIVED board render draws after a signed-out read
SURVIVED bus view render draws a signed-out read
KILLED send after sign-out fetches
KILLED send redirect does not sign out
KILLED bus listener keeps the held rows
KILLED bus listener keeps the bus page
KILLED app listener keeps the sessions
KILLED app listener: refresh stays enabled
30/35 killed; files restored: True
+2
View File
@@ -0,0 +1,2 @@
mutants start 2026-10-12T21:03:18Z
mutants end 2026-10-12T21:27:35Z rc=0