Skip to content

Commit a81eb6c

Browse files
committed
test(cli): pin the seed-settled ipc message, its ordering, and the banner rider
64 assertions across three files: the message reaches process.send and stays silent without a channel; the latch cannot let it overtake `objectstack:listening` and cannot send twice; the over-budget path withholds then speaks; both suppressed modes announce with their reason rather than hanging; a kernel with no seed pipeline counts as settled; `os dev` relays verbatim and claims only its own message; and the banner's over-budget row, with the ablation showing the same boot without the reading is the byte-identical no-seeds transcript. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TSf4DV7ziu4V5j73e46b7c
1 parent 7dd12ec commit a81eb6c

3 files changed

Lines changed: 535 additions & 0 deletions

File tree

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
import { describe, it, expect } from 'vitest';
4+
import { forwardSeedSettledToParent } from './dev.js';
5+
6+
/**
7+
* #17329 — `os dev` relays the `serve` child's settle announcement to its OWN
8+
* parent, and does nothing at all when no parent holds the channel.
9+
*
10+
* ## Why the hop is the card
11+
*
12+
* The producer already exists and is published: `@objectstack/runtime` declares
13+
* every seed source and settles it at the moment its boot-time write is done,
14+
* under the spec's `seed-settlement` contract. `serve` now announces that on the
15+
* ipc channel. But the consumer — a demo script, a test harness, anything that
16+
* spawns a dev server and wants to print one line after the boot — spawns
17+
* `os dev`, not `serve`; `os dev` runs the child over
18+
* `stdio: ['inherit','inherit','inherit','ipc']`, so without this the message
19+
* lands in the middle process and stops. One hop is the whole of what was
20+
* missing.
21+
*
22+
* ⚠️ Under vitest's `forks` pool `process.send` is the RUNNER's own control
23+
* channel. Every swap below is synchronous, spans one call, and is undone in
24+
* `finally` — a real message must never reach it.
25+
*/
26+
describe('#17329 `os dev` forwards `objectstack:seed-settled` outward', () => {
27+
/** Drive `fn` with `process.send` replaced by a recorder. */
28+
const recording = (fn: () => void): unknown[] => {
29+
const sent: unknown[] = [];
30+
const prior = process.send;
31+
(process as { send?: unknown }).send = (m: unknown) => { sent.push(m); return true; };
32+
try { fn(); } finally { (process as { send?: unknown }).send = prior; }
33+
return sent;
34+
};
35+
36+
/** Drive `fn` with NO ipc channel — the ordinary terminal `os dev`. */
37+
const withoutChannel = <T>(fn: () => T): T => {
38+
const prior = process.send;
39+
(process as { send?: unknown }).send = undefined;
40+
try { return fn(); } finally { (process as { send?: unknown }).send = prior; }
41+
};
42+
43+
const settled = {
44+
type: 'objectstack:seed-settled',
45+
ok: false,
46+
suppressed: [],
47+
sources: [{ source: 'showcase', inserted: 24, updated: 0, skipped: 0, rejected: 14 }],
48+
};
49+
50+
it('relays the message VERBATIM, not a re-derivation of it', () => {
51+
// ⛔ This process has no kernel and could only guess. Passing the object
52+
// through is what keeps `os dev`'s parent and the `serve` child from being
53+
// made to say two different things about one boot.
54+
const sent = recording(() => {
55+
expect(forwardSeedSettledToParent(settled)).toBe(true);
56+
});
57+
expect(sent).toEqual([settled]);
58+
expect(sent[0], 'the message was rebuilt rather than relayed').toBe(settled);
59+
});
60+
61+
it('⛔ a parent with no ipc channel is UNAFFECTED — no throw, no send', () => {
62+
// An ipc channel must not become a requirement of running a published
63+
// command. `process.send` is undefined under a terminal `os dev`.
64+
withoutChannel(() => {
65+
expect(() => forwardSeedSettledToParent(settled)).not.toThrow();
66+
expect(forwardSeedSettledToParent(settled), 'the message is still HANDLED here').toBe(true);
67+
});
68+
});
69+
70+
it('survives a parent channel that has already closed', () => {
71+
// Best-effort, exactly like the child's own `announceListening`: a
72+
// supervision nicety must never take a healthy dev server down.
73+
const prior = process.send;
74+
(process as { send?: unknown }).send = () => { throw new Error('channel closed'); };
75+
try {
76+
expect(() => forwardSeedSettledToParent(settled)).not.toThrow();
77+
} finally {
78+
(process as { send?: unknown }).send = prior;
79+
}
80+
});
81+
82+
describe('⛔ and it claims ONLY its own message', () => {
83+
it.each([
84+
['the listening announcement', { type: 'objectstack:listening', port: 3001, url: 'http://localhost:3001' }],
85+
['an unrelated type', { type: 'something:else' }],
86+
['no type at all', { port: 3001 }],
87+
['null', null],
88+
['undefined', undefined],
89+
['a string', 'objectstack:seed-settled'],
90+
])('%s is left to the caller', (_label, msg) => {
91+
// Returning `true` here would swallow `objectstack:listening` and take
92+
// the bound-port readout and the MCP connect hint down with it.
93+
const sent = recording(() => {
94+
expect(forwardSeedSettledToParent(msg)).toBe(false);
95+
});
96+
expect(sent, 'a message that is not ours was forwarded anyway').toEqual([]);
97+
});
98+
99+
it('…and the positive control on the same path still fires', () => {
100+
// So the zeros above are readings rather than a function that forwards
101+
// nothing at all.
102+
const sent = recording(() => { forwardSeedSettledToParent(settled); });
103+
expect(sent).toHaveLength(1);
104+
});
105+
});
106+
});

0 commit comments

Comments
 (0)