Files
stack/agents/dewey/work/queue-89/mutants.py
T
jason.woltjeandClaude Opus 5.5 4a2e18c90d dewey: row 89 (#1583) PM task reads packet, round 1, manifest b8c5bb3a
Candidate on base a3f6e9e6: tasks takes filters and paging, list_tasks and get_task,
a 40000-character reply cap with compact JSON, and the built-in tools line per harness.
Gate and 34 mutants in the packet.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-12 15:45:08 -05:00

70 lines
5.1 KiB
Python

# Row 89 mutants (Dewey): each swaps one exact string in the scratch copy of the candidate, runs
# the bus or harness node tests, restores the file, and reports killed (tests fail) or SURVIVED.
import subprocess, sys, os, signal
M = os.path.expanduser('~/dewey-scratch/r89mut')
env = dict(os.environ, TMPDIR=os.path.expanduser('~/dewey-scratch/tmp'))
B = 'packages/bus/src/broker.mjs'
T = 'packages/harness/src/tools.mjs'
MCP = 'packages/harness/src/mcp-server.mjs'
PI = 'packages/harness/src/pi-extension.mjs'
BU = 'packages/harness/src/bundle.mjs'
BUS = ['packages/bus/tests/*.test.mjs']
HAR = ['packages/harness/tests/*.test.mjs']
muts = [
('no args filtered too', B, " if (!Object.keys(a).length) return rows;\n", "", BUS),
('tombstones kept', B, " .filter((r) => !r.fields.gone)\n", "", BUS),
('next on an exact last page', B, "rest.length > limit ?", "rest.length >= limit ?", BUS),
('default limit ignored', B, "const limit = a.limit ?? TASK_PAGE;", "const limit = a.limit ?? TASK_PAGE_MAX;", BUS),
('limit max unchecked', B, "!positive(a.limit, TASK_PAGE_MAX)", "!positive(a.limit)", BUS),
('assignee by raw id', B, ".filter((u) => bots.has(u)).map((u) => bots.get(u)).sort()", ".sort()", BUS),
('assignees keep persons', B, ".filter((u) => bots.has(u)).map((u) => bots.get(u))", ".map((u) => bots.get(u) ?? u)", BUS),
('task_ref with other args', B, "Object.keys(a).length !== 1 || ", "", BUS),
('missing task_ref is null', B, "?? fail('task-not-found')", "?? null", BUS),
('done check dropped', B, "('done' in a && typeof a.done !== 'boolean') ||", "", BUS),
('done filter dropped', B, "(!('done' in a) || t.done === a.done) &&", "", BUS),
('assignee any string', B, "!(typeof a.assignee === 'string' && Object.hasOwn(roles, a.assignee))", "typeof a.assignee !== 'string'", BUS),
('label check dropped', B, "('label' in a && !positive(a.label)) ||", "", BUS),
('label filter dropped', B, "(!('label' in a) || t.labels.includes(a.label)) &&", "", BUS),
('requirement pattern dropped', B, " && /^REQ-[A-Z]+-[1-9][0-9]*$/.test(a.requirement)", "", BUS),
('requirement filter dropped', B, "(!('requirement' in a) || t.requirement === a.requirement)", "true", BUS),
('after ignored', B, "matched.filter((t) => t.task_ref > a.after)", "matched", BUS),
('after inclusive', B, "t.task_ref > a.after", "t.task_ref >= a.after", BUS),
('total counts the page', B, "total: matched.length", "total: tasks.length", BUS),
('description kept', B, " title: f.title ?? null,\n", " title: f.title ?? null,\n description: f.description,\n", BUS),
('tasks keys closed', B, "verb === 'tasks' ? TASK_ARGS : []", "[]", BUS),
('ARG_DEFAULTS removed', T, "args: { ...ARG_DEFAULTS[tool], ...args }", "args", HAR),
('default overrides the caller', T, "args: { ...ARG_DEFAULTS[tool], ...args }", "args: { ...args, ...ARG_DEFAULTS[tool] }", HAR),
('cap at >=', T, "if (text.length > REPLY_CAP)", "if (text.length >= REPLY_CAP)", HAR),
('cap counts bytes', T, "if (text.length > REPLY_CAP)", "if (Buffer.byteLength(text) > REPLY_CAP)", HAR),
('indented JSON', T, "const text = JSON.stringify(result) ?? \"null\";", "const text = JSON.stringify(result, null, 2) ?? \"null\";", HAR),
('cap 50000', T, "export const REPLY_CAP = 40_000;", "export const REPLY_CAP = 50_000;", HAR + BUS),
('mcp server uncapped', MCP, "text: replyText(result)", "text: JSON.stringify(result)", HAR),
('pi uncapped', PI, "text: replyText(result)", "text: JSON.stringify(result)", HAR),
('tool limit max 500', T, "maximum: TASK_PAGE_MAX,", "maximum: 500,", HAR),
('get_task not required', T, "obj({ task_ref: TASK_REF }, [\"task_ref\"])", "obj({ task_ref: TASK_REF })", HAR),
('get_task without tracker', T, " add(\"get_task\", \"tasks\",", " }\n {\n add(\"get_task\", \"tasks\",", HAR),
('bundle line pi names', BU, "(session.harness === \"pi\" ? resolved.limits.tools : claudeBuiltins({ tools: resolved.limits.tools }))", "resolved.limits.tools", HAR),
('bundle none dropped', BU, ".join(\", \") || \"none\"}", ".join(\", \")}", HAR),
]
only = sys.argv[1:]
for name, f, old, new, tests in muts:
if only and not any(o in name for o in only): continue
p = os.path.join(M, f); src = open(p).read()
n = src.count(old)
if n != 1: print(f'BAD-ANCHOR ({n}) {name}', flush=True); continue
open(p, 'w').write(src.replace(old, new))
# Its own process group, so a hung test's children die with it (a mutant that loops forever
# never fails on its own; a hang is a kill).
proc = subprocess.Popen(['node', '--test', *tests], cwd=M, env=env, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, start_new_session=True)
try:
out, _ = proc.communicate(timeout=300)
except subprocess.TimeoutExpired:
os.killpg(proc.pid, signal.SIGKILL); proc.communicate()
out = None
finally:
open(p, 'w').write(src)
if out is None:
print(f'killed {name} (hung: killed after 300 s)', flush=True); continue
fails = [l for l in out.splitlines() if l.startswith('not ok')]
print(('killed ' if proc.returncode else 'SURVIVED') + f' {name}' + (f' ({len(fails)} failing)' if fails else ''), flush=True)