Files
jason.woltjeandClaude Opus 5.5 80797a5398 docs: add Dewey row 131 r2 packet (plug-in loader and setter fixes)
Round 2 for #1626 per sage-0314: F1a setter refuses a non-plain prototype,
F1b organization writer validates and logs JSON.parse(bytes), F2 loader
refuses unreadable paths, README top-level-code limit, control run.
Gate 591/0, 0 skipped; controls pass; mutants 47/47 killed (slot sage-0316).
Candidate manifest sha256 466c835f7dce79d20c7ab74c197a52b70e7fdeb0efae0456222dc6d0a633f219,
14 paths on base 0bdd573ff; no commit in base..1376caab9 touches them.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-15 11:44:04 -05:00

92 lines
7.6 KiB
Python

#!/usr/bin/env python3
"""Row 131: one fault per check. Sequential process groups; whole-group timeout cleanup.
--dry checks every mutation site matches and changes source, and runs nothing.
A control run first runs each distinct test set on the unmutated source; any failure stops the run."""
import json,os,pathlib,signal,subprocess,sys
args=[a for a in sys.argv[1:] if a!='--dry'];DRY='--dry' in sys.argv
ROOT=pathlib.Path(args[0] if args else os.path.expanduser('~/dewey-scratch/r131/wt'))
OUT=pathlib.Path(__file__).resolve().parent/'mutants'
LT='packages/plugins/tests/loader.test.mjs';KT='packages/plugins/tests/kinds.test.mjs';PT=[KT,LT]
ST='packages/business/tests/plugin-settings.test.mjs';CT='packages/cli/tests/stack-copy.test.mjs'
cases=[]
def add(id,path,old,new,test):cases.append(dict(id=id,path=path,old=old,new=new,test=test if isinstance(test,list) else [test]))
p='packages/plugins/src/kinds.mjs'
add('01-name-pattern',p,'if (typeof name !== "string" || !NAME.test(name)) {','if (typeof name !== "string") {',PT)
add('02-list-cardinality',p,'} else if (!Array.isArray(setting)) {','} else if (false) {',PT)
add('03-one-cardinality',p,'if (typeof setting !== "string") fail(','if (false) fail(',KT)
add('04-no-duplicates',p,'if (seen.has(name)) fail(','if (false) fail(',PT)
add('05-known-kind',p,'if (!Object.hasOwn(kinds, kind)) {','if (false) {',KT)
add('06-plugins-object',p,'if (value === null || typeof value !== "object" || Array.isArray(value)) {','if (value === null) {',KT)
add('07-registry-exports',p,'exports: Object.freeze(["mayLaunch", "mustStop"])','exports: Object.freeze(["mayLaunch"])',PT)
add('08-registry-version',p,'kind: "usage-source", interfaceVersion: 1,','kind: "usage-source", interfaceVersion: 2,',PT)
p='packages/plugins/src/loader.mjs'
add('09-missing-directory',p,'if (e.code === "ENOENT" || e.code === "ENOTDIR") refuse("missing", `${path} is missing`);','/* missing not mapped */',LT)
add('10-component-type',p,'if (directory ? !st.isDirectory() : !st.isFile()) {','if (false) {',LT)
add('11-symlink-test',p,'if (st.isSymbolicLink()) refuse("symlink", `${path} is a symlink`);','/* symlinks allowed */',LT)
add('12-plugins-dir-checked',p,' component(ROOT, true);\n','',LT)
add('13-kind-dir-checked',p,' component(kindDir, true);\n','',LT)
add('14-name-dir-checked',p,' component(nameDir, true);\n','',LT)
add('15-index-checked',p,' component(index, false);\n','',LT)
add('16-realpath-before-import',p,'if (real !== expected) refuse(','if (false) refuse(',LT)
add('17-load-failure',p,'refuse("load-failed", `${spec.kind} ${name} failed to load: ${e.message}`);','throw e;',LT)
add('18-export-present',p,'if (!Object.hasOwn(module, key)) refuse(','if (false) refuse(',LT)
add('19-export-function',p,'if (typeof module[key] !== "function") refuse(','if (false) refuse(',LT)
add('20-interface-version',p,'if (module.interfaceVersion !== spec.interfaceVersion) {','if (false) {',LT)
add('21-version-strict',p,'if (module.interfaceVersion !== spec.interfaceVersion) {','if (module.interfaceVersion != spec.interfaceVersion) {',LT)
add('22-keep-old-setting',p,'if (ok) current.set(kind, Object.freeze(next));','current.set(kind, Object.freeze(next));',LT)
add('23-first-load-nothing-on',p,'if (ok) current.set(kind, Object.freeze(next));','if (ok || current.get(kind).length === 0) current.set(kind, Object.freeze(next));',LT)
add('24-refusal-logged',p,' log(entry);\n','',LT)
add('25-log-names-module',p,'name: e.plugin ?? name, code','name, code',LT)
add('26-log-kept',p,'kept: kind === null ? loaded() : current.has(kind) ? current.get(kind).map(m => m.name) : [],','kept: [],',LT)
add('27-whole-setting-object',p,'if (plugins === null || typeof plugins !== "object" || Array.isArray(plugins)) {','if (plugins === null || typeof plugins !== "object") {',LT)
add('28-unknown-kind-logged',p,'refused(kind, null, new PluginError("unknown-kind"','void (kind, null, new PluginError("unknown-kind"',LT)
add('29-absent-key-off',p,'if (Object.hasOwn(plugins, kind)) names = namesFor(','if (!Object.hasOwn(plugins, kind)) continue; names = namesFor(',LT)
add('30-loads-serialized',p,'const run = queue.then(() => apply(plugins));','const run = apply(plugins);',LT)
add('31-log-required',p,'if (typeof log !== "function") throw','if (false) throw',LT)
add('42-modules-known-kind',p,'if (!current.has(kind)) refuse("unknown-kind"','if (false) refuse("unknown-kind"',LT)
add('43-unreadable-refused',p,'refuse("unreadable", `${path} can\'t be read (${e.code ?? e.message})`);','throw e;',LT)
add('44-realpath-index-mapped',p,'const real = fsCall(index, realpathSync);','const real = realpathSync(index);',LT)
add('45-realpath-root-mapped',p,'join(fsCall(ROOT, realpathSync),','join(realpathSync(ROOT),',LT)
p='packages/business/src/business.mjs'
add('32-schema-key',p,'"roles", "launch", "plugins"];','"roles", "launch"];',[ST,LT])
add('33-schema-check',p,'structuredClone(checkPlugins(document.plugins, `${file} plugins`, { fail: (_code, message) => refuse(message) }))','structuredClone(document.plugins)',ST)
add('34-schema-returned',p,'launch, plugins, definitions,','launch, definitions,',[ST,LT])
add('35-schema-copy',p,' : structuredClone(checkPlugins(',' : (checkPlugins(',ST)
p='packages/business/src/plugin-settings.mjs'
add('36-setter-strict-keys',p,'Object.keys(args).some(key => key !== "revision" && key !== "plugins")','false',ST)
add('37-setter-revision-format',p,'if (typeof args.revision !== "string" || !/^[a-f0-9]{64}$/.test(args.revision)) {','if (false) {',ST)
add('38-setter-checks-names',p,' checkPlugins(args.plugins, "plugins");\n','',ST)
add('39-setter-actor-logged',p,'{ revision: args.revision, actor }','{ revision: args.revision, actor: "host" }',ST)
add('40-setter-writes-plugins',p,' next.plugins = structuredClone(args.plugins);\n','',ST)
add('46-setter-plain-prototype',p,'if (proto !== Object.prototype && proto !== null) {','if (false) {',ST)
p='packages/business/src/organization.mjs'
add('47-writer-checks-bytes',p,' validateBusinessDocument(written, file, { rolesDir });\n','',ST)
add('41-copy-ships-plugins','scripts/stack-copy.sh',' packages/plugins\n','',CT)
def run(name,test):
env={**os.environ,'DOCKER_HOST':'unix:///nonexistent.sock'}
with (OUT/(name+'.log')).open('w') as log:
p=subprocess.Popen(['node','--test',*test],cwd=ROOT,env=env,stdout=log,stderr=subprocess.STDOUT,start_new_session=True)
try:return p.wait(timeout=300)
except subprocess.TimeoutExpired:os.killpg(p.pid,signal.SIGKILL);p.wait();return 'timeout'
results=[];control=[]
if not DRY:
OUT.mkdir(exist_ok=True)
sets=[];[sets.append(c['test']) for c in cases if c['test'] not in sets]
for i,t in enumerate(sets):
rc=run('control-%02d'%(i+1),t);control.append(dict(id='control-%02d'%(i+1),test=t,exit=rc));print('control-%02d'%(i+1),' '.join(t),'PASS' if rc==0 else 'FAIL',rc,flush=True)
if rc!=0:(OUT/'control.json').write_text(json.dumps(control,indent=2)+'\n');sys.exit('control failed: unmutated source fails '+' '.join(t))
(OUT/'control.json').write_text(json.dumps(control,indent=2)+'\n')
for c in cases:
file=ROOT/c['path'];before=file.read_text();matches=before.count(c['old'])
if not matches:raise RuntimeError('missing mutation site '+c['id'])
after=before.replace(c['old'],c['new'])
if after==before:raise RuntimeError('no change '+c['id'])
if DRY:print(c['id'],'sites',matches,flush=True);continue
file.write_text(after)
try:
rc=run(c['id'],c['test'])
verdict='KILLED' if isinstance(rc,int) and rc!=0 else 'SURVIVED' if rc==0 else 'TIMEOUT'
row={k:c[k] for k in ['id','path','test']};row.update(result=verdict,exit=rc,sites=matches);results.append(row);print(c['id'],verdict,rc,flush=True)
finally:file.write_text(before)
if not DRY:(OUT/'results.json').write_text(json.dumps(results,indent=2)+'\n')