// Desktop regression: a local `executor daemon run` (the CLI's foreground// daemon, or a daemon a prior session left behind) already owns ~/.executor// when the app launches. The app must NOT try to spawn a second server on top// of it — that dies on the scope lock ("already running ... owns the current// data directory") and historically wedged the app onto the crash screen with// no way back until the foreign daemon was killed by hand. The fix: when the// data dir is owned by a healthy cli-daemon, the app attaches to it instead of// spawning. This drives the dev app (dev boot always takes the managed-spawn// path, so it exercises the spawn -> attach fallback directly) against a// pre-running CLI daemon and proves the window comes up attached to it.import { type ChildProcess, spawn } from "node:child_process";import { mkdtempSync, readFileSync, rmSync } from "node:fs";import { createRequire } from "node:module";import net from "node:net";import { tmpdir } from "node:os";import { join } from "node:path";import { fileURLToPath } from "node:url"; import { expect } from "@effect/vitest";import { Effect } from "effect";import { _electron } from "playwright"; import { scenario } from "../src/scenario";import { RunDir } from "../src/services"; const appDir = fileURLToPath(new URL("../../apps/desktop/", import.meta.url));const repoRoot = fileURLToPath(new URL("../../", import.meta.url));const cliEntry = join(repoRoot, "apps/cli/src/main.ts");const electronBinary = createRequire(join(appDir, "package.json"))("electron") as string; interface Manifest { readonly kind: string; readonly pid: number;} const freePort = (): Promise<number> => new Promise((resolve, reject) => { const srv = net.createServer(); srv.on("error", reject); srv.listen(0, "127.0.0.1", () => { const port = (srv.address() as net.AddressInfo).port; srv.close(() => resolve(port)); }); }); interface DaemonStart { readonly child: ChildProcess; readonly ready: boolean; readonly stderr: string;} // Start a plain CLI daemon owning `dataDir` — no EXECUTOR_SUPERVISED, owner// "cli", exactly what `executor daemon run` writes when a user starts it by// hand. Resolves once it announces readiness (or exits / times out).const startCliDaemon = (dataDir: string, home: string, port: number): Promise<DaemonStart> => new Promise((resolve) => { const child = spawn( "bun", [ "run", cliEntry, "daemon", "run", "--foreground", "--port", String(port), "--hostname", "127.0.0.1", ], { cwd: repoRoot, env: { ...process.env, HOME: home, EXECUTOR_DATA_DIR: dataDir, EXECUTOR_SCOPE_DIR: dataDir, }, stdio: ["ignore", "pipe", "pipe"], }, ); let stderr = ""; const settle = (ready: boolean) => resolve({ child, ready, stderr }); const timer = setTimeout(() => settle(false), 60_000); child.stdout?.on("data", (chunk: Buffer) => { if (/Daemon ready on http:\/\//.test(chunk.toString())) { clearTimeout(timer); settle(true); } }); child.stderr?.on("data", (chunk: Buffer) => { stderr += chunk.toString(); }); child.once("exit", () => { clearTimeout(timer); settle(false); }); }); const stopDaemon = async (child: ChildProcess | undefined): Promise<void> => { if (!child || child.exitCode !== null || child.signalCode !== null) return; await new Promise<void>((resolve) => { const timeout = setTimeout(() => { child.kill("SIGKILL"); resolve(); }, 5_000); child.once("exit", () => { clearTimeout(timeout); resolve(); }); child.kill("SIGTERM"); });}; scenario( "Desktop · attaches to a CLI daemon that already owns the data dir instead of wedging", { timeout: 300_000 }, Effect.gen(function* () { const runDir = yield* RunDir; yield* Effect.promise(() => run(runDir)); }),); const run = async (runDir: string) => { const home = mkdtempSync(join(tmpdir(), "executor-desktop-attach-")); const dataDir = join(home, ".executor"); const manifestPath = join(dataDir, "server-control", "server.json"); const port = await freePort(); let daemon: ChildProcess | undefined; let app: Awaited<ReturnType<typeof _electron.launch>> | undefined; try { const started = await startCliDaemon(dataDir, home, port); daemon = started.child; expect(started.ready, `CLI daemon became ready; stderr:\n${started.stderr}`).toBe(true); const before = JSON.parse(readFileSync(manifestPath, "utf8")) as Manifest; expect(before.kind, "the CLI daemon advertises itself as cli-daemon").toBe("cli-daemon"); const daemonPid = before.pid; expect(daemonPid, "CLI daemon pid recorded in the manifest").toBeGreaterThan(0); // Launch the dev app against the SAME HOME. Boot has no supervised path in // dev, so it goes straight to managed-spawn — which collides with the // daemon already owning ~/.executor and must fall back to attaching. app = await _electron.launch({ executablePath: electronBinary, args: [appDir], cwd: appDir, env: { ...process.env, HOME: home }, timeout: 120_000, }); // firstWindow resolves only once a connection was established and the window // was created with its URL. Pre-fix the spawn died on the scope lock, boot // surfaced a fatal dialog, and no window was ever created — so this wait IS // the regression assertion: it only resolves if the app attached. const page = await app.firstWindow({ timeout: 120_000 }); await page.screenshot({ path: join(runDir, "01-attached-window.png") }); // Proof it ATTACHED rather than spawned: the manifest is untouched — same // pid, still cli-daemon. A managed sidecar would have rewritten it to // "desktop-sidecar" with a fresh pid. const after = JSON.parse(readFileSync(manifestPath, "utf8")) as Manifest; expect(after.kind, "still the CLI daemon (the app did not spawn its own sidecar)").toBe( "cli-daemon", ); expect(after.pid, "the app attached to the running CLI daemon, not a new sidecar").toBe( daemonPid, ); } finally { await app?.close().catch(() => {}); await stopDaemon(daemon); rmSync(home, { recursive: true, force: true }); }};
// Desktop regression: a local `executor daemon run` (the CLI's foreground
// daemon, or a daemon a prior session left behind) already owns ~/.executor
// when the app launches. The app must NOT try to spawn a second server on top
// of it — that dies on the scope lock ("already running ... owns the current
// data directory") and historically wedged the app onto the crash screen with
// no way back until the foreign daemon was killed by hand. The fix: when the
// data dir is owned by a healthy cli-daemon, the app attaches to it instead of
// spawning. This drives the dev app (dev boot always takes the managed-spawn
// path, so it exercises the spawn -> attach fallback directly) against a
// pre-running CLI daemon and proves the window comes up attached to it.
import { type ChildProcess, spawn } from "node:child_process";
import { mkdtempSync, readFileSync, rmSync } from "node:fs";
import { createRequire } from "node:module";
import net from "node:net";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
import { expect } from "@effect/vitest";
import { Effect } from "effect";
import { _electron } from "playwright";
import { scenario } from "../src/scenario";
import { RunDir } from "../src/services";
const appDir = fileURLToPath(new URL("../../apps/desktop/", import.meta.url));
const repoRoot = fileURLToPath(new URL("../../", import.meta.url));
const cliEntry = join(repoRoot, "apps/cli/src/main.ts");
const electronBinary = createRequire(join(appDir, "package.json"))("electron") as string;
interface Manifest {
readonly kind: string;
readonly pid: number;
}
const freePort = (): Promise<number> =>
new Promise((resolve, reject) => {
const srv = net.createServer();
srv.on("error", reject);
srv.listen(0, "127.0.0.1", () => {
const port = (srv.address() as net.AddressInfo).port;
srv.close(() => resolve(port));
});
});
interface DaemonStart {
readonly child: ChildProcess;
readonly ready: boolean;
readonly stderr: string;
}
// Start a plain CLI daemon owning `dataDir` — no EXECUTOR_SUPERVISED, owner
// "cli", exactly what `executor daemon run` writes when a user starts it by
// hand. Resolves once it announces readiness (or exits / times out).
const startCliDaemon = (dataDir: string, home: string, port: number): Promise<DaemonStart> =>
new Promise((resolve) => {
const child = spawn(
"bun",
[
"run",
cliEntry,
"daemon",
"run",
"--foreground",
"--port",
String(port),
"--hostname",
"127.0.0.1",
],
{
cwd: repoRoot,
env: {
...process.env,
HOME: home,
EXECUTOR_DATA_DIR: dataDir,
EXECUTOR_SCOPE_DIR: dataDir,
},
stdio: ["ignore", "pipe", "pipe"],
},
);
let stderr = "";
const settle = (ready: boolean) => resolve({ child, ready, stderr });
const timer = setTimeout(() => settle(false), 60_000);
child.stdout?.on("data", (chunk: Buffer) => {
if (/Daemon ready on http:\/\//.test(chunk.toString())) {
clearTimeout(timer);
settle(true);
}
});
child.stderr?.on("data", (chunk: Buffer) => {
stderr += chunk.toString();
});
child.once("exit", () => {
clearTimeout(timer);
settle(false);
});
});
const stopDaemon = async (child: ChildProcess | undefined): Promise<void> => {
if (!child || child.exitCode !== null || child.signalCode !== null) return;
await new Promise<void>((resolve) => {
const timeout = setTimeout(() => {
child.kill("SIGKILL");
resolve();
}, 5_000);
child.once("exit", () => {
clearTimeout(timeout);
resolve();
});
child.kill("SIGTERM");
});
};
scenario(
"Desktop · attaches to a CLI daemon that already owns the data dir instead of wedging",
{ timeout: 300_000 },
Effect.gen(function* () {
const runDir = yield* RunDir;
yield* Effect.promise(() => run(runDir));
}),
);
const run = async (runDir: string) => {
const home = mkdtempSync(join(tmpdir(), "executor-desktop-attach-"));
const dataDir = join(home, ".executor");
const manifestPath = join(dataDir, "server-control", "server.json");
const port = await freePort();
let daemon: ChildProcess | undefined;
let app: Awaited<ReturnType<typeof _electron.launch>> | undefined;
try {
const started = await startCliDaemon(dataDir, home, port);
daemon = started.child;
expect(started.ready, `CLI daemon became ready; stderr:\n${started.stderr}`).toBe(true);
const before = JSON.parse(readFileSync(manifestPath, "utf8")) as Manifest;
expect(before.kind, "the CLI daemon advertises itself as cli-daemon").toBe("cli-daemon");
const daemonPid = before.pid;
expect(daemonPid, "CLI daemon pid recorded in the manifest").toBeGreaterThan(0);
// Launch the dev app against the SAME HOME. Boot has no supervised path in
// dev, so it goes straight to managed-spawn — which collides with the
// daemon already owning ~/.executor and must fall back to attaching.
app = await _electron.launch({
executablePath: electronBinary,
args: [appDir],
cwd: appDir,
env: { ...process.env, HOME: home },
timeout: 120_000,
});
// firstWindow resolves only once a connection was established and the window
// was created with its URL. Pre-fix the spawn died on the scope lock, boot
// surfaced a fatal dialog, and no window was ever created — so this wait IS
// the regression assertion: it only resolves if the app attached.
const page = await app.firstWindow({ timeout: 120_000 });
await page.screenshot({ path: join(runDir, "01-attached-window.png") });
// Proof it ATTACHED rather than spawned: the manifest is untouched — same
// pid, still cli-daemon. A managed sidecar would have rewritten it to
// "desktop-sidecar" with a fresh pid.
const after = JSON.parse(readFileSync(manifestPath, "utf8")) as Manifest;
expect(after.kind, "still the CLI daemon (the app did not spawn its own sidecar)").toBe(
"cli-daemon",
);
expect(after.pid, "the app attached to the running CLI daemon, not a new sidecar").toBe(
daemonPid,
);
} finally {
await app?.close().catch(() => {});
await stopDaemon(daemon);
rmSync(home, { recursive: true, force: true });
}
};