#!/usr/bin/env bash
# Row 100 (#1594) mutants. Run from the candidate worktree's root:
# LOG=
bash mutants.sh > mutants.txt
# Each mutant edits one site with mut.mjs (exactly one match, else exit 3),
# runs the named test files (DRY=1 only checks the match) in their own process group (run.py, killed after
# 240 s) and restores the file. "killed" means the run failed; "survived"
# means it passed.
set -u
Q=$1
run() {
local name=$1 file=$2 from=$3 to=$4; shift 4
cp -- "$file" "$file.orig"
if ! node "$Q/mut.mjs" "$file" "$from" "$to"; then
echo "$name: NO MATCH"; mv -- "$file.orig" "$file"; return
fi
if [ -n "${DRY:-}" ]; then echo "$name: match"; mv -- "$file.orig" "$file"; return; fi
local log="${LOG:-.}/mut-$name.log"
if python3 "$Q/run.py" 240 "$log" node --test "$@"; then
echo "$name: survived"
else
echo "$name: killed ($(grep -m1 -o 'ℹ fail [0-9]*' "$log"; grep -m1 -o 'TIMEOUT.*' "$log"), $(grep -m1 '^✖' "$log" | cut -c1-90))"
fi
mv -- "$file.orig" "$file"
}
WK=packages/seat/src/working.mjs
GS=packages/cli/src/git-socket.mjs
GTL=packages/harness/src/git-tools.mjs
WT=packages/seat/tests/working.test.mjs
GT=packages/cli/tests/git-socket.test.mjs
HT=packages/harness/tests/git-tools.test.mjs
# Timeouts: SIGTERM first, then SIGKILL after the grace period.
run sigkill-first $WK $' child.kill("SIGTERM");\n kill = setTimeout(() => child.kill("SIGKILL"), limits.graceMs);' ' child.kill("SIGKILL");' $WT
run no-escalation $WK $' kill = setTimeout(() => child.kill("SIGKILL"), limits.graceMs);\n' '' $WT
# rollback-failed, refused and logged
run no-rollback-check $WK $' if (!undone.ok) refuse("rollback-failed",' $' if (false) refuse("rollback-failed",' $WT $GT
run rollback-not-logged $GS 'if (code === "git-failed" || code === "rollback-failed") log(' 'if (code === "git-failed") log(' $GT
# The five untested refusals
run no-root-check $WK ' if (!r.ok) refuse("root-unreadable",' ' if (false) refuse("root-unreadable",' $WT
run no-role-check $GS 'if (!who.role || who.business !== b.id) throw' 'if (who.business !== b.id) throw' $GT
run no-business-check $GS 'if (!who.role || who.business !== b.id) throw' 'if (!who.role) throw' $GT
run no-project-check $GS ' if (!p) throw new Refusal("unknown-project");' '' $GT
run no-busy-check $GS 'if (active.size >= maxActive) return' 'if (false) return' $GT
run no-closing-answer $GS $' waiting.set(socket, () => done(new Refusal("host-closing")));\n' '' $GT
# The body's total deadline
run no-body-deadline $GS '), deadlineMs);' '), 2 ** 31 - 1);' $GT
# fetch-aborted
run abort-as-git-failed $GS 'const aborted = () => new Refusal("fetch-aborted",' 'const aborted = () => new Error(' $GT
# A reviewer that hangs up once it has every byte was served. The first cut
# filed any close before 'finish' as fetch-aborted; so does this mutant.
run close-is-aborted $GS 'const served = () => !socket.errored;' 'const served = () => socket.writableFinished;' $GT
# A queued write that fails is fetch-aborted.
run errored-ignored $GS 'const served = () => !socket.errored;' 'const served = () => true;' $GT
# have: working refs first, at most HAVE_MAX
run have-origin-first $GTL 'for (const remote of ["refs/remotes/working", "refs/remotes/origin"])' 'for (const remote of ["refs/remotes/origin", "refs/remotes/working"])' $HT
run have-uncapped $GTL 'if (OID.test(oid) && have.size < HAVE_MAX) have.add(oid);' 'if (OID.test(oid)) have.add(oid);' $HT