@junghanacs/entwurf 0.13.0 → 0.14.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 +52 -17
- package/BASELINE.md +60 -206
- package/CHANGELOG.md +64 -0
- package/CONTRIBUTING.md +14 -10
- package/DELIVERY.md +118 -261
- package/README.md +91 -431
- package/VERIFY.md +74 -95
- package/demo/README.md +1 -1
- package/demo/demo-baseline.sh +1 -3
- package/demo/demo.sh +2 -5
- package/docs/acp-backend-rail.md +236 -0
- package/docs/external-mcp-host.md +131 -0
- package/docs/fresh-cut-policy.md +99 -0
- package/docs/setup-clean-host.md +124 -328
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +160 -67
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/acp-client.js +12 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/backend-adapter.js +11 -10
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/config.js +1 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/engraving.js +42 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/models.js +4 -4
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/overlay.js +11 -9
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-control-rpc.js +7 -5
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-core.js +13 -14
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-resume-args.js +45 -40
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-contract.js +117 -95
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-decider.js +23 -57
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-lock.js +16 -7
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-production.js +5 -53
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-release.js +21 -36
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-runner.js +3 -15
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-send-fallback.js +12 -11
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-send.js +2 -7
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-surface.js +30 -67
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-visible-resume.js +256 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js +91 -5
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +258 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-launch.js +202 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-placement.js +289 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-resume-call.js +170 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/resume-launch-identity.js +136 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/session-id.js +8 -5
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/socket-discovery.js +3 -3
- package/mcp/entwurf-bridge/dist/scripts/meta-facts.js +51 -0
- package/mcp/entwurf-bridge/dist/scripts/new-session-id.js +9 -4
- package/mcp/entwurf-bridge/src/index.ts +176 -67
- package/mcp/entwurf-bridge/start.sh +2 -2
- package/mcp/entwurf-bridge/test.sh +23 -9
- package/mcp/entwurf-bridge/tsconfig.build.json +11 -2
- package/package.json +25 -11
- package/pi-extensions/entwurf-control.ts +220 -71
- package/pi-extensions/lib/acp/acp-client.ts +22 -0
- package/pi-extensions/lib/acp/augment.ts +42 -3
- package/pi-extensions/lib/acp/backend-adapter.ts +12 -11
- package/pi-extensions/lib/acp/backend.ts +366 -38
- package/pi-extensions/lib/acp/config.ts +1 -1
- package/pi-extensions/lib/acp/engraving.ts +43 -1
- package/pi-extensions/lib/acp/event-mapper.ts +16 -13
- package/pi-extensions/lib/acp/models.ts +4 -4
- package/pi-extensions/lib/acp/overlay.ts +11 -9
- package/pi-extensions/lib/entwurf-control-rpc.ts +7 -5
- package/pi-extensions/lib/entwurf-core.ts +15 -15
- package/pi-extensions/lib/entwurf-resume-args.ts +41 -52
- package/pi-extensions/lib/entwurf-v2-contract-schema.ts +1 -1
- package/pi-extensions/lib/entwurf-v2-contract.ts +120 -99
- package/pi-extensions/lib/entwurf-v2-decider.ts +30 -91
- package/pi-extensions/lib/entwurf-v2-lock.ts +16 -7
- package/pi-extensions/lib/entwurf-v2-production.ts +4 -78
- package/pi-extensions/lib/entwurf-v2-release.ts +25 -49
- package/pi-extensions/lib/entwurf-v2-runner.ts +6 -21
- package/pi-extensions/lib/entwurf-v2-send-fallback.ts +12 -11
- package/pi-extensions/lib/entwurf-v2-send.ts +2 -7
- package/pi-extensions/lib/entwurf-v2-surface.ts +36 -76
- package/pi-extensions/lib/entwurf-v2-visible-resume.ts +370 -0
- package/pi-extensions/lib/meta-session.ts +93 -5
- package/pi-extensions/lib/mux-fresh-call.ts +328 -0
- package/pi-extensions/lib/mux-launch.ts +267 -0
- package/pi-extensions/lib/mux-placement.ts +387 -0
- package/pi-extensions/lib/mux-resume-call.ts +221 -0
- package/pi-extensions/lib/resume-launch-identity.ts +162 -0
- package/pi-extensions/lib/session-id.js +8 -5
- package/pi-extensions/lib/socket-discovery.ts +3 -3
- package/prompts/engraving.md +13 -5
- package/run.sh +688 -282
- package/scripts/agy-bridge-config.py +5 -1
- package/scripts/check-acp-backend-preflight.ts +1 -1
- package/scripts/check-acp-carrier-augment.ts +246 -10
- package/scripts/check-acp-cortex.ts +5 -5
- package/scripts/check-acp-overlay.ts +13 -3
- package/scripts/check-acp-prompt-lifecycle.ts +565 -0
- package/scripts/check-acp-stop-reason.ts +342 -0
- package/scripts/check-acp-stream-hooks.ts +504 -0
- package/scripts/check-elapsed.sh +25 -0
- package/scripts/check-entwurf-bridge-boot.ts +51 -4
- package/scripts/check-entwurf-bridge-pi-free.ts +6 -5
- package/scripts/check-entwurf-control-rpc.ts +4 -3
- package/scripts/check-entwurf-resume-args.ts +72 -70
- package/scripts/check-entwurf-session-identity.ts +14 -10
- package/scripts/check-entwurf-v2-contract.ts +34 -59
- package/scripts/check-entwurf-v2-decider.ts +17 -177
- package/scripts/check-entwurf-v2-lock.ts +5 -2
- package/scripts/check-entwurf-v2-matrix.ts +3 -53
- package/scripts/check-entwurf-v2-production.ts +2 -91
- package/scripts/check-entwurf-v2-release.ts +10 -105
- package/scripts/check-entwurf-v2-runner.ts +4 -85
- package/scripts/check-entwurf-v2-send-fallback.ts +5 -6
- package/scripts/check-entwurf-v2-send.ts +0 -28
- package/scripts/check-entwurf-v2-surface.ts +198 -128
- package/scripts/check-entwurf-v2-visible-resume.ts +445 -0
- package/scripts/check-fresh-cut-gate.sh +1 -1
- package/scripts/check-gate-qualification.ts +101 -7
- package/scripts/check-install-container.sh +10 -2
- package/scripts/check-install-surface.ts +1 -1
- package/scripts/check-keyset-overlap.py +1 -1
- package/scripts/check-meta-facts.ts +249 -0
- package/scripts/check-meta-identity-consumers.ts +1 -1
- package/scripts/check-meta-session.ts +169 -0
- package/scripts/check-mux-launch-tmux.ts +316 -0
- package/scripts/check-mux-launch.ts +288 -0
- package/scripts/check-mux-launcher-fence.ts +264 -0
- package/scripts/check-mux-parent-artifact.ts +195 -0
- package/scripts/check-mux-placement-tmux.ts +322 -0
- package/scripts/check-mux-placement.ts +323 -0
- package/scripts/check-mux-resume-call.ts +282 -0
- package/scripts/check-probe-cli-shim.ts +25 -22
- package/scripts/check-probe-ordering.ts +121 -81
- package/scripts/check-release-gate-outcomes.ts +452 -0
- package/scripts/check-resume-launch-identity.ts +244 -0
- package/scripts/check-socket-discovery.ts +1 -1
- package/scripts/fixtures/mux-parent-transcript.scrubbed.jsonl +3 -0
- package/scripts/inventory-verification-surface.ts +349 -0
- package/scripts/lib/claude-launcher-fence.ts +322 -0
- package/scripts/lib/live-skip.ts +33 -0
- package/scripts/lib/mutation-qualify.ts +109 -3
- package/scripts/lib/probe-acp-turn.ts +33 -12
- package/scripts/lib/step-outcome.sh +88 -0
- package/scripts/meta-bridge-doctor.sh +6 -8
- package/scripts/meta-facts.ts +60 -0
- package/scripts/mutants/acp-augment.json +106 -0
- package/scripts/mutants/acp-cortex.json +2 -2
- package/scripts/mutants/acp-overlay.json +17 -0
- package/scripts/mutants/acp-prompt-lifecycle.json +100 -0
- package/scripts/mutants/acp-stop-reason.json +80 -0
- package/scripts/mutants/acp-stream-hooks.json +158 -0
- package/scripts/mutants/bridge-boot-resume.json +45 -0
- package/scripts/mutants/meta-facts.json +50 -0
- package/scripts/mutants/meta-identity.json +36 -0
- package/scripts/mutants/mux-boundary.json +196 -0
- package/scripts/mutants/mux-fresh-call.json +185 -0
- package/scripts/mutants/mux-launcher-fence.json +123 -0
- package/scripts/mutants/mux-parent-artifact.json +39 -0
- package/scripts/mutants/mux-resume-call.json +148 -0
- package/scripts/mutants/probe-ordering.json +6 -1021
- package/scripts/mutants/release-gate.json +140 -0
- package/scripts/mutants/resume-args.json +76 -0
- package/scripts/mutants/resume-launch-identity.json +96 -0
- package/scripts/mutants/v2-surface.json +74 -23
- package/scripts/mutants/v2-visible-resume.json +215 -0
- package/scripts/new-session-id.ts +9 -4
- package/scripts/smoke-acp-bundled-mcp-live.ts +4 -3
- package/scripts/smoke-acp-carrier-augment-live.ts +2 -2
- package/scripts/smoke-acp-cortex-live.ts +8 -14
- package/scripts/smoke-acp-long-turn-live.ts +185 -0
- package/scripts/smoke-acp-mcp-live.ts +2 -2
- package/scripts/smoke-acp-memory-containment-live.ts +2 -2
- package/scripts/smoke-acp-ordering-probe-live.ts +2 -2
- package/scripts/smoke-acp-overlay-live.ts +2 -2
- package/scripts/smoke-acp-provider-live.ts +2 -2
- package/scripts/smoke-acp-raw-turn-live.ts +3 -3
- package/scripts/smoke-acp-session-reuse-live.ts +2 -2
- package/scripts/smoke-acp-skill-live.ts +2 -2
- package/scripts/smoke-acp-socket-citizen-live.ts +4 -3
- package/scripts/smoke-acp-v2-send-live.ts +4 -3
- package/scripts/smoke-agy-native-push-live.ts +10 -20
- package/scripts/smoke-claude-native-resume-live.sh +13 -3
- package/scripts/smoke-entwurf-chain-live.ts +352 -0
- package/scripts/smoke-entwurf-v2-matrix-live.ts +3 -3
- package/scripts/smoke-meta-honesty.sh +1 -1
- package/scripts/smoke-meta-install-state.sh +4 -0
- package/scripts/smoke-mux-fresh-call-live.ts +365 -0
- package/scripts/smoke-mux-lifecycle-live.ts +1136 -0
- package/scripts/smoke-pi-attach.ts +1 -1
- package/scripts/smoke-user-scope-citizen.sh +1 -1
- package/scripts/tsconfig.json +1 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-preflight.js +0 -160
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-spawn-production.js +0 -273
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-spawn.js +0 -216
- package/pi-extensions/lib/entwurf-v2-spawn-production.ts +0 -373
- package/pi-extensions/lib/entwurf-v2-spawn.ts +0 -323
- package/scripts/check-acp-sdk-surface.ts +0 -236
- package/scripts/check-entwurf-v2-spawn-production.ts +0 -551
- package/scripts/check-entwurf-v2-spawn.ts +0 -399
- package/scripts/smoke-entwurf-v2-spawn-live.ts +0 -188
- package/scripts/smoke-entwurf-v2-spawn-resume-live.ts +0 -469
|
@@ -1,399 +0,0 @@
|
|
|
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
|
-
wantsReply: false,
|
|
76
|
-
launchArgs: ["--approve"],
|
|
77
|
-
expectedSocketPath: SOCK,
|
|
78
|
-
observeTimeoutMs: 30_000,
|
|
79
|
-
releaseWhen: "socket-alive-or-child-exited",
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
interface Deferred<T> {
|
|
84
|
-
promise: Promise<T>;
|
|
85
|
-
resolve: (v: T) => void;
|
|
86
|
-
reject: (e: unknown) => void;
|
|
87
|
-
}
|
|
88
|
-
function deferred<T>(): Deferred<T> {
|
|
89
|
-
let resolve!: (v: T) => void;
|
|
90
|
-
let reject!: (e: unknown) => void;
|
|
91
|
-
const promise = new Promise<T>((res, rej) => {
|
|
92
|
-
resolve = res;
|
|
93
|
-
reject = rej;
|
|
94
|
-
});
|
|
95
|
-
return { promise, resolve, reject };
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
const flush = (): Promise<void> => new Promise((res) => setImmediate(res));
|
|
99
|
-
|
|
100
|
-
interface Calls {
|
|
101
|
-
spawn: number;
|
|
102
|
-
socket: number;
|
|
103
|
-
exit: number;
|
|
104
|
-
timeout: number;
|
|
105
|
-
kill: number;
|
|
106
|
-
release: number;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
interface Harness {
|
|
110
|
-
deps: SpawnBgResumeDeps;
|
|
111
|
-
calls: Calls;
|
|
112
|
-
socket: Deferred<void>;
|
|
113
|
-
exit: Deferred<number | null>;
|
|
114
|
-
observe: Deferred<void>; // 1st awaitTimeout call (observeTimeoutMs)
|
|
115
|
-
grace: Deferred<void>; // 2nd awaitTimeout call (killGraceMs)
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
interface HarnessOpts {
|
|
119
|
-
spawnThrows?: string;
|
|
120
|
-
killThrows?: boolean;
|
|
121
|
-
releaseThrows?: boolean;
|
|
122
|
-
socketSyncThrows?: string; // awaitSocketAlive throws SYNCHRONOUSLY (buggy dep)
|
|
123
|
-
exitSyncThrows?: string; // awaitChildExit throws SYNCHRONOUSLY (buggy dep)
|
|
124
|
-
child?: SpawnedChild;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
function makeHarness(opts: HarnessOpts = {}): Harness {
|
|
128
|
-
const calls: Calls = { spawn: 0, socket: 0, exit: 0, timeout: 0, kill: 0, release: 0 };
|
|
129
|
-
const socket = deferred<void>();
|
|
130
|
-
const exit = deferred<number | null>();
|
|
131
|
-
const observe = deferred<void>();
|
|
132
|
-
const grace = deferred<void>();
|
|
133
|
-
const child: SpawnedChild = opts.child ?? { pid: 9191 };
|
|
134
|
-
|
|
135
|
-
const deps: SpawnBgResumeDeps = {
|
|
136
|
-
spawnChild: async () => {
|
|
137
|
-
calls.spawn++;
|
|
138
|
-
if (opts.spawnThrows) throw new Error(opts.spawnThrows);
|
|
139
|
-
return child;
|
|
140
|
-
},
|
|
141
|
-
awaitSocketAlive: (_socketPath, _signal) => {
|
|
142
|
-
calls.socket++;
|
|
143
|
-
if (opts.socketSyncThrows) throw new Error(opts.socketSyncThrows);
|
|
144
|
-
return socket.promise;
|
|
145
|
-
},
|
|
146
|
-
awaitChildExit: (_c, _signal) => {
|
|
147
|
-
calls.exit++;
|
|
148
|
-
if (opts.exitSyncThrows) throw new Error(opts.exitSyncThrows);
|
|
149
|
-
return exit.promise;
|
|
150
|
-
},
|
|
151
|
-
awaitTimeout: (_ms, _signal) => {
|
|
152
|
-
calls.timeout++;
|
|
153
|
-
return calls.timeout === 1 ? observe.promise : grace.promise;
|
|
154
|
-
},
|
|
155
|
-
killChild: (_c) => {
|
|
156
|
-
calls.kill++;
|
|
157
|
-
if (opts.killThrows) throw new Error("kill failed");
|
|
158
|
-
},
|
|
159
|
-
releaseLock: (_lock) => {
|
|
160
|
-
calls.release++;
|
|
161
|
-
if (opts.releaseThrows) throw new Error("release failed");
|
|
162
|
-
},
|
|
163
|
-
killGraceMs: 5_000,
|
|
164
|
-
};
|
|
165
|
-
return { deps, calls, socket, exit, observe, grace };
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
async function main(): Promise<void> {
|
|
169
|
-
// ── 1. spawnChild throws → spawn-start-failed, release ×1, error surfaced ────────
|
|
170
|
-
{
|
|
171
|
-
const h = makeHarness({ spawnThrows: "ENOENT pi binary" });
|
|
172
|
-
const r = await executeSpawnBgResume(spawnBgPlan(), lockClaim(), h.deps);
|
|
173
|
-
ok("1 spawn-start-failed kind", r.kind === "spawn-start-failed");
|
|
174
|
-
ok("1 released true", r.released === true);
|
|
175
|
-
ok("1 error surfaced", r.kind === "spawn-start-failed" && r.error.includes("ENOENT pi binary"));
|
|
176
|
-
ok("1 release ×1", h.calls.release === 1);
|
|
177
|
-
ok("1 no socket/exit/timeout watch", h.calls.socket === 0 && h.calls.exit === 0 && h.calls.timeout === 0);
|
|
178
|
-
ok("1 no kill", h.calls.kill === 0);
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// ── 2. socket-alive wins → socket-alive, release ×1, no kill; exit loser no-op ───
|
|
182
|
-
{
|
|
183
|
-
const h = makeHarness();
|
|
184
|
-
const p = executeSpawnBgResume(spawnBgPlan(), lockClaim(), h.deps);
|
|
185
|
-
await flush();
|
|
186
|
-
h.socket.resolve();
|
|
187
|
-
await flush();
|
|
188
|
-
// the exit loser settles AFTER the win — must not double release.
|
|
189
|
-
h.exit.resolve(0);
|
|
190
|
-
const r = await p;
|
|
191
|
-
ok("2 socket-alive kind", r.kind === "socket-alive");
|
|
192
|
-
ok("2 released true", r.released === true);
|
|
193
|
-
ok("2 pid surfaced", r.kind === "socket-alive" && r.pid === 9191);
|
|
194
|
-
ok("2 release exactly once", h.calls.release === 1);
|
|
195
|
-
ok("2 no kill (no timeout)", h.calls.kill === 0);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
// ── 3. child-exited wins (code 0 / 1 / null) → child-exited, release ×1, no kill ──
|
|
199
|
-
for (const code of [0, 1, null] as const) {
|
|
200
|
-
const h = makeHarness();
|
|
201
|
-
const p = executeSpawnBgResume(spawnBgPlan(), lockClaim(), h.deps);
|
|
202
|
-
await flush();
|
|
203
|
-
h.exit.resolve(code);
|
|
204
|
-
await flush();
|
|
205
|
-
h.socket.resolve(); // loser
|
|
206
|
-
const r = await p;
|
|
207
|
-
ok(`3 child-exited kind (code=${code})`, r.kind === "child-exited");
|
|
208
|
-
ok(`3 exitCode=${code}`, r.kind === "child-exited" && r.exitCode === code);
|
|
209
|
-
ok(`3 release exactly once (code=${code})`, h.calls.release === 1);
|
|
210
|
-
ok(`3 no kill (code=${code})`, h.calls.kill === 0);
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// ── 4. timeout wins → release 0 at kill time, killChild ×1 (bare timeout ≠ release) ─
|
|
214
|
-
{
|
|
215
|
-
const h = makeHarness();
|
|
216
|
-
const p = executeSpawnBgResume(spawnBgPlan(), lockClaim(), h.deps);
|
|
217
|
-
await flush();
|
|
218
|
-
h.observe.resolve(); // observe timeout fires first
|
|
219
|
-
await flush();
|
|
220
|
-
// At this point the hand has killed and is awaiting the grace observation.
|
|
221
|
-
ok("4 release 0 after bare timeout", h.calls.release === 0);
|
|
222
|
-
ok("4 killChild ×1 on timeout", h.calls.kill === 1);
|
|
223
|
-
// drain: let the kill produce an exit so the promise settles.
|
|
224
|
-
h.exit.resolve(null);
|
|
225
|
-
const r = await p;
|
|
226
|
-
ok("4 settles as child-exited after kill", r.kind === "child-exited");
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
// ── 5. timeout → kill → child-exited(null) in grace → child-exited, release ×1 ───
|
|
230
|
-
{
|
|
231
|
-
const h = makeHarness();
|
|
232
|
-
const p = executeSpawnBgResume(spawnBgPlan(), lockClaim(), h.deps);
|
|
233
|
-
await flush();
|
|
234
|
-
h.observe.resolve();
|
|
235
|
-
await flush();
|
|
236
|
-
h.exit.resolve(null); // the kill's exit observed within grace
|
|
237
|
-
const r = await p;
|
|
238
|
-
ok("5 child-exited after kill", r.kind === "child-exited");
|
|
239
|
-
ok("5 exitCode null (signal)", r.kind === "child-exited" && r.exitCode === null);
|
|
240
|
-
ok("5 release exactly once", h.calls.release === 1);
|
|
241
|
-
ok("5 kill ×1", h.calls.kill === 1);
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
// ── 6. timeout → kill → socket-alive in grace → socket-alive, release ×1 ─────────
|
|
245
|
-
{
|
|
246
|
-
const h = makeHarness();
|
|
247
|
-
const p = executeSpawnBgResume(spawnBgPlan(), lockClaim(), h.deps);
|
|
248
|
-
await flush();
|
|
249
|
-
h.observe.resolve();
|
|
250
|
-
await flush();
|
|
251
|
-
h.socket.resolve(); // child raced its socket up just as we killed
|
|
252
|
-
const r = await p;
|
|
253
|
-
ok("6 socket-alive after kill", r.kind === "socket-alive");
|
|
254
|
-
ok("6 release exactly once", h.calls.release === 1);
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
// ── 7. timeout → kill → grace elapses, no observation → lock-retained, release 0 ──
|
|
258
|
-
{
|
|
259
|
-
const h = makeHarness();
|
|
260
|
-
const p = executeSpawnBgResume(spawnBgPlan(), lockClaim(), h.deps);
|
|
261
|
-
await flush();
|
|
262
|
-
h.observe.resolve();
|
|
263
|
-
await flush();
|
|
264
|
-
h.grace.resolve(); // grace elapses with no socket/exit
|
|
265
|
-
const r = await p;
|
|
266
|
-
ok("7 lock-retained kind", r.kind === "lock-retained");
|
|
267
|
-
ok("7 released false", r.released === false);
|
|
268
|
-
ok("7 reason kill-unconfirmed", r.kind === "lock-retained" && r.reason === "kill-unconfirmed");
|
|
269
|
-
ok("7 release NEVER (no blind release)", h.calls.release === 0);
|
|
270
|
-
const diag = r.kind === "lock-retained" ? r.diagnostic : null;
|
|
271
|
-
ok("7 diagnostic targetGardenId", diag?.targetGardenId === GID);
|
|
272
|
-
ok("7 diagnostic socketPath", diag?.expectedSocketPath === SOCK);
|
|
273
|
-
ok("7 diagnostic lockPath", diag?.lockPath === LOCKPATH);
|
|
274
|
-
ok("7 diagnostic pid", diag?.pid === 9191);
|
|
275
|
-
ok("7 diagnostic observeTimeoutMs", diag?.observeTimeoutMs === 30_000);
|
|
276
|
-
ok("7 diagnostic killGraceMs", diag?.killGraceMs === 5_000);
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
// ── 8. post-spawn dep throw (awaitChildExit rejects) → kill, no exit → retained ──
|
|
280
|
-
{
|
|
281
|
-
const h = makeHarness();
|
|
282
|
-
const p = executeSpawnBgResume(spawnBgPlan(), lockClaim(), h.deps);
|
|
283
|
-
await flush();
|
|
284
|
-
// the exit watcher itself fails — its rejection both triggers the backstop AND means
|
|
285
|
-
// the exit can never be observed → fail-closed to lock-retained.
|
|
286
|
-
h.exit.reject(new Error("exit watcher EBADF"));
|
|
287
|
-
const r = await p;
|
|
288
|
-
ok("8 lock-retained kind", r.kind === "lock-retained");
|
|
289
|
-
ok("8 reason observe-failed", r.kind === "lock-retained" && r.reason === "observe-failed");
|
|
290
|
-
ok("8 original error surfaced", r.kind === "lock-retained" && (r.error ?? "").includes("exit watcher EBADF"));
|
|
291
|
-
ok("8 best-effort kill called", h.calls.kill === 1);
|
|
292
|
-
ok("8 release NEVER", h.calls.release === 0);
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
// ── 9. post-spawn dep throw (awaitSocketAlive rejects) → kill → exit in grace ────
|
|
296
|
-
{
|
|
297
|
-
const h = makeHarness();
|
|
298
|
-
const p = executeSpawnBgResume(spawnBgPlan(), lockClaim(), h.deps);
|
|
299
|
-
await flush();
|
|
300
|
-
h.socket.reject(new Error("socket watcher EACCES")); // primary race rejects → backstop
|
|
301
|
-
await flush();
|
|
302
|
-
// the exit watcher is healthy → the kill's exit is observed within grace.
|
|
303
|
-
h.exit.resolve(137);
|
|
304
|
-
const r = await p;
|
|
305
|
-
ok("9 child-exited after backstop kill", r.kind === "child-exited");
|
|
306
|
-
ok("9 exitCode 137", r.kind === "child-exited" && r.exitCode === 137);
|
|
307
|
-
ok("9 release exactly once", h.calls.release === 1);
|
|
308
|
-
ok("9 kill ×1", h.calls.kill === 1);
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
// ── 10. releaseLock throw on a released path PROPAGATES (caller must not re-spawn) ─
|
|
312
|
-
{
|
|
313
|
-
const h = makeHarness({ releaseThrows: true });
|
|
314
|
-
const p = executeSpawnBgResume(spawnBgPlan(), lockClaim(), h.deps);
|
|
315
|
-
await flush();
|
|
316
|
-
h.socket.resolve();
|
|
317
|
-
let threw: unknown;
|
|
318
|
-
try {
|
|
319
|
-
await p;
|
|
320
|
-
} catch (e) {
|
|
321
|
-
threw = e;
|
|
322
|
-
}
|
|
323
|
-
ok("10 release error propagates", threw instanceof Error && threw.message === "release failed");
|
|
324
|
-
ok("10 release was attempted once", h.calls.release === 1);
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
// ── 11. lock invariants → decideReleasePolicy throws BEFORE any spawn ────────────
|
|
328
|
-
{
|
|
329
|
-
const h = makeHarness();
|
|
330
|
-
let threw = false;
|
|
331
|
-
try {
|
|
332
|
-
await executeSpawnBgResume(spawnBgPlan(), null, h.deps);
|
|
333
|
-
} catch {
|
|
334
|
-
threw = true;
|
|
335
|
-
}
|
|
336
|
-
ok("11 null lock throws", threw);
|
|
337
|
-
ok("11 null lock: no spawn attempted", h.calls.spawn === 0);
|
|
338
|
-
|
|
339
|
-
const h2 = makeHarness();
|
|
340
|
-
let threw2 = false;
|
|
341
|
-
try {
|
|
342
|
-
await executeSpawnBgResume(spawnBgPlan(), lockClaim(WRONG_GID), h2.deps);
|
|
343
|
-
} catch {
|
|
344
|
-
threw2 = true;
|
|
345
|
-
}
|
|
346
|
-
ok("11 mismatched-gid lock throws", threw2);
|
|
347
|
-
ok("11 mismatched-gid: no spawn attempted", h2.calls.spawn === 0);
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
// ── 12. kill grace where kill itself throws but exit still observed → released ───
|
|
351
|
-
// (proves killChild throwing is best-effort: the observation still drives release.)
|
|
352
|
-
{
|
|
353
|
-
const h = makeHarness({ killThrows: true });
|
|
354
|
-
const p = executeSpawnBgResume(spawnBgPlan(), lockClaim(), h.deps);
|
|
355
|
-
await flush();
|
|
356
|
-
h.observe.resolve();
|
|
357
|
-
await flush();
|
|
358
|
-
h.exit.resolve(0); // exit observed despite the kill throw
|
|
359
|
-
const r = await p;
|
|
360
|
-
ok("12 child-exited despite kill throw", r.kind === "child-exited");
|
|
361
|
-
ok("12 release exactly once", h.calls.release === 1);
|
|
362
|
-
ok("12 kill attempted once", h.calls.kill === 1);
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
// ── 13. S1 (Fable 2차): awaitSocketAlive throws SYNCHRONOUSLY at creation (a buggy dep
|
|
366
|
-
// that throws where it must return a Promise). The child exists → no blind release;
|
|
367
|
-
// best-effort kill + lock-retained fail-closed. The async race (cases 8/9) never
|
|
368
|
-
// runs, so this would be a silent leak without the dedicated creation try. ────────
|
|
369
|
-
{
|
|
370
|
-
const h = makeHarness({ socketSyncThrows: "awaitSocketAlive EFAULT (sync)" });
|
|
371
|
-
const r = await executeSpawnBgResume(spawnBgPlan(), lockClaim(), h.deps);
|
|
372
|
-
ok("13 lock-retained on sync socket throw", r.kind === "lock-retained");
|
|
373
|
-
ok("13 reason observe-failed", r.kind === "lock-retained" && r.reason === "observe-failed");
|
|
374
|
-
ok("13 sync error surfaced", r.kind === "lock-retained" && (r.error ?? "").includes("awaitSocketAlive EFAULT"));
|
|
375
|
-
ok("13 best-effort kill ×1", h.calls.kill === 1);
|
|
376
|
-
ok("13 release NEVER (no blind release)", h.calls.release === 0);
|
|
377
|
-
ok("13 diagnostic lockPath surfaced", r.kind === "lock-retained" && r.diagnostic.lockPath === LOCKPATH);
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
// ── 14. S1 sibling: awaitChildExit throws SYNCHRONOUSLY at creation (socket watcher DID
|
|
381
|
-
// start, then exit creation throws). Same fail-closed retained; the started socket
|
|
382
|
-
// watcher is defused so it cannot surface as an unhandled rejection. ──────────────
|
|
383
|
-
{
|
|
384
|
-
const h = makeHarness({ exitSyncThrows: "awaitChildExit EFAULT (sync)" });
|
|
385
|
-
const r = await executeSpawnBgResume(spawnBgPlan(), lockClaim(), h.deps);
|
|
386
|
-
ok("14 lock-retained on sync exit throw", r.kind === "lock-retained");
|
|
387
|
-
ok("14 reason observe-failed", r.kind === "lock-retained" && r.reason === "observe-failed");
|
|
388
|
-
ok("14 best-effort kill ×1", h.calls.kill === 1);
|
|
389
|
-
ok("14 release NEVER", h.calls.release === 0);
|
|
390
|
-
ok("14 socket watcher had started", h.calls.socket === 1);
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
console.log(`\ncheck-entwurf-v2-spawn: ${passed} checks passed`);
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
main().catch((err) => {
|
|
397
|
-
console.error(err);
|
|
398
|
-
process.exit(1);
|
|
399
|
-
});
|
|
@@ -1,188 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* smoke-entwurf-v2-spawn-live — the 5c-3c LIVE phase gate (D5). Unlike the deterministic
|
|
3
|
-
* gate (check-entwurf-v2-spawn-production, fakes only), this exercises the production
|
|
4
|
-
* SpawnBgResumeDeps against REAL OS objects — a real unix socket, real child processes, real
|
|
5
|
-
* timers, real abort teardown — to catch what fakes cannot: actual spawn/exit/error event
|
|
6
|
-
* semantics, real lstat+connect liveness, real timer/abort cleanup, and the 5c-3a watcher's
|
|
7
|
-
* timeout→kill→child-exited→release integration on a live process.
|
|
8
|
-
*
|
|
9
|
-
* It does NOT spawn a real `pi --entwurf-control` resume (that needs a saved session, model
|
|
10
|
-
* auth, and backend state — flaky); proving the argv actually launches pi end-to-end is the
|
|
11
|
-
* 5d surface matrix's job. `buildResumePiArgs` is already pinned by the deterministic gate.
|
|
12
|
-
*
|
|
13
|
-
* Kept OUT of `pnpm check` (it spawns processes / opens sockets); run it manually before 5d:
|
|
14
|
-
* LIVE=1 ./run.sh smoke-entwurf-v2-spawn-live
|
|
15
|
-
* and record the result in NEXT.md / the commit body (chain-outside, phase-gate-inside).
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import { type ChildProcess, spawn } from "node:child_process";
|
|
19
|
-
import * as fsp from "node:fs/promises";
|
|
20
|
-
import * as net from "node:net";
|
|
21
|
-
import * as os from "node:os";
|
|
22
|
-
import * as path from "node:path";
|
|
23
|
-
import type { LockClaim } from "../pi-extensions/lib/entwurf-v2-lock.ts";
|
|
24
|
-
import { executeSpawnBgResume, type SpawnBgPlan } from "../pi-extensions/lib/entwurf-v2-spawn.ts";
|
|
25
|
-
import { makeProductionSpawnBgResumeDeps } from "../pi-extensions/lib/entwurf-v2-spawn-production.ts";
|
|
26
|
-
|
|
27
|
-
let passed = 0;
|
|
28
|
-
function ok(label: string, cond: boolean): void {
|
|
29
|
-
if (!cond) throw new Error(`SMOKE FAIL: ${label}`);
|
|
30
|
-
console.log(` ok ${label}`);
|
|
31
|
-
passed++;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// A resident child that survives until SIGTERM, then exits 0 — a stand-in for a pi child
|
|
35
|
-
// (we are proving the OS-level lifecycle wiring, not pi's resume behavior).
|
|
36
|
-
const RESIDENT_CHILD = "process.on('SIGTERM',()=>process.exit(0)); setInterval(()=>{},1e9);";
|
|
37
|
-
|
|
38
|
-
function lockClaim(gardenId: string): LockClaim {
|
|
39
|
-
return {
|
|
40
|
-
gardenId,
|
|
41
|
-
pid: process.pid,
|
|
42
|
-
hostname: os.hostname(),
|
|
43
|
-
createdAt: new Date(0).toISOString(),
|
|
44
|
-
nonce: "live5c3csmoke00",
|
|
45
|
-
owner: "entwurf_v2",
|
|
46
|
-
lockPath: `/tmp/${gardenId}.lock`,
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function spawnBgPlan(over: Partial<SpawnBgPlan> = {}): SpawnBgPlan {
|
|
51
|
-
const gid = "20260613T000000-aaaaaa";
|
|
52
|
-
return {
|
|
53
|
-
transport: "spawn-bg",
|
|
54
|
-
action: "resume",
|
|
55
|
-
targetGardenId: gid,
|
|
56
|
-
sessionId: gid,
|
|
57
|
-
cwd: process.cwd(),
|
|
58
|
-
prompt: "continue",
|
|
59
|
-
wantsReply: false,
|
|
60
|
-
launchArgs: [],
|
|
61
|
-
expectedSocketPath: "/nonexistent/never.sock",
|
|
62
|
-
observeTimeoutMs: 30_000,
|
|
63
|
-
releaseWhen: "socket-alive-or-child-exited",
|
|
64
|
-
...over,
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
async function main(): Promise<void> {
|
|
69
|
-
const tmp = await fsp.mkdtemp(path.join(os.tmpdir(), "v2spawn-"));
|
|
70
|
-
const children: ChildProcess[] = [];
|
|
71
|
-
let server: net.Server | null = null;
|
|
72
|
-
|
|
73
|
-
try {
|
|
74
|
-
// ── S1: real unix-socket connectability (real fs.lstat + real probeSocketLiveness) ─
|
|
75
|
-
{
|
|
76
|
-
const sockPath = path.join(tmp, "live.sock");
|
|
77
|
-
server = net.createServer();
|
|
78
|
-
await new Promise<void>((resolve) => server?.listen(sockPath, resolve));
|
|
79
|
-
const deps = makeProductionSpawnBgResumeDeps({ pollIntervalMs: 20 });
|
|
80
|
-
await deps.awaitSocketAlive(sockPath, new AbortController().signal);
|
|
81
|
-
ok("S1 a real listening unix socket → awaitSocketAlive resolves (connectable)", true);
|
|
82
|
-
|
|
83
|
-
// symlink at the path → forged (real lstat catches it; never connected — P1).
|
|
84
|
-
const symPath = path.join(tmp, "sym.sock");
|
|
85
|
-
await fsp.symlink(sockPath, symPath);
|
|
86
|
-
let forged = false;
|
|
87
|
-
try {
|
|
88
|
-
await deps.awaitSocketAlive(symPath, new AbortController().signal);
|
|
89
|
-
} catch (e) {
|
|
90
|
-
forged = e instanceof Error && e.message.includes("forged");
|
|
91
|
-
}
|
|
92
|
-
ok("S1 a symlink at the socket path → forged reject (real lstat P1)", forged);
|
|
93
|
-
|
|
94
|
-
// absent path → dead → wait; abort settles it (real timer + abort cleanup).
|
|
95
|
-
const ctrl = new AbortController();
|
|
96
|
-
const absentP = deps.awaitSocketAlive(path.join(tmp, "absent.sock"), ctrl.signal);
|
|
97
|
-
setTimeout(() => ctrl.abort(), 60);
|
|
98
|
-
let aborted = false;
|
|
99
|
-
try {
|
|
100
|
-
await absentP;
|
|
101
|
-
} catch {
|
|
102
|
-
aborted = true;
|
|
103
|
-
}
|
|
104
|
-
ok("S1 an absent socket → waits, then abort settles it (no hang)", aborted);
|
|
105
|
-
|
|
106
|
-
await new Promise<void>((resolve) => server?.close(() => resolve()));
|
|
107
|
-
server = null;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// ── S2: real child lifecycle — spawn event, exit capture, SIGTERM kill ───────────
|
|
111
|
-
{
|
|
112
|
-
const deps = makeProductionSpawnBgResumeDeps({
|
|
113
|
-
resolveIdentity: () => ({
|
|
114
|
-
sessionFile: path.join(tmp, "smoke-session.jsonl"),
|
|
115
|
-
cwd: tmp,
|
|
116
|
-
explicitExtensionArgs: [],
|
|
117
|
-
provider: null,
|
|
118
|
-
model: "smoke",
|
|
119
|
-
}),
|
|
120
|
-
spawnChild: () => {
|
|
121
|
-
const proc = spawn(process.execPath, ["-e", RESIDENT_CHILD], { stdio: "ignore" });
|
|
122
|
-
children.push(proc);
|
|
123
|
-
return proc;
|
|
124
|
-
},
|
|
125
|
-
});
|
|
126
|
-
// spawnChild awaits the REAL 'spawn' event before resolving (B1).
|
|
127
|
-
const child = await deps.spawnChild(spawnBgPlan());
|
|
128
|
-
ok("S2 spawnChild resolves on the real 'spawn' event (B1)", typeof child.pid === "number" && child.pid > 0);
|
|
129
|
-
|
|
130
|
-
// SIGTERM → the resident child terminates; awaitChildExit OBSERVES the exit via the
|
|
131
|
-
// eager exitPromise (B2). The code is 0 if the child's SIGTERM handler ran, or null if
|
|
132
|
-
// the signal landed before the handler was installed — both are a real, observed exit
|
|
133
|
-
// (the watcher releases on child-exited for ANY code), which is what this proves.
|
|
134
|
-
deps.killChild(child);
|
|
135
|
-
const code = await deps.awaitChildExit(child, new AbortController().signal);
|
|
136
|
-
ok("S2 killChild(SIGTERM) → awaitChildExit observes the exit (code 0 or null)", code === 0 || code === null);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// ── S3: watcher integration — real timeout → kill → child-exited → release ───────
|
|
140
|
-
{
|
|
141
|
-
let releases = 0;
|
|
142
|
-
const deps = makeProductionSpawnBgResumeDeps({
|
|
143
|
-
resolveIdentity: () => ({
|
|
144
|
-
sessionFile: path.join(tmp, "smoke-session.jsonl"),
|
|
145
|
-
cwd: tmp,
|
|
146
|
-
explicitExtensionArgs: [],
|
|
147
|
-
provider: null,
|
|
148
|
-
model: "smoke",
|
|
149
|
-
}),
|
|
150
|
-
spawnChild: () => {
|
|
151
|
-
const proc = spawn(process.execPath, ["-e", RESIDENT_CHILD], { stdio: "ignore" });
|
|
152
|
-
children.push(proc);
|
|
153
|
-
return proc;
|
|
154
|
-
},
|
|
155
|
-
releaseFn: () => {
|
|
156
|
-
releases++;
|
|
157
|
-
},
|
|
158
|
-
pollIntervalMs: 20,
|
|
159
|
-
killGraceMs: 2_000,
|
|
160
|
-
});
|
|
161
|
-
// A child that never stands a socket up + a short observe window → the watcher must
|
|
162
|
-
// time out, NOT release on the timeout, kill, observe the real exit, then release once.
|
|
163
|
-
const plan = spawnBgPlan({ observeTimeoutMs: 80, expectedSocketPath: path.join(tmp, "never-appears.sock") });
|
|
164
|
-
const result = await executeSpawnBgResume(plan, lockClaim(plan.sessionId), deps);
|
|
165
|
-
ok("S3 timeout→kill→child-exited (real timer + real child + 5c-3a watcher)", result.kind === "child-exited");
|
|
166
|
-
ok("S3 lock released exactly once on the observed exit", result.released === true && releases === 1);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
console.log(`\nsmoke-entwurf-v2-spawn-live: ${passed} checks passed (real OS primitives)`);
|
|
170
|
-
} finally {
|
|
171
|
-
for (const c of children) {
|
|
172
|
-
try {
|
|
173
|
-
c.kill("SIGKILL");
|
|
174
|
-
} catch {
|
|
175
|
-
// best-effort
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
if (server) {
|
|
179
|
-
await new Promise<void>((resolve) => server?.close(() => resolve()));
|
|
180
|
-
}
|
|
181
|
-
await fsp.rm(tmp, { recursive: true, force: true });
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
main().catch((err) => {
|
|
186
|
-
console.error(err);
|
|
187
|
-
process.exit(1);
|
|
188
|
-
});
|