#!/usr/bin/env bash
# Row 103 (#1597) 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"
}
CO=packages/seat/src/checkout.mjs
GTL=packages/harness/src/git-tools.mjs
GS=packages/cli/src/git-socket.mjs
HM=packages/cli/src/host.mjs
CT=packages/seat/tests/checkout.test.mjs
HT=packages/harness/tests/git-tools.test.mjs
GT=packages/cli/tests/git-socket.test.mjs
HOT=packages/cli/tests/host.test.mjs
TERM_THEN_KILL=$' group("SIGTERM");\n kill = setTimeout(() => group("SIGKILL"), limits.graceMs);'
ESCALATE=$' kill = setTimeout(() => group("SIGKILL"), limits.graceMs);\n'
# Host git (checkout.mjs runGit, which working.mjs uses too): SIGTERM, the
# grace period, SIGKILL, all to git's process group, and the sweep.
run co-sigkill-first $CO "$TERM_THEN_KILL" ' group("SIGKILL");' $CT
run co-no-escalation $CO "$ESCALATE" '' $CT
run co-not-detached $CO 'spawn("git", argv, { env, detached: true,' 'spawn("git", argv, { env, detached: false,' $CT
run co-pid-not-group $CO 'process.kill(-child.pid, signal);' 'process.kill(child.pid, signal);' $CT
run co-no-sweep $CO ' if (signalled) group("SIGKILL");' '' $CT
# Runner git (git-tools.mjs): the same sequence.
run gt-sigkill-first $GTL "$TERM_THEN_KILL" ' group("SIGKILL");' $HT
run gt-no-escalation $GTL "$ESCALATE" '' $HT
run gt-not-detached $GTL ' detached: true,' ' detached: false,' $HT
run gt-pid-not-group $GTL 'process.kill(-child.pid, signal);' 'process.kill(child.pid, signal);' $HT
run gt-no-sweep $GTL ' if (signalled) group("SIGKILL");' '' $HT
# have: working refs first, newest first within a clone, 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-oldest-first $GTL '"--sort=-committerdate"' '"--sort=committerdate"' $HT
run have-uncapped $GTL 'if (OID.test(oid) && have.size < HAVE_MAX) have.add(oid);' 'if (OID.test(oid)) have.add(oid);' $HT
# close(): the cap on the wait for told sockets, then destroy.
run letgo-no-cap $GS 'const capped = new Promise((r) => (timer = setTimeout(r, ms)));' 'const capped = new Promise(() => {});' $GT
run letgo-no-destroy $GS ' for (const s of sockets) s.destroy();' '' $GT
# Expected to survive: a told socket on a real unix socket flushes its few
# bytes whether or not the peer reads (build.md).
run close-skips-letgo $GS ' await letGo([...told]);' '' $GT
# send()'s post-loop arm, and a failed write anywhere.
run postloop-always-served $GS $' if (socket.destroyed) {\n if (served()) return;\n throw aborted();\n }' ' if (socket.destroyed) return;' $GT
run postloop-always-aborted $GS $' if (socket.destroyed) {\n if (served()) return;\n throw aborted();\n }' ' if (socket.destroyed) throw aborted();' $GT
run errored-ignored $GS 'const served = () => !socket.errored;' 'const served = () => true;' $GT
# The idle limit on bundle bytes.
run no-idle $GS ' socket.setTimeout(idleMs);' ' socket.setTimeout(0);' $GT
# host.lock: an atomic claim, the live check, the identity check on the
# moved-aside file, the release on each boot failure and on close, the lock
# kept apart from host.json, and the check for a host that holds only host.json.
run claim-rename $HM ' linkSync(tmp, file);' ' renameSync(tmp, file);' $HOT
run claim-no-live-check $HM ' if (prior.live) throw' ' if (false) throw' $HOT
run claim-dead-blocks $HM ' if (prior.live) throw' ' if (prior) throw' $HOT
run claim-no-identity-check $HM ' if (!same(readState(aside), prior)) {' ' if (false) {' $HOT
run claim-no-release $HM $' if (same(held, state)) rmSync(hostLock(dataRoot), { force: true });' '' $HOT
run release-boot-timeout $HM $' await ended(broker, 5000);\n release();\n throw e;' $' await ended(broker, 5000);\n throw e;' $HOT
run release-broker-refused $HM $' release();\n throw new CliError(`broker refused' $' throw new CliError(`broker refused' $HOT
run release-notifier-dies $HM $' release();\n throw e;' ' throw e;' $HOT
run release-notifier-refused $HM $' release();\n throw new CliError(`notifier refused' $' throw new CliError(`notifier refused' $HOT
run release-on-close $HM $' release();\n finish(result);' ' finish(result);' $HOT
run lock-is-host-json $HM 'join(hostDir(dataRoot), "host.lock")' 'join(hostDir(dataRoot), "host.json")' $HOT
run no-host-json-check $HM ' if (prior?.live) throw new CliError(' ' if (false) throw new CliError(' $HOT