Candidate on dda7e31b0: staff.request verb (pm only, STAFF-REQUEST class no other writer can produce),
request_staff pm tool and prompt line, pm and chief-of-staff contract text, 7 bus tests, tools and bundle
tests, one launcher end-to-end test. 21/21 mutants killed; node suites and all ten scripts/test-*.sh green.
Packet only; the candidate itself is row140.patch and is not applied to the tree. Nothing pushed.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
97 lines
6.2 KiB
Bash
97 lines
6.2 KiB
Bash
#!/usr/bin/env bash
|
||
# Row 140 (#1634): pm asks chief for staff, and chief hires and launches.
|
||
# Each check must fail on a mutant. For each mutant this runs one named test file against the
|
||
# real source (control) and against the source with one exact-once text edit, in the given worktree,
|
||
# one test file at a time, and restores every source file on exit.
|
||
# Usage: mutants.sh <worktree> <outdir> (MUTANT_DRY=1 only proves every edit matches exactly once)
|
||
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
|
||
if [ -n "${MUTANT_DRY:-}" ]; then echo "$1: edit ok (dry)"; return; fi
|
||
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; }
|
||
diff -u "$BAK/$3" "$3" > "$OUT/$1.patch"
|
||
run "$1" "$2"
|
||
cp "$BAK/$3" "$3"
|
||
}
|
||
B=packages/bus/src/broker.mjs; BT=packages/bus/tests/staff-request.test.mjs
|
||
T=packages/harness/src/tools.mjs; TT=packages/harness/tests/tools.test.mjs
|
||
U=packages/harness/src/bundle.mjs; UT=packages/harness/tests/bundle.test.mjs
|
||
for t in "$BT" "$TT" "$UT"; do run "control-$(basename "$t" .test.mjs)" "$t"; done
|
||
|
||
# m1: any role can send a staff request, not only the pm
|
||
mutant m1-sender-any-role "$BT" "$B" "if (b.roles[s.role]?.definition !== STAFF_FROM) fail('staff-request-refused');" ''
|
||
# m2: the request goes to the launcher whatever it is, not only a chief of staff
|
||
mutant m2-launcher-any-role "$BT" "$B" "if (!chief || b.roles[chief]?.definition !== STAFF_TO) fail('no-chief-of-staff');" "if (!chief) fail('no-chief-of-staff');"
|
||
# m3: no launcher at all still sends (to a null recipient is the broker's problem, not this verb's)
|
||
mutant m3-no-launcher-passes "$BT" "$B" "if (!chief || b.roles[chief]?.definition !== STAFF_TO) fail('no-chief-of-staff');" "if (chief && b.roles[chief]?.definition !== STAFF_TO) fail('no-chief-of-staff');"
|
||
# m4: the role definitions' messaging lists no longer apply to a staff request
|
||
mutant m4-messaging-lists "$BT" "$B" " this.#checkMessaging(s.business, this.#sender(s), chief);
|
||
const mid = randomUUID();
|
||
const list" " const mid = randomUUID();
|
||
const list"
|
||
# m5: message.send writes the STAFF-REQUEST class, so any role can forge one
|
||
mutant m5-message-send-class "$BT" "$B" " 'REACTION',
|
||
].includes(a.class)" " 'REACTION',
|
||
'STAFF-REQUEST',
|
||
].includes(a.class)"
|
||
# m6: thread.send writes the STAFF-REQUEST class
|
||
mutant m6-thread-send-class "$BT" "$B" "['REQUEST', 'INFO', 'REACTION'].includes(a.class)) fail('invalid-class');" "['REQUEST', 'INFO', 'REACTION', 'STAFF-REQUEST'].includes(a.class)) fail('invalid-class');"
|
||
# m7: the count has no upper bound
|
||
mutant m7-count-max "$BT" "$B" "|| r.count < 1 || r.count > STAFF_COUNT) fail" "|| r.count < 1) fail"
|
||
# m8: a count of zero is accepted
|
||
mutant m8-count-min "$BT" "$B" "|| r.count < 1 || r.count > STAFF_COUNT) fail" "|| r.count > STAFF_COUNT) fail"
|
||
# m9: a count may be a fraction or a string
|
||
mutant m9-count-integer "$BT" "$B" "if (!Number.isInteger(r.count) || r.count < 1" "if (r.count < 1"
|
||
# m10: the role list has no upper bound
|
||
mutant m10-roles-max "$BT" "$B" " || a.roles.length > STAFF_ROLES)" ")"
|
||
# m11: an empty role list is accepted
|
||
mutant m11-roles-min "$BT" "$B" "|| a.roles.length < 1 || a.roles.length > STAFF_ROLES)" "|| a.roles.length > STAFF_ROLES)"
|
||
# m12: the same role may be named twice, so counts could be split past the bound
|
||
mutant m12-duplicate-role "$BT" "$B" " if (new Set(a.roles.map((r) => r.role)).size !== a.roles.length) fail('invalid-request');
|
||
" ''
|
||
# m13: the task reference is not checked
|
||
mutant m13-task-ref "$BT" "$B" "if (!taskRef(a.task_ref) || !Array.isArray(a.roles)" "if (!Array.isArray(a.roles)"
|
||
# m14: a role entry may carry keys beyond role and count
|
||
mutant m14-role-entry-keys "$BT" "$B" "keys(r, ['role', 'count'], ['role', 'count']);" "keys(r, ['role', 'count', 'model', 'harness'], ['role']);"
|
||
# m15: the request goes to the sender itself, not the launcher
|
||
mutant m15-recipient "$BT" "$B" " to: chief,
|
||
cls: 'STAFF-REQUEST'," " to: s.role,
|
||
cls: 'STAFF-REQUEST',"
|
||
# m16: the log no longer names the task the request is for
|
||
mutant m16-log-task "$BT" "$B" "staff_for: a.task_ref," "staff_for: null,"
|
||
# m17: a pm without message.send is no longer refused: the action is not the one pm holds
|
||
mutant m17-action-spent "$BT" "$B" " 'message.send',
|
||
{ decision: withinRole ? null : (a.decision ?? null), target: chief ?? null }," " 'task.assign',
|
||
{ decision: withinRole ? null : (a.decision ?? null), target: chief ?? null },"
|
||
# m18: every role with message.send is offered request_staff
|
||
mutant m18-tool-any-role "$TT" "$T" 'if (resolved.definition === "pm")
|
||
add("request_staff"' 'if (true)
|
||
add("request_staff"'
|
||
# m19: the tool's count has no upper bound in its schema
|
||
mutant m19-tool-count-max "$TT" "$T" 'count: { type: "integer", minimum: 1, maximum: 20, description: "How many" },' 'count: { type: "integer", minimum: 1, description: "How many" },'
|
||
# m20: the tool sends the wrong broker verb
|
||
mutant m20-tool-verb "$TT" "$T" 'add("request_staff", "staff.request", "message.send",' 'add("request_staff", "message.send", "message.send",'
|
||
# m21: the prompt's staff line goes to every role that can send a message
|
||
mutant m21-prompt-any-role "$UT" "$U" 'if (named.has("request_staff"))' 'if (named.has("send_message"))'
|
||
echo "mutants finished"
|