@crouton-kit/crouter 0.3.17 → 0.3.19
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/build-root.js +6 -0
- package/dist/builtin-memory/crouter-development/marketplaces.md +164 -0
- package/dist/builtin-memory/crouter-development/personas/base-prompt.md +58 -0
- package/dist/builtin-memory/crouter-development/personas/orchestrator-prompt.md +60 -0
- package/dist/builtin-memory/crouter-development/personas.md +117 -0
- package/dist/builtin-memory/crouter-development/plugins.md +163 -0
- package/dist/builtin-memory/design.md +60 -0
- package/dist/builtin-memory/development.md +116 -0
- package/dist/builtin-memory/planning.md +66 -0
- package/dist/builtin-memory/spec.md +99 -0
- package/dist/builtin-personas/design/{base.md → PERSONA.md} +4 -0
- package/dist/builtin-personas/design/orchestrator.md +1 -1
- package/dist/builtin-personas/developer/{base.md → PERSONA.md} +4 -0
- package/dist/builtin-personas/developer/orchestrator.md +1 -1
- package/dist/builtin-personas/explore/{base.md → PERSONA.md} +4 -0
- package/dist/builtin-personas/general/{base.md → PERSONA.md} +4 -0
- package/dist/builtin-personas/lifecycle/resident.md +1 -1
- package/dist/builtin-personas/lifecycle/terminal.md +5 -2
- package/dist/builtin-personas/orchestration-kernel.md +15 -24
- package/dist/builtin-personas/plan/{base.md → PERSONA.md} +4 -0
- package/dist/builtin-personas/plan/orchestrator.md +1 -1
- package/dist/builtin-personas/plan/reviewers/architecture-fit/{base.md → PERSONA.md} +1 -1
- package/dist/builtin-personas/plan/reviewers/code-smells/{base.md → PERSONA.md} +1 -1
- package/dist/builtin-personas/plan/reviewers/pattern-consistency/{base.md → PERSONA.md} +1 -1
- package/dist/builtin-personas/plan/reviewers/requirements-coverage/{base.md → PERSONA.md} +1 -1
- package/dist/builtin-personas/plan/reviewers/security/{base.md → PERSONA.md} +1 -1
- package/dist/builtin-personas/review/{base.md → PERSONA.md} +4 -0
- package/dist/builtin-personas/runtime-base.md +6 -3
- package/dist/builtin-personas/spec/{base.md → PERSONA.md} +4 -0
- package/dist/builtin-personas/spec/orchestrator.md +1 -1
- package/dist/builtin-personas/waiting.md +8 -0
- package/dist/builtin-skills/skills/crouter-development/personas/SKILL.md +24 -14
- package/dist/builtin-skills/skills/crouter-development/personas/base-prompt/SKILL.md +4 -4
- package/dist/builtin-skills/skills/crouter-development/personas/orchestrator-prompt/SKILL.md +1 -1
- package/dist/builtin-skills/skills/crouter-development/plugins/SKILL.md +2 -2
- package/dist/builtin-views/_lib/states.mjs +161 -0
- package/dist/builtin-views/canvas/client.mjs +303 -0
- package/dist/builtin-views/canvas/view.mjs +576 -0
- package/dist/builtin-views/git-pr/client.mjs +440 -0
- package/dist/builtin-views/git-pr/view.mjs +675 -0
- package/dist/builtin-views/inbox/_lib/render.mjs +177 -0
- package/dist/builtin-views/inbox/sources/gmail.mjs +965 -0
- package/dist/builtin-views/inbox/sources/linkedin.mjs +427 -0
- package/dist/builtin-views/inbox/view.mjs +889 -0
- package/dist/builtin-views/linkedin/client.mjs +610 -0
- package/dist/builtin-views/linkedin/view.mjs +1171 -0
- package/dist/clients/attach/attach-cmd.d.ts +2 -0
- package/dist/clients/attach/attach-cmd.js +354 -0
- package/dist/clients/attach/chat-view.d.ts +77 -0
- package/dist/clients/attach/chat-view.js +450 -0
- package/dist/clients/attach/clipboard-image.d.ts +16 -0
- package/dist/clients/attach/clipboard-image.js +113 -0
- package/dist/clients/attach/config-load.d.ts +31 -0
- package/dist/clients/attach/config-load.js +113 -0
- package/dist/clients/attach/extension-dialogs.d.ts +29 -0
- package/dist/clients/attach/extension-dialogs.js +101 -0
- package/dist/clients/attach/input-controller.d.ts +54 -0
- package/dist/clients/attach/input-controller.js +204 -0
- package/dist/clients/attach/slash-commands.d.ts +36 -0
- package/dist/clients/attach/slash-commands.js +200 -0
- package/dist/clients/attach/view-socket.d.ts +48 -0
- package/dist/clients/attach/view-socket.js +126 -0
- package/dist/commands/attention.js +3 -3
- package/dist/commands/canvas-prune.js +1 -1
- package/dist/commands/daemon.js +4 -3
- package/dist/commands/human/prompts.js +4 -10
- package/dist/commands/human/queue.js +43 -8
- package/dist/commands/human/shared.d.ts +28 -1
- package/dist/commands/human/shared.js +48 -10
- package/dist/commands/memory/find.d.ts +1 -0
- package/dist/commands/memory/find.js +180 -0
- package/dist/commands/memory/lint.d.ts +1 -0
- package/dist/commands/memory/lint.js +140 -0
- package/dist/commands/memory/list.d.ts +1 -0
- package/dist/commands/memory/list.js +79 -0
- package/dist/commands/memory/read.js +103 -0
- package/dist/commands/memory/shared.d.ts +30 -0
- package/dist/commands/memory/shared.js +122 -0
- package/dist/commands/memory/write.d.ts +1 -0
- package/dist/commands/memory/write.js +85 -0
- package/dist/commands/memory.d.ts +2 -0
- package/dist/commands/memory.js +27 -0
- package/dist/commands/node.d.ts +3 -2
- package/dist/commands/node.js +660 -65
- package/dist/commands/pkg/market-manage.js +1 -1
- package/dist/commands/push.js +6 -6
- package/dist/commands/revive.js +1 -1
- package/dist/commands/skill/author.js +1 -1
- package/dist/commands/skill/shared.d.ts +1 -8
- package/dist/commands/skill/shared.js +2 -55
- package/dist/commands/skill.js +9 -14
- package/dist/commands/sys/doctor.js +1 -1
- package/dist/commands/sys/update.js +1 -1
- package/dist/commands/view-cycle.d.ts +2 -0
- package/dist/commands/view-cycle.js +125 -0
- package/dist/commands/view-list.d.ts +2 -0
- package/dist/commands/view-list.js +66 -0
- package/dist/commands/view-new.d.ts +2 -0
- package/dist/commands/view-new.js +70 -0
- package/dist/commands/view-pick.d.ts +2 -0
- package/dist/commands/view-pick.js +119 -0
- package/dist/commands/view-run.d.ts +2 -0
- package/dist/commands/view-run.js +191 -0
- package/dist/commands/view.d.ts +2 -0
- package/dist/commands/view.js +29 -0
- package/dist/core/__tests__/broker-lifecycle.test.d.ts +1 -0
- package/dist/core/__tests__/broker-lifecycle.test.js +677 -0
- package/dist/core/__tests__/broker-sdk-wiring.test.d.ts +1 -0
- package/dist/core/__tests__/broker-sdk-wiring.test.js +166 -0
- package/dist/core/__tests__/cascade-close.test.js +12 -2
- package/dist/core/__tests__/child-death-wake.test.d.ts +1 -0
- package/dist/core/__tests__/child-death-wake.test.js +245 -0
- package/dist/core/__tests__/context-intro.test.js +76 -62
- package/dist/core/__tests__/daemon-boot.test.js +14 -5
- package/dist/core/__tests__/daemon-liveness.test.js +34 -9
- package/dist/core/__tests__/detach-focus.test.d.ts +1 -0
- package/dist/core/__tests__/detach-focus.test.js +206 -0
- package/dist/core/__tests__/draw-style.test.d.ts +1 -0
- package/dist/core/__tests__/draw-style.test.js +258 -0
- package/dist/core/__tests__/fixtures/c3-custom-provider-ext.d.ts +2 -0
- package/dist/core/__tests__/fixtures/c3-custom-provider-ext.js +18 -0
- package/dist/core/__tests__/fixtures/fake-engine.d.ts +138 -0
- package/dist/core/__tests__/fixtures/fake-engine.js +614 -0
- package/dist/core/__tests__/fixtures/fake-pi-host.js +1 -0
- package/dist/core/__tests__/flagship-lifecycle.test.js +7 -1
- package/dist/core/__tests__/frame-decoder-perf.test.d.ts +1 -0
- package/dist/core/__tests__/frame-decoder-perf.test.js +198 -0
- package/dist/core/__tests__/helpers/harness.d.ts +9 -0
- package/dist/core/__tests__/helpers/harness.js +111 -1
- package/dist/core/__tests__/home-session.test.js +41 -8
- package/dist/core/__tests__/human-new-window-regression.test.d.ts +1 -0
- package/dist/core/__tests__/human-new-window-regression.test.js +101 -0
- package/dist/core/__tests__/human-surface-target.test.d.ts +1 -0
- package/dist/core/__tests__/human-surface-target.test.js +98 -0
- package/dist/core/__tests__/kickoff.test.js +37 -3
- package/dist/core/__tests__/live-mutation.test.js +50 -33
- package/dist/core/__tests__/memory.test.js +23 -115
- package/dist/core/__tests__/persona-subkind.test.js +18 -15
- package/dist/core/__tests__/placement-focus.test.js +5 -0
- package/dist/core/__tests__/placement-teardown.test.js +54 -11
- package/dist/core/__tests__/relaunch.test.js +4 -3
- package/dist/core/__tests__/review-render-pane-regression.test.d.ts +1 -0
- package/dist/core/__tests__/review-render-pane-regression.test.js +133 -0
- package/dist/core/__tests__/spawn-root.test.js +10 -0
- package/dist/core/__tests__/spike-harness.test.js +1 -0
- package/dist/core/__tests__/wake-bearings.test.d.ts +1 -0
- package/dist/core/__tests__/wake-bearings.test.js +156 -0
- package/dist/core/__tests__/wake-origin.test.d.ts +1 -0
- package/dist/core/__tests__/wake-origin.test.js +110 -0
- package/dist/core/bootstrap.js +1 -1
- package/dist/core/canvas/browse/__tests__/render.test.js +1 -0
- package/dist/core/canvas/browse/app.js +24 -2
- package/dist/core/canvas/browse/model.d.ts +38 -2
- package/dist/core/canvas/browse/model.js +134 -10
- package/dist/core/canvas/browse/render.d.ts +6 -12
- package/dist/core/canvas/browse/render.js +72 -104
- package/dist/core/canvas/canvas.js +15 -22
- package/dist/core/canvas/db.js +46 -0
- package/dist/core/canvas/index.d.ts +1 -0
- package/dist/core/canvas/index.js +1 -0
- package/dist/core/canvas/paths.d.ts +4 -1
- package/dist/core/canvas/paths.js +10 -4
- package/dist/core/canvas/pid.d.ts +4 -0
- package/dist/core/canvas/pid.js +23 -0
- package/dist/core/canvas/render.d.ts +11 -2
- package/dist/core/canvas/render.js +69 -0
- package/dist/core/canvas/types.d.ts +85 -3
- package/dist/core/canvas/types.js +2 -2
- package/dist/core/canvas/wakeups.d.ts +76 -0
- package/dist/core/canvas/wakeups.js +185 -0
- package/dist/core/config.js +4 -1
- package/dist/core/frontmatter.js +37 -124
- package/dist/core/help.d.ts +6 -0
- package/dist/core/help.js +7 -0
- package/dist/core/memory-resolver.d.ts +49 -0
- package/dist/core/memory-resolver.js +141 -0
- package/dist/core/personas/index.d.ts +4 -3
- package/dist/core/personas/index.js +3 -2
- package/dist/core/personas/loader.d.ts +41 -16
- package/dist/core/personas/loader.js +133 -29
- package/dist/core/personas/resolve.d.ts +4 -4
- package/dist/core/personas/resolve.js +28 -16
- package/dist/core/predicate.d.ts +63 -0
- package/dist/core/predicate.js +189 -0
- package/dist/core/resolver.js +26 -5
- package/dist/core/runtime/bearings.d.ts +53 -12
- package/dist/core/runtime/bearings.js +132 -59
- package/dist/core/runtime/broker-cli.d.ts +1 -0
- package/dist/core/runtime/broker-cli.js +46 -0
- package/dist/core/runtime/broker-protocol.d.ts +332 -0
- package/dist/core/runtime/broker-protocol.js +153 -0
- package/dist/core/runtime/broker-sdk.d.ts +48 -0
- package/dist/core/runtime/broker-sdk.js +72 -0
- package/dist/core/runtime/broker.d.ts +55 -0
- package/dist/core/runtime/broker.js +1128 -0
- package/dist/core/runtime/close.js +35 -6
- package/dist/core/runtime/host.d.ts +53 -0
- package/dist/core/runtime/host.js +186 -0
- package/dist/core/runtime/kickoff.d.ts +2 -1
- package/dist/core/runtime/kickoff.js +91 -5
- package/dist/core/runtime/launch.d.ts +45 -2
- package/dist/core/runtime/launch.js +65 -2
- package/dist/core/runtime/lifecycle.js +23 -6
- package/dist/core/runtime/memory.d.ts +2 -42
- package/dist/core/runtime/memory.js +11 -162
- package/dist/core/runtime/nodes.d.ts +33 -0
- package/dist/core/runtime/nodes.js +59 -1
- package/dist/core/runtime/persona.js +21 -11
- package/dist/core/runtime/pi-vendored.d.ts +18 -0
- package/dist/core/runtime/pi-vendored.js +49 -0
- package/dist/core/runtime/placement.d.ts +42 -14
- package/dist/core/runtime/placement.js +228 -38
- package/dist/core/runtime/promote.d.ts +0 -6
- package/dist/core/runtime/promote.js +1 -12
- package/dist/core/runtime/{demote.d.ts → recycle.d.ts} +5 -5
- package/dist/core/runtime/{demote.js → recycle.js} +27 -11
- package/dist/core/runtime/reset.js +8 -6
- package/dist/core/runtime/revive.d.ts +2 -0
- package/dist/core/runtime/revive.js +34 -34
- package/dist/core/runtime/spawn.d.ts +19 -0
- package/dist/core/runtime/spawn.js +75 -22
- package/dist/core/runtime/stop-guard.d.ts +1 -1
- package/dist/core/runtime/stop-guard.js +6 -1
- package/dist/core/runtime/tmux-chrome.d.ts +1 -1
- package/dist/core/runtime/tmux-chrome.js +1 -1
- package/dist/core/runtime/tmux.d.ts +28 -0
- package/dist/core/runtime/tmux.js +80 -6
- package/dist/core/scope.d.ts +11 -0
- package/dist/core/scope.js +39 -0
- package/dist/core/spawn.d.ts +25 -1
- package/dist/core/spawn.js +72 -7
- package/dist/core/substrate/gate.d.ts +13 -0
- package/dist/core/substrate/gate.js +21 -0
- package/dist/core/substrate/index.d.ts +7 -0
- package/dist/core/substrate/index.js +18 -0
- package/dist/core/substrate/on-read.d.ts +14 -0
- package/dist/core/substrate/on-read.js +292 -0
- package/dist/core/substrate/render.d.ts +25 -0
- package/dist/core/substrate/render.js +256 -0
- package/dist/core/substrate/schema.d.ts +76 -0
- package/dist/core/substrate/schema.js +124 -0
- package/dist/core/substrate/session-cache.d.ts +30 -0
- package/dist/core/substrate/session-cache.js +77 -0
- package/dist/core/substrate/subject.d.ts +41 -0
- package/dist/core/substrate/subject.js +54 -0
- package/dist/core/tui/contract.d.ts +83 -0
- package/dist/core/tui/contract.js +8 -0
- package/dist/core/tui/draw.d.ts +96 -0
- package/dist/core/tui/draw.js +339 -0
- package/dist/core/tui/host.d.ts +29 -0
- package/dist/core/tui/host.js +379 -0
- package/dist/core/tui/loader.d.ts +16 -0
- package/dist/core/tui/loader.js +94 -0
- package/dist/core/{canvas/browse → tui}/terminal.js +7 -6
- package/dist/core/wake.d.ts +86 -0
- package/dist/core/wake.js +308 -0
- package/dist/daemon/crtrd.d.ts +29 -4
- package/dist/daemon/crtrd.js +662 -46
- package/dist/pi-extensions/__tests__/canvas-context-intro.test.d.ts +1 -0
- package/dist/pi-extensions/__tests__/canvas-context-intro.test.js +171 -0
- package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +19 -12
- package/dist/pi-extensions/canvas-commands.d.ts +3 -0
- package/dist/pi-extensions/canvas-commands.js +10 -0
- package/dist/pi-extensions/canvas-context-intro.d.ts +17 -0
- package/dist/pi-extensions/canvas-context-intro.js +55 -15
- package/dist/pi-extensions/canvas-doc-substrate.d.ts +44 -0
- package/dist/pi-extensions/canvas-doc-substrate.js +112 -0
- package/dist/pi-extensions/canvas-nav.d.ts +3 -0
- package/dist/pi-extensions/canvas-nav.js +145 -34
- package/dist/pi-extensions/canvas-stophook.js +17 -8
- package/dist/pi-extensions/canvas-view.d.ts +21 -0
- package/dist/pi-extensions/canvas-view.js +75 -0
- package/dist/prompts/skill.js +19 -26
- package/dist/prompts/view.d.ts +7 -0
- package/dist/prompts/view.js +101 -0
- package/dist/types.d.ts +4 -0
- package/dist/types.js +1 -0
- package/package.json +9 -4
- package/dist/commands/skill/find.d.ts +0 -4
- package/dist/commands/skill/find.js +0 -257
- package/dist/commands/skill/read.js +0 -91
- /package/dist/commands/{skill → memory}/read.d.ts +0 -0
- /package/dist/core/{canvas/browse → tui}/terminal.d.ts +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
// Run with: node --import tsx/esm --test src/core/__tests__/broker-sdk-wiring.test.ts
|
|
2
|
+
//
|
|
3
|
+
// REAL-SDK regression tests for the latent broker gaps the viewer-reuse scout
|
|
4
|
+
// (node mq5thyli) found (findings-viewer-reuse.md §Q2). These deliberately drive
|
|
5
|
+
// the ACTUAL `@earendil-works/pi-coding-agent` SDK — NOT the `CRTR_BROKER_ENGINE`
|
|
6
|
+
// fake the lifecycle suite uses — because C3 and C4 slipped past the Phase-3 gate
|
|
7
|
+
// PRECISELY because that gate mocked the SDK out. Each test exercises the exact
|
|
8
|
+
// production wiring (`buildBrokerSession` / `makeBrokerUiContext`) and would FAIL
|
|
9
|
+
// against the pre-fix broker:
|
|
10
|
+
//
|
|
11
|
+
// C3 — the broker drives the SERVICES path, so an extension-registered custom
|
|
12
|
+
// model provider is registered into the ModelRegistry and resolves onto
|
|
13
|
+
// the session. Plain createAgentSession never calls registerProvider, so a
|
|
14
|
+
// custom-provider node would get NO model.
|
|
15
|
+
// C4 — a project AGENTS.md is injected into the assembled system prompt (the
|
|
16
|
+
// services path loads project context; 0.78.1 has no project-trust gate so
|
|
17
|
+
// context is unconditionally loaded — see buildBrokerSession's C4 note).
|
|
18
|
+
// C2 — with ZERO viewers attached, the broker UI context resolves confirm() /
|
|
19
|
+
// select() / input() / editor() to deny/cancel IMMEDIATELY (noOp), instead
|
|
20
|
+
// of relying on a per-dialog timeout and hanging the agent turn forever.
|
|
21
|
+
//
|
|
22
|
+
// These run offline (no network/auth): registration + system-prompt assembly +
|
|
23
|
+
// dialog routing are all local. No tmux, no broker process — direct unit drives.
|
|
24
|
+
import { test } from 'node:test';
|
|
25
|
+
import assert from 'node:assert/strict';
|
|
26
|
+
import { mkdtempSync, writeFileSync, rmSync } from 'node:fs';
|
|
27
|
+
import { tmpdir } from 'node:os';
|
|
28
|
+
import { join, dirname } from 'node:path';
|
|
29
|
+
import { fileURLToPath } from 'node:url';
|
|
30
|
+
import { createAgentSessionServices, createAgentSessionFromServices, SessionManager, VERSION, } from '../runtime/broker-sdk.js';
|
|
31
|
+
import { buildBrokerSession, makeBrokerUiContext } from '../runtime/broker.js';
|
|
32
|
+
// The REAL engine, assembled from broker-sdk's static SDK re-exports — bypasses
|
|
33
|
+
// the CRTR_BROKER_ENGINE seam entirely so these tests can never accidentally hit
|
|
34
|
+
// the fake (the whole point: prove the wiring the mock hid).
|
|
35
|
+
const realEngine = {
|
|
36
|
+
createAgentSessionServices,
|
|
37
|
+
createAgentSessionFromServices,
|
|
38
|
+
SessionManager,
|
|
39
|
+
VERSION,
|
|
40
|
+
};
|
|
41
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
42
|
+
const C3_EXT = join(HERE, 'fixtures', 'c3-custom-provider-ext.ts');
|
|
43
|
+
function cfg(cwd, extra = {}) {
|
|
44
|
+
return { cwd, extensionPaths: [], ...extra };
|
|
45
|
+
}
|
|
46
|
+
// ===========================================================================
|
|
47
|
+
// C3 — the SERVICES path registers an extension's custom model provider, so the
|
|
48
|
+
// broker resolves a model that plain createAgentSession (no registerProvider)
|
|
49
|
+
// would never see.
|
|
50
|
+
// ===========================================================================
|
|
51
|
+
test('C3 — services path registers an extension model provider; the broker session gets it', async () => {
|
|
52
|
+
const cwd = mkdtempSync(join(tmpdir(), 'crtr-c3-'));
|
|
53
|
+
try {
|
|
54
|
+
const { session, services } = await buildBrokerSession(realEngine, cfg(cwd, { extensionPaths: [C3_EXT], model: 'c3prov/c3model' }));
|
|
55
|
+
try {
|
|
56
|
+
// The extension's provider was registered into the SERVICES registry (this
|
|
57
|
+
// is the registerProvider step plain createAgentSession skips).
|
|
58
|
+
const found = services.modelRegistry.find('c3prov', 'c3model');
|
|
59
|
+
assert.ok(found, 'C3: extension-registered model is present in the services ModelRegistry');
|
|
60
|
+
assert.equal(found.provider, 'c3prov', 'C3: provider name round-trips');
|
|
61
|
+
assert.equal(found.id, 'c3model', 'C3: model id round-trips');
|
|
62
|
+
// …and the broker resolved it onto the session AGAINST the services registry
|
|
63
|
+
// (a fresh ModelRegistry would have returned undefined → no model).
|
|
64
|
+
assert.equal(session.model?.id, 'c3model', 'C3: the broker session got the custom-provider model');
|
|
65
|
+
}
|
|
66
|
+
finally {
|
|
67
|
+
session.dispose();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
finally {
|
|
71
|
+
rmSync(cwd, { recursive: true, force: true });
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
// ===========================================================================
|
|
75
|
+
// C4 — project context (AGENTS.md) is injected into the system prompt. The
|
|
76
|
+
// services path loads project context files; on the 0.78.1 pin there is no
|
|
77
|
+
// project-trust gate, so they load unconditionally (and buildBrokerSession does
|
|
78
|
+
// NOT re-introduce the CLI's headless trust resolver, which would drop them).
|
|
79
|
+
// ===========================================================================
|
|
80
|
+
test('C4 — a project AGENTS.md is injected into the assembled system prompt', async () => {
|
|
81
|
+
const cwd = mkdtempSync(join(tmpdir(), 'crtr-c4-'));
|
|
82
|
+
try {
|
|
83
|
+
const marker = 'C4_AGENTS_MARKER_' + Math.random().toString(36).slice(2);
|
|
84
|
+
writeFileSync(join(cwd, 'AGENTS.md'), `# Project rules\n\n${marker}\n`);
|
|
85
|
+
const { session } = await buildBrokerSession(realEngine, cfg(cwd));
|
|
86
|
+
try {
|
|
87
|
+
assert.ok(session.systemPrompt.includes(marker), 'C4: project AGENTS.md content is present in the assembled system prompt');
|
|
88
|
+
}
|
|
89
|
+
finally {
|
|
90
|
+
session.dispose();
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
finally {
|
|
94
|
+
rmSync(cwd, { recursive: true, force: true });
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
// ===========================================================================
|
|
98
|
+
// C2 — zero-viewer dialogs resolve to deny/cancel IMMEDIATELY (noOp), never
|
|
99
|
+
// hanging the turn and never waiting on a per-dialog timeout. Drives the REAL
|
|
100
|
+
// makeBrokerUiContext (the exact ExtensionUIContext the SDK hands extensions),
|
|
101
|
+
// with a controller() that reports zero viewers.
|
|
102
|
+
// ===========================================================================
|
|
103
|
+
test('C2 — zero-viewer UI context resolves dialogs to deny/cancel immediately (noOp, no hang)', async () => {
|
|
104
|
+
const ctx = makeBrokerUiContext({
|
|
105
|
+
controller: () => null, // ZERO viewers attached
|
|
106
|
+
forward: () => {
|
|
107
|
+
throw new Error('C2: must NOT forward a dialog when no controller is attached');
|
|
108
|
+
},
|
|
109
|
+
pending: new Map(),
|
|
110
|
+
});
|
|
111
|
+
// confirm() WITH a large timeout: the fix must resolve at once (deny), NOT after
|
|
112
|
+
// the timeout — the old timeout-reliant path would have waited 60s (or, with no
|
|
113
|
+
// timeout, hung forever). Race against a 2s "HANG" sentinel to make a regression
|
|
114
|
+
// a hard failure rather than a slow pass.
|
|
115
|
+
const start = Date.now();
|
|
116
|
+
const confirmed = await Promise.race([
|
|
117
|
+
ctx.confirm('proceed?', 'really?', { timeout: 60_000 }),
|
|
118
|
+
new Promise((r) => setTimeout(() => r('HANG'), 2_000)),
|
|
119
|
+
]);
|
|
120
|
+
assert.equal(confirmed, false, 'C2: confirm() denies (false) with zero viewers');
|
|
121
|
+
assert.ok(Date.now() - start < 1_000, 'C2: resolved immediately, not after the 60s timeout');
|
|
122
|
+
// The other blocking dialogs cancel to undefined — including editor(), which
|
|
123
|
+
// takes NO opts at all (so it could never carry a timeout to lean on).
|
|
124
|
+
assert.equal(await ctx.select('pick', ['a', 'b']), undefined, 'C2: select() cancels (undefined)');
|
|
125
|
+
assert.equal(await ctx.input('name'), undefined, 'C2: input() cancels (undefined)');
|
|
126
|
+
assert.equal(await ctx.editor('edit'), undefined, 'C2: editor() cancels (undefined)');
|
|
127
|
+
});
|
|
128
|
+
// ===========================================================================
|
|
129
|
+
// M2 (review mq5wkqep / T4) — REPLACES the Wave-0 M-1 cancel-on-detach. A dialog
|
|
130
|
+
// forwarded to a controller that then DETACHES must NOT be cancelled: it stays
|
|
131
|
+
// pending so a brief detach/reattach (or a handoff to another controller) does
|
|
132
|
+
// not lose an answerable dialog. The broker-side default timeout is the ONLY
|
|
133
|
+
// non-answer resolution (proven here with a short per-dialog timeout standing in
|
|
134
|
+
// for the 120s default). Guards against regressing to the over-eager cancel.
|
|
135
|
+
// ===========================================================================
|
|
136
|
+
test('M2 — a forwarded dialog stays pending on controller detach, resolving only on the broker-side timeout', async () => {
|
|
137
|
+
const pending = new Map();
|
|
138
|
+
let attached = true;
|
|
139
|
+
const ctx = makeBrokerUiContext({
|
|
140
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
141
|
+
controller: () => (attached ? { id: 'c1' } : null),
|
|
142
|
+
forward: () => {
|
|
143
|
+
/* a real controller would receive the request over its socket */
|
|
144
|
+
},
|
|
145
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
146
|
+
pending: pending,
|
|
147
|
+
});
|
|
148
|
+
// Controller attached → confirm() forwards + registers a pending dialog (with the
|
|
149
|
+
// request retained for welcome.pending_dialog / re-route, T4) and does NOT
|
|
150
|
+
// resolve yet. A short per-dialog timeout stands in for the 120s broker default.
|
|
151
|
+
const p = ctx.confirm('proceed?', 'really?', { timeout: 80 });
|
|
152
|
+
assert.equal(pending.size, 1, 'M2: a forwarded dialog is pending while a controller is attached');
|
|
153
|
+
const entry = [...pending.values()][0];
|
|
154
|
+
assert.ok(entry.request, 'M2: the pending entry retains the request (welcome/re-route need it)');
|
|
155
|
+
assert.equal(entry.cancel, undefined, 'M2: there is no cancel-on-detach path anymore');
|
|
156
|
+
// Controller detaches → the dialog STAYS pending (the broker no longer cancels it).
|
|
157
|
+
attached = false;
|
|
158
|
+
assert.equal(pending.size, 1, 'M2: detach does NOT cancel the in-flight dialog');
|
|
159
|
+
// Only the broker-side timeout resolves it, to the SAFE default (deny).
|
|
160
|
+
const resolved = await Promise.race([
|
|
161
|
+
p,
|
|
162
|
+
new Promise((r) => setTimeout(() => r('HANG'), 2_000)),
|
|
163
|
+
]);
|
|
164
|
+
assert.equal(resolved, false, 'M2: resolves to deny on the broker-side timeout, never hangs');
|
|
165
|
+
assert.equal(pending.size, 0, 'M2: the pending registry is drained after the timeout');
|
|
166
|
+
});
|
|
@@ -134,13 +134,23 @@ test('cascade close: middle-node close reaps EXACTLY its subtree (canceled), anc
|
|
|
134
134
|
// ASSERT 3 — EXCLUSIVE-SUBTREE OWNERSHIP: nothing ABOVE (A) and nothing
|
|
135
135
|
// SIDEWAYS (S, G) was reaped. close(B) reaped EXACTLY B's own subtree.
|
|
136
136
|
// ===================================================================
|
|
137
|
-
// Ancestor A —
|
|
137
|
+
// Ancestor A — lifecycle untouched (still the live resident root), but A is
|
|
138
|
+
// the SURVIVING manager of the close-root B, so the wake doctrine fix WAKES
|
|
139
|
+
// it: a "child closed" notice is fanned to a manager OUTSIDE the closing set
|
|
140
|
+
// (D-1 — previously A heard nothing and a dormant A could hang forever).
|
|
138
141
|
{
|
|
139
142
|
const a = h.node(A);
|
|
140
143
|
assert.equal(a.status, 'active', 'A (ancestor) still active — untouched by the cascade');
|
|
141
144
|
assert.equal(a.lifecycle, 'resident', 'A still resident');
|
|
142
145
|
assert.equal(a.intent ?? null, null, 'A intent untouched');
|
|
143
|
-
|
|
146
|
+
// A's PRIOR entries are preserved; exactly ONE new child-closed entry
|
|
147
|
+
// from the closed child B is appended (the doctrine wake).
|
|
148
|
+
const aInbox = h.inbox(A);
|
|
149
|
+
assert.deepEqual(aInbox.slice(0, aInboxBefore.length), aInboxBefore, "A's prior inbox entries are preserved across the close");
|
|
150
|
+
const added = aInbox.slice(aInboxBefore.length);
|
|
151
|
+
assert.equal(added.length, 1, 'exactly one new entry appended to the surviving manager A');
|
|
152
|
+
assert.equal(added[0].from, B, 'the new entry is from the closed child B');
|
|
153
|
+
assert.match(added[0].label, /closed/i, 'the new entry tells A its child B was closed');
|
|
144
154
|
}
|
|
145
155
|
// Sideways subtree S→G — wholly alive: status active AND real panes alive.
|
|
146
156
|
for (const id of sideways) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
// Run with: node --import tsx/esm --test src/core/__tests__/child-death-wake.test.ts
|
|
2
|
+
//
|
|
3
|
+
// D-1 BUG REGRESSION + node/canvas LIFECYCLE — "the runtime wakes a dormant
|
|
4
|
+
// parent on ANY terminal child outcome."
|
|
5
|
+
//
|
|
6
|
+
// The bug (D-1 finding, context/d1-finding.md): a parent that delegates and
|
|
7
|
+
// then JUST STOPS — retaining its inbox-wait, arming no self-wake — was woken
|
|
8
|
+
// ONLY when a child `push final`s or never boots. A child that crashes AFTER
|
|
9
|
+
// booting, is daemon-finalized after a quiet turn, or is `node close`d marked
|
|
10
|
+
// the child dead/done/canceled with NO push to the parent, so a purely-inbox-
|
|
11
|
+
// waiting parent hung dormant forever. The wake doctrine ("delegate and stop;
|
|
12
|
+
// the runtime wakes you") is only honest if EVERY terminal child outcome reaches
|
|
13
|
+
// the parent — this locks that in.
|
|
14
|
+
//
|
|
15
|
+
// The fix fans a system inbox entry to subscribersOf(child) on three previously-
|
|
16
|
+
// silent terminal outcomes — mirroring surfaceBootFailure:
|
|
17
|
+
// • daemon CRASH after boot (crtrd pane-gone, mid-generation → 'dead')
|
|
18
|
+
// • daemon FINALIZE on quiet (crtrd pane-gone, turn ended, nothing live → 'done')
|
|
19
|
+
// • `node close` (close.ts, the surviving manager outside the set)
|
|
20
|
+
//
|
|
21
|
+
// THE CRUX (the whole risk): the wake must fire ONLY on genuine death/close —
|
|
22
|
+
// NEVER on healthy dormancy. A child that ended its turn STILL waiting (an active
|
|
23
|
+
// live grandchild, or a pending self-wake) is alive-and-dormant, not dead; waking
|
|
24
|
+
// its parent then would re-create the spurious-wake storm the doctrine exists to
|
|
25
|
+
// kill (the concrete case: a developer child that finished its edits, spawned its
|
|
26
|
+
// own reviewer, and went dormant waiting on it — its parent must stay asleep).
|
|
27
|
+
// The daemon draws that boundary EXACTLY where the stop-guard does:
|
|
28
|
+
// hasActiveLiveSubscription || hasPendingSelfWake ⇒ healthy dormancy ⇒ release,
|
|
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).
|
|
37
|
+
import { test, before, after, beforeEach } from 'node:test';
|
|
38
|
+
import assert from 'node:assert/strict';
|
|
39
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
40
|
+
import { tmpdir } from 'node:os';
|
|
41
|
+
import { join } from 'node:path';
|
|
42
|
+
import { spawnSync } from 'node:child_process';
|
|
43
|
+
import { createNode, getNode, subscribe } from '../canvas/canvas.js';
|
|
44
|
+
import { armWake } from '../canvas/wakeups.js';
|
|
45
|
+
import { closeDb } from '../canvas/db.js';
|
|
46
|
+
import { readInboxSince } from '../feed/inbox.js';
|
|
47
|
+
import { markBusy, clearBusy } from '../runtime/busy.js';
|
|
48
|
+
import { closeNode } from '../runtime/close.js';
|
|
49
|
+
import { superviseTick } from '../../daemon/crtrd.js';
|
|
50
|
+
let home;
|
|
51
|
+
function node(id, over = {}) {
|
|
52
|
+
return {
|
|
53
|
+
node_id: id,
|
|
54
|
+
name: id,
|
|
55
|
+
created: new Date().toISOString(),
|
|
56
|
+
cwd: '/tmp/work',
|
|
57
|
+
kind: 'general',
|
|
58
|
+
mode: 'base',
|
|
59
|
+
lifecycle: 'terminal',
|
|
60
|
+
status: 'active',
|
|
61
|
+
...over,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function hasTmux() {
|
|
65
|
+
return spawnSync('tmux', ['-V'], { stdio: 'ignore' }).status === 0;
|
|
66
|
+
}
|
|
67
|
+
/** A pid that is guaranteed dead. */
|
|
68
|
+
function deadPid() {
|
|
69
|
+
const r = spawnSync('true', [], { stdio: 'ignore' });
|
|
70
|
+
return r.pid ?? 0x7ffffffe;
|
|
71
|
+
}
|
|
72
|
+
const cyclesOf = (id) => getNode(id)?.cycles ?? 0;
|
|
73
|
+
before(() => {
|
|
74
|
+
home = mkdtempSync(join(tmpdir(), 'crtr-child-death-wake-'));
|
|
75
|
+
process.env['CRTR_HOME'] = home;
|
|
76
|
+
});
|
|
77
|
+
beforeEach(() => {
|
|
78
|
+
closeDb();
|
|
79
|
+
rmSync(home, { recursive: true, force: true });
|
|
80
|
+
});
|
|
81
|
+
after(() => {
|
|
82
|
+
closeDb();
|
|
83
|
+
rmSync(home, { recursive: true, force: true });
|
|
84
|
+
delete process.env['CRTR_HOME'];
|
|
85
|
+
});
|
|
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
|
+
// ===========================================================================
|
|
128
|
+
// POSITIVE — a genuine terminal child outcome WAKES the dormant parent.
|
|
129
|
+
// ===========================================================================
|
|
130
|
+
test('CRASH after boot (mid-generation, pane gone) wakes the dormant inbox-waiting parent', { skip: !hasTmux() ? 'tmux unavailable' : false, timeout: 60_000 }, async () => {
|
|
131
|
+
await withLivePane('crash', async (session, window, pane) => {
|
|
132
|
+
dormantParent('PARENT', session, window, pane);
|
|
133
|
+
const dead = deadPaneIn(session, window);
|
|
134
|
+
createNode(node('CHILD', {
|
|
135
|
+
pane: dead, tmux_session: session, window,
|
|
136
|
+
pi_pid: deadPid(), pi_session_id: 'booted', intent: null, status: 'active',
|
|
137
|
+
}));
|
|
138
|
+
subscribe('PARENT', 'CHILD', true); // the spawn-time spine edge
|
|
139
|
+
markBusy('CHILD'); // pane died INSIDE a turn → genuine mid-run crash
|
|
140
|
+
const before = cyclesOf('PARENT');
|
|
141
|
+
await superviseTick();
|
|
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
|
+
});
|
|
151
|
+
});
|
|
152
|
+
test('quiet-turn FINALIZE (pane gone, nothing live to wait for) wakes the dormant parent', { skip: !hasTmux() ? 'tmux unavailable' : false, timeout: 60_000 }, async () => {
|
|
153
|
+
await withLivePane('finalize', async (session, window, pane) => {
|
|
154
|
+
dormantParent('PARENT', session, window, pane);
|
|
155
|
+
const dead = deadPaneIn(session, window);
|
|
156
|
+
// Booted, turn finished (NO busy marker), no live subscription, no pending
|
|
157
|
+
// self-wake → the daemon finalizes it (done): a dismissal of a finished node.
|
|
158
|
+
createNode(node('CHILD', {
|
|
159
|
+
pane: dead, tmux_session: session, window,
|
|
160
|
+
pi_pid: deadPid(), pi_session_id: 'booted', intent: null, status: 'active',
|
|
161
|
+
}));
|
|
162
|
+
subscribe('PARENT', 'CHILD', true);
|
|
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
|
+
});
|
|
171
|
+
});
|
|
172
|
+
test('node close of a child wakes its SURVIVING manager (the parent outside the closing set)', { skip: !hasTmux() ? 'tmux unavailable' : false, timeout: 60_000 }, async () => {
|
|
173
|
+
await withLivePane('close', async (session, window, pane) => {
|
|
174
|
+
dormantParent('PARENT', session, window, pane);
|
|
175
|
+
const childPane = deadPaneIn(session, window); // any pane handle; closeNode tears it down
|
|
176
|
+
createNode(node('CHILD', {
|
|
177
|
+
pane: childPane, tmux_session: session, window,
|
|
178
|
+
pi_session_id: 'booted', status: 'active',
|
|
179
|
+
}));
|
|
180
|
+
subscribe('PARENT', 'CHILD', true); // PARENT is a manager OUTSIDE the close set
|
|
181
|
+
// Closing CHILD: closingSet({CHILD}) cannot pull in PARENT (PARENT is a
|
|
182
|
+
// SUBSCRIBER of CHILD, never a descendant), so PARENT survives + must wake.
|
|
183
|
+
const res = closeNode('CHILD');
|
|
184
|
+
assert.deepEqual(res.closed, ['CHILD'], 'only CHILD is closed; PARENT spared');
|
|
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
|
+
});
|
|
194
|
+
});
|
|
195
|
+
// ===========================================================================
|
|
196
|
+
// NEGATIVE — HEALTHY DORMANCY must NOT wake the parent (the correctness crux).
|
|
197
|
+
// ===========================================================================
|
|
198
|
+
test('CRUX: a child dormant while awaiting its OWN live grandchild does NOT wake the parent', { skip: !hasTmux() ? 'tmux unavailable' : false, timeout: 60_000 }, async () => {
|
|
199
|
+
await withLivePane('dorm-sub', async (session, window, pane) => {
|
|
200
|
+
dormantParent('PARENT', session, window, pane);
|
|
201
|
+
const dead = deadPaneIn(session, window);
|
|
202
|
+
// CHILD finished its turn (no busy) and its pane is gone, BUT it actively
|
|
203
|
+
// subscribes to a LIVE grandchild — exactly the developer-spawned-reviewer
|
|
204
|
+
// case. This is HEALTHY dormancy, not death.
|
|
205
|
+
createNode(node('CHILD', {
|
|
206
|
+
pane: dead, tmux_session: session, window,
|
|
207
|
+
pi_pid: deadPid(), pi_session_id: 'booted', intent: null, status: 'active',
|
|
208
|
+
}));
|
|
209
|
+
createNode(node('GRANDCHILD', { status: 'active', pi_session_id: 'booted' })); // live, no pane (inline)
|
|
210
|
+
subscribe('PARENT', 'CHILD', true);
|
|
211
|
+
subscribe('CHILD', 'GRANDCHILD', true); // CHILD awaits a LIVE grandchild
|
|
212
|
+
const before = cyclesOf('PARENT');
|
|
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
|
+
});
|
|
221
|
+
});
|
|
222
|
+
test('CRUX: a child dormant on a pending self-wake (no live sub) does NOT wake the parent', { skip: !hasTmux() ? 'tmux unavailable' : false, timeout: 60_000 }, async () => {
|
|
223
|
+
await withLivePane('dorm-wake', async (session, window, pane) => {
|
|
224
|
+
dormantParent('PARENT', session, window, pane);
|
|
225
|
+
const dead = deadPaneIn(session, window);
|
|
226
|
+
createNode(node('CHILD', {
|
|
227
|
+
pane: dead, tmux_session: session, window,
|
|
228
|
+
pi_pid: deadPid(), pi_session_id: 'booted', intent: null, status: 'active',
|
|
229
|
+
}));
|
|
230
|
+
subscribe('PARENT', 'CHILD', true);
|
|
231
|
+
// CHILD has NO live subscription, but holds a pending self-wake (far-future
|
|
232
|
+
// so the wakeups pass never fires it this tick). hasPendingSelfWake(CHILD)
|
|
233
|
+
// ⇒ the daemon treats it as STILL WAITING (the stop-guard's boundary), so
|
|
234
|
+
// it RELEASES instead of finalizing. NON-VACUOUS: drop the hasPendingSelfWake
|
|
235
|
+
// guard and CHILD finalizes → PARENT is spuriously woken.
|
|
236
|
+
const future = new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).toISOString();
|
|
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
|
+
});
|
|
245
|
+
});
|