Files
nexu-io__open-design/apps/packaged/tests/headless-runtime.test.ts
lefarcen e1c277c593 fix(packaged): recover od:// after web sidecar exits (#6364)
* fix(packaged): resolve od:// target per request instead of snapshotting it

registerOdProtocol captured the web sidecar URL once at startup. When
the sidecar died or was respawned on a fresh ephemeral port, the od://
proxy stayed pinned to the dead port for the rest of the process
lifetime and every renderer fetch returned OD_PROTOCOL_PROXY_FAILED
until the user quit and relaunched the app.

Take a resolver instead so the target is re-read on each request.

* feat(packaged): add bounded restart policy helpers for sidecars

createRestartPolicy caps respawns to a sliding window so a sidecar
that crashes during boot cannot respawn forever. resolveWebRestart
wraps one policy-gated attempt and swallows spawn failures, so the
supervisor wired up next can call it straight from an exit handler.

* fix(packaged): respawn the web sidecar on exit and serve od:// from the live URL

The web sidecar was spawned once and never watched. When it died
mid-session, nothing respawned it and the od:// proxy kept forwarding
to a dead port, so the whole UI returned OD_PROTOCOL_PROXY_FAILED
until the user quit and relaunched.

Supervise the child: on an unexpected exit, respawn it under the
bounded restart policy and publish the new URL through
PackagedSidecarHandle.currentWebUrl(), which the od:// resolver reads
per request. close() flips a `closing` flag first so an intentional
shutdown is not mistaken for a crash.

Adjacent, not fixed here: index.ts discoverDaemonUrl() reads the same
kind of boot snapshot for the daemon sidecar, which has no supervisor
of its own.

* test(packaged): update headless sidecar handle mock

* fix(packaged): close web sidecar restart races

---------

Co-authored-by: Bassi <info@beazyagency.it>
2026-08-03 10:16:10 +00:00

128 lines
3.7 KiB
TypeScript

import { describe, expect, it, vi } from "vitest";
import {
acquirePackagedHeadlessStartup,
parsePackagedHeadlessRequest,
resolvePackagedMcpBootstrapLaunch,
} from "../src/headless-runtime.js";
describe("parsePackagedHeadlessRequest", () => {
it("accepts a headless Codex MCP install request", () => {
expect(parsePackagedHeadlessRequest([
"--headless",
"--mcp-install",
"codex",
])).toEqual({
headless: true,
mcpInstallAgent: "codex",
});
});
it("rejects unsupported MCP install targets", () => {
expect(() => parsePackagedHeadlessRequest([
"--headless",
"--mcp-install",
"claude",
])).toThrow(/only supports codex/i);
});
});
describe("resolvePackagedMcpBootstrapLaunch", () => {
it("uses macOS open against the stable signed app bundle", () => {
expect(resolvePackagedMcpBootstrapLaunch({
currentExecutablePath:
"/private/payload/Open Design.app/Contents/MacOS/Open Design",
installedLaunchPath: "/Applications/Open Design.app",
platform: "darwin",
})).toEqual({
command: "/usr/bin/open",
args: [
"-g",
"-j",
"/Applications/Open Design.app",
"--args",
"--headless",
],
});
});
it("invokes a non-macOS installed launcher directly", () => {
expect(resolvePackagedMcpBootstrapLaunch({
currentExecutablePath: "/tmp/payload/open-design",
installedLaunchPath: "/opt/open-design/open-design",
platform: "linux",
})).toEqual({
command: "/opt/open-design/open-design",
args: ["--headless"],
});
});
});
describe("acquirePackagedHeadlessStartup", () => {
function createDependencies(failAt: "mcp" | "web-identity") {
const closed: string[] = [];
const exit = vi.fn();
return {
closed,
dependencies: {
confirmRuntime: vi.fn(async () => undefined),
createIpcServer: vi.fn(async () => ({
close: async () => {
closed.push("ipc");
},
})),
exit,
installMcp: vi.fn(async () => {
if (failAt === "mcp") throw new Error("MCP install failed");
}),
startSidecars: vi.fn(async () => ({
close: async () => {
closed.push("sidecars");
},
currentWebUrl: () => "http://127.0.0.1:7456",
daemon: {
desktopAuthGateActive: false,
state: "running" as const,
url: "http://127.0.0.1:7457",
},
web: { state: "running" as const, url: "http://127.0.0.1:7456" },
})),
writeIdentity: vi.fn(async () => ({
close: async () => {
closed.push("identity");
},
identity: {} as never,
})),
writeWebIdentity: vi.fn(async () => {
if (failAt === "web-identity") {
throw new Error("web identity write failed");
}
}),
},
exit,
};
}
it("closes identity and sidecars when MCP installation fails", async () => {
const { closed, dependencies, exit } = createDependencies("mcp");
await expect(acquirePackagedHeadlessStartup(dependencies)).rejects.toThrow(
"MCP install failed",
);
expect(closed).toEqual(["sidecars", "identity"]);
expect(exit).not.toHaveBeenCalled();
});
it("closes IPC, sidecars, and identity when identity publication fails", async () => {
const { closed, dependencies, exit } = createDependencies("web-identity");
await expect(acquirePackagedHeadlessStartup(dependencies)).rejects.toThrow(
"web identity write failed",
);
expect(closed).toEqual(["ipc", "sidecars", "identity"]);
expect(exit).not.toHaveBeenCalled();
});
});