@kodelyth/acpx 2026.5.42 → 2026.6.2

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.
@@ -1,330 +0,0 @@
1
- import path from "node:path";
2
- import { describe, expect, it, vi } from "vitest";
3
- import { KLAW_ACPX_LEASE_ID_ARG, KLAW_GATEWAY_INSTANCE_ID_ARG } from "./process-lease.js";
4
- import {
5
- cleanupKlawOwnedAcpxProcessTree,
6
- isKlawLeaseAwareAcpxProcessCommand,
7
- isKlawOwnedAcpxProcessCommand,
8
- reapStaleKlawOwnedAcpxOrphans,
9
- type AcpxProcessInfo,
10
- } from "./process-reaper.js";
11
-
12
- const WRAPPER_ROOT = "/tmp/klaw-state/acpx";
13
- const CODEX_WRAPPER_COMMAND = `node ${WRAPPER_ROOT}/codex-acp-wrapper.mjs`;
14
- const CODEX_WRAPPER_COMMAND_WITH_LEASE = `${CODEX_WRAPPER_COMMAND} ${KLAW_ACPX_LEASE_ID_ARG} lease-1 ${KLAW_GATEWAY_INSTANCE_ID_ARG} gateway-1`;
15
- const CLAUDE_WRAPPER_COMMAND = `node ${WRAPPER_ROOT}/claude-agent-acp-wrapper.mjs`;
16
- const PLUGIN_DEPS_CODEX_COMMAND =
17
- "node /tmp/klaw/plugin-runtime-deps/node_modules/@zed-industries/codex-acp/bin/codex-acp.js";
18
- const LOCAL_NODE_MODULES_CODEX_COMMAND = `node ${path.resolve(
19
- "node_modules/@zed-industries/codex-acp/bin/codex-acp.js",
20
- )}`;
21
- const LOCAL_NODE_MODULES_CODEX_PLATFORM_COMMAND = path.resolve(
22
- "node_modules/@zed-industries/codex-acp-linux-x64/bin/codex-acp",
23
- );
24
-
25
- function cleanupDeps(processes: AcpxProcessInfo[]) {
26
- const killed: Array<{ pid: number; signal: NodeJS.Signals }> = [];
27
- return {
28
- killed,
29
- deps: {
30
- listProcesses: vi.fn(async () => processes),
31
- killProcess: vi.fn((pid: number, signal: NodeJS.Signals) => {
32
- killed.push({ pid, signal });
33
- }),
34
- sleep: vi.fn(async () => {}),
35
- },
36
- };
37
- }
38
-
39
- function collectMatching<T, U>(
40
- items: readonly T[],
41
- predicate: (item: T) => boolean,
42
- map: (item: T) => U,
43
- ): U[] {
44
- const matches: U[] = [];
45
- for (const item of items) {
46
- if (predicate(item)) {
47
- matches.push(map(item));
48
- }
49
- }
50
- return matches;
51
- }
52
-
53
- describe("process reaper", () => {
54
- it("recognizes generated Codex and Claude wrappers only under the configured root", () => {
55
- expect(
56
- isKlawOwnedAcpxProcessCommand({
57
- command: CODEX_WRAPPER_COMMAND,
58
- wrapperRoot: WRAPPER_ROOT,
59
- }),
60
- ).toBe(true);
61
- expect(
62
- isKlawOwnedAcpxProcessCommand({
63
- command: CLAUDE_WRAPPER_COMMAND,
64
- wrapperRoot: WRAPPER_ROOT,
65
- }),
66
- ).toBe(true);
67
- expect(
68
- isKlawOwnedAcpxProcessCommand({
69
- command: "node /tmp/other/codex-acp-wrapper.mjs",
70
- wrapperRoot: WRAPPER_ROOT,
71
- }),
72
- ).toBe(false);
73
- });
74
-
75
- it("only treats generated wrappers as launch-lease aware", () => {
76
- expect(
77
- isKlawLeaseAwareAcpxProcessCommand({
78
- command: CODEX_WRAPPER_COMMAND,
79
- wrapperRoot: WRAPPER_ROOT,
80
- }),
81
- ).toBe(true);
82
- expect(isKlawLeaseAwareAcpxProcessCommand({ command: LOCAL_NODE_MODULES_CODEX_COMMAND })).toBe(
83
- false,
84
- );
85
- expect(isKlawLeaseAwareAcpxProcessCommand({ command: PLUGIN_DEPS_CODEX_COMMAND })).toBe(false);
86
- });
87
-
88
- it("recognizes Klaw plugin-runtime-deps ACP adapter children", () => {
89
- expect(isKlawOwnedAcpxProcessCommand({ command: PLUGIN_DEPS_CODEX_COMMAND })).toBe(true);
90
- expect(isKlawOwnedAcpxProcessCommand({ command: "npx @zed-industries/codex-acp" })).toBe(false);
91
- });
92
-
93
- it("recognizes plugin-local ACP adapter package paths without trusting arbitrary installs", () => {
94
- expect(isKlawOwnedAcpxProcessCommand({ command: LOCAL_NODE_MODULES_CODEX_COMMAND })).toBe(true);
95
- expect(
96
- isKlawOwnedAcpxProcessCommand({
97
- command: "node /tmp/other-project/node_modules/@zed-industries/codex-acp/bin/codex-acp.js",
98
- }),
99
- ).toBe(false);
100
- });
101
-
102
- it("kills an owned recorded process tree children first", async () => {
103
- const { deps, killed } = cleanupDeps([
104
- { pid: 100, ppid: 1, command: CODEX_WRAPPER_COMMAND },
105
- { pid: 101, ppid: 100, command: PLUGIN_DEPS_CODEX_COMMAND },
106
- { pid: 102, ppid: 101, command: "node child.js" },
107
- ]);
108
-
109
- const result = await cleanupKlawOwnedAcpxProcessTree({
110
- rootPid: 100,
111
- rootCommand: CODEX_WRAPPER_COMMAND,
112
- wrapperRoot: WRAPPER_ROOT,
113
- deps,
114
- });
115
-
116
- expect(result.skippedReason).toBeUndefined();
117
- expect(result.inspectedPids).toEqual([100, 101, 102]);
118
- expect(killed.slice(0, 3)).toEqual([
119
- { pid: 102, signal: "SIGTERM" },
120
- { pid: 101, signal: "SIGTERM" },
121
- { pid: 100, signal: "SIGTERM" },
122
- ]);
123
- });
124
-
125
- it("allows wrapper-root verification when stored wrapper commands are shell-quoted", async () => {
126
- const { deps, killed } = cleanupDeps([{ pid: 110, ppid: 1, command: CODEX_WRAPPER_COMMAND }]);
127
-
128
- const result = await cleanupKlawOwnedAcpxProcessTree({
129
- rootPid: 110,
130
- rootCommand: `"/usr/local/bin/node" "${WRAPPER_ROOT}/codex-acp-wrapper.mjs"`,
131
- wrapperRoot: WRAPPER_ROOT,
132
- deps,
133
- });
134
-
135
- expect(result.skippedReason).toBeUndefined();
136
- expect(killed[0]).toEqual({ pid: 110, signal: "SIGTERM" });
137
- });
138
-
139
- it("requires matching lease identity before killing a leased process tree", async () => {
140
- const { deps, killed } = cleanupDeps([
141
- { pid: 112, ppid: 1, command: CODEX_WRAPPER_COMMAND_WITH_LEASE },
142
- ]);
143
-
144
- const result = await cleanupKlawOwnedAcpxProcessTree({
145
- rootPid: 112,
146
- rootCommand: CODEX_WRAPPER_COMMAND,
147
- expectedLeaseId: "lease-1",
148
- expectedGatewayInstanceId: "gateway-1",
149
- wrapperRoot: WRAPPER_ROOT,
150
- deps,
151
- });
152
-
153
- expect(result.skippedReason).toBeUndefined();
154
- expect(killed[0]).toEqual({ pid: 112, signal: "SIGTERM" });
155
- });
156
-
157
- it("does not kill a reused same-root wrapper pid with a different lease identity", async () => {
158
- const { deps, killed } = cleanupDeps([
159
- {
160
- pid: 113,
161
- ppid: 1,
162
- command: `${CODEX_WRAPPER_COMMAND} ${KLAW_ACPX_LEASE_ID_ARG} other-lease ${KLAW_GATEWAY_INSTANCE_ID_ARG} gateway-1`,
163
- },
164
- ]);
165
-
166
- const result = await cleanupKlawOwnedAcpxProcessTree({
167
- rootPid: 113,
168
- rootCommand: CODEX_WRAPPER_COMMAND,
169
- expectedLeaseId: "lease-1",
170
- expectedGatewayInstanceId: "gateway-1",
171
- wrapperRoot: WRAPPER_ROOT,
172
- deps,
173
- });
174
-
175
- expect(result).toEqual({
176
- inspectedPids: [113],
177
- terminatedPids: [],
178
- skippedReason: "not-klaw-owned",
179
- });
180
- expect(killed).toStrictEqual([]);
181
- });
182
-
183
- it("skips recorded pid cleanup when process listing is unavailable", async () => {
184
- const killed: Array<{ pid: number; signal: NodeJS.Signals }> = [];
185
- const result = await cleanupKlawOwnedAcpxProcessTree({
186
- rootPid: 200,
187
- rootCommand: CODEX_WRAPPER_COMMAND,
188
- wrapperRoot: WRAPPER_ROOT,
189
- deps: {
190
- listProcesses: vi.fn(async () => {
191
- throw new Error("ps unavailable");
192
- }),
193
- killProcess: vi.fn((pid, signal) => {
194
- killed.push({ pid, signal });
195
- }),
196
- sleep: vi.fn(async () => {}),
197
- },
198
- });
199
-
200
- expect(result).toEqual({
201
- inspectedPids: [],
202
- terminatedPids: [],
203
- skippedReason: "unverified-root",
204
- });
205
- expect(killed).toStrictEqual([]);
206
- });
207
-
208
- it("does not kill a reused pid when the live command is not Klaw-owned", async () => {
209
- const { deps, killed } = cleanupDeps([{ pid: 250, ppid: 1, command: "node unrelated.js" }]);
210
-
211
- const result = await cleanupKlawOwnedAcpxProcessTree({
212
- rootPid: 250,
213
- rootCommand: CODEX_WRAPPER_COMMAND,
214
- wrapperRoot: WRAPPER_ROOT,
215
- deps,
216
- });
217
-
218
- expect(result).toEqual({
219
- inspectedPids: [250],
220
- terminatedPids: [],
221
- skippedReason: "not-klaw-owned",
222
- });
223
- expect(killed).toStrictEqual([]);
224
- });
225
-
226
- it("does not kill a reused adapter pid when the stored root was a generated wrapper", async () => {
227
- const { deps, killed } = cleanupDeps([
228
- {
229
- pid: 260,
230
- ppid: 1,
231
- command: PLUGIN_DEPS_CODEX_COMMAND,
232
- },
233
- ]);
234
-
235
- const result = await cleanupKlawOwnedAcpxProcessTree({
236
- rootPid: 260,
237
- rootCommand: CODEX_WRAPPER_COMMAND,
238
- wrapperRoot: WRAPPER_ROOT,
239
- deps,
240
- });
241
-
242
- expect(result).toEqual({
243
- inspectedPids: [260],
244
- terminatedPids: [],
245
- skippedReason: "not-klaw-owned",
246
- });
247
- expect(killed).toStrictEqual([]);
248
- });
249
-
250
- it("skips non-owned recorded process trees", async () => {
251
- const { deps, killed } = cleanupDeps([{ pid: 300, ppid: 1, command: "node server.js" }]);
252
-
253
- const result = await cleanupKlawOwnedAcpxProcessTree({
254
- rootPid: 300,
255
- rootCommand: "node server.js",
256
- wrapperRoot: WRAPPER_ROOT,
257
- deps,
258
- });
259
-
260
- expect(result.skippedReason).toBe("not-klaw-owned");
261
- expect(killed).toStrictEqual([]);
262
- });
263
-
264
- it("reaps stale Klaw-owned wrapper and adapter orphans on startup", async () => {
265
- const { deps, killed } = cleanupDeps([
266
- { pid: 400, ppid: 1, command: CODEX_WRAPPER_COMMAND },
267
- { pid: 401, ppid: 400, command: PLUGIN_DEPS_CODEX_COMMAND },
268
- { pid: 402, ppid: 401, command: "node child.js" },
269
- { pid: 403, ppid: 1, command: CLAUDE_WRAPPER_COMMAND },
270
- { pid: 404, ppid: 403, command: "node claude-child.js" },
271
- { pid: 405, ppid: 1, command: PLUGIN_DEPS_CODEX_COMMAND },
272
- { pid: 406, ppid: 1, command: "node /tmp/other/codex-acp-wrapper.mjs" },
273
- ]);
274
-
275
- const result = await reapStaleKlawOwnedAcpxOrphans({
276
- wrapperRoot: WRAPPER_ROOT,
277
- deps,
278
- });
279
-
280
- expect(result.skippedReason).toBeUndefined();
281
- expect(result.inspectedPids).toEqual([400, 401, 402, 403, 404, 405]);
282
- expect(
283
- collectMatching(
284
- killed,
285
- (entry) => entry.signal === "SIGTERM",
286
- (entry) => entry.pid,
287
- ),
288
- ).toEqual([402, 401, 400, 404, 403, 405]);
289
- });
290
-
291
- it("reaps plugin-local Codex ACP adapter orphans when the generated wrapper is already gone", async () => {
292
- const { deps, killed } = cleanupDeps([
293
- { pid: 500, ppid: 1, command: LOCAL_NODE_MODULES_CODEX_COMMAND },
294
- { pid: 501, ppid: 500, command: LOCAL_NODE_MODULES_CODEX_PLATFORM_COMMAND },
295
- ]);
296
-
297
- const result = await reapStaleKlawOwnedAcpxOrphans({
298
- wrapperRoot: WRAPPER_ROOT,
299
- deps,
300
- });
301
-
302
- expect(result.skippedReason).toBeUndefined();
303
- expect(result.inspectedPids).toEqual([500, 501]);
304
- expect(
305
- collectMatching(
306
- killed,
307
- (entry) => entry.signal === "SIGTERM",
308
- (entry) => entry.pid,
309
- ),
310
- ).toEqual([501, 500]);
311
- });
312
-
313
- it("keeps startup scans quiet when process listing is unavailable", async () => {
314
- const result = await reapStaleKlawOwnedAcpxOrphans({
315
- wrapperRoot: WRAPPER_ROOT,
316
- deps: {
317
- listProcesses: vi.fn(async () => {
318
- throw new Error("ps unavailable");
319
- }),
320
- sleep: vi.fn(async () => {}),
321
- },
322
- });
323
-
324
- expect(result).toEqual({
325
- inspectedPids: [],
326
- terminatedPids: [],
327
- skippedReason: "process-list-unavailable",
328
- });
329
- });
330
- });