gentle-pi 2.7.0 → 3.0.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/README.md +24 -6
- package/assets/agents/gentle-ai-worker.md +5 -1
- package/assets/agents/sdd-apply.md +9 -7
- package/assets/agents/sdd-archive.md +42 -23
- package/assets/agents/sdd-proposal.md +2 -2
- package/assets/agents/sdd-remediate.md +4 -4
- package/assets/agents/sdd-research.md +20 -48
- package/assets/agents/sdd-tasks.md +5 -5
- package/assets/agents/sdd-verify.md +6 -28
- package/assets/chains/sdd-full.chain.md +4 -22
- package/assets/chains/sdd-verify.chain.md +3 -12
- package/assets/orchestrator-delegation.md +33 -3
- package/assets/orchestrator-memory.md +20 -7
- package/assets/orchestrator.md +5 -3
- package/assets/sdd-orchestrator-workflow.md +25 -58
- package/assets/support/sdd-status-contract.md +9 -12
- package/docs/gentle-shell.md +14 -4
- package/docs/readme-reference.md +171 -32
- package/extensions/codegraph-tools.ts +2 -0
- package/extensions/gentle-agents.ts +281 -361
- package/extensions/gentle-ai.ts +588 -117
- package/extensions/gentle-shell.ts +74 -30
- package/extensions/pi-pretty.ts +63 -14
- package/extensions/quiet-tools.ts +1 -2
- package/extensions/startup-banner.ts +10 -9
- package/lib/agent-home.ts +8 -0
- package/lib/agent-profile-pin.ts +336 -0
- package/lib/agent-profiles.ts +28 -8
- package/lib/agents-config.ts +24 -2
- package/lib/agents-history.ts +3 -97
- package/lib/agents-keys.ts +27 -0
- package/lib/agents-protocol.ts +2 -15
- package/lib/agents-runner.ts +67 -111
- package/lib/agents-session-transport.ts +691 -0
- package/lib/command-palette-catalog.ts +87 -0
- package/lib/command-palette.ts +346 -0
- package/lib/native-choice-list.ts +5 -0
- package/lib/native-review-cli.ts +10 -97
- package/lib/review-publication-gate.ts +11 -1
- package/lib/review-repository.ts +1 -1
- package/lib/review-snapshot.ts +1 -0
- package/lib/review-transaction.ts +4 -2
- package/lib/sdd-preflight.ts +2 -1
- package/lib/sdd-research-capabilities.ts +18 -152
- package/lib/sdd-status.ts +7 -779
- package/lib/session-change-capture.ts +2 -1
- package/lib/session-changes.ts +8 -1
- package/lib/shell-bar.ts +21 -12
- package/lib/shell-card.ts +8 -12
- package/lib/shell-changes.ts +3 -2
- package/lib/shell-prompt.ts +25 -8
- package/lib/shell-sidebar-banner.ts +2 -2
- package/lib/shell-sidebar-layout.ts +5 -2
- package/lib/windows-session-transport.ts +877 -0
- package/package.json +3 -3
- package/runtime/native-review-cli.mjs +9 -96
- package/runtime/windows-session-transport.ps1 +791 -0
- package/scripts/test-packed-runner.mjs +1668 -20
- package/scripts/verify-package-files.mjs +0 -1
- package/tests/agent-home.test.ts +52 -0
- package/tests/agent-profiles.test.ts +30 -1
- package/tests/agents-config.test.ts +44 -0
- package/tests/agents-history.test.ts +12 -24
- package/tests/agents-runner.test.ts +307 -58
- package/tests/agents-session-transport-process.test.ts +249 -0
- package/tests/agents-session-transport.test.ts +823 -0
- package/tests/artifact-language.test.ts +10 -7
- package/tests/command-palette.test.ts +378 -0
- package/tests/delegated-key-learnings-contract.test.ts +2 -2
- package/tests/fixtures/agents-session-transport-process.mjs +108 -0
- package/tests/fixtures/legacy/sdd-research-v2.5.0.md +54 -0
- package/tests/fixtures/windows-session-bootstrap.ps1 +129 -0
- package/tests/fixtures/windows-session-compile.ps1 +110 -0
- package/tests/gentle-agents.test.ts +849 -356
- package/tests/gentle-ai.test.ts +472 -4
- package/tests/gentle-shell.test.ts +201 -8
- package/tests/native-choice-list.test.ts +13 -0
- package/tests/native-review-cli.test.ts +0 -33
- package/tests/odd-routing-contract.test.ts +208 -0
- package/tests/orchestrator-budget.test.ts +17 -2
- package/tests/package-manifest.test.ts +115 -27
- package/tests/persona-single-channel.test.ts +3 -3
- package/tests/pi-pretty.test.ts +45 -0
- package/tests/profile-pin.test.ts +370 -0
- package/tests/quiet-tool-rendering.test.ts +32 -5
- package/tests/review-contract-prompt.test.ts +9 -0
- package/tests/review-controller.test.ts +0 -44
- package/tests/review-session-standing-permission-ipc.test.ts +427 -13
- package/tests/runtime-harness.mjs +4 -4
- package/tests/sdd-agent-tools.test.ts +15 -36
- package/tests/sdd-archive-replay.test.ts +82 -0
- package/tests/sdd-classical-continuation.test.ts +74 -0
- package/tests/sdd-execution-routing-contract.test.ts +18 -2
- package/tests/sdd-managed-runtime-settlement.test.ts +42 -330
- package/tests/sdd-native-managed-uptake.test.ts +11 -21
- package/tests/sdd-no-attempts-contract.test.ts +15 -0
- package/tests/sdd-odd-integration.test.ts +33 -0
- package/tests/sdd-optional-research.test.ts +124 -0
- package/tests/sdd-planning-routing-contract.test.ts +1 -1
- package/tests/sdd-preflight-rpc-input.test.ts +125 -0
- package/tests/sdd-preflight.test.ts +1 -1
- package/tests/sdd-research-capabilities.test.ts +20 -162
- package/tests/sdd-selection-transport.test.ts +180 -88
- package/tests/sdd-status.test.ts +5 -778
- package/tests/sdd-task-truth.test.ts +43 -0
- package/tests/session-change-capture.test.ts +20 -2
- package/tests/session-changes.test.ts +11 -0
- package/tests/shell-bar.test.ts +21 -0
- package/tests/shell-card.test.ts +8 -6
- package/tests/shell-changes.test.ts +8 -0
- package/tests/shell-prompt.test.ts +41 -7
- package/tests/shell-sidebar-banner.test.ts +4 -4
- package/tests/shell-sidebar-layout.test.ts +97 -13
- package/tests/startup-banner.test.ts +55 -2
- package/tests/windows-hidden-processes.test.ts +303 -0
- package/tests/windows-session-bootstrap.test.ts +1772 -0
- package/tests/windows-session-compile.test.ts +170 -0
- package/tests/windows-session-transport.test.ts +754 -0
- package/assets/agents/sdd-sync.md +0 -146
- package/lib/openspec-guardrails.ts +0 -99
- package/tests/native-sdd-attempt-authority.test.ts +0 -240
- package/tests/openspec-guardrails.test.ts +0 -71
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { EventEmitter } from "node:events";
|
|
3
|
+
import { PassThrough } from "node:stream";
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
import { lstat, mkdtemp, rm } from "node:fs/promises";
|
|
6
|
+
import { tmpdir } from "node:os";
|
|
7
|
+
import { join, resolve } from "node:path";
|
|
8
|
+
import nodeTest from "node:test";
|
|
9
|
+
import type { TestContext } from "node:test";
|
|
10
|
+
const test = process.platform === "win32" ? nodeTest.skip : nodeTest;
|
|
11
|
+
|
|
12
|
+
const fixture = resolve("tests/fixtures/agents-session-transport-process.mjs");
|
|
13
|
+
const CONTROL_BYTES = 16_384;
|
|
14
|
+
const WAIT_MS = 2_000;
|
|
15
|
+
|
|
16
|
+
type Reply = { requestId: string; ok: boolean; result?: Record<string, unknown>; error?: string };
|
|
17
|
+
type Callback = { event: "callback"; requestId: string; pid: number; id: string; senderSessionId: string; message: string };
|
|
18
|
+
type Pending = { timer: ReturnType<typeof setTimeout>; settled: boolean; resolve: (reply: Reply) => void; reject: (error: Error) => void };
|
|
19
|
+
type Exit = { code: number | null; signal: NodeJS.Signals | null };
|
|
20
|
+
type SpawnedProcess = EventEmitter & {
|
|
21
|
+
pid?: number | undefined;
|
|
22
|
+
stdin: (NodeJS.WritableStream & EventEmitter) | null;
|
|
23
|
+
stdout: (NodeJS.ReadableStream & EventEmitter) | null;
|
|
24
|
+
stderr: (NodeJS.ReadableStream & EventEmitter) | null;
|
|
25
|
+
exitCode: number | null;
|
|
26
|
+
signalCode: NodeJS.Signals | null;
|
|
27
|
+
kill(signal?: NodeJS.Signals): boolean;
|
|
28
|
+
};
|
|
29
|
+
type SpawnProcess = (command: string, args: readonly string[], options: { env?: NodeJS.ProcessEnv; stdio: ["pipe", "pipe", "pipe"] }) => SpawnedProcess;
|
|
30
|
+
const defaultSpawn: SpawnProcess = (command, args, options) => spawn(command, args, options);
|
|
31
|
+
|
|
32
|
+
class MemoryProcess extends EventEmitter implements SpawnedProcess {
|
|
33
|
+
pid = undefined;
|
|
34
|
+
stdin = new PassThrough();
|
|
35
|
+
stdout = new PassThrough();
|
|
36
|
+
stderr = new PassThrough();
|
|
37
|
+
exitCode: number | null = null;
|
|
38
|
+
signalCode: NodeJS.Signals | null = null;
|
|
39
|
+
kill(): boolean {
|
|
40
|
+
this.exitCode = 0;
|
|
41
|
+
this.emit("exit", 0, null);
|
|
42
|
+
this.emit("close", 0, null);
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const memorySpawn: SpawnProcess = () => new MemoryProcess();
|
|
48
|
+
|
|
49
|
+
const bounded = async <T>(label: string, operation: Promise<T>) => {
|
|
50
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
51
|
+
try { return await Promise.race([operation, new Promise<never>((_, reject) => { timer = setTimeout(() => reject(new Error(`timed out waiting for ${label}`)), WAIT_MS); })]); }
|
|
52
|
+
finally { if (timer) clearTimeout(timer); }
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
function child(agentHome: string, spawnProcess: SpawnProcess = defaultSpawn) {
|
|
56
|
+
const process = spawnProcess(globalThis.process.execPath, ["--experimental-strip-types", fixture], {
|
|
57
|
+
env: { GENTLE_AGENT_HOME: agentHome }, stdio: ["pipe", "pipe", "pipe"],
|
|
58
|
+
});
|
|
59
|
+
const replies = new Map<string, Pending>();
|
|
60
|
+
const callbacks: Callback[] = [];
|
|
61
|
+
const termination: { term?: boolean; kill?: boolean } = {};
|
|
62
|
+
let sequence = 0, exited = false, closed = false, spawnFailed = false, readerClosed = false;
|
|
63
|
+
let resolveExit!: (exit: Exit) => void, exitResolved = false;
|
|
64
|
+
const exitedPromise = new Promise<Exit>((resolve) => { resolveExit = resolve; });
|
|
65
|
+
const settleExit = (exit: Exit) => { if (!exitResolved) { exitResolved = true; resolveExit(exit); } };
|
|
66
|
+
const terminal = () => exited || closed || spawnFailed || process.exitCode !== null || process.signalCode !== null;
|
|
67
|
+
const failure = () => new Error(spawnFailed ? "child spawn failed" : "child exited before response");
|
|
68
|
+
const settle = (pending: Pending, error?: Error, reply?: Reply) => {
|
|
69
|
+
if (pending.settled) return;
|
|
70
|
+
pending.settled = true;
|
|
71
|
+
clearTimeout(pending.timer);
|
|
72
|
+
for (const [requestId, current] of replies) if (current === pending) replies.delete(requestId);
|
|
73
|
+
if (error) pending.reject(error); else pending.resolve(reply!);
|
|
74
|
+
};
|
|
75
|
+
const rejectAll = (error: Error) => { for (const pending of [...replies.values()]) settle(pending, error); };
|
|
76
|
+
let partial = Buffer.alloc(0);
|
|
77
|
+
const closeReader = () => {
|
|
78
|
+
if (readerClosed) return;
|
|
79
|
+
readerClosed = true;
|
|
80
|
+
partial = Buffer.alloc(0);
|
|
81
|
+
process.stdout?.off("data", onOutput);
|
|
82
|
+
process.stdout?.off("error", onReaderError);
|
|
83
|
+
process.stderr?.pause();
|
|
84
|
+
};
|
|
85
|
+
const onReaderError = () => rejectAll(new Error("child output failed"));
|
|
86
|
+
const receive = (bytes: Buffer) => {
|
|
87
|
+
let message: Reply | Callback;
|
|
88
|
+
try { message = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes)) as Reply | Callback; }
|
|
89
|
+
catch { rejectAll(new Error("child output frame invalid")); return; }
|
|
90
|
+
if ("event" in message) {
|
|
91
|
+
if (callbacks.length === 64) { rejectAll(new Error("child event queue exceeds bound")); closeReader(); return; }
|
|
92
|
+
callbacks.push(message);
|
|
93
|
+
} else {
|
|
94
|
+
const pending = replies.get(message.requestId);
|
|
95
|
+
if (pending) settle(pending, undefined, message);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
const onOutput = (chunk: Buffer) => {
|
|
99
|
+
let start = 0;
|
|
100
|
+
while (start < chunk.length) {
|
|
101
|
+
const newline = chunk.indexOf(10, start), end = newline < 0 ? chunk.length : newline;
|
|
102
|
+
const fragment = chunk.subarray(start, end);
|
|
103
|
+
if (partial.length + fragment.length > CONTROL_BYTES) { rejectAll(new Error("child output frame exceeds bound")); closeReader(); return; }
|
|
104
|
+
if (newline < 0) { partial = Buffer.concat([partial, fragment]); return; }
|
|
105
|
+
receive(partial.length ? Buffer.concat([partial, fragment]) : fragment);
|
|
106
|
+
partial = Buffer.alloc(0);
|
|
107
|
+
start = newline + 1;
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
process.stdout!.on("data", onOutput);
|
|
111
|
+
process.stdout!.on("error", onReaderError);
|
|
112
|
+
process.stdin!.on("error", (error) => rejectAll(error));
|
|
113
|
+
process.stderr!.resume();
|
|
114
|
+
process.once("error", (error) => { spawnFailed = true; rejectAll(error); closeReader(); settleExit({ code: null, signal: null }); });
|
|
115
|
+
process.once("exit", (code, signal) => { exited = true; rejectAll(failure()); closeReader(); settleExit({ code, signal }); });
|
|
116
|
+
process.once("close", (code, signal) => { closed = true; rejectAll(failure()); closeReader(); settleExit({ code, signal }); });
|
|
117
|
+
const command = (operation: string, values: Record<string, unknown> = {}) => {
|
|
118
|
+
if (terminal()) return Promise.reject(failure());
|
|
119
|
+
const requestId = `request-${++sequence}`, frame = JSON.stringify({ requestId, operation, ...values });
|
|
120
|
+
assert.ok(Buffer.byteLength(frame) <= CONTROL_BYTES, "fixture control frame must stay bounded");
|
|
121
|
+
return new Promise<Record<string, unknown>>((resolve, reject) => {
|
|
122
|
+
const pending: Pending = { settled: false, timer: undefined as never, resolve: (reply) => reply.ok ? resolve(reply.result!) : reject(new Error(`fixture ${operation} failed: ${reply.error}`)), reject };
|
|
123
|
+
pending.timer = setTimeout(() => settle(pending, new Error(`timed out waiting for ${operation}`)), WAIT_MS);
|
|
124
|
+
replies.set(requestId, pending);
|
|
125
|
+
try { process.stdin!.write(`${frame}\n`, (error) => { if (error) settle(pending, error); }); }
|
|
126
|
+
catch (error) { settle(pending, error instanceof Error ? error : new Error("child input failed")); }
|
|
127
|
+
});
|
|
128
|
+
};
|
|
129
|
+
const callback = (id: string) => bounded(`callback ${id}`, new Promise<Callback>((resolve, reject) => {
|
|
130
|
+
const found = () => callbacks.find((event) => event.requestId === id);
|
|
131
|
+
const existing = found();
|
|
132
|
+
if (existing) { resolve(existing); return; }
|
|
133
|
+
const interval = setInterval(() => { const event = found(); if (event) { clearInterval(interval); clearTimeout(timeout); resolve(event); } }, 5);
|
|
134
|
+
const timeout = setTimeout(() => { clearInterval(interval); reject(new Error(`callback ${id} was not observed`)); }, WAIT_MS);
|
|
135
|
+
}));
|
|
136
|
+
const terminate = async () => {
|
|
137
|
+
if (terminal()) return;
|
|
138
|
+
await command("shutdown").catch(() => {});
|
|
139
|
+
if (terminal()) return;
|
|
140
|
+
process.stdin!.end();
|
|
141
|
+
await bounded("child graceful exit", exitedPromise);
|
|
142
|
+
};
|
|
143
|
+
return { process, command, callback, terminate, exited: terminal, exitedPromise, pendingCount: () => replies.size, raw: (frame: string) => process.stdin!.write(frame), termination };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
test("stdin EPIPE rejects the pending command without escaping the transport helper", async (t: TestContext) => {
|
|
147
|
+
const owned = child("fixture", memorySpawn);
|
|
148
|
+
const expected = Object.assign(new Error("fixture EPIPE"), { code: "EPIPE" });
|
|
149
|
+
const pending = owned.command("pending");
|
|
150
|
+
const observed = pending.then(() => undefined, (error) => error);
|
|
151
|
+
let primary: unknown;
|
|
152
|
+
try {
|
|
153
|
+
assert.doesNotThrow(() => owned.process.stdin!.emit("error", expected), "stdin EPIPE must be handled by the transport helper");
|
|
154
|
+
assert.strictEqual(await bounded("EPIPE command rejection", observed), expected, "the pending command receives the stdin error instance");
|
|
155
|
+
assert.equal(owned.pendingCount(), 0, "stdin failure drains pending commands");
|
|
156
|
+
} catch (error) {
|
|
157
|
+
primary = error;
|
|
158
|
+
} finally {
|
|
159
|
+
const lateErrors: Error[] = [];
|
|
160
|
+
const onStdinError = (error: Error) => { lateErrors.push(error); };
|
|
161
|
+
owned.process.stdin!.on("error", onStdinError);
|
|
162
|
+
try {
|
|
163
|
+
owned.process.stdin!.end();
|
|
164
|
+
owned.process.emit("exit", 1, null);
|
|
165
|
+
owned.process.emit("close", 1, null);
|
|
166
|
+
await bounded("memory transport command settlement", observed);
|
|
167
|
+
await bounded("memory transport pending drain", (async () => {
|
|
168
|
+
while (owned.pendingCount() !== 0) await new Promise((resolve) => setImmediate(resolve));
|
|
169
|
+
})());
|
|
170
|
+
assert.deepEqual(lateErrors, [], "cleanup must not emit an additional stdin error");
|
|
171
|
+
} catch (error) {
|
|
172
|
+
if (primary === undefined) primary = error;
|
|
173
|
+
else t.diagnostic(`EPIPE cleanup secondary failure: ${error instanceof Error ? error.message : String(error)}`);
|
|
174
|
+
} finally {
|
|
175
|
+
owned.process.stdin!.off("error", onStdinError);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (primary !== undefined) throw primary;
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
async function ensureStopped(owned: ReturnType<typeof child>) {
|
|
182
|
+
if (owned.exited()) return;
|
|
183
|
+
try { await owned.terminate(); return; } catch {}
|
|
184
|
+
const signal = (name: "SIGTERM" | "SIGKILL") => {
|
|
185
|
+
if (owned.exited()) return true;
|
|
186
|
+
let delivered = false;
|
|
187
|
+
try { delivered = owned.process.kill(name); } catch {}
|
|
188
|
+
if (name === "SIGTERM") owned.termination.term = delivered; else owned.termination.kill = delivered;
|
|
189
|
+
if (!delivered && !owned.exited()) throw new Error(`owned child ${name} was not delivered`);
|
|
190
|
+
return delivered;
|
|
191
|
+
};
|
|
192
|
+
signal("SIGTERM");
|
|
193
|
+
try { await bounded("child TERM exit", owned.exitedPromise); return; } catch {}
|
|
194
|
+
signal("SIGKILL");
|
|
195
|
+
await bounded("child KILL exit", owned.exitedPromise);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const absent = async (path: string) => assert.rejects(lstat(path));
|
|
199
|
+
async function withChild(run: (owned: ReturnType<typeof child>) => Promise<void>) {
|
|
200
|
+
const agentHome = await mkdtemp(join(tmpdir(), "ga-x-")), owned = child(agentHome);
|
|
201
|
+
try { await run(owned); } finally { await ensureStopped(owned); await rm(agentHome, { recursive: true, force: true }); }
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
test("independent Node children exchange transport notifications and clean only their own activations", async () => {
|
|
205
|
+
const agentHome = await mkdtemp(join(tmpdir(), "ga-x-"));
|
|
206
|
+
assert.ok(Buffer.byteLength(agentHome) <= 100, "fresh test agentHome must be a short Unix path");
|
|
207
|
+
const children: ReturnType<typeof child>[] = [];
|
|
208
|
+
try {
|
|
209
|
+
const alpha = child(agentHome), beta = child(agentHome);
|
|
210
|
+
children.push(alpha, beta);
|
|
211
|
+
const alphaRecord = await alpha.command("start", { sessionId: "alpha" });
|
|
212
|
+
const betaRecord = await beta.command("start", { sessionId: "beta" });
|
|
213
|
+
assert.notEqual(alpha.process.pid, beta.process.pid, "children must be independent processes");
|
|
214
|
+
assert.deepEqual(await alpha.command("send", { recipientSessionId: "beta", id: "alpha-to-beta", message: "from alpha" }), { id: "alpha-to-beta", accepted: true });
|
|
215
|
+
assert.deepEqual(await beta.command("send", { recipientSessionId: "alpha", id: "beta-to-alpha", message: "from beta" }), { id: "beta-to-alpha", accepted: true });
|
|
216
|
+
assert.deepEqual(await beta.callback("alpha-to-beta"), { event: "callback", requestId: "alpha-to-beta", pid: beta.process.pid, id: "alpha-to-beta", senderSessionId: "alpha", message: "from alpha" });
|
|
217
|
+
assert.deepEqual(await alpha.callback("beta-to-alpha"), { event: "callback", requestId: "beta-to-alpha", pid: alpha.process.pid, id: "beta-to-alpha", senderSessionId: "beta", message: "from beta" });
|
|
218
|
+
const betaStopped = await beta.command("stop");
|
|
219
|
+
await absent(betaStopped.presencePath as string); await absent(betaStopped.endpoint as string);
|
|
220
|
+
const newerAlpha = await beta.command("start", { sessionId: "alpha" });
|
|
221
|
+
const alphaStopped = await alpha.command("stop");
|
|
222
|
+
await absent(alphaStopped.presencePath as string); await absent(alphaStopped.endpoint as string);
|
|
223
|
+
assert.deepEqual(await beta.command("resolve", { sessionId: "alpha" }), newerAlpha, "old owner shutdown must preserve newer same-session activation");
|
|
224
|
+
const betaFinal = await beta.command("stop");
|
|
225
|
+
await absent(betaFinal.presencePath as string); await absent(betaFinal.endpoint as string);
|
|
226
|
+
assert.ok(alphaRecord.endpoint && betaRecord.endpoint, "child start evidence includes exact owned endpoints");
|
|
227
|
+
} finally { await Promise.all(children.map(ensureStopped)); await rm(agentHome, { recursive: true, force: true }); }
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
test("clears pending commands on timeout, late reply, and child exit", () => withChild(async (owned) => {
|
|
231
|
+
await assert.rejects(owned.command("delay", { delayMs: WAIT_MS + 100 }), /timed out waiting for delay/);
|
|
232
|
+
assert.equal(owned.pendingCount(), 0, "timed-out commands must not retain reply entries");
|
|
233
|
+
assert.deepEqual(await owned.command("delay", { delayMs: 0 }), { delayed: true }, "late replies must not block later commands");
|
|
234
|
+
assert.equal(owned.pendingCount(), 0, "late replies must not recreate a pending entry");
|
|
235
|
+
await assert.rejects(owned.command("exit"), /child exited before response/);
|
|
236
|
+
assert.equal(owned.pendingCount(), 0, "child exit must reject and clear every pending request");
|
|
237
|
+
}));
|
|
238
|
+
|
|
239
|
+
test("rejects oversized unterminated fixture control input before unbounded buffering", () => withChild(async (owned) => {
|
|
240
|
+
owned.raw("x".repeat(CONTROL_BYTES + 1));
|
|
241
|
+
assert.deepEqual(await bounded("oversized fixture exit", owned.exitedPromise), { code: 2, signal: null });
|
|
242
|
+
}));
|
|
243
|
+
|
|
244
|
+
test("records retained-child TERM fallback only before observed exit", () => withChild(async (owned) => {
|
|
245
|
+
await owned.command("block-shutdown");
|
|
246
|
+
await ensureStopped(owned);
|
|
247
|
+
assert.equal(owned.termination.term, true, "retained child received the recorded TERM fallback");
|
|
248
|
+
assert.equal(owned.exited(), true, "fallback waits for the retained child exit");
|
|
249
|
+
}));
|