dewey: row 83 round 1 packet, unit script takes --send (#1576)

build.patch against d13b9300 (brief blob f8d14d70, rev 630), candidate
manifest, identity and mutant harnesses with results, gate summary.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-11 22:59:05 -05:00
co-authored by Claude Opus 5.5
parent a1c5c8fcc1
commit e1bf582a1a
8 changed files with 812 additions and 0 deletions
+212
View File
@@ -0,0 +1,212 @@
# Row 83 (#1576): Console service, the unit script takes --send, round 1
Owner: Dewey. Reviewers: Darkwing and Filbert, then Sage's gate. Brief:
`docs/plans/2026-10-12_console-service-send.md` § "The unit script takes
--send", blob `f8d14d70` (re-pinned at rev 630 for lead decision 102; first
pinned as `739bea66` at rev 627, briefed at rev 628). Lead decision 101
gives the reason. Base `d13b9300`. None of the candidate's paths, nor the
template, changed between `d13b9300` and `refactor` HEAD (`a1c5c8fc`) when
the packet was built.
`build.patch` is `git diff --cached --binary d13b9300 --` the four paths
below. Apply it with `git apply --index` to a detached worktree of the base;
`candidate-manifest.sha256` then checks 4/4 OK (checked on a fresh
worktree).
## What ships
- `render` and `install` take `--send`, a flag with no value. It is
accepted only with all three preview flags. Otherwise `serve_flags`
refuses before anything is written: exit 1, empty stdout, and on stderr
`webui-service: --send needs the preview mode (--public-host,
--auth-header and --allow-user)`, serve's own wording (cli.mjs:37).
`install` renders before `mkdir`, so a refusal creates no directory and
leaves an installed unit alone (tested).
- With `--send`, ExecStart ends `... --allow-user ID --send`. Nothing else
in the unit changes (tested line by line against the same render without
`--send`).
- Without `--send`, every render is the template with its four
placeholders filled, byte for byte (new test), and the base and
candidate scripts give the same stdout, stderr, status and installed
files on every argument list the base tests use (`identity.txt`).
- `uninstall` and `status` take no `--send`: it sets SERVE_SET like the
other serve flags, so both exit 4 (tested with a stand-in systemctl).
- `status` prints one new line when a unit is installed: `send: on
(--send; human messages to roles)` or `send: off (read-only)`. The brief
allowed this "if small"; it is five lines plus a comment. With no unit
installed there is no send line.
- `USAGE` and the header comment name `[--send]` inside the preview set.
README's service paragraph and TOOLS.md's "Console service" section say
the script takes `--send` only with the preview set (exit 1, nothing
written), that ExecStart then ends with `--send`, and that the Console
needs the system config at start (`--send needs the bus configured`,
exit 2, which the unit doesn't restart: `RestartPreventExitStatus=2` in
the template). README's "Message a role" `--send` paragraph gets one
sentence pointing at `scripts/webui-service.sh install` with `--send`.
- The start wording (lead decision 102). The script's header comment, its
install message, TOOLS.md's "Console service" and README's service
paragraph now say Jason starts and restarts the Console, and so may Mos
under decision 102, which accepts that sends from a Console Mos started
enter the trail as Jason (`human-cli`). Any other agent starting it,
even through systemd, still works around the human proof (decisions 62
and 94). The script still has no start, stop or restart verb. A test
checks the install message's wording.
The template (`packages/webui/systemd/mosaic-webui.service.in`) is
unchanged. `install` prints no new line; its `serve:` line already shows
the ExecStart, `--send` included. The install message's opening sentence
changed wording only (decision 102).
## Decisions and tradeoffs
- **Refusal order.** The `--send` check runs before the all-three check.
`--send` with one or two preview flags therefore gets the `--send`
message, not "all three or none". The brief asks for a message naming
the preview flags the way serve refuses, and serve checks the preview
set first. Both messages name the three flags. A partial preview set
without `--send` still gets "all three or none" (base test unchanged).
- **Status detection is anchored on the allowed user.** My first version
matched `^ExecStart=.* --send$`. The mutant `status-send-anywhere`
survived it, and that showed a real gap: `--allow-user` accepts the value
`--send` (its character set allows `-`, and serve's `USER` pattern
accepts it), so a read-only unit's ExecStart can end
`--allow-user --send`. The check is now
`^ExecStart=.* --allow-user [^ ][^ ]* --send$`. render always puts
`--send` right after the allowed user, so a sending unit ends with both
the allowed user and `--send`. A test installs both cases, read-only
`--allow-user --send` and sending `--allow-user --send --send`, and
checks status for each.
- **No config or bus check in the script** (out of scope per the brief).
A unit installed with `--send` on a host without the system config
installs fine and exits 2 at start; the docs say so.
## Files
- `scripts/webui-service.sh`: `SEND=0`, the `--send` case, the refusal
and the append in `serve_flags`, the status line, USAGE and header.
- `packages/webui/tests/service.test.mjs`: 5 new tests, 2 widened.
- `packages/webui/README.md`: the service paragraph, one sentence in
"Message a role".
- `docs/TOOLS.md`: the "Console service" FLAGS comment and prose.
## Tests
`packages/webui/tests/service.test.mjs`, 15 tests (10 base, 5 new). The
helper `filled(flags)` is the template with `@REPO@`, `@PATH@`, `@NODE@`
and `@FLAGS@` replaced, computed in the test the way the base script does
it.
- *without --send, render is the template with its placeholders filled,
byte for byte*: five flag combinations, from none to every flag.
- *render --send with the preview set ends ExecStart with --send and
changes nothing else*: four argument orders (`--send` first, last, in the
middle of the preview set, with `--port` and `--business`). Each equals
`filled(flags + ' --send')`, and against the same render without
`--send` exactly one line differs, the ExecStart, by ` --send`.
- *--send without the whole preview set is refused and writes nothing*:
`--send` alone, with `--port`, with `--business`, with each one of the
three preview flags, and with each two of the three. Each `render` gives
exit 1, empty stdout and serve's message. Each `install` into a fresh
`--dir` gives the same, and the directory is never created. An install
of `--send` with two preview flags over an installed unit leaves it byte
for byte, with no temp file and no systemctl call.
- *install --send into --dir with --no-reload writes the sending unit, and
status says whether it sends*: `written:`, the `serve:` line ending
`--send`, the file equal to `filled(...)`, mode 0644, no temp file;
status `send: on`; reinstall `unchanged:`; install without `--send`
writes the read-only unit and status says `send: off (read-only)`; the
odd allowed user `--send` case above; no `send:` line without a unit.
The only systemctl calls are status's reads. Status's `/healthz` goes to
a loopback port that was just closed, so it reaches no real Console.
- *uninstall and status take no --send*: exit 4 for `uninstall --send`
(with and without `--no-reload`) and `status --send`, against a stand-in
systemctl that records no call; the unit stays installed.
- Widened: the agent-marker test also renders `[...PREVIEW, '--send']`;
the usage test adds `render --send yes ...PREVIEW` (`--send` takes no
value); the install test checks the decision 102 start wording in the
install message.
Why the uninstall and status cases sit in their own test: in the usage
loop they run with the real `/usr/bin/systemctl` on PATH. A mutant where
`--send` doesn't set SERVE_SET would let `uninstall --send` reach a real
`systemctl --user daemon-reload`. With the stand-in, no mutant can.
## Byte identity (`identity.mjs`, `identity.txt`)
One scratch worktree of `d13b9300` holds the base script as
`scripts/webui-service-base.sh` and the candidate as
`scripts/webui-service.sh`, so REPO and the template are the same for
both. Each script runs the same 86 argument lists from a clean scratch:
every render the base tests make (the value and port refusals included),
the usage list, `--help`, and an install, reinstall, refused install,
install over, status, uninstall, uninstall and status sequence, all with
`--no-reload` and a stand-in systemctl.
Result: 69 identical, 17 expected differences, 0 unexpected. The 17 are
the usage text on 12 exit-4 cases plus `--help` (the FLAGS line names
`[--send]`), three installs whose message differs only in the decision 102
start wording (the base message's opening lines replaced by the
candidate's, then everything else identical, installed unit included),
and one status with a unit installed (the added `send: off (read-only)`
line, nothing else). A self-check: with the append made
unconditional, the same run reports 13 unexpected differences.
## Mutants (`mutants.py`, `mutants.txt`)
15 single-site mutants of `scripts/webui-service.sh`, each run against the
base tests (`d13b9300`'s `service.test.mjs`), the candidate tests and
`identity.mjs`. Baselines: all three pass on the unmutated candidate.
Rerun on the decision 102 candidate, 2026-10-12T03:53Z; same results as
the first run.
| mutant | base tests | candidate tests |
|---|---|---|
| send-no-preview-check | survived | killed |
| send-needs-any-preview | survived | killed |
| send-message-reworded | survived | killed |
| send-no-append | survived | killed |
| send-append-always | killed | killed |
| send-append-before-preview | survived | killed |
| send-default-on | killed | killed |
| send-no-serve-set | survived | killed |
| send-takes-value | survived | killed |
| send-flag-unknown | survived | killed |
| status-send-always-off | survived | killed |
| status-send-always-on | survived | killed |
| status-no-send-line | survived | killed |
| status-send-anywhere | survived | killed |
| status-send-suffix-only | survived | killed |
The candidate tests kill 15 of 15. The two the brief names,
`send-no-preview-check` and `send-no-append`, survive the base tests.
`send-append-always` and `send-default-on` are killed by the base tests
too, as a change to every render should be.
## Gate (my run on the candidate)
On the staged candidate in a worktree of `d13b9300`, run sequentially,
2026-10-12T03:54:20Z to 03:58:42Z (`gate-summary.txt`):
- `packages/webui`, `node --test 'tests/*.test.mjs'`: 81 tests, 81 pass,
rc=0.
- `scripts/test-auth.sh`, `test-conductor.sh`, `test-config.sh`,
`test-discord.sh`, `test-extension-package.sh`, `test-foundation.sh`,
`test-queue.sh`, `test-release.sh`, `test-task.sh`: all rc=0.
## For reviewers
- **Allowed user starting with `-`.** The base script and serve both accept
an allowed user like `--send` or `-x`. That isn't new here, and changing
it would change no-`--send` renders, which the brief rules out. The only
effect on this row was status detection, now anchored. If it should be
refused, that's a follow-up for the script and serve together.
- **The template's comment still says Jason starts it.**
`mosaic-webui.service.in` lines 6-9 say Jason starts it from a personal
shell and that an agent starting it works around the human proof. The
template isn't in this row's files and is rendered into every unit, so
changing it would break the byte identity the brief asks for. Bringing
it in line with decision 102 is a follow-up (a template change plus the
render tests that pin it).
- Nothing here starts, enables or restarts a unit, and no test reaches the
real systemd: every install and uninstall uses `--no-reload` or a
stand-in systemctl.
+366
View File
@@ -0,0 +1,366 @@
diff --git a/docs/TOOLS.md b/docs/TOOLS.md
index 631e1814..89c85715 100644
--- a/docs/TOOLS.md
+++ b/docs/TOOLS.md
@@ -246,17 +246,22 @@ scripts/webui-service.sh install [--dir DIR] [--no-reload] [FLAGS]
scripts/webui-service.sh uninstall [--dir DIR] [--no-reload]
scripts/webui-service.sh status [--dir DIR]
# FLAGS: [--port N] [--business ID] [--board URL]
-# [--public-host NAME --auth-header HEADER --allow-user ID]
+# [--public-host NAME --auth-header HEADER --allow-user ID [--send]]
```
The systemd user unit `mosaic-webui.service` runs the Console,
`node packages/webui/src/cli.mjs serve`, with serve's flags as given:
`--port` defaults to 7332 (7330 is the operator's own Console), and the
-preview set goes all three or none. The template is
+preview set goes all three or none. `--send` (row 83) goes only with the
+preview set and is refused otherwise (exit 1, nothing written); ExecStart
+then ends with `--send`, and the Console needs the system config at start
+(`--send needs the bus configured`, exit 2). Without `--send` the unit is
+read-only. The template is
`packages/webui/systemd/mosaic-webui.service.in`. `install` writes a temp
file, renames it and runs `systemctl --user daemon-reload`; `uninstall`
-refuses while the unit is active; `status` prints the unit state and
-`GET /healthz` on the installed port. Each value is held to a plain
+refuses while the unit is active; `status` prints the unit state, whether
+the installed unit sends, and `GET /healthz` on the installed port. Each
+value is held to a plain
character set the unit can carry (no space, `%`, `$`, quote, backslash,
`&` or `|`) and refused otherwise. The unit sets no `MOSAIC_*`, `CLAUDE*`,
`CODEX*` or `PI_*` variable. Serve's exit 2 (usage, config, or a port
@@ -264,9 +269,11 @@ already in use) is not restarted.
There is no start, stop, enable or restart verb. The Console reads the bus
through the human transport, so whoever starts it holds the human's
-capability; an agent starting it, even through systemd, works around the
-human proof (lead decisions 62 and 94). Jason starts it from his own shell:
-`systemctl --user enable --now mosaic-webui`.
+capability. Jason starts and restarts it
+(`systemctl --user enable --now mosaic-webui`), and so may Mos under lead
+decision 102, which accepts that sends from a Console Mos started enter the
+trail as Jason (`human-cli`). Any other agent starting it, even through
+systemd, works around the human proof (lead decisions 62 and 94).
Exit codes: `0` ok · `1` failed or refused · `4` usage.
## Discord connector (`scripts/discord.sh`)
diff --git a/packages/webui/README.md b/packages/webui/README.md
index 5ac12d95..4a7cf535 100644
--- a/packages/webui/README.md
+++ b/packages/webui/README.md
@@ -100,8 +100,15 @@ Under `--send` the first of those can also send (next section).
To run the Console as a systemd user unit, `scripts/webui-service.sh`
renders and installs one with these flags (row 75; `docs/TOOLS.md`
-"Console service"). Jason starts it from his own shell (lead decision 94).
-The script takes no `--send`, so a unit it installs is read-only.
+"Console service"). Jason starts and restarts it, and so may Mos under lead
+decision 102, which accepts that sends from a Console Mos started enter the
+trail as Jason. Any other agent starting it works around the human proof
+(lead decisions 62 and 94). Without `--send` the unit is read-only. The script takes `--send` (row 83)
+only with the three preview flags; otherwise it exits 1 and writes nothing.
+With it, the unit's ExecStart ends with `--send`, and the Console needs the
+system config at start: without it serve refuses with `--send needs the bus
+configured` and exits 2, which the unit doesn't restart. `status` says
+whether the installed unit sends.
## Message a role (row 76, #1570)
@@ -113,9 +120,11 @@ node packages/webui/src/cli.mjs serve --port 7332 \
`--send` is off by default. It goes only with the three preview flags and
refuses at start without them. It also needs the system config: without it
the start refuses with `--send needs the bus configured`, where a read-only
-Console would still serve the board. With `--send` the Inbox has a "Message
-a role" box: a role picker filled from the agents read, a text box and
-Send. The message reaches the role as a message from the human, class INFO,
+Console would still serve the board. A unit that sends comes from
+`scripts/webui-service.sh install` with `--send` (above). With `--send` the
+Inbox has a "Message a role" box: a role picker filled from the agents read,
+a text box and Send. The message reaches the role as a message from the
+human, class INFO,
through `message.send` on the human transport. It goes the same way as one
sent from the CLI, with class INFO where `mosaic talk` uses REQUEST (lead
decision 95). Decisions are still answered in a
diff --git a/packages/webui/tests/service.test.mjs b/packages/webui/tests/service.test.mjs
index d50b6652..da440d6b 100644
--- a/packages/webui/tests/service.test.mjs
+++ b/packages/webui/tests/service.test.mjs
@@ -11,10 +11,35 @@ import { fileURLToPath } from 'node:url';
// scripts/webui-service.sh (row 75, #1569). Nothing here starts a unit or
// reaches the real systemd: install and uninstall run with --no-reload or a
// stand-in systemctl on PATH, and XDG_CONFIG_HOME points into a temp dir.
+// Row 83 (#1576) adds --send, taken only with the preview set.
const REPO = fileURLToPath(new URL('../../..', import.meta.url)).replace(/\/$/, '');
const SCRIPT = join(REPO, 'scripts', 'webui-service.sh');
const NODE_DIR = dirname(process.execPath);
const PREVIEW = ['--public-host', 'console.example.com', '--auth-header', 'X-authentik-uid', '--allow-user', 'jason'];
+const PREVIEW_FLAGS = '--public-host console.example.com --auth-header X-authentik-uid --allow-user jason';
+const SEND_REFUSAL = /^webui-service: --send needs the preview mode \(--public-host, --auth-header and --allow-user\)$/m;
+const TEMPLATE = readFileSync(join(REPO, 'packages', 'webui', 'systemd', 'mosaic-webui.service.in'), 'utf8');
+
+// The unit as the base script renders it (row 75): the template with its four
+// placeholders filled in, nothing else. Row 83 adds --send and must leave
+// every unit rendered without it byte for byte as this.
+function filled(flags) {
+ const base = '/usr/local/bin:/usr/bin:/bin';
+ const path = `:${base}:`.includes(`:${NODE_DIR}:`) ? base : `${NODE_DIR}:${base}`;
+ return TEMPLATE.replaceAll('@REPO@', REPO).replaceAll('@PATH@', path).replaceAll('@NODE@', process.execPath).replaceAll('@FLAGS@', flags);
+}
+
+// A loopback port with nothing listening, so status's /healthz request
+// reaches no real Console.
+async function closedPort() {
+ const server = createServer();
+ server.listen(0, '127.0.0.1');
+ await once(server, 'listening');
+ const { port } = server.address();
+ server.close();
+ await once(server, 'close');
+ return port;
+}
function tmp(t) {
const dir = mkdtempSync(join(tmpdir(), 'webui-service-'));
@@ -114,7 +139,7 @@ test('the rendered unit names none of the human proof agent markers and sets no
const source = readFileSync(join(REPO, 'packages', 'bus', 'src', 'human.mjs'), 'utf8');
const markers = [...source.match(/const MARKERS = \[([^\]]*)\]/)[1].matchAll(/'([A-Z_]+)'/g)].map((m) => m[1]);
assert.ok(markers.length >= 7 && markers.includes('MOSAIC_BUS_CAP') && markers.includes('CLAUDECODE'), markers.join(' '));
- for (const args of [[], [...PREVIEW, '--business', 'mosaic']]) {
+ for (const args of [[], [...PREVIEW, '--business', 'mosaic'], [...PREVIEW, '--send']]) {
const r = await run(dir, ['render', ...args]);
assert.equal(r.status, 0, r.stderr);
for (const marker of markers) assert.ok(!r.stdout.includes(marker), marker);
@@ -212,9 +237,129 @@ test('status prints the unit state and GET /healthz on the installed port', asyn
test('usage: an unknown verb or flag, and start or enable, exit 4', async (t) => {
const dir = tmp(t);
- for (const args of [[], ['start'], ['stop'], ['enable'], ['restart'], ['render', '--bogus'], ['render', '--port'], ['render', '--dir', dir], ['render', '--no-reload'], ['uninstall', '--port', '7332'], ['status', '--business', 'mosaic'], ['status', '--no-reload']]) {
+ for (const args of [[], ['start'], ['stop'], ['enable'], ['restart'], ['render', '--bogus'], ['render', '--port'], ['render', '--dir', dir], ['render', '--no-reload'], ['uninstall', '--port', '7332'], ['status', '--business', 'mosaic'], ['status', '--no-reload'], ['render', '--send', 'yes', ...PREVIEW]]) {
const r = await run(dir, args);
assert.equal(r.status, 4, args.join(' '));
assert.match(r.stderr, /^usage: scripts\/webui-service\.sh/);
}
});
+
+test('without --send, render is the template with its placeholders filled, byte for byte', async (t) => {
+ const dir = tmp(t);
+ for (const [args, flags] of [
+ [[], '--port 7332'],
+ [['--port', '7340'], '--port 7340'],
+ [['--business', 'mosaic'], '--port 7332 --business mosaic'],
+ [PREVIEW, `--port 7332 ${PREVIEW_FLAGS}`],
+ [['--port', '7340', '--business', 'mosaic.dev_1', '--board', 'http://127.0.0.1:7331', ...PREVIEW], `--port 7340 --business mosaic.dev_1 --board http://127.0.0.1:7331 ${PREVIEW_FLAGS}`],
+ ]) {
+ const r = await run(dir, ['render', ...args]);
+ assert.equal(r.status, 0, r.stderr);
+ assert.equal(r.stdout, filled(flags), args.join(' '));
+ }
+});
+
+test('render --send with the preview set ends ExecStart with --send and changes nothing else', async (t) => {
+ const dir = tmp(t);
+ const extra = ['--port', '7340', '--business', 'mosaic'];
+ for (const args of [[...PREVIEW, '--send'], ['--send', ...PREVIEW], [...PREVIEW.slice(0, 2), '--send', ...PREVIEW.slice(2)], ['--send', ...extra, ...PREVIEW]]) {
+ const plain = args.filter((a) => a !== '--send');
+ const flags = `${plain.includes('--port') ? '--port 7340 --business mosaic' : '--port 7332'} ${PREVIEW_FLAGS}`;
+ const r = await run(dir, ['render', ...args]);
+ assert.equal(r.status, 0, r.stderr);
+ assert.equal(r.stdout, filled(`${flags} --send`), args.join(' '));
+ assert.ok(execStart(r.stdout).endsWith(' --allow-user jason --send'), execStart(r.stdout));
+ const without = await run(dir, ['render', ...plain]);
+ assert.equal(without.status, 0, without.stderr);
+ const a = r.stdout.split('\n'), b = without.stdout.split('\n');
+ assert.equal(a.length, b.length);
+ assert.deepEqual(a.map((l, i) => (l === b[i] ? null : [l, b[i]])).filter(Boolean), [[`${execStart(without.stdout)} --send`, execStart(without.stdout)]]);
+ }
+});
+
+test('--send without the whole preview set is refused and writes nothing', async (t) => {
+ const dir = tmp(t);
+ const pairs = [PREVIEW.slice(0, 2), PREVIEW.slice(2, 4), PREVIEW.slice(4, 6)];
+ const partial = [[], ['--port', '7340'], ['--business', 'mosaic'], ...pairs, [...pairs[0], ...pairs[1]], [...pairs[0], ...pairs[2]], [...pairs[1], ...pairs[2]]];
+ for (const args of partial) {
+ const r = await run(dir, ['render', '--send', ...args]);
+ assert.equal(r.status, 1, args.join(' '));
+ assert.equal(r.stdout, '', args.join(' '));
+ assert.match(r.stderr, SEND_REFUSAL, args.join(' '));
+ }
+ const bin = standIn(dir);
+ const fresh = join(dir, 'never');
+ for (const args of partial) {
+ const r = await run(dir, ['install', '--dir', fresh, '--no-reload', ...args, '--send'], bin);
+ assert.equal(r.status, 1, args.join(' '));
+ assert.match(r.stderr, SEND_REFUSAL, args.join(' '));
+ }
+ assert.ok(!existsSync(fresh), 'a refusal creates no directory');
+ const unitDir = join(dir, 'units');
+ const target = join(unitDir, 'mosaic-webui.service');
+ assert.equal((await run(dir, ['install', '--dir', unitDir, '--no-reload', ...PREVIEW], bin)).status, 0);
+ const before = readFileSync(target, 'utf8');
+ const r = await run(dir, ['install', '--dir', unitDir, '--no-reload', '--send', ...PREVIEW.slice(0, 4)], bin);
+ assert.equal(r.status, 1);
+ assert.equal(readFileSync(target, 'utf8'), before, 'the installed unit is left alone');
+ assert.deepEqual(readdirSync(unitDir), ['mosaic-webui.service']);
+ assert.ok(!existsSync(join(dir, 'calls.txt')), 'no systemctl call');
+});
+
+test('install --send into --dir with --no-reload writes the sending unit, and status says whether it sends', async (t) => {
+ const dir = tmp(t);
+ const unitDir = join(dir, 'units');
+ const target = join(unitDir, 'mosaic-webui.service');
+ const port = await closedPort();
+ const flags = `--port ${port} ${PREVIEW_FLAGS}`;
+ const r = await run(dir, ['install', '--dir', unitDir, '--no-reload', '--port', String(port), ...PREVIEW, '--send']);
+ assert.equal(r.status, 0, r.stderr);
+ assert.match(r.stdout, new RegExp(`^written: ${target}$`, 'm'));
+ // Lead decision 102: Jason or Mos starts it, no other agent.
+ assert.match(r.stdout, /^this script starts nothing: .* Jason\nstarts and restarts it, or Mos under lead decision 102 .*\ntrail as Jason\); no other agent does:$/m);
+ assert.match(r.stdout, new RegExp(`^serve: \\S+ ${REPO.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}/packages/webui/src/cli\\.mjs serve ${flags} --send$`, 'm'));
+ assert.equal(readFileSync(target, 'utf8'), filled(`${flags} --send`));
+ assert.equal(statSync(target).mode & 0o777, 0o644);
+ assert.deepEqual(readdirSync(unitDir), ['mosaic-webui.service']);
+ const bin = standIn(dir);
+ const sending = await run(dir, ['status', '--dir', unitDir], bin);
+ assert.equal(sending.status, 0, sending.stderr);
+ assert.match(sending.stdout, /^send: on \(--send; human messages to roles\)$/m);
+ assert.match(sending.stdout, /^healthz: \S+ unreachable \(ECONNREFUSED\)$/m);
+ const again = await run(dir, ['install', '--dir', unitDir, '--no-reload', '--port', String(port), '--send', ...PREVIEW], bin);
+ assert.match(again.stdout, new RegExp(`^unchanged: ${target}$`, 'm'));
+ const readOnly = await run(dir, ['install', '--dir', unitDir, '--no-reload', '--port', String(port), ...PREVIEW], bin);
+ assert.match(readOnly.stdout, new RegExp(`^written: ${target}$`, 'm'));
+ assert.equal(readFileSync(target, 'utf8'), filled(flags));
+ const off = await run(dir, ['status', '--dir', unitDir], bin);
+ assert.match(off.stdout, /^send: off \(read-only\)$/m);
+ assert.doesNotMatch(off.stdout, /^send: on/m);
+ // The only systemctl calls are status's reads; install ran none.
+ assert.deepEqual(readFileSync(join(dir, 'calls.txt'), 'utf8').split('\n').filter(Boolean).filter((c) => !/^--user (is-enabled|is-active|show) mosaic-webui\.service/.test(c)), []);
+ const none = await run(dir, ['status', '--dir', join(dir, 'empty')], bin);
+ assert.doesNotMatch(none.stdout, /^send:/m, 'no unit, no send line');
+ // serve takes --send as an allowed user, so ExecStart can end with
+ // `--allow-user --send` on a unit that doesn't send.
+ const oddUser = [...PREVIEW.slice(0, 4), '--allow-user', '--send'];
+ for (const [args, ending, state] of [[oddUser, '--allow-user --send', 'off'], [[...oddUser, '--send'], '--allow-user --send --send', 'on']]) {
+ const r = await run(dir, ['install', '--dir', unitDir, '--no-reload', '--port', String(port), ...args]);
+ assert.equal(r.status, 0, r.stderr);
+ assert.ok(execStart(readFileSync(target, 'utf8')).endsWith(` ${ending}`), args.join(' '));
+ assert.match((await run(dir, ['status', '--dir', unitDir], bin)).stdout, new RegExp(`^send: ${state} `, 'm'), args.join(' '));
+ }
+});
+
+test('uninstall and status take no --send', async (t) => {
+ const dir = tmp(t);
+ const bin = standIn(dir);
+ const unitDir = join(dir, 'units');
+ assert.equal((await run(dir, ['install', '--dir', unitDir, '--no-reload', ...PREVIEW, '--send'])).status, 0);
+ for (const args of [['uninstall', '--dir', unitDir, '--send'], ['uninstall', '--dir', unitDir, '--no-reload', '--send'], ['status', '--dir', unitDir, '--send']]) {
+ const r = await run(dir, args, bin);
+ assert.equal(r.status, 4, args.join(' '));
+ assert.match(r.stderr, /^usage: scripts\/webui-service\.sh/);
+ assert.equal(r.stdout, '', args.join(' '));
+ }
+ assert.ok(existsSync(join(unitDir, 'mosaic-webui.service')), 'still installed');
+ assert.ok(!existsSync(join(dir, 'calls.txt')), 'no systemctl call');
+});
diff --git a/scripts/webui-service.sh b/scripts/webui-service.sh
index 4ff06833..944bb97a 100755
--- a/scripts/webui-service.sh
+++ b/scripts/webui-service.sh
@@ -11,19 +11,24 @@
# rename) and run `systemctl --user daemon-reload`
# uninstall [--dir DIR] [--no-reload]
# remove the unit; refuses while it is active
-# status [--dir DIR] unit state, then GET /healthz on its port
+# status [--dir DIR] unit state, whether it sends, then GET
+# /healthz on its port
#
# FLAGS are serve's: --port N (default 7332; 7330 is the operator's own
# Console), --business ID, --board URL, and the preview set --public-host
-# NAME --auth-header HEADER --allow-user ID, all three or none. systemd
+# NAME --auth-header HEADER --allow-user ID, all three or none, and --send
+# (row 83), taken only with the preview set: the unit's Console then sends
+# human messages to roles and needs the system config at start. systemd
# expands % and $ and splits on spaces, so each value is held to a plain
# character set and refused otherwise. None is a secret.
#
# There is no start, stop, enable or restart verb. The Console reads the
# bus through the human transport, so whoever starts it holds the human's
-# capability. An agent starting it, even detached through systemd, works
-# around the human proof (lead decisions 62 and 94). Jason starts it from
-# his own shell: `systemctl --user enable --now mosaic-webui`.
+# capability. Jason starts and restarts it
+# (`systemctl --user enable --now mosaic-webui`), and so may Mos under lead
+# decision 102, which accepts that sends from a Console Mos started enter
+# the trail as Jason (human-cli). Any other agent starting it, even detached
+# through systemd, works around the human proof (lead decisions 62 and 94).
#
# Exit codes: 0 ok, 1 operation failed, 4 usage.
set -euo pipefail
@@ -31,7 +36,7 @@ REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
TEMPLATE="$REPO/packages/webui/systemd/mosaic-webui.service.in"
UNIT_NAME="mosaic-webui.service"
USAGE="usage: scripts/webui-service.sh render [FLAGS] | install [--dir DIR] [--no-reload] [FLAGS] | uninstall [--dir DIR] [--no-reload] | status [--dir DIR]
- FLAGS: [--port N] [--business ID] [--board URL] [--public-host NAME --auth-header HEADER --allow-user ID]"
+ FLAGS: [--port N] [--business ID] [--board URL] [--public-host NAME --auth-header HEADER --allow-user ID [--send]]"
die() { echo "webui-service: $*" >&2; exit 1; }
usage() { echo "$USAGE" >&2; exit 4; }
@@ -45,6 +50,7 @@ BOARD=""
PUBLIC_HOST=""
AUTH_HEADER=""
ALLOW_USER=""
+SEND=0
SERVE_SET=0
parse_flags() {
@@ -58,6 +64,7 @@ parse_flags() {
--public-host) [ $# -ge 2 ] || usage; PUBLIC_HOST="$2"; SERVE_SET=1; shift 2 ;;
--auth-header) [ $# -ge 2 ] || usage; AUTH_HEADER="$2"; SERVE_SET=1; shift 2 ;;
--allow-user) [ $# -ge 2 ] || usage; ALLOW_USER="$2"; SERVE_SET=1; shift 2 ;;
+ --send) SEND=1; SERVE_SET=1; shift ;;
*) usage ;;
esac
done
@@ -86,6 +93,9 @@ serve_flags() {
[ -z "$PUBLIC_HOST" ] || preview=$((preview + 1))
[ -z "$AUTH_HEADER" ] || preview=$((preview + 1))
[ -z "$ALLOW_USER" ] || preview=$((preview + 1))
+ # serve refuses --send without the preview set; refuse it here first,
+ # so no unit is written that would only exit 2 at start.
+ [ "$SEND" = 0 ] || [ "$preview" -eq 3 ] || die "--send needs the preview mode (--public-host, --auth-header and --allow-user)"
if [ "$preview" -ne 0 ]; then
[ "$preview" -eq 3 ] || die "--public-host, --auth-header and --allow-user go together: all three or none"
case "$PUBLIC_HOST" in [A-Za-z0-9]*) ;; *) die "public host must be a hostname, no port or scheme: $PUBLIC_HOST" ;; esac
@@ -95,6 +105,7 @@ serve_flags() {
case "$ALLOW_USER" in *[!A-Za-z0-9._@+-]*) die "allowed user contains a character the unit cannot carry: $ALLOW_USER" ;; esac
flags="$flags --public-host $PUBLIC_HOST --auth-header $AUTH_HEADER --allow-user $ALLOW_USER"
fi
+ [ "$SEND" = 0 ] || flags="$flags --send"
echo "$flags"
}
@@ -138,8 +149,9 @@ install_unit() {
echo "daemon-reload done"
fi
cat <<MSG
-this script starts nothing: the Console holds the human's capability, so the
-human starts it from their own shell:
+this script starts nothing: the Console holds the human's capability. Jason
+starts and restarts it, or Mos under lead decision 102 (sends then enter the
+trail as Jason); no other agent does:
systemctl --user enable --now mosaic-webui start now and at login
systemctl --user status mosaic-webui
journalctl --user -u mosaic-webui -f the Console's log
@@ -182,6 +194,13 @@ status_unit() {
echo "healthz: not checked; no unit at $target"
return 0
fi
+ # render puts --send right after the allowed user, which may itself be
+ # the value --send; a unit that sends ends with both.
+ if grep -q '^ExecStart=.* --allow-user [^ ][^ ]* --send$' "$target"; then
+ echo "send: on (--send; human messages to roles)"
+ else
+ echo "send: off (read-only)"
+ fi
local port
port="$(sed -n 's/^ExecStart=.* --port \([0-9][0-9]*\).*$/\1/p' "$target")"
[ -n "$port" ] || { echo "healthz: not checked; no --port in $target"; return 0; }
@@ -0,0 +1,4 @@
fa02b37dd32589c6f85083bcda286af1d9b3890216807d9b6eb96bfac621be44 scripts/webui-service.sh
4f199141046bd15dae24b2c33b0bbf0226f7ae6da820acb0b185eafe7458ebe3 packages/webui/tests/service.test.mjs
09c4a1ddf8bdaa3948939b0667d05ec1d7a981ff3e9ae30371bdbf24459025d6 packages/webui/README.md
39e2752b8c0d7c622f515ae8724bcd5c90914c5aa99c48126b6a0c29985b3358 docs/TOOLS.md
@@ -0,0 +1,16 @@
2026-10-12T03:54:20Z
node-webui rc=0
test-auth rc=0
test-conductor rc=0
test-config rc=0
test-discord rc=0
test-extension-package rc=0
test-foundation rc=0
test-queue rc=0
test-release rc=0
test-task rc=0
GATE-DONE
2026-10-12T03:58:42Z
ℹ tests 81
ℹ pass 81
ℹ fail 0
+88
View File
@@ -0,0 +1,88 @@
#!/usr/bin/env node
// Dewey row 83: the base script (d13b9300) and the candidate, run side by side
// in one scratch worktree (same REPO, same template) on every argument list
// the base service tests use, without --send. Expected: stdout, stderr and
// exit status identical, and every installed file identical, except the usage
// text on exit 4 (it names [--send]), status's new send: line, and the
// install message's start wording (lead decision 102).
// Usage: identity.mjs <worktree>, where scripts/webui-service-base.sh is the
// base script and scripts/webui-service.sh the candidate. Nothing here reaches
// the real systemd: install and uninstall run with --no-reload, status with a
// stand-in systemctl.
import { spawnSync } from 'node:child_process';
import { chmodSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { dirname, join } from 'node:path';
const WT = process.argv[2];
const BASE = join(WT, 'scripts', 'webui-service-base.sh');
const CAND = join(WT, 'scripts', 'webui-service.sh');
const NODE_DIR = dirname(process.execPath);
const PREVIEW = ['--public-host', 'console.example.com', '--auth-header', 'X-authentik-uid', '--allow-user', 'jason'];
const scratch = mkdtempSync(join(tmpdir(), 'r83-identity-'));
const bin = join(scratch, 'bin');
mkdirSync(bin);
writeFileSync(join(bin, 'systemctl'), '#!/bin/sh\ncase "$*" in *is-enabled*) echo enabled ;; *is-active*) echo inactive ;; esac\nexit 0\n');
chmodSync(join(bin, 'systemctl'), 0o755);
const set = (flag, value) => {
const preview = PREVIEW.slice();
const i = preview.indexOf(flag);
if (i >= 0) { preview[i + 1] = value; return preview; }
return [flag, value];
};
const U = join(scratch, 'units');
const cases = [
[], ['--port', '7340', '--business', 'mosaic.dev_1', '--board', 'http://127.0.0.1:7331', ...PREVIEW],
PREVIEW, ['--business', 'mosaic'], [...PREVIEW, '--business', 'mosaic'],
PREVIEW.slice(0, 2), PREVIEW.slice(2, 6), [...PREVIEW.slice(0, 2), ...PREVIEW.slice(4, 6)],
].map((a) => ['render', ...a]);
for (const flag of ['--business', '--board', '--public-host', '--auth-header', '--allow-user']) {
for (const bad of [' x', '%n', '\n', '$HOME', '"', '\\', '|', '&']) {
cases.push(['render', ...set(flag, flag === '--board' ? `http://127.0.0.1:7331${bad}` : `a${bad}b`)]);
}
}
for (const port of ['0', '65536', '7a', '', '-1', '999999', '7332', '65535']) cases.push(['render', '--port', port]);
for (const args of [['--business', 'Mosaic'], ['--business', '-x'], ['--business', 'a'.repeat(65)], ['--board', 'https://127.0.0.1:7331'], ['--board', '127.0.0.1:7331'], set('--public-host', 'console.example.com:443'), set('--public-host', 'https://console.example.com'), set('--auth-header', 'X_uid')]) cases.push(['render', ...args]);
for (const args of [[], ['start'], ['stop'], ['enable'], ['restart'], ['render', '--bogus'], ['render', '--port'], ['render', '--dir', scratch], ['render', '--no-reload'], ['uninstall', '--port', '7332'], ['status', '--business', 'mosaic'], ['status', '--no-reload'], ['--help']]) cases.push(args);
// Install, then the same again (unchanged), a refusal over it, then uninstall.
cases.push(['install', '--dir', U, '--no-reload', '--business', 'mosaic'], ['install', '--dir', U, '--no-reload', '--business', 'mosaic'], ['install', '--dir', U, '--no-reload', '--business', 'a b'], ['install', '--dir', join(scratch, 'never'), '--no-reload', '--port', '0'], ['install', '--dir', U, '--no-reload', ...PREVIEW, '--port', '7340'], ['status', '--dir', U], ['uninstall', '--dir', U, '--no-reload'], ['uninstall', '--dir', U, '--no-reload'], ['status', '--dir', U]);
const env = { PATH: `${bin}:${NODE_DIR}:/usr/bin:/bin`, HOME: scratch, XDG_CONFIG_HOME: join(scratch, 'config') };
const files = (dir) => (existsSync(dir) ? readdirSync(dir).sort().map((f) => `${f}\n${readFileSync(join(dir, f), 'utf8')}`).join('\n') : '<absent>');
// Each script runs the whole sequence from a clean scratch, so install state carries the same way.
function sequence(script) {
rmSync(U, { recursive: true, force: true });
rmSync(join(scratch, 'never'), { recursive: true, force: true });
return cases.map((args) => {
const r = spawnSync(script, args, { env, encoding: 'utf8' });
return { status: r.status, stdout: r.stdout, stderr: r.stderr, units: files(U), never: existsSync(join(scratch, 'never')) };
});
}
const base = sequence(BASE);
const cand = sequence(CAND);
const usageBase = readFileSync(BASE, 'utf8').match(/^ FLAGS: .*"$/m)[0].slice(0, -1);
const usageCand = readFileSync(CAND, 'utf8').match(/^ FLAGS: .*"$/m)[0].slice(0, -1);
// The install message's lines before its command list.
const message = (script) => { const s = readFileSync(script, 'utf8'); return s.slice(s.indexOf('cat <<MSG\n') + 10, s.indexOf(' systemctl --user enable --now mosaic-webui start')); };
const msgBase = message(BASE), msgCand = message(CAND);
let same = 0, expected = 0, unexpected = 0;
cases.forEach((args, i) => {
const b = base[i], c = cand[i];
const label = JSON.stringify(args).replaceAll(scratch, '$S');
if (JSON.stringify(b) === JSON.stringify(c)) { same++; return; }
const usageOnly = b.status === 4 && c.status === 4 && JSON.stringify({ ...b, stderr: b.stderr.replace(usageBase, usageCand) }) === JSON.stringify(c);
const helpOnly = args[0] === '--help' && JSON.stringify({ ...b, stdout: b.stdout.replace(usageBase, usageCand) }) === JSON.stringify(c);
const bl = b.stdout.split('\n'), cl = c.stdout.split('\n');
const sendLineOnly = args[0] === 'status' && b.status === c.status && b.stderr === c.stderr && b.units === c.units
&& JSON.stringify(cl.filter((l) => !/^send: off \(read-only\)$/.test(l))) === JSON.stringify(bl) && cl.length === bl.length + 1;
const messageOnly = args[0] === 'install' && b.status === 0 && msgBase !== msgCand && b.stdout.includes(msgBase) && JSON.stringify({ ...b, stdout: b.stdout.replace(msgBase, msgCand) }) === JSON.stringify(c);
if (messageOnly) { expected++; console.log(`start wording only ${label}`); return; }
if (usageOnly || helpOnly) { expected++; console.log(`usage text only ${label}`); return; }
if (sendLineOnly) { expected++; console.log(`send: line only ${label}: + ${cl.find((l) => l.startsWith('send:'))}`); return; }
unexpected++;
console.log(`DIFFERS ${label}\n base ${JSON.stringify(b)}\n cand ${JSON.stringify(c)}`);
});
console.log(`${cases.length} cases: ${same} identical, ${expected} expected differences, ${unexpected} unexpected`);
rmSync(scratch, { recursive: true, force: true });
process.exitCode = unexpected ? 1 : 0;
+18
View File
@@ -0,0 +1,18 @@
usage text only []
usage text only ["start"]
usage text only ["stop"]
usage text only ["enable"]
usage text only ["restart"]
usage text only ["render","--bogus"]
usage text only ["render","--port"]
usage text only ["render","--dir","$S"]
usage text only ["render","--no-reload"]
usage text only ["uninstall","--port","7332"]
usage text only ["status","--business","mosaic"]
usage text only ["status","--no-reload"]
usage text only ["--help"]
start wording only ["install","--dir","$S/units","--no-reload","--business","mosaic"]
start wording only ["install","--dir","$S/units","--no-reload","--business","mosaic"]
start wording only ["install","--dir","$S/units","--no-reload","--public-host","console.example.com","--auth-header","X-authentik-uid","--allow-user","jason","--port","7340"]
send: line only ["status","--dir","$S/units"]: + send: off (read-only)
86 cases: 69 identical, 17 expected differences, 0 unexpected
+89
View File
@@ -0,0 +1,89 @@
#!/usr/bin/env python3
# Dewey row 83 mutants: single-site edits to scripts/webui-service.sh in a mutant worktree (never
# the build tree). Each mutant runs against the base tests (d13b9300's service.test.mjs, copied in
# as tests/base-service.test.mjs) and the candidate tests (tests/service.test.mjs), then
# identity.mjs. The candidate is uncommitted there, so the script is restored from the copy read
# before the edit, not from git.
# Usage: mutants.py [mutant ...]. A site that doesn't match exactly once is BAD-SITE.
import os, subprocess, sys
R = os.path.expanduser('~/dewey-scratch/r83')
T = R + '/mut'
OUT = R + '/out/mut'
S = 'scripts/webui-service.sh'
BASE_TESTS = ['node', '--test', 'packages/webui/tests/base-service.test.mjs']
CAND_TESTS = ['node', '--test', 'packages/webui/tests/service.test.mjs']
IDENTITY = ['node', os.path.join(os.path.dirname(os.path.abspath(__file__)), 'identity.mjs'), T]
CHECK = ' [ "$SEND" = 0 ] || [ "$preview" -eq 3 ] || die "--send needs the preview mode (--public-host, --auth-header and --allow-user)"\n'
APPEND = ' [ "$SEND" = 0 ] || flags="$flags --send"\n'
CASE = ' --send) SEND=1; SERVE_SET=1; shift ;;\n'
STATUS_IF = """ if grep -q '^ExecStart=.* --allow-user [^ ][^ ]* --send$' "$target"; then\n"""
STATUS_BLOCK = STATUS_IF + """ echo "send: on (--send; human messages to roles)"
else
echo "send: off (read-only)"
fi
"""
MUTANTS = [
# name, edits [(old, new)]
('send-no-preview-check', [(CHECK, '')]),
('send-needs-any-preview', [(CHECK, CHECK.replace('-eq 3', '-ne 0'))]),
('send-message-reworded', [(CHECK, CHECK.replace('needs the preview mode', 'needs the preview set'))]),
('send-no-append', [(APPEND, '')]),
('send-append-always', [(APPEND, ' flags="$flags --send"\n')]),
('send-append-before-preview', [(APPEND, ''), (' local preview=0\n', APPEND + ' local preview=0\n')]),
('send-default-on', [('SEND=0\n', 'SEND=1\n')]),
('send-no-serve-set', [(CASE, CASE.replace(' SERVE_SET=1;', ''))]),
('send-takes-value', [(CASE, ' --send) [ $# -ge 2 ] || usage; SEND=1; SERVE_SET=1; shift 2 ;;\n')]),
('send-flag-unknown', [(CASE, '')]),
('status-send-always-off', [(STATUS_IF, ' if false; then\n')]),
('status-send-always-on', [(STATUS_IF, ' if true; then\n')]),
('status-no-send-line', [(STATUS_BLOCK, '')]),
('status-send-anywhere', [(STATUS_IF, """ if grep -q -- '--send' "$target"; then\n""")]),
('status-send-suffix-only', [(STATUS_IF, """ if grep -q '^ExecStart=.* --send$' "$target"; then\n""")]),
]
def run(cmd):
try:
p = subprocess.run(cmd, cwd=T, capture_output=True, text=True, env={**os.environ, 'TMPDIR': R + '/tmp'}, timeout=600)
except subprocess.TimeoutExpired:
return 'TIMEOUT', 'timed out after 600 s'
return p.returncode, p.stdout + p.stderr
def verdict(rc):
return 'survived' if rc == 0 else ('TIMEOUT' if rc == 'TIMEOUT' else 'killed')
os.makedirs(OUT, exist_ok=True)
os.makedirs(R + '/tmp', exist_ok=True)
path = os.path.join(T, S)
pristine = open(path).read()
for label, cmd in (('base tests', BASE_TESTS), ('candidate tests', CAND_TESTS), ('identity', IDENTITY)):
rc, out = run(cmd)
open(f'{OUT}/baseline-{label.replace(" ", "-")}.txt', 'w').write(out)
print(f'baseline {label}:', 'pass' if rc == 0 else f'FAIL rc={rc}', flush=True)
want = sys.argv[1:]
results = []
for name, edits in MUTANTS:
if want and name not in want:
continue
bad = [old for old, _ in edits if pristine.count(old) != 1]
if bad:
results.append(f'{name} BAD-SITE ({[pristine.count(o) for o in bad]} matches)')
print(results[-1], flush=True)
continue
mutated = pristine
for old, new in edits:
mutated = mutated.replace(old, new)
try:
open(path, 'w').write(mutated)
b_rc, b_out = run(BASE_TESTS)
c_rc, c_out = run(CAND_TESTS)
i_rc, i_out = run(IDENTITY)
finally:
open(path, 'w').write(pristine)
open(f'{OUT}/{name}.txt', 'w').write(f'== base tests rc={b_rc}\n{b_out}\n== candidate tests rc={c_rc}\n{c_out}\n== identity rc={i_rc}\n{i_out}')
results.append(f'{name:28} base tests {verdict(b_rc):8} candidate tests {verdict(c_rc):8} identity {"same" if i_rc == 0 else "differs"}')
print(results[-1], flush=True)
assert open(path).read() == pristine, 'script not restored'
killed = sum(1 for r in results if 'candidate tests killed' in r)
print(f'{len(results)} mutants: candidate tests kill {killed}', flush=True)
+19
View File
@@ -0,0 +1,19 @@
baseline base tests: pass
baseline candidate tests: pass
baseline identity: pass
send-no-preview-check base tests survived candidate tests killed identity same
send-needs-any-preview base tests survived candidate tests killed identity same
send-message-reworded base tests survived candidate tests killed identity same
send-no-append base tests survived candidate tests killed identity same
send-append-always base tests killed candidate tests killed identity differs
send-append-before-preview base tests survived candidate tests killed identity same
send-default-on base tests killed candidate tests killed identity differs
send-no-serve-set base tests survived candidate tests killed identity same
send-takes-value base tests survived candidate tests killed identity same
send-flag-unknown base tests survived candidate tests killed identity same
status-send-always-off base tests survived candidate tests killed identity same
status-send-always-on base tests survived candidate tests killed identity differs
status-no-send-line base tests survived candidate tests killed identity same
status-send-anywhere base tests survived candidate tests killed identity same
status-send-suffix-only base tests survived candidate tests killed identity same
15 mutants: candidate tests kill 15