mirror of
https://github.com/nexu-io/open-design.git
synced 2026-09-20 06:15:06 +08:00
0688faaec3
* reland(sidecar): converge product clients after weekly release
Restore the sidecar convergence from #7244 after the release-scheduling revert in #7634. This tree matches the previously reviewed and cross-platform QA-accepted implementation at d83d4e032f.
* fix(sidecar): clarify identity and cache witnesses
Generated-By: looper 0.11.2 (runner=fixer, agent=codex)
* fix(packaged): probe cross-source desktop owners
Generated-By: looper 0.11.2 (runner=fixer, agent=codex)
* fix(sidecar): preserve cross-source launch ownership
Generated-By: looper 0.11.2 (runner=fixer, agent=codex)
* test(pack): assert cross-source Windows launch ownership
Generated-By: looper 0.11.2 (runner=fixer, agent=codex)
* fix(sidecar): align HyperFrames with client boundary
Generated-By: looper 0.11.2 (runner=fixer, agent=codex)
* fix(e2e): keep HyperFrames fixture in helper lane
Generated-By: looper 0.11.2 (runner=fixer, agent=codex)
* docs(sidecar): restore review and handoff contracts
Generated-By: looper 0.11.2 (runner=fixer, agent=codex)
* fix(sidecar): restore cache and daemon boundaries
Generated-By: looper 0.11.2 (runner=fixer, agent=codex)
* fix(pack): restore sourcemaps and Linux smoke discovery
Generated-By: looper 0.11.2 (runner=fixer, agent=codex)
* fix(sidecar): make payload handoff generation-atomic
* fix(sidecar): preserve packaged action and mode parity
Generated-By: looper 0.11.2 (runner=fixer, agent=codex)
* test(pack): align Linux cleanup resource count
Generated-By: looper 0.11.2 (runner=fixer, agent=codex)
* fix(packaged): preserve headless sidecar mode
* test: normalize source line endings
* fix(daemon): launch Codex cmd shims on Windows
---------
Co-authored-by: Looper <looper@noreply.github.com>
179 lines
6.5 KiB
TypeScript
179 lines
6.5 KiB
TypeScript
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
|
|
import { describe, expect, it, vi } from "vitest";
|
|
|
|
vi.mock("electron", () => ({
|
|
app: {},
|
|
}));
|
|
|
|
import { PackagedPathAccessError } from "../src/errors.js";
|
|
import { inspectExistingDesktopForLauncher } from "../src/launcher-after-quit.js";
|
|
import {
|
|
claimPackagedSingleInstanceLock,
|
|
createPackagedSecondInstanceHandoff,
|
|
stabilizePackagedWorkingDirectory,
|
|
verifyPackagedDataRootWritable,
|
|
} from "../src/launch.js";
|
|
import type { PackagedNamespacePaths } from "../src/paths.js";
|
|
import { findPackagedDeeplinkArg } from "../src/payload-desktop-launch.js";
|
|
|
|
function fakePaths(root: string): PackagedNamespacePaths {
|
|
return {
|
|
cacheRoot: join(root, "cache"),
|
|
dataRoot: join(root, "data"),
|
|
desktopLogPath: join(root, "logs", "desktop", "latest.log"),
|
|
desktopLogsRoot: join(root, "logs", "desktop"),
|
|
electronSessionDataRoot: join(root, "user-data", "session"),
|
|
electronUserDataRoot: join(root, "user-data"),
|
|
installationRoot: root,
|
|
installerObservationRoot: join(root, "data", "observations", "installer"),
|
|
logsRoot: join(root, "logs"),
|
|
namespaceRoot: root,
|
|
resourceRoot: join(root, "resources", "open-design"),
|
|
runtimeRoot: join(root, "runtime"),
|
|
updateRoot: join(root, "updates"),
|
|
};
|
|
}
|
|
|
|
describe("stabilizePackagedWorkingDirectory", () => {
|
|
it("switches to the namespace runtime root without reading the inherited cwd", () => {
|
|
const runtimeRoot = join(tmpdir(), "od-packaged-runtime");
|
|
const chdir = vi.fn();
|
|
const cwd = vi.spyOn(process, "cwd").mockImplementation(() => {
|
|
throw new Error("uv_cwd");
|
|
});
|
|
|
|
try {
|
|
stabilizePackagedWorkingDirectory({ runtimeRoot }, chdir);
|
|
|
|
expect(chdir).toHaveBeenCalledWith(runtimeRoot);
|
|
expect(cwd).not.toHaveBeenCalled();
|
|
} finally {
|
|
cwd.mockRestore();
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("verifyPackagedDataRootWritable", () => {
|
|
it("accepts a writable dataRoot", async () => {
|
|
const root = mkdtempSync(join(tmpdir(), "od-packaged-launch-"));
|
|
try {
|
|
const dataRoot = join(root, "namespaces", "release-beta", "data");
|
|
await expect(verifyPackagedDataRootWritable({ dataRoot })).resolves.toBeUndefined();
|
|
} finally {
|
|
rmSync(root, { force: true, recursive: true });
|
|
}
|
|
});
|
|
|
|
it("wraps low-level mkdir/access failures with a user-actionable error", async () => {
|
|
const root = mkdtempSync(join(tmpdir(), "od-packaged-launch-"));
|
|
try {
|
|
const blocker = join(root, "namespaces", "release-beta");
|
|
mkdirSync(blocker, { recursive: true });
|
|
writeFileSync(join(blocker, "data"), "not a directory");
|
|
|
|
let captured: unknown;
|
|
try {
|
|
await verifyPackagedDataRootWritable({ dataRoot: join(blocker, "data") });
|
|
} catch (error) {
|
|
captured = error;
|
|
}
|
|
|
|
expect(captured).toBeInstanceOf(PackagedPathAccessError);
|
|
expect((captured as Error).message).toContain("Open Design could not create or write to:");
|
|
expect((captured as Error).message).toContain(join(blocker, "data"));
|
|
expect((captured as Error).message).toContain("Current user:");
|
|
expect((captured as Error).message).toContain("Try in Terminal:");
|
|
expect((captured as Error).message).toContain("sudo chown -R");
|
|
} finally {
|
|
rmSync(root, { force: true, recursive: true });
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("claimPackagedSingleInstanceLock", () => {
|
|
it("registers a second-instance focus callback when the lock is acquired", () => {
|
|
const listeners = new Map<string, (event: unknown, argv: string[]) => void>();
|
|
const app = {
|
|
on: vi.fn((event: string, listener: (event: unknown, argv: string[]) => void) => {
|
|
listeners.set(event, listener);
|
|
return app;
|
|
}),
|
|
quit: vi.fn(),
|
|
requestSingleInstanceLock: vi.fn(() => true),
|
|
};
|
|
const focusExisting = vi.fn();
|
|
|
|
expect(claimPackagedSingleInstanceLock(app, focusExisting)).toBe(true);
|
|
listeners.get("second-instance")?.({}, ["Open Design.exe", "--from-protocol"]);
|
|
|
|
expect(app.requestSingleInstanceLock).toHaveBeenCalledTimes(1);
|
|
expect(app.on).toHaveBeenCalledWith("second-instance", expect.any(Function));
|
|
expect(app.quit).not.toHaveBeenCalled();
|
|
expect(focusExisting).toHaveBeenCalledExactlyOnceWith([
|
|
"Open Design.exe",
|
|
"--from-protocol",
|
|
]);
|
|
});
|
|
|
|
it("queues a deeplink from the lock fallback while desktop IPC is unavailable", async () => {
|
|
const root = mkdtempSync(join(tmpdir(), "od-packaged-lock-deeplink-"));
|
|
const listeners = new Map<string, (event: unknown, argv: string[]) => void>();
|
|
const deeplinkUrl = "opendesign://workspace/invite/continue?nonce=cold-race";
|
|
const handoff = createPackagedSecondInstanceHandoff();
|
|
const app = {
|
|
on: vi.fn((event: string, listener: (event: unknown, argv: string[]) => void) => {
|
|
listeners.set(event, listener);
|
|
return app;
|
|
}),
|
|
quit: vi.fn(),
|
|
requestSingleInstanceLock: vi.fn(() => true),
|
|
};
|
|
const dispatchDeeplink = vi.fn();
|
|
const show = vi.fn();
|
|
|
|
try {
|
|
await expect(inspectExistingDesktopForLauncher({
|
|
app: "desktop", channel: "beta", mode: "runtime", namespace: "release-beta-win", source: "packaged",
|
|
}, {
|
|
deeplinkUrl,
|
|
paths: fakePaths(root),
|
|
getStatus: vi.fn(async () => {
|
|
throw new Error("desktop IPC is not ready");
|
|
}),
|
|
})).resolves.toEqual({ action: "continue", reason: "inspect-failed" });
|
|
|
|
expect(claimPackagedSingleInstanceLock(app, (argv) => {
|
|
handoff.handle(findPackagedDeeplinkArg(argv));
|
|
})).toBe(true);
|
|
listeners.get("second-instance")?.({}, ["Open Design.exe", deeplinkUrl]);
|
|
|
|
expect(show).not.toHaveBeenCalled();
|
|
expect(dispatchDeeplink).not.toHaveBeenCalled();
|
|
|
|
handoff.attach({ dispatchDeeplink, show });
|
|
|
|
expect(show).toHaveBeenCalledTimes(1);
|
|
expect(dispatchDeeplink).toHaveBeenCalledExactlyOnceWith(deeplinkUrl);
|
|
} finally {
|
|
rmSync(root, { force: true, recursive: true });
|
|
}
|
|
});
|
|
|
|
it("quits the duplicate process before packaged sidecars start when the lock is held", () => {
|
|
const app = {
|
|
on: vi.fn(),
|
|
quit: vi.fn(),
|
|
requestSingleInstanceLock: vi.fn(() => false),
|
|
};
|
|
|
|
expect(claimPackagedSingleInstanceLock(app, vi.fn())).toBe(false);
|
|
|
|
expect(app.requestSingleInstanceLock).toHaveBeenCalledTimes(1);
|
|
expect(app.quit).toHaveBeenCalledTimes(1);
|
|
expect(app.on).not.toHaveBeenCalled();
|
|
});
|
|
});
|