返回 CodeWhale
app-shutdown.test.mjs
根目录 / crates / tui / plugins / computer-use / tests / app-shutdown.test.mjs
1 import { test } from "node:test";
2 import assert from "node:assert/strict";
3 import fs from "node:fs";
4 import os from "node:os";
5 import path from "node:path";
6 import { fileURLToPath } from "node:url";
7 import { spawn } from "node:child_process";
8 import { setTimeout as delay } from "node:timers/promises";
9 import { appRequest, hello } from "../src/app-socket.mjs";
10
11 test("retiring daemon cleanup preserves a replacement listener and its run receipt", { timeout: 15_000 }, async t => {
12 const directory = fs.mkdtempSync(path.join(os.tmpdir(), "cu-retire-"));
13 const endpoint = process.platform === "win32" ? `\\\\.\\pipe\\cu-retire-${process.pid}` : path.join(directory, "app.sock");
14 const previous = process.env.CODEWHALE_CU_APP_SOCKET;
15 process.env.CODEWHALE_CU_APP_SOCKET = endpoint;
16 const children = [], sockets = [];
17 t.after(async () => {
18 sockets.forEach(socket => socket.destroy());
19 for (const { child, exited } of children) {
20 if (child.exitCode === null && child.signalCode === null) child.kill("SIGKILL");
21 await exited;
22 }
23 if (previous === undefined) delete process.env.CODEWHALE_CU_APP_SOCKET;
24 else process.env.CODEWHALE_CU_APP_SOCKET = previous;
25 fs.rmSync(directory, { recursive: true, force: true });
26 });
27 const started = path.join(directory, "cleanup-started"), release = path.join(directory, "allow-cleanup");
28 const fixture = path.join(directory, "backend.mjs");
29 fs.writeFileSync(fixture, `
30 import fs from 'node:fs';
31 import {setTimeout as delay} from 'node:timers/promises';
32 export function create() { return {
33 async get_app_state() { return {found:true,elements:[]}; },
34 async releaseInput() {
35 if (process.env.CU_BLOCK_CLEANUP !== '1') return;
36 fs.writeFileSync(process.env.CU_CLEANUP_STARTED, 'started');
37 while (!fs.existsSync(process.env.CU_ALLOW_CLEANUP)) await delay(10);
38 }
39 }; }
40 `);
41 function launch(block) {
42 const child = spawn(process.execPath, [fileURLToPath(new URL("../app/daemon.mjs", import.meta.url))], {
43 env: { ...process.env, CODEWHALE_CU_STATE_DIR: directory, CODEWHALE_CU_APP_SOCKET: endpoint,
44 CODEWHALE_CU_TEST_BACKEND: fixture, CU_BLOCK_CLEANUP: block ? "1" : "0",
45 CU_CLEANUP_STARTED: started, CU_ALLOW_CLEANUP: release, CODEWHALE_CU_CONTROL_FD: "3" },
46 stdio: ["ignore", "ignore", "pipe", "overlapped"],
47 });
48 let errors = ""; child.stderr.on("data", chunk => { errors += chunk; });
49 const exited = new Promise(resolve => { child.once("exit", resolve); child.once("error", resolve); });
50 const processState = { child, exited, errors: () => errors };
51 children.push(processState); return processState;
52 }
53 async function until(check, label) {
54 const deadline = Date.now() + 5000;
55 while (!(await check())) {
56 assert.ok(Date.now() < deadline, `${label}: ${children.map(p => p.errors()).join("\n")}`);
57 await delay(10, undefined, { signal: t.signal });
58 }
59 }
60 const old = launch(true);
61 await until(async () => (await hello({ timeoutMs: 100 }))?.pid === old.child.pid, "first daemon ready");
62 const owner = await appRequest({ tool: "open_session", sessionId: "retiring" }, { keepOpen: true });
63 sockets.push(owner.socket);
64 assert.equal((await appRequest({ tool: "get_app_state", sessionId: "retiring", leaseToken: owner.reply.leaseToken })).ok, true);
65 old.child.stdio[3].destroy(); // Owner loss is graceful on Windows too; SIGTERM there force-kills.
66 await until(() => fs.existsSync(started), "cleanup started");
67 const replacement = launch(false);
68 await until(async () => (await hello({ timeoutMs: 100 }))?.pid === replacement.child.pid, "replacement ready during old cleanup");
69 fs.writeFileSync(release, "release");
70 await until(() => old.child.exitCode !== null, "old daemon exit");
71 assert.equal(old.child.exitCode, 0, old.errors());
72 assert.equal((await hello()).pid, replacement.child.pid, "cleanup must not remove the new listener");
73 assert.equal(JSON.parse(fs.readFileSync(path.join(directory, "app-run.json"))).pid, replacement.child.pid);
74 });
75
75 lines Plain Text