Files
jason.woltje 158205c5cc lead: commit darkwing's untracked evidence (348 files: queue-137 queue-141 queue-150 queue-162 queue-165 queue-166 queue-167 queue-169 queue-179 queue-180 queue-181 queue-182 queue-185 queue-186 queue-187 queue-188 queue-190 queue-93)
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.
2026-11-02 15:05:14 -06:00

171 lines
8.5 KiB
Python
Raw Permalink 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
# Row 169 mutants (agents/darkwing/work/queue-169/BUILD.md). Each mutant is exact-string edits to
# scripts/stack-release.sh in the r169 worktree; the test it names must fail. CONTROL makes no edit
# and must pass. The touched file is backed up first, restored after each mutant and checked by
# sha256. Run with python3 -I, only on Sage's slot go, behind busy.sh. DRY=1 checks the anchors
# without running anything.
import hashlib, os, re, shutil, signal, subprocess, sys, time
HOME = os.environ["HOME"]
R = f"{HOME}/darkwing-scratch/r169"
WT = f"{R}/wt"
OUT = f"{R}/out/mutants-{time.strftime('%Y%m%dT%H%M%SZ', time.gmtime())}"
SR = "scripts/stack-release.sh"
T_SR = "cli/tests/stack-release.test.mjs"
M = []
def mut(mid, what, edits, tests):
M.append((mid, what, edits, tests))
# Each test is "pkg/file::T-number"; the runner passes --test-name-pattern for the T-number.
T32, T33, T34, T35, T36 = (f"{T_SR}::T{n}" for n in range(32, 37))
mut("CONTROL", "no edit; T32 to T36 pass", [], [T32, T33, T34, T35, T36])
# Item 1
mut("1a", "pm-after-harness-target removed", [
(SR, ' plugins has "$TO" harness 2>/dev/null || { check_refuse pm-after-harness-target "$TO has no harness plug-in kind, so there is nothing to wait for; use --pm"; return 0; }\n', ''),
], [T32])
mut("1b", "pm-after-harness-set removed", [
(SR, ' [ "$out" = none ] || check_refuse pm-after-harness-set "the business file already sets harness $out; use --pm"\n', ''),
], [T32])
mut("1c", "--pm with --pm-after-harness no longer a usage error", [
(SR, ' [ "$WANT_PM" = 0 ] || [ "$AFTER_HARNESS" = 0 ] || usage\n', ''),
], [T32])
mut("1d", "--pm-after-harness accepts a pointer without pm", [
(SR, ' [ "$P_PM" = 1 ] || d+=$\'\\n\'" pm: pointer $P_PM, --pm-after-harness needs a pointer with pm 1"\n', ' :\n'),
], [T32])
mut("1e", "phase 1 installs and gates --pm", [
(SR, 'then RUN_PM=0 RUN_PENDING=1\n', 'then RUN_PM=1 RUN_PENDING=1\n'),
], [T32])
mut("1f", "phase 1 writes no pmPending", [
(SR, '...(pending==="1"?{pmPending:true}:{})', '...({})'),
], [T32])
# Item 2
mut("2a", "read_pointer takes any pmPending", [
(SR, '&&(p.pmPending===undefined||(p.pmPending===true&&p.pm===false))', ''),
], [T33])
mut("2b", "read_pointer takes pmPending beside pm true", [
(SR, '(p.pmPending===true&&p.pm===false)', 'p.pmPending===true'),
], [T33])
mut("2c", "pm-pending removed", [
(SR, ' [ "$P_PENDING" = 0 ] || check_refuse pm-pending "the pointer is pending (activate --pm-after-harness); run pm-on or rollback first"\n', ''),
], [T33])
mut("2d", "status prints no pending line", [
(SR, ' [ "$P_PENDING" = 0 ] || echo "pm: pending (activate --pm-after-harness); next: Jason\'s plugins set of harness, then stack-release.sh pm-on $BUSINESS"\n', ''),
], [T33])
# Item 3
mut("3a", "not-pending removed", [
(SR, ' [ "$P_PENDING" = 1 ] || refuse not-pending "the pointer is not pending; pm-on follows activate --pm-after-harness"\n', ''),
], [T34])
mut("3b", "harness-unset for a missing harness removed", [
(SR, ' [ "$out" != none ] || refuse harness-unset "the business file sets no harness; Jason sets it with plugins set first"\n', ''),
], [T34])
mut("3c", "harness-unset for an empty list removed", [
(SR, "process.exit(Array.isArray(h)&&h.length>0?0:1)", "process.exit(Array.isArray(h)?0:1)"),
], [T34])
mut("3d", "pm-on installs without --pm", [
(SR, 'scripts/bus-service.sh install --copy "$P_COPY" --copies "$COPIES" --pm --claude', 'scripts/bus-service.sh install --copy "$P_COPY" --copies "$COPIES" --claude'),
], [T34])
mut("3e", "pm-on gates without pm_launched", [
(SR, ' gate "$P_COPY" 1 || pm_on_fail "$GATE"\n', ' gate "$P_COPY" 0 || pm_on_fail "$GATE"\n'),
], [T34])
mut("3f", "a failed pm-on keeps the --pm unit", [
(SR, ' elif ! put_back; then pcode=put-back\n', ''),
], [T34])
mut("3g", "a failed pm-on skips the phase-1 gate", [
(SR, ' elif ! gate "$P_COPY" 0; then pcode="$GATE"\n', ''),
], [T34])
# Item 4
mut("4a", "rollback from pending installs without --pm", [
(SR, ' elif [ "$P_PENDING" = 1 ]; then RUN_PM=1\n', ''),
], [T35])
mut("4b", "unknown-kind-for-target removed", [
(SR, ' if [ -n "$out" ]; then\n [ "$OP" != rollback ]', ' if false; then\n [ "$OP" != rollback ]'),
], [T35])
mut("4c", "unknown-kind-for-target names only the first kind", [
(SR, '.filter((k)=>!Object.hasOwn(KINDS,k)).join(" ")', '.filter((k)=>!Object.hasOwn(KINDS,k)).slice(0,1).join(" ")'),
], [T35])
# --plan (brief item 4, last point)
mut("5a", "--plan prints no pending pointer line", [
(SR, ' [ "$P_PENDING" = 0 ] || echo "pm: the pointer is pending (activate --pm-after-harness); the bus runs without its PM"\n', ''),
], [T36])
mut("5b", "--plan lists no refusal", [
(SR, ' for r in "${PLAN_REFUSE[@]}"; do echo "refuses $r"; done\n', ''),
], [T36])
mut("5c", "--plan skips the plug-in checks", [
(SR, ' if [ "$PLAN" = 1 ]; then check_target; plugin_checks; plan; exit 0; fi\n precheck\n plugin_checks\n new_activation\n run_steps migrate', ' if [ "$PLAN" = 1 ]; then check_target; plan; exit 0; fi\n precheck\n plugin_checks\n new_activation\n run_steps migrate'),
], [T36])
def sha(p):
return hashlib.sha256(open(p, "rb").read()).hexdigest()
def main():
only = set(sys.argv[1:])
dry = bool(os.environ.get("DRY"))
files = [SR]
pristine = {}
for f in files:
p = f"{R}/edit/pristine-{f.replace('/', '_')}"
shutil.copy2(f"{WT}/{f}", p)
if sha(p) != sha(f"{WT}/{f}"):
sys.exit(f"ABORT: backup of {f} failed")
pristine[f] = (p, sha(p))
if not dry:
os.makedirs(OUT, exist_ok=False)
env = dict(os.environ, TMPDIR=f"{R}/tmp")
results = []
try:
for mid, what, edits, tests in M:
if only and mid not in only:
continue
texts = {}
for f, old, new in edits:
text = texts.get(f, open(pristine[f][0]).read())
if text.count(old) != 1:
sys.exit(f"ABORT {mid}: expected 1 match of {old[:80]!r} in {f}, found {text.count(old)}")
texts[f] = text.replace(old, new)
if dry:
print(f"{mid} ok {what}", flush=True); continue
for f, text in texts.items():
open(f"{WT}/{f}", "w").write(text)
t0 = time.time(); per = []
try:
for t in tests:
path, tn = t.split("::")
pkg, rel = path.split("/", 1)
log = f"{OUT}/{mid}_{tn}.log"
with open(log, "w") as fh:
p = subprocess.Popen(["node", "--test", f"--test-name-pattern=^{tn} ", rel], cwd=f"{WT}/packages/{pkg}", env=env,
stdout=fh, stderr=subprocess.STDOUT, start_new_session=True)
try:
rc = p.wait(timeout=600)
except subprocess.TimeoutExpired:
os.killpg(p.pid, signal.SIGKILL); p.wait(); rc = "timeout"
body = open(log).read()
fails = re.search(r"^ℹ fail (\d+)", body, re.M)
passes = re.search(r"^ℹ pass (\d+)", body, re.M)
per.append((tn, rc, int(fails.group(1)) if fails else -1, int(passes.group(1)) if passes else -1))
finally:
for f in texts:
shutil.copy2(pristine[f][0], f"{WT}/{f}")
if sha(f"{WT}/{f}") != pristine[f][1]:
sys.exit(f"ABORT {mid}: restore of {f} failed")
if mid == "CONTROL":
verdict = "PASS" if all(rc == 0 and n == 0 and k > 0 for _, rc, n, k in per) else "FAIL"
else:
verdict = "KILLED" if any(rc != 0 and n > 0 for _, rc, n, _ in per) else "SURVIVED"
detail = " ".join(f"{t}:rc={rc},fail={n},pass={k}" for t, rc, n, k in per)
print(f"{mid} {verdict} {time.time()-t0:.0f}s {what} | {detail}", flush=True)
results.append((mid, verdict))
finally:
for f, (p, s) in pristine.items():
shutil.copy2(p, f"{WT}/{f}")
if sha(f"{WT}/{f}") != s:
print(f"RESTORE FAILED {f}", flush=True)
if dry:
print(f"DRY {len(M)} mutants ok"); return
surv = [m for m, v in results if v not in ("KILLED", "PASS")]
print(f"DONE {len(results)} mutants, {len(results)-len(surv)} killed, survived: {surv} OUT={OUT}", flush=True)
sys.exit(1 if surv else 0)
main()