@junghanacs/entwurf 0.12.0
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/AGENTS.md +240 -0
- package/BASELINE.md +227 -0
- package/CHANGELOG.md +1210 -0
- package/CONTRIBUTING.md +63 -0
- package/DELIVERY.md +209 -0
- package/LICENSE +21 -0
- package/README.md +504 -0
- package/VERIFY.md +260 -0
- package/demo/README.md +188 -0
- package/demo/demo-baseline.sh +156 -0
- package/demo/demo.sh +183 -0
- package/docs/assets/entwurf-demo.gif +0 -0
- package/docs/assets/entwurf-doomemacs.gif +0 -0
- package/docs/assets/entwurf-entwurf.gif +0 -0
- package/docs/assets/entwurf-hero.jpg +0 -0
- package/docs/setup-clean-host.md +305 -0
- package/mcp/entwurf-bridge/src/index.ts +513 -0
- package/mcp/entwurf-bridge/start.sh +25 -0
- package/mcp/entwurf-bridge/test.sh +54 -0
- package/mcp/tsconfig.json +29 -0
- package/package.json +130 -0
- package/pi/entwurf-capabilities.json +9 -0
- package/pi/entwurf-targets.json +20 -0
- package/pi/meta-bridge/.claude-plugin/marketplace.json +12 -0
- package/pi/meta-bridge/entwurf-meta-receive/.claude-plugin/plugin.json +5 -0
- package/pi/meta-bridge/entwurf-meta-receive/hooks/hooks.json +50 -0
- package/pi/meta-bridge/entwurf-meta-receive/scripts/doorbell.sh +54 -0
- package/pi/settings.reference.json +43 -0
- package/pi/skill-plugin-example/.claude-plugin/plugin.json +4 -0
- package/pi/skill-plugin-example/skills/hello/SKILL.md +18 -0
- package/pi-extensions/acp-provider.ts +63 -0
- package/pi-extensions/entwurf-control.ts +1692 -0
- package/pi-extensions/lib/acp/acp-client.ts +90 -0
- package/pi-extensions/lib/acp/augment.ts +238 -0
- package/pi-extensions/lib/acp/backend-adapter.ts +319 -0
- package/pi-extensions/lib/acp/backend.ts +873 -0
- package/pi-extensions/lib/acp/config.ts +552 -0
- package/pi-extensions/lib/acp/context.ts +177 -0
- package/pi-extensions/lib/acp/engraving.ts +123 -0
- package/pi-extensions/lib/acp/event-mapper.ts +339 -0
- package/pi-extensions/lib/acp/models.ts +102 -0
- package/pi-extensions/lib/acp/overlay.ts +220 -0
- package/pi-extensions/lib/acp/prompts/engraving.md +1 -0
- package/pi-extensions/lib/acp/session-store.ts +418 -0
- package/pi-extensions/lib/acp/tool-surface.ts +184 -0
- package/pi-extensions/lib/entwurf-control-rpc.ts +208 -0
- package/pi-extensions/lib/entwurf-core.ts +2033 -0
- package/pi-extensions/lib/entwurf-deliverability.ts +123 -0
- package/pi-extensions/lib/entwurf-fact-provider.ts +158 -0
- package/pi-extensions/lib/entwurf-facts.ts +251 -0
- package/pi-extensions/lib/entwurf-mailbox-guard.ts +100 -0
- package/pi-extensions/lib/entwurf-peers-render.ts +154 -0
- package/pi-extensions/lib/entwurf-preflight.ts +247 -0
- package/pi-extensions/lib/entwurf-resume-args.ts +88 -0
- package/pi-extensions/lib/entwurf-self-address.ts +117 -0
- package/pi-extensions/lib/entwurf-v2-contract.ts +427 -0
- package/pi-extensions/lib/entwurf-v2-decider.ts +441 -0
- package/pi-extensions/lib/entwurf-v2-lock.ts +412 -0
- package/pi-extensions/lib/entwurf-v2-mailbox.ts +87 -0
- package/pi-extensions/lib/entwurf-v2-production.ts +326 -0
- package/pi-extensions/lib/entwurf-v2-release.ts +149 -0
- package/pi-extensions/lib/entwurf-v2-resume-marker.ts +34 -0
- package/pi-extensions/lib/entwurf-v2-runner.ts +196 -0
- package/pi-extensions/lib/entwurf-v2-send-fallback.ts +166 -0
- package/pi-extensions/lib/entwurf-v2-send.ts +271 -0
- package/pi-extensions/lib/entwurf-v2-spawn-production.ts +337 -0
- package/pi-extensions/lib/entwurf-v2-spawn.ts +323 -0
- package/pi-extensions/lib/entwurf-v2-surface.ts +205 -0
- package/pi-extensions/lib/meta-mailbox-body.ts +80 -0
- package/pi-extensions/lib/meta-session.ts +1951 -0
- package/pi-extensions/lib/project-trust-handler.ts +154 -0
- package/pi-extensions/lib/session-id.js +57 -0
- package/pi-extensions/lib/socket-discovery.ts +346 -0
- package/pi-extensions/lib/socket-probe.ts +92 -0
- package/pi-extensions/meta-bridge-hook.ts +250 -0
- package/pi-extensions/model-lock.ts +235 -0
- package/prompts/engraving.md +27 -0
- package/protocol.js +31 -0
- package/run.sh +2832 -0
- package/scripts/check-acp-backend-preflight.ts +130 -0
- package/scripts/check-acp-carrier-augment.ts +297 -0
- package/scripts/check-acp-config.ts +322 -0
- package/scripts/check-acp-event-mapper.ts +253 -0
- package/scripts/check-acp-overlay.ts +154 -0
- package/scripts/check-acp-prompt-builder.ts +191 -0
- package/scripts/check-acp-provider-surface.ts +159 -0
- package/scripts/check-acp-sdk-surface.ts +211 -0
- package/scripts/check-acp-session-reuse.ts +756 -0
- package/scripts/check-acp-session-store.ts +387 -0
- package/scripts/check-acp-tool-surface.ts +159 -0
- package/scripts/check-entwurf-bridge-boot.ts +199 -0
- package/scripts/check-entwurf-capabilities.ts +123 -0
- package/scripts/check-entwurf-control-rpc.ts +187 -0
- package/scripts/check-entwurf-deliverability.ts +158 -0
- package/scripts/check-entwurf-fact-provider.ts +316 -0
- package/scripts/check-entwurf-facts.ts +358 -0
- package/scripts/check-entwurf-mailbox-guard.ts +264 -0
- package/scripts/check-entwurf-peers-surface.ts +235 -0
- package/scripts/check-entwurf-resume-args.ts +149 -0
- package/scripts/check-entwurf-self-address.ts +209 -0
- package/scripts/check-entwurf-session-identity.ts +703 -0
- package/scripts/check-entwurf-v2-contract.ts +536 -0
- package/scripts/check-entwurf-v2-decider.ts +795 -0
- package/scripts/check-entwurf-v2-lock.ts +368 -0
- package/scripts/check-entwurf-v2-mailbox.ts +228 -0
- package/scripts/check-entwurf-v2-matrix.ts +437 -0
- package/scripts/check-entwurf-v2-production.ts +406 -0
- package/scripts/check-entwurf-v2-release.ts +245 -0
- package/scripts/check-entwurf-v2-runner.ts +452 -0
- package/scripts/check-entwurf-v2-send-fallback.ts +371 -0
- package/scripts/check-entwurf-v2-send.ts +454 -0
- package/scripts/check-entwurf-v2-spawn-production.ts +452 -0
- package/scripts/check-entwurf-v2-spawn.ts +398 -0
- package/scripts/check-entwurf-v2-surface.ts +352 -0
- package/scripts/check-keyset-overlap.py +120 -0
- package/scripts/check-mailbox-receipt-state.ts +170 -0
- package/scripts/check-meta-capability-source.ts +112 -0
- package/scripts/check-meta-dual-consumers.ts +154 -0
- package/scripts/check-meta-dual-read.ts +158 -0
- package/scripts/check-meta-listing.ts +138 -0
- package/scripts/check-meta-mailbox-state-write.ts +135 -0
- package/scripts/check-meta-migration.ts +212 -0
- package/scripts/check-meta-receiver-marker.ts +185 -0
- package/scripts/check-meta-record-v2.ts +191 -0
- package/scripts/check-meta-session.ts +673 -0
- package/scripts/check-model-lock.ts +408 -0
- package/scripts/check-package-source-routing.ts +253 -0
- package/scripts/check-pi-preflight.ts +304 -0
- package/scripts/check-project-trust-handler.ts +265 -0
- package/scripts/check-shell-quote.ts +121 -0
- package/scripts/check-socket-discovery.ts +428 -0
- package/scripts/check-socket-probe.ts +106 -0
- package/scripts/fixtures/probe-mcp-server.ts +33 -0
- package/scripts/gnew-rpc-drive.ts +211 -0
- package/scripts/lib/acp-child-cleanup.ts +116 -0
- package/scripts/meta-bridge-doctor.sh +315 -0
- package/scripts/meta-bridge-hook-log.sh +26 -0
- package/scripts/meta-bridge-install.sh +135 -0
- package/scripts/meta-bridge-prune.ts +199 -0
- package/scripts/meta-bridge-state.py +549 -0
- package/scripts/meta-bridge-statusline.sh +192 -0
- package/scripts/meta-bridge-store-doctor.ts +64 -0
- package/scripts/meta-bridge-uninstall.sh +39 -0
- package/scripts/new-session-id.ts +25 -0
- package/scripts/postinstall-chmod.cjs +58 -0
- package/scripts/raw-async-delivery/README.md +258 -0
- package/scripts/raw-async-delivery/cc-enqueue-addressed.sh +35 -0
- package/scripts/raw-async-delivery/cc-mailbox-rewake.sh +38 -0
- package/scripts/raw-async-delivery/cc-watch-filechanged.sh +19 -0
- package/scripts/raw-async-delivery/cc-watch-sessionstart.sh +15 -0
- package/scripts/raw-async-delivery/codex-local-appserver.sh +39 -0
- package/scripts/raw-async-delivery/plugin-entwurf-receive/.claude-plugin/plugin.json +5 -0
- package/scripts/raw-async-delivery/plugin-entwurf-receive/hooks/hooks.json +28 -0
- package/scripts/raw-async-delivery/plugin-entwurf-receive/scripts/watch-filechanged.sh +28 -0
- package/scripts/raw-async-delivery/plugin-entwurf-receive/scripts/watch-sessionstart.sh +27 -0
- package/scripts/raw-async-delivery/raw-agy-send.sh +29 -0
- package/scripts/raw-async-delivery/raw-claude-enqueue.sh +30 -0
- package/scripts/raw-async-delivery/raw-codex-ws-turn-start.py +164 -0
- package/scripts/raw-async-delivery/repro-addressed-routing.sh +96 -0
- package/scripts/raw-async-delivery/repro-plugin-idle-wake.sh +104 -0
- package/scripts/resolve-acp-bridge.ts +25 -0
- package/scripts/smoke-acp-bundled-mcp-live.ts +292 -0
- package/scripts/smoke-acp-carrier-augment-live.ts +192 -0
- package/scripts/smoke-acp-mcp-live.ts +129 -0
- package/scripts/smoke-acp-memory-containment-live.ts +389 -0
- package/scripts/smoke-acp-overlay-live.ts +314 -0
- package/scripts/smoke-acp-provider-live.ts +162 -0
- package/scripts/smoke-acp-raw-turn-live.ts +261 -0
- package/scripts/smoke-acp-session-reuse-live.ts +172 -0
- package/scripts/smoke-acp-skill-live.ts +144 -0
- package/scripts/smoke-acp-socket-citizen-live.ts +168 -0
- package/scripts/smoke-claude-native-resume-live.sh +198 -0
- package/scripts/smoke-entwurf-v2-matrix-live.ts +398 -0
- package/scripts/smoke-entwurf-v2-spawn-live.ts +175 -0
- package/scripts/smoke-entwurf-v2-spawn-resume-live.ts +428 -0
- package/scripts/smoke-meta-async-drift.sh +171 -0
- package/scripts/smoke-meta-honesty.sh +147 -0
- package/scripts/smoke-meta-install-state.sh +403 -0
- package/scripts/smoke-meta-keyset-guard.sh +111 -0
- package/scripts/smoke-meta-prune.sh +174 -0
- package/scripts/smoke-resident-garden-guard.sh +433 -0
- package/scripts/smoke-session-id-name.ts +187 -0
- package/scripts/tsconfig.json +34 -0
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* check-entwurf-v2-spawn — deterministic gate for the 5c-3a spawn-bg RESUME watcher hand
|
|
3
|
+
* (`executeSpawnBgResume`). It proves the spawn→observe→release WIRING over injected fakes
|
|
4
|
+
* with controlled promises — NO real child, socket, or timer — exactly as 5b/5c-2 proved
|
|
5
|
+
* their hands. The load-bearing property under test is Fable 3: TIMEOUT IS NOT A RELEASE.
|
|
6
|
+
*
|
|
7
|
+
* 1. spawnChild throws → `spawn-start-failed`, release ×1, original error surfaced.
|
|
8
|
+
* 2. socket-alive wins → `socket-alive`, release ×1; the exit loser settling later is a
|
|
9
|
+
* no-op (single release); killChild NOT called.
|
|
10
|
+
* 3. child-exited wins (code 0 / 1 / null) → `child-exited`, release ×1; killChild NOT
|
|
11
|
+
* called; socket loser later no-op.
|
|
12
|
+
* 4. timeout wins → release 0 AT KILL TIME (bare timeout never releases) AND killChild
|
|
13
|
+
* called ×1 — the timeout escalates to a kill, it does not release.
|
|
14
|
+
* 5. timeout → kill → child-exited (null) observed in grace → `child-exited`, release ×1.
|
|
15
|
+
* 6. timeout → kill → socket-alive observed in grace → `socket-alive`, release ×1.
|
|
16
|
+
* 7. timeout → kill → grace elapses with NO observation → `lock-retained`
|
|
17
|
+
* (reason `kill-unconfirmed`), release 0, full diagnostic surfaced, bounded return.
|
|
18
|
+
* 8. post-spawn dep throw (awaitChildExit rejects in the primary race) → best-effort kill,
|
|
19
|
+
* exit cannot be observed → `lock-retained` (reason `observe-failed`), release 0.
|
|
20
|
+
* 9. post-spawn dep throw (awaitSocketAlive rejects in the primary race) → kill → exit
|
|
21
|
+
* observed in grace → `child-exited`, release ×1 (the working exit watcher still wins).
|
|
22
|
+
* 10. releaseLock throw on a RELEASED (socket-alive) path PROPAGATES — the observation
|
|
23
|
+
* already happened, the caller must not re-spawn (release error surfaces honestly).
|
|
24
|
+
* 11. lock invariants: null lock / mismatched-gid lock → throws (decideReleasePolicy),
|
|
25
|
+
* BEFORE any spawn (spawnChild NOT called).
|
|
26
|
+
* 12. no direct-release hatch: across the retained paths (7,8) deps.releaseLock is never
|
|
27
|
+
* reached — the ONLY release path is reduceRelease on a real observation event.
|
|
28
|
+
*
|
|
29
|
+
* Each await dep is a deferred the gate resolves in a chosen order, flushing microtasks
|
|
30
|
+
* between steps, so "release happens after the observation, exactly once" is asserted
|
|
31
|
+
* structurally over every event order.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import assert from "node:assert/strict";
|
|
35
|
+
import type { LockClaim } from "../pi-extensions/lib/entwurf-v2-lock.ts";
|
|
36
|
+
import {
|
|
37
|
+
executeSpawnBgResume,
|
|
38
|
+
type SpawnBgPlan,
|
|
39
|
+
type SpawnBgResumeDeps,
|
|
40
|
+
type SpawnedChild,
|
|
41
|
+
} from "../pi-extensions/lib/entwurf-v2-spawn.ts";
|
|
42
|
+
|
|
43
|
+
let passed = 0;
|
|
44
|
+
function ok(label: string, cond: boolean): void {
|
|
45
|
+
assert.ok(cond, label);
|
|
46
|
+
console.log(` ok ${label}`);
|
|
47
|
+
passed++;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const GID = "20260612T100000-aaaaaa";
|
|
51
|
+
const WRONG_GID = "20260612T999999-bbbbbb";
|
|
52
|
+
const SOCK = "/fake/entwurf-control/20260612T100000-aaaaaa.sock";
|
|
53
|
+
const LOCKPATH = `/fake/locks/${GID}.lock`;
|
|
54
|
+
|
|
55
|
+
function lockClaim(gardenId = GID): LockClaim {
|
|
56
|
+
return {
|
|
57
|
+
gardenId,
|
|
58
|
+
pid: 4242,
|
|
59
|
+
hostname: "test-host",
|
|
60
|
+
createdAt: "2026-06-12T01:00:00.000Z",
|
|
61
|
+
nonce: "deadbeefcafef00d",
|
|
62
|
+
owner: "entwurf_v2",
|
|
63
|
+
lockPath: `/fake/locks/${gardenId}.lock`,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function spawnBgPlan(): SpawnBgPlan {
|
|
68
|
+
return {
|
|
69
|
+
transport: "spawn-bg",
|
|
70
|
+
action: "resume",
|
|
71
|
+
targetGardenId: GID,
|
|
72
|
+
sessionId: GID,
|
|
73
|
+
cwd: "/home/test/repo",
|
|
74
|
+
prompt: "continue",
|
|
75
|
+
launchArgs: ["--approve"],
|
|
76
|
+
expectedSocketPath: SOCK,
|
|
77
|
+
observeTimeoutMs: 30_000,
|
|
78
|
+
releaseWhen: "socket-alive-or-child-exited",
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
interface Deferred<T> {
|
|
83
|
+
promise: Promise<T>;
|
|
84
|
+
resolve: (v: T) => void;
|
|
85
|
+
reject: (e: unknown) => void;
|
|
86
|
+
}
|
|
87
|
+
function deferred<T>(): Deferred<T> {
|
|
88
|
+
let resolve!: (v: T) => void;
|
|
89
|
+
let reject!: (e: unknown) => void;
|
|
90
|
+
const promise = new Promise<T>((res, rej) => {
|
|
91
|
+
resolve = res;
|
|
92
|
+
reject = rej;
|
|
93
|
+
});
|
|
94
|
+
return { promise, resolve, reject };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const flush = (): Promise<void> => new Promise((res) => setImmediate(res));
|
|
98
|
+
|
|
99
|
+
interface Calls {
|
|
100
|
+
spawn: number;
|
|
101
|
+
socket: number;
|
|
102
|
+
exit: number;
|
|
103
|
+
timeout: number;
|
|
104
|
+
kill: number;
|
|
105
|
+
release: number;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
interface Harness {
|
|
109
|
+
deps: SpawnBgResumeDeps;
|
|
110
|
+
calls: Calls;
|
|
111
|
+
socket: Deferred<void>;
|
|
112
|
+
exit: Deferred<number | null>;
|
|
113
|
+
observe: Deferred<void>; // 1st awaitTimeout call (observeTimeoutMs)
|
|
114
|
+
grace: Deferred<void>; // 2nd awaitTimeout call (killGraceMs)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
interface HarnessOpts {
|
|
118
|
+
spawnThrows?: string;
|
|
119
|
+
killThrows?: boolean;
|
|
120
|
+
releaseThrows?: boolean;
|
|
121
|
+
socketSyncThrows?: string; // awaitSocketAlive throws SYNCHRONOUSLY (buggy dep)
|
|
122
|
+
exitSyncThrows?: string; // awaitChildExit throws SYNCHRONOUSLY (buggy dep)
|
|
123
|
+
child?: SpawnedChild;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function makeHarness(opts: HarnessOpts = {}): Harness {
|
|
127
|
+
const calls: Calls = { spawn: 0, socket: 0, exit: 0, timeout: 0, kill: 0, release: 0 };
|
|
128
|
+
const socket = deferred<void>();
|
|
129
|
+
const exit = deferred<number | null>();
|
|
130
|
+
const observe = deferred<void>();
|
|
131
|
+
const grace = deferred<void>();
|
|
132
|
+
const child: SpawnedChild = opts.child ?? { pid: 9191 };
|
|
133
|
+
|
|
134
|
+
const deps: SpawnBgResumeDeps = {
|
|
135
|
+
spawnChild: async () => {
|
|
136
|
+
calls.spawn++;
|
|
137
|
+
if (opts.spawnThrows) throw new Error(opts.spawnThrows);
|
|
138
|
+
return child;
|
|
139
|
+
},
|
|
140
|
+
awaitSocketAlive: (_socketPath, _signal) => {
|
|
141
|
+
calls.socket++;
|
|
142
|
+
if (opts.socketSyncThrows) throw new Error(opts.socketSyncThrows);
|
|
143
|
+
return socket.promise;
|
|
144
|
+
},
|
|
145
|
+
awaitChildExit: (_c, _signal) => {
|
|
146
|
+
calls.exit++;
|
|
147
|
+
if (opts.exitSyncThrows) throw new Error(opts.exitSyncThrows);
|
|
148
|
+
return exit.promise;
|
|
149
|
+
},
|
|
150
|
+
awaitTimeout: (_ms, _signal) => {
|
|
151
|
+
calls.timeout++;
|
|
152
|
+
return calls.timeout === 1 ? observe.promise : grace.promise;
|
|
153
|
+
},
|
|
154
|
+
killChild: (_c) => {
|
|
155
|
+
calls.kill++;
|
|
156
|
+
if (opts.killThrows) throw new Error("kill failed");
|
|
157
|
+
},
|
|
158
|
+
releaseLock: (_lock) => {
|
|
159
|
+
calls.release++;
|
|
160
|
+
if (opts.releaseThrows) throw new Error("release failed");
|
|
161
|
+
},
|
|
162
|
+
killGraceMs: 5_000,
|
|
163
|
+
};
|
|
164
|
+
return { deps, calls, socket, exit, observe, grace };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async function main(): Promise<void> {
|
|
168
|
+
// ── 1. spawnChild throws → spawn-start-failed, release ×1, error surfaced ────────
|
|
169
|
+
{
|
|
170
|
+
const h = makeHarness({ spawnThrows: "ENOENT pi binary" });
|
|
171
|
+
const r = await executeSpawnBgResume(spawnBgPlan(), lockClaim(), h.deps);
|
|
172
|
+
ok("1 spawn-start-failed kind", r.kind === "spawn-start-failed");
|
|
173
|
+
ok("1 released true", r.released === true);
|
|
174
|
+
ok("1 error surfaced", r.kind === "spawn-start-failed" && r.error.includes("ENOENT pi binary"));
|
|
175
|
+
ok("1 release ×1", h.calls.release === 1);
|
|
176
|
+
ok("1 no socket/exit/timeout watch", h.calls.socket === 0 && h.calls.exit === 0 && h.calls.timeout === 0);
|
|
177
|
+
ok("1 no kill", h.calls.kill === 0);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ── 2. socket-alive wins → socket-alive, release ×1, no kill; exit loser no-op ───
|
|
181
|
+
{
|
|
182
|
+
const h = makeHarness();
|
|
183
|
+
const p = executeSpawnBgResume(spawnBgPlan(), lockClaim(), h.deps);
|
|
184
|
+
await flush();
|
|
185
|
+
h.socket.resolve();
|
|
186
|
+
await flush();
|
|
187
|
+
// the exit loser settles AFTER the win — must not double release.
|
|
188
|
+
h.exit.resolve(0);
|
|
189
|
+
const r = await p;
|
|
190
|
+
ok("2 socket-alive kind", r.kind === "socket-alive");
|
|
191
|
+
ok("2 released true", r.released === true);
|
|
192
|
+
ok("2 pid surfaced", r.kind === "socket-alive" && r.pid === 9191);
|
|
193
|
+
ok("2 release exactly once", h.calls.release === 1);
|
|
194
|
+
ok("2 no kill (no timeout)", h.calls.kill === 0);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ── 3. child-exited wins (code 0 / 1 / null) → child-exited, release ×1, no kill ──
|
|
198
|
+
for (const code of [0, 1, null] as const) {
|
|
199
|
+
const h = makeHarness();
|
|
200
|
+
const p = executeSpawnBgResume(spawnBgPlan(), lockClaim(), h.deps);
|
|
201
|
+
await flush();
|
|
202
|
+
h.exit.resolve(code);
|
|
203
|
+
await flush();
|
|
204
|
+
h.socket.resolve(); // loser
|
|
205
|
+
const r = await p;
|
|
206
|
+
ok(`3 child-exited kind (code=${code})`, r.kind === "child-exited");
|
|
207
|
+
ok(`3 exitCode=${code}`, r.kind === "child-exited" && r.exitCode === code);
|
|
208
|
+
ok(`3 release exactly once (code=${code})`, h.calls.release === 1);
|
|
209
|
+
ok(`3 no kill (code=${code})`, h.calls.kill === 0);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ── 4. timeout wins → release 0 at kill time, killChild ×1 (bare timeout ≠ release) ─
|
|
213
|
+
{
|
|
214
|
+
const h = makeHarness();
|
|
215
|
+
const p = executeSpawnBgResume(spawnBgPlan(), lockClaim(), h.deps);
|
|
216
|
+
await flush();
|
|
217
|
+
h.observe.resolve(); // observe timeout fires first
|
|
218
|
+
await flush();
|
|
219
|
+
// At this point the hand has killed and is awaiting the grace observation.
|
|
220
|
+
ok("4 release 0 after bare timeout", h.calls.release === 0);
|
|
221
|
+
ok("4 killChild ×1 on timeout", h.calls.kill === 1);
|
|
222
|
+
// drain: let the kill produce an exit so the promise settles.
|
|
223
|
+
h.exit.resolve(null);
|
|
224
|
+
const r = await p;
|
|
225
|
+
ok("4 settles as child-exited after kill", r.kind === "child-exited");
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ── 5. timeout → kill → child-exited(null) in grace → child-exited, release ×1 ───
|
|
229
|
+
{
|
|
230
|
+
const h = makeHarness();
|
|
231
|
+
const p = executeSpawnBgResume(spawnBgPlan(), lockClaim(), h.deps);
|
|
232
|
+
await flush();
|
|
233
|
+
h.observe.resolve();
|
|
234
|
+
await flush();
|
|
235
|
+
h.exit.resolve(null); // the kill's exit observed within grace
|
|
236
|
+
const r = await p;
|
|
237
|
+
ok("5 child-exited after kill", r.kind === "child-exited");
|
|
238
|
+
ok("5 exitCode null (signal)", r.kind === "child-exited" && r.exitCode === null);
|
|
239
|
+
ok("5 release exactly once", h.calls.release === 1);
|
|
240
|
+
ok("5 kill ×1", h.calls.kill === 1);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// ── 6. timeout → kill → socket-alive in grace → socket-alive, release ×1 ─────────
|
|
244
|
+
{
|
|
245
|
+
const h = makeHarness();
|
|
246
|
+
const p = executeSpawnBgResume(spawnBgPlan(), lockClaim(), h.deps);
|
|
247
|
+
await flush();
|
|
248
|
+
h.observe.resolve();
|
|
249
|
+
await flush();
|
|
250
|
+
h.socket.resolve(); // child raced its socket up just as we killed
|
|
251
|
+
const r = await p;
|
|
252
|
+
ok("6 socket-alive after kill", r.kind === "socket-alive");
|
|
253
|
+
ok("6 release exactly once", h.calls.release === 1);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// ── 7. timeout → kill → grace elapses, no observation → lock-retained, release 0 ──
|
|
257
|
+
{
|
|
258
|
+
const h = makeHarness();
|
|
259
|
+
const p = executeSpawnBgResume(spawnBgPlan(), lockClaim(), h.deps);
|
|
260
|
+
await flush();
|
|
261
|
+
h.observe.resolve();
|
|
262
|
+
await flush();
|
|
263
|
+
h.grace.resolve(); // grace elapses with no socket/exit
|
|
264
|
+
const r = await p;
|
|
265
|
+
ok("7 lock-retained kind", r.kind === "lock-retained");
|
|
266
|
+
ok("7 released false", r.released === false);
|
|
267
|
+
ok("7 reason kill-unconfirmed", r.kind === "lock-retained" && r.reason === "kill-unconfirmed");
|
|
268
|
+
ok("7 release NEVER (no blind release)", h.calls.release === 0);
|
|
269
|
+
const diag = r.kind === "lock-retained" ? r.diagnostic : null;
|
|
270
|
+
ok("7 diagnostic targetGardenId", diag?.targetGardenId === GID);
|
|
271
|
+
ok("7 diagnostic socketPath", diag?.expectedSocketPath === SOCK);
|
|
272
|
+
ok("7 diagnostic lockPath", diag?.lockPath === LOCKPATH);
|
|
273
|
+
ok("7 diagnostic pid", diag?.pid === 9191);
|
|
274
|
+
ok("7 diagnostic observeTimeoutMs", diag?.observeTimeoutMs === 30_000);
|
|
275
|
+
ok("7 diagnostic killGraceMs", diag?.killGraceMs === 5_000);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// ── 8. post-spawn dep throw (awaitChildExit rejects) → kill, no exit → retained ──
|
|
279
|
+
{
|
|
280
|
+
const h = makeHarness();
|
|
281
|
+
const p = executeSpawnBgResume(spawnBgPlan(), lockClaim(), h.deps);
|
|
282
|
+
await flush();
|
|
283
|
+
// the exit watcher itself fails — its rejection both triggers the backstop AND means
|
|
284
|
+
// the exit can never be observed → fail-closed to lock-retained.
|
|
285
|
+
h.exit.reject(new Error("exit watcher EBADF"));
|
|
286
|
+
const r = await p;
|
|
287
|
+
ok("8 lock-retained kind", r.kind === "lock-retained");
|
|
288
|
+
ok("8 reason observe-failed", r.kind === "lock-retained" && r.reason === "observe-failed");
|
|
289
|
+
ok("8 original error surfaced", r.kind === "lock-retained" && (r.error ?? "").includes("exit watcher EBADF"));
|
|
290
|
+
ok("8 best-effort kill called", h.calls.kill === 1);
|
|
291
|
+
ok("8 release NEVER", h.calls.release === 0);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// ── 9. post-spawn dep throw (awaitSocketAlive rejects) → kill → exit in grace ────
|
|
295
|
+
{
|
|
296
|
+
const h = makeHarness();
|
|
297
|
+
const p = executeSpawnBgResume(spawnBgPlan(), lockClaim(), h.deps);
|
|
298
|
+
await flush();
|
|
299
|
+
h.socket.reject(new Error("socket watcher EACCES")); // primary race rejects → backstop
|
|
300
|
+
await flush();
|
|
301
|
+
// the exit watcher is healthy → the kill's exit is observed within grace.
|
|
302
|
+
h.exit.resolve(137);
|
|
303
|
+
const r = await p;
|
|
304
|
+
ok("9 child-exited after backstop kill", r.kind === "child-exited");
|
|
305
|
+
ok("9 exitCode 137", r.kind === "child-exited" && r.exitCode === 137);
|
|
306
|
+
ok("9 release exactly once", h.calls.release === 1);
|
|
307
|
+
ok("9 kill ×1", h.calls.kill === 1);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// ── 10. releaseLock throw on a released path PROPAGATES (caller must not re-spawn) ─
|
|
311
|
+
{
|
|
312
|
+
const h = makeHarness({ releaseThrows: true });
|
|
313
|
+
const p = executeSpawnBgResume(spawnBgPlan(), lockClaim(), h.deps);
|
|
314
|
+
await flush();
|
|
315
|
+
h.socket.resolve();
|
|
316
|
+
let threw: unknown;
|
|
317
|
+
try {
|
|
318
|
+
await p;
|
|
319
|
+
} catch (e) {
|
|
320
|
+
threw = e;
|
|
321
|
+
}
|
|
322
|
+
ok("10 release error propagates", threw instanceof Error && threw.message === "release failed");
|
|
323
|
+
ok("10 release was attempted once", h.calls.release === 1);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// ── 11. lock invariants → decideReleasePolicy throws BEFORE any spawn ────────────
|
|
327
|
+
{
|
|
328
|
+
const h = makeHarness();
|
|
329
|
+
let threw = false;
|
|
330
|
+
try {
|
|
331
|
+
await executeSpawnBgResume(spawnBgPlan(), null, h.deps);
|
|
332
|
+
} catch {
|
|
333
|
+
threw = true;
|
|
334
|
+
}
|
|
335
|
+
ok("11 null lock throws", threw);
|
|
336
|
+
ok("11 null lock: no spawn attempted", h.calls.spawn === 0);
|
|
337
|
+
|
|
338
|
+
const h2 = makeHarness();
|
|
339
|
+
let threw2 = false;
|
|
340
|
+
try {
|
|
341
|
+
await executeSpawnBgResume(spawnBgPlan(), lockClaim(WRONG_GID), h2.deps);
|
|
342
|
+
} catch {
|
|
343
|
+
threw2 = true;
|
|
344
|
+
}
|
|
345
|
+
ok("11 mismatched-gid lock throws", threw2);
|
|
346
|
+
ok("11 mismatched-gid: no spawn attempted", h2.calls.spawn === 0);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// ── 12. kill grace where kill itself throws but exit still observed → released ───
|
|
350
|
+
// (proves killChild throwing is best-effort: the observation still drives release.)
|
|
351
|
+
{
|
|
352
|
+
const h = makeHarness({ killThrows: true });
|
|
353
|
+
const p = executeSpawnBgResume(spawnBgPlan(), lockClaim(), h.deps);
|
|
354
|
+
await flush();
|
|
355
|
+
h.observe.resolve();
|
|
356
|
+
await flush();
|
|
357
|
+
h.exit.resolve(0); // exit observed despite the kill throw
|
|
358
|
+
const r = await p;
|
|
359
|
+
ok("12 child-exited despite kill throw", r.kind === "child-exited");
|
|
360
|
+
ok("12 release exactly once", h.calls.release === 1);
|
|
361
|
+
ok("12 kill attempted once", h.calls.kill === 1);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// ── 13. S1 (Fable 2차): awaitSocketAlive throws SYNCHRONOUSLY at creation (a buggy dep
|
|
365
|
+
// that throws where it must return a Promise). The child exists → no blind release;
|
|
366
|
+
// best-effort kill + lock-retained fail-closed. The async race (cases 8/9) never
|
|
367
|
+
// runs, so this would be a silent leak without the dedicated creation try. ────────
|
|
368
|
+
{
|
|
369
|
+
const h = makeHarness({ socketSyncThrows: "awaitSocketAlive EFAULT (sync)" });
|
|
370
|
+
const r = await executeSpawnBgResume(spawnBgPlan(), lockClaim(), h.deps);
|
|
371
|
+
ok("13 lock-retained on sync socket throw", r.kind === "lock-retained");
|
|
372
|
+
ok("13 reason observe-failed", r.kind === "lock-retained" && r.reason === "observe-failed");
|
|
373
|
+
ok("13 sync error surfaced", r.kind === "lock-retained" && (r.error ?? "").includes("awaitSocketAlive EFAULT"));
|
|
374
|
+
ok("13 best-effort kill ×1", h.calls.kill === 1);
|
|
375
|
+
ok("13 release NEVER (no blind release)", h.calls.release === 0);
|
|
376
|
+
ok("13 diagnostic lockPath surfaced", r.kind === "lock-retained" && r.diagnostic.lockPath === LOCKPATH);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// ── 14. S1 sibling: awaitChildExit throws SYNCHRONOUSLY at creation (socket watcher DID
|
|
380
|
+
// start, then exit creation throws). Same fail-closed retained; the started socket
|
|
381
|
+
// watcher is defused so it cannot surface as an unhandled rejection. ──────────────
|
|
382
|
+
{
|
|
383
|
+
const h = makeHarness({ exitSyncThrows: "awaitChildExit EFAULT (sync)" });
|
|
384
|
+
const r = await executeSpawnBgResume(spawnBgPlan(), lockClaim(), h.deps);
|
|
385
|
+
ok("14 lock-retained on sync exit throw", r.kind === "lock-retained");
|
|
386
|
+
ok("14 reason observe-failed", r.kind === "lock-retained" && r.reason === "observe-failed");
|
|
387
|
+
ok("14 best-effort kill ×1", h.calls.kill === 1);
|
|
388
|
+
ok("14 release NEVER", h.calls.release === 0);
|
|
389
|
+
ok("14 socket watcher had started", h.calls.socket === 1);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
console.log(`\ncheck-entwurf-v2-spawn: ${passed} checks passed`);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
main().catch((err) => {
|
|
396
|
+
console.error(err);
|
|
397
|
+
process.exit(1);
|
|
398
|
+
});
|