@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
|
+
* smoke-entwurf-v2-matrix-live — the 5d-5 D4-b LIVE sentinel for the release-gate matrix.
|
|
3
|
+
*
|
|
4
|
+
* The deterministic sibling (check-entwurf-v2-matrix) drives the REAL decider over fakes with
|
|
5
|
+
* ZERO IO — it fixes every (target kind → transport → lock) cell as a table. This LIVE sentinel
|
|
6
|
+
* drives the REAL production `runEntwurfV2` deps against REAL OS objects on the substrate's
|
|
7
|
+
* happy path, to catch what fakes cannot: a real `pi --entwurf-control` control socket + RPC,
|
|
8
|
+
* a real lock acquire→release on disk, and a real mailbox enqueue. Negative/timeout/contention
|
|
9
|
+
* paths stay in the deterministic gate (GPT Q2/Q4) — this is "substrate still works", not a
|
|
10
|
+
* behavior suite.
|
|
11
|
+
*
|
|
12
|
+
* FOUR cells (GPT verdict — model-in-loop is OUT, this is a transport/lock/enqueue gate):
|
|
13
|
+
* C1 control-socket — a real `pi --entwurf-control` resident (a pi citizen with a live
|
|
14
|
+
* socket) → decider routes fire-and-forget/live to control-socket → real RPC send →
|
|
15
|
+
* lock acquire→release ×1 → no lock garbage.
|
|
16
|
+
* C1b socket-only (A1 narrow) — a real `pi --entwurf-control` resident with NO meta-record
|
|
17
|
+
* (the operator-greeted record-less case) → resolveTarget promotes its live control socket
|
|
18
|
+
* to a socket-only pi endpoint → fire-and-forget routes to control-socket (outcome=sent),
|
|
19
|
+
* while owned-outcome on the SAME record-less target is refused (bad-target, no spawn).
|
|
20
|
+
* This is the LIVE proof that closes the gap C1's hand-minted record was hiding.
|
|
21
|
+
* C2 meta-mailbox (deliverable) — a self-fetch citizen (claude-code) with an ARMED receiver
|
|
22
|
+
* marker → decider routes to meta-mailbox → a real `.msg` + signal is enqueued → lock-free
|
|
23
|
+
* path (no lock file) → no garbage beyond the one message.
|
|
24
|
+
* C3 meta-mailbox guard (undeliverable) — the SAME citizen kind but with NO armed receiver
|
|
25
|
+
* (a terminated self-fetch session) → deliverability guard rejects (mailbox-undeliverable)
|
|
26
|
+
* → NO `.msg` written (SE-2: no mailbox garbage for a dead receiver).
|
|
27
|
+
*
|
|
28
|
+
* Why programmatic, not model-in-loop (GPT Q2): "does the sender model actually call
|
|
29
|
+
* the dispatch verb (entwurf_v2)" is a SEPARATE behavior concern. Folding it in here
|
|
30
|
+
* would make a flaky model-tool-arg-variance failure indistinguishable from a bridge regression.
|
|
31
|
+
* So C2/C3 drive the production enqueue path directly; only C1 needs a real pi process (to prove
|
|
32
|
+
* the control socket + RPC are real, which fakes cannot).
|
|
33
|
+
*
|
|
34
|
+
* LIVE-only (spawns a real pi, opens a real socket) — kept OUT of `pnpm check`; honest skip when
|
|
35
|
+
* LIVE!=1 (a release-gate that hard-fails without auth/model is unrunnable unattended). Model:
|
|
36
|
+
* ENTWURF_LIVE_TARGET = "<provider>/<model>" (default "openai-codex/gpt-5.4")
|
|
37
|
+
* (or split: ENTWURF_LIVE_PROVIDER + ENTWURF_LIVE_MODEL)
|
|
38
|
+
* LIVE=1 ./run.sh smoke-entwurf-v2-matrix-live
|
|
39
|
+
*
|
|
40
|
+
* Automation seams (GPT Q1): the resident child + its socket are always reaped in `finally`; a
|
|
41
|
+
* clean pass tears down the temp world while a failure PRESERVES it for post-mortem; fresh garden
|
|
42
|
+
* ids + temp dirs (no fixed session id → repeatable); staged timeouts (boot/observe); on failure
|
|
43
|
+
* a diagnostic block dumps the target id, socket/lock/mailbox paths, and the pi stderr tail.
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
import { type ChildProcess, spawn } from "node:child_process";
|
|
47
|
+
import { existsSync } from "node:fs";
|
|
48
|
+
import * as fsp from "node:fs/promises";
|
|
49
|
+
import * as os from "node:os";
|
|
50
|
+
import * as path from "node:path";
|
|
51
|
+
import { fileURLToPath } from "node:url";
|
|
52
|
+
import type { SenderEnvelope } from "../pi-extensions/lib/entwurf-control-rpc.ts";
|
|
53
|
+
import { generateSessionId } from "../pi-extensions/lib/entwurf-core.ts";
|
|
54
|
+
import { lockPathFor } from "../pi-extensions/lib/entwurf-v2-lock.ts";
|
|
55
|
+
import { makeProductionEntwurfV2Deps } from "../pi-extensions/lib/entwurf-v2-production.ts";
|
|
56
|
+
import type { EntwurfV2RunResult } from "../pi-extensions/lib/entwurf-v2-runner.ts";
|
|
57
|
+
import { runEntwurfV2 } from "../pi-extensions/lib/entwurf-v2-runner.ts";
|
|
58
|
+
import {
|
|
59
|
+
metaRecordExistsByGardenId,
|
|
60
|
+
upsertMetaSession,
|
|
61
|
+
writeMetaReceiverMarker,
|
|
62
|
+
} from "../pi-extensions/lib/meta-session.ts";
|
|
63
|
+
import { terminateChild } from "./lib/acp-child-cleanup.ts";
|
|
64
|
+
|
|
65
|
+
// pi's control socket lives at the canonical dir keyed by session id — pi owns this path, so
|
|
66
|
+
// C1 must point the decider's controlSocketDir at the REAL dir (a fresh gid avoids collision).
|
|
67
|
+
const REAL_CONTROL_DIR = path.join(os.homedir(), ".pi", "entwurf-control");
|
|
68
|
+
const SOCKET_SUFFIX = ".sock";
|
|
69
|
+
// Release-gate topology: repo-under-test, not deployment smoke. Load only this
|
|
70
|
+
// checkout's extension so resident behavior is independent of global pi packages.
|
|
71
|
+
const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
72
|
+
const REPO_EXTENSION_ARGS = ["--no-extensions", "-e", REPO_ROOT] as const;
|
|
73
|
+
|
|
74
|
+
// Staged timeouts (automation): short and per-stage so a stall is attributable.
|
|
75
|
+
const BOOT_TIMEOUT_MS = 30_000; // pi --entwurf-control socket appears
|
|
76
|
+
const POLL_MS = 100;
|
|
77
|
+
|
|
78
|
+
let passed = 0;
|
|
79
|
+
const artifacts: Record<string, string> = {};
|
|
80
|
+
|
|
81
|
+
function ok(label: string, cond: boolean): void {
|
|
82
|
+
if (!cond) throw new Error(`SMOKE FAIL: ${label}`);
|
|
83
|
+
console.log(` ok ${label}`);
|
|
84
|
+
passed++;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function resolveTarget(): { provider: string; model: string } {
|
|
88
|
+
const combined = process.env.ENTWURF_LIVE_TARGET?.trim();
|
|
89
|
+
if (combined) {
|
|
90
|
+
const slash = combined.indexOf("/");
|
|
91
|
+
if (slash <= 0 || slash === combined.length - 1) {
|
|
92
|
+
throw new Error(`ENTWURF_LIVE_TARGET must be "<provider>/<model>", got: ${JSON.stringify(combined)}`);
|
|
93
|
+
}
|
|
94
|
+
return { provider: combined.slice(0, slash), model: combined.slice(slash + 1) };
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
provider: process.env.ENTWURF_LIVE_PROVIDER?.trim() || "openai-codex",
|
|
98
|
+
model: process.env.ENTWURF_LIVE_MODEL?.trim() || "gpt-5.4",
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function sleep(ms: number): Promise<void> {
|
|
103
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function waitForSocket(sockPath: string, timeoutMs: number): Promise<boolean> {
|
|
107
|
+
const deadline = Date.now() + timeoutMs;
|
|
108
|
+
while (Date.now() < deadline) {
|
|
109
|
+
if (existsSync(sockPath)) return true;
|
|
110
|
+
await sleep(POLL_MS);
|
|
111
|
+
}
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function smokeSender(gardenId: string, cwd: string): SenderEnvelope {
|
|
116
|
+
return {
|
|
117
|
+
sessionId: gardenId,
|
|
118
|
+
agentId: "smoke/matrix-live",
|
|
119
|
+
cwd,
|
|
120
|
+
timestamp: new Date(0).toISOString(),
|
|
121
|
+
origin: "pi-session",
|
|
122
|
+
replyable: false,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function main(): Promise<void> {
|
|
127
|
+
if (process.env.LIVE !== "1") {
|
|
128
|
+
console.log("[smoke-entwurf-v2-matrix-live] skipped — set LIVE=1 to run (spawns a real pi + opens a real socket).");
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const { provider, model } = resolveTarget();
|
|
133
|
+
console.log(`[smoke-entwurf-v2-matrix-live] target = ${provider}/${model}`);
|
|
134
|
+
|
|
135
|
+
// ── temp world (repeatable: fresh dirs, no fixed ids) ────────────────────────
|
|
136
|
+
const tmp = await fsp.mkdtemp(path.join(os.tmpdir(), "v2matrix-"));
|
|
137
|
+
const sessionsDir = path.join(tmp, "sessions");
|
|
138
|
+
const mailboxDir = path.join(tmp, "mailbox");
|
|
139
|
+
const lockDir = path.join(tmp, "locks");
|
|
140
|
+
const receiversDir = path.join(tmp, "receivers");
|
|
141
|
+
for (const d of [sessionsDir, mailboxDir, lockDir, receiversDir]) await fsp.mkdir(d, { recursive: true });
|
|
142
|
+
// The receiver marker is read through defaultMetaReceiversDir() (env-overridable); point it
|
|
143
|
+
// (and the store/mailbox defaults, belt-and-suspenders to the opts dirs) at the temp world.
|
|
144
|
+
process.env.ENTWURF_META_RECEIVERS_DIR = receiversDir;
|
|
145
|
+
process.env.ENTWURF_META_SESSIONS_DIR = sessionsDir;
|
|
146
|
+
process.env.ENTWURF_META_MAILBOX_DIR = mailboxDir;
|
|
147
|
+
|
|
148
|
+
let resident: ChildProcess | null = null;
|
|
149
|
+
let residentGid = "";
|
|
150
|
+
let c1bGid = "";
|
|
151
|
+
let stderrTail = "";
|
|
152
|
+
let succeeded = false;
|
|
153
|
+
|
|
154
|
+
const prodDeps = (sender: SenderEnvelope) =>
|
|
155
|
+
makeProductionEntwurfV2Deps({
|
|
156
|
+
senderProvider: () => sender,
|
|
157
|
+
sessionsDir,
|
|
158
|
+
mailboxDir,
|
|
159
|
+
lockDir,
|
|
160
|
+
controlSocketDir: REAL_CONTROL_DIR,
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
// ── C1: control-socket — real `pi --entwurf-control` resident ──────────────
|
|
165
|
+
{
|
|
166
|
+
// DECIDER IDENTITY FIXTURE (NOT a writer E2E — R1): a real pi session does NOT
|
|
167
|
+
// self-write a meta-record (SE-1), but v2 production resolveTarget reads the target
|
|
168
|
+
// via metaRecordExists/readIdentity, so to exercise the control-socket TRANSPORT we
|
|
169
|
+
// hand-mint a backend=pi citizen and launch pi under the minted gid (record.gardenId ==
|
|
170
|
+
// the live socket's session id). nativeSessionId here is synthetic; the future pi
|
|
171
|
+
// writer's real shape is nativeSessionId==sessionId — irrelevant to this transport probe.
|
|
172
|
+
const minted = upsertMetaSession({
|
|
173
|
+
input: { backend: "pi", nativeSessionId: `smoke-c1-${process.pid}`, cwd: tmp, model },
|
|
174
|
+
dir: sessionsDir,
|
|
175
|
+
});
|
|
176
|
+
residentGid = minted.record.gardenId;
|
|
177
|
+
const sockPath = path.join(REAL_CONTROL_DIR, `${residentGid}${SOCKET_SUFFIX}`);
|
|
178
|
+
artifacts["C1.gid"] = residentGid;
|
|
179
|
+
artifacts["C1.socket"] = sockPath;
|
|
180
|
+
// B3: the fresh gid must not collide with a pre-existing socket — else cleanup would
|
|
181
|
+
// delete someone else's live socket. Fail loud rather than risk it.
|
|
182
|
+
ok("C1 fresh gid has no pre-existing control socket", !existsSync(sockPath));
|
|
183
|
+
|
|
184
|
+
// B1: drive the resident in `--mode rpc` with stdin held open (keepalive). A non-TTY
|
|
185
|
+
// interactive `pi` (stdin ignored) can abort or silently exit; rpc mode is the control
|
|
186
|
+
// substrate this cell actually probes (model UI is irrelevant here).
|
|
187
|
+
resident = spawn(
|
|
188
|
+
"pi",
|
|
189
|
+
[
|
|
190
|
+
...REPO_EXTENSION_ARGS,
|
|
191
|
+
"--session-id",
|
|
192
|
+
residentGid,
|
|
193
|
+
"--entwurf-control",
|
|
194
|
+
"--provider",
|
|
195
|
+
provider,
|
|
196
|
+
"--model",
|
|
197
|
+
model,
|
|
198
|
+
"--mode",
|
|
199
|
+
"rpc",
|
|
200
|
+
],
|
|
201
|
+
{ cwd: tmp, stdio: ["pipe", "ignore", "pipe"], detached: false },
|
|
202
|
+
);
|
|
203
|
+
resident.stderr?.on("data", (b: Buffer) => {
|
|
204
|
+
stderrTail = (stderrTail + b.toString()).slice(-2000);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
const up = await waitForSocket(sockPath, BOOT_TIMEOUT_MS);
|
|
208
|
+
ok("C1 real `pi --entwurf-control` stood up a control socket", up);
|
|
209
|
+
|
|
210
|
+
const result: EntwurfV2RunResult = await runEntwurfV2(
|
|
211
|
+
{ target: residentGid, intent: "fire-and-forget", message: "matrix-live C1 control-socket probe" },
|
|
212
|
+
prodDeps(smokeSender(residentGid, tmp)),
|
|
213
|
+
);
|
|
214
|
+
// R2: not just "executed/control-socket" — the final outcome must be a real `sent`
|
|
215
|
+
// (an in-band reject or dead-fallback would also surface as control-socket otherwise).
|
|
216
|
+
ok(
|
|
217
|
+
"C1 alive pi citizen → control-socket RPC, outcome=sent",
|
|
218
|
+
result.kind === "executed" &&
|
|
219
|
+
result.transport === "control-socket" &&
|
|
220
|
+
result.outcome.transport === "control-socket" &&
|
|
221
|
+
result.outcome.outcome === "sent",
|
|
222
|
+
);
|
|
223
|
+
// in-domain control-socket send keeps then releases the lock → no lock file remains.
|
|
224
|
+
ok("C1 lock released (no lock file left for the target)", !existsSync(lockPathFor(residentGid, lockDir)));
|
|
225
|
+
|
|
226
|
+
if (resident) {
|
|
227
|
+
await terminateChild(resident);
|
|
228
|
+
resident = null;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ── C1b: A1 narrow — RECORD-LESS live pi control socket → socket-only target ─
|
|
233
|
+
// The operator-greeted case the deterministic gates model: a real `pi --entwurf-control`
|
|
234
|
+
// resident with NO meta-record. resolveTarget finds no record, sees a live non-symlink
|
|
235
|
+
// control socket (presence hint), promotes it to a socket-only pi endpoint, and routes
|
|
236
|
+
// fire-and-forget to control-socket — the end-to-end proof that closes the gap C1's
|
|
237
|
+
// hand-minted record was hiding. owned-outcome on the SAME record-less target is refused
|
|
238
|
+
// (bad-target, no spawn). Unlike C1, NO upsertMetaSession is called for this gid.
|
|
239
|
+
{
|
|
240
|
+
c1bGid = generateSessionId();
|
|
241
|
+
const sockPath = path.join(REAL_CONTROL_DIR, `${c1bGid}${SOCKET_SUFFIX}`);
|
|
242
|
+
artifacts["C1b.gid"] = c1bGid;
|
|
243
|
+
artifacts["C1b.socket"] = sockPath;
|
|
244
|
+
ok("C1b fresh gid has no pre-existing control socket", !existsSync(sockPath));
|
|
245
|
+
// The gap-closing precondition: this gid is genuinely RECORD-LESS (C1 minted one).
|
|
246
|
+
ok(
|
|
247
|
+
"C1b target has NO meta-record (record-less, the operator-greeted case)",
|
|
248
|
+
!metaRecordExistsByGardenId(c1bGid, sessionsDir),
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
resident = spawn(
|
|
252
|
+
"pi",
|
|
253
|
+
[
|
|
254
|
+
...REPO_EXTENSION_ARGS,
|
|
255
|
+
"--session-id",
|
|
256
|
+
c1bGid,
|
|
257
|
+
"--entwurf-control",
|
|
258
|
+
"--provider",
|
|
259
|
+
provider,
|
|
260
|
+
"--model",
|
|
261
|
+
model,
|
|
262
|
+
"--mode",
|
|
263
|
+
"rpc",
|
|
264
|
+
],
|
|
265
|
+
{ cwd: tmp, stdio: ["pipe", "ignore", "pipe"], detached: false },
|
|
266
|
+
);
|
|
267
|
+
resident.stderr?.on("data", (b: Buffer) => {
|
|
268
|
+
stderrTail = (stderrTail + b.toString()).slice(-2000);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
const up = await waitForSocket(sockPath, BOOT_TIMEOUT_MS);
|
|
272
|
+
ok("C1b real record-less `pi --entwurf-control` stood up a control socket", up);
|
|
273
|
+
|
|
274
|
+
const result: EntwurfV2RunResult = await runEntwurfV2(
|
|
275
|
+
{ target: c1bGid, intent: "fire-and-forget", message: "matrix-live C1b record-less socket-only probe" },
|
|
276
|
+
prodDeps(smokeSender(c1bGid, tmp)),
|
|
277
|
+
);
|
|
278
|
+
ok(
|
|
279
|
+
"C1b record-less live pi → socket-only control-socket RPC, outcome=sent",
|
|
280
|
+
result.kind === "executed" &&
|
|
281
|
+
result.transport === "control-socket" &&
|
|
282
|
+
result.outcome.transport === "control-socket" &&
|
|
283
|
+
result.outcome.outcome === "sent",
|
|
284
|
+
);
|
|
285
|
+
ok("C1b lock released (no lock file left for the socket-only target)", !existsSync(lockPathFor(c1bGid, lockDir)));
|
|
286
|
+
|
|
287
|
+
// owned-outcome on the record-less but LIVE target → owned-live-no-autosend (the honest
|
|
288
|
+
// table verdict), NOT the `bad-target` lie: the live resident is a real addressable
|
|
289
|
+
// citizen. spawn-bg still never opens (allowResume:false guard; no record = no resume
|
|
290
|
+
// authority). The no-spawn invariant is also pinned deterministically in the gate.
|
|
291
|
+
const owned: EntwurfV2RunResult = await runEntwurfV2(
|
|
292
|
+
{ target: c1bGid, intent: "owned-outcome", message: "matrix-live C1b owned must be refused" },
|
|
293
|
+
prodDeps(smokeSender(c1bGid, tmp)),
|
|
294
|
+
);
|
|
295
|
+
ok(
|
|
296
|
+
"C1b record-less(live) + owned-outcome → rejected owned-live-no-autosend (NOT bad-target, no spawn)",
|
|
297
|
+
owned.kind === "rejected" && owned.receipt.reason === "owned-live-no-autosend",
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
if (resident) {
|
|
301
|
+
await terminateChild(resident);
|
|
302
|
+
resident = null;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// ── C2: meta-mailbox — deliverable self-fetch citizen (ARMED receiver) ──────
|
|
307
|
+
{
|
|
308
|
+
const minted = upsertMetaSession({
|
|
309
|
+
input: { backend: "claude-code", nativeSessionId: `smoke-c2-${process.pid}`, cwd: tmp },
|
|
310
|
+
dir: sessionsDir,
|
|
311
|
+
});
|
|
312
|
+
const gid = minted.record.gardenId;
|
|
313
|
+
artifacts["C2.gid"] = gid;
|
|
314
|
+
// armed = a presence marker owned by THIS (live) process → deliverable.
|
|
315
|
+
writeMetaReceiverMarker({
|
|
316
|
+
gardenId: gid,
|
|
317
|
+
backend: "claude-code",
|
|
318
|
+
nativeSessionId: minted.record.nativeSessionId,
|
|
319
|
+
ownerPid: process.pid,
|
|
320
|
+
armProvenance: "session-start",
|
|
321
|
+
receiversDir,
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
const result: EntwurfV2RunResult = await runEntwurfV2(
|
|
325
|
+
{ target: gid, intent: "fire-and-forget", message: "matrix-live C2 meta-mailbox probe" },
|
|
326
|
+
prodDeps(smokeSender(gid, tmp)),
|
|
327
|
+
);
|
|
328
|
+
ok(
|
|
329
|
+
"C2 decider routed an armed self-fetch citizen to meta-mailbox (executed)",
|
|
330
|
+
result.kind === "executed" && result.transport === "meta-mailbox",
|
|
331
|
+
);
|
|
332
|
+
const boxDir = path.join(mailboxDir, gid);
|
|
333
|
+
artifacts["C2.mailbox"] = boxDir;
|
|
334
|
+
const msgs = await fsp.readdir(boxDir).catch(() => [] as string[]);
|
|
335
|
+
// B4: enqueue is exactly one .msg AND the doorbell signal is poked.
|
|
336
|
+
ok("C2 exactly one .msg enqueued to the target mailbox", msgs.filter((f) => f.endsWith(".msg")).length === 1);
|
|
337
|
+
ok("C2 the doorbell inbox.signal was poked", existsSync(path.join(boxDir, "inbox.signal")));
|
|
338
|
+
ok("C2 meta-mailbox is lock-free (no lock file)", !existsSync(lockPathFor(gid, lockDir)));
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// ── C3: meta-mailbox guard — undeliverable (NO armed receiver) ──────────────
|
|
342
|
+
{
|
|
343
|
+
const minted = upsertMetaSession({
|
|
344
|
+
input: { backend: "claude-code", nativeSessionId: `smoke-c3-${process.pid}`, cwd: tmp },
|
|
345
|
+
dir: sessionsDir,
|
|
346
|
+
});
|
|
347
|
+
const gid = minted.record.gardenId;
|
|
348
|
+
artifacts["C3.gid"] = gid;
|
|
349
|
+
// NO receiver marker written → terminated self-fetch session → fail-closed.
|
|
350
|
+
|
|
351
|
+
const result: EntwurfV2RunResult = await runEntwurfV2(
|
|
352
|
+
{ target: gid, intent: "fire-and-forget", message: "matrix-live C3 should be refused" },
|
|
353
|
+
prodDeps(smokeSender(gid, tmp)),
|
|
354
|
+
);
|
|
355
|
+
// B5: pin the reason — a different reject (bad-target, etc.) must not pass as "guard works".
|
|
356
|
+
ok(
|
|
357
|
+
"C3 deliverability guard rejected with reason=mailbox-undeliverable",
|
|
358
|
+
result.kind === "rejected" && result.receipt.reason === "mailbox-undeliverable",
|
|
359
|
+
);
|
|
360
|
+
// B4: SE-2 reject contract = no enqueue AND no signal poke (mailbox fs untouched).
|
|
361
|
+
const boxDir = path.join(mailboxDir, gid);
|
|
362
|
+
const msgs = await fsp.readdir(boxDir).catch(() => [] as string[]);
|
|
363
|
+
ok("C3 no .msg garbage left for the dead receiver (SE-2)", !msgs.some((f) => f.endsWith(".msg")));
|
|
364
|
+
ok("C3 no inbox.signal poked for the dead receiver (SE-2)", !existsSync(path.join(boxDir, "inbox.signal")));
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
succeeded = true;
|
|
368
|
+
console.log(`\nsmoke-entwurf-v2-matrix-live: ${passed} checks passed (real pi control socket + real mailbox)`);
|
|
369
|
+
} catch (err) {
|
|
370
|
+
console.error("\n[smoke-entwurf-v2-matrix-live] FAILED — diagnostic artifacts:");
|
|
371
|
+
for (const [k, v] of Object.entries(artifacts)) console.error(` ${k} = ${v}`);
|
|
372
|
+
if (stderrTail) console.error(` pi stderr (tail):\n${stderrTail.replace(/^/gm, " ")}`);
|
|
373
|
+
throw err;
|
|
374
|
+
} finally {
|
|
375
|
+
if (resident) {
|
|
376
|
+
await terminateChild(resident).catch(() => {});
|
|
377
|
+
resident = null;
|
|
378
|
+
}
|
|
379
|
+
// remove the real control sockets pi may have left under the fresh gids (C1, C1b)
|
|
380
|
+
for (const gid of [residentGid, c1bGid]) {
|
|
381
|
+
if (gid) {
|
|
382
|
+
await fsp.rm(path.join(REAL_CONTROL_DIR, `${gid}${SOCKET_SUFFIX}`), { force: true }).catch(() => {});
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
// B6: keep the temp world on failure so the printed artifact paths are real for post-mortem
|
|
386
|
+
// (CI/cron). Only a clean pass tears it down.
|
|
387
|
+
if (succeeded) {
|
|
388
|
+
await fsp.rm(tmp, { recursive: true, force: true });
|
|
389
|
+
} else {
|
|
390
|
+
console.error(`[smoke-entwurf-v2-matrix-live] temp world preserved for inspection: ${tmp}`);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
main().catch((err) => {
|
|
396
|
+
console.error(err);
|
|
397
|
+
process.exit(1);
|
|
398
|
+
});
|
|
@@ -0,0 +1,175 @@
|
|
|
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
|
+
launchArgs: [],
|
|
60
|
+
expectedSocketPath: "/nonexistent/never.sock",
|
|
61
|
+
observeTimeoutMs: 30_000,
|
|
62
|
+
releaseWhen: "socket-alive-or-child-exited",
|
|
63
|
+
...over,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function main(): Promise<void> {
|
|
68
|
+
const tmp = await fsp.mkdtemp(path.join(os.tmpdir(), "v2spawn-"));
|
|
69
|
+
const children: ChildProcess[] = [];
|
|
70
|
+
let server: net.Server | null = null;
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
// ── S1: real unix-socket connectability (real fs.lstat + real probeSocketLiveness) ─
|
|
74
|
+
{
|
|
75
|
+
const sockPath = path.join(tmp, "live.sock");
|
|
76
|
+
server = net.createServer();
|
|
77
|
+
await new Promise<void>((resolve) => server?.listen(sockPath, resolve));
|
|
78
|
+
const deps = makeProductionSpawnBgResumeDeps({ pollIntervalMs: 20 });
|
|
79
|
+
await deps.awaitSocketAlive(sockPath, new AbortController().signal);
|
|
80
|
+
ok("S1 a real listening unix socket → awaitSocketAlive resolves (connectable)", true);
|
|
81
|
+
|
|
82
|
+
// symlink at the path → forged (real lstat catches it; never connected — P1).
|
|
83
|
+
const symPath = path.join(tmp, "sym.sock");
|
|
84
|
+
await fsp.symlink(sockPath, symPath);
|
|
85
|
+
let forged = false;
|
|
86
|
+
try {
|
|
87
|
+
await deps.awaitSocketAlive(symPath, new AbortController().signal);
|
|
88
|
+
} catch (e) {
|
|
89
|
+
forged = e instanceof Error && e.message.includes("forged");
|
|
90
|
+
}
|
|
91
|
+
ok("S1 a symlink at the socket path → forged reject (real lstat P1)", forged);
|
|
92
|
+
|
|
93
|
+
// absent path → dead → wait; abort settles it (real timer + abort cleanup).
|
|
94
|
+
const ctrl = new AbortController();
|
|
95
|
+
const absentP = deps.awaitSocketAlive(path.join(tmp, "absent.sock"), ctrl.signal);
|
|
96
|
+
setTimeout(() => ctrl.abort(), 60);
|
|
97
|
+
let aborted = false;
|
|
98
|
+
try {
|
|
99
|
+
await absentP;
|
|
100
|
+
} catch {
|
|
101
|
+
aborted = true;
|
|
102
|
+
}
|
|
103
|
+
ok("S1 an absent socket → waits, then abort settles it (no hang)", aborted);
|
|
104
|
+
|
|
105
|
+
await new Promise<void>((resolve) => server?.close(() => resolve()));
|
|
106
|
+
server = null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ── S2: real child lifecycle — spawn event, exit capture, SIGTERM kill ───────────
|
|
110
|
+
{
|
|
111
|
+
const deps = makeProductionSpawnBgResumeDeps({
|
|
112
|
+
resolveIdentity: () => ({ cwd: tmp, explicitExtensionArgs: [], provider: null, model: "smoke" }),
|
|
113
|
+
spawnChild: () => {
|
|
114
|
+
const proc = spawn(process.execPath, ["-e", RESIDENT_CHILD], { stdio: "ignore" });
|
|
115
|
+
children.push(proc);
|
|
116
|
+
return proc;
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
// spawnChild awaits the REAL 'spawn' event before resolving (B1).
|
|
120
|
+
const child = await deps.spawnChild(spawnBgPlan());
|
|
121
|
+
ok("S2 spawnChild resolves on the real 'spawn' event (B1)", typeof child.pid === "number" && child.pid > 0);
|
|
122
|
+
|
|
123
|
+
// SIGTERM → the resident child terminates; awaitChildExit OBSERVES the exit via the
|
|
124
|
+
// eager exitPromise (B2). The code is 0 if the child's SIGTERM handler ran, or null if
|
|
125
|
+
// the signal landed before the handler was installed — both are a real, observed exit
|
|
126
|
+
// (the watcher releases on child-exited for ANY code), which is what this proves.
|
|
127
|
+
deps.killChild(child);
|
|
128
|
+
const code = await deps.awaitChildExit(child, new AbortController().signal);
|
|
129
|
+
ok("S2 killChild(SIGTERM) → awaitChildExit observes the exit (code 0 or null)", code === 0 || code === null);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ── S3: watcher integration — real timeout → kill → child-exited → release ───────
|
|
133
|
+
{
|
|
134
|
+
let releases = 0;
|
|
135
|
+
const deps = makeProductionSpawnBgResumeDeps({
|
|
136
|
+
resolveIdentity: () => ({ cwd: tmp, explicitExtensionArgs: [], provider: null, model: "smoke" }),
|
|
137
|
+
spawnChild: () => {
|
|
138
|
+
const proc = spawn(process.execPath, ["-e", RESIDENT_CHILD], { stdio: "ignore" });
|
|
139
|
+
children.push(proc);
|
|
140
|
+
return proc;
|
|
141
|
+
},
|
|
142
|
+
releaseFn: () => {
|
|
143
|
+
releases++;
|
|
144
|
+
},
|
|
145
|
+
pollIntervalMs: 20,
|
|
146
|
+
killGraceMs: 2_000,
|
|
147
|
+
});
|
|
148
|
+
// A child that never stands a socket up + a short observe window → the watcher must
|
|
149
|
+
// time out, NOT release on the timeout, kill, observe the real exit, then release once.
|
|
150
|
+
const plan = spawnBgPlan({ observeTimeoutMs: 80, expectedSocketPath: path.join(tmp, "never-appears.sock") });
|
|
151
|
+
const result = await executeSpawnBgResume(plan, lockClaim(plan.sessionId), deps);
|
|
152
|
+
ok("S3 timeout→kill→child-exited (real timer + real child + 5c-3a watcher)", result.kind === "child-exited");
|
|
153
|
+
ok("S3 lock released exactly once on the observed exit", result.released === true && releases === 1);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
console.log(`\nsmoke-entwurf-v2-spawn-live: ${passed} checks passed (real OS primitives)`);
|
|
157
|
+
} finally {
|
|
158
|
+
for (const c of children) {
|
|
159
|
+
try {
|
|
160
|
+
c.kill("SIGKILL");
|
|
161
|
+
} catch {
|
|
162
|
+
// best-effort
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (server) {
|
|
166
|
+
await new Promise<void>((resolve) => server?.close(() => resolve()));
|
|
167
|
+
}
|
|
168
|
+
await fsp.rm(tmp, { recursive: true, force: true });
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
main().catch((err) => {
|
|
173
|
+
console.error(err);
|
|
174
|
+
process.exit(1);
|
|
175
|
+
});
|