Candidate on 0bdd573ff + row 49 r1 (manifest 87ef1321...), 21 mutants killed,
targeted gate green. Nothing landed or pushed.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
114 lines
7.4 KiB
Bash
114 lines
7.4 KiB
Bash
#!/usr/bin/env bash
|
||
# Row 123 (#1617): the chief of staff ends runs and changes the organization file.
|
||
# 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"
|
||
}
|
||
CS=packages/bus/tests/chief-of-staff.test.mjs; BCS=packages/business/tests/chief-of-staff.test.mjs
|
||
B=packages/bus/src/broker.mjs; V=packages/business/src/vocabulary.mjs; C=roles/chief-of-staff.json
|
||
for t in "$CS" "$BCS"; do run "control-$(basename "$(dirname "$(dirname "$t")")")" "$t"; done
|
||
|
||
# role.end (REQ-ROLE-2, REQ-COS-1)
|
||
# m1: the reason form skips the authority check, so any caller may end a run with a reason
|
||
mutant m1-role-end-unchecked "$CS" "$B" "this.#consumeAuthority(s, 'role.end', { target: a.role }, { reason: a.reason });" "void 0;"
|
||
# m2: a reason and a decision together are accepted
|
||
mutant m2-reason-and-decision-accepted "$CS" "$B" "if ('decision' in a) fail('invalid-request');" "void 0;"
|
||
# m3: the reason has no length bound
|
||
mutant m3-reason-unbounded "$CS" "$B" "string(a.reason, 500);" "string(a.reason, 100000);"
|
||
# m4: the chief may end its own run
|
||
mutant m4-self-end-allowed "$CS" "$B" "if (a.role === s.role || END_PROTECTED" "if (END_PROTECTED"
|
||
# m5: the chief may end the assistant's run
|
||
mutant m5-assistant-end-allowed "$CS" "$B" "END_PROTECTED.includes(b.roles[a.role]?.definition)" "false"
|
||
# m6: the reason is not written to role_claims.reason
|
||
mutant m6-reason-not-stored "$CS" "$B" "this.#claimEnd(s, h, 'revoke', a.decision ?? null, a.reason ?? null);" "this.#claimEnd(s, h, 'revoke', a.decision ?? null, null);"
|
||
# m7: the reason is not in the action.allowed body
|
||
mutant m7-reason-not-logged "$CS" "$B" "{ target: a.role }, { reason: a.reason });" "{ target: a.role }, {});"
|
||
# m8: the decision path checks role.end instead of role.revoke, so another role's decision no longer ends a run
|
||
mutant m8-decision-path-wrong-action "$CS" "$B" "this.#consumeAuthority(s, 'role.revoke', { decision: a.decision, target: a.role });" "this.#consumeAuthority(s, 'role.end', { decision: a.decision, target: a.role });"
|
||
|
||
# organization.set (REQ-LIMIT-1, REQ-ROLE-3, REQ-COS-1)
|
||
# m9: organization.set skips the authority check, so any agent may change the file
|
||
mutant m9-organization-set-unchecked "$CS" "$B" 'this.#store.transaction(() => this.#consumeAuthority(s, "organization.change"));' "void 0;"
|
||
# m10: organization.set accepts a key beyond the write path (launch)
|
||
mutant m10-key-list-widened "$CS" "$B" "const ORGANIZATION_KEYS = ['revision', 'limits', 'models', 'instances'];" "const ORGANIZATION_KEYS = ['revision', 'limits', 'models', 'instances', 'launch'];"
|
||
# m11: the revision is optional
|
||
mutant m11-revision-optional "$CS" "$B" 'else keys(args, ORGANIZATION_KEYS, ["revision"]);' "else keys(args, ORGANIZATION_KEYS, []);"
|
||
# m12: the writer is told the wrong actor
|
||
mutant m12-wrong-actor "$CS" "$B" 'actor = this.#business(s.business).roles[s.role].definition + "." + s.role;' "actor = s.role;"
|
||
# m13: limits and limits.set stop being human-only
|
||
mutant m13-limits-open-to-agents "$CS" "$B" 'if (!role && !s.human) fail("human-required");' "void 0;"
|
||
# m14: a validation failure from the shared writer no longer maps to invalid-request
|
||
mutant m14-invalid-change-not-refused "$CS" "$B" 'if (e.name === "BusinessError" || e.code === "invalid-request"' 'if (e.name === "NoSuchError" || e.code === "invalid-request"'
|
||
|
||
# vocabulary and the shipped role file (REQ-COS-1: role.revoke stays gated for every role)
|
||
# m15: role.end leaves the action vocabulary
|
||
mutant m15-vocabulary-drops-role-end "$BCS" "$V" ' "role.end",
|
||
' ''
|
||
# m16: the shipped chief file drops its role.end grant
|
||
mutant m16-chief-file-drops-role-end "$CS" "$C" '"role.launch", "role.end", "organization.change"' '"role.launch", "organization.change"'
|
||
# m17: the shipped chief file drops its organization.change grant
|
||
mutant m17-chief-file-drops-organization-change "$CS" "$C" '"role.end", "organization.change"' '"role.end"'
|
||
# m18: the shipped chief file lists role.revoke as within-role
|
||
mutant m18-chief-file-grants-role-revoke "$BCS" "$C" '"role.launch", "role.end"' '"role.launch", "role.revoke", "role.end"'
|
||
|
||
# schema (REQ-ROLE-2): the 3c→3d trigger. A schema edit must keep the pinned digest honest, so each of these
|
||
# edits schema.sql and the 3c-3d step the same way and re-seals SCHEMA_SHA256 from the edited schema.sql.
|
||
SQ=packages/bus/schema.sql; ST=packages/bus/src/store.mjs; MG=packages/bus/src/migrations/3c-3d.sql
|
||
reseal() {
|
||
save "$ST"
|
||
local sha; sha=$(sha256sum "$SQ" | cut -d' ' -f1)
|
||
node -e '
|
||
const fs = require("fs"); const [f, sha] = process.argv.slice(1);
|
||
const s = fs.readFileSync(f, "utf8");
|
||
fs.writeFileSync(f, s.replace(/export const SCHEMA_SHA256 = \x27[a-f0-9]+\x27;/, "export const SCHEMA_SHA256 = \x27" + sha + "\x27;"));
|
||
' "$ST" "$sha"
|
||
}
|
||
schema_mutant() { # name testfile old new
|
||
edit "$SQ" "$3" "$4" && edit "$MG" "$3" "$4" || { echo "$1: edit failed"; return; }
|
||
reseal
|
||
{ diff -u "$BAK/$SQ" "$SQ"; diff -u "$BAK/$MG" "$MG"; diff -u "$BAK/$ST" "$ST"; } > "$OUT/$1.patch"
|
||
run "$1" "$2"
|
||
for f in "$SQ" "$MG" "$ST"; do cp "$BAK/$f" "$f"; done
|
||
}
|
||
# m19: the trigger goes back to the 3c rule (a revoke needs a decision), so a reason alone is refused by storage
|
||
schema_mutant m19-trigger-decision-only "$CS" "WHEN NEW.op = 'revoke' AND NEW.decision IS NULL AND NEW.reason IS NULL" "WHEN NEW.op = 'revoke' AND NEW.decision IS NULL"
|
||
# m20: the trigger never fires, so a revoke with neither a decision nor a reason is accepted
|
||
schema_mutant m20-trigger-never-fires "$CS" "WHEN NEW.op = 'revoke' AND NEW.decision IS NULL AND NEW.reason IS NULL" "WHEN 0"
|
||
# m21: the 3c→3d step is a no-op on the trigger: the shipped schema has it, a migrated 3c database keeps the old rule.
|
||
# (Run against the migration test only; the step file keeps its text but the DROP/CREATE are removed, so the engine
|
||
# refuses at import with migration-target-mismatch. That refusal is the check: a step that does not reach the
|
||
# pinned schema cannot ship.)
|
||
save "$MG"; : > "$MG"
|
||
{ diff -u "$BAK/$MG" "$MG"; } > "$OUT/m21-step-empty.patch"
|
||
run m21-step-empty "$CS"
|
||
cp "$BAK/$MG" "$MG"
|