Files
jason.woltjeandClaude Opus 5.5 fafee2f228 agents: Rocko row 147 packets r1, r2 and r2-before-142 (#1641)
Round 2 candidate 7a585aa9606863a0d44a26094b585160914e3d8fd5f13436b56470cd69e6d867,
packet manifest 4292da90162c801a9c56716a0539f8fa072e67e93e4f4d9a0f1e107b84b652ea,
base 02ee7f640. Filbert approved round 2 in comment 28707. Committed by Sage
for Rocko, who has no git access; secrets grep clean, no binaries (one empty file).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-15 19:00:06 -05:00

82 lines
7.6 KiB
Python

#!/usr/bin/env python3
"""Sequential focused acceptance mutants, each isolated in a process group; timeout is not a kill."""
import json,os,pathlib,signal,subprocess,sys
ROOT=pathlib.Path(sys.argv[1] if len(sys.argv)>1 else '/mnt/storage/src/mosaic-stack-rocko-147-r1')
OUT=pathlib.Path(__file__).resolve().parent/'mutants';OUT.mkdir(exist_ok=True)
T='packages/project/tests/project.test.mjs';C='packages/cli/tests/project.test.mjs';B='packages/business/tests/project-repo.test.mjs'
cases=[]
def add(id,path,old,new,test=T):cases.append(dict(id=id,path=path,old=old,new=new,test=test))
p='packages/project/src/index.mjs'
add('01-init-nonempty',p,"if (existsSync(repo) && readdirSync(repo).length)","if (false)")
add('02-unknown-top-level',p,"if (!['.git', 'project.json', ...DIRECTORIES].includes(name))",'if (false)')
add('03-check-record-schema',p,'for (const problem of validateRecord(kind, value))','for (const problem of [])')
add('04-check-filename-id',p,'if (value && name !== `${value.id}.json`)','if (false)')
add('05-writer-record-schema',p,'const problems = validateRecord(kind, record);','const problems = [];')
add('06-writer-filename-id',p,'problems.length || record.id !== match[2]','problems.length')
add('07-agent-spec-refusal',p,'bytes = recordBytes(path, record)','bytes = recordBytes(path.replace(/^spec\\//, "docs/"), record)')
add('08-author-shape',p,"if (!object(author) || Object.keys(author).sort().join(',') !== 'definition,instance')",'if (false)')
add('09-agent-commit-author',p,'identity(name));','identity(context.business.human));')
add('10-human-initial-author',p,"identity(human));","identity('wrong-human'));")
add('11-preserve-dirty-human-edits',p,' await requireClean(repo);',' /* dirty check omitted */')
add('12-exclusive-writer',p,"fd = openSync(lock, 'wx', 0o600);","fd = openSync(lock, 'w', 0o600);")
add('13-context-business',p,'if (value && value.business !== context.business.id)','if (false)')
add('14-file-links',p,'!st.isFile() || st.isSymbolicLink() || st.nlink !== 1 || st.size > MAX_BYTES','!st.isFile() && !st.isSymbolicLink() || st.size > MAX_BYTES')
add('15-invalid-utf8',p,"{ fatal: true }","{ fatal: false }")
add('16-file-nul',p,"if (value.includes('\\0'))",'if (false)')
add('17-file-size',p,'st.nlink !== 1 || st.size > MAX_BYTES','st.nlink !== 1')
add('18-writer-size',p,"if (text.includes('\\0') || Buffer.byteLength(text) > MAX_BYTES)",'if (false)')
add('19-unknown-project-schema',p,'if (!TEMPLATES.includes(name))','if (false)')
add('20-required-template',p,"if (!readdirSync(base).includes(name)) problem(`templates/${name}`, 'missing');",'/* required template omitted */;')
add('21-rollback-file',p,'if (before === null) unlinkSync(file); else publish(file, before);','/* rollback omitted */')
p='packages/project/src/git.mjs'
add('22-no-remotes',p,"if (key.startsWith('remote.')) throw new ProjectError('project-remote', 'project repository must have no remote');","if (key.startsWith('remote.')) continue;")
add('23-disable-hooks',p,'core.hooksPath=/dev/null','core.hooksPath=.git/hooks')
p='packages/project/src/paths.mjs'
add('24-code-repo-separation',p,'if (contains(root, repo) || contains(repo, root))','if (false)')
add('25-repo-override',p,"entry.repo ?? join(dataRoot, 'projects', business.id, project)","join(dataRoot, 'projects', business.id, project)")
p='packages/project/src/validate.mjs'
add('26-const-version',p,"if (Object.hasOwn(schema, 'const') && value !== schema.const)",'if (false)')
add('27-enum-values',p,'if (schema.enum && !schema.enum.includes(value))','if (false)')
add('28-schema-types',p,'if (types.length && !types.some(matches))','if (false)')
add('29-id-pattern',p,'if (schema.pattern && !new RegExp(schema.pattern).test(value))','if (false)')
add('30-canonical-date',p,"if (schema.format === 'date-time' && (!Number.isFinite(Date.parse(value)) ||\n new Date(value).toISOString() !== value || value.length !== 24))",'if (false)')
add('31-min-array',p,'if (schema.minItems !== undefined && value.length < schema.minItems)','if (false)')
add('32-max-array',p,'if (schema.maxItems !== undefined && value.length > schema.maxItems)','if (false)')
add('33-required-fields',p,'for (const key of schema.required ?? [])','for (const key of [])')
add('34-closed-keys',p,'else if (schema.additionalProperties === false)','else if (false)')
add('35-decision-choice',p,"if (value.status === 'open' ? value.choice !== null : !keys.includes(value.choice))",'if (false)')
add('36-distinct-options',p,'if (new Set(keys).size !== keys.length)','if (false)')
add('37-recommendation-option',p,'if (!keys.includes(value.recommendation))','if (false)')
p='packages/business/src/business.mjs'
add('38-business-repo-preservation',p,'...(Object.hasOwn(entry, "repo") ? { repo: entry.repo } : {})','...{}',B)
add('39-business-repo-validation',p,'if (Object.hasOwn(entry, "repo") && (typeof entry.repo !== "string" || !isAbsolute(entry.repo) ||\n normalize(entry.repo) !== entry.repo || entry.repo.includes("\\0")))','if (false)',B)
add('40-human-cli-init','packages/cli/src/project.mjs',"if (verb === 'init') refuseInsideAgent(io.env);",'/* agent check omitted */',C)
add('41-cli-dispatch','scripts/mosaic','project|limits|inbox|','limits|inbox|',C)
add('42-pinned-package','scripts/stack-copy.sh',' packages/cli/src packages/project',' packages/cli/src','packages/cli/tests/stack-copy.test.mjs')
add('43-min-string','packages/project/src/validate.mjs','if (schema.minLength !== undefined && value.length < schema.minLength)','if (false)')
add('44-max-string','packages/project/src/validate.mjs','if (schema.maxLength !== undefined && value.length > schema.maxLength)','if (false)')
add('45-schema-keywords','packages/project/src/validate.mjs','for (const key of Object.keys(schema))','for (const key of [])')
add('46-absolute-normalized-repo','packages/project/src/paths.mjs',"if (typeof path !== 'string' || !isAbsolute(path) || normalize(path) !== path || path.includes('\\0'))",'if (false)')
add('47-declared-project','packages/project/src/paths.mjs','if (!Object.hasOwn(business?.projects ?? {}, project))','if (false)')
add('48-git-config','packages/project/src/git.mjs','if (!allowed[key]?.includes(value))','if (false)')
add('49-empty-layout-marker','packages/project/src/index.mjs',"if (text !== '') problem(label, 'layout marker must be empty');",'if (false) problem(label, \'layout marker must be empty\');')
add('50-gitdir-before-lock','packages/project/src/index.mjs','if (!metadata.isDirectory() || metadata.isSymbolicLink())','if (false)')
results=[]
if os.environ.get('MUTANT_ONLY'):cases=[c for c in cases if c['id'] in os.environ['MUTANT_ONLY'].split(',')]
for c in cases:
file=ROOT/c['path'];original=file.read_bytes();text=original.decode();count=text.count(c['old'])
if count!=1:raise RuntimeError(f"{c['id']}: expected one site, found {count}")
file.write_text(text.replace(c['old'],c['new']))
try:
with (OUT/(c['id']+'.log')).open('w') as log:
p=subprocess.Popen(['node','--test',c['test']],cwd=ROOT,env={**os.environ,'DOCKER_HOST':'unix:///nonexistent.sock'},stdout=log,stderr=subprocess.STDOUT,start_new_session=True)
try:rc=p.wait(timeout=120)
except subprocess.TimeoutExpired:os.killpg(p.pid,signal.SIGKILL);p.wait();rc='timeout'
content=(OUT/(c['id']+'.log')).read_text()
invalid='SyntaxError:' in content or ('ERR_MODULE_NOT_FOUND' in content and c['id']!='42-pinned-package')
verdict='INVALID' if invalid else 'TIMEOUT' if rc=='timeout' else 'SURVIVED' if rc==0 else 'KILLED'
row={k:c[k] for k in ['id','path','test']};row.update(result=verdict,exit=rc)
results.append(row);(OUT/'results.json').write_text(json.dumps(results,indent=2)+'\n');print(c['id'],verdict,rc,flush=True)
finally:file.write_bytes(original)
if verdict!='KILLED':sys.exit(1)