#!/usr/bin/env bash # Row 109 (#1603), round 3 adds m7-m12: the two re-anchored tests (seat session, cli launcher) and the two N2 tests. # Each new check must fail on a mutant. For each mutant this runs the named test # file against the real source (control) and against the source with one exact-once text edit. # Runs in the given worktree, one test file at a time, and restores every source file on exit. # Usage: mutants.sh set -u WT=$(cd "$1" && pwd); OUT=$(mkdir -p "$2" && cd "$2" && pwd) cd "$WT" || exit 2 BAK=$(mktemp -d "$OUT/.bak.XXXXXX") declare -A SAVED save() { [ -n "${SAVED[$1]:-}" ] || { mkdir -p "$BAK/$(dirname "$1")"; cp "$1" "$BAK/$1"; SAVED[$1]=1; }; } restore() { for f in "${!SAVED[@]}"; do cp "$BAK/$f" "$f"; done; rm -rf "$BAK"; } trap restore EXIT edit() { # file old new: exact-once replace save "$1" node -e ' const fs = require("fs"); const [f, a, b] = process.argv.slice(1); const s = fs.readFileSync(f, "utf8"); const n = s.split(a).length - 1; if (n !== 1) { console.error("expected one match, found " + n); process.exit(3); } fs.writeFileSync(f, s.replace(a, () => b)); ' "$1" "$2" "$3" } run() { # name testfile setsid -w timeout -k 10 400 node --test "$2" > "$OUT/$1.out" 2>&1; echo "$?" > "$OUT/$1.rc" echo "$1: exit $(cat "$OUT/$1.rc"); $(grep -E '^ℹ (tests|pass|fail)' "$OUT/$1.out" | tr '\n' ' ')" grep -E '^✖' "$OUT/$1.out" | sort -u | sed 's/ ([0-9.]*ms)$//' | sed "s/^/ failed: /" } mutant() { # name testfile file old new edit "$3" "$4" "$5" || { echo "$1: edit failed"; return; } git diff -- "$3" > "$OUT/$1.patch" run "$1" "$2" cp "$BAK/$3" "$3" } R=packages/harness/src/runner.mjs; RT=packages/harness/tests/runner.test.mjs B=packages/bus/src/broker.mjs; BT=packages/bus/tests/state.test.mjs run control-runner "$RT" run control-bus "$BT" # m1: the turn request no longer carries the state block mutant m1-drop-block "$RT" "$R" 'request: `${block}\n${turnRequest(m)}`' 'request: turnRequest(m)' # m2: the header no longer carries the message time mutant m2-drop-time "$RT" "$R" ' if (m.at) head.push(`sent ${m.at}`); ' '' # m3: a failed state read no longer fails the turn; the turn runs with no block mutant m3-drop-failure-check "$RT" "$R" 'unread = `could not read the current state: ${e.code ?? e.message}`;' 'block = "";' # m4: the block's launch line shows the older event instead of the latest mutant m4-oldest-launch-event "$BT" "$B" "kind IN ('launch.revoked','launch.restored') ORDER BY seq DESC LIMIT 1" "kind IN ('launch.revoked','launch.restored') ORDER BY seq ASC LIMIT 1" # m5: the open list includes closed tasks mutant m5-closed-in-open "$BT" "$B" 'const open = this.#tasks(s.business, { done: false });' 'const open = this.#tasks(s.business, { limit: TASK_PAGE });' # m6: the cap isn't stated in the output mutant m6-cap-unstated "$BT" "$B" 'total: open.total, cap: TASK_PAGE }' 'total: open.total }' # Round 3. SE and CL are the seat and cli tests that now anchor the new request shape. SE=packages/seat/tests/session.test.mjs; CL=packages/cli/tests/launcher.test.mjs run control-seat "$SE" run control-cli "$CL" # m7, m9: the turn request no longer carries the state block mutant m7-seat-drop-block "$SE" "$R" 'request: `${block}\n${turnRequest(m)}`' 'request: turnRequest(m)' mutant m9-cli-drop-block "$CL" "$R" 'request: `${block}\n${turnRequest(m)}`' 'request: turnRequest(m)' # m8, m10: the header no longer carries the message time mutant m8-seat-drop-time "$SE" "$R" ' if (m.at) head.push(`sent ${m.at}`); ' '' mutant m10-cli-drop-time "$CL" "$R" ' if (m.at) head.push(`sent ${m.at}`); ' '' # m11: a human session is checked as an agent, so it can't read state mutant m11-human-checked-as-agent "$BT" "$B" 'if (!s.human && !s.reader) this.#agent(s); const set' 'if (!s.reader) this.#agent(s); const set' # m12: the state read writes an event mutant m12-state-writes-event "$BT" "$B" " case 'state': { keys(a, []);" " case 'state': { keys(a, []); this.#event(s, 'action.allowed', {});"