Unclaimed files left in the canonical checkout. Inventory: canonical-cleanup-inventory.md (commit-as-evidence groups). detect-secrets scan clean on 2026-11-02: hits are sha256 pins, git object ids, test sentinels already in tracked tests and unit-file lines.
532 lines
33 KiB
Markdown
532 lines
33 KiB
Markdown
# Row 190: several Mosaic Stack instances on one host
|
||
|
||
Brief: `docs/plans/2026-10-20_multi-instance.md`, blob `4e8f1e052894` (commit 48ebee0ff, rev 2259). Gitea
|
||
#1688. Owner Darkwing, reviewer Filbert, gate owner Sage. Design: `DESIGN.md` in this directory, revision 3
|
||
(sha256 75b21f7561cb...), approved by Filbert. Row 191 (Rocko) merge-chains on this candidate.
|
||
|
||
Round 1 is the record below up to "Follow-ups". Filbert's round 1 verdict was CHANGES (Gitea comment 29634,
|
||
queue rev 2362), and "Round 2" at the end replaces round 1's Candidate and gate sections.
|
||
|
||
## Files
|
||
|
||
| File | What |
|
||
|---|---|
|
||
| `DESIGN.md` | the approved design, revision 3 |
|
||
| `build.patch` | the change, `git diff da230ca48` over the 24 paths below, the two new files included |
|
||
| `candidate-manifest.sha256` | one line per path |
|
||
| `ti8/` | the T-I8 one-shot: the test file (not committed), its runner, `record.txt` and the run logs |
|
||
|
||
Base: da230ca48. Changed (22) and new (2):
|
||
|
||
- Scripts: `scripts/instance.sh` (new), `scripts/mosaic-config.mjs`, `scripts/bootstrap.sh`,
|
||
`scripts/bus-service.sh`, `scripts/webui-service.sh`, `scripts/discord-service.sh`, `scripts/stack-copy.sh`,
|
||
`scripts/stack-release.sh`.
|
||
- Templates: `packages/cli/systemd/mosaic-bus.service.in`, `packages/webui/systemd/mosaic-webui.service.in`,
|
||
`packages/discord/systemd/[email protected]`. Each gains one `@CONFIG_ENV@` line, and the bus
|
||
template's `EnvironmentFile` becomes `@SESSION_ENV@`.
|
||
- Guard: `packages/conversation/src/guard.mjs`.
|
||
- Tests: `packages/cli/tests/stack-instances.test.mjs` (new), `packages/cli/tests/stack-release.test.mjs`
|
||
(T55 and the stand-in), and fixture changes in `packages/cli/tests/host.test.mjs`,
|
||
`packages/webui/tests/service.test.mjs`, `packages/webui/tests/pinned-copy.test.mjs`,
|
||
`packages/discord/tests/copy-service.test.mjs` and `scripts/test-discord.sh`.
|
||
- Docs: `AGENTS.md` (the two lines in Mos's wording, sage-1335), `docs/TOOLS.md` (the `--instance` section,
|
||
which I wrote, per sage-1335), and the three package READMEs.
|
||
|
||
The worktree's `node_modules` symlink is not part of the candidate.
|
||
|
||
## The tests
|
||
|
||
| Design test | Where | Subtests |
|
||
|---|---|---|
|
||
| T-I1 bootstrap | `stack-instances.test.mjs` | 7: alpha and beta created with modes; a second run changes no byte, mode or mtime; a non-empty unmarked root refuses, `worktrees` included; an empty root is claimed and a lost marker refuses; a dangling symlink at the config path is never written through, named or default; bad names exit 4; `--instance dev` writes what no flag writes |
|
||
| T-I2 selection | `stack-instances.test.mjs` | 6: the four D1 rows, a named instance with no config, and the flag through an entry point |
|
||
| T-I3 render | T55 in `stack-release.test.mjs` | each instance's own `MOSAIC_CONFIG`, session env and Description, nothing of the other's |
|
||
| T-I4 ports | T55 | port-claimed for the Console port and the tester port, the default on a named port, the reserved ports, the stack-release run and `--plan` |
|
||
| T-I5 side by side | `stack-instances.test.mjs` | two real bus hosts at once, each answering from its own socket; stopping alpha leaves beta up |
|
||
| T-I6 no cross-reach | T55 | alpha's activate and rollback reach nothing of beta's, then swapped |
|
||
| T-I7 claims | `stack-instances.test.mjs` (4) and T55 (1) | the mismatch in every op, D1 row 2, a default config on alpha's root, and each bad marker shape; T55 runs D1 row 2 through `stack-release.sh activate` |
|
||
| T-I8 default unchanged | `ti8/` one-shot, plus the existing suites | see below |
|
||
| T-I9 busy scoping | T55 | another instance's bus refuses nothing; this one's does |
|
||
| T-I10 guard | `stack-instances.test.mjs` | every instance's config dir and roots, a corrupt config, `mosaic.bak` |
|
||
|
||
### T-I8
|
||
|
||
The one-shot copies the fixture repository, puts the da230ca48 blob of each of the 11 changed scripts,
|
||
templates and guard into the copy, removes `scripts/instance.sh` from it, and asserts each of the 11 now
|
||
differs from the candidate's. It then runs the same 15 commands from both trees on one seeded fixture:
|
||
|
||
- `bus-service.sh render` with and without `--pm`, `webui-service.sh render` with and without `--tester-port`,
|
||
`discord-service.sh render`, and `stack-copy.sh check` with and without `--business`;
|
||
- `stack-release.sh status`, four `activate ... --plan` forms (C1, C1 with `--tester-port`, C with
|
||
`--migrate`, and B, which refuses) and `rollback --plan` (which refuses);
|
||
- after a real `activate C1`, `status` and `rollback --plan` again.
|
||
|
||
Exit code, stdout and stderr match on all 15 (`ti8/record.txt` holds each sha256). The one normalization
|
||
replaces the base tree's path with the candidate's in the output, since each script names its own
|
||
repository. `discord-service.sh render` runs without `--copy`: a copy render needs
|
||
`packages/discord/src/cli.mjs`, which the fixture copy lacks (run 2 failed on that, `ti8/run-2.log`; run 3
|
||
passed, `ti8/run-3.log`). The checkout render goes through the same template fill. The test file is
|
||
`ti8/zz-ti8-oneshot.test.mjs.txt` (sha256 e888e55e140a...). It is not in the candidate, because it reads
|
||
da230ca48 from git and stops meaning anything once that base is old.
|
||
|
||
The default renders are also pinned by the existing fixtures: `service.test.mjs` and `copy-service.test.mjs`
|
||
compare a full render with the template, now with the `@CONFIG_ENV@` line deleted.
|
||
|
||
## Deviations and readings
|
||
|
||
1. Names alpha and beta, not the design's `a` and `b`. The D1 rule `^[a-z][a-z0-9]{1,15}$` needs two
|
||
characters or more, so `a` is invalid.
|
||
2. The new test file is `stack-instances.test.mjs`, not `instances.test.mjs`. It names what it tests the way
|
||
`stack-release.test.mjs` does.
|
||
3. Existing tests changed only to keep the instance selection off `~/.config/mosaic-dev`:
|
||
- `host.test.mjs`: the two bus-service tests pass a scratch `MOSAIC_CONFIG` that does not exist, so the
|
||
default names are selected as before.
|
||
- `scripts/test-discord.sh`: the service-unit block exports the same kind of scratch `MOSAIC_CONFIG` and
|
||
puts the caller's value back after it.
|
||
- `pinned-copy.test.mjs`: "copy writes nothing when an archived path is missing" removed
|
||
`scripts/mosaic-config.mjs`. The entry point now runs that file to select the instance before it copies,
|
||
so the test removes `packages/webui/README.md` instead, a path only the copy reads.
|
||
- `service.test.mjs` and `copy-service.test.mjs`: the expected render deletes the `@CONFIG_ENV@` line.
|
||
4. T-I4: the design says a named instance with no `--port`, and one on 7332, refuse `port-claimed`. They exit
|
||
4, with `needs --port` and `port-reserved` (7330, 7332 and 7333). Both are usage errors that need no unit
|
||
on disk, so I read them as exit 4 like the name errors. A port another installed Console holds is
|
||
`port-claimed`: exit 1 from `webui-service.sh`, exit 3 from `stack-release.sh`.
|
||
5. `stack-release.sh --plan` now runs `ports_unclaimed`, so the plan lists a `port-claimed` refusal with the
|
||
others. With no claimed port the plan text is unchanged (T-I8).
|
||
6. T-I5 checks the writer-lock pid against the broker process under the host, not the host itself. The host
|
||
spawns the broker, and the broker holds `bus/writer.lock`.
|
||
7. The `systemctl` stand-in now reads drop-ins in systemd's full order: `service.d`, then the unit name cut
|
||
after each hyphen (`mosaic-.service.d`, `mosaic-bus-.service.d`), then the template's and the unit's own
|
||
`.d`. Before, it read only the last two. T-I6 needs the prefix directories to exist and be read.
|
||
8. The webui template still says "The unit sets no MOSAIC_*" (`mosaic-webui.service.in:15`). A named unit now
|
||
sets `MOSAIC_CONFIG`. I left the comment as it is so the default render stays byte-identical; correcting it
|
||
changes every default unit's bytes. Follow-up for Sage to rule on. Round 2: Sage ruled (sage-1474), see
|
||
round 2's N3.
|
||
9. M11 had no killer in the first mutant list. I added the T-I1 subtest "a dangling symlink at the config path
|
||
is never written through, named or default": with `w` instead of `wx`, bootstrap creates the link's target.
|
||
|
||
## R3: bare default literals left in code
|
||
|
||
`grep -nE 'mosaic-dev|mosaic-bus@|mosaic-webui\.service|mosaic-discord@|\$BIN/mosaic\b'` over the eight
|
||
changed scripts, comment lines dropped. Every hit, with the reason it stays:
|
||
|
||
| Line | Literal | Why |
|
||
|---|---|---|
|
||
| `scripts/webui-service.sh:77` | `mosaic-webui.service.in` | the template file's path, which every instance renders from |
|
||
| `scripts/discord-service.sh:24` | `[email protected]` | same, for Discord |
|
||
| `scripts/discord-service.sh:162` | `.../mosaic-dev/config.json` | `status` reads the default's config only when no suffix is set; the next line takes `MOSAIC_INSTANCE_CONFIG` for a named instance. The base's line, kept so the default's status is unchanged |
|
||
| `scripts/instance.sh:87` | the default bus drop-in globs | the default instance's own scope, set in the one place every script reads |
|
||
| `scripts/instance.sh:88` | the default `SESSION_ENV` | the default's `EnvironmentFile` path, the base template's literal (T-I8 pins it) |
|
||
| `scripts/instance.sh:119` | the port holder's Console scan | it scans every Console unit, default and named, to find a port another instance holds |
|
||
| `scripts/stack-copy.sh:72` | `file="$BIN/mosaic"` | the default wrapper's path only; a named instance returns earlier through `wrapper_instance` |
|
||
| `scripts/stack-copy.sh:106` | `mosaic-bus@*\|mosaic-bus-*@*` | the `unit_root` case matches both the default's and a named instance's bus units |
|
||
| `scripts/stack-release.sh:1446` | `LIVE_CFG` default | the rehearsal reads `MOSAIC_CONFIG` first, and `instance_select` exports it for a named instance (`instance.sh:99`), so the default path is reached only by the default instance (F9) |
|
||
| `scripts/mosaic-config.mjs:307` | `DEFAULT_CONFIG` root `~/.mosaic-dev` | the default instance's config; a named bootstrap writes `~/.mosaic-NAME` |
|
||
|
||
The unit names themselves come from `scripts/instance.sh:80-85`, built from `$MOSAIC_UNIT_SUFFIX`, so they
|
||
don't match the grep.
|
||
|
||
## Run environment
|
||
|
||
- Every run is in the git worktree `/mnt/storage/scratch/darkwing-r190` (da230ca48 plus the candidate).
|
||
- TMPDIR is `/mnt/storage/scratch/tmp/dw190`, 30 bytes, within the cli suite's limit (row 177).
|
||
- T-I5's longest socket path is TMPDIR + `/si-XXXXXX/h/.mosaic-alpha/bus/broker.sock`, 72 bytes, under the
|
||
kernel's 107.
|
||
- No test reads or writes `~/.mosaic-dev`, `~/.config/mosaic-dev`, a live unit or a slot file. `HOME` is a
|
||
scratch directory in every new test.
|
||
|
||
## Results
|
||
|
||
### Builder gate
|
||
|
||
`~/darkwing-scratch/r190/gate.sh`, 2026-10-20T20:07:11Z-20:17:52Z, after busy.sh was clear on two checks 20 s
|
||
apart. 17 steps, each passing, counts summed by awk over the gate log:
|
||
|
||
| Step | Pass | Fail |
|
||
|---|---|---|
|
||
| `packages/cli` `node --test tests/*.test.mjs` | 501 | 0 |
|
||
| `packages/conversation` | 182 | 0 |
|
||
| `packages/control-board` | 124 | 0 |
|
||
| `packages/discord` | 183 | 0 |
|
||
| `packages/plugins` `harness.test.mjs` | 9 | 0 |
|
||
| `packages/webui` `service.test.mjs` | 31 | 0 |
|
||
| `packages/webui` `pinned-copy.test.mjs` | 10 | 0 |
|
||
| `scripts/test-auth.sh` | 15 | 0 |
|
||
| `scripts/test-conductor.sh` | 17 | 0 |
|
||
| `scripts/test-config.sh` | 24 | 0 |
|
||
| `scripts/test-discord.sh` | 78 | 0 |
|
||
| `scripts/test-extension-package.sh` | 18 | 0 |
|
||
| `scripts/test-foundation.sh` | 44 | 0 |
|
||
| `scripts/test-land-work.sh` | 86 | 0 |
|
||
| `scripts/test-queue.sh` | 156 node tests, 28 suite checks | 0 |
|
||
| `scripts/test-release.sh` | 14 | 0 |
|
||
| `scripts/test-task.sh` | 98 | 0 |
|
||
|
||
The node tests total 1196 and the selftests 422. test-task's compose ran as project `darkwing-r190`, and the
|
||
EXIT trap removed its network with no containers on it.
|
||
|
||
An earlier gate run (20:01:24Z) overlapped Launchpad's row 200 mutants, because busy.sh didn't match their
|
||
directory. I killed it and threw away its results. busy.sh now matches every `scratch/<seat>-r<N>/` directory.
|
||
|
||
### Mutants
|
||
|
||
`~/darkwing-scratch/r190/mutants.sh`, 2026-10-20T20:17:53Z-20:27:16Z, after the gate in the same slot. First
|
||
three controls with no change, each passing: SI (`stack-instances.test.mjs`), T55 (`--test-name-pattern='^T55 '`
|
||
on `stack-release.test.mjs`) and FIX (`webui service.test.mjs`, then `discord copy-service.test.mjs`).
|
||
|
||
Each mutant does the following, and the script stops at the first step that fails:
|
||
- back up the file and check the backup's sha256;
|
||
- make one exact replacement (count 1);
|
||
- run a syntax check;
|
||
- run its killers;
|
||
- put the file back and check its sha256.
|
||
|
||
Every restore matched.
|
||
|
||
| # | File | Replacement | Killers run | Fail | Failing tests |
|
||
|---|---|---|---|---|---|
|
||
| M1 | `instance.sh` | `local s=""` | T55 | 7 | T-I3, T-I4, both T-I6, T-I7, T-I9 |
|
||
| M2 | `instance.sh` | the port holder's match line becomes `:` | T55 | 2 | T-I4 |
|
||
| M3 | `mosaic-config.mjs` | `return;` at the top of `checkClaim` | SI, T55 | 7 + 2 | five T-I1 and T-I7 subtests in SI; T-I7 in T55 |
|
||
| M4 | `bus-service.sh` | adds `-e '/Environment=MOSAIC_CONFIG=/d'` to the render | T55 | 5 | T-I3, both T-I6, T-I9 |
|
||
| M5 | `instance.sh` | a named `SESSION_ENV` set to the default's path | T55 | 2 | T-I3 |
|
||
| M6 | `instance.sh` | a named `BUS_GLOB='mosaic-bus*@*.service'` | T55 | 6 | T-I9, and T-I3, T-I4 and both T-I6 through their seeds |
|
||
| M7 | `instance.sh` | `/^@CONFIG_ENV@$/d` becomes `s/^@CONFIG_ENV@$//` | FIX | 6 | six webui `service.test.mjs` tests that compare the default render byte for byte |
|
||
| M8 | `stack-release.sh` | capture copies `$BIN/mosaic` | T55 | 3 | both T-I6 |
|
||
| M9 | `mosaic-config.mjs` | `if (false) {` for the mismatch | SI | 3 | T-I2 row 4 and the entry-point subtest |
|
||
| M10 | `mosaic-config.mjs` | the name pattern allows `-` | SI | 4 | T-I1 bad names, T-I2 entry point |
|
||
| M11 | `mosaic-config.mjs` | `"w"` instead of `"wx"` | SI | 2 | T-I1 dangling symlink |
|
||
| M12 | `guard.mjs` | `for (const name of [])` | SI | 1 | T-I10 |
|
||
| M13 | `mosaic-config.mjs` | the no-flag branch's `checkClaim` dropped | SI, T55 | 3 + 2 | T-I7's two D1 row 2 subtests and the default-on-alpha's-marker one; T-I7 in T55 |
|
||
| M14 | `stack-release.sh` | the capture loop adds `mosaic-bus-.service.d` | T55 | 3 | both T-I6 |
|
||
| M15 | `guard.mjs` | the unconditional `~/.mosaic-NAME` push dropped | SI | 1 | T-I10 (`.mosaic-alpha/x` under a corrupt config) |
|
||
|
||
The fail counts are the runner's `ℹ fail` lines and include each failing test's parent. Under M4, each T-I6
|
||
subtest runs about 122 s before it fails, because without `MOSAIC_CONFIG` the stand-in bus waits for a host
|
||
that never comes up. FIX stops at the webui file under M7, so the discord file didn't run there.
|
||
|
||
## Candidate
|
||
|
||
- `build.patch`: sha256 `6389af4fe8fb7a49ac561adb586474e8328cc401d95ae985c385ca73484bd5e4`, 2611 lines. It is
|
||
`git diff da230ca48` over the 24 paths, with the two new files added as intent-to-add for the diff only.
|
||
- `candidate-manifest.sha256`: sha256 `068828bcb5c75033aa441037bf3e9b84de0ac0f8a624aa0d0bc5a32fb9198901`,
|
||
24 lines.
|
||
- The patch applies on da230ca48 in a fresh detached worktree, and `sha256sum -c` of the manifest passes
|
||
there. I built both after the mutant run had ended and its restores had matched. A first build during the
|
||
run held M4's `bus-service.sh`; I deleted it unread.
|
||
- The review candidate is commit `8862feccc9c6c96088ce934d984e00731da0b425` on local branch
|
||
`darkwing/row190`, parent da230ca48. It is not pushed. The queue refused the manifest as a candidate,
|
||
because `PATH_RE` (`packages/queue/src/queue.mjs:37`) has no `@` and
|
||
`packages/discord/systemd/[email protected]` is one of the 24 paths. Each path's blob in the
|
||
commit hashes to its manifest line, with zero diff lines.
|
||
|
||
## For Sage's gate
|
||
|
||
- Run the cli suite and every `scripts/test-*.sh` from a git worktree of 8862feccc, with TMPDIR at 30
|
||
bytes or less.
|
||
- No test pins a changed file by sha256 (grep of `packages/*/tests` and `scripts/test-*.sh` for each changed
|
||
path and a 64-hex string found none).
|
||
|
||
## Follow-ups
|
||
|
||
- The Console and Discord uninstall and status scans read only their own instance's drop-ins; a scan of the
|
||
shared prefix directories is out of this row.
|
||
- The webui template comment (deviation 8).
|
||
|
||
## Round 2
|
||
|
||
Filbert's round 1 (comment 29634): B1 blocking, N1-N7 not blocking. Sage's sage-1474 ruled N3 and left the
|
||
rest to me.
|
||
|
||
### B1: rebase
|
||
|
||
Row 189 (ace27dc5e) landed after da230ca48 on `scripts/stack-release.sh`, `stack-release.test.mjs` and
|
||
`docs/TOOLS.md`. I rebased onto refactor 088087ac9 on a new local branch, `darkwing/row190-r2`, because
|
||
#426(a) refuses a force-push to `darkwing/row190`. The rebased commit is ceaf93837.
|
||
|
||
- `scripts/stack-release.sh`: one conflict in `plan()`. Row 189 added `echo "$SEND_PLAN"` before the Console
|
||
line, and row 190 added `$flags` to the Console and CLI lines. The resolution keeps both. `$flags` is set
|
||
four lines above, from `INSTANCE_FLAGS`.
|
||
- `stack-release.test.mjs` merged with no conflict, but both rows had named a test T55. Row 190's is now
|
||
T56 (`T56 row 190: named instances side by side ...`). The comment in `stack-instances.test.mjs` and the
|
||
mutant script's killer pattern now say T56. Round 1's tables above keep the name T55, the name it had
|
||
when they ran.
|
||
- `docs/TOOLS.md` merged with no conflict.
|
||
|
||
After the rebase, T55 (row 189, 7 subtests) and T56 (row 190, 6 subtests) passed together, 15 tests and
|
||
0 failures, before any round 2 edit.
|
||
|
||
### Notes
|
||
|
||
- N1, TOOLS.md. The bus-service block and the discord-service line now show `[--instance NAME]`.
|
||
`webui-service.sh --instance NAME copy` used to drop the flag without a word, while `stack-copy.sh copy
|
||
--instance` exits 4. Now both refuse it with exit 4, because every instance shares the copies. The
|
||
webui usage puts `[--instance NAME]` on the verbs that take it, and TOOLS.md says both copy verbs refuse
|
||
it. New T-I2 subtest in `stack-instances.test.mjs`: "copy takes no --instance in either script". Mutant
|
||
M17 below.
|
||
- N2, T-I8. Deviation 10 below.
|
||
- N3, Sage's ruling (sage-1474): the named-only sed rewrites the Console comment "The unit sets no
|
||
MOSAIC_*, CLAUDE*, CODEX* or PI_* variable." to "The unit sets MOSAIC_CONFIG and no other MOSAIC_*,
|
||
CLAUDE*, CODEX* or PI_* variable." The default keeps the line byte for byte. T56's T-I3 pins both. It
|
||
checks the named Console render for the new line and for no "sets no MOSAIC_", and the default render
|
||
for the template's line and for no `MOSAIC_CONFIG`. Mutants M16 and M19.
|
||
- N4, `instance_port_holder` reads only the unit file's `ExecStart=`, so a drop-in that overrides
|
||
`ExecStart` is missed. Moot on this host (no Console drop-in sets one), so it's a follow-up.
|
||
- N5, TOOLS.md said the config directory is 0700. Bootstrap creates either directory 0700, and one that
|
||
already exists keeps its mode. TOOLS.md now says that. The code is unchanged: changing the mode of a
|
||
directory the user already made isn't bootstrap's job.
|
||
- N6, `stack-copy.sh check --instance NAME` now also needs the wrapper's line 4 to be
|
||
`export MOSAIC_CONFIG='<NAME's config>'`, as `wrapper_instance` writes it. If it isn't, the CLI line reads
|
||
`<copy> (MOSAIC_CONFIG mismatch)`, the same form as `unit_root`'s `(ExecStart mismatch)`, and check exits
|
||
1. The new test in `packages/cli/tests/stack-copy.test.mjs`, "row 190 N6", covers a good wrapper, one with
|
||
another instance's config and one with no config line. With the check line disabled, the test failed
|
||
(exit 1); restored, it passed. Mutant M18.
|
||
- N7, `discord-service.sh status` for the default reads `~/.config/mosaic-dev/config.json` and not
|
||
`MOSAIC_CONFIG`. That's base behavior (R3 table, `discord-service.sh:162`), and D1 row 2 says
|
||
`MOSAIC_CONFIG` alone selects that config. Follow-up for Sage, not changed here, so the default's status
|
||
stays as it is.
|
||
|
||
### Deviations, round 2
|
||
|
||
10. T-I8 is a one-shot, not a committed test (Filbert N2). It ran once against da230ca48 (`ti8/`), and no
|
||
committed test compares the full default bus render with the base's. Committed tests pin the default
|
||
Console and Discord renders byte for byte against their templates (`service.test.mjs`,
|
||
`copy-service.test.mjs`), plus the default bus render's lines that `host.test.mjs` checks. I didn't
|
||
rerun T-I8 on 088087ac9. Row 189 changed none of the 11 files it compares except `stack-release.sh`,
|
||
and that change is the `send:` line, which shows on both sides.
|
||
11. `packages/cli/tests/stack-copy.test.mjs` joins the manifest for N6, so there are 25 paths.
|
||
12. Round 2 adds four mutants, M16-M19, on top of the design's M1-M15. Each one guards one of the round 2
|
||
changes.
|
||
|
||
### Gate and mutants, round 2
|
||
|
||
Sage's slot sage-grant-190-r2 (sage-1478). `~/darkwing-scratch/r190/run-all.sh` waited for busy.sh to be
|
||
clear on two checks 20 s apart, then ran the gate and the mutants on 537a02392 in the r190 worktree.
|
||
|
||
Gate, 2026-10-20T20:56:42Z-21:07:41Z. 17 steps, each rc=0. Counts are awk sums over the gate logs:
|
||
|
||
| Step | Pass | Fail |
|
||
|---|---|---|
|
||
| `packages/cli` `node --test tests/*.test.mjs` | 511 | 0 |
|
||
| `packages/conversation` | 182 | 0 |
|
||
| `packages/control-board` | 124 | 0 |
|
||
| `packages/discord` | 183 | 0 |
|
||
| `packages/plugins` `harness.test.mjs` | 9 | 0 |
|
||
| `packages/webui` `service.test.mjs` | 31 | 0 |
|
||
| `packages/webui` `pinned-copy.test.mjs` | 10 | 0 |
|
||
| `scripts/test-auth.sh` | 15 | 0 |
|
||
| `scripts/test-conductor.sh` | 17 | 0 |
|
||
| `scripts/test-config.sh` | 24 | 0 |
|
||
| `scripts/test-discord.sh` | 78 | 0 |
|
||
| `scripts/test-extension-package.sh` | 18 | 0 |
|
||
| `scripts/test-foundation.sh` | 44 | 0 |
|
||
| `scripts/test-land-work.sh` | 86 | 0 |
|
||
| `scripts/test-queue.sh` | 156 node tests, 28 suite checks | 0 |
|
||
| `scripts/test-release.sh` | 14 | 0 |
|
||
| `scripts/test-task.sh` | 98 | 0 |
|
||
|
||
The node tests total 1206 and the selftests 422. pkg-cli has 10 more than round 1: row 189's T55 (7) and
|
||
round 2's N1 and N6 subtests. test-task's compose ran as project `darkwing-r190`, and the EXIT trap removed
|
||
its network.
|
||
|
||
Mutants, 2026-10-20T21:07:42Z-21:18:02Z, with the same steps as round 1. Four controls with no change,
|
||
each exit 0: SI, T56 (`--test-name-pattern='^T56 '` on `stack-release.test.mjs`), SC (`--test-name-pattern='^row
|
||
190 N6'` on `stack-copy.test.mjs`) and FIX. Every restore matched. 19 run, 19 killed, 0 survived:
|
||
|
||
| # | Killers run | Fail | Failing tests |
|
||
|---|---|---|---|
|
||
| M1 | T56 | 7 | T-I3, T-I4, both T-I6, T-I7, T-I9 |
|
||
| M2 | T56 | 2 | T-I4 |
|
||
| M3 | SI, T56 | 9 | T-I1 and T-I7 subtests in SI, T-I7 in T56 |
|
||
| M4 | T56 | 5 | T-I3, both T-I6, T-I9 |
|
||
| M5 | T56 | 2 | T-I3 |
|
||
| M6 | T56 | 6 | T-I3, T-I4, both T-I6, T-I9 |
|
||
| M7 | FIX | 6 | the webui `service.test.mjs` tests that compare the default render byte for byte |
|
||
| M8 | T56 | 3 | both T-I6 |
|
||
| M9 | SI | 3 | T-I2 row 4 and the entry-point subtest |
|
||
| M10 | SI | 4 | T-I1 bad names, T-I2 entry point |
|
||
| M11 | SI | 2 | T-I1 dangling symlink |
|
||
| M12 | SI | 1 | T-I10 |
|
||
| M13 | SI, T56 | 5 | T-I7's D1 row 2 subtests, T-I7 in T56 |
|
||
| M14 | T56 | 3 | both T-I6 |
|
||
| M15 | SI | 1 | T-I10 |
|
||
| M16 | T56 | 2 | T-I3 (the named Console comment kept) |
|
||
| M17 | SI | 2 | T-I2 "copy takes no --instance in either script" |
|
||
| M18 | SC | 1 | row 190 N6 |
|
||
| M19 | T56 | 2 | T-I3 (the default Console comment rewritten) |
|
||
|
||
M16 drops the named sed line for the comment, M17 makes `webui-service.sh copy` accept `--instance`, M18
|
||
skips the wrapper config check in `stack-copy.sh check`, and M19 adds the comment rewrite to the default's
|
||
sed. Fail counts are the runner's `ℹ fail` lines, parents included. The M3 count is 9 here against 7 + 2 in
|
||
round 1 because this table sums both killers.
|
||
|
||
### Candidate, round 2
|
||
|
||
- The review candidate is commit `537a02392bf5eeecf3f55dbfec5162565b786456` on `darkwing/row190-r2` (pushed),
|
||
parent ceaf93837, which is round 1's change rebased onto refactor 088087ac9. It's a commit, not the
|
||
manifest, because `PATH_RE` still refuses the `@` path.
|
||
- `build-r2.patch`: sha256 `0ba1aa3ec6b466055754893c33c8934eb1323627cb4ef32b163d18b489421f8b`, 2707 lines,
|
||
`git diff 088087ac9 537a02392` over the 25 paths.
|
||
- `candidate-manifest-r2.sha256`: sha256 `2296e9cb130eafdd79515aec364393e1493dad81e10a2e57e4ac4ba96f024e0f`,
|
||
25 lines.
|
||
- The patch applies with `git apply --index` on 088087ac9 in a fresh detached worktree, `sha256sum -c` of the
|
||
manifest passes there, and each path's blob there equals its blob in 537a02392. I built both after the
|
||
mutant run ended and its restores matched.
|
||
- Round 1's `build.patch` and `candidate-manifest.sha256` stay as round 1's record.
|
||
- No test pins a changed file by sha256. The only test with a 64-hex string that names a changed file is
|
||
`packages/webui/tests/service.test.mjs`, and its string is a fake UID (`:21`).
|
||
|
||
For Sage's gate: run the cli suite and every `scripts/test-*.sh` from a git worktree of 537a02392, with TMPDIR
|
||
at 30 bytes or less.
|
||
|
||
### Follow-ups, round 2
|
||
|
||
- N4: `instance_port_holder` and a drop-in `ExecStart` override.
|
||
- N7: the default `discord-service.sh status` and `MOSAIC_CONFIG` (D1 row 2).
|
||
- `PATH_RE` and `@` paths (reported to Sage in round 1).
|
||
- Round 1's first follow-up (drop-in scans) stands. The webui template comment is settled by N3.
|
||
|
||
## Round 3
|
||
|
||
Filbert's round 2 (comment 29685) had one blocking item, B1. Row 192 (4429a104f, Q175 phase 1 SSO) landed at
|
||
2026-10-20T21:21:29Z, after round 2's candidate and gate. Row 192 touches four of the 25 paths:
|
||
`scripts/mosaic-config.mjs`, `scripts/webui-service.sh`, `packages/webui/tests/service.test.mjs` and
|
||
`packages/webui/README.md`. Round 2's own changes needed nothing more.
|
||
|
||
### B1: rebase
|
||
|
||
I rebased onto refactor 176a6bf4b on a new local branch, `darkwing/row190-r3`, because #426(a) refuses a
|
||
force-push. The commits:
|
||
|
||
- ab1d2ee9e: round 1's change, rebased.
|
||
- cdac7ba92: round 2's change, rebased.
|
||
- df65a3d4c: round 3's only new code, the T-I3 assertion below.
|
||
|
||
I didn't run `npm ci` in the worktree. Its `node_modules` is a symlink to the canonical checkout's, which
|
||
already has row 192's oauth4webapi 3.8.8. The webui suite's SSO tests passed on it in the gate.
|
||
|
||
Filbert's merge hazards, each checked on df65a3d4c:
|
||
|
||
- Row 192 makes `validate` and `load` async. Every call to `load` in `mosaic-config.mjs` is awaited: in
|
||
`bootstrapDefault` (:350, :356, :359), `bootstrapNamed` (:368, :394, :398), `selected` (:415, :420) and
|
||
`loadSelected` (:442). Those four functions are async now, and the command switch awaits each one (:457-483).
|
||
`checkClaim` stays sync, and every caller passes it a `dataRoot` from an awaited `load`, so it never gets a
|
||
Promise.
|
||
- The top-level `try { switch ... } catch` (:487-490) awaits everything inside it, so a Failure thrown from row
|
||
192's `validateConsole` still exits with its own code. `scripts/test-config.sh` proves it for the default:
|
||
five bad `console` bodies through `validate` each exit 2 and name the key path. That suite has 31 checks now,
|
||
up from 24 in round 2, and the 7 new ones are row 192's. No test sends a bad `console` through a named
|
||
instance. That path is the same `loadSelected` to `load` to `validate` chain with `instance` set.
|
||
- `webui-service.sh` keeps row 192's `--sso` comment (:52), flag (:143), refusals (:221-227) and unit flag
|
||
(:260), plus row 190's instance block (:86-91). USAGE has one FLAGS line:
|
||
`[--sso [--send] | --public-host NAME --auth-header HEADER --allow-user ID [--send]]`. In
|
||
`service.test.mjs`, row 192's tests merged with round 1's `@CONFIG_ENV@` line removal in the expected
|
||
render, with no conflict.
|
||
- Filbert note 4. A named unit's `Environment=MOSAIC_CONFIG=` gives each instance its own `console.sso` (read
|
||
through `loadSystem`) and its own slots (`business.mjs` `configDir()`). T56's T-I3 now renders alpha's
|
||
Console with `--sso` and checks that the unit carries both `--sso` on `ExecStart` and alpha's
|
||
`MOSAIC_CONFIG` line. That's 6 lines in `stack-release.test.mjs`. Mutant M20 below drops `--sso` from
|
||
the named render.
|
||
|
||
### Deviations, round 3
|
||
|
||
13. Filbert asked for the 19 mutants on the new base. I ran 20: M1-M19 unchanged, plus M20 for note 4's
|
||
assertion.
|
||
|
||
### Gate and mutants, round 3
|
||
|
||
Sage's slot (sage-1512). `~/darkwing-scratch/r190/run-all.sh` waited for busy.sh to be clear on two checks 20 s
|
||
apart, then ran the gate and the mutants on df65a3d4c in the r190 worktree.
|
||
|
||
Gate, 2026-10-20T21:47:27Z-21:59:13Z. 17 steps, each rc=0. Counts are awk sums over `gate-r3.txt`:
|
||
|
||
| Step | Pass | Fail |
|
||
|---|---|---|
|
||
| `packages/cli` `node --test tests/*.test.mjs` | 511 | 0 |
|
||
| `packages/conversation` | 182 | 0 |
|
||
| `packages/control-board` | 124 | 0 |
|
||
| `packages/discord` | 183 | 0 |
|
||
| `packages/business` | 149 | 0 |
|
||
| `packages/webui` (whole suite) | 299, 1 skipped | 0 |
|
||
| `packages/plugins` `harness.test.mjs` | 9 | 0 |
|
||
| `scripts/test-auth.sh` | 15 | 0 |
|
||
| `scripts/test-conductor.sh` | 17 | 0 |
|
||
| `scripts/test-config.sh` | 31 | 0 |
|
||
| `scripts/test-discord.sh` | 78 | 0 |
|
||
| `scripts/test-extension-package.sh` | 18 | 0 |
|
||
| `scripts/test-foundation.sh` | 44 | 0 |
|
||
| `scripts/test-land-work.sh` | 86 | 0 |
|
||
| `scripts/test-queue.sh` | 156 node tests, 28 suite checks | 0 |
|
||
| `scripts/test-release.sh` | 14 | 0 |
|
||
| `scripts/test-task.sh` | 98 | 0 |
|
||
|
||
That's 2042 passed: 1613 node tests and 429 selftests. Round 2 had 1206 and 422. This gate adds
|
||
`packages/business` (149) and runs the whole webui suite (299) instead of its two service files (41). Those two
|
||
changes account for the 407 extra node tests. The 7 extra selftests are row 192's `test-config.sh` checks.
|
||
pkg-cli is still 511, and that includes T56's new T-I3 assertion, which sits inside an existing subtest.
|
||
test-task's compose ran as project `darkwing-r190`, and the EXIT trap removed its network.
|
||
|
||
Row 192's re-pin activate (W5) ran at 22:02:09Z, after the gate and during the mutants. Nothing in the gate or
|
||
the mutants uses the live units. M4's two T-I6 cases ran across 22:02. Each one timed out at about 122 s,
|
||
against 4 s in the control. Rounds 1 and 2 show the same 122 s timeouts under M4 (`mut-r1/`, `mut-r2/`), so the
|
||
mutant causes them, not the activate.
|
||
|
||
Mutants, 2026-10-20T21:59:14Z-22:09:37Z, with the same steps as round 2. The four controls (SI, T56, SC, FIX)
|
||
each exited 0. Every restore matched, and the worktree ended at df65a3d4c with no changes. 20 run, 20 killed,
|
||
0 survived:
|
||
|
||
| # | Killers run | Fail | Failing tests |
|
||
|---|---|---|---|
|
||
| M1 | T56 | 7 | T-I3, T-I4, both T-I6, T-I7, T-I9 |
|
||
| M2 | T56 | 2 | T-I4 |
|
||
| M3 | SI, T56 | 9 | T-I1 and T-I7 subtests in SI, T-I7 in T56 |
|
||
| M4 | T56 | 5 | T-I3, both T-I6, T-I9 |
|
||
| M5 | T56 | 2 | T-I3 |
|
||
| M6 | T56 | 6 | T-I3, T-I4, both T-I6, T-I9 |
|
||
| M7 | FIX | 6 | the webui `service.test.mjs` tests that compare the default render byte for byte |
|
||
| M8 | T56 | 3 | both T-I6 |
|
||
| M9 | SI | 3 | T-I2 row 4 and the entry-point subtest |
|
||
| M10 | SI | 4 | T-I1 bad names, T-I2 entry point |
|
||
| M11 | SI | 2 | T-I1 dangling symlink |
|
||
| M12 | SI | 1 | T-I10 |
|
||
| M13 | SI, T56 | 5 | T-I7's D1 row 2 subtests, T-I7 in T56 |
|
||
| M14 | T56 | 3 | both T-I6 |
|
||
| M15 | SI | 1 | T-I10 |
|
||
| M16 | T56 | 2 | T-I3 (the named Console comment kept) |
|
||
| M17 | SI | 2 | T-I2 "copy takes no --instance in either script" |
|
||
| M18 | SC | 1 | row 190 N6 |
|
||
| M19 | T56 | 2 | T-I3 (the default Console comment rewritten) |
|
||
| M20 | T56 | 2 | T-I3 (the named `--sso` render) |
|
||
|
||
M1-M19 fail the same tests, with the same counts, as round 2. M20 replaces `[ "$SSO" = 0 ] || flags="$flags
|
||
--sso"` in `webui-service.sh` with `:`, so a named `--sso` render loses `--sso`. Fail counts are the runner's
|
||
`ℹ fail` lines, parents included.
|
||
|
||
### Candidate, round 3
|
||
|
||
- The review candidate is commit `df65a3d4c8fec64ba3aba20a481c244f9d73a84d` on `darkwing/row190-r3` (pushed),
|
||
base refactor 176a6bf4b. It's a commit, not the manifest, because `PATH_RE` still refuses the `@` path.
|
||
- `build-r3.patch`: sha256 `efe898c2ce2de5c98cf0354746aa3b7ba8c61467499ccf67d62a9fa33a59ad04`, 2713 lines,
|
||
`git diff 176a6bf4b df65a3d4c` over the 25 paths, the same path set as round 2.
|
||
- `candidate-manifest-r3.sha256`: sha256 `cc0ea9e8f352a4805f8cda6dae8c2401880bed32fc10f25fee4ae7091c7970af`,
|
||
25 lines.
|
||
- I built both from git objects, not from the worktree the mutants edit. The patch applies with `git apply
|
||
--index` on 176a6bf4b in a fresh detached worktree, `sha256sum -c` of the manifest passes there, and that
|
||
tree equals df65a3d4c's.
|
||
- No test pins a changed file by sha256. Two test files with a 64-hex string name a changed path. In
|
||
`packages/webui/tests/service.test.mjs` the string is the fake UID (`:21`), and in
|
||
`packages/queue/tests/fixtures/queue-marked.md` it's an old manifest digest in history text (`:119`).
|
||
|
||
For Sage's gate: run the cli suite and every `scripts/test-*.sh` from a git worktree of df65a3d4c, with
|
||
`node_modules` linked and TMPDIR at 30 bytes or less.
|
||
|
||
### Follow-ups, round 3
|
||
|
||
Unchanged from round 2: N4 (`instance_port_holder` and a drop-in `ExecStart`), N7 (the default
|
||
`discord-service.sh status` and `MOSAIC_CONFIG`), and `PATH_RE` with `@` paths. Row 230 (C2-13a) leaves out row
|
||
190's `@CONFIG_ENV@` and `@SESSION_ENV@` tokens (gosalyn-0058 item 4). Whichever of the two rows lands second
|
||
adds them.
|
||
|
||
Filbert approved round 3 (Gitea 29713, queue rev 2504, 2026-10-20T22:15:59Z). One more follow-up came from that
|
||
review, not blocking: no test sends a bad `console` body through a named instance's `validate`.
|