Candidate diff and manifest against 66ea40580; messaging suite now 12 tests; gate green; 21/21 mutants caught (m16, m17 added). No push. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
94 lines
5.2 KiB
Bash
Executable File
94 lines
5.2 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# Row 120 (#1614): the broker limits who the assistant sends to and hears from.
|
||
# 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>
|
||
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; }
|
||
diff -u "$BAK/$3" "$3" > "$OUT/$1.patch"
|
||
run "$1" "$2"
|
||
cp "$BAK/$3" "$3"
|
||
}
|
||
BT=packages/business/tests; MSG=packages/bus/tests/messaging.test.mjs
|
||
ROLE=$BT/role.test.mjs; AST=$BT/assistant.test.mjs
|
||
B=packages/bus/src/broker.mjs; R=packages/business/src/role.mjs; A=roles/assistant.json
|
||
for t in "$MSG" "$ROLE" "$AST"; do run "control-$(basename "$t" .test.mjs)" "$t"; done
|
||
|
||
# m1: drop the sender-side check (the sender's sendTo)
|
||
mutant m1-drop-sender-side-check "$MSG" "$B" "if (out && !out.sendTo.includes(target)) fail('message-recipient-refused');" "if (false) fail('message-recipient-refused');"
|
||
# m2: drop the target-side check (the target's receiveFrom)
|
||
mutant m2-drop-target-side-check "$MSG" "$B" "if (inbound && !inbound.receiveFrom.includes(sender)) fail('message-sender-refused');" "if (false) fail('message-sender-refused');"
|
||
# m3: drop the refusal log for these two refusals
|
||
mutant m3-drop-refusal-log "$MSG" "$B" "const error = e instanceof BusError ? e : new BusError(other);" "const error = e instanceof BusError ? e : new BusError(other);
|
||
if (error.code.startsWith('message-')) return error;"
|
||
# m4, m5: each direction reads the other direction's list
|
||
mutant m4-sender-reads-receive-list "$MSG" "$B" "out.sendTo.includes(target)" "out.receiveFrom.includes(target)"
|
||
mutant m5-target-reads-send-list "$MSG" "$B" "inbound.receiveFrom.includes(sender)" "inbound.sendTo.includes(sender)"
|
||
# m6: the tester is exempt from the target's receive list
|
||
mutant m6-tester-exempt "$MSG" "$B" "if (inbound && !inbound.receiveFrom" "if (inbound && !s.tester && !inbound.receiveFrom"
|
||
# m7, m8: a role without the field is refused instead of unchanged (each direction)
|
||
mutant m7-no-field-refuses-sender "$MSG" "$B" "definition(s.role)?.messaging;" "definition(s.role)?.messaging ?? { sendTo: [], receiveFrom: [] };"
|
||
mutant m8-no-field-refuses-target "$MSG" "$B" "definition(to)?.messaging;" "definition(to)?.messaging ?? { sendTo: [], receiveFrom: [] };"
|
||
# m9: the lists are matched on instance names, not definition names
|
||
mutant m9-target-by-instance-name "$MSG" "$B" ": b.roles[to]?.definition;" ": to;"
|
||
# m10: the constructor stops checking the shape
|
||
mutant m10-boot-shape-unchecked "$MSG" "$B" "fail('invalid-messaging');" "void 0;"
|
||
# m11: the validator accepts the tester in a list
|
||
mutant m11-validator-accepts-tester "$ROLE" "$R" 'if (entry === "tester") refuse(' 'if (false) refuse('
|
||
# m12: the validator drops the field from the loaded role
|
||
for pair in "a $ROLE" "b $AST"; do
|
||
set -- $pair
|
||
mutant "m12$1-loader-drops-field" "$2" "$R" 'messaging: document.messaging === undefined ? null : checkMessaging(document.messaging),' 'messaging: null,'
|
||
done
|
||
# m13: the shipped assistant file lets it send to the PM, or hear from the coder
|
||
for pair in "a $MSG" "b $AST"; do
|
||
set -- $pair
|
||
mutant "m13$1-assistant-sendTo-pm" "$2" "$A" '"sendTo": ["human", "chief-of-staff"]' '"sendTo": ["human", "chief-of-staff", "pm"]'
|
||
done
|
||
for pair in "a $MSG" "b $AST"; do
|
||
set -- $pair
|
||
mutant "m14$1-assistant-receiveFrom-coder" "$2" "$A" '"receiveFrom": ["human", "chief-of-staff"]' '"receiveFrom": ["human", "chief-of-staff", "coder"]'
|
||
done
|
||
# m15: the shipped assistant file has no messaging field at all
|
||
for pair in "a $MSG" "b $AST"; do
|
||
set -- $pair
|
||
mutant "m15$1-assistant-unlimited" "$2" "$A" '"credentials": [],
|
||
"messaging": {
|
||
"sendTo": ["human", "chief-of-staff"],
|
||
"receiveFrom": ["human", "chief-of-staff"]
|
||
}
|
||
}' '"credentials": []
|
||
}'
|
||
done
|
||
# m16: review.request skips the lists (round 1 F1)
|
||
mutant m16-review-request-unchecked "$MSG" "$B" "this.#checkMessaging(s, a.to);
|
||
const mid = randomUUID();
|
||
// Row 105" "const mid = randomUUID();
|
||
// Row 105"
|
||
# m17: review.verdict skips the lists (round 1 F1)
|
||
mutant m17-review-verdict-unchecked "$MSG" "$B" "this.#checkMessaging(s, r.from);" "void 0;"
|
||
echo done
|