@crouton-kit/crouter 0.3.15 → 0.3.17
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/builtin-personas/developer/orchestrator.md +1 -1
- package/dist/builtin-personas/orchestration-kernel.md +6 -6
- package/dist/builtin-personas/plan/base.md +1 -1
- package/dist/builtin-personas/plan/orchestrator.md +1 -1
- package/dist/builtin-personas/spec/base.md +1 -1
- package/dist/commands/canvas-browse.d.ts +2 -0
- package/dist/commands/canvas-browse.js +45 -0
- package/dist/commands/canvas-prune.js +11 -2
- package/dist/commands/canvas.js +3 -2
- package/dist/commands/chord.js +1 -1
- package/dist/commands/human/shared.js +1 -1
- package/dist/commands/node.js +14 -2
- package/dist/commands/skill/author.js +2 -2
- package/dist/commands/tmux-spread.js +2 -3
- package/dist/core/__tests__/cascade-close.test.js +199 -0
- package/dist/core/__tests__/close.test.js +2 -2
- package/dist/core/__tests__/daemon-boot.test.js +7 -0
- package/dist/core/__tests__/daemon-liveness.test.js +59 -4
- package/dist/core/__tests__/dead-pane-regression.test.js +151 -0
- package/dist/core/__tests__/fixtures/fake-pi-host.d.ts +2 -0
- package/dist/core/__tests__/fixtures/fake-pi-host.js +301 -0
- package/dist/core/__tests__/flagship-lifecycle.test.js +273 -0
- package/dist/core/__tests__/focuses.test.js +5 -68
- package/dist/core/__tests__/grace-clock.test.d.ts +1 -0
- package/dist/core/__tests__/grace-clock.test.js +115 -0
- package/dist/core/__tests__/helpers/harness.d.ts +78 -0
- package/dist/core/__tests__/helpers/harness.js +406 -0
- package/dist/core/__tests__/home-session.test.js +1 -1
- package/dist/core/__tests__/lifecycle.test.js +6 -13
- package/dist/core/__tests__/live-mutation.test.d.ts +1 -0
- package/dist/core/__tests__/live-mutation.test.js +341 -0
- package/dist/core/__tests__/placement-focus.test.js +106 -46
- package/dist/core/__tests__/placement-teardown.test.js +4 -9
- package/dist/core/__tests__/relaunch.test.js +22 -16
- package/dist/core/__tests__/reset.test.js +11 -6
- package/dist/core/__tests__/spike-harness.test.d.ts +1 -0
- package/dist/core/__tests__/spike-harness.test.js +241 -0
- package/dist/core/__tests__/subscription-delivery.test.d.ts +1 -0
- package/dist/core/__tests__/subscription-delivery.test.js +233 -0
- package/dist/core/__tests__/tmux-surface.test.js +8 -9
- package/dist/core/canvas/browse/__tests__/model.test.d.ts +1 -0
- package/dist/core/canvas/browse/__tests__/model.test.js +142 -0
- package/dist/core/canvas/browse/__tests__/render.test.d.ts +1 -0
- package/dist/core/canvas/browse/__tests__/render.test.js +102 -0
- package/dist/core/canvas/browse/app.d.ts +4 -0
- package/dist/core/canvas/browse/app.js +349 -0
- package/dist/core/canvas/browse/model.d.ts +97 -0
- package/dist/core/canvas/browse/model.js +258 -0
- package/dist/core/canvas/browse/render.d.ts +41 -0
- package/dist/core/canvas/browse/render.js +387 -0
- package/dist/core/canvas/browse/terminal.d.ts +23 -0
- package/dist/core/canvas/browse/terminal.js +100 -0
- package/dist/core/canvas/canvas.d.ts +9 -2
- package/dist/core/canvas/canvas.js +41 -3
- package/dist/core/canvas/db.js +2 -3
- package/dist/core/canvas/focuses.d.ts +2 -2
- package/dist/core/canvas/focuses.js +4 -3
- package/dist/core/canvas/render.d.ts +10 -0
- package/dist/core/canvas/render.js +25 -1
- package/dist/core/canvas/types.d.ts +1 -1
- package/dist/core/feed/inbox.d.ts +0 -3
- package/dist/core/feed/inbox.js +1 -5
- package/dist/core/runtime/busy.d.ts +8 -0
- package/dist/core/runtime/busy.js +46 -0
- package/dist/core/runtime/close.js +2 -2
- package/dist/core/runtime/demote.js +2 -7
- package/dist/core/runtime/launch.d.ts +3 -1
- package/dist/core/runtime/launch.js +4 -1
- package/dist/core/runtime/lifecycle.d.ts +1 -1
- package/dist/core/runtime/lifecycle.js +12 -4
- package/dist/core/runtime/naming.d.ts +3 -3
- package/dist/core/runtime/naming.js +6 -6
- package/dist/core/runtime/nodes.d.ts +7 -0
- package/dist/core/runtime/nodes.js +10 -1
- package/dist/core/runtime/placement.d.ts +39 -10
- package/dist/core/runtime/placement.js +100 -44
- package/dist/core/runtime/reset.d.ts +11 -8
- package/dist/core/runtime/reset.js +36 -31
- package/dist/core/runtime/revive.d.ts +1 -1
- package/dist/core/runtime/revive.js +2 -2
- package/dist/core/runtime/spawn.js +3 -3
- package/dist/core/runtime/tmux-chrome.d.ts +1 -0
- package/dist/core/runtime/tmux-chrome.js +4 -0
- package/dist/core/runtime/tmux.d.ts +13 -6
- package/dist/core/runtime/tmux.js +21 -12
- package/dist/daemon/crtrd.js +43 -21
- package/dist/pi-extensions/canvas-nav.js +40 -28
- package/dist/pi-extensions/canvas-resume.d.ts +21 -0
- package/dist/pi-extensions/canvas-resume.js +82 -0
- package/dist/pi-extensions/canvas-stophook.d.ts +1 -1
- package/dist/pi-extensions/canvas-stophook.js +21 -9
- package/dist/prompts/skill.js +6 -1
- package/package.json +2 -2
- package/dist/commands/__tests__/skill.test.js +0 -290
- package/dist/core/__tests__/pkg.test.js +0 -218
- package/dist/core/__tests__/sys.test.js +0 -208
- package/dist/core/runtime/presence.d.ts +0 -30
- package/dist/core/runtime/presence.js +0 -178
- /package/dist/{commands/__tests__/skill.test.d.ts → core/__tests__/cascade-close.test.d.ts} +0 -0
- /package/dist/core/__tests__/{pkg.test.d.ts → dead-pane-regression.test.d.ts} +0 -0
- /package/dist/core/__tests__/{sys.test.d.ts → flagship-lifecycle.test.d.ts} +0 -0
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
// Run with: node --import tsx/esm --test src/core/__tests__/flagship-lifecycle.test.ts
|
|
2
|
+
//
|
|
3
|
+
// FLAGSHIP end-to-end lifecycle test — the faithful integration harness driving
|
|
4
|
+
// a full node-graph scenario through the REAL CLI, a REAL isolated tmux session,
|
|
5
|
+
// the REAL extension hooks (fired inside the fake-pi vehicle), and the REAL
|
|
6
|
+
// daemon decision pass (superviseTick, in-process). Every assertion reads the
|
|
7
|
+
// canvas data layer and is checked against the state-model ORACLE.
|
|
8
|
+
//
|
|
9
|
+
// Scenario (each hop asserted):
|
|
10
|
+
// A (base→orchestrator, RESIDENT root) spawns terminal child B.
|
|
11
|
+
// B spawns child C, then goes DORMANT (terminal idle-release, unfocused→pane closes).
|
|
12
|
+
// C finishes → ONE-HOP wakes B (A is NOT woken — the oracle's wake contract).
|
|
13
|
+
// B PROMOTES to orchestrator (terminal), takes a turn (persona drift), then
|
|
14
|
+
// YIELDS and revives in place (refresh-yield).
|
|
15
|
+
// B spawns child D; D finishes; B pushes its final up to A.
|
|
16
|
+
//
|
|
17
|
+
// 2×2 coverage (mode {base,orchestrator} × lifecycle {terminal,resident}):
|
|
18
|
+
// • base×resident — A's PERSISTED ROW SHAPE at S1 (a harness-set fixture,
|
|
19
|
+
// NOT a real bootRoot — see S1 + MINOR-1 caveat below).
|
|
20
|
+
// The resident BEHAVIOR (does not idle-release) is
|
|
21
|
+
// exercised faithfully in live-mutation.test.ts.
|
|
22
|
+
// • orchestrator×resident — A after promote (top-level orchestrator)
|
|
23
|
+
// • base×terminal — B/C/D workers (GENUINE managed-child birth at S3)
|
|
24
|
+
// • orchestrator×terminal — B after promote (sub-orchestrator)
|
|
25
|
+
//
|
|
26
|
+
// ── Known coverage boundaries (DELIBERATE, documented gaps) ────────────────
|
|
27
|
+
// This flagship + the live-mutation/cascade/subscription siblings cover the
|
|
28
|
+
// HAPPY-PATH lifecycle and the live-mutation axis faithfully. The following
|
|
29
|
+
// fault/grace/focus paths are OUT OF SCOPE for the faithful tier in this pass
|
|
30
|
+
// (some are backstopped at the in-process unit tier, cited):
|
|
31
|
+
// • crash → dead (a vehicle that boots then its pane vanishes mid-run) — unit:
|
|
32
|
+
// daemon-liveness.test.ts "pane GONE … crash → dead".
|
|
33
|
+
// • boot-failure push (a vehicle that never boots → surfaceBootFailure urgent
|
|
34
|
+
// push up the spine) — no faithful coverage here.
|
|
35
|
+
// • focused-FREEZE (F3: a focused-dormant node frozen via remain-on-exit,
|
|
36
|
+
// pane-alive but pi-dead) — unit: daemon-liveness.test.ts "idle-release +
|
|
37
|
+
// live (frozen) pane …"; the grace-window double-spawn guard around it is
|
|
38
|
+
// exercised faithfully in grace-clock.test.ts.
|
|
39
|
+
// • node lifecycle --detach (A3: orphaned-focus-row hazard) — untested.
|
|
40
|
+
// • node msg / focus / cycle wake of a dormant node (A7) — untested faithfully.
|
|
41
|
+
// These are intentional boundaries, not oversights.
|
|
42
|
+
import { test } from 'node:test';
|
|
43
|
+
import assert from 'node:assert/strict';
|
|
44
|
+
import { spawnSync } from 'node:child_process';
|
|
45
|
+
import { createHarness, hasTmux } from './helpers/harness.js';
|
|
46
|
+
import { STALL_REPROMPT } from '../runtime/stop-guard.js';
|
|
47
|
+
function sessionExists(session) {
|
|
48
|
+
return spawnSync('tmux', ['has-session', '-t', session], { stdio: 'ignore' }).status === 0;
|
|
49
|
+
}
|
|
50
|
+
test('flagship: full node lifecycle — spawn, dormancy, one-hop wake, promote, yield, finish', { skip: !hasTmux() ? 'tmux unavailable' : false, timeout: 180_000 }, async () => {
|
|
51
|
+
const h = await createHarness({ sessionPrefix: 'crtr-flagship' });
|
|
52
|
+
try {
|
|
53
|
+
// ===================================================================
|
|
54
|
+
// S1 — A's PERSISTED ROW SHAPE: base × RESIDENT (the user's front door).
|
|
55
|
+
// ⚠ MINOR-1 caveat: this is NOT a real root birth. The harness mints A
|
|
56
|
+
// via in-process createNode over a hand-built meta whose defaults ARE
|
|
57
|
+
// mode:'base'/lifecycle:'resident' — the REAL birth/boot path (bootRoot)
|
|
58
|
+
// execs pi inline and never returns, so it is structurally unreachable
|
|
59
|
+
// from the harness (harness-design Wall #7). These four asserts therefore
|
|
60
|
+
// PIN THE PERSISTED ROW SHAPE the rest of the scenario builds on — they
|
|
61
|
+
// do NOT prove the real resident-birth path. The resident-at-birth 2×2
|
|
62
|
+
// quadrant is out of reach; the resident *behavior* (no idle-release) is
|
|
63
|
+
// exercised faithfully in live-mutation.test.ts, and the genuine real
|
|
64
|
+
// birth of a managed child (→ terminal) IS exercised at S3 below.
|
|
65
|
+
// ===================================================================
|
|
66
|
+
const A = h.spawnRoot('top-level orchestrator');
|
|
67
|
+
{
|
|
68
|
+
const a = h.node(A);
|
|
69
|
+
assert.equal(a.mode, 'base', 'A row shape: base');
|
|
70
|
+
assert.equal(a.lifecycle, 'resident', 'A row shape: resident (fixture, not a real boot)');
|
|
71
|
+
assert.equal(a.status, 'active', 'A active');
|
|
72
|
+
assert.equal(a.intent ?? null, null, 'A no intent');
|
|
73
|
+
}
|
|
74
|
+
// ===================================================================
|
|
75
|
+
// S2 — A promotes: orchestrator × RESIDENT (top-level orchestrator).
|
|
76
|
+
// mode flips; lifecycle/status/intent untouched (no transition()).
|
|
77
|
+
// ===================================================================
|
|
78
|
+
{
|
|
79
|
+
const res = h.cli(A, ['node', 'promote']);
|
|
80
|
+
assert.equal(res.code, 0, `promote A exit 0\n${res.stderr}`);
|
|
81
|
+
const a = h.node(A);
|
|
82
|
+
assert.equal(a.mode, 'orchestrator', 'A → orchestrator');
|
|
83
|
+
assert.equal(a.lifecycle, 'resident', 'A stays resident (no --resident)');
|
|
84
|
+
assert.equal(a.status, 'active', 'A still active (promote does not transition)');
|
|
85
|
+
assert.equal(a.intent ?? null, null, 'A intent untouched by promote');
|
|
86
|
+
}
|
|
87
|
+
// ===================================================================
|
|
88
|
+
// S3 — A spawns terminal child B: base × TERMINAL. The spawn seed wires
|
|
89
|
+
// the spine — A auto-subscribes ACTIVE to B, B spawned_by A.
|
|
90
|
+
// ===================================================================
|
|
91
|
+
const B = await h.spawnChild(A, 'do the work', { kind: 'developer' });
|
|
92
|
+
{
|
|
93
|
+
const b = h.node(B);
|
|
94
|
+
assert.equal(b.mode, 'base', 'B born base');
|
|
95
|
+
assert.equal(b.lifecycle, 'terminal', 'B born terminal (managed child)');
|
|
96
|
+
assert.equal(b.status, 'active', 'B active after boot');
|
|
97
|
+
assert.equal(b.intent ?? null, null, 'B no intent');
|
|
98
|
+
assert.equal(b.parent, A, 'B spawned_by / parent = A');
|
|
99
|
+
// The load-bearing spine seed: A subscribes_to B (active).
|
|
100
|
+
const subsToB = h.subscribers(B);
|
|
101
|
+
assert.deepEqual(subsToB, [{ node_id: A, active: true }], 'A is B\'s sole ACTIVE subscriber (spawn seed)');
|
|
102
|
+
assert.equal(h.inbox(A).length, 0, 'A inbox empty (spawn pushes nothing)');
|
|
103
|
+
}
|
|
104
|
+
// ===================================================================
|
|
105
|
+
// S4 — B spawns child C (base × terminal). B now holds an active live
|
|
106
|
+
// subscription to C → B is legitimately "awaiting" if it stops.
|
|
107
|
+
// ===================================================================
|
|
108
|
+
const C = await h.spawnChild(B, 'a subtask');
|
|
109
|
+
{
|
|
110
|
+
const c = h.node(C);
|
|
111
|
+
assert.equal(c.mode, 'base', 'C base');
|
|
112
|
+
assert.equal(c.lifecycle, 'terminal', 'C terminal');
|
|
113
|
+
assert.equal(c.status, 'active', 'C active');
|
|
114
|
+
assert.deepEqual(h.subscribers(C), [{ node_id: B, active: true }], 'B is C\'s sole ACTIVE subscriber (spawn seed)');
|
|
115
|
+
assert.deepEqual(h.subscriptions(B), [{ node_id: C, active: true }], 'B subscribes_to C (active)');
|
|
116
|
+
assert.equal(h.status(B), 'active', 'B still active');
|
|
117
|
+
}
|
|
118
|
+
// ===================================================================
|
|
119
|
+
// S5 — B goes DORMANT. It stops while awaiting C → terminal idle-release:
|
|
120
|
+
// transition('release') → idle / idle-release; ctx.shutdown kills pi;
|
|
121
|
+
// UNFOCUSED → the backstage pane CLOSES (fully dormant).
|
|
122
|
+
// ===================================================================
|
|
123
|
+
await h.stop(B);
|
|
124
|
+
await h.waitForStatus(B, 'idle');
|
|
125
|
+
{
|
|
126
|
+
const b = h.node(B);
|
|
127
|
+
assert.equal(b.status, 'idle', 'B idle (released)');
|
|
128
|
+
assert.equal(b.intent, 'idle-release', 'B intent=idle-release');
|
|
129
|
+
}
|
|
130
|
+
await h.waitForPaneGone(B);
|
|
131
|
+
assert.equal(h.paneAlive(B), false, 'B unfocused → pane closed on idle-release');
|
|
132
|
+
assert.equal(h.status(C), 'active', 'C still active while B sleeps');
|
|
133
|
+
// A untouched — a dormant terminal worker does not disturb its manager.
|
|
134
|
+
assert.equal(h.status(A), 'active', 'A still active');
|
|
135
|
+
assert.equal(h.inbox(A).length, 0, 'A inbox still empty');
|
|
136
|
+
// ===================================================================
|
|
137
|
+
// S6 — C FINISHES (push final). Pointer fans to subscribersOf(C) = {B}
|
|
138
|
+
// ONLY. C → done/done. A is NOT a subscriber of C → A inbox stays
|
|
139
|
+
// empty. This is the one-hop wake contract at the push site.
|
|
140
|
+
// ===================================================================
|
|
141
|
+
await h.finish(C, 'C result body');
|
|
142
|
+
{
|
|
143
|
+
const c = h.node(C);
|
|
144
|
+
assert.equal(c.status, 'done', 'C done');
|
|
145
|
+
assert.equal(c.intent, 'done', 'C intent=done');
|
|
146
|
+
assert.equal(h.paneAlive(C), false, 'C pane closed on done');
|
|
147
|
+
const bInbox = h.inbox(B);
|
|
148
|
+
const cFinal = bInbox.find((e) => e.from === C && e.kind === 'final');
|
|
149
|
+
assert.ok(cFinal, 'B inbox received C\'s final pointer (one hop)');
|
|
150
|
+
// ORACLE: ONE-HOP. A is not woken — only B (the direct subscriber) hears C.
|
|
151
|
+
assert.equal(h.inbox(A).length, 0, 'A inbox empty — A NOT woken by C (one-hop)');
|
|
152
|
+
}
|
|
153
|
+
// ===================================================================
|
|
154
|
+
// S7 — WAKE B. The in-process daemon second pass sees B idle/idle-release,
|
|
155
|
+
// pi dead, an unseen inbox entry → reviveNode(resume). B → active,
|
|
156
|
+
// intent cleared (revive). Its FRESH watcher delivers C's report.
|
|
157
|
+
// ===================================================================
|
|
158
|
+
const injBeforeWake = h.injected(B).length;
|
|
159
|
+
await h.tick(); // one superviseTick: 1st pass nulls the stale window, 2nd revives on inbox
|
|
160
|
+
await h.waitForStatus(B, 'active');
|
|
161
|
+
{
|
|
162
|
+
const b = h.node(B);
|
|
163
|
+
assert.equal(b.status, 'active', 'B revived → active');
|
|
164
|
+
assert.equal(b.intent ?? null, null, 'B intent cleared by revive');
|
|
165
|
+
}
|
|
166
|
+
await h.awaitBoot(B, { minCount: 2 }); // the resume boot (spawn boot + wake boot)
|
|
167
|
+
const wakeDigests = await h.awaitWake(B, { sinceCount: injBeforeWake, match: /C result body/ });
|
|
168
|
+
assert.ok(wakeDigests.some((d) => /C result body/.test(d)), 'B\'s real inbox-watcher delivered C\'s report after the wake');
|
|
169
|
+
// Still one-hop: A remained dormant-as-resident and was never touched.
|
|
170
|
+
assert.equal(h.inbox(A).length, 0, 'A inbox STILL empty after B woke (one-hop confirmed)');
|
|
171
|
+
assert.equal(h.status(A), 'active', 'A unchanged');
|
|
172
|
+
// ===================================================================
|
|
173
|
+
// S8 — B PROMOTES: base → orchestrator, lifecycle TERMINAL unchanged
|
|
174
|
+
// (no --resident). orchestrator × terminal (the sub-orchestrator).
|
|
175
|
+
// ===================================================================
|
|
176
|
+
{
|
|
177
|
+
const res = h.cli(B, ['node', 'promote', '--kind', 'developer']);
|
|
178
|
+
assert.equal(res.code, 0, `promote B exit 0\n${res.stderr}`);
|
|
179
|
+
const b = h.node(B);
|
|
180
|
+
assert.equal(b.mode, 'orchestrator', 'B → orchestrator');
|
|
181
|
+
assert.equal(b.lifecycle, 'terminal', 'B stays TERMINAL (promote is mode-only)');
|
|
182
|
+
assert.equal(b.status, 'active', 'B still active');
|
|
183
|
+
assert.equal(b.intent ?? null, null, 'B intent untouched by promote');
|
|
184
|
+
}
|
|
185
|
+
// ===================================================================
|
|
186
|
+
// S9 — B takes a TURN. turn_end detects the base→orchestrator persona
|
|
187
|
+
// drift and injects the orchestration guidance as a 'steer', then
|
|
188
|
+
// commits the ack. agent_end then stalls (no live sub, no final) →
|
|
189
|
+
// STALL_REPROMPT (followUp). Both are real stophook branches.
|
|
190
|
+
// ===================================================================
|
|
191
|
+
const injBeforeTurn = h.injected(B).length;
|
|
192
|
+
await h.turn(B, 'orchestrating');
|
|
193
|
+
const turnInjected = await h.waitFor(() => {
|
|
194
|
+
const fresh = h.injected(B).slice(injBeforeTurn);
|
|
195
|
+
// MINOR-3: match the steer by CONTENT (/ORCHESTRATOR/i), not deliverAs
|
|
196
|
+
// alone — mirroring live-mutation.test.ts's orchestrationSteers() — so
|
|
197
|
+
// this pins the orchestration guidance specifically, not "some steer."
|
|
198
|
+
const steer = fresh.find((e) => e.deliverAs === 'steer' && /ORCHESTRATOR/i.test(e.content));
|
|
199
|
+
const reprompt = fresh.find((e) => e.content.includes(STALL_REPROMPT));
|
|
200
|
+
return steer && reprompt ? fresh : null;
|
|
201
|
+
}, { timeoutMs: 15_000, label: 'B turn injected persona-drift steer + stall reprompt' });
|
|
202
|
+
assert.ok(turnInjected.some((e) => e.deliverAs === 'steer' && /ORCHESTRATOR/i.test(e.content)), 'turn_end injected the base→orchestrator persona-drift guidance (steer with ORCHESTRATOR content)');
|
|
203
|
+
assert.ok(turnInjected.some((e) => e.content.includes(STALL_REPROMPT)), 'agent_end stalled (orchestrator, no live sub, no final) → STALL_REPROMPT');
|
|
204
|
+
assert.equal(h.status(B), 'active', 'B still active after the turn (reprompt keeps it alive)');
|
|
205
|
+
// ===================================================================
|
|
206
|
+
// S10 — B YIELDS + revives. node yield → intent=refresh (active kept);
|
|
207
|
+
// agent_end (b') runs reviveInPlace (respawn-pane -k) IN the fake-pi
|
|
208
|
+
// pane; the fresh pi's session_start clears refresh → active. mode
|
|
209
|
+
// and lifecycle survive the refresh.
|
|
210
|
+
// ===================================================================
|
|
211
|
+
await h.yieldNode(B, 'refresh against the roadmap');
|
|
212
|
+
{
|
|
213
|
+
const b = h.node(B);
|
|
214
|
+
assert.equal(b.status, 'active', 'B active after refresh-yield');
|
|
215
|
+
assert.equal(b.intent ?? null, null, 'B intent=refresh cleared by the fresh boot');
|
|
216
|
+
assert.equal(b.mode, 'orchestrator', 'B mode survives the refresh');
|
|
217
|
+
assert.equal(b.lifecycle, 'terminal', 'B lifecycle survives the refresh');
|
|
218
|
+
}
|
|
219
|
+
// ===================================================================
|
|
220
|
+
// S11 — B spawns child D (base × terminal). New spine seed B→D active.
|
|
221
|
+
// ===================================================================
|
|
222
|
+
const D = await h.spawnChild(B, 'a second subtask');
|
|
223
|
+
{
|
|
224
|
+
const d = h.node(D);
|
|
225
|
+
assert.equal(d.mode, 'base', 'D base');
|
|
226
|
+
assert.equal(d.lifecycle, 'terminal', 'D terminal');
|
|
227
|
+
assert.equal(d.status, 'active', 'D active');
|
|
228
|
+
assert.ok(h.subscriptions(B).some((s) => s.node_id === D && s.active), 'B subscribes_to D (active)');
|
|
229
|
+
}
|
|
230
|
+
// ===================================================================
|
|
231
|
+
// S12 — D FINISHES → pointer to B (its only subscriber).
|
|
232
|
+
// ===================================================================
|
|
233
|
+
await h.finish(D, 'D result body');
|
|
234
|
+
{
|
|
235
|
+
assert.equal(h.node(D).status, 'done', 'D done');
|
|
236
|
+
const dFinal = h.inbox(B).find((e) => e.from === D && e.kind === 'final');
|
|
237
|
+
assert.ok(dFinal, 'B inbox received D\'s final pointer');
|
|
238
|
+
}
|
|
239
|
+
// ===================================================================
|
|
240
|
+
// S13 — B pushes its FINAL up the spine → done/done; the pointer fans to
|
|
241
|
+
// subscribersOf(B) = {A}. NOW A finally hears B (its own explicit
|
|
242
|
+
// push — the only way the chain propagates a hop).
|
|
243
|
+
// ===================================================================
|
|
244
|
+
{
|
|
245
|
+
const res = h.cli(B, ['push', 'final', 'all work complete']);
|
|
246
|
+
assert.equal(res.code, 0, `B push final exit 0\n${res.stderr}`);
|
|
247
|
+
const b = h.node(B);
|
|
248
|
+
assert.equal(b.status, 'done', 'B done after push final');
|
|
249
|
+
assert.equal(b.intent, 'done', 'B intent=done');
|
|
250
|
+
const aInbox = h.inbox(A);
|
|
251
|
+
assert.equal(aInbox.length, 1, 'A inbox now has exactly B\'s final (one hop, on B\'s push)');
|
|
252
|
+
assert.equal(aInbox[0].from, B, 'A\'s entry is from B');
|
|
253
|
+
assert.equal(aInbox[0].kind, 'final', 'A\'s entry is a final');
|
|
254
|
+
}
|
|
255
|
+
// Close B's window faithfully (done branch shutdown).
|
|
256
|
+
await h.stop(B);
|
|
257
|
+
await h.waitForPaneGone(B);
|
|
258
|
+
assert.equal(h.paneAlive(B), false, 'B pane closed on done');
|
|
259
|
+
// A — the resident top-level orchestrator — is still alive and well.
|
|
260
|
+
{
|
|
261
|
+
const a = h.node(A);
|
|
262
|
+
assert.equal(a.status, 'active', 'A still active at the end');
|
|
263
|
+
assert.equal(a.mode, 'orchestrator', 'A still orchestrator');
|
|
264
|
+
assert.equal(a.lifecycle, 'resident', 'A still resident');
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
finally {
|
|
268
|
+
const session = h.session;
|
|
269
|
+
await h.dispose();
|
|
270
|
+
// Teardown leaves NO stray session.
|
|
271
|
+
assert.equal(sessionExists(session), false, 'isolated session killed — no stray');
|
|
272
|
+
}
|
|
273
|
+
});
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
// Run with: node --import tsx/esm --test src/core/__tests__/focuses.test.ts
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// nothing reads it as authority yet (that switch is Step 6). Covers:
|
|
3
|
+
// The `focuses` table (canvas.db, migration v6) + its canvas setters + the
|
|
4
|
+
// placement reads that compose over them. The table is the CANONICAL focus store
|
|
5
|
+
// — there is no focus.ptr file and no dual-write bridge. Covers:
|
|
7
6
|
// - migration v6 adds `focuses` to a fresh db (and a legacy v5 db migrates up);
|
|
8
7
|
// idempotent / forward-only on re-run + re-open
|
|
9
8
|
// - canvas setters/reads round-trip: open / setOccupant / setPane / close;
|
|
@@ -13,23 +12,17 @@
|
|
|
13
12
|
// - independent focus rows don't contend
|
|
14
13
|
// - placement focusOf / isFocused / focusByPane / focusedNodes / listFocuses
|
|
15
14
|
// agree with the rows
|
|
16
|
-
// - dual-write: setFocus populates the table; getFocus falls back to the table
|
|
17
|
-
// when focus.ptr is absent; setFocus('') clears both
|
|
18
15
|
import { test, before, beforeEach, after } from 'node:test';
|
|
19
16
|
import assert from 'node:assert/strict';
|
|
20
|
-
import { mkdtempSync, rmSync
|
|
17
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
21
18
|
import { tmpdir } from 'node:os';
|
|
22
19
|
import { join } from 'node:path';
|
|
23
20
|
import { DatabaseSync } from 'node:sqlite';
|
|
24
21
|
import { openFocusRow, setFocusOccupant, setFocusPane, closeFocusRow, getFocusByNode, getFocusByPane, getFocusById, listFocuses, } from '../canvas/focuses.js';
|
|
25
22
|
import { openDb, closeDb, migrate, MIGRATIONS } from '../canvas/db.js';
|
|
26
|
-
import { canvasDbPath, ensureHome
|
|
23
|
+
import { canvasDbPath, ensureHome } from '../canvas/paths.js';
|
|
27
24
|
import { focusOf, isFocused, focusByPane, focusedNodes, listFocuses as placementListFocuses, } from '../runtime/placement.js';
|
|
28
|
-
import { setFocus, getFocus } from '../runtime/presence.js';
|
|
29
25
|
let home;
|
|
30
|
-
// Saved/restored so the bridge always exercises its deterministic no-tmux path
|
|
31
|
-
// regardless of whether the suite is run from inside a tmux session.
|
|
32
|
-
let savedTmux;
|
|
33
26
|
function userVersion(db) {
|
|
34
27
|
return db.prepare('PRAGMA user_version').get().user_version;
|
|
35
28
|
}
|
|
@@ -39,8 +32,6 @@ function tableNames(db) {
|
|
|
39
32
|
before(() => {
|
|
40
33
|
home = mkdtempSync(join(tmpdir(), 'crtr-focuses-'));
|
|
41
34
|
process.env['CRTR_HOME'] = home;
|
|
42
|
-
savedTmux = process.env['TMUX'];
|
|
43
|
-
delete process.env['TMUX'];
|
|
44
35
|
});
|
|
45
36
|
beforeEach(() => {
|
|
46
37
|
closeDb();
|
|
@@ -50,8 +41,6 @@ after(() => {
|
|
|
50
41
|
closeDb();
|
|
51
42
|
rmSync(home, { recursive: true, force: true });
|
|
52
43
|
delete process.env['CRTR_HOME'];
|
|
53
|
-
if (savedTmux !== undefined)
|
|
54
|
-
process.env['TMUX'] = savedTmux;
|
|
55
44
|
});
|
|
56
45
|
// ---------------------------------------------------------------------------
|
|
57
46
|
// Migration v6 — the additive `focuses` table.
|
|
@@ -205,55 +194,3 @@ test('placement focus reads agree with the focus rows', () => {
|
|
|
205
194
|
assert.deepEqual(focusedNodes(), new Set(['A', 'B']));
|
|
206
195
|
assert.deepEqual(placementListFocuses().map((f) => f.node_id), ['A', 'B']);
|
|
207
196
|
});
|
|
208
|
-
// ---------------------------------------------------------------------------
|
|
209
|
-
// Dual-write bridge — setFocus populates the table; getFocus falls back to the
|
|
210
|
-
// table when focus.ptr is absent; setFocus('') clears both.
|
|
211
|
-
// ---------------------------------------------------------------------------
|
|
212
|
-
function focusPtrPath() {
|
|
213
|
-
return join(crtrHome(), 'focus.ptr');
|
|
214
|
-
}
|
|
215
|
-
test('setFocus populates the focuses table in lockstep with focus.ptr', () => {
|
|
216
|
-
openDb();
|
|
217
|
-
setFocus('A');
|
|
218
|
-
assert.equal(getFocus(), 'A', 'focus.ptr reads back');
|
|
219
|
-
const row = getFocusByNode('A');
|
|
220
|
-
assert.ok(row, 'a canonical focus row mirrors the current focus');
|
|
221
|
-
assert.equal(row?.node_id, 'A');
|
|
222
|
-
assert.equal(isFocused('A'), true, 'placement.isFocused agrees');
|
|
223
|
-
assert.deepEqual(focusOf('A')?.node_id, 'A', 'placement.focusOf agrees with getFocus');
|
|
224
|
-
// Re-focusing a different node re-points the SAME canonical row (no stray rows,
|
|
225
|
-
// UNIQUE(node_id) upheld).
|
|
226
|
-
setFocus('B');
|
|
227
|
-
assert.equal(getFocus(), 'B');
|
|
228
|
-
assert.equal(getFocusByNode('A'), null, 'the old occupant is dropped');
|
|
229
|
-
assert.equal(getFocusByNode('B')?.node_id, 'B');
|
|
230
|
-
assert.equal(listFocuses().length, 1, 'still exactly one canonical row');
|
|
231
|
-
});
|
|
232
|
-
test('getFocus falls back to the table when focus.ptr is absent', () => {
|
|
233
|
-
openDb();
|
|
234
|
-
setFocus('A'); // writes both focus.ptr and the canonical row
|
|
235
|
-
// Simulate a missing pointer (a writer that reached only the table, or a lost
|
|
236
|
-
// file): delete focus.ptr and confirm getFocus recovers the focus from the row.
|
|
237
|
-
if (existsSync(focusPtrPath()))
|
|
238
|
-
unlinkSync(focusPtrPath());
|
|
239
|
-
assert.equal(getFocus(), 'A', 'getFocus recovers the focus from the table');
|
|
240
|
-
});
|
|
241
|
-
test("setFocus('') clears both the pointer and the canonical focus row", () => {
|
|
242
|
-
openDb();
|
|
243
|
-
setFocus('A');
|
|
244
|
-
assert.equal(getFocus(), 'A');
|
|
245
|
-
assert.ok(getFocusByNode('A'), 'precondition: row present');
|
|
246
|
-
setFocus('');
|
|
247
|
-
assert.equal(getFocus(), null, 'getFocus is null after clear (ptr empty, no row)');
|
|
248
|
-
assert.equal(getFocusByNode('A'), null, 'the canonical row was closed');
|
|
249
|
-
assert.deepEqual(listFocuses(), [], 'no focus rows remain');
|
|
250
|
-
});
|
|
251
|
-
test('a focus row written directly (no focus.ptr) is visible through getFocus + placement', () => {
|
|
252
|
-
openDb();
|
|
253
|
-
// A writer that reached only the table (the canonical bridge row), with no
|
|
254
|
-
// focus.ptr on disk at all.
|
|
255
|
-
openFocusRow('__focus_ptr__', null, null, 'X');
|
|
256
|
-
assert.ok(!existsSync(focusPtrPath()), 'precondition: no focus.ptr file');
|
|
257
|
-
assert.equal(getFocus(), 'X', 'getFocus falls back to the canonical row');
|
|
258
|
-
assert.equal(isFocused('X'), true);
|
|
259
|
-
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// Run with: node --import tsx/esm --test src/core/__tests__/grace-clock.test.ts
|
|
2
|
+
//
|
|
3
|
+
// AXIS: the REVIVE_GRACE_MS double-spawn guard (daemon invariant 10), exercised
|
|
4
|
+
// FAITHFULLY with a CONTROLLED CLOCK via the harness's injectable tick(now).
|
|
5
|
+
//
|
|
6
|
+
// Why this exists (MINOR-4): the harness exposes superviseTick's injectable
|
|
7
|
+
// `now` through h.tick(now), but every other faithful test calls h.tick() with
|
|
8
|
+
// no arg — so the grace window (the guard that a pi observed dead-while-its-pane-
|
|
9
|
+
// lives must pend through REVIVE_GRACE_MS before a revive, lest a revive land in
|
|
10
|
+
// the transient old-pi-dies→fresh-pi-boots gap and DOUBLE-SPAWN) was never
|
|
11
|
+
// exercised end-to-end. daemon-liveness.test.ts pins livenessVerdict purely and
|
|
12
|
+
// drives superviseTick with a FABRICATED pi-death (deadPid); this drives a REAL
|
|
13
|
+
// fake-pi boot, kills it under a FROZEN (remain-on-exit) pane so the pane stays
|
|
14
|
+
// alive while pi is genuinely dead, then walks a deterministic clock across the
|
|
15
|
+
// grace boundary.
|
|
16
|
+
//
|
|
17
|
+
// This file is ADDITIVE and uses ONLY the public Harness API + h.tick(now) +
|
|
18
|
+
// test-local tmux/file reads (the same shape as live-mutation.test.ts's
|
|
19
|
+
// firstPaneOf/demote helpers). It does NOT edit harness.ts / fake-pi-host.ts or
|
|
20
|
+
// any production file, and adds no harness helper.
|
|
21
|
+
import { test } from 'node:test';
|
|
22
|
+
import assert from 'node:assert/strict';
|
|
23
|
+
import { spawnSync } from 'node:child_process';
|
|
24
|
+
import { readFileSync } from 'node:fs';
|
|
25
|
+
import { join } from 'node:path';
|
|
26
|
+
import { createHarness, hasTmux } from './helpers/harness.js';
|
|
27
|
+
import { isPidAlive } from '../../daemon/crtrd.js';
|
|
28
|
+
const SKIP = !hasTmux() ? 'tmux unavailable' : false;
|
|
29
|
+
function sessionExists(session) {
|
|
30
|
+
return spawnSync('tmux', ['has-session', '-t', session], { stdio: 'ignore' }).status === 0;
|
|
31
|
+
}
|
|
32
|
+
/** Count the fake-pi boots recorded for a node (one line per boot in
|
|
33
|
+
* fake-pi.boots.jsonl) — the observable for "did a revive double-spawn?". */
|
|
34
|
+
function bootCount(home, id) {
|
|
35
|
+
try {
|
|
36
|
+
return readFileSync(join(home, 'nodes', id, 'fake-pi.boots.jsonl'), 'utf8')
|
|
37
|
+
.split('\n')
|
|
38
|
+
.filter((l) => l.trim() !== '').length;
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return 0;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// REVIVE_GRACE_MS is 20_000 (crtrd.ts). We choose offsets well inside / well
|
|
45
|
+
// past it so the test is robust to the exact value without importing it.
|
|
46
|
+
const NOW = 5_000_000;
|
|
47
|
+
const WITHIN_GRACE = NOW + 10_000; // < 20s after first-observed-dead → still pending
|
|
48
|
+
const PAST_GRACE = NOW + 25_000; // > 20s after first-observed-dead → revive
|
|
49
|
+
// ===========================================================================
|
|
50
|
+
// The grace window guards against a double-spawn: while a node's pane is alive
|
|
51
|
+
// but its pi has been observed dead for LESS than REVIVE_GRACE_MS, the daemon
|
|
52
|
+
// must NOT revive (a revive there would race the in-flight respawn and spawn a
|
|
53
|
+
// second vehicle on the same pane). Once the pi has been dead PAST the grace, a
|
|
54
|
+
// revive proceeds.
|
|
55
|
+
// ===========================================================================
|
|
56
|
+
test('grace clock: a dead-pi/alive-pane node does NOT revive within REVIVE_GRACE_MS, but DOES once it elapses', { skip: SKIP, timeout: 120_000 }, async () => {
|
|
57
|
+
const h = await createHarness({ sessionPrefix: 'crtr-grace' });
|
|
58
|
+
try {
|
|
59
|
+
const A = h.spawnRoot('resident root');
|
|
60
|
+
const B = await h.spawnChild(A, 'do the work', { kind: 'developer' });
|
|
61
|
+
// The boot proof carries the live fake-pi pid; the row records it via the
|
|
62
|
+
// real session_start hook. Both are the SAME process — that pid is what
|
|
63
|
+
// handleLiveWindow judges liveness on.
|
|
64
|
+
const boot = await h.awaitBoot(B);
|
|
65
|
+
const b0 = h.node(B);
|
|
66
|
+
assert.equal(b0.status, 'active', 'B active after boot');
|
|
67
|
+
assert.equal(b0.intent ?? null, null, 'B intent=null (NOT idle-release — the grace path, not the frozen early-return)');
|
|
68
|
+
assert.equal(b0.pi_pid, boot.pid, 'row pi_pid == the live fake-pi pid (recorded at session_start)');
|
|
69
|
+
assert.equal(bootCount(h.home, B), 1, 'exactly one boot so far');
|
|
70
|
+
// Arm remain-on-exit on B's window so that when we kill its pi the PANE
|
|
71
|
+
// survives (frozen) rather than closing — that is the only way to produce
|
|
72
|
+
// the "pane alive but pi dead" state handleLiveWindow's grace path judges.
|
|
73
|
+
const ro = spawnSync('tmux', ['set-window-option', '-t', b0.window, 'remain-on-exit', 'on'], { stdio: 'ignore' });
|
|
74
|
+
assert.equal(ro.status, 0, 'armed remain-on-exit on B\'s window');
|
|
75
|
+
// Kill the fake-pi. pi dies; the frozen pane stays alive.
|
|
76
|
+
process.kill(boot.pid, 'SIGKILL');
|
|
77
|
+
await h.waitFor(() => !isPidAlive(boot.pid), { timeoutMs: 10_000, label: 'fake-pi pid dead' });
|
|
78
|
+
assert.equal(h.paneAlive(B), true, 'pane is FROZEN alive after pi death (remain-on-exit)');
|
|
79
|
+
// --- TICK 1 @ NOW: first observation of the dead pi → 'pending'. The
|
|
80
|
+
// daemon records first-observed-dead and does NOT revive. ---
|
|
81
|
+
await h.tick(NOW);
|
|
82
|
+
{
|
|
83
|
+
const b = h.node(B);
|
|
84
|
+
assert.equal(bootCount(h.home, B), 1, 'first tick: NO revive on first-observed-dead (still 1 boot)');
|
|
85
|
+
assert.equal(b.pi_pid, boot.pid, 'pi_pid unchanged — no fresh vehicle spawned');
|
|
86
|
+
assert.equal(b.status, 'active', 'B left active (pending, not revived)');
|
|
87
|
+
assert.equal(h.paneAlive(B), true, 'frozen pane still alive');
|
|
88
|
+
}
|
|
89
|
+
// --- TICK 2 @ NOW+10s (WITHIN the 20s grace): STILL pending. This is the
|
|
90
|
+
// double-spawn guard: a revive here would land in the respawn gap. ---
|
|
91
|
+
await h.tick(WITHIN_GRACE);
|
|
92
|
+
{
|
|
93
|
+
const b = h.node(B);
|
|
94
|
+
assert.equal(bootCount(h.home, B), 1, 'within REVIVE_GRACE_MS: NO double-spawn — the dead pi must pend, not revive');
|
|
95
|
+
assert.equal(b.pi_pid, boot.pid, 'pi_pid STILL the dead pid — guard held');
|
|
96
|
+
assert.equal(b.status, 'active', 'B still active inside the grace window');
|
|
97
|
+
}
|
|
98
|
+
// --- TICK 3 @ NOW+25s (PAST the 20s grace): now a revive proceeds — a
|
|
99
|
+
// FRESH fake-pi boots in the frozen pane (respawn-pane -k resume). ---
|
|
100
|
+
await h.tick(PAST_GRACE);
|
|
101
|
+
await h.awaitBoot(B, { minCount: 2, timeoutMs: 30_000 });
|
|
102
|
+
assert.ok(bootCount(h.home, B) >= 2, 'past REVIVE_GRACE_MS: the dead pi is revived → a fresh vehicle boots');
|
|
103
|
+
await h.waitForStatus(B, 'active');
|
|
104
|
+
{
|
|
105
|
+
const b = h.node(B);
|
|
106
|
+
assert.equal(b.status, 'active', 'B active after the grace-window revive');
|
|
107
|
+
assert.notEqual(b.pi_pid, boot.pid, 'pi_pid advanced to the fresh vehicle — the revive landed');
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
finally {
|
|
111
|
+
const session = h.session;
|
|
112
|
+
await h.dispose();
|
|
113
|
+
assert.equal(sessionExists(session), false, 'isolated session killed — no stray');
|
|
114
|
+
}
|
|
115
|
+
});
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { NodeMeta, NodeStatus, Mode, Lifecycle } from '../../canvas/types.js';
|
|
2
|
+
import type { InboxEntry } from '../../feed/inbox.js';
|
|
3
|
+
/** True when a usable tmux is on PATH — tests gate on this and SKIP otherwise. */
|
|
4
|
+
export declare function hasTmux(): boolean;
|
|
5
|
+
export interface WaitOpts {
|
|
6
|
+
timeoutMs?: number;
|
|
7
|
+
intervalMs?: number;
|
|
8
|
+
label?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface Injected {
|
|
11
|
+
content: string;
|
|
12
|
+
deliverAs?: string;
|
|
13
|
+
}
|
|
14
|
+
export interface BootProof {
|
|
15
|
+
pid: number;
|
|
16
|
+
nodeId: string;
|
|
17
|
+
resuming: boolean;
|
|
18
|
+
prompt: string | null;
|
|
19
|
+
extPaths: string[];
|
|
20
|
+
loaded: string[];
|
|
21
|
+
failedExt: string[];
|
|
22
|
+
env: Record<string, string | null>;
|
|
23
|
+
[k: string]: unknown;
|
|
24
|
+
}
|
|
25
|
+
export interface CliResult {
|
|
26
|
+
code: number;
|
|
27
|
+
stdout: string;
|
|
28
|
+
stderr: string;
|
|
29
|
+
json?: unknown;
|
|
30
|
+
}
|
|
31
|
+
export interface HarnessOpts {
|
|
32
|
+
sessionPrefix?: string;
|
|
33
|
+
}
|
|
34
|
+
export interface SpawnOpts {
|
|
35
|
+
kind?: string;
|
|
36
|
+
mode?: Mode;
|
|
37
|
+
lifecycle?: Lifecycle;
|
|
38
|
+
id?: string;
|
|
39
|
+
}
|
|
40
|
+
export interface Harness {
|
|
41
|
+
home: string;
|
|
42
|
+
session: string;
|
|
43
|
+
spawnRoot(task: string, o?: SpawnOpts): string;
|
|
44
|
+
spawnChild(parentId: string, task: string, o?: SpawnOpts): Promise<string>;
|
|
45
|
+
cli(nodeId: string | null, args: string[]): CliResult;
|
|
46
|
+
turn(nodeId: string, text?: string): Promise<void>;
|
|
47
|
+
stop(nodeId: string, reason?: 'stop' | 'length' | 'aborted' | 'error'): Promise<void>;
|
|
48
|
+
finish(nodeId: string, finalText: string): Promise<void>;
|
|
49
|
+
yieldNode(nodeId: string, note: string): Promise<void>;
|
|
50
|
+
tick(now?: number): Promise<void>;
|
|
51
|
+
awaitBoot(nodeId: string, o?: {
|
|
52
|
+
minCount?: number;
|
|
53
|
+
timeoutMs?: number;
|
|
54
|
+
}): Promise<BootProof>;
|
|
55
|
+
awaitWake(nodeId: string, o?: {
|
|
56
|
+
sinceCount?: number;
|
|
57
|
+
timeoutMs?: number;
|
|
58
|
+
match?: RegExp;
|
|
59
|
+
}): Promise<string[]>;
|
|
60
|
+
waitForStatus(nodeId: string, status: NodeStatus, timeoutMs?: number): Promise<void>;
|
|
61
|
+
waitForPaneGone(nodeId: string, timeoutMs?: number): Promise<void>;
|
|
62
|
+
waitFor<T>(probe: () => T | undefined | null | false, o?: WaitOpts): Promise<T>;
|
|
63
|
+
node(nodeId: string): NodeMeta | null;
|
|
64
|
+
status(nodeId: string): NodeStatus | null;
|
|
65
|
+
paneAlive(nodeId: string): boolean;
|
|
66
|
+
inbox(nodeId: string): InboxEntry[];
|
|
67
|
+
injected(nodeId: string): Injected[];
|
|
68
|
+
subscribers(nodeId: string): {
|
|
69
|
+
node_id: string;
|
|
70
|
+
active: boolean;
|
|
71
|
+
}[];
|
|
72
|
+
subscriptions(nodeId: string): {
|
|
73
|
+
node_id: string;
|
|
74
|
+
active: boolean;
|
|
75
|
+
}[];
|
|
76
|
+
dispose(): Promise<void>;
|
|
77
|
+
}
|
|
78
|
+
export declare function createHarness(opts?: HarnessOpts): Promise<Harness>;
|