fix(app): run isolated daemon cleanup outside the test loader

Importing the server lifecycle into the browser test runner exposes its
transitive plugin exports to the runner's transforming loader. Use the
supported CLI in ordinary Node so confirmed shutdown and terminal reaping
complete before removing the owned home.
This commit is contained in:
Mohamed Boudra
2026-10-30 11:31:30 +01:00
parent 4c2bd3527f
commit 7cef8ef43b
@@ -1,7 +1,11 @@
import { pathToFileURL } from "node:url";
import type { DaemonClient } from "@getpaseo/client/internal/daemon-client";
import { connectDaemonClient } from "./daemon-client-loader";
import { spawn, execFileSync, type ChildProcess, type SpawnOptions } from "node:child_process";
import { promisify } from "node:util";
import {
spawn,
execFile,
execFileSync,
type ChildProcess,
type SpawnOptions,
} from "node:child_process";
import { mkdtemp, rm, writeFile } from "node:fs/promises";
import net from "node:net";
import { tmpdir } from "node:os";
@@ -9,6 +13,8 @@ import path from "node:path";
import { withDisabledE2ESpeechEnv } from "./speech-env";
import { killProcessTree, spawnTsx } from "./spawn-node";
const execFileAsync = promisify(execFile);
export interface IsolatedHostDaemon {
serverId: string;
port: number;
@@ -157,30 +163,20 @@ export async function startIsolatedHostDaemon(
: path.resolve(__dirname, "../../../../server");
let child: ChildProcess | null = null;
const stopDaemon = async (): Promise<void> => {
// The daemon lifecycle owns its worker and detached PTYs. Waiting only for
// the supervisor lets a surviving worker recreate the home during removal.
// Load the ESM lifecycle entry natively, as daemon-client-loader does for
// the SDK; Playwright's CommonJS transform cannot load its createRequire.
const lifecycleUrl = pathToFileURL(
path.resolve(__dirname, "../../../../server/dist/server/server/daemon-instance.js"),
).href;
const { stopDaemonInstance } = (await import(
lifecycleUrl
)) as typeof import("@getpaseo/server/daemon-control");
await stopDaemonInstance(paseoHome, {
force: true,
requestShutdown: async () => {
const client = await connectDaemonClient<DaemonClient>({
port,
clientIdPrefix: "isolated-host-cleanup",
});
try {
await client.shutdownServer();
} finally {
await client.close();
}
},
});
// The CLI owns graceful shutdown, forced cleanup and detached terminal
// reaping. Keep its ESM graph out of Playwright's transforming loader.
await execFileAsync(
process.execPath,
[
path.resolve(__dirname, "../../../../cli/dist/index.js"),
"daemon",
"stop",
"--home",
paseoHome,
"--force",
],
{ timeout: 30_000 },
);
await killProcessTree(child);
};
const spawnDaemon = async (): Promise<ChildProcess> => {