Files
stack/agents/darkwing/work/queue-116/r2/mutants.py
T
jason.woltjeandClaude Opus 5.5 0a4076e8e6 row 116 round 2 builder packet: C1 cases and mutants, D1
Six cases for Filbert's C1 (f1 split into actor_role and actor_run), the
earlier-merge dry run, a node_modules canary with s27, and the TOOLS.md
usage line. 84 cases, 55 of 56 mutants killed (s26 equivalent), targeted
gate green. Candidate is build.patch against 1223a0f37; not committed.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-14 14:11:59 -05:00

111 lines
7.4 KiB
Python

#!/usr/bin/env python3
"""Row 116 mutants: one per check. Each edits land-work.sh or read.mjs,
runs scripts/test-land-work.sh, and restores the file from a .orig copy."""
import hashlib, os, re, shutil, signal, subprocess, sys
WT = os.path.expanduser('~/darkwing-scratch/r116/wt')
PKT = os.path.expanduser('~/darkwing-scratch/r116/pkt')
SH = 'scripts/land-work.sh'
RD = 'scripts/land-work/read.mjs'
M = [
# read.mjs
('r01-schema-digest', RD, "if (digest !== expected) refuse(", "if (false) refuse("),
('r02-schema-meta', RD, "if (meta('schema_version') !== SCHEMA_VERSION || meta('schema_digest') !== expected)", "if (false)"),
('r03-read-only', RD, "{ readOnly: true, timeout", "{ readOnly: false, timeout"),
('r04-business-scope', RD, "business = ?", "? IS NOT NULL", True),
('r05-one-raise', RD, "if (!d || raises.length !== 1)", "if (!d)"),
('r06-not-a-merge', RD, "if (d.action !== MERGE || raise.action !== MERGE)", "if (false)"),
('r07-resolved-op', RD, "closes.length !== 1 || closes[0].op !== 'resolved'", "closes.length !== 1"),
('r08-human-by', RD, "res.by !== business.human ||", ""),
('r09-human-via', RD, "res.via !== 'cli' ||", ""),
('r10-human-answer', RD, "const answer = answers.length === 1 ? answers[0] : null;",
"const answer = answers[0] ?? { actor_role: null, actor_run: null, body: JSON.stringify({ choice: res.choice }) };"),
('r11-merge-option', RD, "if (res.choice !== raise.approvalChoice)", "if (false)"),
('r12-one-work-ref', RD, "named.length === 1 ?", "named.length >= 1 ?"),
('r13-work-ref-shape', RD, "WORK_REF.exec(named[0])", "/^work\\/[a-z]*-?([^-]*)-?(.*)$/.exec(named[0])"),
('r14-task-ref-agrees', RD, "if (ref != null && ref !== task)", "if (false)"),
('r15-target', RD, "if (raise.target !== 'refactor' || raise.target !== base)", "if (false)"),
('r16-later-supersedes', RD, "(r) => r.supersedes === id ||", "(r) =>"),
('r17-later-task', RD, "r.task_ref === task ||", ""),
('r18-later-question', RD, " || (r.question.match(ANY_WORK_REF) ?? []).includes(workRef)", ""),
('r19-no-verdict', RD, "if (!v) refuse('no-verdict'", "if (false) refuse('no-verdict'"),
('r20-latest-verdict', RD, "ORDER BY seq DESC LIMIT 1", "ORDER BY seq LIMIT 1"),
('r21-approve', RD, "vb.verdict !== 'approve' ||", ""),
('r22-verdict-task', RD, "vb.task_ref !== task ||", ""),
('r23-full-oid', RD, "!OID.test(vb.commit ?? '')", "false"),
# round 2, Filbert's C1 (f1 split into r24 and r25, f2 r26, f3 r27, f4 r28, f5 r29)
('r24-answer-role', RD, "answer.actor_role !== null ||", ""),
('r25-answer-run', RD, "answer.actor_run !== null ||", ""),
('r26-answer-choice', RD, "json(answer.body)?.choice !== res.choice", "false"),
('r27-one-answer', RD, "answers.length === 1 ? answers[0] : null", "answers.length >= 1 ? answers[0] : null"),
('r28-raise-task-ref', RD, "for (const ref of [d.task_ref, raise.task_ref])", "for (const ref of [d.task_ref])"),
('r29-later-only', RD, "(seq > ? AND action = ?)", "(seq <> ? AND action = ?)"),
# land-work.sh
('s01-base-early', SH, '[ "$BASE" = "refactor" ] || refuse target-branch', 'true || refuse target-branch'),
('s02-ref-moved', SH, '[ "$AT" = "$C" ] || refuse ref-moved', 'true || refuse ref-moved'),
('s03-merge-base', SH, '|| refuse base-conflict "$C shares no history with $BASE"', '|| true'),
('s04-empty-diff', SH, '[ -s "$SCRATCH/reviewed.patch" ] || refuse', 'true || refuse'),
('s05-root-top-file', SH, '*) ROOT_HITS="$ROOT_HITS $f"; continue ;;', '*) continue ;;'),
('s06-root-new-dir', SH, 'if [ "$known" = "no" ] || [ "$top" = "src" ]', 'if [ "$top" = "src" ]'),
('s07-root-src-v1', SH, ' || [ "$top" = "src" ] || [ "$top" = "v1" ]; then', '; then'),
('s08-dirty-changed', SH, 'lgit "$CHECKOUT" diff --quiet HEAD -- "${PATHS[@]}" || dirty="changed"', ':'),
('s09-dirty-staged', SH, 'lgit "$CHECKOUT" diff --cached --quiet HEAD -- "${PATHS[@]}" || dirty="staged"', ':'),
('s10-dirty-untracked', SH, '[ -e "$CHECKOUT/$f" ]; then dirty="untracked $f"; fi', 'false; then :; fi'),
('s11-dirty-recheck', SH, '\ncheckout_clean\ngit -C "$WT" commit', '\ngit -C "$WT" commit'),
('s12-apply-conflict', SH, '|| refuse base-conflict "$(tail -n1 "$SCRATCH/apply.err")"', '|| true'),
('s13-patch-drift', SH, '[ "$APPLIED_ID" = "$REVIEWED_ID" ] || refuse', 'true || refuse'),
('s14-gate-node-modules', SH, '[ -d "$TARGET/node_modules" ] || fail', 'true || fail'),
('s15-gate-step-fails', SH, '[ "$rc" -eq 0 ] || refuse gate-failed', 'true || refuse gate-failed'),
('s16-gate-skips-queue', SH, '[ "$p" = queue ] && continue', ':'),
('s17-gate-scripts', SH, 'for s in "$WT"/scripts/test-*.sh; do', 'for s in; do'),
('s18-base-moved-cas', SH, '"refs/heads/$BASE" "$NEW" "$OLD" 2>/dev/null', '"refs/heads/$BASE" "$NEW" 2>/dev/null'),
('s19-dry-run', SH, 'if [ "$DRY_RUN" = "yes" ]; then', 'if false; then'),
('s20-checkout-sync', SH, 'if [ -n "$CHECKOUT" ]; then\n # HEAD is now NEW', 'if false; then\n # HEAD is now NEW'),
('s21-subject', SH, '[ -n "$SUBJECT" ] || SUBJECT="$(value subject)"', '[ -n "$SUBJECT" ] || SUBJECT="land work"'),
('s22-attribution', SH, '${ATTRIBUTION:-Applied-under: scripts/land-work.sh (conductor)}\n', ''),
('s23-cleanup-worktree', SH, 'git -C "$TARGET" worktree remove --force "$WT" >/dev/null 2>&1 || rm -rf "$WT"', ':'),
('s24-cleanup-network', SH, 'docker network rm "${WT_NAME}_default" >/dev/null 2>&1 ||', 'false ||'),
('s25-cleanup-scratch', SH, ' rm -rf "$SCRATCH"\n', '\n'),
('s26-cleanup-link', SH, '[ -L "$WT/node_modules" ] && rm -f "$WT/node_modules"', ':'),
# round 2: cleanup that follows the link and empties the target's node_modules
('s27-cleanup-follows-link', SH, '[ -L "$WT/node_modules" ] && rm -f "$WT/node_modules"', 'rm -rf "$WT/node_modules/"'),
]
def sha(p):
return hashlib.sha256(open(p, 'rb').read()).hexdigest()
only = set(sys.argv[1:])
os.chdir(WT)
env = dict(os.environ, TMPDIR=os.path.join(os.path.dirname(WT), 'tmp'), NO_COLOR='1')
base = {f: sha(f) for f in (SH, RD)}
log = open(os.path.join(PKT, 'mutants-run.log'), 'a')
for name, f, old, new, *rest in M:
if only and name not in only:
continue
src = open(f).read()
n = src.count(old)
if n == 0 or (n > 1 and not rest):
print(f'{name}: pattern found {n} times, skipped', file=log, flush=True); print(name, 'BAD PATTERN', n); continue
shutil.copy2(f, f + '.orig')
try:
open(f, 'w').write(src.replace(old, new))
p = subprocess.Popen(['bash', 'scripts/test-land-work.sh'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
env=env, start_new_session=True, text=True)
try:
out, _ = p.communicate(timeout=600)
except subprocess.TimeoutExpired:
os.killpg(p.pid, signal.SIGKILL); out, _ = p.communicate(); out += '\nTIMEOUT\n'
open(os.path.join(PKT, 'mutants', f'mutant-{name}.log'), 'w').write(out)
fails = [l[5:] for l in out.splitlines() if l.startswith('FAIL')]
tally = (re.findall(r'selftests: (\d+) passed, (\d+) failed', out) or [('?', '?')])[-1]
verdict = 'killed' if p.returncode != 0 else 'SURVIVED'
print(f'{name}: {verdict} (rc {p.returncode}, {tally[1]} fail of {int(tally[0]) + int(tally[1]) if tally[0] != "?" else "?"})', file=log, flush=True)
for l in fails:
print(f' {l}', file=log, flush=True)
print(name, verdict, len(fails), flush=True)
finally:
shutil.move(f + '.orig', f)
assert sha(f) == base[f], f'{f} not restored'
print('restored:', all(sha(f) == base[f] for f in base), file=log, flush=True)