The task field-writer table as data: build.patch (12 files on 4f8ac1e7),
candidate manifest, gate summary (all green), mutants 33/33 killed.
The candidate is not committed.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
75 lines
5.4 KiB
Bash
Executable File
75 lines
5.4 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Darkwing row 61 mutants: single-site edits to the candidate in WORKTREE.
|
|
# Each mutant backs up one file, edits one site with mut.mjs (exactly one
|
|
# match, else BAD-SITE), runs the named test files and restores the backup.
|
|
# Usage: mutants.sh WORKTREE [browser|light|NAME,NAME...] (TMPDIR must be
|
|
# set; outputs go there). `light` skips the browser test mutants, `browser`
|
|
# runs only them, and a list of names runs only those.
|
|
set -u
|
|
WT="$1" ONLY="${2:-}"
|
|
MUT="$(cd "$(dirname "$0")/../queue-59-review/r1" && pwd)/mut.mjs"
|
|
cd "$WT" || exit 1
|
|
F=packages/tasks/src/fields.mjs J=packages/tasks/fields.json V=packages/tasks/src/verbs.mjs
|
|
S=packages/webui/src/serve.mjs R=packages/webui/src/reads.mjs B=packages/webui/src/public/bus.js
|
|
TF=tests/fields.test.mjs
|
|
|
|
m() { # name file pkg tests from to
|
|
local name="$1" file="$2" pkg="$3" tests="$4" from="$5" to="$6"
|
|
case "$ONLY" in
|
|
light) [[ $tests == *browser* ]] && return ;;
|
|
browser) [[ $tests == *browser* ]] || return ;;
|
|
'') ;;
|
|
*) [[ ,$ONLY, == *,$name,* ]] || return ;;
|
|
esac
|
|
cp -- "$file" "$TMPDIR/mut.bak"
|
|
if ! node "$MUT" "$file" "$from" "$to"; then echo "$name BAD-SITE"; cp -- "$TMPDIR/mut.bak" "$file"; return; fi
|
|
if (cd "packages/$pkg" && node --test $tests > "$TMPDIR/$name.out" 2>&1); then
|
|
echo "$name SURVIVED"
|
|
else
|
|
echo "$name killed: $(grep -E '^✖ ' "$TMPDIR/$name.out" | grep -v 'failing tests' | sort -u | sed 's/ ([0-9.]*ms)$//' | tr '\n' ';')"
|
|
fi
|
|
cp -- "$TMPDIR/mut.bak" "$file"
|
|
}
|
|
|
|
# The schema (fields.mjs).
|
|
m unknown-key-allowed $F tasks $TF "if (!allowed.includes(k)) throw" "if (false) throw"
|
|
m required-key-optional $F tasks $TF "if (!Object.hasOwn(x, k)) throw" "if (false) throw"
|
|
m writer-any-string $F tasks $TF "if (!WRITERS.includes(w)) throw" "if (typeof w !== 'string') throw"
|
|
m field-twice $F tasks $TF "if (seen.has(f.field)) throw" "if (false) throw"
|
|
m field-name-any $F tasks $TF "!FIELD.test(f.field)" "!f.field"
|
|
m label-unbounded $F tasks $TF "!str(f.label, 80)" "!str(f.label, 1e9)"
|
|
m note-any $F tasks $TF "f.note !== undefined && !str(f.note, 256)" "false"
|
|
m verb-twice $F tasks $TF "if (mine.has(w.verb)) throw" "if (false) throw"
|
|
m call-any $F tasks $TF "!CALL.test(c)" "!c"
|
|
m call-twice $F tasks $TF "new Set(calls).size !== calls.length" "false"
|
|
m unused-verb $F tasks $TF "if (!used.has(v)) throw" "if (false) throw"
|
|
m verbs-subset $F tasks $TF "exact(table.verbs, 'verbs', verbs);" "exact(table.verbs, 'verbs', verbs, []);"
|
|
m source-any $F tasks $TF "!str(table.source, 256)" "!table.source"
|
|
m no-freeze $F tasks $TF " Object.freeze(x);" " void x;"
|
|
m not-json-code $F tasks $TF "'fields.json: not JSON'" "'fields.json: unreadable'"
|
|
|
|
# The table (fields.json): the walk must notice data that no longer matches the code.
|
|
m scope-pm $J tasks $TF '"task.scope.change": "any"' '"task.scope.change": "pm"'
|
|
m priority-any $J tasks $TF '"task.priority.change": "pm"' '"task.priority.change": "any"'
|
|
m update-any $J tasks $TF '"task.update.assigned": "assigned"' '"task.update.assigned": "any"'
|
|
m close-assigned $J tasks $TF '"task.close": "pm"' '"task.close": "assigned"'
|
|
m reassign-no-delete $J tasks $TF '"calls": ["POST /tasks/{t}/assignees", "DELETE /tasks/{t}/assignees/{u}"]' '"calls": ["POST /tasks/{t}/assignees"]'
|
|
m create-no-due $J tasks $TF $' "label": "Due date",\n "writes": [\n { "verb": "task.create", "calls": ["POST /projects/{p}/tasks"] },\n' $' "label": "Due date",\n "writes": [\n'
|
|
m comments-by-close $J tasks $TF '"writes": [{ "verb": "task.update.assigned", "calls": ["POST /tasks/{t}/comments"] }]' '"writes": [{ "verb": "task.close", "calls": ["POST /tasks/{t}/comments"] }]'
|
|
m done-to-bucket $J tasks $TF '"writes": [{ "verb": "task.close", "calls": ["PUT /projects/{p}/views/{k}/buckets/{b}/tasks"] }]' '"writes": [{ "verb": "task.update.assigned", "calls": ["PUT /projects/{p}/views/{k}/buckets/{b}/tasks"] }]'
|
|
|
|
# The code (verbs.mjs): the walk must notice code that no longer matches the table.
|
|
m code-priority-open $V tasks $TF "'task.schedule', 'task.priority.change', 'task.close'" "'task.schedule', 'task.close'"
|
|
m code-scope-pm $V tasks $TF "'task.priority.change', 'task.close']);" "'task.priority.change', 'task.close', 'task.scope.change']);"
|
|
m code-assigned-open $V tasks $TF "if (!fields.assignees.includes(mine.botId)) fail('not-assigned');" "void 0;"
|
|
|
|
# The webui route and section.
|
|
m route-any-method $S webui tests/reads.test.mjs "if (req.method !== 'GET') return json(res, 405, { error: 'method not allowed' });
|
|
return consoleRead(res, () => fieldTable(fieldsFile));" "return consoleRead(res, () => fieldTable(fieldsFile));"
|
|
m route-ignores-file $S webui tests/reads.test.mjs "() => fieldTable(fieldsFile)" "() => fieldTable()"
|
|
m read-partial $R webui tests/reads.test.mjs " try { return loadFields(file); } catch (err) {" " try { return loadFields(file); } catch (err) { return {};"
|
|
m read-any-message $R webui tests/reads.test.mjs "err instanceof FieldTableError ? err.message : 'unreadable'" "'unreadable'"
|
|
m section-throws $B webui tests/bus-browser.test.mjs "catch (err) { return \`<p class=\"small\">\${why(err)}</p>\`; }" "catch (err) { throw err; }"
|
|
m section-writer-swap $B webui tests/bus-browser.test.mjs "const WRITER = { pm: 'pm', assigned: 'the assigned role', any: 'any role' };" "const WRITER = { pm: 'pm', assigned: 'any role', any: 'the assigned role' };"
|
|
m section-no-note $B webui tests/bus-browser.test.mjs "\${f.note ? \`<span class=\"source\">\${txt(f.note)}</span>\` : ''}" "''"
|