✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (36.439821ms)
✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (33.925039ms)
✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (21.158075ms)
✔ decide prints a declining choice as declining (18.952008ms)
✔ an unknown outcome is reported once and never resent (16.952476ms)
✔ a decision closed before the answer arrives exits 2 and points at its trail (20.117146ms)
✔ a prefix that matches two open decisions exits 2 and resolves neither (16.377237ms)
✔ without --business a command uses the live host's business, and a stale host.json is not a host (17.785936ms)
✔ every human command refuses inside an agent run before it touches the bus (16.701711ms)
✔ usage errors exit 4; no business and no host is a usage error (17.493924ms)
✔ agents and tasks print through the broker (17.802635ms)
✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (3.969558ms)
✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (65.153084ms)
✔ trackers come from the tracker.* variables of the one project that names a tracker project (65.019891ms)
✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (63.315796ms)
✔ two projects that each name a tracker project refuse, since the boot shape holds one (57.643913ms)
✔ a business without tracker.baseUrl gets no trackers entry (79.072851ms)
✔ an unknown business and a broken system config refuse with exit 3 (100.614075ms)
✔ empty views say so (1.321977ms)
✔ the trail keeps the broker's order and names a decision's task without its rows (1.642474ms)
✔ tasks print the tracker fields the snapshot carries (0.24347ms)
✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (979.002116ms)
✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (149.546048ms)
✔ a second host for the same data root refuses with exit 3 while the first runs (75.917239ms)
✔ a notifier that refuses stops the broker and the host refuses with exit 3 (128.077629ms)
✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (125.479723ms)
✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (74.096919ms)
✔ a close send that fails with EPIPE after the notifier refuses still gives the notifier's refusal, exit 3 (128.383414ms)
✔ a close send that fails with EPIPE after the notifier dies unanswered still gives the notifier's error (75.365589ms)
✔ close() whose stop and close sends fail with EPIPE still finishes, with exit 1 (149.996979ms)
✔ watchChildren reports a child that died before it was called, and one that dies later (24.844573ms)
✔ bus stop refuses to signal a live pid that is not a bus host (202.886459ms)
✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (209.271349ms)
✔ bus start refuses with exit 3 without a notifier config (89.833801ms)
✔ bus start runs until bus stop; status reports it while it runs (596.240557ms)
✖ bus-service.sh renders the unit and installs it into a given directory (13.023386ms)
✔ zoned uses the IANA zone across DST (25.081139ms)
✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (33.087017ms)
✔ two blocking decisions get two DMs with different nonces (21.832137ms)
✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.313202ms)
✔ a failed DM is journaled, backs off, and is retried until it lands (20.727213ms)
✔ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (26.646629ms)
✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (26.996695ms)
✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (17.003301ms)
✔ polled every POLL_MS against a permanent 403, a DM is sent at 0, 30, 60, 90 and 120 min and gives up only then (154.842007ms)
✔ a restart after the second refusal does not send before that refusal's 30 min are up (73.169397ms)
✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (20.197995ms)
✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (12.140882ms)
✔ an inbox read failure is logged and the next poll retries (0.826196ms)
✔ no Discord id reaches the journal or the log (12.640011ms)
✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (2.577376ms)
✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (2.291009ms)
✔ the journal: a whole file that is one torn line truncates to empty (1.274937ms)
✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (0.572193ms)
✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (0.647559ms)
✔ the journal: a line with a wrong type refuses with exit 3 and names the field (3.528525ms)
✔ the journal: a symlinked directory refuses and says it is a link (0.371869ms)
✔ the journal: a dangling directory link, a parent that is a file and a journal that is a directory each refuse with exit 3 (0.540285ms)
✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (0.444668ms)
✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.517872ms)
✔ digest content stays within Discord's 2000 characters (0.282252ms)
✔ runLoop never overlaps ticks and stops after the one in flight (111.600047ms)
task.close {} answered: invalid-request; fake saw 18 requests, first GET /info 200, GET /projects/1 200, GET /projects/1/views 200
task.close on a missing task answered: task-not-found; it made GET /tasks/999 404
✔ bootConfig trackers reach the S3 adapter in the real broker child, which goes ready against a fake Vikunja (371.436455ms)
✔ the transport writes {business, verb, args} to the child and reads its JSON (64.122569ms)
✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2279.073027ms)
✔ busExit and refuseInsideAgent (0.386801ms)
✔ approvals: a request is validated before anything is posted; the rendering shows names and never ids (5.051726ms)
✔ approvals: the ledger is appended and folded into open requests with bind and approval states (1.451345ms)
✔ approvals: a reply approves only when it points at a request, says exactly approve, and comes from a listed approver once (0.679177ms)
✔ approvals: a button approves only on its own request message with the matching custom id (0.505543ms)
✔ approvals flow: a turn that opened a request posts the message with the button, records it, binds it, and both approvers approve (16.759513ms)
✔ approvals flow: a non-approver, a repeat, a wrong custom id and a service refusal each get their fixed line and a drop entry (6.847581ms)
✔ approvals flow: an invalid request from the model, a refused post, and no api client are recorded and post nothing (7.885012ms)
✔ approvals flow: start retries a bind and an approval left as unknown, under their original keys (2.733518ms)
✔ authorize: open channel, listed user (2.103689ms)
✔ authorize: wrong guild (0.204134ms)
✔ authorize: no guild (DM) (0.250642ms)
✔ authorize: unlisted channel (0.179972ms)
✔ authorize: unknown channel, no info (0.260484ms)
✔ authorize: thread of listed parent (0.444085ms)
✔ authorize: thread of unlisted parent (0.296317ms)
✔ authorize: text channel that is not a thread and not listed (0.141873ms)
✔ authorize: unlisted user (1.233958ms)
✔ authorize: no author (0.509405ms)
✔ authorize: bot author (listed id, bot flag) (0.144304ms)
✔ authorize: system author (0.325929ms)
✔ authorize: the bot itself (0.098491ms)
✔ authorize: webhook (0.66558ms)
✔ authorize: mention channel without mention (0.157236ms)
✔ authorize: mention channel with bot mention (0.138165ms)
✔ authorize: mention channel with @everyone only (0.089143ms)
✔ authorize: mention channel mentioning someone else (0.085157ms)
✔ authorize: mention channel, content says @bot but mentions empty (0.083237ms)
✔ authorize: private thread under mention channel, mentioned (0.094689ms)
✔ authorize: private thread under mention channel, not mentioned (0.266387ms)
✔ authorize: thread in another guild per channel info (0.083028ms)
✔ authorize: not an object (0.07959ms)
✔ authorize: no id (0.070516ms)
✔ authorize: oversize content is accepted and flagged (0.084489ms)
✔ authorize: exactly the limit is not oversize (0.068505ms)
✔ authorize: a user's channel allowlist drops them outside it, threads count as the parent, others are unaffected (0.465465ms)
✔ authorize: order puts wrong guild before user, and user before channel (no channel lookup for strangers) (0.138041ms)
✔ binding: a complete binding validates and is frozen (2.574002ms)
✔ binding: unknown key, missing field, wrong type refuse with exit 2 (1.281521ms)
✔ binding: empty allowlists refuse (0.325095ms)
✔ binding: a user's channel allowlist must be non-empty, listed and unique; absent means every listed channel (1.384415ms)
✔ reloadDiff: reloadable keys are summarised by id; every fixed key refuses with exit 2 (1.623987ms)
✔ binding: dmRecipient is optional, a snowflake, one of the listed users, and a fixed key (1.200867ms)
✔ binding: file must be 0600, regular, not a symlink (1.649798ms)
✔ binding: token file mode, symlink, emptiness and shape are checked; token never appears in errors (2.365167ms)
✔ cli: check refuses a non-0600 token file with exit 2 before any network use (112.716415ms)
✔ context files: absolute paths, traversal, symlinks and out-of-repo targets refuse; in-repo files resolve (2.641304ms)
✔ cli: check refuses a missing context file and a missing binding with exit 2; usage is exit 4 (435.544546ms)
✔ cli: reload validates the file first (exit 2), then needs a live owner (exit 1); usage is exit 4 (202.966592ms)
✔ cli: run refuses when STOP is present, before any network use (151.624562ms)
✔ binding: tools is optional, validated strictly, a fixed key for reload, and its roots are resolved against the data root (1.8529ms)
✔ binding: a git key is validated at load and reaches the extension whole, and only on a writable root (1.386448ms)
✔ delivery: an accepted message is in the inbox before the turn, the reply is chunked with one nonce per chunk, and the turn record is write-once (22.641577ms)
✔ delivery: refused and unknown outcomes are journaled; a later chunk is not sent after a failure (16.801329ms)
✔ delivery: restart with an unknown entry re-sends the same nonce once and reconciles before accepting traffic (2.489508ms)
✔ delivery: an unknown entry older than the dedupe window is marked refused, not re-sent; a still-unknown one refuses start (1.553887ms)
✔ delivery: repeated unknown reconciliations never refresh the dedupe window; the original intent time decides (1.715878ms)
✔ turn: a failed engine turn posts the fixed line, never model output, and writes a failed record (2.415618ms)
✔ turn: a second message during a turn is held by the engine, both get their own reply and record (34.013139ms)
✔ turn: a thread under a listed channel is answered in the thread; an unknown thread is looked up once (3.915224ms)
✔ drop: an unlisted user gets silence and one drop line; no inbox entry, no REST call, no engine call (1.294216ms)
✔ drop: an oversize message is accepted into the inbox, answered with the fixed line and journaled as a drop (1.052537ms)
✔ restart: an inbox with three ids and a replay of the same three produces zero turns (44.144941ms)
✔ stop: STOP present refuses start; STOP written while running refuses new turns and the current one finishes (33.552931ms)
✔ ceiling: the ceiling plus one is refused and journaled; one fixed line per UTC day; a new day accepts again (7.922673ms)
✔ ceiling: a burst arriving while turns are still running cannot queue past the ceiling (7.891341ms)
✔ ceiling: a turn interrupted by a crash still counts after restart; admissions are durable (3.13703ms)
✔ ceiling: the daily notice survives a same-day restart; one delivery attempt in total, even when the first attempt crashed mid-flight (5.04555ms)
✔ duplicate: the same event delivered twice while the thread lookup is held yields one prompt, one admission and one reply (3.47815ms)
✔ journal: no token-shaped string and no model output on the drop path reaches disk (1.089163ms)
✔ receipt: an admitted message gets one eyes reaction on the inbound message; drops and refusals get none; a failed reaction is recorded and does not fail the turn (8.646223ms)
✔ receipt: Discord refusing the reaction leaves the turn intact and records ok false (3.375862ms)
✔ reload: a new user is silent before and answered after; a removed channel goes silent; a lower ceiling applies at once (4.101761ms)
✔ reload: a fixed key refuses with exit 2 and the old binding stays in force (4.56979ms)
✔ tools: with a tools binding the turn record lists every read and its outcome; without one the field is null (8.744825ms)
✔ context: the Discord block names the server, channels and modes, and states the rules from Q15 and Q16 (2.499542ms)
✔ context: with tools the block names the roots, keeps file content as data, and says to state refusals plainly (0.776987ms)
✔ context: a writable root adds the write rules and says a write is real only once Jason commits (1.883161ms)
✔ context: the envelope is one bracketed line then the text; names cannot break the line (1.241348ms)
✔ context: a git root swaps the terminal-commit line for the git verbs, and a vault root adds the id protocol (1.405812ms)
✔ context: assembleContext concatenates files in launcher format and appends the block; sha256 is stable (0.999705ms)
✔ context: splitReply keeps paragraphs together under the limit and splits long ones at lines, spaces, then hard (0.576183ms)
✔ engine: buildPiArgs carries the fixed flags, engine settings, session dir and prompt file (2.819668ms)
✔ engine: with tools, buildPiArgs turns pi's own tools off, loads the extension explicitly and allowlists exactly our three (0.461278ms)
✔ engine: a run with tool turns settles once, on the answer, with every tool call in the result (55.109589ms)
✔ engine: a run that ends on a tool-only turn fails the prompt as empty; a retried run settles on the real end (50.2841ms)
✔ engine: one prompt, one turn, text and usage come back (48.874702ms)
✔ engine: a prompt while streaming is held until pi settles, then sent as its own run, and answered in order (358.597213ms)
✔ engine: a held prompt that times out before pi settles fails on its own and is never sent (234.327562ms)
✔ engine: timeout sends abort and fails only that turn; the process stays (115.980983ms)
✔ engine: tool events from a run that outlived its timeout never land in the next prompt's record (232.999465ms)
✔ engine: a prompt after a turn that timed out before its agent_start waits for pi to settle instead of being refused (127.109157ms)
✔ engine: when pi has not started a timed-out turn by the end of the abort grace, the engine stops pi and fails held prompts (212.630669ms)
✔ engine: a timed-out turn pi starts only after the grace never answers a later prompt (614.344473ms)
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (early prompt response) (1.354236ms)
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (late prompt response) (0.500428ms)
✔ engine: a timed-out run pi did start outlives the grace; the next prompt goes out when it ends (425.782047ms)
✔ engine: a malformed JSONL line fails the turn, not the process (24.521955ms)
✔ engine: a turn that ends in error rejects with the error code; process exit fails pending turns (45.954091ms)
✔ gateway: hello -> identify with intents, ready, heartbeat with jitter, ack (2.575865ms)
✔ gateway: missed ack closes the socket and resumes with the last sequence (1.640094ms)
✔ gateway: op 7 reconnect resumes; op 9 non-resumable re-identifies (0.494401ms)
✔ gateway: op 9 resumable resumes (0.680377ms)
✔ gateway: close 4014 is fatal, reports the missing intent, never reconnects (0.950533ms)
✔ gateway: 4004 and 4013 are fatal too; 1006 reconnects with identify when no session (0.5513ms)
✔ gateway: close() is final and unparseable frames are ignored (0.412734ms)
✔ git: config validation is strict, needs write: true, a work tree and a private token file (79.181806ms)
✔ git: the child environment drops every host git config, names one helper, and carries the token path only for origin (67.333024ms)
✔ git: status reports the branch, ahead/behind and changed paths, and refuses off the named branch or mid-merge (163.8268ms)
✔ git: parseStatus reads porcelain v2 including renames and conflicts (0.339034ms)
✔ git: a commit stages exactly the named files, carries the seat author and the requester trailer, and pushes at once (96.931881ms)
✔ git: commit refusals: message, paths, requester, nothing to commit, and an index that already holds other work (86.40052ms)
✔ git: a commit whose push fails is still a commit, says so, and the next commit's push carries both (D6) (89.861594ms)
✔ git: pull is fast-forward only; a diverged origin or dirty local files refuse with nothing merged (213.438671ms)
✔ git: push pushes the named branch only and reports up to date (79.995959ms)
✔ git: no token value or token path ever reaches a git argument list; outputs are masked and capped (91.684346ms)
✔ git: the credential helper answers get over https from a private file and nothing else (198.939395ms)
✔ git: the vault protocol validates before a commit, honours another owner's lock, reserves ids, and locks around writes (760.532022ms)
✔ lock: the claim is exclusive; a second start against a live owner refuses (4.518779ms)
✔ lock: a stale lock (dead owner, reused pid, or record without start) refuses run and is never signaled; only unlock clears it (94.136648ms)
✔ lock: an incomplete claim (directory without owner record) is busy and refuses run; unlock clears it (1.321714ms)
✔ lock: an owner record that exists but cannot be read is invalid: never signaled, never removed, never claimed over (5.010352ms)
✔ lock: legacy upgrade; a live connector holding a {pid, start} record is unknown, unlock refuses and nothing changes; after it exits, unlock clears it (84.932333ms)
✔ lock: a live pid whose record carries a malformed or noncanonical start or boot string is unknown, not a mismatch; nothing signals, removes, or claims over it (371.359177ms)
✔ lock: identity syntax; only canonical unsigned decimal start ticks and lowercase boot uuids are identities (0.953502ms)
✔ lock: a process whose start marker or boot id cannot be read refuses to claim (25.87851ms)
✔ lock: a live pid whose identity cannot be read right now is unknown: never signaled, never removed, never claimed over (1.08047ms)
✔ lock: four processes racing for the same binding; exactly one claims it and the others refuse (42.520075ms)
✔ lock: stale handoff; concurrent starts over a stale lock all refuse, nothing reclaims, one unlock then exactly one live owner (162.423161ms)
✔ lock: four-party schedule; claims landing inside an unlock's gap never survive, one unlock leaves no owner and no residue (90.616142ms)
✔ notices: a kind is recorded per UTC day and found again (0.478887ms)
✔ notify: the DM channel opens once, every send carries the nonce, and only the message id comes back (5.466472ms)
✔ notify: refusals and unknowns surface as RestOutcome without ids; a refused send reopens the channel next time (2.244007ms)
✔ notify: no dmRecipient, a non-0600 binding or token, or a missing binding refuse before any network use (1.967992ms)
✔ recover: nothing to do is clean; a lock whose owner is gone or that has no record is cleared and STOP ends up absent (55.135659ms)
✔ recover: an operator STOP refuses with exit 3 and is never removed, whatever the lock says (68.081432ms)
✔ recover: a brake written during the unlock wins; STOP stays with both lines and the start is refused (52.742578ms)
✔ recover: a held binding refuses with exit 3 and writes no STOP: live owner, alive pid without verifiable identity, unreadable record (92.661558ms)
✔ cli: recover exits 0 when ready, 3 behind a brake or a held binding, and run's own STOP refusal is 3 (685.712976ms)
✔ rest: createMessage sends nonce, enforce_nonce, empty allowed_mentions and a soft reply reference (2.555387ms)
✔ rest: 429 waits retry_after and retries; 4xx is refused; 5xx and socket errors are unknown (4.983774ms)
✔ rest: content and nonce limits are enforced locally; typing never throws (0.512018ms)
✔ rest: react PUTs the encoded emoji on the inbound message for @me; 2xx is true, anything else is false and never throws (0.572608ms)
✔ rest: createDm posts the recipient and resolves the channel id; 4xx refused, 5xx unknown, bad id never sent (0.682831ms)
✔ setspark config: a bare https or loopback origin, a private key file, a principal (5.690252ms)
✔ setspark config: reaches the tools config and the binding as a fixed key (3.4364ms)
✔ setspark config: the binding's key survives resolveToolRoots and the engine's JSON hand-off to the extension (1.873619ms)
✔ setspark config: approvers come from the binding's users, never from the binding's setspark key (2.484239ms)
✔ setspark verbs: required_approvers go out as discord ids from names and come back as names (26.401741ms)
✔ setspark verbs: no Discord user id reaches tool text, whatever shape the service returns it in (13.780187ms)
✔ setspark contract: a decision made with names opens a request the connector accepts; names stored by an old record still refuse (9.506372ms)
✔ setspark keys: read per call, one printable token per file, rotation without a restart (4.523326ms)
✔ setspark idempotency keys: principal, turn id, call index; connector keys name a step (0.868572ms)
✔ setspark http core: json in and out, bearer header, idempotency header, fixed user agent, no key anywhere else (3.473946ms)
✔ setspark http core: error bodies become fixed refusals with code and the 409 fields; server text is data, cut (1017.080749ms)
✔ setspark verbs: a setspark key enables the eight verbs and no counters (0.417568ms)
✔ setspark verbs: writes carry the turn's key and the asserted requester, reads carry no key, and the api key never appears in text or details (6.467204ms)
✔ setspark verbs: no turn refuses every write before any request; bad arguments refuse before any request; reads still work (1.171844ms)
✔ setspark verbs: renderRecord caps long output and hides the accepted snapshot (0.198172ms)
✔ setspark api: bind, add_approval (button and reply) and get use integer request ids and the connector's keys (2.465489ms)
✔ tools: config refuses a missing, symlinked, dotted, non-directory or duplicate root and bad limits (4.187242ms)
✔ tools: every escape is refused with a fixed reason and nothing outside the root is read (4.395016ms)
✔ tools: happy paths list, read a window, and search case-insensitively; dotfiles and symlinks never appear (6.590029ms)
✔ tools: the tool set renders text for the model, records details for the journal, and enforces the per-run budget (3.883006ms)
✔ tools: listing and search caps hold (13.764368ms)
✔ tools: credential shapes are caught; ordinary prose and ids are not (0.895509ms)
✔ tools: the read uses the checked file itself; a symlink, a swapped file, a FIFO, a grown file or a hard link at read time is refused (6.850724ms)
✔ tools: an unreadable file under the root is skipped by search and refused by read (1.464872ms)
✔ tools: config accepts write: true only as a boolean, and enables the write tools only then (1.848609ms)
✔ tools: every write outside the fence is refused before any byte lands, and no temp file remains (6.002398ms)
✔ tools: write_file leaves the exact bytes, edit_file replaces one exact match, and the set renders the change as uncommitted (3.244872ms)
✔ tools: a target that changed between the check and the rename is refused and the temp file is removed (1.849368ms)
✔ web: config takes an https or loopback-http SearXNG base url and a bounded fetch cap (3.865861ms)
✔ web: address rules refuse every private, loopback, link-local, mapped and multicast form (2.229826ms)
✔ web: web_fetch refuses bad urls, private hosts, rebinding names, non-https redirects, too many hops, error status, non-text bodies, and times out (1032.551897ms)
✔ web: web_fetch returns html as text with the title, follows an https redirect, keeps plain text and json, and cuts at the cap (4.2694ms)
✔ web: html to text drops scripts, styles and comments, decodes entities and keeps block breaks (0.235987ms)
✔ web: web_search asks the instance for json, returns at most ten clean results, and refuses a bad query, a down instance or an unusable answer (2.621124ms)
✔ web: the tool set enables the web tools only with a web key, counts them in the budget, and records url, status and hits (2.199604ms)
ℹ tests 244
ℹ suites 0
ℹ pass 243
ℹ fail 1
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 3152.644118

✖ failing tests:

test at packages/cli/tests/host.test.mjs:396:1
✖ bus-service.sh renders the unit and installs it into a given directory (13.023386ms)
  AssertionError [ERR_ASSERTION]: The input did not match the regular expression /^ {2}mkdir -m 0700 -p <dataRoot>\/notify\/<business> +the notifier refuses a looser directory$/m. Input:
  
  'written: /mnt/storage/scratch/tmp/mosaic-cli-L14dvV/mosaic-bus@.service\n' +
    'next, for one business (one per data root):\n' +
    '  write <dataRoot>/notify/<business>/notify.json, mode 0600:\n' +
    '    {"notifyVersion": 1, "binding": "<discord binding>"}   or "binding": null for no DMs\n' +
    '  systemctl --user enable --now mosaic-bus@<business>      start now and at login\n' +
    '  systemctl --user status mosaic-bus@<business>\n' +
    "  journalctl --user -u mosaic-bus@<business> -f             the host's log\n" +
    '  systemctl --user stop mosaic-bus@<business>               SIGTERM; restartable\n' +
    'survive logout and reboot only with lingering on: loginctl enable-linger jwoltje\n'
  
      at TestContext.<anonymous> (file:///mnt/storage/scratch/rocko-r45/tree/packages/cli/tests/host.test.mjs:409:10)
      at Test.runInAsyncScope (node:async_hooks:226:14)
      at Test.run (node:internal/test_runner/test:1402:25)
      at Test.processPendingSubtests (node:internal/test_runner/test:974:18)
      at Test.postRun (node:internal/test_runner/test:1542:19)
      at Test.run (node:internal/test_runner/test:1467:12)
      at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
      at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
    generatedMessage: true,
    code: 'ERR_ASSERTION',
    actual: `written: /mnt/storage/scratch/tmp/mosaic-cli-L14dvV/mosaic-bus@.service\nnext, for one business (one per data root):\n  write <dataRoot>/notify/<business>/notify.json, mode 0600:\n    {"notifyVersion": 1, "binding": "<discord binding>"}   or "binding": null for no DMs\n  systemctl --user enable --now mosaic-bus@<business>      start now and at login\n  systemctl --user status mosaic-bus@<business>\n  journalctl --user -u mosaic-bus@<business> -f             the host's log\n  systemctl --user stop mosaic-bus@<business>               SIGTERM; restartable\nsurvive logout and reboot only with lingering on: loginctl enable-linger jwoltje\n`,
    expected: /^ {2}mkdir -m 0700 -p <dataRoot>\/notify\/<business> +the notifier refuses a looser directory$/m,
    operator: 'match',
    diff: 'simple'
  }
