@neuralnomads/codenomad-dev 0.18.0-dev-20260716-40cee071 → 0.18.0-dev-20260720-4c50829d

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 (89) hide show
  1. package/dist/auth/manager.js +1 -1
  2. package/dist/filesystem/__tests__/search-cache.test.js +5 -5
  3. package/dist/filesystem/search-cache.js +1 -1
  4. package/dist/index.js +42 -0
  5. package/dist/index.test.js +40 -10
  6. package/dist/opencode-update/service.js +150 -0
  7. package/dist/opencode-update/service.test.js +107 -0
  8. package/dist/permissions/auto-accept-manager.js +165 -0
  9. package/dist/permissions/auto-accept-manager.test.js +208 -0
  10. package/dist/permissions/auto-accept-store.js +5 -2
  11. package/dist/permissions/opencode-yolo-metadata.js +81 -0
  12. package/dist/permissions/opencode-yolo-metadata.test.js +54 -0
  13. package/dist/server/__tests__/remote-proxy.test.js +64 -56
  14. package/dist/server/http-server.js +12 -15
  15. package/dist/server/remote-proxy.js +68 -98
  16. package/dist/server/routes/opencode-update.js +37 -0
  17. package/dist/server/routes/opencode-update.test.js +34 -0
  18. package/dist/server/routes/remote-servers.js +1 -1
  19. package/dist/server/routes/workspaces.test.js +38 -0
  20. package/dist/server/routes/worktrees.js +19 -0
  21. package/dist/server/routes/yolo.js +2 -1
  22. package/dist/server/tls.js +2 -2
  23. package/dist/settings/binaries.test.js +2 -7
  24. package/dist/shutdown.js +67 -59
  25. package/dist/shutdown.test.js +115 -78
  26. package/dist/ui/remote-ui.js +2 -2
  27. package/dist/usage/providers/api-key.js +3 -3
  28. package/dist/usage/providers/google.js +1 -1
  29. package/dist/usage/providers/oauth.js +1 -36
  30. package/dist/usage/providers/special.js +3 -3
  31. package/dist/usage/service.js +1 -1
  32. package/dist/usage/service.test.js +5 -3
  33. package/dist/usage/shared.js +4 -4
  34. package/dist/usage/shared.test.js +4 -4
  35. package/dist/workspaces/__tests__/spawn.test.js +2 -2
  36. package/dist/workspaces/__tests__/workspace-identity.test.js +94 -286
  37. package/dist/workspaces/binary-path.test.js +10 -0
  38. package/dist/workspaces/instance-client.js +4 -3
  39. package/dist/workspaces/launch-cleanup.test.js +21 -25
  40. package/dist/workspaces/manager.js +300 -351
  41. package/dist/workspaces/manager.test.js +209 -237
  42. package/dist/workspaces/process-identity.darwin.test.js +88 -0
  43. package/dist/workspaces/process-identity.js +172 -149
  44. package/dist/workspaces/process-identity.test.js +86 -140
  45. package/dist/workspaces/runtime.js +261 -373
  46. package/dist/workspaces/runtime.test.js +222 -547
  47. package/package.json +1 -1
  48. package/public/assets/{FilesTab-BjztpSdj.js → FilesTab-ChfE8-8N.js} +2 -2
  49. package/public/assets/{GitChangesTab-B9XraSoR.js → GitChangesTab-ntRtkxIF.js} +2 -2
  50. package/public/assets/{SplitFilePanel-Cu_5-Z-E.js → SplitFilePanel-Dav7VCqK.js} +1 -1
  51. package/public/assets/StatusTab-D3toOefp.js +1 -0
  52. package/public/assets/{align-justify-BKS9gyRk.js → align-justify-D91sAsLc.js} +1 -1
  53. package/public/assets/{bundle-full-CXGXXlzR.js → bundle-full-CWkY9-v4.js} +1 -1
  54. package/public/assets/{diff-viewer-CSNOYoEB.js → diff-viewer-Cn_41zpc.js} +1 -1
  55. package/public/assets/index-3dx-hhNQ.js +1 -0
  56. package/public/assets/index-BKwfZLzw.js +1 -0
  57. package/public/assets/index-C5IioEFT.js +1 -0
  58. package/public/assets/index-CZYc0VcU.js +2 -0
  59. package/public/assets/index-CdCoXYXP.js +1 -0
  60. package/public/assets/index-CwAVops2.js +1 -0
  61. package/public/assets/index-CzvCOwKc.js +1 -0
  62. package/public/assets/index-D-loOWPv.js +1 -0
  63. package/public/assets/{index-DVc4ls8c.js → index-DM1cpVNo.js} +1 -1
  64. package/public/assets/{index-sLe9hEfQ.js → index-DOprq6Gt.js} +1 -1
  65. package/public/assets/index-L7fX3W8k.css +1 -0
  66. package/public/assets/index-w2KdHEME.js +1 -0
  67. package/public/assets/{loading-CDmt9OJU.js → loading-BfoDcWcg.js} +1 -1
  68. package/public/assets/main-CaTCcck1.js +68 -0
  69. package/public/assets/{markdown-1ZLSxCFT.js → markdown-D33YI2g2.js} +3 -3
  70. package/public/assets/{monaco-viewer-D6JgH76W.js → monaco-viewer-DM35aobI.js} +4 -4
  71. package/public/assets/{tool-call-CyXb63LH.js → tool-call-tV85Jv2i.js} +3 -3
  72. package/public/assets/{unified-picker-DCCdSVwI.js → unified-picker-DSRT9GMh.js} +1 -1
  73. package/public/assets/{wrap-text-pNMdTIq3.js → wrap-text-Dmrn2DKo.js} +1 -1
  74. package/public/index.html +4 -4
  75. package/public/loading.html +4 -4
  76. package/public/sw.js +1 -1
  77. package/dist/workspaces/launch-cleanup.js +0 -132
  78. package/public/assets/StatusTab-D_IE79r-.js +0 -1
  79. package/public/assets/index-B3YjNCh4.js +0 -1
  80. package/public/assets/index-B3ZD2_SU.js +0 -1
  81. package/public/assets/index-Bh2ofaQo.js +0 -1
  82. package/public/assets/index-CM0c9xRZ.js +0 -1
  83. package/public/assets/index-CdDxouJl.js +0 -2
  84. package/public/assets/index-D06Y2cFq.js +0 -1
  85. package/public/assets/index-PmE51vPh.js +0 -1
  86. package/public/assets/index-Qj63IiSj.css +0 -1
  87. package/public/assets/index-VY_zwmsG.js +0 -1
  88. package/public/assets/index-qiiH7egV.js +0 -1
  89. package/public/assets/main-CTN6Wwz1.js +0 -68
@@ -2,8 +2,8 @@ import assert from "node:assert/strict";
2
2
  import { describe, it } from "node:test";
3
3
  import pino from "pino";
4
4
  import { EventBus } from "../events/bus";
5
- import { WorkspaceRuntimeIdentityCaptureError, WorkspaceRuntimeLaunchCancelledError, WorkspaceWindowsTreeCleanupIncompleteError, } from "./runtime";
6
- import { WorkspaceLaunchCancelledError, WorkspaceLaunchSettlementTimeoutError, WorkspaceManager, WorkspaceShutdownError, WorkspaceShutdownTimeoutError, } from "./manager";
5
+ import { WorkspaceWindowsTreeCleanupIncompleteError, } from "./runtime";
6
+ import { WorkspaceCleanupTimeoutError, WorkspaceLaunchCancelledError, WorkspaceLaunchTimeoutError, WorkspaceManager, WorkspaceShutdownError, } from "./manager";
7
7
  function deferred() {
8
8
  let resolve;
9
9
  let reject;
@@ -17,302 +17,274 @@ class ControlledRuntime {
17
17
  constructor() {
18
18
  this.launchResult = deferred();
19
19
  this.launchCalled = deferred();
20
- this.cancellation = deferred();
21
20
  this.active = new Set();
22
- this.stopCalls = [];
23
- this.launchCalls = 0;
21
+ this.stopCalls = 0;
24
22
  this.failStops = 0;
25
23
  this.launch = (options) => {
26
- this.launchCalls += 1;
27
24
  this.active.add(options.workspaceId);
25
+ this.onExit = options.onExit;
28
26
  this.launchCalled.resolve(options.workspaceId);
27
+ options.signal?.addEventListener("abort", () => this.launchResult.reject(options.signal?.reason), { once: true });
29
28
  return this.launchResult.promise;
30
29
  };
31
30
  this.stop = async (workspaceId) => {
32
- this.stopCalls.push(workspaceId);
33
- this.cancellation.resolve(new WorkspaceRuntimeLaunchCancelledError(workspaceId));
34
- if (this.failStops > 0) {
35
- this.failStops -= 1;
31
+ this.stopCalls += 1;
32
+ if (this.failStops-- > 0)
36
33
  throw new Error("controlled stop failure");
37
- }
38
34
  this.active.delete(workspaceId);
39
35
  };
40
36
  }
41
- resolveLaunch(workspaceId) {
37
+ resolveLaunch() {
42
38
  this.launchResult.resolve({
43
39
  pid: 1234,
44
40
  port: 4321,
45
41
  exitPromise: new Promise(() => undefined),
46
- cancellationPromise: this.cancellation.promise,
47
42
  getLastOutput: () => "",
48
43
  });
49
44
  }
50
45
  }
51
- function createHarness(managerOptions = {}) {
46
+ function createHarness(options = {}) {
52
47
  const eventBus = new EventBus();
53
48
  const runtime = new ControlledRuntime();
54
49
  const readiness = deferred();
55
50
  const started = [];
56
- let createdId = "";
57
- eventBus.on("workspace.created", (event) => {
58
- createdId = event.workspace.id;
59
- });
51
+ const stopped = [];
60
52
  eventBus.on("workspace.started", (event) => started.push(event.workspace.id));
53
+ eventBus.on("workspace.stopped", (event) => stopped.push(event.workspaceId));
61
54
  const manager = new WorkspaceManager({
62
55
  rootDir: process.cwd(),
63
56
  settings: { getOwner: () => ({}) },
64
- binaryResolver: {
65
- resolve: () => ({ path: "test-opencode", label: "test-opencode" }),
66
- },
57
+ binaryResolver: { resolve: () => ({ path: "test-opencode", label: "test-opencode" }) },
67
58
  eventBus,
68
59
  logger: pino({ level: "silent" }),
69
60
  getServerBaseUrl: () => "http://127.0.0.1:4000",
70
61
  runtime,
71
- ...managerOptions,
62
+ ...options,
72
63
  });
73
- manager.waitForWorkspaceReadiness = () => readiness.promise;
74
- return { manager, runtime, readiness, started, eventBus, getCreatedId: () => createdId };
64
+ manager.waitForWorkspaceReadiness = ({ signal }) => Promise.race([
65
+ readiness.promise,
66
+ new Promise((_resolve, reject) => {
67
+ const cancel = () => reject(signal?.reason);
68
+ signal?.addEventListener("abort", cancel, { once: true });
69
+ if (signal?.aborted)
70
+ cancel();
71
+ }),
72
+ ]);
73
+ return { manager, runtime, readiness, started, stopped };
75
74
  }
76
- describe("workspace manager launch cancellation", () => {
77
- it("keeps a launched workspace unpublished until runtime identity launch succeeds", async () => {
78
- const harness = createHarness();
79
- let deletion;
80
- harness.eventBus.on("workspace.created", (event) => {
81
- deletion = harness.manager.delete(event.workspace.id);
82
- });
83
- const creation = harness.manager.create(process.cwd());
84
- const workspaceId = await harness.runtime.launchCalled.promise;
85
- assert.equal(harness.getCreatedId(), "");
86
- assert.deepEqual(harness.manager.list(), []);
87
- assert.equal(harness.manager.getInstancePort(workspaceId), undefined);
88
- assert.equal(harness.manager.getInstanceAuthorizationHeader(workspaceId), undefined);
89
- harness.runtime.resolveLaunch(workspaceId);
90
- await assert.rejects(creation, WorkspaceLaunchCancelledError);
91
- await deletion;
92
- assert.equal(harness.runtime.launchCalls, 1);
93
- assert.equal(harness.runtime.active.size, 0);
94
- assert.deepEqual(harness.started, []);
95
- assert.deepEqual(harness.manager.list(), []);
96
- });
97
- it("stops a late launch and shares cleanup across concurrent deletes", async () => {
98
- const harness = createHarness();
99
- const creation = harness.manager.create(process.cwd());
100
- const workspaceId = await harness.runtime.launchCalled.promise;
101
- const firstDelete = harness.manager.delete(workspaceId);
102
- const secondDelete = harness.manager.delete(workspaceId);
103
- assert.strictEqual(firstDelete, secondDelete);
104
- assert.equal(harness.manager.get(workspaceId), undefined);
105
- harness.runtime.resolveLaunch(workspaceId);
106
- await assert.rejects(creation, WorkspaceLaunchCancelledError);
107
- await Promise.all([firstDelete, secondDelete]);
108
- assert.equal(harness.runtime.active.has(workspaceId), false);
109
- assert.deepEqual(harness.started, []);
110
- assert.equal(harness.manager.get(workspaceId), undefined);
111
- assert.deepEqual(harness.manager.list(), []);
112
- });
113
- it("does not publish or retain a workspace when mandatory identity launch fails", async () => {
114
- const events = [];
115
- const eventBus = new EventBus();
116
- eventBus.on("workspace.created", () => events.push("created"));
117
- eventBus.on("workspace.started", () => events.push("started"));
118
- eventBus.on("workspace.error", () => events.push("error"));
119
- const runtime = {
120
- launch: ((options) => Promise.reject(new WorkspaceRuntimeIdentityCaptureError(options.workspaceId, "probe unavailable"))),
121
- stop: (() => Promise.resolve()),
122
- };
123
- const manager = new WorkspaceManager({
124
- rootDir: process.cwd(),
125
- settings: { getOwner: () => ({}) },
126
- binaryResolver: { resolve: () => ({ path: "test-opencode", label: "test-opencode" }) },
127
- eventBus,
128
- logger: pino({ level: "silent" }),
129
- getServerBaseUrl: () => "http://127.0.0.1:4000",
130
- runtime,
131
- });
132
- await assert.rejects(manager.create(process.cwd()), WorkspaceRuntimeIdentityCaptureError);
133
- assert.deepEqual(events, []);
134
- assert.deepEqual(manager.list(), []);
135
- });
136
- it("retains an unpublished cleanup record when identity-failure cleanup cannot be proven", async () => {
137
- let workspaceId = "";
138
- let stopAttempts = 0;
139
- const runtime = {
140
- launch: ((options) => {
141
- workspaceId = options.workspaceId;
142
- return Promise.reject(new WorkspaceRuntimeIdentityCaptureError(options.workspaceId, "probe unavailable"));
143
- }),
144
- stop: (async () => {
145
- stopAttempts += 1;
146
- if (stopAttempts === 1)
147
- throw new Error("cleanup proof unavailable");
148
- }),
149
- };
150
- const manager = new WorkspaceManager({
151
- rootDir: process.cwd(),
152
- settings: { getOwner: () => ({}) },
153
- binaryResolver: { resolve: () => ({ path: "test-opencode", label: "test-opencode" }) },
154
- eventBus: new EventBus(),
155
- logger: pino({ level: "silent" }),
156
- getServerBaseUrl: () => "http://127.0.0.1:4000",
157
- runtime,
75
+ async function createReady(harness) {
76
+ const creation = harness.manager.create(process.cwd());
77
+ const workspaceId = await harness.runtime.launchCalled.promise;
78
+ harness.runtime.resolveLaunch();
79
+ harness.readiness.resolve(undefined);
80
+ await creation;
81
+ return workspaceId;
82
+ }
83
+ describe("workspace manager lifecycle", () => {
84
+ for (const boundary of ["launch", "readiness", "shutdown"]) {
85
+ it(`cancels and cleans a workspace during ${boundary}`, async () => {
86
+ const harness = createHarness();
87
+ const creation = harness.manager.create(process.cwd());
88
+ const workspaceId = await harness.runtime.launchCalled.promise;
89
+ let cleanup;
90
+ if (boundary === "readiness") {
91
+ harness.runtime.resolveLaunch();
92
+ await new Promise((resolve) => setImmediate(resolve));
93
+ cleanup = harness.manager.delete(workspaceId);
94
+ }
95
+ else {
96
+ cleanup = boundary === "shutdown" ? harness.manager.shutdown() : harness.manager.delete(workspaceId);
97
+ harness.runtime.resolveLaunch();
98
+ }
99
+ await assert.rejects(creation, WorkspaceLaunchCancelledError);
100
+ await cleanup;
101
+ assert.deepEqual([harness.runtime.active.size, harness.started, harness.manager.list(), harness.stopped], [0, [], [], boundary === "readiness" ? [workspaceId] : []]);
158
102
  });
159
- await assert.rejects(manager.create(process.cwd()), /cleanup proof unavailable/);
160
- assert.deepEqual(manager.list(), []);
161
- assert.equal(manager.get(workspaceId), undefined);
162
- await manager.delete(workspaceId);
163
- assert.equal(stopAttempts, 3);
164
- });
165
- it("actively cancels pending readiness without publishing workspace.started", async () => {
103
+ }
104
+ it("shares failed cleanup and allows a later delete retry", async () => {
166
105
  const harness = createHarness();
167
- const creation = harness.manager.create(process.cwd());
168
- const workspaceId = await harness.runtime.launchCalled.promise;
169
- harness.runtime.resolveLaunch(workspaceId);
170
- await new Promise((resolve) => setImmediate(resolve));
171
- const deletion = harness.manager.delete(workspaceId);
172
- await assert.rejects(creation, WorkspaceLaunchCancelledError);
173
- await deletion;
106
+ const workspaceId = await createReady(harness);
107
+ harness.runtime.failStops = 2;
108
+ const first = harness.manager.delete(workspaceId);
109
+ const concurrent = harness.manager.delete(workspaceId);
110
+ assert.strictEqual(first, concurrent);
111
+ const failures = await Promise.allSettled([first, concurrent]);
112
+ assert.deepEqual(failures.map((result) => result.status), ["rejected", "rejected"]);
113
+ assert.equal(harness.runtime.active.has(workspaceId), true);
114
+ await harness.manager.delete(workspaceId);
174
115
  assert.equal(harness.runtime.active.has(workspaceId), false);
175
- assert.deepEqual(harness.started, []);
176
116
  assert.equal(harness.manager.get(workspaceId), undefined);
177
117
  });
178
- it("cancels and cleans a starting child during shutdown", async () => {
118
+ it("retries cancellation deletion for an already-cancelled request", async () => {
179
119
  const harness = createHarness();
180
- const creation = harness.manager.create(process.cwd());
120
+ const creation = harness.manager.create(process.cwd(), undefined, { requestId: "retry-cancel" });
181
121
  const workspaceId = await harness.runtime.launchCalled.promise;
182
- const shutdown = harness.manager.shutdown();
183
- harness.runtime.resolveLaunch(workspaceId);
184
- await assert.rejects(creation, WorkspaceLaunchCancelledError);
185
- await shutdown;
186
- assert.equal(harness.runtime.active.size, 0);
187
- assert.deepEqual(harness.started, []);
188
- assert.deepEqual(harness.manager.list(), []);
189
- });
190
- it("still starts and deletes an ordinary ready workspace", async () => {
191
- const harness = createHarness();
192
- const creation = harness.manager.create(process.cwd(), "ordinary");
193
- const workspaceId = await harness.runtime.launchCalled.promise;
194
- harness.runtime.resolveLaunch(workspaceId);
195
- harness.readiness.resolve("2.0.0");
196
- const { workspace } = await creation;
197
- assert.equal(workspace.id, harness.getCreatedId());
198
- assert.equal(workspace.status, "ready");
199
- assert.equal(workspace.binaryVersion, "2.0.0");
200
- assert.deepEqual(harness.started, [workspaceId]);
122
+ harness.runtime.resolveLaunch();
123
+ harness.readiness.resolve(undefined);
124
+ await creation;
125
+ harness.runtime.failStops = 2;
126
+ await assert.rejects(harness.manager.cancelCreationRequest("retry-cancel"), /controlled stop failure/);
127
+ assert.equal(harness.manager.get(workspaceId)?.id, workspaceId);
201
128
  assert.equal(harness.runtime.active.has(workspaceId), true);
202
- await harness.manager.delete(workspaceId);
203
- assert.equal(harness.runtime.active.has(workspaceId), false);
129
+ await harness.manager.cancelCreationRequest("retry-cancel");
204
130
  assert.equal(harness.manager.get(workspaceId), undefined);
131
+ assert.equal(harness.runtime.active.has(workspaceId), false);
132
+ assert.deepEqual(harness.stopped, [workspaceId]);
133
+ });
134
+ it("gives release and cancellation one terminal winner", async () => {
135
+ const cancelled = createHarness();
136
+ const cancelledCreation = cancelled.manager.create(process.cwd(), undefined, { requestId: "cancel-wins" });
137
+ const cancelledId = await cancelled.runtime.launchCalled.promise;
138
+ cancelled.runtime.resolveLaunch();
139
+ cancelled.readiness.resolve(undefined);
140
+ await cancelledCreation;
141
+ const stopStarted = deferred();
142
+ const finishStop = deferred();
143
+ const originalStop = cancelled.runtime.stop;
144
+ cancelled.runtime.stop = async (workspaceId) => {
145
+ stopStarted.resolve();
146
+ await finishStop.promise;
147
+ await originalStop(workspaceId);
148
+ };
149
+ const cancellation = cancelled.manager.cancelCreationRequest("cancel-wins");
150
+ await stopStarted.promise;
151
+ assert.equal(cancelled.manager.releaseCreationRequest(cancelledId, "cancel-wins"), false);
152
+ assert.equal(cancelled.manager.get(cancelledId)?.id, cancelledId);
153
+ finishStop.resolve();
154
+ await cancellation;
155
+ assert.equal(cancelled.manager.get(cancelledId), undefined);
156
+ const released = createHarness();
157
+ const releasedCreation = released.manager.create(process.cwd(), undefined, { requestId: "release-wins" });
158
+ const releasedId = await released.runtime.launchCalled.promise;
159
+ released.runtime.resolveLaunch();
160
+ released.readiness.resolve(undefined);
161
+ await releasedCreation;
162
+ assert.equal(released.manager.releaseCreationRequest(releasedId, "release-wins"), true);
163
+ await released.manager.cancelCreationRequest("release-wins");
164
+ assert.equal(released.manager.releaseCreationRequest(releasedId, "release-wins"), true);
165
+ assert.equal(released.manager.get(releasedId)?.id, releasedId);
166
+ assert.equal(released.runtime.active.has(releasedId), true);
205
167
  });
206
- it("publishes stopped and removes a workspace after readiness fails", async () => {
168
+ it("retains unresolved pre-creation cancellation until its delayed create", async () => {
207
169
  const harness = createHarness();
208
- const stopped = [];
209
- harness.eventBus.on("workspace.stopped", (event) => stopped.push(event.workspaceId));
210
- const creation = harness.manager.create(process.cwd());
211
- const workspaceId = await harness.runtime.launchCalled.promise;
212
- harness.runtime.resolveLaunch(workspaceId);
213
- await new Promise((resolve) => setImmediate(resolve));
214
- harness.readiness.reject(new Error("readiness failed"));
215
- await assert.rejects(creation, /readiness failed/);
216
- assert.deepEqual(stopped, [workspaceId]);
217
- assert.equal(harness.manager.get(workspaceId), undefined);
218
- assert.deepEqual(harness.manager.list(), []);
170
+ const requestIds = Array.from({ length: 1_025 }, (_, index) => `pending-cancel-${index}`);
171
+ await Promise.all(requestIds.map((requestId) => harness.manager.cancelCreationRequest(requestId)));
172
+ assert.equal(harness.manager.cancelledCreationRequests.size, requestIds.length);
173
+ await assert.rejects(harness.manager.create(process.cwd(), undefined, { requestId: requestIds[0] }), /was cancelled/);
174
+ assert.equal(harness.manager.cancelledCreationRequests.has(requestIds[0]), false);
175
+ assert.equal(harness.manager.cancelledCreationRequests.size, requestIds.length - 1);
219
176
  });
220
- it("retains a cancelled record after failed cleanup and allows delete retry", async () => {
177
+ it("returns scoped correlation while an ordinary shared launch remains retained", async () => {
221
178
  const harness = createHarness();
222
- const creation = harness.manager.create(process.cwd());
179
+ const ordinary = harness.manager.create(process.cwd());
223
180
  const workspaceId = await harness.runtime.launchCalled.promise;
224
- harness.runtime.resolveLaunch(workspaceId);
181
+ const scoped = harness.manager.create(process.cwd(), undefined, { requestId: "restore-shared" });
182
+ harness.runtime.resolveLaunch();
225
183
  harness.readiness.resolve(undefined);
226
- await creation;
227
- harness.runtime.failStops = 1;
228
- const firstDelete = harness.manager.delete(workspaceId);
229
- const concurrentDelete = harness.manager.delete(workspaceId);
230
- assert.strictEqual(firstDelete, concurrentDelete);
231
- const failedDeletes = await Promise.allSettled([firstDelete, concurrentDelete]);
232
- assert.equal(failedDeletes[0].status, "rejected");
233
- assert.equal(failedDeletes[1].status, "rejected");
234
- if (failedDeletes[0].status === "rejected" && failedDeletes[1].status === "rejected") {
235
- assert.strictEqual(failedDeletes[0].reason, failedDeletes[1].reason);
236
- assert.match(String(failedDeletes[0].reason), /controlled stop failure/);
237
- }
238
- assert.equal(harness.manager.get(workspaceId)?.status, "ready");
184
+ const [ordinaryResult, scopedResult] = await Promise.all([ordinary, scoped]);
185
+ assert.equal(ordinaryResult.created, true);
186
+ assert.equal(ordinaryResult.workspace.requestId, undefined);
187
+ assert.equal(scopedResult.created, false);
188
+ assert.equal(scopedResult.workspace.id, workspaceId);
189
+ assert.equal(scopedResult.workspace.requestId, "restore-shared");
190
+ assert.equal(harness.manager.releaseCreationRequest(workspaceId, "restore-shared"), true);
191
+ assert.equal(harness.manager.get(workspaceId)?.id, workspaceId);
239
192
  assert.equal(harness.runtime.active.has(workspaceId), true);
240
- const retryDelete = harness.manager.delete(workspaceId);
241
- assert.notStrictEqual(retryDelete, firstDelete);
242
- await retryDelete;
243
- assert.equal(harness.runtime.active.has(workspaceId), false);
244
- assert.equal(harness.manager.get(workspaceId), undefined);
193
+ const reused = await harness.manager.create(process.cwd(), undefined, { requestId: "restore-reused" });
194
+ assert.equal(reused.workspace.requestId, "restore-reused");
195
+ await harness.manager.cancelCreationRequest("restore-reused");
196
+ assert.equal(harness.manager.get(workspaceId)?.id, workspaceId);
197
+ assert.equal(harness.runtime.active.has(workspaceId), true);
198
+ await assert.rejects(harness.manager.create(process.cwd(), undefined, { requestId: "restore-reused" }), /was cancelled/);
199
+ assert.equal(harness.manager.releaseCreationRequest(workspaceId, "restore-reused"), false);
245
200
  });
246
- it("rejects shutdown at its deadline instead of waiting forever", async () => {
201
+ for (const boundary of ["runtime launch", "health readiness"]) {
202
+ it(`applies one shared end-to-end deadline during ${boundary} and cleans up`, async () => {
203
+ const deadlines = [];
204
+ const harness = createHarness({
205
+ launchTimeoutMs: 25,
206
+ setTimeout: ((callback) => {
207
+ const timer = { active: true };
208
+ deadlines.push(() => { if (timer.active)
209
+ callback(); });
210
+ return timer;
211
+ }),
212
+ clearTimeout: ((timer) => { timer.active = false; }),
213
+ });
214
+ const first = harness.manager.create(process.cwd(), undefined, { requestId: "deadline-one" });
215
+ const workspaceId = await harness.runtime.launchCalled.promise;
216
+ const shared = harness.manager.create(process.cwd(), undefined, { requestId: "deadline-two" });
217
+ while ([...harness.manager.pendingWorkspaceCreations.values()][0]?.ownership.size !== 2) {
218
+ await new Promise((resolve) => setImmediate(resolve));
219
+ }
220
+ if (boundary === "health readiness") {
221
+ harness.runtime.resolveLaunch();
222
+ await new Promise((resolve) => setImmediate(resolve));
223
+ }
224
+ for (const fire of deadlines)
225
+ fire();
226
+ const outcomes = await Promise.allSettled([first, shared]);
227
+ assert.deepEqual(outcomes.map((outcome) => outcome.status), ["rejected", "rejected"]);
228
+ assert.ok(outcomes.every((outcome) => outcome.status === "rejected" && outcome.reason instanceof WorkspaceLaunchTimeoutError));
229
+ assert.strictEqual(outcomes[0].reason, outcomes[1].reason);
230
+ assert.equal(harness.runtime.active.has(workspaceId), false);
231
+ assert.equal(harness.runtime.stopCalls >= 1, true);
232
+ assert.deepEqual(harness.manager.list(), []);
233
+ });
234
+ }
235
+ it("bounds shutdown instead of waiting forever", async () => {
247
236
  let fireDeadline;
248
- const timer = {};
237
+ let cleared = 0;
249
238
  const harness = createHarness({
250
239
  shutdownTimeoutMs: 25,
251
- setTimeout: (callback) => {
240
+ setTimeout: ((callback) => {
252
241
  fireDeadline = callback;
253
- return timer;
254
- },
255
- clearTimeout: () => undefined,
242
+ return {};
243
+ }),
244
+ clearTimeout: () => { cleared += 1; },
256
245
  });
257
- const creation = harness.manager.create(process.cwd());
258
- const workspaceId = await harness.runtime.launchCalled.promise;
259
- harness.runtime.resolveLaunch(workspaceId);
260
- harness.readiness.resolve(undefined);
261
- await creation;
246
+ const workspaceId = await createReady(harness);
247
+ cleared = 0;
262
248
  harness.runtime.stop = () => new Promise(() => undefined);
263
249
  const shutdown = harness.manager.shutdown();
264
- assert.ok(fireDeadline);
265
250
  fireDeadline();
266
- await assert.rejects(shutdown, WorkspaceShutdownTimeoutError);
267
- assert.equal(harness.manager.get(workspaceId)?.status, "ready");
268
- });
269
- it("rejects failed shutdown cleanup and allows a later delete retry", async () => {
270
- const harness = createHarness();
271
- const creation = harness.manager.create(process.cwd());
272
- const workspaceId = await harness.runtime.launchCalled.promise;
273
- harness.runtime.resolveLaunch(workspaceId);
274
- harness.readiness.resolve(undefined);
275
- await creation;
276
- harness.runtime.failStops = 1;
277
- await assert.rejects(harness.manager.shutdown(), WorkspaceShutdownError);
251
+ await assert.rejects(shutdown, WorkspaceCleanupTimeoutError);
278
252
  assert.equal(harness.manager.get(workspaceId)?.status, "ready");
279
- assert.equal(harness.runtime.active.has(workspaceId), true);
280
- await harness.manager.delete(workspaceId);
281
- assert.equal(harness.manager.get(workspaceId), undefined);
282
- assert.equal(harness.runtime.active.has(workspaceId), false);
253
+ assert.equal(cleared, 1);
283
254
  });
284
- it("reports incomplete Windows tree cleanup during shutdown", async () => {
285
- const harness = createHarness();
286
- const creation = harness.manager.create(process.cwd());
287
- const workspaceId = await harness.runtime.launchCalled.promise;
288
- harness.runtime.resolveLaunch(workspaceId);
289
- harness.readiness.resolve(undefined);
290
- await creation;
291
- harness.runtime.stop = async () => {
292
- throw new WorkspaceWindowsTreeCleanupIncompleteError(workspaceId, 4242, ["taskkill /T failed: unavailable"]);
293
- };
294
- await assert.rejects(harness.manager.shutdown(), (error) => {
295
- assert.ok(error instanceof WorkspaceShutdownError);
296
- assert.ok(error.errors[0] instanceof WorkspaceWindowsTreeCleanupIncompleteError);
297
- return true;
298
- });
299
- assert.equal(harness.manager.get(workspaceId)?.status, "ready");
300
- assert.equal(harness.runtime.active.has(workspaceId), true);
255
+ it("publishes stopped exactly once for normal exit, readiness failure, and manager cleanup", async () => {
256
+ const normal = createHarness();
257
+ const normalId = await createReady(normal);
258
+ normal.runtime.onExit?.({ workspaceId: normalId, code: 0, signal: null, requested: false });
259
+ await normal.manager.delete(normalId);
260
+ assert.deepEqual(normal.stopped, [normalId]);
261
+ const failed = createHarness();
262
+ const failedCreation = failed.manager.create(process.cwd());
263
+ const failedId = await failed.runtime.launchCalled.promise;
264
+ failed.runtime.resolveLaunch();
265
+ failed.readiness.reject(new Error("not ready"));
266
+ await assert.rejects(failedCreation, /not ready/);
267
+ assert.deepEqual(failed.stopped, [failedId]);
268
+ const cleaned = createHarness();
269
+ const cleanedId = await createReady(cleaned);
270
+ await cleaned.manager.shutdown();
271
+ assert.deepEqual(cleaned.stopped, [cleanedId]);
301
272
  });
302
- it("bounds cleanup when a runtime does not settle its cancelled launch", async () => {
303
- let fireDeadline;
304
- const timer = {};
305
- const harness = createHarness({
306
- launchSettlementTimeoutMs: 25,
307
- setTimeout: (callback) => {
308
- fireDeadline = callback;
309
- return timer;
310
- },
311
- clearTimeout: () => undefined,
273
+ for (const [name, failure] of [
274
+ ["cleanup failures", new Error("stop failed")],
275
+ ["incomplete Windows tree cleanup", new WorkspaceWindowsTreeCleanupIncompleteError("workspace", 4242, ["taskkill failed"])],
276
+ ]) {
277
+ it(`aggregates ${name} during shutdown`, async () => {
278
+ const harness = createHarness();
279
+ const workspaceId = await createReady(harness);
280
+ harness.runtime.stop = async () => { throw failure; };
281
+ await assert.rejects(harness.manager.shutdown(), (error) => {
282
+ assert.ok(error instanceof WorkspaceShutdownError);
283
+ assert.strictEqual(error.errors[0], failure);
284
+ return true;
285
+ });
286
+ assert.equal(harness.manager.get(workspaceId)?.status, "ready");
287
+ assert.equal(harness.runtime.active.has(workspaceId), true);
312
288
  });
313
- const deletion = harness.manager.withLaunchSettlementTimeout("workspace-1", new Promise(() => undefined));
314
- assert.ok(fireDeadline);
315
- fireDeadline();
316
- await assert.rejects(deletion, WorkspaceLaunchSettlementTimeoutError);
317
- });
289
+ }
318
290
  });
@@ -0,0 +1,88 @@
1
+ import assert from "node:assert/strict";
2
+ import { spawn, spawnSync } from "node:child_process";
3
+ import { once } from "node:events";
4
+ import { setTimeout as delay } from "node:timers/promises";
5
+ import { it } from "node:test";
6
+ import { LAUNCH_CLEANUP_TOKEN_ENV, probePosixProcesses, signalOwnedPosixProcessGroup, signalPosixProcesses, } from "./process-identity";
7
+ const darwinOnly = { skip: process.platform !== "darwin", timeout: 10_000 };
8
+ async function spawnDetachedGroup(cleanupToken) {
9
+ const leader = spawn(process.execPath, ["-e", `
10
+ const { spawn } = require("node:child_process")
11
+ spawn(process.execPath, ["-e", "setInterval(() => {}, 1000)"], { stdio: "ignore" })
12
+ process.stdout.write("ready\\n")
13
+ setInterval(() => {}, 1000)
14
+ `], {
15
+ detached: true,
16
+ stdio: ["ignore", "pipe", "ignore"],
17
+ env: { ...process.env, ...(cleanupToken ? { [LAUNCH_CLEANUP_TOKEN_ENV]: cleanupToken } : {}) },
18
+ });
19
+ assert.ok(leader.pid);
20
+ await once(leader.stdout, "data");
21
+ return leader;
22
+ }
23
+ async function assertGroupGone(groupId) {
24
+ for (let attempt = 0; attempt < 20; attempt += 1) {
25
+ const remaining = probePosixProcesses(spawnSync, 1_000, "darwin", { groupId });
26
+ if (remaining.ok && remaining.processes.size === 0)
27
+ return;
28
+ await delay(50);
29
+ }
30
+ assert.fail("owned Darwin process group remained alive after signaling");
31
+ }
32
+ it("uses real Darwin ps identities to stop an owned detached process group", darwinOnly, async () => {
33
+ const leader = await spawnDetachedGroup();
34
+ try {
35
+ const snapshot = probePosixProcesses(spawnSync, 1_000, "darwin", {
36
+ pids: [leader.pid],
37
+ groupId: leader.pid,
38
+ });
39
+ assert.equal(snapshot.ok, true);
40
+ assert.equal(snapshot.ok && snapshot.processes.get(leader.pid)?.groupId, leader.pid);
41
+ assert.equal(snapshot.ok && snapshot.processes.size >= 2, true);
42
+ const signaled = signalOwnedPosixProcessGroup(spawnSync, leader.pid, "SIGTERM", 1_000);
43
+ assert.equal(signaled.ok && signaled.matched, true);
44
+ assert.equal(signaled.ok && signaled.signalSent, true);
45
+ await assertGroupGone(leader.pid);
46
+ }
47
+ finally {
48
+ try {
49
+ process.kill(-leader.pid, "SIGKILL");
50
+ }
51
+ catch {
52
+ // The successful path has already removed the process group.
53
+ }
54
+ }
55
+ });
56
+ it("uses a retained real Darwin identity anchor after the group leader exits", darwinOnly, async () => {
57
+ const cleanupToken = "darwin-integration-cleanup-token";
58
+ const leader = await spawnDetachedGroup(cleanupToken);
59
+ try {
60
+ const snapshot = probePosixProcesses(spawnSync, 1_000, "darwin", { groupId: leader.pid });
61
+ assert.equal(snapshot.ok, true);
62
+ const leaderIdentity = snapshot.ok ? snapshot.processes.get(leader.pid) : undefined;
63
+ assert.ok(leaderIdentity);
64
+ assert.equal(snapshot.ok && snapshot.processes.size >= 2, true);
65
+ leader.kill("SIGTERM");
66
+ if (leader.exitCode === null)
67
+ await once(leader, "exit");
68
+ const signaled = signalPosixProcesses(spawnSync, {
69
+ leader: leaderIdentity,
70
+ groupId: leader.pid,
71
+ members: [],
72
+ signal: "SIGTERM",
73
+ allowLeaderlessGroup: true,
74
+ cleanupToken,
75
+ }, 1_000, "darwin");
76
+ assert.equal(signaled.ok && signaled.matched, true);
77
+ assert.equal(signaled.ok && signaled.signalSent, true);
78
+ await assertGroupGone(leader.pid);
79
+ }
80
+ finally {
81
+ try {
82
+ process.kill(-leader.pid, "SIGKILL");
83
+ }
84
+ catch {
85
+ // The successful path has already removed the process group.
86
+ }
87
+ }
88
+ });