Round 2 for #1626 per sage-0314: F1a setter refuses a non-plain prototype, F1b organization writer validates and logs JSON.parse(bytes), F2 loader refuses unreadable paths, README top-level-code limit, control run. Gate 591/0, 0 skipped; controls pass; mutants 47/47 killed (slot sage-0316). Candidate manifest sha256 466c835f7dce79d20c7ab74c197a52b70e7fdeb0efae0456222dc6d0a633f219, 14 paths on base 0bdd573ff; no commit in base..1376caab9 touches them. Co-Authored-By: Claude Opus 5.5 <[email protected]>
12 KiB
Row 131 mutants (round 2)
One fault per check. mutants.py makes each edit to the candidate, runs only the
listed test files with node --test in its own process group (whole-group kill at
300 s), records the result and restores the file. mutants.py --dry checks that each
mutation site matches. Per-mutant output is in mutants/<id>.log;
mutants/results.json holds every result. Run: 2026-10-15T16:42:28Z to 16:42:52Z, in
the slot Sage gave in sage-0316.
Control
Before any mutant, each distinct test set runs on the unmutated candidate; one
failure stops the run (round 2, sage-0314). Logs: mutants/control-*.log,
mutants/control.json.
| Control | Test files | Exit | Tests | Pass | Fail | Skipped |
|---|---|---|---|---|---|---|
| control-01 | plugins/tests/kinds.test.mjs, plugins/tests/loader.test.mjs | 0 | 21 | 21 | 0 | 0 |
| control-02 | plugins/tests/kinds.test.mjs | 0 | 5 | 5 | 0 | 0 |
| control-03 | plugins/tests/loader.test.mjs | 0 | 16 | 16 | 0 | 0 |
| control-04 | business/tests/plugin-settings.test.mjs, plugins/tests/loader.test.mjs | 0 | 20 | 20 | 0 | 0 |
| control-05 | business/tests/plugin-settings.test.mjs | 0 | 4 | 4 | 0 | 0 |
| control-06 | cli/tests/stack-copy.test.mjs | 0 | 12 | 12 | 0 | 0 |
All pass with 0 skipped, so the chmod tests (run as a normal user) ran.
Mutants
Result: 47 of 47 cases run; 47 KILLED, 0 SURVIVED, 0 TIMEOUT.
| Mutant | Check | Result | Test files | First failing test |
|---|---|---|---|---|
| 01-name-pattern | kinds: names must match ^[a-z][a-z0-9-]*$ | KILLED (exit 1) | plugins/tests/kinds.test.mjs, plugins/tests/loader.test.mjs | REQ-PLUG-1 checkPlugins refuses bad shapes, unknown kinds, bad names and duplicates |
| 02-list-cardinality | kinds: a list kind refuses a non-list | KILLED (exit 1) | plugins/tests/kinds.test.mjs, plugins/tests/loader.test.mjs | REQ-PLUG-1 the setting's shape: unknown kinds refuse alone, a bad whole setting keeps everything |
| 03-one-cardinality | kinds: a one-module kind refuses a non-string | KILLED (exit 1) | plugins/tests/kinds.test.mjs | REQ-PLUG-1 a kind that switches on one module takes one name, not a list |
| 04-no-duplicates | kinds: a name twice refuses | KILLED (exit 1) | plugins/tests/kinds.test.mjs, plugins/tests/loader.test.mjs | REQ-PLUG-1 checkPlugins refuses bad shapes, unknown kinds, bad names and duplicates |
| 05-known-kind | kinds: an unknown kind refuses | KILLED (exit 1) | plugins/tests/kinds.test.mjs | REQ-PLUG-1 checkPlugins refuses bad shapes, unknown kinds, bad names and duplicates |
| 06-plugins-object | kinds: plugins must be a non-array object | KILLED (exit 1) | plugins/tests/kinds.test.mjs | REQ-PLUG-1 checkPlugins refuses bad shapes, unknown kinds, bad names and duplicates |
| 07-registry-exports | registry: limit-check exports mayLaunch and mustStop | KILLED (exit 1) | plugins/tests/kinds.test.mjs, plugins/tests/loader.test.mjs | REQ-PLUG-1 REQ-LIMIT-4 the registry: usage-source and limit-check at interface version 1, each a list |
| 08-registry-version | registry: usage-source is interface version 1 | KILLED (exit 1) | plugins/tests/kinds.test.mjs, plugins/tests/loader.test.mjs | REQ-PLUG-1 REQ-LIMIT-4 the registry: usage-source and limit-check at interface version 1, each a list |
| 09-missing-directory | loader: an absent path refuses as missing | KILLED (exit 1) | plugins/tests/loader.test.mjs | REQ-PLUG-1 a missing module directory or index.mjs refuses and keeps the old setting |
| 10-component-type | loader: a component of the wrong type refuses | KILLED (exit 1) | plugins/tests/loader.test.mjs | REQ-PLUG-1 a missing module directory or index.mjs refuses and keeps the old setting |
| 11-symlink-test | loader: a symlinked component refuses | KILLED (exit 1) | plugins/tests/loader.test.mjs | REQ-PLUG-1 a symlinked module directory or index.mjs refuses and keeps the old setting |
| 12-plugins-dir-checked | loader: packages/plugins is checked | KILLED (exit 1) | plugins/tests/loader.test.mjs | REQ-PLUG-1 a symlinked packages/plugins refuses (run with --preserve-symlinks so the path keeps it) |
| 13-kind-dir-checked | loader: is checked | KILLED (exit 1) | plugins/tests/loader.test.mjs | REQ-PLUG-1 a symlinked kind directory refuses and keeps the old setting |
| 14-name-dir-checked | loader: is checked | KILLED (exit 1) | plugins/tests/loader.test.mjs | REQ-PLUG-1 a missing module directory or index.mjs refuses and keeps the old setting |
| 15-index-checked | loader: index.mjs is checked | KILLED (exit 1) | plugins/tests/loader.test.mjs | REQ-PLUG-1 a missing module directory or index.mjs refuses and keeps the old setting |
| 16-realpath-before-import | loader: realpath(index.mjs) equals the expected path just before import | KILLED (exit 1) | plugins/tests/loader.test.mjs | REQ-PLUG-1 index.mjs swapped for a symlink after the lstat checks refuses at the realpath check |
| 17-load-failure | loader: a module that fails to load refuses | KILLED (exit 1) | plugins/tests/loader.test.mjs | REQ-PLUG-1 a module that fails to load refuses and keeps the old setting |
| 18-export-present | loader: every export is present | KILLED (exit 1) | plugins/tests/loader.test.mjs | REQ-PLUG-1 a missing export refuses and keeps the old setting |
| 19-export-function | loader: the kind's exports are functions | KILLED (exit 1) | plugins/tests/loader.test.mjs | REQ-PLUG-1 a missing export refuses and keeps the old setting |
| 20-interface-version | loader: interfaceVersion must equal the kind's | KILLED (exit 1) | plugins/tests/loader.test.mjs | REQ-PLUG-1 a different interface version refuses and keeps the old setting |
| 21-version-strict | loader: the version comparison is strict (string "1" refuses) | KILLED (exit 1) | plugins/tests/loader.test.mjs | REQ-PLUG-1 a different interface version refuses and keeps the old setting |
| 22-keep-old-setting | loader: a refusal keeps the last good modules | KILLED (exit 1) | plugins/tests/loader.test.mjs | REQ-PLUG-1 a missing module directory or index.mjs refuses and keeps the old setting |
| 23-first-load-nothing-on | loader: a kind refused on first load has nothing on | KILLED (exit 1) | plugins/tests/loader.test.mjs | REQ-PLUG-1 a kind refused on its first load has nothing switched on, and the log names kind and name |
| 24-refusal-logged | loader: every refusal calls log | KILLED (exit 1) | plugins/tests/loader.test.mjs | REQ-PLUG-1 a name not matching ^[a-z][a-z0-9-]*$ refuses and keeps the old setting |
| 25-log-names-module | loader: the log names the refused module | KILLED (exit 1) | plugins/tests/loader.test.mjs | REQ-PLUG-1 a name not matching ^[a-z][a-z0-9-]*$ refuses and keeps the old setting |
| 26-log-kept | loader: the log says what stays on | KILLED (exit 1) | plugins/tests/loader.test.mjs | REQ-PLUG-1 a name not matching ^[a-z][a-z0-9-]*$ refuses and keeps the old setting |
| 27-whole-setting-object | loader: an array setting refuses every kind | KILLED (exit 1) | plugins/tests/loader.test.mjs | REQ-PLUG-1 the setting's shape: unknown kinds refuse alone, a bad whole setting keeps everything |
| 28-unknown-kind-logged | loader: an unknown kind refuses and is logged | KILLED (exit 1) | plugins/tests/loader.test.mjs | REQ-PLUG-1 the setting's shape: unknown kinds refuse alone, a bad whole setting keeps everything |
| 29-absent-key-off | loader: an absent key switches the kind off | KILLED (exit 1) | plugins/tests/loader.test.mjs | REQ-PLUG-1 a path that becomes unreadable after the lstat checks refuses at either realpath |
| 30-loads-serialized | loader: loads run one at a time | KILLED (exit 1) | plugins/tests/loader.test.mjs | REQ-PLUG-1 loads run one at a time, so the last requested setting wins |
| 31-log-required | loader: createLoader needs a log callback | KILLED (exit 1) | plugins/tests/loader.test.mjs | REQ-PLUG-1 the loader needs a log callback |
| 32-schema-key | schema: plugins is an allowed top-level key | KILLED (exit 1) | business/tests/plugin-settings.test.mjs, plugins/tests/loader.test.mjs | REQ-PLUG-1 schema: plugins is optional, names known kinds and checked module names |
| 33-schema-check | schema: plugins is checked with checkPlugins | KILLED (exit 1) | business/tests/plugin-settings.test.mjs | REQ-PLUG-1 schema: plugins is optional, names known kinds and checked module names |
| 34-schema-returned | schema: plugins is returned | KILLED (exit 1) | business/tests/plugin-settings.test.mjs, plugins/tests/loader.test.mjs | REQ-PLUG-1 schema: plugins is optional, names known kinds and checked module names |
| 35-schema-copy | schema: the returned value is a frozen copy, the document stays mutable | KILLED (exit 1) | business/tests/plugin-settings.test.mjs | REQ-PLUG-1 schema: plugins is optional, names known kinds and checked module names |
| 36-setter-strict-keys | setter: only revision and plugins are accepted | KILLED (exit 1) | business/tests/plugin-settings.test.mjs | REQ-PLUG-1 REQ-ROLE-3 a refused setter write throws a code, changes nothing and logs nothing |
| 37-setter-revision-format | setter: revision must be 64 hex | KILLED (exit 1) | business/tests/plugin-settings.test.mjs | REQ-PLUG-1 REQ-ROLE-3 a refused setter write throws a code, changes nothing and logs nothing |
| 38-setter-checks-names | setter: plugins is checked before the writer runs | KILLED (exit 1) | business/tests/plugin-settings.test.mjs | REQ-PLUG-1 REQ-ROLE-3 a refused setter write throws a code, changes nothing and logs nothing |
| 39-setter-actor-logged | setter: the actor is logged | KILLED (exit 1) | business/tests/plugin-settings.test.mjs | REQ-PLUG-1 REQ-ROLE-3 the setter writes plugins through the organization writer and logs old and new names |
| 40-setter-writes-plugins | setter: plugins is written | KILLED (exit 1) | business/tests/plugin-settings.test.mjs | REQ-PLUG-1 REQ-ROLE-3 the setter writes plugins through the organization writer and logs old and new names |
| 41-copy-ships-plugins | stack-copy: packages/plugins ships in the pinned copy | KILLED (exit 1) | cli/tests/stack-copy.test.mjs | shared copy: npm ci, pushed reachability, readonly/existing copy, webui delegation |
| 42-modules-known-kind | loader: modules() refuses an unknown kind | KILLED (exit 1) | plugins/tests/loader.test.mjs | REQ-PLUG-1 the setting's shape: unknown kinds refuse alone, a bad whole setting keeps everything |
| 43-unreadable-refused | loader: a read error other than ENOENT/ENOTDIR refuses as unreadable | KILLED (exit 1) | plugins/tests/loader.test.mjs | REQ-PLUG-1 a path that can't be read refuses as unreadable, is logged and keeps the old setting |
| 44-realpath-index-mapped | loader: realpath(index.mjs) errors become refusals | KILLED (exit 1) | plugins/tests/loader.test.mjs | REQ-PLUG-1 a path that becomes unreadable after the lstat checks refuses at either realpath |
| 45-realpath-root-mapped | loader: realpath(packages/plugins) errors become refusals | KILLED (exit 1) | plugins/tests/loader.test.mjs | REQ-PLUG-1 a path that becomes unreadable after the lstat checks refuses at either realpath |
| 46-setter-plain-prototype | setter: plugins must have prototype Object.prototype or null | KILLED (exit 1) | business/tests/plugin-settings.test.mjs | REQ-PLUG-1 REQ-ROLE-3 a refused setter write throws a code, changes nothing and logs nothing |
| 47-writer-checks-bytes | writer: validates JSON.parse of the bytes it writes | KILLED (exit 1) | business/tests/plugin-settings.test.mjs | REQ-PLUG-1 REQ-ROLE-3 the writer checks the bytes it writes: a Date in plugins is refused there too |
New in round 2: 43, 44 and 45 (F2), 46 (F1a), 47 (F1b). 43 to 45 fail because
load() rejects on the raw EACCES instead of logging a refusal; 46 lets a Date
through the setter, written as a date string; 47 lets the writer take a Date
without a BusinessError. Mutant 09 now sits in the shared fsCall helper:
without the ENOENT mapping a missing path refuses as unreadable, not
missing. Mutant 17's log shows a SyntaxError: it is the fixture module's
deliberate syntax error, which the mutant rethrows instead of refusing.
changes(old, written) has no mutant; see BUILD.md, round 2.