@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,73 +1,80 @@
|
|
|
1
1
|
// Run with: node --import tsx/esm --test src/core/__tests__/flagship-lifecycle.test.ts
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
3
|
+
// HEADLESS RETARGET (foundation-spec §C.9 + §E). The flagship end-to-end
|
|
4
|
+
// lifecycle scenario, driven entirely at the MODEL layer against FABRICATED
|
|
5
|
+
// broker-hosted (paneless) nodes — no real tmux session, no pane chrome, and NO
|
|
6
|
+
// real broker boot. The full status×intent×lifecycle×mode state machine is host-
|
|
7
|
+
// independent: every hop is a CLI lifecycle verb (promote/yield) or a pure model
|
|
8
|
+
// op (fabricate + subscribe + transition + pushFinal + superviseTick), so the
|
|
9
|
+
// scenario never pays the ~5s SDK-boot cost.
|
|
8
10
|
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
11
|
+
// THREE-PART LOCK HEADER ──────────────────────────────────────────────────
|
|
12
|
+
// (1) BUG LOCKED — the full lifecycle state machine (state-model §2): each hop
|
|
13
|
+
// (spawn → terminal idle-release dormancy → one-hop wake → promote → yield-
|
|
14
|
+
// refresh → finish) must land in the correct durable (status,intent,
|
|
15
|
+
// lifecycle,mode) and route completion ONE HOP up the spine (a push fans to
|
|
16
|
+
// DIRECT subscribers only; an indirect ancestor hears nothing until a middle
|
|
17
|
+
// node re-pushes).
|
|
18
|
+
// (2) WHY MODEL-LEVEL, NOT PANE/WINDOW — every transition lives in canvas.db +
|
|
19
|
+
// the lifecycle table (transition()) + the daemon's second-pass wake gate +
|
|
20
|
+
// the feed fan-out. A tmux node showed dormancy as a closed pane; a broker
|
|
21
|
+
// shows it as an EXITED process (status=idle, intent=idle-release, pi_pid
|
|
22
|
+
// dead) — fabricated directly. The only thing a pane ever added was a visible
|
|
23
|
+
// dormancy signal, which a broker expresses as a dead pid. No pane is read.
|
|
24
|
+
// (3) HOW THE HEADLESS DRIVE STILL FAILS IF THE FIX REGRESSES — the one-hop wake
|
|
25
|
+
// is asserted via getNode(B).cycles (reviveNode bumps cycles +
|
|
26
|
+
// transition('revive') BEFORE the detached, unawaited broker spawn — revive.
|
|
27
|
+
// ts:114,145 — so the wake is observable INSTANTLY). If the daemon's second-
|
|
28
|
+
// pass reviveNode is removed, the post-inbox tick never bumps cycles → S7's
|
|
29
|
+
// "B revived" asserts go RED. If a push wrongly fanned past its direct
|
|
30
|
+
// subscriber, the "A inbox empty" one-hop asserts go RED.
|
|
16
31
|
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
// •
|
|
32
|
+
// STOPHOOK CAVEAT — split (option b). The original leaned on the REAL stophook
|
|
33
|
+
// committing idle-release / the stall reprompt / the persona steer INSIDE a live
|
|
34
|
+
// broker at agent_end/turn_end. Fabrication can set the post-stop row but cannot
|
|
35
|
+
// run that hook. So for each such point this test fabricates the post-stop MODEL
|
|
36
|
+
// state (or asserts the PURE decision the hook enacts — evaluateStop /
|
|
37
|
+
// personaDrift) and asserts the downstream lifecycle/daemon CONSEQUENCE, leaving
|
|
38
|
+
// the hook-commit proof to the already-pure siblings:
|
|
39
|
+
// • idle-release commit at agent_end → canvas-stophook-agentend.test.ts
|
|
40
|
+
// ('natural stop while awaiting a live worker → idle-release …').
|
|
41
|
+
// • stall reprompt at agent_end → canvas-stophook-agentend.test.ts
|
|
42
|
+
// ('stalled leaf … is still reprompted').
|
|
43
|
+
// • persona-drift steer + ack commit at turn_end → persona.test.ts
|
|
44
|
+
// ('personaDrift detects base→orchestrator after promote, then clears on
|
|
45
|
+
// commit'). Here we assert only the PENDING drift the injector would deliver.
|
|
25
46
|
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
// •
|
|
32
|
-
//
|
|
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) — faithful E2E:
|
|
40
|
-
// detach-focus.test.ts (the real verb on a FOCUSED live node → terminal +
|
|
41
|
-
// pane relocated to the backstage + focus row CLOSED).
|
|
42
|
-
// • focused-finish → manager-TAKEOVER (handFocusToManager swap) — NOT harness-
|
|
43
|
-
// reachable: the harness root is a paneless never-booted row, so the done-
|
|
44
|
-
// branch only ever hits the paneless-manager false guard (closeFocusToShell),
|
|
45
|
-
// never a live/dormant-idle-release takeover. Unit: placement-teardown.test.ts.
|
|
46
|
-
// • node msg / focus / cycle wake of a dormant node (A7) — untested faithfully.
|
|
47
|
-
// These are intentional boundaries, not oversights.
|
|
47
|
+
// 2×2 coverage (mode {base,orchestrator} × lifecycle {terminal,resident}):
|
|
48
|
+
// • base×resident — A's fabricated row shape at S1 (a spawnRoot
|
|
49
|
+
// fixture; the real resident-birth boot path is
|
|
50
|
+
// structurally unreachable headlessly).
|
|
51
|
+
// • orchestrator×resident — A after promote (top-level orchestrator).
|
|
52
|
+
// • base×terminal — B/C/D workers (fabricated managed children).
|
|
53
|
+
// • orchestrator×terminal — B after promote (sub-orchestrator).
|
|
48
54
|
import { test } from 'node:test';
|
|
49
55
|
import assert from 'node:assert/strict';
|
|
50
56
|
import { spawnSync } from 'node:child_process';
|
|
51
|
-
import {
|
|
52
|
-
import {
|
|
53
|
-
|
|
54
|
-
|
|
57
|
+
import { createHeadlessHarness } from './helpers/harness.js';
|
|
58
|
+
import { subscribe } from '../canvas/canvas.js';
|
|
59
|
+
import { transition } from '../runtime/lifecycle.js';
|
|
60
|
+
import { pushFinal } from '../feed/feed.js';
|
|
61
|
+
import { evaluateStop, STALL_REPROMPT } from '../runtime/stop-guard.js';
|
|
62
|
+
import { personaDrift, commitPersonaAck } from '../runtime/persona.js';
|
|
63
|
+
// A pid reaped (dead) by the time spawnSync returns — a released broker's exited
|
|
64
|
+
// process (the headless analog of a closed pane).
|
|
65
|
+
function deadPid() {
|
|
66
|
+
const r = spawnSync('true', [], { stdio: 'ignore' });
|
|
67
|
+
return r.pid ?? 0x7ffffffe;
|
|
55
68
|
}
|
|
56
|
-
test('flagship: full node lifecycle — spawn, dormancy, one-hop wake, promote, yield, finish
|
|
57
|
-
const h = await
|
|
69
|
+
test('flagship: full node lifecycle — spawn, dormancy, one-hop wake, promote, yield, finish (headless)', { timeout: 30_000 }, async () => {
|
|
70
|
+
const h = await createHeadlessHarness({ sessionPrefix: 'crtr-flagship' });
|
|
58
71
|
try {
|
|
59
72
|
// ===================================================================
|
|
60
|
-
// S1 — A's
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
// from the harness (harness-design Wall #7). These four asserts therefore
|
|
66
|
-
// PIN THE PERSISTED ROW SHAPE the rest of the scenario builds on — they
|
|
67
|
-
// do NOT prove the real resident-birth path. The resident-at-birth 2×2
|
|
68
|
-
// quadrant is out of reach; the resident *behavior* (no idle-release) is
|
|
69
|
-
// exercised faithfully in live-mutation.test.ts, and the genuine real
|
|
70
|
-
// birth of a managed child (→ terminal) IS exercised at S3 below.
|
|
73
|
+
// S1 — A's ROW SHAPE: base × RESIDENT (the user's front door). A spawnRoot
|
|
74
|
+
// fixture pins the PERSISTED row shape the scenario builds on; the real
|
|
75
|
+
// resident-birth boot path (bootRoot execs pi inline) is unreachable
|
|
76
|
+
// headlessly, exactly as in the tmux flagship. The resident BEHAVIOR (no
|
|
77
|
+
// idle-release) is exercised in live-mutation.test.ts.
|
|
71
78
|
// ===================================================================
|
|
72
79
|
const A = h.spawnRoot('top-level orchestrator');
|
|
73
80
|
{
|
|
@@ -78,8 +85,9 @@ test('flagship: full node lifecycle — spawn, dormancy, one-hop wake, promote,
|
|
|
78
85
|
assert.equal(a.intent ?? null, null, 'A no intent');
|
|
79
86
|
}
|
|
80
87
|
// ===================================================================
|
|
81
|
-
// S2 — A promotes: orchestrator × RESIDENT (top-level orchestrator).
|
|
82
|
-
// mode flips
|
|
88
|
+
// S2 — A promotes: orchestrator × RESIDENT (top-level orchestrator). The
|
|
89
|
+
// real `node promote` verb; mode flips, lifecycle/status/intent
|
|
90
|
+
// untouched (no transition()).
|
|
83
91
|
// ===================================================================
|
|
84
92
|
{
|
|
85
93
|
const res = h.cli(A, ['node', 'promote']);
|
|
@@ -91,88 +99,98 @@ test('flagship: full node lifecycle — spawn, dormancy, one-hop wake, promote,
|
|
|
91
99
|
assert.equal(a.intent ?? null, null, 'A intent untouched by promote');
|
|
92
100
|
}
|
|
93
101
|
// ===================================================================
|
|
94
|
-
// S3 — A spawns terminal child B: base × TERMINAL.
|
|
95
|
-
//
|
|
102
|
+
// S3 — A spawns terminal child B: base × TERMINAL. Fabricated as a broker-
|
|
103
|
+
// hosted node (pi_pid dead + pi_session_id set, so its later dormant
|
|
104
|
+
// wake RESUMES that session); the spine seed is wired explicitly
|
|
105
|
+
// (fabrication does not auto-subscribe). B is born acked to its own
|
|
106
|
+
// persona (commitPersonaAck), mirroring a real birth.
|
|
96
107
|
// ===================================================================
|
|
97
|
-
const B =
|
|
108
|
+
const B = h.fabricateBrokerNode({
|
|
109
|
+
parent: A, kind: 'developer', mode: 'base', lifecycle: 'terminal',
|
|
110
|
+
status: 'active', pi_pid: deadPid(), pi_session_id: 'sess-B',
|
|
111
|
+
});
|
|
112
|
+
subscribe(A, B, true); // the load-bearing spine seed: A subscribes_to B (active).
|
|
113
|
+
commitPersonaAck(B, { mode: 'base', lifecycle: 'terminal' }); // born acked to its persona
|
|
98
114
|
{
|
|
99
115
|
const b = h.node(B);
|
|
116
|
+
assert.equal(b.host_kind, 'broker', 'B broker-hosted (paneless)');
|
|
100
117
|
assert.equal(b.mode, 'base', 'B born base');
|
|
101
118
|
assert.equal(b.lifecycle, 'terminal', 'B born terminal (managed child)');
|
|
102
|
-
assert.equal(b.status, 'active', 'B active
|
|
119
|
+
assert.equal(b.status, 'active', 'B active');
|
|
103
120
|
assert.equal(b.intent ?? null, null, 'B no intent');
|
|
104
121
|
assert.equal(b.parent, A, 'B spawned_by / parent = A');
|
|
105
|
-
|
|
106
|
-
const subsToB = h.subscribers(B);
|
|
107
|
-
assert.deepEqual(subsToB, [{ node_id: A, active: true }], 'A is B\'s sole ACTIVE subscriber (spawn seed)');
|
|
122
|
+
assert.deepEqual(h.subscribers(B), [{ node_id: A, active: true }], "A is B's sole ACTIVE subscriber (spine seed)");
|
|
108
123
|
assert.equal(h.inbox(A).length, 0, 'A inbox empty (spawn pushes nothing)');
|
|
109
124
|
}
|
|
110
125
|
// ===================================================================
|
|
111
126
|
// S4 — B spawns child C (base × terminal). B now holds an active live
|
|
112
127
|
// subscription to C → B is legitimately "awaiting" if it stops.
|
|
113
128
|
// ===================================================================
|
|
114
|
-
const C =
|
|
129
|
+
const C = h.fabricateBrokerNode({ parent: B, mode: 'base', lifecycle: 'terminal', status: 'active' });
|
|
130
|
+
subscribe(B, C, true);
|
|
115
131
|
{
|
|
116
132
|
const c = h.node(C);
|
|
117
133
|
assert.equal(c.mode, 'base', 'C base');
|
|
118
134
|
assert.equal(c.lifecycle, 'terminal', 'C terminal');
|
|
119
135
|
assert.equal(c.status, 'active', 'C active');
|
|
120
|
-
assert.deepEqual(h.subscribers(C), [{ node_id: B, active: true }],
|
|
136
|
+
assert.deepEqual(h.subscribers(C), [{ node_id: B, active: true }], "B is C's sole ACTIVE subscriber");
|
|
121
137
|
assert.deepEqual(h.subscriptions(B), [{ node_id: C, active: true }], 'B subscribes_to C (active)');
|
|
122
138
|
assert.equal(h.status(B), 'active', 'B still active');
|
|
123
139
|
}
|
|
124
140
|
// ===================================================================
|
|
125
|
-
// S5 — B goes DORMANT.
|
|
126
|
-
//
|
|
127
|
-
//
|
|
141
|
+
// S5 — B goes DORMANT. PURE precondition: B is TERMINAL holding an active
|
|
142
|
+
// live sub to C, so the stop-guard classifies its stop 'awaiting'
|
|
143
|
+
// (legitimate idle-release). We then enact the post-stop MODEL state
|
|
144
|
+
// directly: transition('release') → idle / idle-release (a released
|
|
145
|
+
// broker is a dead pid; pi_pid was fabricated dead at S3). The actual
|
|
146
|
+
// agent_end idle-release COMMIT is locked by canvas-stophook-agentend
|
|
147
|
+
// ('natural stop while awaiting a live worker → idle-release …').
|
|
128
148
|
// ===================================================================
|
|
129
|
-
|
|
130
|
-
|
|
149
|
+
{
|
|
150
|
+
const stop = evaluateStop(B, { pushedFinal: false, askedHuman: false });
|
|
151
|
+
assert.equal(stop.action, 'allow', 'awaiting terminal B → allow (legit idle-release)');
|
|
152
|
+
assert.equal(stop.reason, 'awaiting', "B classified awaiting on its ACTIVE live sub to C");
|
|
153
|
+
}
|
|
154
|
+
transition(B, 'release');
|
|
131
155
|
{
|
|
132
156
|
const b = h.node(B);
|
|
133
157
|
assert.equal(b.status, 'idle', 'B idle (released)');
|
|
134
158
|
assert.equal(b.intent, 'idle-release', 'B intent=idle-release');
|
|
135
159
|
}
|
|
136
|
-
await h.waitForPaneGone(B);
|
|
137
|
-
assert.equal(h.paneAlive(B), false, 'B unfocused → pane closed on idle-release');
|
|
138
160
|
assert.equal(h.status(C), 'active', 'C still active while B sleeps');
|
|
139
|
-
|
|
140
|
-
assert.equal(h.status(A), 'active', 'A still active');
|
|
161
|
+
assert.equal(h.status(A), 'active', 'A still active (a dormant worker does not disturb its manager)');
|
|
141
162
|
assert.equal(h.inbox(A).length, 0, 'A inbox still empty');
|
|
142
163
|
// ===================================================================
|
|
143
|
-
// S6 — C FINISHES (
|
|
164
|
+
// S6 — C FINISHES (pushFinal). The pointer fans to subscribersOf(C) = {B}
|
|
144
165
|
// ONLY. C → done/done. A is NOT a subscriber of C → A inbox stays
|
|
145
|
-
// empty
|
|
166
|
+
// empty — the one-hop wake contract at the push site.
|
|
146
167
|
// ===================================================================
|
|
147
|
-
await
|
|
168
|
+
await pushFinal(C, 'C result body');
|
|
148
169
|
{
|
|
149
170
|
const c = h.node(C);
|
|
150
171
|
assert.equal(c.status, 'done', 'C done');
|
|
151
172
|
assert.equal(c.intent, 'done', 'C intent=done');
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
const cFinal = bInbox.find((e) => e.from === C && e.kind === 'final');
|
|
155
|
-
assert.ok(cFinal, 'B inbox received C\'s final pointer (one hop)');
|
|
156
|
-
// ORACLE: ONE-HOP. A is not woken — only B (the direct subscriber) hears C.
|
|
173
|
+
const cFinal = h.inbox(B).find((e) => e.from === C && e.kind === 'final');
|
|
174
|
+
assert.ok(cFinal, "B inbox received C's final pointer (one hop)");
|
|
157
175
|
assert.equal(h.inbox(A).length, 0, 'A inbox empty — A NOT woken by C (one-hop)');
|
|
158
176
|
}
|
|
159
177
|
// ===================================================================
|
|
160
178
|
// S7 — WAKE B. The in-process daemon second pass sees B idle/idle-release,
|
|
161
|
-
// pi dead, an unseen inbox entry → reviveNode(resume).
|
|
162
|
-
//
|
|
179
|
+
// pi dead, an unseen inbox entry → reviveNode(resume). reviveNode
|
|
180
|
+
// bumps cycles + transition('revive') BEFORE the (detached, unawaited)
|
|
181
|
+
// broker spawn, so the wake is observable INSTANTLY via cycles — no
|
|
182
|
+
// awaitBoot. (The real inbox-watcher's redelivery of C's report on the
|
|
183
|
+
// fresh boot is an engine artifact, out of the model tier; the wake
|
|
184
|
+
// DECISION is locked here by cycles + the S6 inbox entry.)
|
|
163
185
|
// ===================================================================
|
|
164
|
-
|
|
165
|
-
await h.tick();
|
|
166
|
-
await h.waitForStatus(B, 'active');
|
|
186
|
+
assert.equal(h.node(B).cycles ?? 0, 0, 'B not yet revived');
|
|
187
|
+
await h.tick();
|
|
167
188
|
{
|
|
168
189
|
const b = h.node(B);
|
|
190
|
+
assert.equal(b.cycles ?? 0, 1, 'unseen inbox + tick → daemon pass-2 REVIVED (cycles bumped to 1)');
|
|
169
191
|
assert.equal(b.status, 'active', 'B revived → active');
|
|
170
192
|
assert.equal(b.intent ?? null, null, 'B intent cleared by revive');
|
|
171
193
|
}
|
|
172
|
-
await h.awaitBoot(B, { minCount: 2 }); // the resume boot (spawn boot + wake boot)
|
|
173
|
-
const wakeDigests = await h.awaitWake(B, { sinceCount: injBeforeWake, match: /C result body/ });
|
|
174
|
-
assert.ok(wakeDigests.some((d) => /C result body/.test(d)), 'B\'s real inbox-watcher delivered C\'s report after the wake');
|
|
175
|
-
// Still one-hop: A remained dormant-as-resident and was never touched.
|
|
176
194
|
assert.equal(h.inbox(A).length, 0, 'A inbox STILL empty after B woke (one-hop confirmed)');
|
|
177
195
|
assert.equal(h.status(A), 'active', 'A unchanged');
|
|
178
196
|
// ===================================================================
|
|
@@ -189,35 +207,45 @@ test('flagship: full node lifecycle — spawn, dormancy, one-hop wake, promote,
|
|
|
189
207
|
assert.equal(b.intent ?? null, null, 'B intent untouched by promote');
|
|
190
208
|
}
|
|
191
209
|
// ===================================================================
|
|
192
|
-
// S9 —
|
|
193
|
-
//
|
|
194
|
-
//
|
|
195
|
-
//
|
|
210
|
+
// S9 — The next turn's hooks, asserted as the PURE decisions they enact:
|
|
211
|
+
// (a) turn_end would inject the base→orchestrator persona-drift steer —
|
|
212
|
+
// personaDrift(B) reports that PENDING drift (the guidance the
|
|
213
|
+
// injector delivers). The actual delivery + ack commit is locked
|
|
214
|
+
// by persona.test.ts.
|
|
215
|
+
// (b) agent_end would stall (orchestrator, no live sub — C done, D not
|
|
216
|
+
// yet spawned — no final) → evaluateStop → 'stalled'/STALL_REPROMPT.
|
|
217
|
+
// The actual reprompt injection is locked by canvas-stophook-agentend.
|
|
196
218
|
// ===================================================================
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
}
|
|
208
|
-
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)');
|
|
209
|
-
assert.ok(turnInjected.some((e) => e.content.includes(STALL_REPROMPT)), 'agent_end stalled (orchestrator, no live sub, no final) → STALL_REPROMPT');
|
|
210
|
-
assert.equal(h.status(B), 'active', 'B still active after the turn (reprompt keeps it alive)');
|
|
219
|
+
{
|
|
220
|
+
const drift = personaDrift(B);
|
|
221
|
+
assert.ok(drift !== null, 'turn_end would inject a persona-drift steer (drift pending)');
|
|
222
|
+
assert.deepEqual(drift?.from, { mode: 'base', lifecycle: 'terminal' }, 'drift.from = the acked base×terminal');
|
|
223
|
+
assert.deepEqual(drift?.to, { mode: 'orchestrator', lifecycle: 'terminal' }, 'drift.to = the new orchestrator persona');
|
|
224
|
+
assert.match(drift?.guidance ?? '', /ORCHESTRATOR/i, 'the steer carries the base→orchestrator guidance');
|
|
225
|
+
const stop = evaluateStop(B, { pushedFinal: false, askedHuman: false });
|
|
226
|
+
assert.equal(stop.action, 'reprompt', 'orchestrator B with no live sub, no final → reprompt');
|
|
227
|
+
assert.equal(stop.reason, 'stalled', 'classified stalled');
|
|
228
|
+
assert.equal(stop.action === 'reprompt' ? stop.message : '', STALL_REPROMPT, 'the stall reprompt nudge');
|
|
229
|
+
}
|
|
211
230
|
// ===================================================================
|
|
212
|
-
// S10 — B YIELDS
|
|
213
|
-
//
|
|
214
|
-
//
|
|
215
|
-
//
|
|
231
|
+
// S10 — B YIELDS. The real `node yield` verb sets intent=refresh (already
|
|
232
|
+
// orchestrator → no auto-promote); persona survives the request. We
|
|
233
|
+
// then model the daemon's completed refresh-revive with transition
|
|
234
|
+
// ('revive') (the in-place fresh boot is engine; its model effect —
|
|
235
|
+
// active, intent cleared, persona preserved — is asserted here).
|
|
216
236
|
// ===================================================================
|
|
217
|
-
|
|
237
|
+
{
|
|
238
|
+
const res = h.cli(B, ['node', 'yield', 'refresh against the roadmap']);
|
|
239
|
+
assert.equal(res.code, 0, `node yield exit 0\n${res.stderr}`);
|
|
240
|
+
const b = h.node(B);
|
|
241
|
+
assert.equal(b.intent, 'refresh', 'yield set intent=refresh');
|
|
242
|
+
assert.equal(b.mode, 'orchestrator', 'B mode survives the yield request');
|
|
243
|
+
assert.equal(b.lifecycle, 'terminal', 'B lifecycle survives the yield request');
|
|
244
|
+
}
|
|
245
|
+
transition(B, 'revive'); // model the completed refresh-revive (fresh boot is engine)
|
|
218
246
|
{
|
|
219
247
|
const b = h.node(B);
|
|
220
|
-
assert.equal(b.status, 'active', 'B active after refresh-
|
|
248
|
+
assert.equal(b.status, 'active', 'B active after the refresh-revive');
|
|
221
249
|
assert.equal(b.intent ?? null, null, 'B intent=refresh cleared by the fresh boot');
|
|
222
250
|
assert.equal(b.mode, 'orchestrator', 'B mode survives the refresh');
|
|
223
251
|
assert.equal(b.lifecycle, 'terminal', 'B lifecycle survives the refresh');
|
|
@@ -225,7 +253,8 @@ test('flagship: full node lifecycle — spawn, dormancy, one-hop wake, promote,
|
|
|
225
253
|
// ===================================================================
|
|
226
254
|
// S11 — B spawns child D (base × terminal). New spine seed B→D active.
|
|
227
255
|
// ===================================================================
|
|
228
|
-
const D =
|
|
256
|
+
const D = h.fabricateBrokerNode({ parent: B, mode: 'base', lifecycle: 'terminal', status: 'active' });
|
|
257
|
+
subscribe(B, D, true);
|
|
229
258
|
{
|
|
230
259
|
const d = h.node(D);
|
|
231
260
|
assert.equal(d.mode, 'base', 'D base');
|
|
@@ -236,32 +265,27 @@ test('flagship: full node lifecycle — spawn, dormancy, one-hop wake, promote,
|
|
|
236
265
|
// ===================================================================
|
|
237
266
|
// S12 — D FINISHES → pointer to B (its only subscriber).
|
|
238
267
|
// ===================================================================
|
|
239
|
-
await
|
|
268
|
+
await pushFinal(D, 'D result body');
|
|
240
269
|
{
|
|
241
270
|
assert.equal(h.node(D).status, 'done', 'D done');
|
|
242
271
|
const dFinal = h.inbox(B).find((e) => e.from === D && e.kind === 'final');
|
|
243
|
-
assert.ok(dFinal,
|
|
272
|
+
assert.ok(dFinal, "B inbox received D's final pointer");
|
|
244
273
|
}
|
|
245
274
|
// ===================================================================
|
|
246
275
|
// S13 — B pushes its FINAL up the spine → done/done; the pointer fans to
|
|
247
276
|
// subscribersOf(B) = {A}. NOW A finally hears B (its own explicit
|
|
248
277
|
// push — the only way the chain propagates a hop).
|
|
249
278
|
// ===================================================================
|
|
279
|
+
await pushFinal(B, 'all work complete');
|
|
250
280
|
{
|
|
251
|
-
const res = h.cli(B, ['push', 'final', 'all work complete']);
|
|
252
|
-
assert.equal(res.code, 0, `B push final exit 0\n${res.stderr}`);
|
|
253
281
|
const b = h.node(B);
|
|
254
282
|
assert.equal(b.status, 'done', 'B done after push final');
|
|
255
283
|
assert.equal(b.intent, 'done', 'B intent=done');
|
|
256
284
|
const aInbox = h.inbox(A);
|
|
257
|
-
assert.equal(aInbox.length, 1,
|
|
258
|
-
assert.equal(aInbox[0].from, B,
|
|
259
|
-
assert.equal(aInbox[0].kind, 'final',
|
|
285
|
+
assert.equal(aInbox.length, 1, "A inbox now has exactly B's final (one hop, on B's push)");
|
|
286
|
+
assert.equal(aInbox[0].from, B, "A's entry is from B");
|
|
287
|
+
assert.equal(aInbox[0].kind, 'final', "A's entry is a final");
|
|
260
288
|
}
|
|
261
|
-
// Close B's window faithfully (done branch shutdown).
|
|
262
|
-
await h.stop(B);
|
|
263
|
-
await h.waitForPaneGone(B);
|
|
264
|
-
assert.equal(h.paneAlive(B), false, 'B pane closed on done');
|
|
265
289
|
// A — the resident top-level orchestrator — is still alive and well.
|
|
266
290
|
{
|
|
267
291
|
const a = h.node(A);
|
|
@@ -271,9 +295,6 @@ test('flagship: full node lifecycle — spawn, dormancy, one-hop wake, promote,
|
|
|
271
295
|
}
|
|
272
296
|
}
|
|
273
297
|
finally {
|
|
274
|
-
const session = h.session;
|
|
275
298
|
await h.dispose();
|
|
276
|
-
// Teardown leaves NO stray session.
|
|
277
|
-
assert.equal(sessionExists(session), false, 'isolated session killed — no stray');
|
|
278
299
|
}
|
|
279
300
|
});
|
|
@@ -18,7 +18,11 @@
|
|
|
18
18
|
// These tests lock in (a) behavioral equivalence with the old decoder across
|
|
19
19
|
// randomized chunkings — frames, blank-line skip, malformed-JSON drop, multibyte
|
|
20
20
|
// splits, and both FrameOverflowError caps with identical byte accounting — and
|
|
21
|
-
// (b) the linear growth curve
|
|
21
|
+
// (b) the linear growth curve via a SCALING-RATIO assertion (per-MiB decode cost
|
|
22
|
+
// at 4 MiB vs 16 MiB must stay ~flat), so the quadratic shape cannot silently
|
|
23
|
+
// return. The ratio is measured with min-of-reps and interleaved sizes so it is
|
|
24
|
+
// robust to CPU contention from parallel `test:full` workers — see the perf test
|
|
25
|
+
// for the full rationale.
|
|
22
26
|
import { test } from 'node:test';
|
|
23
27
|
import assert from 'node:assert/strict';
|
|
24
28
|
import { StringDecoder } from 'node:string_decoder';
|
|
@@ -167,32 +171,54 @@ test('FrameDecoder — a frame exactly AT each cap passes; one byte over throws
|
|
|
167
171
|
}
|
|
168
172
|
});
|
|
169
173
|
test('FrameDecoder — large-frame decode is linear, not quadratic (the observed attach-lag regression)', () => {
|
|
170
|
-
//
|
|
171
|
-
//
|
|
172
|
-
//
|
|
173
|
-
//
|
|
174
|
-
//
|
|
174
|
+
// STRATEGY (contention-robust SCALING assertion): decode a single frame in
|
|
175
|
+
// 64 KiB socket-sized chunks at two sizes 4× apart (4 MiB vs 16 MiB) and assert
|
|
176
|
+
// the per-MiB cost stays ~flat. A linear decoder has per-MiB cost independent of
|
|
177
|
+
// size (ratio ≈ 1); the O(n²) bug makes per-MiB cost grow with size, so over a 4×
|
|
178
|
+
// size jump it grows ~4× (measured 5.5×+ pre-fix). Threshold 3× sits cleanly
|
|
179
|
+
// between the two signatures: well above linear noise, well below the quadratic
|
|
180
|
+
// signature.
|
|
181
|
+
//
|
|
182
|
+
// Why a RATIO, not an absolute wall-clock budget: an absolute budget is fragile
|
|
183
|
+
// under the parallel `test:full` workers — CPU contention scales every
|
|
184
|
+
// measurement up, so a tight budget flakes red on a loaded box without any code
|
|
185
|
+
// regression. A ratio of two contention-scaled measurements cancels whole-box
|
|
186
|
+
// slowdown, leaving only the algorithmic shape. Two further guards keep the
|
|
187
|
+
// minima clean under load: (1) MIN across REPS — contention only ADDS time, so
|
|
188
|
+
// the minimum approximates the uncontended cost; (2) the two sizes are timed
|
|
189
|
+
// INTERLEAVED within each rep, so a contention burst inflates both numerator and
|
|
190
|
+
// denominator alike instead of skewing the ratio. This fails ONLY on a
|
|
191
|
+
// complexity regression, not on a slow/busy CI box.
|
|
175
192
|
const CHUNK = 64 * 1024;
|
|
176
|
-
const
|
|
193
|
+
const SMALL_MIB = 4;
|
|
194
|
+
const LARGE_MIB = 16; // 4× SMALL — quadratic ⇒ per-MiB ~4×; linear ⇒ ~1×
|
|
195
|
+
const REPS = 7;
|
|
196
|
+
const makeFrame = (mib) => {
|
|
177
197
|
const overhead = Buffer.byteLength('{"type":"welcome","blob":""}\n');
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
assert.equal(n, 1, 'decoded exactly one frame');
|
|
189
|
-
if (ms < best)
|
|
190
|
-
best = ms;
|
|
198
|
+
return Buffer.from(`{"type":"welcome","blob":"${'x'.repeat(mib * 1024 * 1024 - overhead)}"}\n`);
|
|
199
|
+
};
|
|
200
|
+
const smallFrame = makeFrame(SMALL_MIB);
|
|
201
|
+
const largeFrame = makeFrame(LARGE_MIB);
|
|
202
|
+
const decodeMs = (frame) => {
|
|
203
|
+
const dec = new FrameDecoder(CLIENT_READ_CAPS);
|
|
204
|
+
const t0 = performance.now();
|
|
205
|
+
let n = 0;
|
|
206
|
+
for (let off = 0; off < frame.length; off += CHUNK) {
|
|
207
|
+
n += dec.push(frame.subarray(off, Math.min(off + CHUNK, frame.length))).length;
|
|
191
208
|
}
|
|
192
|
-
|
|
209
|
+
const ms = performance.now() - t0;
|
|
210
|
+
assert.equal(n, 1, 'decoded exactly one frame');
|
|
211
|
+
return ms;
|
|
193
212
|
};
|
|
194
|
-
decodeMs(
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
213
|
+
decodeMs(smallFrame); // warm JIT on both paths before timing
|
|
214
|
+
decodeMs(largeFrame);
|
|
215
|
+
let bestSmall = Infinity;
|
|
216
|
+
let bestLarge = Infinity;
|
|
217
|
+
for (let rep = 0; rep < REPS; rep++) {
|
|
218
|
+
bestSmall = Math.min(bestSmall, decodeMs(smallFrame));
|
|
219
|
+
bestLarge = Math.min(bestLarge, decodeMs(largeFrame));
|
|
220
|
+
}
|
|
221
|
+
const perMibSmall = bestSmall / SMALL_MIB;
|
|
222
|
+
const perMibLarge = bestLarge / LARGE_MIB;
|
|
223
|
+
assert.ok(perMibLarge < perMibSmall * 3, `per-MiB decode cost grew superlinearly: ${perMibSmall.toFixed(2)}ms/MiB @${SMALL_MIB}MiB → ${perMibLarge.toFixed(2)}ms/MiB @${LARGE_MIB}MiB (ratio ${(perMibLarge / perMibSmall).toFixed(2)}×; quadratic regression)`);
|
|
198
224
|
});
|