diff --git a/packages/bus/src/broker.mjs b/packages/bus/src/broker.mjs index d5e6f2bc3..b18965feb 100644 --- a/packages/bus/src/broker.mjs +++ b/packages/bus/src/broker.mjs @@ -1848,6 +1848,11 @@ export class Broker { if (a.class === 'BLOCK') this.#checkBlock(s, to, a.in_reply_to); if (a.decision) this.#decision(s, a.decision); const thread = this.#threadFor(s, to, a.in_reply_to); + // Row 170 (#1667): a tester's message to a role nobody holds is refused before it is + // stored, so a PM test never waits for the role's next run. A run launched but not yet + // claimed counts as no holder, as in #waiting. No lock: a holder that releases once the + // message is routed leaves it like any other message it held. + if (s.tester && !this.#recipient(s.business, to, thread)) fail('tester-target-not-running'); const mid = randomUUID(); this.#store.run( 'INSERT INTO messages(id,at,business,from_role,from_run,to_role,to_name,class,in_reply_to,decision,corrects,body) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)', @@ -2489,7 +2494,8 @@ export class Broker { // told and whether launch.by itself has a holder. A run launched but not yet claimed counts as // no holder; the instance-running refusal stops a second launch. launch.by isn't told of a // message to it, of its own message (this result tells it) or of a tester's (a PM test - // authorizes nothing). Row 152 (#1646): launchable says whether the target is one of + // authorizes nothing); since row 170 (#1667) message.send refuses a tester's message to a role + // nobody holds before it gets here. Row 152 (#1646): launchable says whether the target is one of // launch.instances, so the sender learns at once that no launch can follow. launch.by is // still told: the Console shows the notice as the reply to the human's message. #waiting(s, mid, to, thread) { diff --git a/packages/cli/README.md b/packages/cli/README.md index fb3ad8350..3ed8916d0 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -127,8 +127,8 @@ mosaic launches list [--json] when the bus told `launch.by`; `pm isn't running, and the business names no launch.by` with no launch block; `pm isn't running and isn't in the launch block, so chief can't launch it` when the role isn't one of - `launch.instances` (row 152, #1646); `pm isn't running; a tester's message - tells nobody to launch it` for `--tester`. When `launch.by` itself isn't + `launch.instances` (row 152, #1646). A `--tester` message to such a role is + refused instead (see `--tester` below). When `launch.by` itself isn't running, talk prints ` isn't running` (or `isn't running either` after the told line, but not after the launch-block line) and the command that starts it: @@ -168,8 +168,12 @@ mosaic launches list [--json] (`tester-off`), each exit 3. Like the human commands, `--tester` works only from the tree the bus runs from ("One pinned copy" below). `talk --tester` text must start with - `[mos test]`, else exit 2 before any call. The role's reply comes back to - the tester, not to the human's inbox. `decide`, `stop`, `launches` and + `[mos test]`, else exit 2 before any call. When no run holds the role + (row 170, #1667), the bus refuses the message with + `tester-target-not-running` and stores nothing, and talk prints `pm isn't + running, so the tester's message was refused and nothing was stored` and + exits 3. A run launched but not yet claimed counts as not running. The + role's reply comes back to the tester, not to the human's inbox. `decide`, `stop`, `launches` and `inbox --read` or `--read-all` take no `--tester` (exit 4). ## The app diff --git a/packages/cli/src/cli.mjs b/packages/cli/src/cli.mjs index 6a4f68be6..35a70a5d3 100644 --- a/packages/cli/src/cli.mjs +++ b/packages/cli/src/cli.mjs @@ -339,8 +339,6 @@ function waitingLines(w, business) { // Row 152 (#1646): launch.by can launch only launch.instances. An older bus sends no // launchable, so only an explicit false prints this line. if (w.launchable === false) return `${w.role} isn't running and isn't in the launch block, so ${w.launcher} can't launch it\n`; - // A human's message to another role always tells launch.by; a tester's never does. - if (!w.told) return `${w.role} isn't running; a tester's message tells nobody to launch it\n`; return `${w.role} isn't running, ${w.launcher} has been told\n` + (w.launcherRunning ? "" : restart(w.launcher, " either")); } @@ -383,6 +381,11 @@ async function talk(rest, io, deps, receiveOnly = false) { if (e.code === "thread-required-use-thread-send") { throw new CliError(`${to} runs in threads, so a message to it names one: mosaic talk --thread ; mosaic threads lists yours`, 2); } + // Row 170 (#1667): the bus stores no tester message to a role nobody holds. Exit 3 like the + // other tester refusals; transport.mjs's EXIT map stays as row 164 pinned it. + if (e.code === "tester-target-not-running" && parsed.flags.has("--tester")) { + throw new CliError(`${to} isn't running, so the tester's message was refused and nothing was stored`, 3); + } throw e; } io.stdout.write(`sent ${shortId(sent.id)} to ${to}\n`);