mirror of
https://github.com/nexu-io/open-design.git
synced 2026-08-03 06:05:05 +08:00
* fix(packaged): honor OD_DATA_DIR in desktop runtime Co-authored-by: multica-agent <github@multica.ai> * fix(packaged): scope OD_DATA_DIR by namespace Co-authored-by: multica-agent <github@multica.ai> * fix(packaged): reject relative OD_DATA_DIR overrides Co-authored-by: multica-agent <github@multica.ai> * fix(packaged): preserve scoped OD_DATA_DIR overrides Co-authored-by: multica-agent <github@multica.ai> * fix(packaged): surface OD_DATA_DIR validation as PackagedPathAccessError Relative OD_DATA_DIR in packaged mode now throws PackagedPathAccessError instead of a plain Error. apps/packaged/src/index.ts main() only routes PackagedPathAccessError to dialog.showErrorBox, so the prior plain Error made the app exit silently for GUI launches with an invalid override. Extract PackagedPathAccessError into apps/packaged/src/errors.ts so paths.ts can throw it without an inter-module value cycle with launch.ts. Co-authored-by: multica-agent <github@multica.ai> * fix(packaged): make OD_DATA_DIR absolute-path guard platform-aware The previous guard ran `win32.isAbsolute(expanded)` unconditionally on every platform, so on macOS/Linux a value like `C:\Users\Fred\OD` passed the check (win32 considers it absolute) and silently flowed into `join(expanded, "namespaces", namespace, "data")`, producing a cwd-relative POSIX path instead of throwing. Branch the check on `process.platform === "win32"` so Windows paths are only accepted on Windows. Update the existing Windows-themed test fixtures to stub `process.platform = "win32"` (the omission was what masked this bug) and add a regression that stubs `linux` and asserts `C:\foo` and `\\server\share` are rejected as PackagedPathAccessError. Co-authored-by: multica-agent <github@multica.ai> * fix(packaged): reject mismatched scoped OD_DATA_DIR Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> Co-authored-by: kami.c <kami.c@chative.com>
90 lines
3.1 KiB
TypeScript
90 lines
3.1 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 {
|
|
claimPackagedSingleInstanceLock,
|
|
verifyPackagedDataRootWritable,
|
|
} from "../src/launch.js";
|
|
|
|
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, () => void>();
|
|
const app = {
|
|
on: vi.fn((event: string, listener: () => 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")?.();
|
|
|
|
expect(app.requestSingleInstanceLock).toHaveBeenCalledTimes(1);
|
|
expect(app.on).toHaveBeenCalledWith("second-instance", expect.any(Function));
|
|
expect(app.quit).not.toHaveBeenCalled();
|
|
expect(focusExisting).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
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();
|
|
});
|
|
});
|