bus, cli: a message to a role launch.by can't launch says so at once (row 152, #1646)
Row 152 round 1, Dewey's candidate on56639c99b. Filbert approved (comment 28741). The bus marks a waiting message launchable or not; when the target isn't in the launch block, launch.by is still told, with a notice saying the launch would be refused, and talk prints that launch.by can't launch the role. Sage's landing gate ran on5f57d85e5plus the candidate: 25 steps, all rc 0. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
ccfcc31512
commit
26a0b0fab4
7 files changed
+89
-13
No files matched your search
+4
-1
@@ -265,7 +265,10 @@ crashed host left as `host-lost` and refuses with exit 3 on a
|
||||
`sessions.json` it can't read. `talk` sends a REQUEST and prints only the
|
||||
reply to it, marking only that read (polls every 2 s, `--wait` default 900;
|
||||
`0` only sends); other messages stay unread for `mosaic inbox`, and
|
||||
`--follow` and `follow` print and read everything. `stop` sends SIGTERM to
|
||||
`--follow` and `follow` print and read everything. When no run holds the
|
||||
role, a line after `sent` says whether `launch.by` was told, or that the role
|
||||
isn't in the launch block and `launch.by` can't launch it (row 152); the cli
|
||||
README, "Human commands", lists the lines. `stop` sends SIGTERM to
|
||||
a run's runner after checking it in
|
||||
`sessions.json`. `launches off|on` revokes and restores `role.launch` for
|
||||
the business; running sessions keep running. `launches list` reads
|
||||
|
||||
+17
-3
@@ -380,6 +380,17 @@ any other, so a stopped `launch.by` gets it at its next claim. The body is
|
||||
Message <id> from <sender> to <role> waits: <role> has no running session. Launch <role> within the launch block's limits, or tell <sender> why not with message.send in_reply_to <id>.
|
||||
```
|
||||
|
||||
When the role isn't one of `launch.instances` (row 152, #1646), `launch.by`
|
||||
can't launch it, and the body says so:
|
||||
|
||||
```
|
||||
Message <id> from <sender> to <role> waits: <role> has no running session and isn't in the launch block's instances, so a launch of <role> is refused. Tell <sender> why not with message.send in_reply_to <id>.
|
||||
```
|
||||
|
||||
`launch.by` is still told in that case. The message stays stored and reaches
|
||||
the role at a later claim, and the Console shows the notice as a reply to the
|
||||
human's message; it doesn't read the `waiting` field.
|
||||
|
||||
`_bus` can't be a role: an instance name matches the business `ID_PATTERN`,
|
||||
which starts with `[a-z0-9]`. So `message.send` to `_bus` refuses
|
||||
`unknown-role`, and a caller can't set `from_role` or `from_run` (unknown keys
|
||||
@@ -388,9 +399,12 @@ automatic RESULT reply to every other class, and the bus would refuse a reply
|
||||
to `_bus`; the runner would log it and skip the `message.read`. On a RESULT
|
||||
the runner takes one turn, sends nothing and marks it read.
|
||||
|
||||
The result's `waiting` is `{role,launcher,told,launcherRunning}`: the target,
|
||||
`launch.by` (`null` with no launch block), whether the notice went out, and
|
||||
whether a run holds `launch.by` (`null` with no launch block). No `waiting`
|
||||
The result's `waiting` is `{role,launcher,told,launcherRunning,launchable}`:
|
||||
the target, `launch.by` (`null` with no launch block), whether the notice went
|
||||
out, whether a run holds `launch.by` (`null` with no launch block), and whether
|
||||
the target is one of `launch.instances` (`null` with no launch block; `false`
|
||||
for `launch.by` itself, which `instances` can't list). `launchable` came with
|
||||
row 152; a reader that ignores it sees row 146's shape. No `waiting`
|
||||
field means a run held the target, or the message went to the human, the
|
||||
tester, or a thread instance inside its thread (row 121's thread launcher starts
|
||||
that session). `told` is `false` and no notice goes out for a message to
|
||||
|
||||
@@ -1897,20 +1897,24 @@ 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).
|
||||
// authorizes nothing). 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) {
|
||||
if (to === 'human' || to === 'tester' || (thread && this.#threaded(s.business, to))) return null;
|
||||
if (this.#recipient(s.business, to, thread)) return null;
|
||||
const by = this.#business(s.business).launch?.by ?? null;
|
||||
const launch = this.#business(s.business).launch;
|
||||
const by = launch?.by ?? null;
|
||||
const launchable = launch ? launch.instances.includes(to) : null;
|
||||
const sender = this.#sender(s);
|
||||
const told = by !== null && to !== by && sender !== by && sender !== 'tester';
|
||||
if (told) this.#notice(s.business, mid, by, to, sender);
|
||||
return { role: to, launcher: by, told, launcherRunning: by === null ? null : this.#recipient(s.business, by, null) !== null };
|
||||
if (told) this.#notice(s.business, mid, by, to, sender, launchable);
|
||||
return { role: to, launcher: by, told, launcherRunning: by === null ? null : this.#recipient(s.business, by, null) !== null, launchable };
|
||||
}
|
||||
// One notice per waiting message: only message.send calls this, once for the message it stores, so
|
||||
// a re-route at a claim or a broker restart never sends another. Its class is RESULT: the runner
|
||||
// takes a turn on it and sends no automatic reply, which NOTICE_ROLE could not receive.
|
||||
#notice(business, mid, by, to, sender) {
|
||||
#notice(business, mid, by, to, sender, launchable) {
|
||||
const nid = randomUUID();
|
||||
this.#store.run(
|
||||
'INSERT INTO messages(id,at,business,from_role,from_run,to_role,class,in_reply_to,decision,corrects,body) VALUES(?,?,?,?,?,?,?,?,?,?,?)',
|
||||
@@ -1924,7 +1928,9 @@ export class Broker {
|
||||
mid,
|
||||
null,
|
||||
null,
|
||||
`Message ${mid} from ${sender} to ${to} waits: ${to} has no running session. Launch ${to} within the launch block's limits, or tell ${sender} why not with message.send in_reply_to ${mid}.`,
|
||||
launchable
|
||||
? `Message ${mid} from ${sender} to ${to} waits: ${to} has no running session. Launch ${to} within the launch block's limits, or tell ${sender} why not with message.send in_reply_to ${mid}.`
|
||||
: `Message ${mid} from ${sender} to ${to} waits: ${to} has no running session and isn't in the launch block's instances, so a launch of ${to} is refused. Tell ${sender} why not with message.send in_reply_to ${mid}.`,
|
||||
);
|
||||
this.#route(business, nid, by, null);
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ function setup(t, businesses = demo()) {
|
||||
test('a message to a stopped role sends launch.by one notice, and the result says so', (t) => {
|
||||
const ctx = setup(t);
|
||||
const sent = ctx.call('human', 'message.send', { to: 'pm', body: 'is the plan current?', class: 'REQUEST' });
|
||||
assert.deepEqual(sent.waiting, { role: 'pm', launcher: 'chief', told: true, launcherRunning: false });
|
||||
assert.deepEqual(sent.waiting, { role: 'pm', launcher: 'chief', told: true, launcherRunning: false, launchable: true });
|
||||
const [n, ...more] = ctx.notices();
|
||||
assert.deepEqual(more, []);
|
||||
assert.equal(n.from_run, 'bus');
|
||||
@@ -90,6 +90,7 @@ test('a message to a stopped role sends launch.by one notice, and the result say
|
||||
launcher: 'chief',
|
||||
told: true,
|
||||
launcherRunning: true,
|
||||
launchable: true,
|
||||
});
|
||||
assert.equal(ctx.notices().length, 2);
|
||||
// Row 151 (#1645): the notice is routed to launch.by's holder, which the Console's waiting lists read.
|
||||
@@ -106,6 +107,31 @@ test('a message to a stopped role sends launch.by one notice, and the result say
|
||||
assert.match(ctx.notices().at(-1).body, / from coder to pm waits: /);
|
||||
});
|
||||
|
||||
// Row 152 (#1646): a target outside launch.instances. The result says so at once, the message is
|
||||
// stored for a later claim, and launch.by is still told, in words that say no launch can follow.
|
||||
test('a message to a role outside launch.instances says so in its result and in the notice', (t) => {
|
||||
const ctx = setup(t, demo({ launch: { by: 'chief', instances: ['pm'], models: {}, limits: {} } }));
|
||||
const sent = ctx.call('human', 'message.send', { to: 'coder', body: 'can you take this?', class: 'REQUEST' });
|
||||
assert.deepEqual(sent.waiting, { role: 'coder', launcher: 'chief', told: true, launcherRunning: false, launchable: false });
|
||||
assert.equal(ctx.store().get('SELECT to_role FROM messages WHERE id=?', sent.id).to_role, 'coder');
|
||||
const [n, ...more] = ctx.notices();
|
||||
assert.deepEqual(more, []);
|
||||
assert.equal(n.to_role, 'chief');
|
||||
assert.equal(n.in_reply_to, sent.id);
|
||||
assert.equal(
|
||||
n.body,
|
||||
`Message ${sent.id} from human to coder waits: coder has no running session and isn't in the launch block's instances, so a launch of coder is refused. Tell human why not with message.send in_reply_to ${sent.id}.`,
|
||||
);
|
||||
// The same business's launchable target keeps row 146's text.
|
||||
assert.equal(ctx.call('human', 'message.send', { to: 'pm', body: 'and you?' }).waiting.launchable, true);
|
||||
assert.match(ctx.notices().at(-1).body, / waits: pm has no running session\. Launch pm within /);
|
||||
// A later claim still delivers the stored message.
|
||||
ctx.launch('coder', 'coder');
|
||||
ctx.claim('coder');
|
||||
assert.deepEqual(ctx.call('coder', 'message.receive').map((m) => m.id), [sent.id]);
|
||||
assert.equal(ctx.notices().length, 2);
|
||||
});
|
||||
|
||||
test('a message to a running role sends no notice and its result is unchanged', (t) => {
|
||||
const ctx = setup(t);
|
||||
ctx.launch('pm', 'pm');
|
||||
@@ -149,6 +175,7 @@ test('no notice for a message to launch.by, from it, from a tester, to the human
|
||||
launcher: 'chief',
|
||||
told: false,
|
||||
launcherRunning: false,
|
||||
launchable: false,
|
||||
});
|
||||
// From launch.by: the result tells it.
|
||||
ctx.launch('chief', 'chief');
|
||||
@@ -158,6 +185,7 @@ test('no notice for a message to launch.by, from it, from a tester, to the human
|
||||
launcher: 'chief',
|
||||
told: false,
|
||||
launcherRunning: true,
|
||||
launchable: true,
|
||||
});
|
||||
// From a tester: a PM test authorizes nothing, so it wakes no launcher.
|
||||
ctx.caps.tester = ctx.b.bindTester({ business: 'demo' });
|
||||
@@ -186,6 +214,7 @@ test('a business with no launch block tells nobody and says there is no launcher
|
||||
launcher: null,
|
||||
told: false,
|
||||
launcherRunning: null,
|
||||
launchable: null,
|
||||
});
|
||||
assert.deepEqual(ctx.notices(), []);
|
||||
});
|
||||
|
||||
@@ -69,10 +69,13 @@ mosaic launches list [--json]
|
||||
`mosaic inbox` shows it. When no run holds the role (row 146, #1640),
|
||||
a line after `sent` says so: `pm isn't running, chief has been told`
|
||||
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; a tester's message
|
||||
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
|
||||
running, talk prints `<launch.by> isn't running` (or `isn't running
|
||||
either` after the told line) and the command that starts it:
|
||||
either` after the told line, but not after the launch-block line) and the
|
||||
command that starts it:
|
||||
`systemctl --user restart mosaic-bus@<business>`, which starts it only if
|
||||
the unit runs with `--pm` (`scripts/bus-service.sh status <business>`
|
||||
shows `pm: on`) and stops running sessions. A hand-started host takes
|
||||
|
||||
@@ -221,6 +221,9 @@ function waitingLines(w, business) {
|
||||
`${role} isn't running${either}. systemctl --user restart mosaic-bus@${business()} starts it if the unit runs with --pm (scripts/bus-service.sh status ${business()} shows pm: on). The restart stops running sessions.\n`;
|
||||
if (w.launcher === null) return `${w.role} isn't running, and the business names no launch.by\n`;
|
||||
if (w.role === w.launcher) return restart(w.role);
|
||||
// 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"));
|
||||
|
||||
@@ -118,6 +118,24 @@ test("talk says when the role isn't running, whether launch.by was told, and how
|
||||
assert.equal(await text(s, "coder"), "");
|
||||
});
|
||||
|
||||
// Row 152 (#1646): a role outside launch.instances. talk says so instead of "has been told", and
|
||||
// prints no restart line for launch.by, whose start wouldn't launch the role.
|
||||
test("talk to a role launch.by can't launch says it isn't in the launch block", async (t) => {
|
||||
const roles = { ...BUSINESSES.demo.roles, qa: { authority: { withinRole: ["message.send"], crossRole: [] } } };
|
||||
const s = setup(t, {}, { demo: { ...BUSINESSES.demo, roles, launch: { by: "pm", instances: ["cto", "coder"], models: {}, limits: {} } } });
|
||||
const text = async (role) => {
|
||||
const r = await s.run(["talk", role, "hello", "--wait", "0", "--business", "demo"]);
|
||||
assert.equal(r.code, 0, r.error);
|
||||
return r.io.out.text.replace(/^sent [0-9a-f]{8} to \S+\n/, "");
|
||||
};
|
||||
assert.equal(await text("qa"), "qa isn't running and isn't in the launch block, so pm can't launch it\n");
|
||||
assert.equal(await text("cto"), "cto isn't running, pm has been told\n" + `pm isn't running either. systemctl --user restart mosaic-bus@demo starts it if the unit runs with --pm (scripts/bus-service.sh status demo shows pm: on). The restart stops running sessions.\n`);
|
||||
const notices = s.store.all("SELECT body FROM messages WHERE from_role='_bus' ORDER BY seq").map((m) => m.body);
|
||||
assert.equal(notices.length, 2, "pm is told of both");
|
||||
assert.match(notices[0], / waits: qa has no running session and isn't in the launch block's instances, so a launch of qa is refused\. /);
|
||||
assert.match(notices[1], / waits: cto has no running session\. Launch cto within /);
|
||||
});
|
||||
|
||||
// Row 151 (#1645): rows 146 and 142 in one talk. The waiting line prints once after "sent", a
|
||||
// message that isn't the reply arrives first and stays unread, then the reply prints and talk exits 0.
|
||||
test("talk to a stopped role prints the waiting line once, skips a non-reply and prints the reply", async (t) => {
|
||||
|
||||
Reference in new issue
Block a user