Files
wehub-resource-sync 070959e133
landing-page-staging / Deploy landing page to staging (push) Has been skipped
landing-page-ci / Validate landing page (push) Failing after 4s
visual-baseline / Capture visual baselines (push) Has been cancelled
bake-plugin-previews / Bake plugin previews (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:00:47 +08:00

225 lines
7.5 KiB
TypeScript

import { join } from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import type { PackagedConfig } from "../src/config.js";
import { PackagedPathAccessError } from "../src/errors.js";
import { resolvePackagedNamespacePaths } from "../src/paths.js";
function stubPlatform(value: NodeJS.Platform): () => void {
const original = process.platform;
Object.defineProperty(process, "platform", { value, configurable: true });
return () => {
Object.defineProperty(process, "platform", { value: original, configurable: true });
};
}
function fakeConfig(): PackagedConfig {
return {
amrProfile: null,
appVersion: null,
daemonCliEntry: null,
daemonSidecarEntry: null,
namespace: "release-stable-win",
namespaceBaseRoot: join("C:", "Users", "Fred", "AppData", "Roaming", "Open Design", "namespaces"),
nodeCommand: null,
posthogHost: null,
posthogKey: null,
resourceRoot: join("C:", "Program Files", "Open Design", "resources", "open-design"),
telemetryRelayUrl: null,
updateMetadataUrl: null,
webOutputMode: "server",
webSidecarEntry: null,
webStandaloneRoot: null,
};
}
describe("resolvePackagedNamespacePaths", () => {
let restorePlatform: () => void = () => {};
beforeEach(() => {
restorePlatform = stubPlatform("win32");
});
afterEach(() => {
restorePlatform();
});
it("models update downloads as a namespace-scoped root beside data", () => {
const config = fakeConfig();
const paths = resolvePackagedNamespacePaths(config, config.namespace);
expect(paths.namespaceRoot).toBe(join(config.namespaceBaseRoot, config.namespace));
expect(paths.dataRoot).toBe(join(paths.namespaceRoot, "data"));
expect(paths.updateRoot).toBe(join(paths.namespaceRoot, "updates"));
expect(paths.installerObservationRoot).toBe(join(paths.dataRoot, "observations", "installer"));
});
it("rejects namespace overrides that would escape the namespace base root", () => {
const config: PackagedConfig = {
amrProfile: null,
appVersion: "1.2.3",
daemonCliEntry: null,
daemonSidecarEntry: null,
namespace: "release",
namespaceBaseRoot: "/tmp/open-design-packaged/namespaces",
nodeCommand: null,
resourceRoot: "/tmp/open-design-packaged/resources",
telemetryRelayUrl: null,
updateMetadataUrl: null,
posthogKey: null,
posthogHost: null,
webSidecarEntry: null,
webStandaloneRoot: null,
webOutputMode: "server",
};
expect(() => resolvePackagedNamespacePaths(config, "../release")).toThrow(/namespace/);
});
it("defaults daemon dataRoot to the namespace-scoped packaged data directory", () => {
const config = fakeConfig();
expect(resolvePackagedNamespacePaths(config, config.namespace).dataRoot).toBe(
join(config.namespaceBaseRoot, config.namespace, "data"),
);
});
it("uses OD_DATA_DIR as a base for the namespace-scoped packaged daemon dataRoot", () => {
const config = fakeConfig();
const override = join("C:", "Users", "Fred", "MyProject", "design", ".od");
expect(
resolvePackagedNamespacePaths(config, config.namespace, { OD_DATA_DIR: override }).dataRoot,
).toBe(join(override, "namespaces", config.namespace, "data"));
});
it("keeps shared OD_DATA_DIR overrides isolated across packaged namespaces", () => {
const config = fakeConfig();
const override = join("C:", "Users", "Fred", "MyProject", "design", ".od");
const stable = resolvePackagedNamespacePaths(config, "release-stable-win", {
OD_DATA_DIR: override,
});
const beta = resolvePackagedNamespacePaths(config, "release-beta-win", {
OD_DATA_DIR: override,
});
expect(stable.dataRoot).toBe(join(override, "namespaces", "release-stable-win", "data"));
expect(beta.dataRoot).toBe(join(override, "namespaces", "release-beta-win", "data"));
expect(stable.dataRoot).not.toBe(beta.dataRoot);
});
it("preserves already-scoped packaged OD_DATA_DIR values as the final daemon dataRoot", () => {
const config = fakeConfig();
const override = join(
"C:",
"Users",
"Fred",
"AppData",
"Roaming",
"Open Design",
"namespaces",
config.namespace,
"data",
);
expect(
resolvePackagedNamespacePaths(config, config.namespace, { OD_DATA_DIR: override }).dataRoot,
).toBe(override);
});
it("rejects already-scoped OD_DATA_DIR values that point at a different packaged namespace", () => {
const config = fakeConfig();
const override = join(
"C:",
"Users",
"Fred",
"AppData",
"Roaming",
"Open Design",
"namespaces",
"release-beta-win",
"data",
);
expect(
() =>
resolvePackagedNamespacePaths(config, config.namespace, {
OD_DATA_DIR: override,
}),
).toThrow(PackagedPathAccessError);
});
it("forwards the OD_DATA_DIR-resolved dataRoot into sidecar launch paths", () => {
const config = fakeConfig();
const override = join("C:", "Users", "Fred", "MyProject", "design", ".od");
const paths = resolvePackagedNamespacePaths(config, config.namespace, {
OD_DATA_DIR: override,
});
expect(paths.dataRoot).toBe(join(override, "namespaces", config.namespace, "data"));
expect(paths.namespaceRoot).toBe(join(config.namespaceBaseRoot, config.namespace));
expect(paths.runtimeRoot).toBe(join(config.namespaceBaseRoot, config.namespace, "runtime"));
});
it("does not read process.env implicitly so headless can keep namespace-root OD_DATA_DIR semantics", () => {
const config = fakeConfig();
const original = process.env.OD_DATA_DIR;
try {
process.env.OD_DATA_DIR = join("C:", "Users", "Fred", "MyProject", "design", ".od");
expect(resolvePackagedNamespacePaths(config).dataRoot).toBe(
join(config.namespaceBaseRoot, config.namespace, "data"),
);
} finally {
if (original == null) delete process.env.OD_DATA_DIR;
else process.env.OD_DATA_DIR = original;
}
});
it("rejects relative OD_DATA_DIR values instead of resolving them against cwd", () => {
const config = fakeConfig();
expect(
() => resolvePackagedNamespacePaths(config, config.namespace, { OD_DATA_DIR: "project/.od" }),
).toThrow(/OD_DATA_DIR.*absolute path/);
});
it("surfaces the relative-OD_DATA_DIR rejection as PackagedPathAccessError so packaged main() can show a dialog", () => {
const config = fakeConfig();
let captured: unknown;
try {
resolvePackagedNamespacePaths(config, config.namespace, { OD_DATA_DIR: "project/.od" });
} catch (error) {
captured = error;
}
expect(captured).toBeInstanceOf(PackagedPathAccessError);
const err = captured as PackagedPathAccessError;
expect(err.title).toMatch(/OD_DATA_DIR/);
expect(err.message).toContain("project/.od");
expect(err.message).toMatch(/absolute path/);
});
it("rejects Windows-style OD_DATA_DIR values on non-Windows hosts so the absolute-path guard is platform-correct", () => {
const config = fakeConfig();
const restore = stubPlatform("linux");
try {
expect(
() =>
resolvePackagedNamespacePaths(config, config.namespace, {
OD_DATA_DIR: "C:\\Users\\Fred\\OD",
}),
).toThrow(PackagedPathAccessError);
expect(
() =>
resolvePackagedNamespacePaths(config, config.namespace, {
OD_DATA_DIR: "\\\\server\\share",
}),
).toThrow(PackagedPathAccessError);
} finally {
restore();
}
});
});