69 lines
4.3 KiB
Python
69 lines
4.3 KiB
Python
"""Row 285 planned mutation evidence; run only in the authorized gate slot."""
|
|
import datetime
|
|
import difflib
|
|
import hashlib
|
|
import os
|
|
from pathlib import Path
|
|
import subprocess
|
|
import sys
|
|
|
|
ROOT = Path(__file__).resolve().parents[5]
|
|
GATE = ROOT / 'agents/magica/work/queue-285/gate'
|
|
os.chdir(ROOT)
|
|
os.environ['TMPDIR'] = (GATE / 'tmpdir.txt').read_text().strip()
|
|
MUTANTS = [
|
|
('M1', 'packages/bus/src/broker.mjs', " || role.definition === STAFF_TO", '', 'packages/bus/tests/gitea-grant.test.mjs', 'X-G1: the chief of staff refuses at raise'),
|
|
('M2', 'packages/business/src/vocabulary.mjs', 'export const CREATIVE_ACTIONS = Object.freeze(["message.send", "library.write"]);', 'export const CREATIVE_ACTIONS = Object.freeze(["message.send", "library.write", "role.end"]);', 'packages/business/tests/role.test.mjs', 'M1: a creative role file wider than read and answer is refused'),
|
|
('M3', 'packages/business/src/role.mjs', ' if (services.length > 0) refuse(`creative role ${role.name} may hold only the ${CREATIVE_SERVICE} credential (got ${services.join(", ")})`);', '', 'packages/business/tests/role.test.mjs', 'M1: a creative role file wider than read and answer is refused'),
|
|
('M4', 'packages/business/src/role.mjs', 'role.authority.crossRole.length > 0 || ', '', 'packages/business/tests/role.test.mjs', 'M1: a creative role file wider than read and answer is refused'),
|
|
('M5', 'roles/companion.json', '"message.send", "library.write"', '"message.send"', 'packages/business/tests/role.test.mjs', 'the six creative roles load:'),
|
|
('M6', 'roles/chief-of-staff.json', '"read:repository", "read:user"', '"read:repository", "read:user", "write:repository"', 'packages/business/tests/chief-of-staff.test.mjs', 'the shipped file is exactly the approved list'),
|
|
('M9', 'packages/business/src/business.mjs', ' || !services.includes("gitea")', '', 'packages/business/tests/agent-identity.test.mjs', 'N10: publish needs identity and a gitea definition'),
|
|
# Amendment 1 (mos-1580, sage-4059): the same dropped condition must fail both files.
|
|
('M10-new', 'packages/business/src/business.mjs', ' && !(entry.definition === "chief-of-staff" && !hasLibrary && s === "gitea")', '', 'packages/business/tests/chief-library-credentials.test.mjs', 'row 285 amendment 1: no library loads with empty chief credentials'),
|
|
('M10-case1', 'packages/business/src/business.mjs', ' && !(entry.definition === "chief-of-staff" && !hasLibrary && s === "gitea")', '', 'packages/business/tests/case1-business-file.test.mjs', 'row 323: committed Case 1 document loads unchanged through the business loader'),
|
|
]
|
|
|
|
def sha(data):
|
|
return hashlib.sha256(data).hexdigest()
|
|
|
|
for name, relative, old, new, test, pattern in MUTANTS:
|
|
path = ROOT / relative
|
|
original = path.read_bytes()
|
|
text = original.decode()
|
|
if text.count(old) != 1:
|
|
sys.exit(f'{name}: mutation target count {text.count(old)}; refusing')
|
|
changed = text.replace(old, new).encode()
|
|
folder = GATE / 'mutants'
|
|
folder.mkdir(exist_ok=True)
|
|
(folder / (name + '.patch')).write_text(''.join(difflib.unified_diff(text.splitlines(True), changed.decode().splitlines(True), fromfile=relative, tofile=relative)))
|
|
cmd = ['timeout', '-k', '10', '180', 'node', '--test', '--test-reporter=tap', '--test-name-pattern=' + pattern, test]
|
|
before = sha(original)
|
|
try:
|
|
path.write_bytes(changed)
|
|
with (folder / (name + '.log')).open('w') as log:
|
|
result = subprocess.run(cmd, stdout=log, stderr=subprocess.STDOUT)
|
|
finally:
|
|
path.write_bytes(original)
|
|
after = sha(path.read_bytes())
|
|
if after != before:
|
|
sys.exit(f'{name}: RESTORE FAILURE')
|
|
output = (folder / (name + '.log')).read_text()
|
|
killed = result.returncode == 1 and 'not ok ' in output and pattern in output and ('AssertionError' in output or 'ERR_ASSERTION' in output)
|
|
receipt = '\n'.join([
|
|
datetime.datetime.now(datetime.timezone.utc).isoformat(),
|
|
'command: ' + ' '.join(cmd),
|
|
'source: ' + relative,
|
|
'before sha256: ' + before,
|
|
'mutant sha256: ' + sha(changed),
|
|
'restored sha256: ' + after,
|
|
'node/timeout exit: ' + str(result.returncode),
|
|
'result: ' + ('KILLED' if killed else 'UNEXPECTED'),
|
|
]) + '\n'
|
|
(folder / (name + '.txt')).write_text(receipt)
|
|
print(name + ' ' + ('KILLED' if killed else 'UNEXPECTED') + ' source restored ' + after, flush=True)
|
|
if not killed:
|
|
print(output[-7000:])
|
|
sys.exit(1)
|
|
print('All nine planned mutant checks KILLED and sources restored.', flush=True)
|