review(rocko): row 75 round 1 packet, the Console as a user service (#1569)
Committed by sage for rocko (no credential). Candidate manifest 7dc571db, packet manifest 0bc9a870, base 43d2328f.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
# Row 75 (#1569): Console as a user service, candidate packet, round 1
|
||||
|
||||
Author: Rocko. Reviewer: Filbert. Brief: `docs/plans/2026-10-11_dogfood-cutover.md`,
|
||||
section "Console as a user service" (blob 1b124173 at 57f36a96; the rev 501
|
||||
re-pin to blob f5d56554 changes only row 76's section). Lead decisions 94 and
|
||||
95. Base: `43d2328f` (`base.txt`). At 2026-10-11T22:32Z the checkout HEAD
|
||||
`2d04426c` has no change to any candidate path since the base, and
|
||||
`build.patch` applies to it cleanly. The packet is uncommitted. I made no
|
||||
commits, pushes or Gitea calls, read no token or private binding, and
|
||||
started no unit.
|
||||
|
||||
## Files (`files.txt`, `candidate-manifest.sha256`)
|
||||
|
||||
| File | What |
|
||||
|---|---|
|
||||
| `scripts/webui-service.sh` (new, 0755) | `render`, `install [--dir DIR] [--no-reload]`, `uninstall [--dir DIR] [--no-reload]`, `status [--dir DIR]` on the `bus-service.sh` pattern; exit 0 ok, 1 failed, 4 usage |
|
||||
| `packages/webui/systemd/mosaic-webui.service.in` (new) | the unit template |
|
||||
| `packages/webui/tests/service.test.mjs` (new) | 10 tests |
|
||||
| `docs/TOOLS.md` | one new section, "Console service (`scripts/webui-service.sh`)", before the Discord connector section; nothing else touched |
|
||||
|
||||
No `src/` file and no README. Row 74 owns those.
|
||||
|
||||
## What it does
|
||||
|
||||
- The serve flags are `--port N` (default 7332, 1..65535), `--business ID`,
|
||||
`--board URL` and the preview set `--public-host NAME --auth-header
|
||||
HEADER --allow-user ID`, all three or none.
|
||||
- Each value has its own plain character set:
|
||||
- business: serve's ID pattern, `[a-z0-9][a-z0-9._-]{0,63}`.
|
||||
- board: `http://` followed by `[A-Za-z0-9.:/_-]`. Serve checks the rest itself: a loopback origin with no path.
|
||||
- public host: `[A-Za-z0-9.-]` with a leading alphanumeric, so no port and no scheme.
|
||||
- header: `[A-Za-z0-9-]`.
|
||||
- user: `[A-Za-z0-9._@+-]`.
|
||||
- None of the sets allows a space, newline, `%`, `$`, quote, backslash, `&` or `|`. That means neither systemd nor the `sed` render can read a value as anything other than the value.
|
||||
- Every refusal exits 1 and writes nothing. A refused `install` doesn't create the unit directory.
|
||||
- `ExecStart=@NODE@ @REPO@/packages/webui/src/cli.mjs serve @FLAGS@`. NODE is
|
||||
`command -v node` at render time, kept as given (here `/usr/bin/node`).
|
||||
systemd doesn't look up an ExecStart command in `Environment=PATH`, so a bare
|
||||
`node` would not be found the way the brief's wording suggests. The REPO
|
||||
and node paths are held to `[A-Za-z0-9/._+-]`.
|
||||
- `WorkingDirectory` and `PATH` are filled in as the bus unit fills them:
|
||||
`/usr/local/bin:/usr/bin:/bin`, with node's directory prepended if it isn't
|
||||
already there. The other settings are `Restart=on-failure`,
|
||||
`RestartPreventExitStatus=2`, `KillSignal=SIGTERM`, `TimeoutStopSec=30`,
|
||||
`NoNewPrivileges=yes` and `WantedBy=default.target`. There is no other
|
||||
`Environment=` line and no `MOSAIC_*`, `CLAUDE*`, `CODEX*` or `PI_*` variable.
|
||||
- `install` prints the `systemctl --user enable --now mosaic-webui` line for
|
||||
the human to run. The script has no start, stop, enable or restart verb; the
|
||||
header and TOOLS.md say why (lead decisions 62 and 94).
|
||||
- `uninstall` refuses while `list-units` shows the unit active, activating or
|
||||
deactivating, unless `--no-reload` is given, as in bus-service.
|
||||
- `status` prints is-enabled, is-active and `show`, then reads `--port` from the
|
||||
installed unit's ExecStart and prints `GET http://127.0.0.1:<port>/healthz`:
|
||||
the status code and body, or `unreachable (<code>)`. It exits 0 either way,
|
||||
as bus-service status does.
|
||||
|
||||
## Points for the reviewer
|
||||
|
||||
1. **Exit 2 isn't retried.** `serve` exits 2 for usage, config and listen errors,
|
||||
EADDRINUSE included (`serve.mjs` lines 149–150 reject, `cli.mjs` sets
|
||||
exitCode 2). With `RestartPreventExitStatus=2`, a port conflict stops the
|
||||
unit rather than looping. I chose that on purpose, and the template comment
|
||||
says so. Serve has no separate config exit code: a missing system config
|
||||
still serves, and the bus routes answer not-configured.
|
||||
2. **The preview flags need row 74.** Until row 74 lands, a unit rendered with
|
||||
the preview set makes `serve` refuse with exit 2 (`unknown option`) and not
|
||||
restart. Rendering and installing are unaffected.
|
||||
3. **Serve runs under systemd.** It runs as written. The user manager's
|
||||
environment (`systemctl --user show-environment`) carries none of the
|
||||
human.mjs markers, and the ancestry walk accepts at ppid 1. That is the gap
|
||||
lead decisions 62 and 94 name, and it's why only Jason starts the unit. I did
|
||||
not start it to prove this; `systemd-analyze --user verify` on the rendered
|
||||
unit exits 0.
|
||||
4. **An empty value counts as unset.** `--board ""` and `--business ""` are
|
||||
treated as unset, the way bus-service treats an empty value. An empty member
|
||||
of the preview set counts as missing, so a set with one empty member is
|
||||
refused as partial.
|
||||
|
||||
## Tests (`packages/webui/tests/service.test.mjs`)
|
||||
|
||||
The brief asks for six things:
|
||||
|
||||
- render with and without the preview flags;
|
||||
- a partial set refused, in all three two-of-three shapes;
|
||||
- space, `%`, newline, `$`, quote, backslash, `|` and `&` refused for each value flag, plus bad ports, business ids, boards, hosts and headers;
|
||||
- the rendered unit names none of the `MARKERS` that the test parses from `packages/bus/src/human.mjs`, and its only Environment key is PATH;
|
||||
- install into `--dir` with `--no-reload` writes the file whole and identical to `render`, at mode 0644, with no temp file left and no systemctl call; a second install reports unchanged; a refused install leaves the installed unit alone;
|
||||
- uninstall refuses for active, activating and deactivating through a stand-in `systemctl` on PATH, then removes the unit once it is inactive.
|
||||
|
||||
I added three tests beyond the brief:
|
||||
|
||||
- install without `--no-reload` calls only `daemon-reload`;
|
||||
- status against an in-process `/healthz` server on port 0, then with the server down, then with no unit installed;
|
||||
- usage exits 4, including the `start`, `stop`, `enable` and `restart` verbs.
|
||||
|
||||
No test reaches the real systemd or spawns `human-cli.mjs`. `XDG_CONFIG_HOME` and `HOME` point into a temp directory.
|
||||
|
||||
## Gate (`out/gate/`, sequential, scratch worktree at 43d2328f with the patch, `node_modules` linked)
|
||||
|
||||
| Suite | Result |
|
||||
|---|---|
|
||||
| webui `node --test 'packages/webui/tests/*.test.mjs'` | 50/50 (service: 10/10) |
|
||||
| test-auth | 15 passed, 0 failed |
|
||||
| test-conductor | 17 passed, 0 failed |
|
||||
| test-config | 24 passed, 0 failed |
|
||||
| test-discord | 66 passed, 0 failed (node 178 pass) |
|
||||
| test-extension-package | 18 passed, 0 failed |
|
||||
| test-foundation | 44 passed, 0 failed |
|
||||
| test-queue | 27 passed, 0 failed |
|
||||
| test-release | 14 passed, 0 failed |
|
||||
| test-task | 98 passed, 0 failed (Docker) |
|
||||
|
||||
After the gate I removed the `rocko-r75_default` Docker network.
|
||||
`out/gate/start.txt` and `out/gate/end.txt` hold the times. shellcheck isn't
|
||||
installed on dragon-lin; `bash -n` passes.
|
||||
@@ -0,0 +1 @@
|
||||
43d2328f3a413b066f435c13cacd4df462670c57
|
||||
@@ -0,0 +1,523 @@
|
||||
diff --git a/docs/TOOLS.md b/docs/TOOLS.md
|
||||
index 9a28a090..631e1814 100644
|
||||
--- a/docs/TOOLS.md
|
||||
+++ b/docs/TOOLS.md
|
||||
@@ -238,6 +238,37 @@ stdin and stdout it exits 4 with the usage.
|
||||
Exit codes: `0` ok · `1` failed or outcome unknown · `2` invalid input ·
|
||||
`3` refused or config problem · `4` usage. Details: `packages/cli/README.md`.
|
||||
|
||||
+## Console service (`scripts/webui-service.sh`)
|
||||
+
|
||||
+```bash
|
||||
+scripts/webui-service.sh render [FLAGS] # print the unit
|
||||
+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]
|
||||
+```
|
||||
+
|
||||
+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
|
||||
+`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
|
||||
+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
|
||||
+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`.
|
||||
+Exit codes: `0` ok · `1` failed or refused · `4` usage.
|
||||
+
|
||||
## Discord connector (`scripts/discord.sh`)
|
||||
|
||||
```bash
|
||||
diff --git a/packages/webui/systemd/mosaic-webui.service.in b/packages/webui/systemd/mosaic-webui.service.in
|
||||
new file mode 100644
|
||||
index 00000000..081bd963
|
||||
--- /dev/null
|
||||
+++ b/packages/webui/systemd/mosaic-webui.service.in
|
||||
@@ -0,0 +1,35 @@
|
||||
+# Mosaic Console: `node packages/webui/src/cli.mjs serve`, one instance.
|
||||
+# Rendered by scripts/webui-service.sh from packages/webui/systemd/; @REPO@,
|
||||
+# @PATH@, @NODE@ and the serve flags below are filled in at install time.
|
||||
+# Edit the template and reinstall; do not edit the installed copy.
|
||||
+#
|
||||
+# The Console reads the bus through the human transport, so whoever starts
|
||||
+# this unit holds the human's capability. Jason starts it from his own
|
||||
+# shell (`systemctl --user enable --now mosaic-webui`); an agent starting
|
||||
+# it works around the human proof (lead decisions 62 and 94). The unit sets
|
||||
+# no MOSAIC_*, CLAUDE*, CODEX* or PI_* variable.
|
||||
+[Unit]
|
||||
+Description=Mosaic Console
|
||||
+Documentation=file://@REPO@/packages/webui/README.md
|
||||
+StartLimitIntervalSec=600
|
||||
+StartLimitBurst=5
|
||||
+
|
||||
+[Service]
|
||||
+Type=simple
|
||||
+WorkingDirectory=@REPO@
|
||||
+Environment=PATH=@PATH@
|
||||
+# The server binds 127.0.0.1 only. Usage, config and listen refusals
|
||||
+# (a port already in use included) exit 2 and are not retried; fix the
|
||||
+# flags or free the port, then start it again.
|
||||
+ExecStart=@NODE@ @REPO@/packages/webui/src/cli.mjs serve @FLAGS@
|
||||
+Restart=on-failure
|
||||
+RestartSec=15
|
||||
+RestartPreventExitStatus=2
|
||||
+# `systemctl --user stop` sends SIGTERM; the server stops accepting and
|
||||
+# closes. Open connections that outlast TimeoutStopSec are killed.
|
||||
+KillSignal=SIGTERM
|
||||
+TimeoutStopSec=30
|
||||
+NoNewPrivileges=yes
|
||||
+
|
||||
+[Install]
|
||||
+WantedBy=default.target
|
||||
diff --git a/packages/webui/tests/service.test.mjs b/packages/webui/tests/service.test.mjs
|
||||
new file mode 100644
|
||||
index 00000000..d50b6652
|
||||
--- /dev/null
|
||||
+++ b/packages/webui/tests/service.test.mjs
|
||||
@@ -0,0 +1,220 @@
|
||||
+import { test } from 'node:test';
|
||||
+import assert from 'node:assert/strict';
|
||||
+import { spawn } from 'node:child_process';
|
||||
+import { createServer } from 'node:http';
|
||||
+import { once } from 'node:events';
|
||||
+import { chmodSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
||||
+import { tmpdir } from 'node:os';
|
||||
+import { dirname, join } from 'node:path';
|
||||
+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.
|
||||
+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'];
|
||||
+
|
||||
+function tmp(t) {
|
||||
+ const dir = mkdtempSync(join(tmpdir(), 'webui-service-'));
|
||||
+ t.after(() => rmSync(dir, { recursive: true, force: true }));
|
||||
+ return dir;
|
||||
+}
|
||||
+
|
||||
+// A stand-in systemctl that prints `lines` for list-units and logs every call.
|
||||
+function standIn(dir, lines = '') {
|
||||
+ const bin = join(dir, 'bin');
|
||||
+ mkdirSync(bin);
|
||||
+ writeFileSync(join(dir, 'list-units.txt'), lines);
|
||||
+ writeFileSync(join(bin, 'systemctl'), `#!/bin/sh
|
||||
+echo "$*" >> "${dir}/calls.txt"
|
||||
+case "$*" in
|
||||
+ *list-units*) cat "${dir}/list-units.txt" ;;
|
||||
+ *is-enabled*) echo enabled ;;
|
||||
+ *is-active*) echo active ;;
|
||||
+esac
|
||||
+exit 0
|
||||
+`);
|
||||
+ chmodSync(join(bin, 'systemctl'), 0o755);
|
||||
+ return bin;
|
||||
+}
|
||||
+
|
||||
+// Async, so an in-process server can answer the script's /healthz request.
|
||||
+async function run(dir, args, bin = null) {
|
||||
+ const env = { PATH: `${bin ? `${bin}:` : ''}${NODE_DIR}:/usr/bin:/bin`, HOME: dir, XDG_CONFIG_HOME: join(dir, 'config') };
|
||||
+ const child = spawn(SCRIPT, args, { env });
|
||||
+ let stdout = '', stderr = '';
|
||||
+ child.stdout.on('data', (d) => { stdout += d; });
|
||||
+ child.stderr.on('data', (d) => { stderr += d; });
|
||||
+ const [status] = await once(child, 'close');
|
||||
+ return { status, stdout, stderr };
|
||||
+}
|
||||
+
|
||||
+const execStart = (unit) => unit.split('\n').find((l) => l.startsWith('ExecStart='));
|
||||
+
|
||||
+test('render prints the unit without the preview flags', async (t) => {
|
||||
+ const dir = tmp(t);
|
||||
+ const r = await run(dir, ['render']);
|
||||
+ assert.equal(r.status, 0, r.stderr);
|
||||
+ assert.match(r.stdout, new RegExp(`^WorkingDirectory=${REPO.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`, 'm'));
|
||||
+ assert.match(r.stdout, /^Environment=PATH=\S*\/usr\/local\/bin:\/usr\/bin:\/bin$/m);
|
||||
+ assert.equal(execStart(r.stdout), `ExecStart=${process.execPath} ${REPO}/packages/webui/src/cli.mjs serve --port 7332`);
|
||||
+ for (const line of ['Restart=on-failure', 'RestartPreventExitStatus=2', 'NoNewPrivileges=yes', 'WantedBy=default.target']) {
|
||||
+ assert.ok(r.stdout.split('\n').includes(line), line);
|
||||
+ }
|
||||
+ assert.doesNotMatch(r.stdout, /@[A-Z]+@/, 'every placeholder is filled');
|
||||
+});
|
||||
+
|
||||
+test('render carries every serve flag, the preview set included', async (t) => {
|
||||
+ const dir = tmp(t);
|
||||
+ const r = await run(dir, ['render', '--port', '7340', '--business', 'mosaic.dev_1', '--board', 'http://127.0.0.1:7331', ...PREVIEW]);
|
||||
+ assert.equal(r.status, 0, r.stderr);
|
||||
+ assert.equal(execStart(r.stdout), `ExecStart=${process.execPath} ${REPO}/packages/webui/src/cli.mjs serve --port 7340 --business mosaic.dev_1 --board http://127.0.0.1:7331 --public-host console.example.com --auth-header X-authentik-uid --allow-user jason`);
|
||||
+});
|
||||
+
|
||||
+test('a partial preview set is refused', async (t) => {
|
||||
+ const dir = tmp(t);
|
||||
+ for (const args of [PREVIEW.slice(0, 2), PREVIEW.slice(2, 6), [...PREVIEW.slice(0, 2), ...PREVIEW.slice(4, 6)]]) {
|
||||
+ const r = await run(dir, ['render', ...args]);
|
||||
+ assert.equal(r.status, 1, args.join(' '));
|
||||
+ assert.equal(r.stdout, '');
|
||||
+ assert.match(r.stderr, /all three or none/);
|
||||
+ }
|
||||
+});
|
||||
+
|
||||
+test('a value the unit cannot carry is refused, for every flag', async (t) => {
|
||||
+ const dir = tmp(t);
|
||||
+ 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];
|
||||
+ };
|
||||
+ for (const flag of ['--business', '--board', '--public-host', '--auth-header', '--allow-user']) {
|
||||
+ for (const bad of [' x', '%n', '\n', '$HOME', '"', '\\', '|', '&']) {
|
||||
+ const value = flag === '--board' ? `http://127.0.0.1:7331${bad}` : `a${bad}b`;
|
||||
+ const r = await run(dir, ['render', ...set(flag, value)]);
|
||||
+ assert.equal(r.status, 1, `${flag} ${JSON.stringify(value)}`);
|
||||
+ assert.equal(r.stdout, '', `${flag} ${JSON.stringify(value)}`);
|
||||
+ }
|
||||
+ }
|
||||
+ for (const port of ['0', '65536', '7a', '', '-1', '999999']) {
|
||||
+ const r = await run(dir, ['render', '--port', port]);
|
||||
+ assert.equal(r.status, 1, `port ${JSON.stringify(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')]) {
|
||||
+ const r = await run(dir, ['render', ...args]);
|
||||
+ assert.equal(r.status, 1, args.join(' '));
|
||||
+ }
|
||||
+});
|
||||
+
|
||||
+test('the rendered unit names none of the human proof agent markers and sets no agent variable', async (t) => {
|
||||
+ const dir = tmp(t);
|
||||
+ 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']]) {
|
||||
+ 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);
|
||||
+ const envLines = r.stdout.split('\n').filter((l) => /^\s*(Environment|EnvironmentFile|PassEnvironment|UnsetEnvironment)\s*=/.test(l));
|
||||
+ assert.deepEqual(envLines.map((l) => l.split('=')[1]), ['PATH']);
|
||||
+ assert.doesNotMatch(r.stdout, /^[^#]*\b(MOSAIC_|CLAUDE|CODEX|PI_)/m);
|
||||
+ }
|
||||
+});
|
||||
+
|
||||
+test('install into --dir with --no-reload writes the file whole, then reports unchanged', async (t) => {
|
||||
+ const dir = tmp(t);
|
||||
+ const unitDir = join(dir, 'units');
|
||||
+ const bin = standIn(dir);
|
||||
+ const rendered = await run(dir, ['render', '--business', 'mosaic']);
|
||||
+ const r = await run(dir, ['install', '--dir', unitDir, '--no-reload', '--business', 'mosaic'], bin);
|
||||
+ assert.equal(r.status, 0, r.stderr);
|
||||
+ const target = join(unitDir, 'mosaic-webui.service');
|
||||
+ assert.match(r.stdout, new RegExp(`^written: ${target}$`, 'm'));
|
||||
+ assert.match(r.stdout, /systemctl --user enable --now mosaic-webui/);
|
||||
+ assert.equal(readFileSync(target, 'utf8'), rendered.stdout);
|
||||
+ assert.equal(statSync(target).mode & 0o777, 0o644);
|
||||
+ assert.deepEqual(readdirSync(unitDir), ['mosaic-webui.service'], 'no temp file left behind');
|
||||
+ const again = await run(dir, ['install', '--dir', unitDir, '--no-reload', '--business', 'mosaic'], bin);
|
||||
+ assert.equal(again.status, 0, again.stderr);
|
||||
+ assert.match(again.stdout, new RegExp(`^unchanged: ${target}$`, 'm'));
|
||||
+ assert.ok(!existsSync(join(dir, 'calls.txt')), 'no systemctl call with --no-reload');
|
||||
+ // A refused value writes nothing and leaves the installed unit alone.
|
||||
+ const bad = await run(dir, ['install', '--dir', unitDir, '--no-reload', '--business', 'a b'], bin);
|
||||
+ assert.equal(bad.status, 1);
|
||||
+ assert.equal(readFileSync(target, 'utf8'), rendered.stdout);
|
||||
+ assert.deepEqual(readdirSync(unitDir), ['mosaic-webui.service']);
|
||||
+ const fresh = join(dir, 'never');
|
||||
+ assert.equal((await run(dir, ['install', '--dir', fresh, '--no-reload', '--port', '0'], bin)).status, 1);
|
||||
+ assert.ok(!existsSync(fresh), 'a refusal creates no directory');
|
||||
+});
|
||||
+
|
||||
+test('install without --no-reload runs daemon-reload and nothing that starts the unit', async (t) => {
|
||||
+ const dir = tmp(t);
|
||||
+ const bin = standIn(dir);
|
||||
+ const r = await run(dir, ['install', '--dir', join(dir, 'units')], bin);
|
||||
+ assert.equal(r.status, 0, r.stderr);
|
||||
+ assert.match(r.stdout, /^daemon-reload done$/m);
|
||||
+ assert.equal(readFileSync(join(dir, 'calls.txt'), 'utf8'), '--user daemon-reload\n');
|
||||
+});
|
||||
+
|
||||
+test('uninstall refuses while the unit is active, and removes it once it is not', async (t) => {
|
||||
+ const dir = tmp(t);
|
||||
+ const unitDir = join(dir, 'units');
|
||||
+ const target = join(unitDir, 'mosaic-webui.service');
|
||||
+ for (const state of ['active', 'activating', 'deactivating']) {
|
||||
+ const bin = standIn(mkdtempSync(join(dir, 'si-')), `mosaic-webui.service loaded ${state} running Mosaic Console\n`);
|
||||
+ assert.equal((await run(dir, ['install', '--dir', unitDir, '--no-reload'])).status, 0);
|
||||
+ const r = await run(dir, ['uninstall', '--dir', unitDir], bin);
|
||||
+ assert.equal(r.status, 1, state);
|
||||
+ assert.match(r.stderr, /refusing: mosaic-webui\.service is still running/);
|
||||
+ assert.ok(existsSync(target), `still installed while ${state}`);
|
||||
+ }
|
||||
+ const bin = standIn(mkdtempSync(join(dir, 'si-')), 'mosaic-webui.service loaded inactive dead Mosaic Console\n');
|
||||
+ const r = await run(dir, ['uninstall', '--dir', unitDir], bin);
|
||||
+ assert.equal(r.status, 0, r.stderr);
|
||||
+ assert.match(r.stdout, new RegExp(`^removed: ${target}$`, 'm'));
|
||||
+ assert.match(r.stdout, /^daemon-reload done$/m);
|
||||
+ assert.ok(!existsSync(target));
|
||||
+ const absent = await run(dir, ['uninstall', '--dir', unitDir, '--no-reload'], bin);
|
||||
+ assert.equal(absent.status, 0);
|
||||
+ assert.match(absent.stdout, /^absent: /m);
|
||||
+});
|
||||
+
|
||||
+test('status prints the unit state and GET /healthz on the installed port', async (t) => {
|
||||
+ const dir = tmp(t);
|
||||
+ const unitDir = join(dir, 'units');
|
||||
+ const bin = standIn(dir);
|
||||
+ const server = createServer((req, res) => {
|
||||
+ if (req.url === '/healthz') { res.writeHead(200, { 'content-type': 'application/json' }); res.end('{"ok":true}\n'); return; }
|
||||
+ res.writeHead(404); res.end();
|
||||
+ });
|
||||
+ server.listen(0, '127.0.0.1');
|
||||
+ await once(server, 'listening');
|
||||
+ t.after(() => server.close());
|
||||
+ const { port } = server.address();
|
||||
+ assert.equal((await run(dir, ['install', '--dir', unitDir, '--no-reload', '--port', String(port)])).status, 0);
|
||||
+ const r = await run(dir, ['status', '--dir', unitDir], bin);
|
||||
+ assert.equal(r.status, 0, r.stderr);
|
||||
+ assert.match(r.stdout, /^ {2}enabled: enabled$/m);
|
||||
+ assert.match(r.stdout, /^ {2}active: {2}active$/m);
|
||||
+ assert.match(r.stdout, new RegExp(`^healthz: http://127\\.0\\.0\\.1:${port}/healthz 200 \\{"ok":true\\}$`, 'm'));
|
||||
+ server.close();
|
||||
+ await once(server, 'close');
|
||||
+ const down = await run(dir, ['status', '--dir', unitDir], bin);
|
||||
+ assert.equal(down.status, 0, down.stderr);
|
||||
+ assert.match(down.stdout, /^healthz: \S+ unreachable \(ECONNREFUSED\)$/m);
|
||||
+ const none = await run(dir, ['status', '--dir', join(dir, 'empty')], bin);
|
||||
+ assert.match(none.stdout, /^healthz: not checked; no unit at /m);
|
||||
+});
|
||||
+
|
||||
+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']]) {
|
||||
+ const r = await run(dir, args);
|
||||
+ assert.equal(r.status, 4, args.join(' '));
|
||||
+ assert.match(r.stderr, /^usage: scripts\/webui-service\.sh/);
|
||||
+ }
|
||||
+});
|
||||
diff --git a/scripts/webui-service.sh b/scripts/webui-service.sh
|
||||
new file mode 100755
|
||||
index 00000000..4ff06833
|
||||
--- /dev/null
|
||||
+++ b/scripts/webui-service.sh
|
||||
@@ -0,0 +1,208 @@
|
||||
+#!/usr/bin/env bash
|
||||
+# `scripts/webui-service.sh render|install|uninstall|status`: the systemd
|
||||
+# user unit that runs the Console, `node packages/webui/src/cli.mjs serve`
|
||||
+# (lead decision 94). The template is
|
||||
+# packages/webui/systemd/mosaic-webui.service.in; the rendered unit is
|
||||
+# mosaic-webui.service in the systemd user directory, one instance.
|
||||
+#
|
||||
+# render [FLAGS] print the rendered unit on stdout
|
||||
+# install [--dir DIR] [--no-reload] [FLAGS]
|
||||
+# write the unit (write to a temp file, then
|
||||
+# 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
|
||||
+#
|
||||
+# 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
|
||||
+# 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`.
|
||||
+#
|
||||
+# Exit codes: 0 ok, 1 operation failed, 4 usage.
|
||||
+set -euo pipefail
|
||||
+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]"
|
||||
+
|
||||
+die() { echo "webui-service: $*" >&2; exit 1; }
|
||||
+usage() { echo "$USAGE" >&2; exit 4; }
|
||||
+
|
||||
+UNIT_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user"
|
||||
+RELOAD=1
|
||||
+DIR_FLAGS=0
|
||||
+PORT=7332
|
||||
+BUSINESS=""
|
||||
+BOARD=""
|
||||
+PUBLIC_HOST=""
|
||||
+AUTH_HEADER=""
|
||||
+ALLOW_USER=""
|
||||
+SERVE_SET=0
|
||||
+
|
||||
+parse_flags() {
|
||||
+ while [ $# -gt 0 ]; do
|
||||
+ case "$1" in
|
||||
+ --dir) [ $# -ge 2 ] || usage; UNIT_DIR="$2"; DIR_FLAGS=1; shift 2 ;;
|
||||
+ --no-reload) RELOAD=0; DIR_FLAGS=1; shift ;;
|
||||
+ --port) [ $# -ge 2 ] || usage; PORT="$2"; SERVE_SET=1; shift 2 ;;
|
||||
+ --business) [ $# -ge 2 ] || usage; BUSINESS="$2"; SERVE_SET=1; shift 2 ;;
|
||||
+ --board) [ $# -ge 2 ] || usage; BOARD="$2"; SERVE_SET=1; shift 2 ;;
|
||||
+ --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 ;;
|
||||
+ *) usage ;;
|
||||
+ esac
|
||||
+ done
|
||||
+}
|
||||
+
|
||||
+# The serve flags as one ExecStart fragment. Each character set excludes
|
||||
+# space, %, $, quotes, backslash, & and |, so neither systemd nor the sed
|
||||
+# below can read a value as anything but itself.
|
||||
+serve_flags() {
|
||||
+ case "$PORT" in ''|*[!0-9]*) die "port must be 1..65535: $PORT" ;; esac
|
||||
+ [ "${#PORT}" -le 5 ] && [ "$PORT" -ge 1 ] && [ "$PORT" -le 65535 ] || die "port must be 1..65535: $PORT"
|
||||
+ local flags="--port $PORT"
|
||||
+ if [ -n "$BUSINESS" ]; then
|
||||
+ case "$BUSINESS" in [a-z0-9]*) ;; *) die "business must be a business id: $BUSINESS" ;; esac
|
||||
+ case "$BUSINESS" in *[!a-z0-9._-]*) die "business must be a business id: $BUSINESS" ;; esac
|
||||
+ [ "${#BUSINESS}" -le 64 ] || die "business must be a business id: $BUSINESS"
|
||||
+ flags="$flags --business $BUSINESS"
|
||||
+ fi
|
||||
+ if [ -n "$BOARD" ]; then
|
||||
+ # serve checks the rest (a loopback origin, no path or credentials).
|
||||
+ case "$BOARD" in http://?*) ;; *) die "board must be an http:// URL: $BOARD" ;; esac
|
||||
+ case "$BOARD" in *[!A-Za-z0-9.:/_-]*) die "board contains a character the unit cannot carry: $BOARD" ;; esac
|
||||
+ flags="$flags --board $BOARD"
|
||||
+ fi
|
||||
+ local preview=0
|
||||
+ [ -z "$PUBLIC_HOST" ] || preview=$((preview + 1))
|
||||
+ [ -z "$AUTH_HEADER" ] || preview=$((preview + 1))
|
||||
+ [ -z "$ALLOW_USER" ] || preview=$((preview + 1))
|
||||
+ 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
|
||||
+ case "$PUBLIC_HOST" in *[!A-Za-z0-9.-]*) die "public host must be a hostname, no port or scheme: $PUBLIC_HOST" ;; esac
|
||||
+ case "$AUTH_HEADER" in [A-Za-z0-9]*) ;; *) die "auth header must be a header name: $AUTH_HEADER" ;; esac
|
||||
+ case "$AUTH_HEADER" in *[!A-Za-z0-9-]*) die "auth header must be a header name: $AUTH_HEADER" ;; esac
|
||||
+ 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
|
||||
+ echo "$flags"
|
||||
+}
|
||||
+
|
||||
+render() {
|
||||
+ [ -f "$TEMPLATE" ] || die "template missing: $TEMPLATE"
|
||||
+ local node
|
||||
+ node="$(command -v node || true)"
|
||||
+ [ -n "$node" ] || die "node not found on PATH"
|
||||
+ # systemd does not look an ExecStart command up in Environment=PATH,
|
||||
+ # so the unit runs node by the path found here, kept as given.
|
||||
+ case "$node" in /*) ;; *) die "node path must be absolute: $node" ;; esac
|
||||
+ case "$node" in *[!A-Za-z0-9/._+-]*) die "node path contains a character the unit cannot carry: $node" ;; esac
|
||||
+ case "$REPO" in *[!A-Za-z0-9/._+-]*) die "repository path contains a character the unit cannot carry: $REPO" ;; esac
|
||||
+ local node_dir path="/usr/local/bin:/usr/bin:/bin"
|
||||
+ node_dir="$(dirname "$node")"
|
||||
+ case ":$path:" in *":$node_dir:"*) ;; *) path="$node_dir:$path" ;; esac
|
||||
+ local flags
|
||||
+ flags="$(serve_flags)" || exit 1
|
||||
+ sed -e "s|@REPO@|$REPO|g" -e "s|@PATH@|$path|g" -e "s|@NODE@|$node|g" -e "s|@FLAGS@|$flags|g" "$TEMPLATE"
|
||||
+}
|
||||
+
|
||||
+install_unit() {
|
||||
+ parse_flags "$@"
|
||||
+ local unit
|
||||
+ unit="$(render)" || exit 1
|
||||
+ mkdir -p "$UNIT_DIR"
|
||||
+ local target="$UNIT_DIR/$UNIT_NAME" tmp
|
||||
+ tmp="$(mktemp "$UNIT_DIR/.$UNIT_NAME.XXXXXX")"
|
||||
+ printf '%s\n' "$unit" > "$tmp"
|
||||
+ chmod 0644 "$tmp"
|
||||
+ if [ -f "$target" ] && cmp -s "$tmp" "$target"; then
|
||||
+ rm -f "$tmp"
|
||||
+ echo "unchanged: $target"
|
||||
+ else
|
||||
+ mv -f "$tmp" "$target"
|
||||
+ echo "written: $target"
|
||||
+ fi
|
||||
+ echo "serve: $(sed -n 's/^ExecStart=//p' "$target")"
|
||||
+ if [ "$RELOAD" = 1 ]; then
|
||||
+ systemctl --user daemon-reload || die "daemon-reload failed"
|
||||
+ 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:
|
||||
+ 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
|
||||
+ systemctl --user stop mosaic-webui SIGTERM; restartable
|
||||
+ scripts/webui-service.sh status unit state and /healthz
|
||||
+survive logout and reboot only with lingering on: loginctl enable-linger ${USER:-$(id -un)}
|
||||
+MSG
|
||||
+}
|
||||
+
|
||||
+uninstall_unit() {
|
||||
+ parse_flags "$@"
|
||||
+ [ "$SERVE_SET" = 0 ] || usage
|
||||
+ local target="$UNIT_DIR/$UNIT_NAME"
|
||||
+ if [ "$RELOAD" = 1 ]; then
|
||||
+ local active
|
||||
+ active="$(systemctl --user list-units --no-legend --plain "$UNIT_NAME" 2>/dev/null | awk '$3 == "active" || $3 == "activating" || $3 == "deactivating" {print $1}')"
|
||||
+ [ -z "$active" ] || die "refusing: $UNIT_NAME is still running; the human stops it first (systemctl --user stop mosaic-webui)"
|
||||
+ fi
|
||||
+ if [ -f "$target" ]; then
|
||||
+ rm -f "$target"
|
||||
+ echo "removed: $target"
|
||||
+ else
|
||||
+ echo "absent: $target"
|
||||
+ fi
|
||||
+ if [ "$RELOAD" = 1 ]; then
|
||||
+ systemctl --user daemon-reload || die "daemon-reload failed"
|
||||
+ echo "daemon-reload done"
|
||||
+ fi
|
||||
+}
|
||||
+
|
||||
+status_unit() {
|
||||
+ parse_flags "$@"
|
||||
+ [ "$SERVE_SET" = 0 ] && [ "$RELOAD" = 1 ] || usage
|
||||
+ local target="$UNIT_DIR/$UNIT_NAME"
|
||||
+ echo "unit: $UNIT_NAME"
|
||||
+ echo " enabled: $(systemctl --user is-enabled "$UNIT_NAME" 2>&1 || true)"
|
||||
+ echo " active: $(systemctl --user is-active "$UNIT_NAME" 2>&1 || true)"
|
||||
+ systemctl --user show "$UNIT_NAME" -p MainPID -p NRestarts -p ExecMainStartTimestamp -p Result 2>/dev/null | sed 's/^/ /' || true
|
||||
+ if [ ! -f "$target" ]; then
|
||||
+ echo "healthz: not checked; no unit at $target"
|
||||
+ return 0
|
||||
+ 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; }
|
||||
+ local url="http://127.0.0.1:$port/healthz"
|
||||
+ echo "healthz: $url $(node -e '
|
||||
+fetch(process.argv[1], { signal: AbortSignal.timeout(5000) })
|
||||
+ .then(async (res) => console.log(`${res.status} ${(await res.text()).trim()}`))
|
||||
+ .catch((err) => console.log(`unreachable (${err.cause?.code ?? err.name})`));
|
||||
+' "$url")"
|
||||
+}
|
||||
+
|
||||
+[ $# -ge 1 ] || usage
|
||||
+cmd="$1"; shift
|
||||
+case "$cmd" in
|
||||
+ render)
|
||||
+ parse_flags "$@"
|
||||
+ [ "$DIR_FLAGS" = 0 ] || usage
|
||||
+ render ;;
|
||||
+ install) install_unit "$@" ;;
|
||||
+ uninstall) uninstall_unit "$@" ;;
|
||||
+ status) status_unit "$@" ;;
|
||||
+ -h|--help) echo "$USAGE" ;;
|
||||
+ *) usage ;;
|
||||
+esac
|
||||
@@ -0,0 +1,4 @@
|
||||
12ab94c9541afa26c4d823fbae53712834b1cdcefbd8f1970fd29145eecef9ad docs/TOOLS.md
|
||||
9c2eec77207baad825bb4ab6a7d2dac17d92cf62f1cf4ab173a4902b7c07002c packages/webui/systemd/mosaic-webui.service.in
|
||||
9c1a6c8b2eeace79f6d061e67d97cfb22195d5fb1e1ea6040049de95c833cb20 packages/webui/tests/service.test.mjs
|
||||
69fad4eb85c3e9297f6ebba2ec84d17d49a5ef028d1787e981f5af61fc9a8912 scripts/webui-service.sh
|
||||
@@ -0,0 +1,4 @@
|
||||
docs/TOOLS.md
|
||||
packages/webui/systemd/mosaic-webui.service.in
|
||||
packages/webui/tests/service.test.mjs
|
||||
scripts/webui-service.sh
|
||||
@@ -0,0 +1 @@
|
||||
2026-10-11T22:32:03Z
|
||||
@@ -0,0 +1 @@
|
||||
2026-10-11T22:25:46Z
|
||||
@@ -0,0 +1,9 @@
|
||||
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
|
||||
@@ -0,0 +1,17 @@
|
||||
OK status with missing harness credential exits 3 and still lists accounts
|
||||
OK status reports harness credential (read-only) + mosaic accounts
|
||||
OK api key material never reaches output
|
||||
OK oauth token material never reaches output
|
||||
OK unparseable credential file exits 2
|
||||
OK symlinked credential file exits 4
|
||||
OK env-side credential names reported
|
||||
OK env var values never reach output
|
||||
OK accounts without an accounts dir reports none and creates nothing
|
||||
OK accounts lists files and marks the active one
|
||||
OK loose account perms flagged in listing
|
||||
OK agent --auth with missing account file refuses (exit 4)
|
||||
OK agent --auth with non-0600 account file refuses
|
||||
OK agent --auth with invalid account name refuses
|
||||
OK auth.sh without valid config refuses
|
||||
|
||||
selftest: 15 passed, 0 failed
|
||||
@@ -0,0 +1,55 @@
|
||||
Note: switching to '43d2328f3a413b066f435c13cacd4df462670c57'.
|
||||
|
||||
You are in 'detached HEAD' state. You can look around, make experimental
|
||||
changes and commit them, and you can discard any commits you make in this
|
||||
state without impacting any branches by switching back to a branch.
|
||||
|
||||
If you want to create a new branch to retain commits you create, you may
|
||||
do so (now or later) by using -c with the switch command. Example:
|
||||
|
||||
git switch -c <new-branch-name>
|
||||
|
||||
Or undo this operation with:
|
||||
|
||||
git switch -
|
||||
|
||||
Turn off this advice by setting config variable advice.detachedHead to false
|
||||
|
||||
Not currently on any branch.
|
||||
nothing to commit, working tree clean
|
||||
Note: switching to '43d2328f3a413b066f435c13cacd4df462670c57'.
|
||||
|
||||
You are in 'detached HEAD' state. You can look around, make experimental
|
||||
changes and commit them, and you can discard any commits you make in this
|
||||
state without impacting any branches by switching back to a branch.
|
||||
|
||||
If you want to create a new branch to retain commits you create, you may
|
||||
do so (now or later) by using -c with the switch command. Example:
|
||||
|
||||
git switch -c <new-branch-name>
|
||||
|
||||
Or undo this operation with:
|
||||
|
||||
git switch -
|
||||
|
||||
Turn off this advice by setting config variable advice.detachedHead to false
|
||||
|
||||
OK dry-run: allowed change, exit 0, nothing committed (exit 0)
|
||||
OK dry-run committed nothing
|
||||
OK apply: allowed change exits 0 (exit 0)
|
||||
OK apply: attribution in commit subject
|
||||
OK apply: target tree clean after commit
|
||||
OK disallowed path refused (exit 1)
|
||||
OK disallowed path: target untouched
|
||||
OK syntax gate refused broken .mjs (exit 1)
|
||||
OK syntax gate: target untouched
|
||||
OK suite failure refused (exit 1)
|
||||
OK suite failure: target reverted to clean
|
||||
OK disabled policy refused (exit 2)
|
||||
OK disabled policy: target untouched
|
||||
OK failed run refused (exit 1)
|
||||
OK failed run: target untouched
|
||||
OK missing run exits 4 (exit 4)
|
||||
OK invalid policy exits 2 (exit 2)
|
||||
|
||||
selftest: 17 passed, 0 failed
|
||||
@@ -0,0 +1,26 @@
|
||||
OK absent adapter defaults to pi
|
||||
OK adapter mock validates (exit 0)
|
||||
OK unsupported adapter exits 2 (exit 2)
|
||||
OK env exports adapter
|
||||
OK bootstrap creates default when absent (exit 0)
|
||||
OK bootstrap wrote config file
|
||||
OK bootstrap is idempotent on existing config (exit 0)
|
||||
OK bootstrap did not rewrite existing config
|
||||
OK validate missing config exits 3 (exit 3)
|
||||
OK malformed JSON exits 2 (exit 2)
|
||||
OK unsupported configVersion exits 2 (exit 2)
|
||||
OK unknown top-level key exits 2 (exit 2)
|
||||
OK unknown execution key exits 2 (exit 2)
|
||||
OK unsupported backend exits 2 (exit 2)
|
||||
OK unsupported environment exits 2 (exit 2)
|
||||
OK relative dataRoot exits 2 (exit 2)
|
||||
OK non-canonical dataRoot exits 2 (exit 2)
|
||||
OK filesystem root dataRoot exits 2 (exit 2)
|
||||
OK home directory dataRoot exits 2 (exit 2)
|
||||
OK dataRoot containing config dir exits 2 (exit 2)
|
||||
OK control character in provider exits 2 (exit 2)
|
||||
OK symlinked config file exits 2 (exit 2)
|
||||
OK env exports resolve correctly
|
||||
OK failed validation modified nothing
|
||||
|
||||
selftest: 24 passed, 0 failed
|
||||
@@ -0,0 +1,70 @@
|
||||
toolchain: node v26.8.1
|
||||
|
||||
OK syntax: packages/discord/src/approvals.mjs
|
||||
OK syntax: packages/discord/src/authorize.mjs
|
||||
OK syntax: packages/discord/src/binding.mjs
|
||||
OK syntax: packages/discord/src/cli.mjs
|
||||
OK syntax: packages/discord/src/connector.mjs
|
||||
OK syntax: packages/discord/src/context.mjs
|
||||
OK syntax: packages/discord/src/engine-pi.mjs
|
||||
OK syntax: packages/discord/src/errors.mjs
|
||||
OK syntax: packages/discord/src/gateway.mjs
|
||||
OK syntax: packages/discord/src/git.mjs
|
||||
OK syntax: packages/discord/src/journal.mjs
|
||||
OK syntax: packages/discord/src/notify.mjs
|
||||
OK syntax: packages/discord/src/rest.mjs
|
||||
OK syntax: packages/discord/src/setspark.mjs
|
||||
OK syntax: packages/discord/src/tools.mjs
|
||||
OK syntax: packages/discord/src/web.mjs
|
||||
OK syntax: packages/discord/bin/git-credential.mjs
|
||||
OK syntax: packages/discord/extension/tools.mjs
|
||||
OK syntax: packages/discord/tests/approvals.test.mjs
|
||||
OK syntax: packages/discord/tests/authorize.test.mjs
|
||||
OK syntax: packages/discord/tests/binding.test.mjs
|
||||
OK syntax: packages/discord/tests/connector.test.mjs
|
||||
OK syntax: packages/discord/tests/context.test.mjs
|
||||
OK syntax: packages/discord/tests/engine.test.mjs
|
||||
OK syntax: packages/discord/tests/fake-pi.mjs
|
||||
OK syntax: packages/discord/tests/gateway.test.mjs
|
||||
OK syntax: packages/discord/tests/git.test.mjs
|
||||
OK syntax: packages/discord/tests/helpers.mjs
|
||||
OK syntax: packages/discord/tests/journal.test.mjs
|
||||
OK syntax: packages/discord/tests/notify.test.mjs
|
||||
OK syntax: packages/discord/tests/recover.test.mjs
|
||||
OK syntax: packages/discord/tests/rest.test.mjs
|
||||
OK syntax: packages/discord/tests/setspark.test.mjs
|
||||
OK syntax: packages/discord/tests/tools.test.mjs
|
||||
OK syntax: packages/discord/tests/web.test.mjs
|
||||
OK syntax: packages/discord/fixtures/claim-worker.mjs
|
||||
OK syntax: packages/discord/fixtures/legacy-owner-worker.mjs
|
||||
OK syntax: scripts/discord.sh
|
||||
OK syntax: scripts/discord-service.sh
|
||||
OK packages/discord declares no dependencies
|
||||
OK no bot-token-shaped string in packages/discord
|
||||
OK fixture binding uses placeholder ids only
|
||||
OK fixture binding validates
|
||||
OK real pi with the extension exposes exactly list_dir, read_file, search and no built-in tool
|
||||
OK real pi with a writable root exposes exactly the three reads plus write_file and edit_file, and writes nothing at start
|
||||
OK real pi with a web key exposes the three reads plus web_fetch and web_search, and no write tool without a writable root
|
||||
OK real pi with a git root exposes the reads, writes and the four git verbs, commits nothing at start, and never shows the token
|
||||
OK real pi with protocol vault adds reserve_id to the git verbs
|
||||
OK real pi with a setspark key exposes the reads and the eight record verbs, no counters, and never shows the key
|
||||
OK real pi refuses a git key on a read-only root (fail closed)
|
||||
OK real pi with the pilot flags (--no-tools) exposes no tool at all
|
||||
OK real pi exits non-zero without MOSAIC_DISCORD_TOOLS: no session, no tools (fail closed)
|
||||
OK a failing nested test fails the run under a parent runner's NODE_TEST_CONTEXT
|
||||
OK node --test packages/discord/tests/ (ℹ pass 178)
|
||||
OK scripts/discord.sh --help exits 0
|
||||
OK scripts/discord.sh check without a binding exits 4
|
||||
OK scripts/discord.sh recover without a binding exits 4
|
||||
OK scripts/discord.sh reload without a binding exits 4
|
||||
OK scripts/discord-service.sh without a command exits 4
|
||||
OK service unit renders with the repository path, a supervised run as the main process, exit 3 never retried, and reload as SIGHUP
|
||||
OK service install writes the rendered unit (0644) and leaves no temp file
|
||||
OK service install a second time reports unchanged
|
||||
OK systemd-analyze verify accepts the rendered unit
|
||||
OK service uninstall removes the unit file
|
||||
OK service install with an unknown flag exits 4
|
||||
OK service install with USER unset finishes and names the account for lingering
|
||||
|
||||
discord suite: 66 passed, 0 failed
|
||||
@@ -0,0 +1,21 @@
|
||||
OK initial ordinary-file install
|
||||
OK installed tree matches canonical source
|
||||
OK installed tree has no symlinks
|
||||
OK check detects installation drift
|
||||
OK sync refuses to overwrite installation drift
|
||||
OK check detects an extra destination file
|
||||
OK check detects an extra destination directory
|
||||
OK check rejects a destination symlink
|
||||
OK sync accepts a canonical source update
|
||||
OK updated installation matches canonical source
|
||||
scripts/test-extension-package.sh: line 14: 3009858 Killed "$@" > /dev/null 2>&1
|
||||
OK forced interruption kills the replacing process
|
||||
OK next invocation recovers old consistent installation
|
||||
OK interrupted replacement rolled back
|
||||
OK sync succeeds after interruption recovery
|
||||
OK unlocked stale lock file does not block
|
||||
OK active lock refuses a concurrent sync
|
||||
OK source symlink fails closed
|
||||
OK nested second entrypoint fails closed
|
||||
|
||||
extension package selftest: 18 passed, 0 failed
|
||||
@@ -0,0 +1,53 @@
|
||||
toolchain: node v26.8.1, python 3.12.8, jsonschema 4.26.0
|
||||
|
||||
OK syntax: scripts/foundation-inspect.mjs
|
||||
OK syntax: scripts/foundation/strict-json.mjs
|
||||
OK syntax: scripts/foundation/canonical.mjs
|
||||
OK syntax: scripts/foundation/resolve.mjs
|
||||
OK syntax: scripts/foundation/validate-record.mjs
|
||||
OK syntax: scripts/foundation/fixtures/build-fixtures.mjs
|
||||
OK syntax: scripts/foundation/canonical.test.mjs
|
||||
OK syntax: scripts/foundation/cli.test.mjs
|
||||
OK syntax: scripts/foundation/fixtures.test.mjs
|
||||
OK syntax: scripts/foundation/resolve.test.mjs
|
||||
OK syntax: scripts/foundation/strict-json.test.mjs
|
||||
OK syntax: scripts/foundation/verify-schema.py (ast only; no bytecode written)
|
||||
OK fixture generator runs
|
||||
OK checked-in fixtures/bundles equal a fresh generation
|
||||
OK checked-in fixtures/raw equal a fresh generation
|
||||
OK checked-in fixtures/index.json equal a fresh generation
|
||||
OK checked-in demo bundles equal a fresh generation
|
||||
OK a failing nested test fails the run under a parent runner's NODE_TEST_CONTEXT
|
||||
OK node --test scripts/foundation/ (ℹ pass 80)
|
||||
OK differential schema oracle: PASS: differential schema oracle (finite corpus; compatibility evidence, not equivalence proof)
|
||||
platform witness: strftime('%Y') for year 999 -> '999' (pinned checker refuses years 0001..0999)
|
||||
node v26.8.1; corpus 1568 records (38 pinned fixtures, 478 unique bundle records, 1052 typeCase/mutation/lexical cases)
|
||||
schema column: agree-valid 540, agree-invalid 991, DISAGREEMENTS 0; strict-only (parser-bound) cases: 27; unsupported-kind records not schema-assessed by the inspector: 10
|
||||
profile column (schema-valid records only): profile-valid 510, profile-invalid 30
|
||||
profile refusals asserted: 30 schema-agreed-valid records refused only by the strict typed-string profile (rule profile-pattern-mismatch), 12 declared by name; 73 named probes verified against declared schema/profile columns
|
||||
OK oracle: zero schema-column disagreements with the pinned checker
|
||||
OK oracle: strict-only profile refusals are counted and asserted
|
||||
OK demo: permitted read preview exits 0 (exit 0)
|
||||
OK demo: permitted file.change preview exits 0 (exit 0)
|
||||
OK demo: assignment.change proposal is unresolved (exit 3) (exit 3)
|
||||
OK demo: revoked registration is refused (exit 3) (exit 3)
|
||||
OK demo: message is not authority (exit 3) (exit 3)
|
||||
OK usage: no arguments exits 2 (exit 2)
|
||||
OK io: missing file exits 4 (exit 4)
|
||||
OK io: directory exits 4 (exit 4)
|
||||
OK io: symlink exits 4 (O_NOFOLLOW) (exit 4)
|
||||
OK bound: oversize fixture exits 2 (exit 2)
|
||||
OK profile: one final LF in a typed selection id is refused before admission (exit 2) (exit 2)
|
||||
OK profile: two final LFs fail the schema pattern itself (exit 2) (exit 2)
|
||||
OK profile: escaped newlines in free-form text stay allowed (exit 0) (exit 0)
|
||||
OK profile refusal is invalid-request/profile-pattern-mismatch with selection and operation withheld, value not echoed
|
||||
OK text output starts with the disclaimer
|
||||
OK json output is valid JSON with result allowed and exactly the charter §7 fields
|
||||
OK json golden matches byte-for-byte
|
||||
OK sandboxed bundle run (env -i, PATH=/nonexistent) produced the unresolved proposal
|
||||
OK sandbox inventory (path/type/size/mode/uid/gid/inode/mtime/sha256) unchanged by runs
|
||||
OK canary never printed (bundle run and credential-file run)
|
||||
OK a non-bundle JSON file is refused at the shape gate, not read into output
|
||||
OK no field of the non-bundle file is echoed
|
||||
|
||||
selftest: 44 passed, 0 failed
|
||||
@@ -0,0 +1,35 @@
|
||||
toolchain: node v26.8.1, git version 2.55.0
|
||||
|
||||
OK syntax: packages/queue/src/cli.mjs
|
||||
OK syntax: packages/queue/src/errors.mjs
|
||||
OK syntax: packages/queue/src/io.mjs
|
||||
OK syntax: packages/queue/src/lock.mjs
|
||||
OK syntax: packages/queue/src/queue.mjs
|
||||
OK syntax: packages/queue/src/review.mjs
|
||||
OK syntax: packages/queue/src/store.mjs
|
||||
OK syntax: packages/queue/tests/commit.test.mjs
|
||||
OK syntax: packages/queue/tests/data.test.mjs
|
||||
OK syntax: packages/queue/tests/dispatch.test.mjs
|
||||
OK syntax: packages/queue/tests/helpers.mjs
|
||||
OK syntax: packages/queue/tests/lock.test.mjs
|
||||
OK syntax: packages/queue/tests/migration.test.mjs
|
||||
OK syntax: packages/queue/tests/review.test.mjs
|
||||
OK syntax: packages/queue/tests/store.test.mjs
|
||||
OK syntax: packages/queue/tests/write.test.mjs
|
||||
OK syntax: packages/queue/tests/fixtures/fake-gitea.mjs
|
||||
OK syntax: packages/queue/tests/fixtures/kill-at.mjs
|
||||
OK syntax: packages/queue/tests/fixtures/lock-child.mjs
|
||||
OK syntax: packages/queue/tests/fixtures/mosaic-pre-a2.sh
|
||||
OK syntax: scripts/queue-commit.sh
|
||||
OK syntax: scripts/git-hooks/pre-commit
|
||||
OK syntax: scripts/mosaic
|
||||
OK queue-commit.sh, the guard and scripts/mosaic are executable
|
||||
OK packages/queue declares no dependencies
|
||||
ℹ tests 150
|
||||
ℹ pass 150
|
||||
ℹ fail 0
|
||||
OK node --test packages/queue/tests/
|
||||
OK scripts/mosaic queue help
|
||||
skip queue verify and render --check: this checkout (/mnt/storage/scratch/rocko-r75) is not the queue's canonical root (/mnt/storage/src/mosaic-stack)
|
||||
|
||||
queue suite: 27 passed, 0 failed
|
||||
@@ -0,0 +1,16 @@
|
||||
OK valid RELEASE resolves (exit 0)
|
||||
OK invalid RELEASE exits 1 (exit 1)
|
||||
OK missing RELEASE exits 1 (exit 1)
|
||||
OK valid RELEASE leaves image tag consistent with version
|
||||
OK status safe on empty state (exit 0)
|
||||
OK status created no pointer
|
||||
OK fault-injected activation refuses (exit 1)
|
||||
OK refused activation wrote no pointer
|
||||
OK refusal logged exactly once with valid fields
|
||||
OK healthy activation succeeds (exit 0)
|
||||
OK pointer written with valid fields
|
||||
OK repeat activation succeeds (log grows) (exit 0)
|
||||
OK log is append-only across activations
|
||||
OK rollback without previous refuses (exit 1)
|
||||
|
||||
selftest: 14 passed, 0 failed
|
||||
@@ -0,0 +1,101 @@
|
||||
OK valid task validates (exit 0)
|
||||
OK unknown task key exits 2 (exit 2)
|
||||
OK unsupported taskVersion exits 2 (exit 2)
|
||||
OK invalid task id exits 2 (exit 2)
|
||||
OK empty prompt exits 2 (exit 2)
|
||||
OK NUL in expectExact exits 2 (exit 2)
|
||||
OK out-of-range timeout exits 2 (exit 2)
|
||||
OK missing mission file exits 4 (exit 4)
|
||||
OK task with valid mission validates (exit 0)
|
||||
OK invalid mission exits 2 (exit 2)
|
||||
OK validate missing task exits 4 (exit 4)
|
||||
OK validation does not modify the task file
|
||||
OK prune dry-run exits 0 (exit 0)
|
||||
OK dry-run deleted nothing
|
||||
OK prune --keep=2 --yes removes oldest (exit 0)
|
||||
OK kept exactly 2 newest runs
|
||||
OK newest run kept, oldest pruned
|
||||
OK append-only receipt written (3 entries)
|
||||
OK sessions/workspaces untouched by prune
|
||||
OK prune with invalid keep exits 4 (exit 4)
|
||||
OK mock adapter: gate passes on matching mock response (exit 0)
|
||||
OK mock adapter: expect-mismatch recorded (exit 1)
|
||||
OK mismatch reason recorded
|
||||
OK unknown adapter fails closed (exit 1)
|
||||
OK mission task runs via mock adapter (exit 0)
|
||||
OK mission section injected into generated prompt
|
||||
OK retry of mission run succeeds (exit 0)
|
||||
OK retriedFrom lineage recorded
|
||||
OK mission section present after retry (relative path resolved)
|
||||
OK retry of missing run exits 4 (exit 4)
|
||||
OK skill install bundled ms-tools (exit 0)
|
||||
OK installed to skills-available
|
||||
OK double install refuses (exit 1)
|
||||
OK activate enables skill (exit 0)
|
||||
OK enabled dir populated
|
||||
OK uninstall while enabled refuses (exit 1)
|
||||
OK deactivate moves back to available (exit 0)
|
||||
OK uninstall removes from available (exit 0)
|
||||
OK seat with enabled skill launches
|
||||
OK skill path delivered to adapter
|
||||
OK shipped researcher contract resolves (ceiling + network)
|
||||
OK resolve-role refuses a non-role document (conductor policy) (exit 2)
|
||||
OK resolve-role refuses name/filename mismatch (exit 2)
|
||||
OK resolve-role refuses unknown network declaration (exit 2)
|
||||
OK resolve-role refuses duplicate role tool (exit 2)
|
||||
OK resolve-role refuses unsupported tool (exit 2)
|
||||
OK resolve-role refuses missing contract file (exit 4)
|
||||
OK version 1 role prints no contract line
|
||||
OK shipped version 2 roles resolve with their contracts (pm, cto, coder, reviewer)
|
||||
OK resolve-role accepts a minimal version 2 role (exit 0)
|
||||
OK resolve-role refuses an action outside the vocabulary (exit 2)
|
||||
OK resolve-role refuses a gated-only action in a role file (exit 2)
|
||||
OK resolve-role refuses a Vikunja verb no role may hold (exit 2)
|
||||
OK resolve-role refuses a version 2 role whose contract is missing (exit 2)
|
||||
OK resolve-role refuses a contract that is a symbolic link (exit 2)
|
||||
OK seat launches under role ceiling
|
||||
OK role ceiling narrows seat tools (read,write,bash -> read,bash)
|
||||
OK narrowing recorded loudly
|
||||
OK missing role contract refuses launch (exit 2)
|
||||
OK missing-contract refusal names the role
|
||||
OK empty ceiling intersection -> tool-free seat
|
||||
OK tool-free outcome recorded loudly
|
||||
OK overridden agents dir without seat definition refuses (exit 4)
|
||||
OK seat-resolution refusal names the seat
|
||||
OK policy: mission only -> mission tools
|
||||
OK policy: task only -> task tools
|
||||
OK policy: both -> intersection (task narrowed)
|
||||
OK policy: empty intersection -> tool-free
|
||||
OK invalid mission capabilities rejected (exit 2)
|
||||
mosaic-task: unsupported mission tool: "sudo" (supported: read, write, edit, bash, grep, find, ls)
|
||||
OK task run with user layer present (exit 0)
|
||||
OK user context dispatched into generated prompt
|
||||
OK workspace+tools task runs via mock (exit 0)
|
||||
OK workspace path + tools delivered to adapter
|
||||
OK persistent workspace created on host
|
||||
OK plain task still runs (no workspace/tools) (exit 0)
|
||||
OK workspace var present but empty when absent
|
||||
OK tools empty when absent
|
||||
OK unknown tool exits 2 (exit 2)
|
||||
OK workspace traversal exits 2 (exit 2)
|
||||
OK live hello task succeeds with exact marker
|
||||
OK result.json written in run dir
|
||||
OK result.json contents are correct
|
||||
OK wrong expectExact fails with exit 1 (reason: expect-mismatch)
|
||||
OK each run gets a distinct run dir (no clobber)
|
||||
OK list shows both runs
|
||||
OK fork base: teach succeeds (exit 0)
|
||||
OK fork child recalls ancestor context (exit 0)
|
||||
OK forked child recalled ancestor code word
|
||||
OK ancestor session untouched by fork
|
||||
OK child session dir has its own branch file
|
||||
OK onboard without name exits 4 (non-interactive) (exit 4)
|
||||
OK onboard --name renders profile (exit 0)
|
||||
OK profile written
|
||||
OK canon structure: required filled, optional placeholdered
|
||||
OK canon sections present
|
||||
OK user recall run succeeds
|
||||
OK recalled user name (response: Jason)
|
||||
OK no agent identity on headless run
|
||||
|
||||
selftest: 98 passed, 0 failed
|
||||
@@ -0,0 +1,59 @@
|
||||
✔ browser edge states: loading, empty, malformed, stale, hostile/long values, in-flight reply and appearance fallback (11674.789973ms)
|
||||
Rendered contrast: {"failures":[],"count":330,"lowest":4.504658476260286}
|
||||
✔ served Console browser: real board fixtures, keyboard, drafts, receipts, themes, 320px and failures (13706.759161ms)
|
||||
✔ S5 pages in a browser: real broker rows, inert text, Copy, routes, failures, layout and no writes (14648.970608ms)
|
||||
✔ bus routes serve the four reads from the reader view, unescaped JSON for the page to escape (394.51471ms)
|
||||
✔ decisions route serves the roles and recent lists from the reader view (222.487933ms)
|
||||
✔ bus routes are GET-only, have no write verb, and keep the same-origin checks (387.603619ms)
|
||||
✔ bus refusals map to statuses and never carry a path or a stack (78.352185ms)
|
||||
✔ humanCall speaks the human transport protocol without blocking the server (3140.208613ms)
|
||||
✔ busReader takes --business, else the live bus host, else refuses (1268.741589ms)
|
||||
✔ humanCall kills a transport that runs past its timeout (689.727705ms)
|
||||
✔ serve refuses a --business value that is not a business id (1230.463868ms)
|
||||
✔ conversation view: full history, collapsed tools, hidden thinking, inert hostile content, malformed and reconcile markers (10519.34976ms)
|
||||
✔ conversation view: a fork keeps the open branch, says so, and opens the new one on request (4307.797103ms)
|
||||
✔ conversation view: a newer session with no readable history keeps the marker (2145.820226ms)
|
||||
✔ conversation view: seats without history say so and offer no reply (1639.322949ms)
|
||||
✔ Discord row through real board/WebUI: independent brake/liveness, no Reply, literal content (10745.517796ms)
|
||||
✔ return flow through the conversation view: send, tool call, delayed result, peer message, exact long answers, relaunch (62200.059017ms)
|
||||
✔ redactor: config, dataRoot, any absolute path, Discord ids and the JSON parser quote (3.112119ms)
|
||||
✔ credential state from the date alone, as the bus states it (0.367852ms)
|
||||
✔ queue: rows, one row, ids and methods, notes; no path or id from a row (3294.622674ms)
|
||||
✔ queue: a refused read fails closed with the store's text, redacted (1038.09559ms)
|
||||
✔ queue: a store that fails to load, or a child that dies, gives a fixed message with no path or stack (5104.478556ms)
|
||||
✔ business: names, vars on the allowlist, authority, credential metadata only (60.140853ms)
|
||||
✔ business: missing or invalid file refuses with the module's text, redacted (52.456468ms)
|
||||
✔ business without --business: the live bus host, else no-bus-host; no system config: not-configured (58.34462ms)
|
||||
✔ settings: release, board, loopback address, notifier binding name only (1081.077431ms)
|
||||
✔ both presentations replace old activity with relaunch notice, label retained history, then resume after new activity (11148.260802ms)
|
||||
✔ reported return flow and relative Age: reply sent from the inspector, then the new answer appears there without manual refresh (28830.543544ms)
|
||||
✔ loopback host and board origin fail closed (24.245814ms)
|
||||
✔ real board fixture passes through WebUI; assets and isolated seen/reply work (561.19726ms)
|
||||
✔ proxy preserves exact request bytes, status and receipt, rejects forms and malformed JSON, never follows redirect (321.722171ms)
|
||||
✔ unreachable board reports URL; CLI rejects unsupported options (2796.028317ms)
|
||||
✔ render prints the unit without the preview flags (75.26698ms)
|
||||
✔ render carries every serve flag, the preview set included (112.407931ms)
|
||||
✔ a partial preview set is refused (197.407027ms)
|
||||
✔ a value the unit cannot carry is refused, for every flag (3876.079297ms)
|
||||
✔ the rendered unit names none of the human proof agent markers and sets no agent variable (315.404716ms)
|
||||
✔ install into --dir with --no-reload writes the file whole, then reports unchanged (682.03444ms)
|
||||
✔ install without --no-reload runs daemon-reload and nothing that starts the unit (588.619294ms)
|
||||
✔ uninstall refuses while the unit is active, and removes it once it is not (987.496602ms)
|
||||
✔ status prints the unit state and GET /healthz on the installed port (1510.367271ms)
|
||||
✔ usage: an unknown verb or flag, and start or enable, exit 4 (285.484472ms)
|
||||
✔ tokens.css and icons.svg are byte copies of docs/design (4.747715ms)
|
||||
✔ JetBrains Mono files match the digests in their sources file, with the OFL text (8.967146ms)
|
||||
✔ shipped strings are unchanged, and the web keeps its write boundary (8.310162ms)
|
||||
✔ shell in a browser: System default, seeded states, 400px, focus and the Ctrl+K palette (17866.768235ms)
|
||||
✔ five states on Board, Inbox, Tasks, Agents and Trail (6193.108566ms)
|
||||
✔ Queue, Business and Settings in their five states, with no move button (14895.192654ms)
|
||||
✔ seeded secrets, Discord ids and paths reach no response and no page; business refusals in the module's words (3021.179379ms)
|
||||
✔ the server log names no config path when the system config refuses (691.292426ms)
|
||||
ℹ tests 50
|
||||
ℹ suites 0
|
||||
ℹ pass 50
|
||||
ℹ fail 0
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 63231.404253
|
||||
@@ -0,0 +1,18 @@
|
||||
6dcdc74e02a3f49c7fed27c21860508cb655980ac71d569c96951ac013145884 ./base.txt
|
||||
0f35cd1953b1038b488347e9df1b4309064fa869e1c14684370753a519b09607 ./BUILD.md
|
||||
27cc48485f92764cf9a4f92a746c209d1e30dd511064cdf8071858d00c62b5a9 ./build.patch
|
||||
7dc571dbe87a484f465c82e1965e499c57d9af047bfe702faa3fe041138dd154 ./candidate-manifest.sha256
|
||||
896d274d49f93a194d36a2cf081574569140f8ab78ff5dead6737ab9619d0a8f ./files.txt
|
||||
94e11500657683c821e50aaf9d5e746a26fc09b1321020fd4bf9b4e2c20dd533 ./out/gate/end.txt
|
||||
a2171dad35389dbd0260fd8fcc70611288eca6fdff4292632e3de50ab14800ef ./out/gate/start.txt
|
||||
573c60b32cb048dbe07739738eaeaa300b7a72d08764f23d63454d8154d582ae ./out/gate/summary.txt
|
||||
f91fc95a173339137f981e24fbf630acfde957e32c3cd71e28304532083c6a77 ./out/gate/test-auth.log
|
||||
1c464d617a1e10ef15a275cc9beeaf975d27de84d05071231970ca76af5d81fd ./out/gate/test-conductor.log
|
||||
52d211444489ea729dafc3c9745d4474d9dfd50f41055cbad792424197f04073 ./out/gate/test-config.log
|
||||
7d5016ded1994048642f8b9ae845e52e8af4a069ba05c056c8f580a4589bacbe ./out/gate/test-discord.log
|
||||
62d2769554912857c4020cd38af23d8711134805dba60983433bd394afc14289 ./out/gate/test-extension-package.log
|
||||
83dfaa21c4e4ad941088efab3f69e87ebd2f6bd3cc72ede938e21dc06da8a34f ./out/gate/test-foundation.log
|
||||
4d3f40bf6a83fca6aed9cab44c2c130ae90c0d2d4943a3a2b51e9141ada71f0c ./out/gate/test-queue.log
|
||||
fa832794ee35025fb4559d58d01a270f6bff2eb65e863e9075b797f47fa68545 ./out/gate/test-release.log
|
||||
ccea37f4ffde1f160dcd1a1864988083a645d974e892065c956b338d0f8c853a ./out/gate/test-task.log
|
||||
c0728d5190cd13bb0bee5d39bddc12776f2f8c724a28faee358ae81a034e1469 ./out/gate/webui.log
|
||||
Reference in New Issue
Block a user