mirror of
https://github.com/nexu-io/open-design.git
synced 2026-09-20 06:15:06 +08:00
e1c277c593
* 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>
128 lines
3.7 KiB
TypeScript
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();
|
|
});
|
|
});
|