#!/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')