@crouton-kit/crouter 0.3.21 → 0.3.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/canvas-history/read.d.ts +1 -0
- package/dist/commands/canvas-history/read.js +61 -0
- package/dist/commands/canvas-history/search.d.ts +1 -0
- package/dist/commands/canvas-history/search.js +270 -0
- package/dist/commands/canvas-history/show.d.ts +1 -0
- package/dist/commands/canvas-history/show.js +79 -0
- package/dist/commands/canvas-history.d.ts +2 -0
- package/dist/commands/canvas-history.js +33 -0
- package/dist/commands/canvas.js +3 -2
- package/dist/commands/memory/find.js +7 -40
- package/dist/commands/memory/lint.js +19 -34
- package/dist/commands/memory/list.js +8 -18
- package/dist/commands/memory/read.js +4 -37
- package/dist/commands/pkg/bridge.d.ts +1 -0
- package/dist/commands/pkg/bridge.js +137 -0
- package/dist/commands/pkg/plugin-inspect.js +7 -6
- package/dist/commands/pkg.js +2 -1
- package/dist/commands/skill/author.js +35 -44
- package/dist/commands/skill/shared.d.ts +1 -5
- package/dist/commands/skill/shared.js +9 -63
- package/dist/commands/skill.js +5 -6
- package/dist/commands/sys/doctor.js +6 -144
- package/dist/core/__tests__/broker-dormant-wake.test.js +71 -34
- package/dist/core/__tests__/canvas-inbox-watcher-hold.test.js +21 -5
- package/dist/core/__tests__/canvas-inbox-watcher.test.js +21 -5
- package/dist/core/__tests__/child-death-wake.test.js +133 -185
- package/dist/core/__tests__/daemon-liveness.test.js +28 -258
- package/dist/core/__tests__/flagship-lifecycle.test.js +156 -135
- package/dist/core/__tests__/frame-decoder-perf.test.js +51 -25
- package/dist/core/__tests__/{broker-attach-limits.test.js → full/broker-attach-limits.test.js} +62 -35
- package/dist/core/__tests__/{broker-attach-stream.test.js → full/broker-attach-stream.test.js} +54 -31
- package/dist/core/__tests__/full/broker-crash-teardown.test.js +143 -0
- package/dist/core/__tests__/full/broker-dialogs.test.js +153 -0
- package/dist/core/__tests__/full/broker-lifecycle.test.js +116 -0
- package/dist/core/__tests__/{cascade-close.test.js → full/cascade-close.test.js} +1 -1
- package/dist/core/__tests__/full/daemon-liveness-pane.full.test.js +292 -0
- package/dist/core/__tests__/{dead-pane-regression.test.js → full/dead-pane-regression.test.js} +2 -2
- package/dist/core/__tests__/{detach-focus.test.js → full/detach-focus.test.js} +5 -5
- package/dist/core/__tests__/{human-new-window-regression.test.js → full/human-new-window-regression.test.js} +2 -2
- package/dist/core/__tests__/{placement-focus.test.js → full/placement-focus.test.js} +5 -5
- package/dist/core/__tests__/{placement-reconcile.test.js → full/placement-reconcile.test.js} +3 -3
- package/dist/core/__tests__/{placement-revive.test.js → full/placement-revive.test.js} +5 -5
- package/dist/core/__tests__/{placement-teardown.test.js → full/placement-teardown.test.js} +4 -4
- package/dist/core/__tests__/{review-render-pane-regression.test.js → full/review-render-pane-regression.test.js} +2 -2
- package/dist/core/__tests__/full/spike-harness.test.js +117 -0
- package/dist/core/__tests__/grace-clock.test.js +72 -75
- package/dist/core/__tests__/helpers/harness.d.ts +35 -1
- package/dist/core/__tests__/helpers/harness.js +70 -12
- package/dist/core/__tests__/live-mutation-verbs.test.js +99 -104
- package/dist/core/__tests__/live-mutation.test.js +111 -134
- package/dist/core/__tests__/revive.test.js +96 -86
- package/dist/core/__tests__/subscription-delivery.test.js +116 -138
- package/dist/core/__tests__/wake-origin.test.js +54 -50
- package/dist/core/bridge-map.d.ts +19 -0
- package/dist/core/bridge-map.js +73 -0
- package/dist/core/canvas/history.d.ts +91 -0
- package/dist/core/canvas/history.js +389 -0
- package/dist/core/canvas/index.d.ts +1 -0
- package/dist/core/canvas/index.js +1 -0
- package/dist/core/config.js +3 -22
- package/dist/core/memory-resolver.d.ts +9 -2
- package/dist/core/memory-resolver.js +47 -7
- package/dist/core/resolver.d.ts +1 -30
- package/dist/core/resolver.js +6 -546
- package/dist/core/runtime/persona.js +5 -7
- package/dist/core/scope.d.ts +7 -5
- package/dist/core/scope.js +16 -17
- package/dist/core/substrate/ceiling.d.ts +28 -0
- package/dist/core/substrate/ceiling.js +87 -0
- package/dist/core/substrate/index.d.ts +2 -0
- package/dist/core/substrate/index.js +1 -0
- package/dist/core/substrate/on-read.js +18 -2
- package/dist/core/substrate/render.d.ts +2 -3
- package/dist/core/substrate/render.js +63 -51
- package/dist/pi-extensions/canvas-inbox-watcher.js +19 -2
- package/dist/types.d.ts +3 -18
- package/dist/types.js +0 -8
- package/package.json +4 -3
- package/dist/builtin-skills/.crouter-plugin/plugin.json +0 -5
- package/dist/builtin-skills/skills/crouter-development/marketplaces/SKILL.md +0 -157
- package/dist/builtin-skills/skills/crouter-development/personas/SKILL.md +0 -106
- package/dist/builtin-skills/skills/crouter-development/personas/base-prompt/SKILL.md +0 -49
- package/dist/builtin-skills/skills/crouter-development/personas/orchestrator-prompt/SKILL.md +0 -49
- package/dist/builtin-skills/skills/crouter-development/plugins/SKILL.md +0 -156
- package/dist/builtin-skills/skills/design/SKILL.md +0 -51
- package/dist/builtin-skills/skills/development/SKILL.md +0 -109
- package/dist/builtin-skills/skills/planning/SKILL.md +0 -59
- package/dist/builtin-skills/skills/spec/SKILL.md +0 -83
- package/dist/commands/skill/state.d.ts +0 -3
- package/dist/commands/skill/state.js +0 -71
- package/dist/core/__tests__/broker-crash-teardown.test.js +0 -116
- package/dist/core/__tests__/broker-dialogs.test.js +0 -126
- package/dist/core/__tests__/broker-lifecycle.test.js +0 -87
- package/dist/core/__tests__/resolver.test.js +0 -181
- package/dist/core/__tests__/spike-harness.test.js +0 -242
- /package/dist/core/__tests__/{broker-attach-limits.test.d.ts → full/broker-attach-limits.test.d.ts} +0 -0
- /package/dist/core/__tests__/{broker-attach-stream.test.d.ts → full/broker-attach-stream.test.d.ts} +0 -0
- /package/dist/core/__tests__/{broker-crash-teardown.test.d.ts → full/broker-crash-teardown.test.d.ts} +0 -0
- /package/dist/core/__tests__/{broker-dialogs.test.d.ts → full/broker-dialogs.test.d.ts} +0 -0
- /package/dist/core/__tests__/{broker-lifecycle.test.d.ts → full/broker-lifecycle.test.d.ts} +0 -0
- /package/dist/core/__tests__/{cascade-close.test.d.ts → full/cascade-close.test.d.ts} +0 -0
- /package/dist/core/__tests__/{dead-pane-regression.test.d.ts → full/daemon-liveness-pane.full.test.d.ts} +0 -0
- /package/dist/core/__tests__/{detach-focus.test.d.ts → full/dead-pane-regression.test.d.ts} +0 -0
- /package/dist/core/__tests__/{human-new-window-regression.test.d.ts → full/detach-focus.test.d.ts} +0 -0
- /package/dist/core/__tests__/{placement-focus.test.d.ts → full/human-new-window-regression.test.d.ts} +0 -0
- /package/dist/core/__tests__/{placement-reconcile.test.d.ts → full/placement-focus.test.d.ts} +0 -0
- /package/dist/core/__tests__/{placement-revive.test.d.ts → full/placement-reconcile.test.d.ts} +0 -0
- /package/dist/core/__tests__/{placement-teardown.test.d.ts → full/placement-revive.test.d.ts} +0 -0
- /package/dist/core/__tests__/{resolver.test.d.ts → full/placement-teardown.test.d.ts} +0 -0
- /package/dist/core/__tests__/{review-render-pane-regression.test.d.ts → full/review-render-pane-regression.test.d.ts} +0 -0
- /package/dist/core/__tests__/{spike-harness.test.d.ts → full/spike-harness.test.d.ts} +0 -0
|
@@ -1,39 +1,46 @@
|
|
|
1
1
|
// Run with: node --import tsx/esm --test src/core/__tests__/child-death-wake.test.ts
|
|
2
2
|
//
|
|
3
|
-
// D-1 BUG REGRESSION
|
|
4
|
-
// parent on
|
|
3
|
+
// HEADLESS RETARGET (foundation-spec §C.7 + §E). D-1 BUG REGRESSION — "the
|
|
4
|
+
// runtime wakes a dormant parent on EVERY terminal child outcome, and ONLY on a
|
|
5
|
+
// genuine one." Drives the REAL daemon decision pass (superviseTick) + the REAL
|
|
6
|
+
// closeNode against canvas rows fabricated DIRECTLY in an isolated home — NO real
|
|
7
|
+
// tmux session, NO remain-on-exit pane, NO broker boot.
|
|
5
8
|
//
|
|
6
|
-
//
|
|
7
|
-
// then JUST
|
|
8
|
-
// ONLY when a child `push final`
|
|
9
|
-
// booting,
|
|
10
|
-
// the child dead/done/canceled with NO push to the parent
|
|
11
|
-
// waiting parent hung dormant forever. The
|
|
12
|
-
//
|
|
13
|
-
//
|
|
9
|
+
// (1) BUG LOCKED — the D-1 finding (context/d1-finding.md): a parent that
|
|
10
|
+
// delegated and then JUST STOPPED (inbox-wait retained, no self-wake armed)
|
|
11
|
+
// was woken ONLY when a child `push final`d or never booted. A child that
|
|
12
|
+
// CRASHED after booting, was daemon-FINALIZED on a quiet turn, or was `node
|
|
13
|
+
// close`d marked the child dead/done/canceled with NO push to the parent — so
|
|
14
|
+
// a purely-inbox-waiting parent hung dormant forever. The fix fans a system
|
|
15
|
+
// inbox entry to subscribersOf(child) on those three previously-silent
|
|
16
|
+
// terminal outcomes (surfaceChildDeath crash/finalize + close.ts), mirroring
|
|
17
|
+
// surfaceBootFailure. THE CRUX: it must fire ONLY on genuine death — NEVER on
|
|
18
|
+
// healthy dormancy (a child that ended its turn still awaiting a LIVE
|
|
19
|
+
// grandchild, or holding a pending self-wake, is alive-and-dormant; waking its
|
|
20
|
+
// parent then re-creates the spurious-wake storm the doctrine exists to kill).
|
|
14
21
|
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
22
|
+
// (2) WHY MODEL-LEVEL, NOT TMUX CHROME — the wake travels through ONE pure
|
|
23
|
+
// data-layer channel the d1-finding identified: a system entry appended to
|
|
24
|
+
// the parent's inbox.jsonl (push() → appendInbox, no tmux). The death-vs-
|
|
25
|
+
// dormancy boundary is pure daemon logic — finishedTurn (isBusy) +
|
|
26
|
+
// hasActiveLiveSubscription || hasPendingSelfWake, the SAME boundary the
|
|
27
|
+
// stop-guard draws (crtrd.ts). The daemon reaches the pane-gone routing for
|
|
28
|
+
// ANY node whose pane is not alive; a row carrying a bogus pane id reads
|
|
29
|
+
// "gone" (paneExists echoes empty for an unknown/absent pane — and with no
|
|
30
|
+
// tmux server at all, the probe simply fails to false) with NO live session
|
|
31
|
+
// to stand up. So the exact "booted child, pane gone, branch on what it was
|
|
32
|
+
// doing" state reproduces with no real pane, and the parent stays an inline
|
|
33
|
+
// node (no placement → the supervise loop's inline-root carve-out skips it),
|
|
34
|
+
// so nothing in the test opens or needs a window.
|
|
20
35
|
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
// no wake.
|
|
30
|
-
//
|
|
31
|
-
// Faithful: REAL canvas data layer, REAL tmux panes, the REAL daemon decision
|
|
32
|
-
// pass (superviseTick) and the REAL closeNode. The wake travels through the ONE
|
|
33
|
-
// channel the d1-finding identified — an entry appended to the parent's
|
|
34
|
-
// inbox.jsonl — so each direction asserts that channel: the system entry appears
|
|
35
|
-
// (parent woken: its dormant pi is then revived by the daemon's second pass,
|
|
36
|
-
// observed via a cycles bump) or it does NOT (parent stays asleep, cycles flat).
|
|
36
|
+
// (3) HOW THE FABRICATED DRIVE STILL FAILS IF THE BUG REGRESSES — each direction
|
|
37
|
+
// asserts the inbox channel: a genuine terminal outcome (crash/finalize/close)
|
|
38
|
+
// MUST land a system entry from the child in the parent's inbox; healthy
|
|
39
|
+
// dormancy MUST leave it empty. Drop the crash-branch surfaceChildDeath call
|
|
40
|
+
// and the CRASH assertion goes RED (no entry); drop the stillWaiting guard and
|
|
41
|
+
// a healthy-dormant child finalizes → the CRUX asserts go RED (a spurious
|
|
42
|
+
// entry appears). Verified by reverting the crash-branch surfaceChildDeath
|
|
43
|
+
// call (see bug-injection report).
|
|
37
44
|
import { test, before, after, beforeEach } from 'node:test';
|
|
38
45
|
import assert from 'node:assert/strict';
|
|
39
46
|
import { mkdtempSync, rmSync } from 'node:fs';
|
|
@@ -61,15 +68,39 @@ function node(id, over = {}) {
|
|
|
61
68
|
...over,
|
|
62
69
|
};
|
|
63
70
|
}
|
|
64
|
-
function hasTmux() {
|
|
65
|
-
return spawnSync('tmux', ['-V'], { stdio: 'ignore' }).status === 0;
|
|
66
|
-
}
|
|
67
71
|
/** A pid that is guaranteed dead. */
|
|
68
72
|
function deadPid() {
|
|
69
73
|
const r = spawnSync('true', [], { stdio: 'ignore' });
|
|
70
74
|
return r.pid ?? 0x7ffffffe;
|
|
71
75
|
}
|
|
72
|
-
|
|
76
|
+
// A pane id no tmux server knows: paneExists echoes empty for it → isNodePaneAlive
|
|
77
|
+
// is false → the supervise loop routes the row down its pane-gone branch with NO
|
|
78
|
+
// live session required (and with no server at all the probe fails to false too).
|
|
79
|
+
const GONE_PANE = '%999999';
|
|
80
|
+
/** A booted child whose pane is GONE — the daemon's pane-gone routing input.
|
|
81
|
+
* `intent:null` so it takes the user-close branch (neither refresh nor
|
|
82
|
+
* idle-release); pi dead so the zombie-kill is skipped; pi_session_id set so it
|
|
83
|
+
* is "booted" (not the never-booted boot-failure leg). The caller layers on the
|
|
84
|
+
* busy marker / subscriptions that decide crash vs finalize vs release. */
|
|
85
|
+
function paneGoneChild(id, over = {}) {
|
|
86
|
+
createNode(node(id, {
|
|
87
|
+
pane: GONE_PANE,
|
|
88
|
+
tmux_session: 'crtr-cdw',
|
|
89
|
+
window: '@1',
|
|
90
|
+
pi_pid: deadPid(),
|
|
91
|
+
pi_session_id: 'booted',
|
|
92
|
+
intent: null,
|
|
93
|
+
status: 'active',
|
|
94
|
+
...over,
|
|
95
|
+
}));
|
|
96
|
+
}
|
|
97
|
+
/** A dormant inbox-waiting PARENT — an INLINE node (no tmux placement), so the
|
|
98
|
+
* supervise loop's inline-root carve-out skips it entirely: it is only the wake
|
|
99
|
+
* TARGET, asserted via its inbox. */
|
|
100
|
+
function inboxWaitingParent(id) {
|
|
101
|
+
createNode(node(id, { status: 'active' }));
|
|
102
|
+
}
|
|
103
|
+
const wakeFromChild = (parent, child) => readInboxSince(parent).find((e) => e.from === child);
|
|
73
104
|
before(() => {
|
|
74
105
|
home = mkdtempSync(join(tmpdir(), 'crtr-child-death-wake-'));
|
|
75
106
|
process.env['CRTR_HOME'] = home;
|
|
@@ -83,163 +114,80 @@ after(() => {
|
|
|
83
114
|
rmSync(home, { recursive: true, force: true });
|
|
84
115
|
delete process.env['CRTR_HOME'];
|
|
85
116
|
});
|
|
86
|
-
/** Run `fn` with a real, live tmux session + its window's live `%pane_id`. The
|
|
87
|
-
* PARENT occupies this (frozen) pane while pi-dead + idle-released — the
|
|
88
|
-
* faithful shape of an unfocused dormant orchestrator the daemon's second pass
|
|
89
|
-
* revives on an unseen inbox entry. */
|
|
90
|
-
async function withLivePane(tag, fn) {
|
|
91
|
-
const session = `crtr-cdw-${process.pid}-${tag}`;
|
|
92
|
-
spawnSync('tmux', ['new-session', '-d', '-s', session, '-c', '/tmp', 'sleep 600']);
|
|
93
|
-
try {
|
|
94
|
-
const w = spawnSync('tmux', ['list-windows', '-t', session, '-F', '#{window_id}'], { encoding: 'utf8' });
|
|
95
|
-
const window = (w.stdout ?? '').trim().split('\n')[0];
|
|
96
|
-
const p = spawnSync('tmux', ['display-message', '-p', '-t', `${session}:${window}`, '#{pane_id}'], { encoding: 'utf8' });
|
|
97
|
-
const pane = (p.stdout ?? '').trim();
|
|
98
|
-
await fn(session, window, pane);
|
|
99
|
-
}
|
|
100
|
-
finally {
|
|
101
|
-
spawnSync('tmux', ['kill-session', '-t', session], { stdio: 'ignore' });
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
/** A guaranteed-DEAD pane id inside `window`: split a fresh pane, then kill it.
|
|
105
|
-
* The window survives via its original pane. This is how a child's pane reads
|
|
106
|
-
* "gone" to the daemon while the session/window are still alive. */
|
|
107
|
-
function deadPaneIn(session, window) {
|
|
108
|
-
const sp = spawnSync('tmux', ['split-window', '-d', '-P', '-F', '#{pane_id}', '-t', `${session}:${window}`, 'sleep 600'], { encoding: 'utf8' });
|
|
109
|
-
const dead = (sp.stdout ?? '').trim();
|
|
110
|
-
spawnSync('tmux', ['kill-pane', '-t', dead], { stdio: 'ignore' });
|
|
111
|
-
return dead;
|
|
112
|
-
}
|
|
113
|
-
/** A dormant, unfocused PARENT: pi-dead, idle-released, frozen on the live pane,
|
|
114
|
-
* home_session set so the daemon's second pass can revive it. */
|
|
115
|
-
function dormantParent(id, session, window, pane) {
|
|
116
|
-
createNode(node(id, {
|
|
117
|
-
pane,
|
|
118
|
-
tmux_session: session,
|
|
119
|
-
window,
|
|
120
|
-
pi_pid: deadPid(),
|
|
121
|
-
pi_session_id: 'booted',
|
|
122
|
-
intent: 'idle-release',
|
|
123
|
-
status: 'idle',
|
|
124
|
-
home_session: session,
|
|
125
|
-
}));
|
|
126
|
-
}
|
|
127
117
|
// ===========================================================================
|
|
128
|
-
// POSITIVE — a genuine terminal child outcome WAKES the
|
|
118
|
+
// POSITIVE — a genuine terminal child outcome WAKES the inbox-waiting parent.
|
|
129
119
|
// ===========================================================================
|
|
130
|
-
test('CRASH after boot (mid-generation, pane gone) wakes the
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
assert.equal(getNode('CHILD').status, 'dead', 'CHILD crashed → dead');
|
|
143
|
-
// THE WAKE CHANNEL: a system entry from CHILD lands in PARENT's inbox.
|
|
144
|
-
const wake = readInboxSince('PARENT').find((e) => e.from === 'CHILD');
|
|
145
|
-
assert.ok(wake, 'crash fanned a system inbox entry to the dormant parent (mirrors surfaceBootFailure)');
|
|
146
|
-
assert.match(wake.label, /died/i, 'the entry tells the parent WHICH child died and how');
|
|
147
|
-
// END-TO-END: the dormant parent is REVIVED on the same tick (second pass).
|
|
148
|
-
assert.equal(cyclesOf('PARENT'), before + 1, 'dormant PARENT revived on the same tick — no longer hangs');
|
|
149
|
-
clearBusy('CHILD');
|
|
150
|
-
});
|
|
120
|
+
test('CRASH after boot (mid-generation, pane gone) wakes the inbox-waiting parent', async () => {
|
|
121
|
+
inboxWaitingParent('PARENT');
|
|
122
|
+
paneGoneChild('CHILD');
|
|
123
|
+
subscribe('PARENT', 'CHILD', true); // the spawn-time spine edge
|
|
124
|
+
markBusy('CHILD'); // pane died INSIDE a turn → genuine mid-run crash
|
|
125
|
+
await superviseTick();
|
|
126
|
+
assert.equal(getNode('CHILD').status, 'dead', 'CHILD crashed → dead');
|
|
127
|
+
// THE WAKE CHANNEL: a system entry from CHILD lands in PARENT's inbox.
|
|
128
|
+
const wake = wakeFromChild('PARENT', 'CHILD');
|
|
129
|
+
assert.ok(wake, 'crash fanned a system inbox entry to the dormant parent (mirrors surfaceBootFailure)');
|
|
130
|
+
assert.match(wake.label, /died/i, 'the entry tells the parent WHICH child died and how');
|
|
131
|
+
clearBusy('CHILD');
|
|
151
132
|
});
|
|
152
|
-
test('quiet-turn FINALIZE (pane gone, nothing live to wait for) wakes the
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
const before = cyclesOf('PARENT');
|
|
164
|
-
await superviseTick();
|
|
165
|
-
assert.equal(getNode('CHILD').status, 'done', 'CHILD finalized → done');
|
|
166
|
-
const wake = readInboxSince('PARENT').find((e) => e.from === 'CHILD');
|
|
167
|
-
assert.ok(wake, 'finalize fanned a system inbox entry to the dormant parent');
|
|
168
|
-
assert.match(wake.label, /without a final report/i, 'the entry tells the parent the child ended with no final');
|
|
169
|
-
assert.equal(cyclesOf('PARENT'), before + 1, 'dormant PARENT revived on the same tick');
|
|
170
|
-
});
|
|
133
|
+
test('quiet-turn FINALIZE (pane gone, nothing live to wait for) wakes the inbox-waiting parent', async () => {
|
|
134
|
+
inboxWaitingParent('PARENT');
|
|
135
|
+
// Booted, turn finished (NO busy marker), no live subscription, no pending
|
|
136
|
+
// self-wake → the daemon finalizes it (done): a dismissal of a finished node.
|
|
137
|
+
paneGoneChild('CHILD');
|
|
138
|
+
subscribe('PARENT', 'CHILD', true);
|
|
139
|
+
await superviseTick();
|
|
140
|
+
assert.equal(getNode('CHILD').status, 'done', 'CHILD finalized → done');
|
|
141
|
+
const wake = wakeFromChild('PARENT', 'CHILD');
|
|
142
|
+
assert.ok(wake, 'finalize fanned a system inbox entry to the dormant parent');
|
|
143
|
+
assert.match(wake.label, /without a final report/i, 'the entry tells the parent the child ended with no final');
|
|
171
144
|
});
|
|
172
|
-
test('node close of a child wakes its SURVIVING manager (the parent outside the closing set)',
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
assert.equal(getNode('CHILD').status, 'canceled', 'CHILD canceled by close');
|
|
186
|
-
const wake = readInboxSince('PARENT').find((e) => e.from === 'CHILD');
|
|
187
|
-
assert.ok(wake, 'node close fanned a child-closed entry to the surviving manager (D-1: previously none)');
|
|
188
|
-
assert.match(wake.label, /closed/i, 'the entry names the closed child');
|
|
189
|
-
// The dormant parent is then revived by the daemon's second pass.
|
|
190
|
-
const before = cyclesOf('PARENT');
|
|
191
|
-
await superviseTick();
|
|
192
|
-
assert.equal(cyclesOf('PARENT'), before + 1, 'dormant PARENT revived on the close entry — no longer hangs');
|
|
193
|
-
});
|
|
145
|
+
test('node close of a child wakes its SURVIVING manager (the parent outside the closing set)', async () => {
|
|
146
|
+
inboxWaitingParent('PARENT');
|
|
147
|
+
// A booted child; closeNode tears its (paneless-for-the-test) engine down.
|
|
148
|
+
createNode(node('CHILD', { pane: GONE_PANE, tmux_session: 'crtr-cdw', window: '@1', pi_session_id: 'booted', status: 'active' }));
|
|
149
|
+
subscribe('PARENT', 'CHILD', true); // PARENT is a manager OUTSIDE the close set
|
|
150
|
+
// Closing CHILD: closingSet({CHILD}) cannot pull in PARENT (PARENT is a
|
|
151
|
+
// SUBSCRIBER of CHILD, never a descendant), so PARENT survives + must wake.
|
|
152
|
+
const res = closeNode('CHILD');
|
|
153
|
+
assert.deepEqual(res.closed, ['CHILD'], 'only CHILD is closed; PARENT spared');
|
|
154
|
+
assert.equal(getNode('CHILD').status, 'canceled', 'CHILD canceled by close');
|
|
155
|
+
const wake = wakeFromChild('PARENT', 'CHILD');
|
|
156
|
+
assert.ok(wake, 'node close fanned a child-closed entry to the surviving manager (D-1: previously none)');
|
|
157
|
+
assert.match(wake.label, /closed/i, 'the entry names the closed child');
|
|
194
158
|
});
|
|
195
159
|
// ===========================================================================
|
|
196
160
|
// NEGATIVE — HEALTHY DORMANCY must NOT wake the parent (the correctness crux).
|
|
197
161
|
// ===========================================================================
|
|
198
|
-
test('CRUX: a child dormant while awaiting its OWN live grandchild does NOT wake the parent',
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
await superviseTick();
|
|
214
|
-
// CHILD is RELEASED (revivable), NOT finalized — the daemon must not orphan
|
|
215
|
-
// its in-flight grandchild and must not wake the parent.
|
|
216
|
-
assert.equal(getNode('CHILD').status, 'idle', 'CHILD released (revivable), NOT finalized');
|
|
217
|
-
assert.equal(getNode('CHILD').intent, 'idle-release', 'CHILD routed to idle-release');
|
|
218
|
-
assert.equal(readInboxSince('PARENT').length, 0, 'PARENT inbox EMPTY — healthy dormancy raises no wake');
|
|
219
|
-
assert.equal(cyclesOf('PARENT'), before, 'dormant PARENT stays asleep (no revive) — no spurious wake');
|
|
220
|
-
});
|
|
162
|
+
test('CRUX: a child dormant while awaiting its OWN live grandchild does NOT wake the parent', async () => {
|
|
163
|
+
inboxWaitingParent('PARENT');
|
|
164
|
+
// CHILD finished its turn (no busy) and its pane is gone, BUT it actively
|
|
165
|
+
// subscribes to a LIVE grandchild — exactly the developer-spawned-reviewer
|
|
166
|
+
// case. This is HEALTHY dormancy, not death.
|
|
167
|
+
paneGoneChild('CHILD');
|
|
168
|
+
createNode(node('GRANDCHILD', { status: 'active', pi_session_id: 'booted' })); // live, inline
|
|
169
|
+
subscribe('PARENT', 'CHILD', true);
|
|
170
|
+
subscribe('CHILD', 'GRANDCHILD', true); // CHILD awaits a LIVE grandchild
|
|
171
|
+
await superviseTick();
|
|
172
|
+
// CHILD is RELEASED (revivable), NOT finalized — the daemon must not orphan
|
|
173
|
+
// its in-flight grandchild and must not wake the parent.
|
|
174
|
+
assert.equal(getNode('CHILD').status, 'idle', 'CHILD released (revivable), NOT finalized');
|
|
175
|
+
assert.equal(getNode('CHILD').intent, 'idle-release', 'CHILD routed to idle-release');
|
|
176
|
+
assert.equal(readInboxSince('PARENT').length, 0, 'PARENT inbox EMPTY — healthy dormancy raises no wake');
|
|
221
177
|
});
|
|
222
|
-
test('CRUX: a child dormant on a pending self-wake (no live sub) does NOT wake the parent',
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
armWake({ wakeup_id: 'wk-cdw-child', node_id: 'CHILD', owner_id: 'CHILD', fire_at: future, kind: 'bare' });
|
|
238
|
-
const before = cyclesOf('PARENT');
|
|
239
|
-
await superviseTick();
|
|
240
|
-
assert.equal(getNode('CHILD').status, 'idle', 'CHILD with a pending self-wake is RELEASED, not finalized');
|
|
241
|
-
assert.equal(getNode('CHILD').intent, 'idle-release', 'CHILD routed to idle-release (its clock will wake it)');
|
|
242
|
-
assert.equal(readInboxSince('PARENT').length, 0, 'PARENT inbox EMPTY — a pending clock is healthy dormancy');
|
|
243
|
-
assert.equal(cyclesOf('PARENT'), before, 'dormant PARENT stays asleep — pending self-wake is not death');
|
|
244
|
-
});
|
|
178
|
+
test('CRUX: a child dormant on a pending self-wake (no live sub) does NOT wake the parent', async () => {
|
|
179
|
+
inboxWaitingParent('PARENT');
|
|
180
|
+
paneGoneChild('CHILD');
|
|
181
|
+
subscribe('PARENT', 'CHILD', true);
|
|
182
|
+
// CHILD has NO live subscription, but holds a pending self-wake (far-future so
|
|
183
|
+
// the wakeups pass never fires it this tick). hasPendingSelfWake(CHILD) ⇒ the
|
|
184
|
+
// daemon treats it as STILL WAITING (the stop-guard's boundary), so it RELEASES
|
|
185
|
+
// instead of finalizing. NON-VACUOUS: drop the hasPendingSelfWake guard and
|
|
186
|
+
// CHILD finalizes → PARENT is spuriously woken.
|
|
187
|
+
const future = new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).toISOString();
|
|
188
|
+
armWake({ wakeup_id: 'wk-cdw-child', node_id: 'CHILD', owner_id: 'CHILD', fire_at: future, kind: 'bare' });
|
|
189
|
+
await superviseTick();
|
|
190
|
+
assert.equal(getNode('CHILD').status, 'idle', 'CHILD with a pending self-wake is RELEASED, not finalized');
|
|
191
|
+
assert.equal(getNode('CHILD').intent, 'idle-release', 'CHILD routed to idle-release (its clock will wake it)');
|
|
192
|
+
assert.equal(readInboxSince('PARENT').length, 0, 'PARENT inbox EMPTY — a pending clock is healthy dormancy');
|
|
245
193
|
});
|