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]>
70 lines
5.1 KiB
Python
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)
|