gentle-pi 2.6.4 → 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.
Files changed (128) hide show
  1. package/README.md +25 -7
  2. package/assets/agents/gentle-ai-worker.md +5 -1
  3. package/assets/agents/sdd-apply.md +9 -7
  4. package/assets/agents/sdd-archive.md +42 -23
  5. package/assets/agents/sdd-proposal.md +2 -2
  6. package/assets/agents/sdd-remediate.md +4 -4
  7. package/assets/agents/sdd-research.md +20 -48
  8. package/assets/agents/sdd-tasks.md +5 -5
  9. package/assets/agents/sdd-verify.md +6 -28
  10. package/assets/chains/sdd-full.chain.md +4 -22
  11. package/assets/chains/sdd-verify.chain.md +3 -12
  12. package/assets/orchestrator-delegation.md +33 -3
  13. package/assets/orchestrator-memory.md +20 -7
  14. package/assets/orchestrator.md +5 -3
  15. package/assets/sdd-orchestrator-workflow.md +25 -58
  16. package/assets/support/sdd-status-contract.md +9 -12
  17. package/docs/gentle-shell.md +41 -19
  18. package/docs/readme-reference.md +175 -36
  19. package/extensions/codegraph-tools.ts +2 -0
  20. package/extensions/gentle-agents.ts +289 -361
  21. package/extensions/gentle-ai.ts +588 -117
  22. package/extensions/gentle-shell.ts +123 -97
  23. package/extensions/pi-pretty.ts +63 -14
  24. package/extensions/quiet-tools.ts +1 -2
  25. package/extensions/startup-banner.ts +10 -9
  26. package/lib/agent-home.ts +8 -0
  27. package/lib/agent-profile-pin.ts +336 -0
  28. package/lib/agent-profiles.ts +28 -8
  29. package/lib/agents-config.ts +24 -2
  30. package/lib/agents-history.ts +3 -97
  31. package/lib/agents-keys.ts +27 -0
  32. package/lib/agents-protocol.ts +2 -15
  33. package/lib/agents-runner.ts +74 -113
  34. package/lib/agents-session-transport.ts +691 -0
  35. package/lib/command-palette-catalog.ts +87 -0
  36. package/lib/command-palette.ts +346 -0
  37. package/lib/native-choice-list.ts +5 -0
  38. package/lib/native-review-cli.ts +19 -97
  39. package/lib/review-publication-gate.ts +11 -1
  40. package/lib/review-repository.ts +1 -1
  41. package/lib/review-snapshot.ts +1 -0
  42. package/lib/review-transaction.ts +4 -2
  43. package/lib/sdd-preflight.ts +2 -1
  44. package/lib/sdd-research-capabilities.ts +18 -152
  45. package/lib/sdd-status.ts +7 -779
  46. package/lib/session-change-capture.ts +88 -0
  47. package/lib/session-changes.ts +147 -0
  48. package/lib/shell-bar.ts +24 -10
  49. package/lib/shell-card.ts +8 -12
  50. package/lib/shell-changes-view.ts +2 -1
  51. package/lib/shell-changes.ts +5 -2
  52. package/lib/shell-prompt.ts +25 -8
  53. package/lib/shell-sidebar-banner.ts +2 -2
  54. package/lib/shell-sidebar-layout.ts +5 -2
  55. package/lib/windows-session-transport.ts +877 -0
  56. package/package.json +3 -3
  57. package/runtime/native-review-cli.mjs +18 -96
  58. package/runtime/windows-session-transport.ps1 +791 -0
  59. package/scripts/gentle-ai-installer.mjs +10 -10
  60. package/scripts/test-packed-runner.mjs +1668 -20
  61. package/scripts/verify-package-files.mjs +2 -3
  62. package/tests/agent-home.test.ts +52 -0
  63. package/tests/agent-profiles.test.ts +30 -1
  64. package/tests/agents-config.test.ts +44 -0
  65. package/tests/agents-history.test.ts +12 -24
  66. package/tests/agents-runner.test.ts +321 -58
  67. package/tests/agents-session-transport-process.test.ts +249 -0
  68. package/tests/agents-session-transport.test.ts +823 -0
  69. package/tests/artifact-language.test.ts +10 -7
  70. package/tests/command-palette.test.ts +378 -0
  71. package/tests/delegated-key-learnings-contract.test.ts +2 -2
  72. package/tests/fixtures/agents-session-transport-process.mjs +108 -0
  73. package/tests/fixtures/legacy/sdd-research-v2.5.0.md +54 -0
  74. package/tests/fixtures/windows-session-bootstrap.ps1 +129 -0
  75. package/tests/fixtures/windows-session-compile.ps1 +110 -0
  76. package/tests/gentle-agents.test.ts +883 -356
  77. package/tests/gentle-ai-binary.test.ts +1 -1
  78. package/tests/gentle-ai-installer.test.ts +47 -47
  79. package/tests/gentle-ai.test.ts +472 -4
  80. package/tests/gentle-shell.test.ts +338 -205
  81. package/tests/native-choice-list.test.ts +13 -0
  82. package/tests/native-review-capability-contract.test.ts +15 -1
  83. package/tests/native-review-cli.test.ts +0 -33
  84. package/tests/odd-routing-contract.test.ts +208 -0
  85. package/tests/orchestrator-budget.test.ts +17 -2
  86. package/tests/package-manifest.test.ts +119 -31
  87. package/tests/persona-single-channel.test.ts +3 -3
  88. package/tests/pi-pretty.test.ts +45 -0
  89. package/tests/profile-pin.test.ts +370 -0
  90. package/tests/quiet-tool-rendering.test.ts +32 -5
  91. package/tests/review-contract-prompt.test.ts +9 -0
  92. package/tests/review-controller.test.ts +0 -44
  93. package/tests/review-session-standing-permission-ipc.test.ts +427 -13
  94. package/tests/runtime-harness.mjs +4 -4
  95. package/tests/sdd-agent-tools.test.ts +15 -36
  96. package/tests/sdd-archive-replay.test.ts +82 -0
  97. package/tests/sdd-classical-continuation.test.ts +74 -0
  98. package/tests/sdd-execution-routing-contract.test.ts +18 -2
  99. package/tests/sdd-managed-runtime-settlement.test.ts +42 -330
  100. package/tests/sdd-native-managed-uptake.test.ts +11 -21
  101. package/tests/sdd-no-attempts-contract.test.ts +15 -0
  102. package/tests/sdd-odd-integration.test.ts +33 -0
  103. package/tests/sdd-optional-research.test.ts +124 -0
  104. package/tests/sdd-planning-routing-contract.test.ts +1 -1
  105. package/tests/sdd-preflight-rpc-input.test.ts +125 -0
  106. package/tests/sdd-preflight.test.ts +1 -1
  107. package/tests/sdd-research-capabilities.test.ts +20 -162
  108. package/tests/sdd-selection-transport.test.ts +180 -88
  109. package/tests/sdd-status.test.ts +5 -778
  110. package/tests/sdd-task-truth.test.ts +43 -0
  111. package/tests/session-change-capture.test.ts +86 -0
  112. package/tests/session-changes-shell.test.ts +38 -0
  113. package/tests/session-changes.test.ts +114 -0
  114. package/tests/shell-bar.test.ts +35 -0
  115. package/tests/shell-card.test.ts +8 -6
  116. package/tests/shell-changes.test.ts +8 -0
  117. package/tests/shell-prompt.test.ts +41 -7
  118. package/tests/shell-sidebar-banner.test.ts +4 -4
  119. package/tests/shell-sidebar-layout.test.ts +97 -13
  120. package/tests/startup-banner.test.ts +55 -2
  121. package/tests/windows-hidden-processes.test.ts +303 -0
  122. package/tests/windows-session-bootstrap.test.ts +1772 -0
  123. package/tests/windows-session-compile.test.ts +170 -0
  124. package/tests/windows-session-transport.test.ts +754 -0
  125. package/assets/agents/sdd-sync.md +0 -146
  126. package/lib/openspec-guardrails.ts +0 -99
  127. package/tests/native-sdd-attempt-authority.test.ts +0 -240
  128. package/tests/openspec-guardrails.test.ts +0 -71
@@ -0,0 +1,754 @@
1
+ import assert from "node:assert/strict";
2
+ import { EventEmitter } from "node:events";
3
+ import { Socket } from "node:net";
4
+ import test from "node:test";
5
+ import { FIXED_WINDOWS_POWERSHELL, WindowsActiveSessionClient, WindowsActiveSessionListener, WindowsSessionPresenceRegistry, WindowsSessionTransportHost, parseWindowsHostFrame, parseWindowsHostNotification } from "../lib/windows-session-transport.ts";
6
+ import type { PresenceRecord } from "../lib/agents-session-transport.ts";
7
+
8
+ class FakeTransportChild extends EventEmitter {
9
+ stdin = Object.assign(new EventEmitter(), { writable: true, endCalls: 0, write: (line: string, callback: (error?: Error) => void) => { this.lines.push(line); callback(); return true; }, end: () => { this.stdin.endCalls++; } });
10
+ stdout = new EventEmitter();
11
+ stderr = new EventEmitter();
12
+ exitCode: number | null = null;
13
+ signalCode: NodeJS.Signals | null = null;
14
+ lines: string[] = [];
15
+ killCalls = 0;
16
+ kill() { this.killCalls++; this.emit("close", 1, null); return true; }
17
+ }
18
+
19
+ const testEndpoint = "\\\\.\\pipe\\gentle-pi-0123456789abcdef0123456789abcdef";
20
+ const nextTurn = () => new Promise<void>((resolve) => setImmediate(resolve));
21
+ const notificationWire = (id: string, message = "ok") => Buffer.from(JSON.stringify({ version: 1, kind: "notification", id, senderSessionId: "sender", recipientSessionId: "recipient", message }) + "\n").toString("base64");
22
+ const notificationEvent = (wire: string, connectionId = "connection-1") => JSON.stringify({ event: "notification", connectionId, generation: 1, wire });
23
+
24
+ async function createListeningFakeHost(callback: () => Promise<boolean>) {
25
+ const child = new FakeTransportChild();
26
+ const host = new WindowsSessionTransportHost({ spawnProcess: () => child as never, callback });
27
+ const emit = (line: string) => child.stdout.emit("data", Buffer.from(`${line}\n`));
28
+ const starting = host.start();
29
+ emit('{"requestId":"start-1","ok":true,"result":{"state":"partial"}}');
30
+ await starting;
31
+ const listening = host.listen("recipient", 1);
32
+ emit(`{"requestId":"listen-2","ok":true,"result":{"version":1,"sessionId":"recipient","endpoint":"${testEndpoint.replaceAll("\\", "\\\\")}","createdAt":1}}`);
33
+ await listening;
34
+ return { child, host, emit };
35
+ }
36
+
37
+ test("Windows listener readiness returns only after helper-owned publication without a stale publish RPC", async () => {
38
+ const child = new FakeTransportChild();
39
+ const host = new WindowsSessionTransportHost({ spawnProcess: () => child as never });
40
+ const Registry = WindowsSessionPresenceRegistry as unknown as { new(host: WindowsSessionTransportHost): WindowsSessionPresenceRegistry };
41
+ const registry = new Registry(host);
42
+ const emit = (line: string) => child.stdout.emit("data", Buffer.from(`${line}\n`));
43
+ const ready = host.start();
44
+ emit('{"requestId":"start-1","ok":true,"result":{"state":"partial"}}');
45
+ await ready;
46
+ let settled = false;
47
+ const starting = registry.startListener("recipient").then((record) => { settled = true; return record; }, (error: unknown) => { settled = true; throw error; });
48
+ try {
49
+ emit(`{"requestId":"listen-2","ok":true,"result":{"version":1,"sessionId":"recipient","endpoint":"${testEndpoint.replaceAll("\\", "\\\\")}","createdAt":1}}`);
50
+ await nextTurn();
51
+ assert.equal(settled, true, "a successful listen reply must already own its publication");
52
+ assert.deepEqual(await starting, { version: 1, sessionId: "recipient", endpoint: testEndpoint, createdAt: 1 });
53
+ assert.equal(child.lines.filter((line) => line.includes('"operation":"publish"')).length, 0, "listener startup must not re-publish a record after helper readiness");
54
+ } finally {
55
+ child.emit("exit", 1, null);
56
+ await starting.catch(() => {});
57
+ }
58
+ });
59
+
60
+ test("Windows bridge accepts only bounded protocol frames and uses the fixed PS5.1 executable", () => {
61
+ assert.equal(FIXED_WINDOWS_POWERSHELL, "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe");
62
+ assert.deepEqual(parseWindowsHostFrame('{"requestId":"r1","ok":true,"result":{"state":"partial"}}'), { requestId: "r1", ok: true, result: { state: "partial" } });
63
+ for (const frame of ["", "{", '{"requestId":"r1","ok":true,"error":"unsafe"}', '{"requestId":"r1","ok":true,"result":{"path":"C:\\\\private"}}', "x".repeat(16_385)]) {
64
+ assert.throws(() => parseWindowsHostFrame(frame), /invalid Windows transport frame/);
65
+ }
66
+ });
67
+
68
+ test("Windows startup markers are monotonic and never satisfy start readiness", async () => {
69
+ const child = new FakeTransportChild();
70
+ const host = new WindowsSessionTransportHost({ spawnProcess: () => child as never });
71
+ const starting = host.start();
72
+ child.stdout.emit("data", Buffer.from('{"event":"startup-marker","marker":"script-entered"}\n'));
73
+ assert.equal(host.lastStartupMarker, "script-entered");
74
+ child.stdout.emit("data", Buffer.from('{"event":"startup-marker","marker":"native-ready"}\n'));
75
+ assert.equal(host.lastStartupMarker, "native-ready");
76
+ let settled = false;
77
+ void starting.then(() => { settled = true; }, () => { settled = true; });
78
+ await nextTurn();
79
+ assert.equal(settled, false, "markers are not RPC replies or readiness");
80
+ child.stdout.emit("data", Buffer.from('{"requestId":"start-1","ok":true,"result":{"state":"partial"}}\n'));
81
+ await starting;
82
+ child.emit("exit", 1, null);
83
+ });
84
+
85
+ test("Windows host bounds default startup separately from ordinary RPCs without resetting for markers", async (t) => {
86
+ t.mock.timers.enable({ apis: ["setTimeout"] });
87
+ for (const deadline of [0, 1.5, Number.NaN, Number.POSITIVE_INFINITY, 2_001]) assert.throws(() => new WindowsSessionTransportHost({ rpcDeadlineMs: deadline }), /invalid Windows transport deadline/);
88
+ for (const deadline of [0, 1.5, Number.NaN, Number.POSITIVE_INFINITY, 30_001]) assert.throws(() => new WindowsSessionTransportHost({ startupDeadlineMs: deadline }), /invalid Windows transport startup deadline/);
89
+
90
+ const startupChild = new FakeTransportChild();
91
+ const startupHost = new WindowsSessionTransportHost({ spawnProcess: () => startupChild as never });
92
+ let defaultStartupSettled = false;
93
+ const starting = startupHost.start();
94
+ void starting.then(() => { defaultStartupSettled = true; }, () => { defaultStartupSettled = true; });
95
+ t.mock.timers.tick(2_000);
96
+ // This test mocks only setTimeout, so the real setImmediate turn drains the
97
+ // attached rejection reaction before checking the 2s boundary.
98
+ await nextTurn();
99
+ assert.equal(defaultStartupSettled, false, "the default start handshake remains pending beyond the default RPC deadline");
100
+ t.mock.timers.tick(28_000);
101
+ await assert.rejects(starting, /Windows transport request timed out/);
102
+ const startupClosing = startupHost.close();
103
+ startupChild.stdout.emit("data", Buffer.from('{"requestId":"shutdown-2","ok":true,"result":{"state":"partial"}}\n'));
104
+ startupChild.emit("close", 0, null);
105
+ await startupClosing;
106
+
107
+ const ordinaryChild = new FakeTransportChild();
108
+ const ordinaryHost = new WindowsSessionTransportHost({ spawnProcess: () => ordinaryChild as never });
109
+ const ordinaryStarting = ordinaryHost.start();
110
+ ordinaryChild.stdout.emit("data", Buffer.from('{"requestId":"start-1","ok":true,"result":{"state":"partial"}}\n'));
111
+ await ordinaryStarting;
112
+ const ordinary = ordinaryHost.request("list", {});
113
+ t.mock.timers.tick(2_000);
114
+ await assert.rejects(ordinary, /Windows transport request timed out/);
115
+ const ordinaryClosing = ordinaryHost.close();
116
+ ordinaryChild.stdout.emit("data", Buffer.from('{"requestId":"shutdown-3","ok":true,"result":{"state":"partial"}}\n'));
117
+ ordinaryChild.emit("close", 0, null);
118
+ await ordinaryClosing;
119
+
120
+ const markerChild = new FakeTransportChild();
121
+ const markerHost = new WindowsSessionTransportHost({ spawnProcess: () => markerChild as never, startupDeadlineMs: 10 });
122
+ const markerStarting = markerHost.start();
123
+ t.mock.timers.tick(5);
124
+ markerChild.stdout.emit("data", Buffer.from('{"event":"startup-marker","marker":"script-entered"}\n{"event":"startup-marker","marker":"native-ready"}\n'));
125
+ t.mock.timers.tick(5);
126
+ await assert.rejects(markerStarting, /Windows transport request timed out/);
127
+ const markerClosing = markerHost.close();
128
+ markerChild.stdout.emit("data", Buffer.from('{"requestId":"shutdown-2","ok":true,"result":{"state":"partial"}}\n'));
129
+ markerChild.emit("close", 0, null);
130
+ await markerClosing;
131
+ });
132
+
133
+ test("Windows startup marker transport rejects unknown, duplicate, and regressive frames", async () => {
134
+ for (const frames of [
135
+ ['{"event":"startup-marker","marker":"native-ready"}'],
136
+ ['{"event":"startup-marker","marker":"script-entered"}', '{"event":"startup-marker","marker":"script-entered"}'],
137
+ ['{"event":"startup-marker","marker":"script-entered"}', '{"event":"startup-marker","marker":"unknown"}'],
138
+ ['{"event":"startup-marker","marker":"script-entered","extra":true}'],
139
+ ]) {
140
+ const child = new FakeTransportChild();
141
+ const host = new WindowsSessionTransportHost({ spawnProcess: () => child as never });
142
+ const starting = host.start();
143
+ for (const frame of frames) child.stdout.emit("data", Buffer.from(`${frame}\n`));
144
+ await assert.rejects(starting, /Windows transport host unavailable/);
145
+ assert.equal(child.killCalls, 1, "invalid marker input must close the owned child");
146
+ }
147
+ });
148
+
149
+ test("Windows bridge admits bounded public presence results while rejecting private metadata", () => {
150
+ const endpoint = "\\\\.\\pipe\\gentle-pi-0123456789abcdef0123456789abcdef";
151
+ assert.deepEqual(parseWindowsHostFrame(`{"requestId":"record-1","ok":true,"result":{"version":1,"sessionId":"session-a","endpoint":"${endpoint.replaceAll("\\", "\\\\")}","createdAt":1}}`), {
152
+ requestId: "record-1", ok: true, result: { version: 1, sessionId: "session-a", endpoint, createdAt: 1 },
153
+ });
154
+ assert.deepEqual(parseWindowsHostFrame(`{"requestId":"list-2","ok":true,"result":{"records":[{"version":1,"sessionId":"session-a","endpoint":"${endpoint.replaceAll("\\", "\\\\")}","createdAt":1}]}}`), {
155
+ requestId: "list-2", ok: true, result: { records: [{ version: 1, sessionId: "session-a", endpoint, createdAt: 1 }] },
156
+ });
157
+ assert.throws(() => parseWindowsHostFrame(`{"requestId":"record-1","ok":true,"result":{"version":1,"sessionId":"session-a","endpoint":"${endpoint.replaceAll("\\", "\\\\")}","createdAt":1,"path":"C:\\\\private"}}`), /invalid Windows transport frame/);
158
+ });
159
+
160
+ test("Windows bridge rejects list elements with extra or malformed public fields", () => {
161
+ const endpoint = "\\\\.\\pipe\\gentle-pi-0123456789abcdef0123456789abcdef".replaceAll("\\", "\\\\");
162
+ for (const record of [
163
+ `{"version":1,"sessionId":"session-a","endpoint":"${endpoint}","createdAt":1,"extra":true}`,
164
+ `{"version":1,"sessionId":"session-a","endpoint":"${endpoint}","createdAt":true}`,
165
+ ]) assert.throws(() => parseWindowsHostFrame(`{"requestId":"list-1","ok":true,"result":{"records":[${record}]}}`), /invalid Windows transport frame/);
166
+ });
167
+
168
+ test("Windows bridge decodes a bounded base64 notification event before semantic acknowledgement", () => {
169
+ const wire = Buffer.from('{"version":1,"kind":"notification","id":"message-1","senderSessionId":"peer","recipientSessionId":"self","message":"hello"}\n').toString("base64");
170
+ assert.deepEqual(parseWindowsHostNotification(`{"event":"notification","connectionId":"c1","generation":1,"wire":"${wire}"}`), {
171
+ event: "notification", connectionId: "c1", generation: 1,
172
+ frame: { version: 1, kind: "notification", id: "message-1", senderSessionId: "peer", recipientSessionId: "self", message: "hello" },
173
+ });
174
+ for (const invalid of [
175
+ '{"event":"notification","connectionId":"c1","generation":1,"wire":"not-base64"}',
176
+ '{"event":"notification","connectionId":"c1","generation":0,"wire":"eA=="}',
177
+ '{"event":"notification","connectionId":"c1","generation":1,"wire":"eA==","path":"private"}',
178
+ ]) assert.throws(() => parseWindowsHostNotification(invalid), /invalid Windows transport frame/);
179
+ });
180
+
181
+ test("Windows bridge correlates callback acknowledgement and rejects pending RPCs when its host exits", async () => {
182
+ class FakeChild extends EventEmitter {
183
+ stdin = Object.assign(new EventEmitter(), { writable: true, write: (line: string, callback: (error?: Error) => void) => { this.lines.push(line); callback(); return true; }, end: () => {} });
184
+ stdout = new EventEmitter();
185
+ stderr = new EventEmitter();
186
+ exitCode: number | null = null;
187
+ signalCode: NodeJS.Signals | null = null;
188
+ lines: string[] = [];
189
+ kill() { return true; }
190
+ }
191
+ const child = new FakeChild();
192
+ const host = new WindowsSessionTransportHost({
193
+ runtimeScript: "C:\\runtime\\windows-session-transport.ps1",
194
+ spawnProcess: () => child as never,
195
+ callback: async (notification) => {
196
+ assert.equal(notification.id, "message-1");
197
+ return true;
198
+ },
199
+ });
200
+ const ready = host.start();
201
+ child.stdout.emit("data", Buffer.from('{"requestId":"start-1","ok":true,"result":{"state":"partial"}}\n'));
202
+ await ready;
203
+ const endpoint = "\\\\.\\pipe\\gentle-pi-0123456789abcdef0123456789abcdef";
204
+ const listening = host.listen("self", 1);
205
+ child.stdout.emit("data", Buffer.from(`{"requestId":"listen-2","ok":true,"result":{"version":1,"sessionId":"self","endpoint":"${endpoint.replaceAll("\\", "\\\\")}","createdAt":1}}\n`));
206
+ await listening;
207
+ const unsupported = host.request("list", {});
208
+ child.stdout.emit("data", Buffer.from('{"requestId":"list-3","ok":false,"error":"invalid"}\n'));
209
+ await assert.rejects(unsupported, /Windows transport request unavailable/);
210
+ const pending = host.request("list", {});
211
+ const wire = Buffer.from('{"version":1,"kind":"notification","id":"message-1","senderSessionId":"peer","recipientSessionId":"self","message":"hello"}\n').toString("base64");
212
+ child.stdout.emit("data", Buffer.from(`{"event":"notification","connectionId":"c1","generation":1,"wire":"${wire}"}\n`));
213
+ await new Promise((resolve) => setImmediate(resolve));
214
+ assert.match(child.lines.at(-1) ?? "", /"operation":"ack"/);
215
+ child.emit("exit", 1, null);
216
+ await assert.rejects(pending, /Windows transport host exited/);
217
+ });
218
+
219
+ test("Windows bridge keeps ACK capacity separate from ordinary RPCs and suppresses stale generations", async () => {
220
+ class FakeChild extends EventEmitter {
221
+ stdin = Object.assign(new EventEmitter(), { writable: true, write: (line: string, callback: (error?: Error) => void) => { this.lines.push(line); callback(); return true; }, end: () => {} });
222
+ stdout = new EventEmitter(); stderr = new EventEmitter(); exitCode: number | null = null; signalCode: NodeJS.Signals | null = null; lines: string[] = [];
223
+ kill() { return true; }
224
+ }
225
+ const child = new FakeChild();
226
+ let callbacks = 0;
227
+ const host = new WindowsSessionTransportHost({ spawnProcess: () => child as never, callback: async () => { callbacks++; return true; } });
228
+ const endpoint = "\\\\.\\pipe\\gentle-pi-0123456789abcdef0123456789abcdef";
229
+ const emit = (value: string) => child.stdout.emit("data", Buffer.from(`${value}\n`));
230
+ const starting = host.start();
231
+ emit('{"requestId":"start-1","ok":true,"result":{"state":"partial"}}');
232
+ await starting;
233
+ const listening = host.listen("recipient", 1);
234
+ emit(`{"requestId":"listen-2","ok":true,"result":{"version":1,"sessionId":"recipient","endpoint":"${endpoint.replaceAll("\\", "\\\\")}","createdAt":1}}`);
235
+ await listening;
236
+ const ordinary = Array.from({ length: 8 }, () => host.request("list", {}));
237
+ const wire = Buffer.from('{"version":1,"kind":"notification","id":"ack-capacity-1","senderSessionId":"sender","recipientSessionId":"recipient","message":"ok"}\n').toString("base64");
238
+ emit(`{"event":"notification","connectionId":"known-1","generation":1,"wire":"${wire}"}`);
239
+ await new Promise<void>((resolve) => setImmediate(resolve));
240
+ assert.equal(callbacks, 1);
241
+ assert.equal(child.lines.filter((line) => line.includes('"operation":"ack"')).length, 1, "ACK must bypass full ordinary RPC capacity");
242
+ emit(`{"event":"notification","connectionId":"stale-1","generation":2,"wire":"${wire}"}`);
243
+ await new Promise<void>((resolve) => setImmediate(resolve));
244
+ assert.equal(callbacks, 1, "a stale helper generation must not reach the callback");
245
+ assert.equal(child.lines.filter((line) => line.includes('"operation":"ack"')).length, 1, "a stale helper generation must not receive an ACK");
246
+ child.emit("exit", 1, null);
247
+ await Promise.all(ordinary.map((request) => assert.rejects(request, /Windows transport host exited/)));
248
+ });
249
+
250
+ test("Windows listener reserves a skipped local epoch and accepts the next early notification", async () => {
251
+ const child = new FakeTransportChild();
252
+ let callbacks = 0;
253
+ const host = new WindowsSessionTransportHost({ spawnProcess: () => child as never, callback: async () => { callbacks++; return true; } });
254
+ const emit = (line: string) => child.stdout.emit("data", Buffer.from(`${line}\n`));
255
+ const starting = host.start();
256
+ emit('{"requestId":"start-1","ok":true,"result":{"state":"partial"}}');
257
+ await starting;
258
+ const saturated = Array.from({ length: 8 }, () => host.request("list", {}));
259
+ await assert.rejects(host.listen("recipient", 1), /Windows transport request unavailable/);
260
+ for (let index = 0; index < 8; index++) emit(`{"requestId":"list-${index + 2}","ok":true,"result":{"records":[]}}`);
261
+ await Promise.all(saturated);
262
+ const listening = host.listen("recipient", 2);
263
+ assert.match(child.lines.at(-1) ?? "", /"operation":"listen".*"generation":2/);
264
+ emit(`{"event":"notification","connectionId":"early-1","generation":2,"wire":"${notificationWire("early-1")}"}`);
265
+ emit(`{"requestId":"listen-10","ok":true,"result":{"version":1,"sessionId":"recipient","endpoint":"${testEndpoint.replaceAll("\\", "\\\\")}","createdAt":2}}`);
266
+ await listening;
267
+ await nextTurn();
268
+ assert.equal(callbacks, 1, "the first accepted helper generation must receive an early notification and ACK");
269
+ assert.equal(child.lines.filter((line) => line.includes('"operation":"ack"')).length, 1);
270
+ child.emit("exit", 1, null);
271
+ });
272
+
273
+ test("Windows listener reserves a fresh epoch after helper busy or invalid rejection", async () => {
274
+ for (const rejection of ["busy", "invalid"] as const) {
275
+ const child = new FakeTransportChild();
276
+ const host = new WindowsSessionTransportHost({ spawnProcess: () => child as never });
277
+ const emit = (line: string) => child.stdout.emit("data", Buffer.from(`${line}\n`));
278
+ const starting = host.start();
279
+ emit('{"requestId":"start-1","ok":true,"result":{"state":"partial"}}');
280
+ await starting;
281
+ const rejected = host.listen("recipient", 1);
282
+ emit(`{"requestId":"listen-2","ok":false,"error":"${rejection}"}`);
283
+ await assert.rejects(rejected, /Windows transport request unavailable/);
284
+ const recovered = host.listen("recipient", 2);
285
+ assert.match(child.lines.at(-1) ?? "", /"operation":"listen".*"generation":2/);
286
+ emit(`{"requestId":"listen-3","ok":true,"result":{"version":1,"sessionId":"recipient","endpoint":"${testEndpoint.replaceAll("\\", "\\\\")}","createdAt":2}}`);
287
+ await recovered;
288
+ child.emit("exit", 1, null);
289
+ }
290
+ });
291
+
292
+ test("Windows listener ignores a late failed epoch while a replacement is pending and ACKs only that replacement", async () => {
293
+ const child = new FakeTransportChild();
294
+ let callbacks = 0;
295
+ const host = new WindowsSessionTransportHost({ spawnProcess: () => child as never, callback: async () => { callbacks++; return true; } });
296
+ const emit = (line: string) => child.stdout.emit("data", Buffer.from(`${line}\n`));
297
+ const ready = host.start(); emit('{"requestId":"start-1","ok":true,"result":{"state":"partial"}}'); await ready;
298
+ const failed = host.listen("recipient", 1);
299
+ emit('{"requestId":"listen-2","ok":false,"error":"unavailable"}');
300
+ await assert.rejects(failed, /Windows transport request unavailable/);
301
+ emit('{"event":"listener-failed","generation":1,"error":"unavailable"}');
302
+ const replacement = host.listen("recipient", 2);
303
+ assert.match(child.lines.at(-1) ?? "", /"generation":2/);
304
+ emit(`{"event":"notification","connectionId":"old-1","generation":1,"wire":"${notificationWire("old-1")}"}`);
305
+ emit('{"event":"listener-failed","generation":1,"error":"unavailable"}');
306
+ emit(`{"event":"notification","connectionId":"new-1","generation":2,"wire":"${notificationWire("new-1")}"}`);
307
+ emit(`{"requestId":"listen-3","ok":true,"result":{"version":1,"sessionId":"recipient","endpoint":"${testEndpoint.replaceAll("\\", "\\\\")}","createdAt":2}}`);
308
+ await replacement; await nextTurn();
309
+ assert.equal(callbacks, 1, "only the pending replacement epoch may deliver or ACK");
310
+ assert.equal(child.lines.filter((line) => line.includes('"operation":"ack"') && line.includes('"generation":2')).length, 1);
311
+ child.emit("exit", 1, null);
312
+ });
313
+
314
+ test("Windows client aborts during resolve, pre-connect, connect, and ACK without a late write or revived result", async () => {
315
+ const record = Object.freeze({ version: 1 as const, sessionId: "recipient", endpoint: testEndpoint, createdAt: 1 });
316
+ for (const stage of ["resolve", "beforeConnect", "connect", "ack"] as const) {
317
+ let release!: () => void;
318
+ const gate = new Promise<void>((resolve) => { release = resolve; });
319
+ const socket = new Socket();
320
+ socket.on("error", () => {});
321
+ let writes = 0;
322
+ (socket as unknown as { write: () => boolean }).write = () => { writes++; return true; };
323
+ const registry = { resolve: async () => { if (stage === "resolve") await gate; return record; } } as unknown as WindowsSessionPresenceRegistry;
324
+ const client = new WindowsActiveSessionClient(registry, "sender", { connect: () => socket });
325
+ const controller = new AbortController();
326
+ const pending = client.sendNotification("recipient", "cancel", {
327
+ signal: controller.signal,
328
+ beforeConnect: stage === "beforeConnect" ? async () => { await gate; return true; } : undefined,
329
+ });
330
+ if (stage === "resolve" || stage === "beforeConnect") { controller.abort(); release(); }
331
+ else {
332
+ await nextTurn();
333
+ if (stage === "connect") controller.abort();
334
+ else { socket.emit("connect"); await nextTurn(); controller.abort(); }
335
+ }
336
+ await assert.rejects(pending, (error: unknown) => error instanceof Error && (error as { code?: unknown }).code === "aborted");
337
+ socket.emit("connect"); socket.emit("end"); socket.emit("error", new Error("late"));
338
+ assert.equal(writes, stage === "ack" ? 1 : 0, `${stage} abort never permits a late write`);
339
+ assert.equal(client.pendingCount, 0);
340
+ }
341
+ });
342
+
343
+ test("Windows client rejects abort while resolve or beforeConnect remains gated", async () => {
344
+ const record = Object.freeze({ version: 1 as const, sessionId: "recipient", endpoint: testEndpoint, createdAt: 1 });
345
+ for (const phase of ["resolve", "beforeConnect"] as const) {
346
+ let release!: () => void, entered!: () => void, connects = 0;
347
+ const gate = new Promise<void>((resolve) => { release = resolve; });
348
+ const enteredGate = new Promise<void>((resolve) => { entered = resolve; });
349
+ const registry = { resolve: async () => { if (phase === "resolve") { entered(); await gate; } return record; } } as unknown as WindowsSessionPresenceRegistry;
350
+ const client = new WindowsActiveSessionClient(registry, "sender", { connect: () => { connects++; return new Socket(); } });
351
+ const controller = new AbortController();
352
+ const pending = client.sendNotification("recipient", "gated", { signal: controller.signal, beforeConnect: phase === "beforeConnect" ? async () => { entered(); await gate; return true; } : undefined });
353
+ await enteredGate;
354
+ controller.abort();
355
+ await assert.rejects(pending, (error: unknown) => error instanceof Error && (error as { code?: unknown }).code === "aborted");
356
+ assert.equal(client.pendingCount, 0, "abort settles before the gate opens");
357
+ assert.equal(connects, 0);
358
+ release(); await nextTurn();
359
+ assert.equal(connects, 0, "a released continuation cannot create a socket");
360
+ }
361
+ });
362
+
363
+ test("Windows client tears down only its owned listeners through registration, write, and registration-error races", async () => {
364
+ class FakeSocket extends EventEmitter {
365
+ writes = 0; destroys = 0; onWrite?: () => void;
366
+ write() { this.writes++; this.onWrite?.(); return true; }
367
+ destroy() { this.destroys++; return this; }
368
+ }
369
+ const record = Object.freeze({ version: 1 as const, sessionId: "recipient", endpoint: testEndpoint, createdAt: 1 });
370
+ for (const mode of ["registration", "write", "throw", "laterThrow"] as const) {
371
+ const socket = new FakeSocket(), controller = new AbortController();
372
+ const timers = new Set<() => void>();
373
+ const scheduler = { setTimeout: (callback: () => void) => (timers.add(callback), callback), clearTimeout: (callback: () => void) => { timers.delete(callback); } };
374
+ const externalData = () => {}, externalError = () => {};
375
+ socket.on("data", externalData); socket.on("error", externalError);
376
+ const originalOnce = socket.once.bind(socket) as (event: string, listener: (...args: unknown[]) => void) => FakeSocket;
377
+ if (mode === "registration") socket.once = ((event: string, listener: (...args: unknown[]) => void) => { const value = originalOnce(event, listener); if (event === "connect") controller.abort(); return value; }) as typeof socket.once;
378
+ if (mode === "throw") socket.once = (() => { throw new Error("registration failed"); }) as typeof socket.once;
379
+ if (mode === "laterThrow") socket.once = ((event: string, listener: (...args: unknown[]) => void) => { const value = originalOnce(event, listener); if (event === "end") throw new Error("later registration failed"); return value; }) as typeof socket.once;
380
+ if (mode === "write") socket.onWrite = () => controller.abort();
381
+ const registry = { resolve: async () => record } as unknown as WindowsSessionPresenceRegistry;
382
+ const client = new WindowsActiveSessionClient(registry, "sender", { connect: () => socket as never, scheduler });
383
+ const pending = client.sendNotification("recipient", "race", { signal: controller.signal });
384
+ if (mode === "write") { await nextTurn(); socket.emit("connect"); }
385
+ await assert.rejects(pending, (error: unknown) => error instanceof Error && (error as { code?: unknown }).code === ((mode === "throw" || mode === "laterThrow") ? "io_error" : "aborted"));
386
+ assert.equal(client.pendingCount, 0);
387
+ assert.equal(timers.size, 0, "settlement leaves no timer");
388
+ assert.equal(socket.listenerCount("connect"), 0, `${mode} removes its owned connect listener`);
389
+ assert.equal(socket.listenerCount("data"), 1, `${mode} preserves external data listeners`);
390
+ assert.equal(socket.listenerCount("end"), 0, `${mode} removes its owned end listener`);
391
+ assert.equal(socket.listenerCount("close"), 0, `${mode} removes its owned close listener`);
392
+ assert.equal(socket.listenerCount("error"), 1, `${mode} preserves external error listeners`);
393
+ socket.emit("error", new Error("late"));
394
+ assert.equal(socket.writes, mode === "write" ? 1 : 0);
395
+ }
396
+ });
397
+
398
+ test("Windows listener registers its callback before listen and cancels a start/close race", async () => {
399
+ const record = Object.freeze({ version: 1 as const, sessionId: "recipient", endpoint: "\\\\.\\pipe\\gentle-pi-0123456789abcdef0123456789abcdef", createdAt: 1 });
400
+ let registered: unknown;
401
+ let releaseStart!: () => void;
402
+ const startGate = new Promise<void>((resolve) => { releaseStart = resolve; });
403
+ let stopped: PresenceRecord | undefined;
404
+ const registry = {
405
+ setNotification(callback: unknown) { registered = callback; },
406
+ clearNotification() {},
407
+ setListenerFailure() {},
408
+ clearListenerFailure() {},
409
+ async startListener() { assert.ok(registered, "callback must be registered before listen"); await startGate; return record; },
410
+ async stopListener(value: PresenceRecord) { stopped = value; },
411
+ async close() {},
412
+ } as unknown as WindowsSessionPresenceRegistry;
413
+ const listener = new WindowsActiveSessionListener(registry, "recipient", async () => {});
414
+ const starting = listener.start();
415
+ await listener.close();
416
+ releaseStart();
417
+ await assert.rejects(starting, /listener is closed/);
418
+ assert.deepEqual(stopped, record);
419
+ assert.equal(listener.status, "closed");
420
+ });
421
+
422
+ test("Windows bridge isolates a malformed helper wire event and continues with the next valid notification", async () => {
423
+ class FakeChild extends EventEmitter {
424
+ stdin = Object.assign(new EventEmitter(), { writable: true, write: (line: string, callback: (error?: Error) => void) => { this.lines.push(line); callback(); return true; }, end: () => {} });
425
+ stdout = new EventEmitter(); stderr = new EventEmitter(); exitCode: number | null = null; signalCode: NodeJS.Signals | null = null; lines: string[] = []; killCalls = 0;
426
+ kill() { this.killCalls++; this.emit("close", 1, null); return true; }
427
+ }
428
+ const child = new FakeChild();
429
+ let callbacks = 0;
430
+ const host = new WindowsSessionTransportHost({ spawnProcess: () => child as never, callback: async () => { callbacks++; return true; } });
431
+ const endpoint = "\\\\.\\pipe\\gentle-pi-0123456789abcdef0123456789abcdef";
432
+ const emit = (line: string) => child.stdout.emit("data", Buffer.from(`${line}\n`));
433
+ const starting = host.start(); emit('{"requestId":"start-1","ok":true,"result":{"state":"partial"}}'); await starting;
434
+ const listening = host.listen("recipient", 1); emit(`{"requestId":"listen-2","ok":true,"result":{"version":1,"sessionId":"recipient","endpoint":"${endpoint.replaceAll("\\", "\\\\")}","createdAt":1}}`); await listening;
435
+ emit('{"event":"notification","connectionId":"bad-1","generation":1,"wire":"eA=="}');
436
+ const wire = Buffer.from('{"version":1,"kind":"notification","id":"after-bad-1","senderSessionId":"sender","recipientSessionId":"recipient","message":"ok"}\n').toString("base64");
437
+ emit(`{"event":"notification","connectionId":"good-1","generation":1,"wire":"${wire}"}`);
438
+ await new Promise<void>((resolve) => setImmediate(resolve));
439
+ assert.equal(callbacks, 1);
440
+ assert.equal(child.lines.filter((line) => line.includes('"operation":"ack"')).length, 1);
441
+ emit("{");
442
+ await host.close();
443
+ assert.equal(child.killCalls, 1, "close must settle an already-aborted owned child");
444
+ });
445
+
446
+ test("Windows bridge buffers private base64 control events per line without stopping its shared helper", async () => {
447
+ for (const fragmented of [false, true]) {
448
+ let callbacks = 0;
449
+ const { child, emit } = await createListeningFakeHost(async () => { callbacks++; return true; });
450
+ const malformed = notificationEvent(Buffer.alloc(65_536, 0x78).toString("base64"), `bad-${fragmented}`);
451
+ if (fragmented) {
452
+ const split = Math.floor(malformed.length / 2);
453
+ child.stdout.emit("data", Buffer.from(malformed.slice(0, split)));
454
+ child.stdout.emit("data", Buffer.from(`${malformed.slice(split)}\n`));
455
+ } else emit(malformed);
456
+ emit(notificationEvent(notificationWire(`after-malformed-${fragmented}`), `good-${fragmented}`));
457
+ await nextTurn();
458
+ assert.equal(callbacks, 1, "a malformed client wire must be isolated to its connection");
459
+ assert.equal(child.killCalls, 0, "a valid helper envelope must keep the shared helper alive");
460
+ child.emit("exit", 1, null);
461
+ }
462
+ });
463
+
464
+ test("Windows bridge accepts batched bounded private events beyond the public control-frame aggregate cap", async () => {
465
+ let callbacks = 0;
466
+ const { child } = await createListeningFakeHost(async () => { callbacks++; return true; });
467
+ const batch = `${notificationEvent(notificationWire("batch-1", "x".repeat(4_000)), "batch-connection-1")}\n${notificationEvent(notificationWire("batch-2", "x".repeat(4_000)), "batch-connection-2")}\n${notificationEvent(notificationWire("batch-3", "x".repeat(4_000)), "batch-connection-3")}\n`;
468
+ assert.ok(Buffer.byteLength(batch) > 16_384);
469
+ child.stdout.emit("data", Buffer.from(batch));
470
+ await nextTurn();
471
+ assert.equal(callbacks, 3);
472
+ assert.equal(child.killCalls, 0);
473
+ child.emit("exit", 1, null);
474
+ });
475
+
476
+ test("Windows bridge fail-closes oversized and malformed helper control frames", async () => {
477
+ for (const line of ["x".repeat(90_000), '{"event":"notification","connectionId":"c1","generation":1,"wire":"not-base64"}', '{"event":"notification","connectionId":"c1","generation":1,"wire":"eA==","extra":true}', '{"event":"listener-failed","generation":1,"error":"C:\\\\private"}']) {
478
+ const { child, emit } = await createListeningFakeHost(async () => true);
479
+ if (line.startsWith("x")) child.stdout.emit("data", Buffer.from(line)); else emit(line);
480
+ assert.equal(child.killCalls, 1, "invalid helper control input must close its owned child");
481
+ }
482
+ });
483
+
484
+ function createListenerBridge() {
485
+ const child = new FakeTransportChild();
486
+ const host = new WindowsSessionTransportHost({ spawnProcess: () => child as never });
487
+ const Registry = WindowsSessionPresenceRegistry as unknown as { new(host: WindowsSessionTransportHost): WindowsSessionPresenceRegistry };
488
+ const registry = new Registry(host);
489
+ const emit = (line: string) => child.stdout.emit("data", Buffer.from(`${line}\n`));
490
+ return { child, host, registry, emit };
491
+ }
492
+
493
+ async function startBridgeListener(bridge: ReturnType<typeof createListenerBridge>, listener: WindowsActiveSessionListener, requestId: string, createdAt: number) {
494
+ const starting = listener.start();
495
+ bridge.emit(`{"requestId":"${requestId}","ok":true,"result":{"version":1,"sessionId":"recipient","endpoint":"${testEndpoint.replaceAll("\\", "\\\\")}","createdAt":${createdAt}}}`);
496
+ await starting;
497
+ }
498
+
499
+ test("Windows listener invalidates only its current native generation and clears activation state", async () => {
500
+ const bridge = createListenerBridge();
501
+ const ready = bridge.host.start();
502
+ bridge.emit('{"requestId":"start-1","ok":true,"result":{"state":"partial"}}');
503
+ await ready;
504
+ const listener = new WindowsActiveSessionListener(bridge.registry, "recipient", async () => {});
505
+ await startBridgeListener(bridge, listener, "listen-2", 1);
506
+ assert.equal(listener.status, "active");
507
+ assert.ok(listener.record);
508
+ const pendingAck = bridge.host.request("ack", { connectionId: "connection-1", generation: 1, id: "ack-1", accepted: true });
509
+ const ackRejected = assert.rejects(pendingAck, /listener failed/);
510
+ bridge.emit('{"event":"listener-failed","generation":1,"error":"unavailable"}');
511
+ await ackRejected;
512
+ await nextTurn();
513
+ assert.equal(listener.status, "idle");
514
+ assert.equal(listener.record, undefined);
515
+ assert.deepEqual(listener.failure, { code: "io_error", message: "listener failed" });
516
+
517
+ await startBridgeListener(bridge, listener, "listen-4", 2);
518
+ bridge.emit('{"event":"listener-failed","generation":1,"error":"unavailable"}');
519
+ await nextTurn();
520
+ assert.equal(listener.status, "active", "a stale failure cannot invalidate a replacement listener");
521
+ assert.equal(listener.record?.createdAt, 2);
522
+ bridge.child.emit("exit", 1, null);
523
+ });
524
+
525
+ test("Windows listener failure during a listen reply race cannot reactivate it", async () => {
526
+ const bridge = createListenerBridge();
527
+ const ready = bridge.host.start();
528
+ bridge.emit('{"requestId":"start-1","ok":true,"result":{"state":"partial"}}');
529
+ await ready;
530
+ const listener = new WindowsActiveSessionListener(bridge.registry, "recipient", async () => {});
531
+ const starting = listener.start();
532
+ const rejected = assert.rejects(starting, /listener failed/);
533
+ bridge.emit('{"event":"listener-failed","generation":1,"error":"unavailable"}');
534
+ bridge.emit(`{"requestId":"listen-2","ok":true,"result":{"version":1,"sessionId":"recipient","endpoint":"${testEndpoint.replaceAll("\\", "\\\\")}","createdAt":1}}`);
535
+ await nextTurn();
536
+ bridge.emit('{"requestId":"stop-listener-3","ok":true,"result":{"state":"initialized","bootstrap":"complete"}}');
537
+ await rejected;
538
+ assert.equal(listener.status, "idle");
539
+ assert.equal(listener.record, undefined);
540
+ bridge.child.emit("exit", 1, null);
541
+ });
542
+
543
+ test("Windows listener stop revokes a matching pending epoch before its late listen reply", async () => {
544
+ const child = new FakeTransportChild();
545
+ let callbacks = 0, failures = 0;
546
+ const host = new WindowsSessionTransportHost({ spawnProcess: () => child as never, callback: async () => { callbacks++; return true; } });
547
+ host.setListenerFailure(() => { failures++; });
548
+ const emit = (line: string) => child.stdout.emit("data", Buffer.from(`${line}\n`));
549
+ const ready = host.start(); emit('{"requestId":"start-1","ok":true,"result":{"state":"partial"}}'); await ready;
550
+ const record = Object.freeze({ version: 1 as const, sessionId: "recipient", endpoint: testEndpoint, createdAt: 1 });
551
+ const pending = host.listen("recipient", 1);
552
+ const unrelated = host.stopListener(Object.freeze({ ...record, sessionId: "other" }));
553
+ emit('{"requestId":"stop-listener-3","ok":true,"result":{"state":"initialized","bootstrap":"complete"}}');
554
+ await unrelated;
555
+ emit(`{"event":"notification","connectionId":"unrelated-1","generation":1,"wire":"${notificationWire("unrelated-1")}"}`);
556
+ await nextTurn();
557
+ assert.equal(callbacks, 1, "a different identity cannot revoke the pending listener");
558
+ const stopping = host.stopListener(record);
559
+ emit(`{"event":"notification","connectionId":"stopped-1","generation":1,"wire":"${notificationWire("stopped-1")}"}`);
560
+ emit('{"event":"listener-failed","generation":1,"error":"unavailable"}');
561
+ emit('{"requestId":"stop-listener-5","ok":true,"result":{"state":"initialized","bootstrap":"complete"}}');
562
+ await stopping;
563
+ emit(`{"requestId":"listen-2","ok":true,"result":{"version":1,"sessionId":"recipient","endpoint":"${testEndpoint.replaceAll("\\", "\\\\")}","createdAt":1}}`);
564
+ await assert.rejects(pending, /Windows transport listener failed/);
565
+ await nextTurn();
566
+ assert.equal(callbacks, 1, "the revoked pending epoch cannot notify");
567
+ assert.equal(failures, 0, "the revoked pending epoch cannot fail the current listener");
568
+ const fresh = host.listen("recipient", 2);
569
+ assert.match(child.lines.at(-1) ?? "", /"operation":"listen".*"generation":2/);
570
+ emit(`{"requestId":"listen-6","ok":true,"result":{"version":1,"sessionId":"recipient","endpoint":"${testEndpoint.replaceAll("\\", "\\\\")}","createdAt":2}}`);
571
+ await fresh;
572
+ emit(`{"event":"notification","connectionId":"fresh-1","generation":2,"wire":"${notificationWire("fresh-1")}"}`);
573
+ await nextTurn();
574
+ assert.equal(callbacks, 2, "the fresh reserved epoch remains usable");
575
+ child.emit("exit", 1, null);
576
+ });
577
+
578
+ test("Windows helper exit fails a pending listener once before its listen reply", async () => {
579
+ const child = new FakeTransportChild();
580
+ let failures = 0;
581
+ const host = new WindowsSessionTransportHost({ spawnProcess: () => child as never });
582
+ host.setListenerFailure(() => { failures++; });
583
+ const emit = (line: string) => child.stdout.emit("data", Buffer.from(`${line}\n`));
584
+ const ready = host.start(); emit('{"requestId":"start-1","ok":true,"result":{"state":"partial"}}'); await ready;
585
+ const pending = host.listen("recipient", 1);
586
+ child.emit("exit", 1, null);
587
+ await assert.rejects(pending, /Windows transport host exited/);
588
+ assert.equal(failures, 1, "unexpected helper exit must report the owned pending generation once");
589
+ child.emit("close", 1, null);
590
+ });
591
+
592
+ test("Windows listener propagates unexpected helper exit or abort while normal close remains idempotent", async () => {
593
+ for (const failure of ["exit", "abort"] as const) {
594
+ const bridge = createListenerBridge();
595
+ const ready = bridge.host.start();
596
+ bridge.emit('{"requestId":"start-1","ok":true,"result":{"state":"partial"}}');
597
+ await ready;
598
+ const listener = new WindowsActiveSessionListener(bridge.registry, "recipient", async () => {});
599
+ await startBridgeListener(bridge, listener, "listen-2", 1);
600
+ if (failure === "exit") bridge.child.emit("exit", 1, null);
601
+ else bridge.child.stdout.emit("error", new Error("stdout failed"));
602
+ assert.equal(listener.status, "idle");
603
+ assert.equal(listener.record, undefined);
604
+ assert.deepEqual(listener.failure, { code: "io_error", message: "listener failed" });
605
+ await listener.close();
606
+ await listener.close();
607
+ assert.equal(listener.status, "closed");
608
+ }
609
+ });
610
+
611
+ test("Windows listener maps a shared host generation to its replacement object without settling its current ACK for an old failure", async () => {
612
+ const bridge = createListenerBridge();
613
+ const ready = bridge.host.start();
614
+ bridge.emit('{"requestId":"start-1","ok":true,"result":{"state":"partial"}}');
615
+ await ready;
616
+ const first = new WindowsActiveSessionListener(bridge.registry, "recipient", async () => {});
617
+ await startBridgeListener(bridge, first, "listen-2", 1);
618
+ const stopping = bridge.registry.stopListener(first.record!);
619
+ bridge.emit('{"requestId":"stop-listener-3","ok":true,"result":{"state":"initialized","bootstrap":"complete"}}');
620
+ await stopping;
621
+ const second = new WindowsActiveSessionListener(bridge.registry, "recipient", async () => {});
622
+ await startBridgeListener(bridge, second, "listen-4", 2);
623
+ const pendingAck = bridge.host.request("ack", { connectionId: "replacement-1", generation: 2, id: "replacement-ack-1", accepted: true });
624
+ let ackSettled = false;
625
+ void pendingAck.then(() => { ackSettled = true; }, () => { ackSettled = true; });
626
+ bridge.emit('{"event":"listener-failed","generation":1,"error":"unavailable"}');
627
+ await nextTurn();
628
+ assert.equal(second.status, "active");
629
+ assert.equal(ackSettled, false, "an old host failure must not settle the replacement ACK");
630
+ const ackRejected = assert.rejects(pendingAck, /listener failed/);
631
+ bridge.emit('{"event":"listener-failed","generation":2,"error":"unavailable"}');
632
+ await ackRejected;
633
+ assert.equal(second.status, "idle");
634
+ assert.equal(second.record, undefined);
635
+ bridge.child.emit("close", 0, null);
636
+ });
637
+
638
+ class CleanupFakeChild extends EventEmitter {
639
+ readonly stdin = Object.assign(new EventEmitter(), { writable: true, endCalls: 0, write: (_line: string, callback: (error?: Error) => void) => { callback(); return true; }, end: () => { this.stdin.endCalls++; } });
640
+ stdout = new EventEmitter();
641
+ stderr = new EventEmitter();
642
+ exitCode: number | null = null;
643
+ signalCode: NodeJS.Signals | null = null;
644
+ killCalls = 0;
645
+ closeObserved = false;
646
+ private readonly closeOnKill: boolean;
647
+ constructor(closeOnKill: boolean) {
648
+ super();
649
+ this.closeOnKill = closeOnKill;
650
+ this.on("close", () => { this.closeObserved = true; });
651
+ }
652
+ kill() {
653
+ this.killCalls++;
654
+ if (this.closeOnKill) this.emit("close", 1, null);
655
+ return true;
656
+ }
657
+ }
658
+
659
+ test("Windows host abort releases its owned child before close and bounds a missing child close", async () => {
660
+ for (const closeOnKill of [true, false]) {
661
+ const child = new CleanupFakeChild(closeOnKill);
662
+ const host = new WindowsSessionTransportHost({ spawnProcess: () => child as never });
663
+ const ready = host.start();
664
+ child.stdout.emit("data", Buffer.from('{"requestId":"start-1","ok":true,"result":{"state":"partial"}}\n'));
665
+ await ready;
666
+ child.stdout.emit("error", new Error("stdout failed"));
667
+ assert.equal(child.killCalls, 1, "abort must immediately terminate its owned child");
668
+ assert.equal(child.stdin.endCalls, 1, "abort must close owned input");
669
+ if (closeOnKill) {
670
+ await host.close();
671
+ assert.equal(child.closeObserved, true);
672
+ } else await assert.rejects(host.close(), /did not close/);
673
+ }
674
+ });
675
+
676
+ test("Windows host disposes only owned lifecycle listeners and retains late error guards until actual close", async () => {
677
+ const external = () => {};
678
+ for (const closeOnKill of [true, false]) {
679
+ const child = new CleanupFakeChild(closeOnKill);
680
+ child.on("error", external); child.on("close", external);
681
+ child.stdin.on("error", external); child.stdout.on("data", external); child.stdout.on("error", external); child.stderr.on("error", external);
682
+ const host = new WindowsSessionTransportHost({ spawnProcess: () => child as never });
683
+ const ready = host.start();
684
+ child.stdout.emit("data", Buffer.from('{"requestId":"start-1","ok":true,"result":{"state":"partial"}}\n'));
685
+ await ready;
686
+ child.stdout.emit("error", new Error("stdout failed"));
687
+ if (closeOnKill) {
688
+ await host.close();
689
+ assert.equal(child.listenerCount("error"), 1);
690
+ assert.equal(child.listenerCount("close"), 2, "fixture and external close listeners remain");
691
+ assert.equal(child.stdin.listenerCount("error"), 1);
692
+ assert.equal(child.stdout.listenerCount("data"), 1);
693
+ assert.equal(child.stdout.listenerCount("error"), 1);
694
+ assert.equal(child.stderr.listenerCount("error"), 1);
695
+ } else {
696
+ await assert.rejects(host.close(), /did not close/);
697
+ assert.equal(child.listenerCount("error"), 2, "late child error sink remains until close");
698
+ assert.equal(child.listenerCount("close"), 3, "fixture, external, and owned close listeners remain until close");
699
+ assert.equal(child.stdin.listenerCount("error"), 2);
700
+ assert.equal(child.stdout.listenerCount("data"), 1, "normal stdout data listener is disposed at timeout");
701
+ assert.equal(child.stdout.listenerCount("error"), 2);
702
+ assert.equal(child.stderr.listenerCount("error"), 2);
703
+ assert.doesNotThrow(() => { child.emit("error", new Error("late child")); child.stdin.emit("error", new Error("late stdin")); child.stdout.emit("error", new Error("late stdout")); child.stderr.emit("error", new Error("late stderr")); });
704
+ child.emit("close", 1, null);
705
+ assert.equal(child.listenerCount("error"), 1);
706
+ assert.equal(child.listenerCount("close"), 2);
707
+ assert.equal(child.stdin.listenerCount("error"), 1);
708
+ assert.equal(child.stdout.listenerCount("data"), 1);
709
+ assert.equal(child.stdout.listenerCount("error"), 1);
710
+ assert.equal(child.stderr.listenerCount("error"), 1);
711
+ }
712
+ }
713
+ });
714
+
715
+ test("Windows host cleanup handoff removes guards when child close reenters late-guard installation", async () => {
716
+ const child = new CleanupFakeChild(false);
717
+ let armed = false;
718
+ let closedDuringGuardInstall = false;
719
+ child.on("newListener", (event: string) => {
720
+ if (armed && !closedDuringGuardInstall && event === "error") {
721
+ closedDuringGuardInstall = true;
722
+ child.emit("close", 1, null);
723
+ }
724
+ });
725
+ const host = new WindowsSessionTransportHost({ spawnProcess: () => child as never });
726
+ const ready = host.start();
727
+ child.stdout.emit("data", Buffer.from('{"requestId":"start-1","ok":true,"result":{"state":"partial"}}\n'));
728
+ await ready;
729
+ armed = true;
730
+ child.stdout.emit("error", new Error("stdout failed"));
731
+ await host.close();
732
+ assert.equal(closedDuringGuardInstall, true);
733
+ assert.equal(child.listenerCount("error"), 0, "no child guard may be installed after its close");
734
+ assert.equal(child.stdin.listenerCount("error"), 0);
735
+ assert.equal(child.stdout.listenerCount("error"), 0);
736
+ assert.equal(child.stderr.listenerCount("error"), 0);
737
+ assert.equal(child.listenerCount("close"), 1, "only the fixture close observer remains");
738
+ });
739
+
740
+ test("Windows host late guards prevent unhandled errors without external error listeners until actual close", async () => {
741
+ const child = new CleanupFakeChild(false);
742
+ const host = new WindowsSessionTransportHost({ spawnProcess: () => child as never });
743
+ const ready = host.start();
744
+ child.stdout.emit("data", Buffer.from('{"requestId":"start-1","ok":true,"result":{"state":"partial"}}\n'));
745
+ await ready;
746
+ child.stdout.emit("error", new Error("stdout failed"));
747
+ await assert.rejects(host.close(), /did not close/);
748
+ assert.doesNotThrow(() => { child.emit("error", new Error("late child")); child.stdin.emit("error", new Error("late stdin")); child.stdout.emit("error", new Error("late stdout")); child.stderr.emit("error", new Error("late stderr")); });
749
+ child.emit("close", 1, null);
750
+ assert.equal(child.listenerCount("error"), 0);
751
+ assert.equal(child.stdin.listenerCount("error"), 0);
752
+ assert.equal(child.stdout.listenerCount("error"), 0);
753
+ assert.equal(child.stderr.listenerCount("error"), 0);
754
+ });