Files
open-design/tools/pack/tests/mac.test.ts
T
44350ad03c fix(packaging): serve baked plugin previews in the packaged app (#4035)
* fix(packaging): serve baked plugin previews in the packaged app

The packaged gallery fell back to live, GPU-expensive iframes instead of the
baked hover-pan clips, because of two gaps that only bite the packaged layout:

1. The baked-preview manifest (data/plugin-previews/manifest.json) was never
   bundled — it's absent from the packaged resource trees. The clips live on R2,
   but without the manifest the daemon has no plugin -> clip mapping.
2. Even bundled, the daemon resolved the manifest dir from PROJECT_ROOT. For the
   prebundled sidecar (Resources/app/prebundled/daemon) PROJECT_ROOT resolves to
   Resources/app — two levels up — which has no data/. The bundled resources
   actually live under OD_RESOURCE_ROOT (Resources/open-design), where every
   other tree (skills, plugins, design-systems…) is resolved from.

Fix both: add data/plugin-previews to BUNDLED_RESOURCE_TREES, and resolve
PLUGIN_PREVIEWS_DIR against DAEMON_RESOURCE_ROOT like the other resource dirs
(explicit OD_PLUGIN_PREVIEWS_DIR override + the dev PROJECT_ROOT layout still
win). Verified end-to-end: a freshly built+installed mac app's packaged daemon
now returns bakedPreview blocks for 126 plugins (R2 URLs) instead of none.

* test(tools-pack): assert the plugin-preview manifest is bundled

* test(daemon): cover packaged plugin-previews dir resolution from OD_RESOURCE_ROOT

Address review: extract resolveDaemonPluginPreviewsDir and prove it resolves the
bundled manifest under the resource root in the packaged layout (and falls back
to PROJECT_ROOT in dev). Also seed data/plugin-previews in the win-resources and
mac packager fixtures so copyBundledResourceTrees' new tree doesn't ENOENT.

* fix(daemon): resolve OD_PLUGIN_PREVIEWS_DIR override from the injected env

Address review: the override branch delegated to resolvePluginPreviewsDir, which
re-reads process.env — so an injected env was ignored and the override path was
untestable. Resolve it directly from env.OD_PLUGIN_PREVIEWS_DIR (absolute
passthrough, relative against projectRoot) and add an override test.

* fix(tools-pack): include plugin-previews in the Windows resource-tree cache key

Address review: adding data/plugin-previews to the bundled tree didn't feed the
Windows resource-tree cache key, so a builder with an existing cache entry would
reuse the old bundle without manifest.json. Hash data/plugin-previews into the
key, bump the cache schema version, and add a regression proving a manifest.json
change forces a cache miss.

---------

Co-authored-by: audit <a@b.c>
2026-06-10 07:45:52 +00:00

379 lines
14 KiB
TypeScript

import { access, mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
import os, { tmpdir } from "node:os";
import { dirname, join, resolve } from "node:path";
import process from "node:process";
import { afterEach, describe, expect, it } from "vitest";
import type { ToolPackConfig } from "../src/config.js";
import {
copyResourceTree,
createMacElectronRebuildOptions,
renderMacPackagedConfig,
validateMacNativeRebuildOutput,
} from "../src/mac/app.js";
import { runElectronBuilder } from "../src/mac/builder.js";
import { resolveSeededAppConfigPaths, seedPackagedAppConfig, writeLaunchPackagedConfig } from "../src/mac/index.js";
import { resolveMacPaths } from "../src/mac/paths.js";
async function pathExists(path: string): Promise<boolean> {
try {
await access(path);
return true;
} catch {
return false;
}
}
function makeConfig(root: string, overrides: Partial<ToolPackConfig> = {}): ToolPackConfig {
return {
containerized: false,
electronBuilderCliPath: "/x/electron-builder/cli.js",
electronDistPath: "/x/electron/dist",
electronVersion: "41.3.0",
macCompression: "normal",
namespace: "local-test",
platform: "mac",
portable: false,
removeData: false,
removeLogs: false,
removeProductUserData: false,
removeSidecars: false,
requireVelaCli: false,
roots: {
output: {
appBuilderRoot: join(root, ".tmp", "tools-pack", "out", "mac", "namespaces", "local-test", "builder"),
namespaceRoot: join(root, ".tmp", "tools-pack", "out", "mac", "namespaces", "local-test"),
platformRoot: join(root, ".tmp", "tools-pack", "out", "mac"),
root: join(root, ".tmp", "tools-pack", "out"),
},
runtime: {
namespaceBaseRoot: join(root, ".tmp", "tools-pack", "runtime", "mac", "namespaces"),
namespaceRoot: join(root, ".tmp", "tools-pack", "runtime", "mac", "namespaces", "local-test"),
},
cacheRoot: join(root, ".tmp", "tools-pack", "cache"),
toolPackRoot: join(root, ".tmp", "tools-pack"),
},
silent: true,
signed: false,
to: "app",
webOutputMode: "standalone",
workspaceRoot: root,
...overrides,
};
}
const envState = { odDataDir: process.env.OD_DATA_DIR };
afterEach(() => {
if (envState.odDataDir == null) {
delete process.env.OD_DATA_DIR;
} else {
process.env.OD_DATA_DIR = envState.odDataDir;
}
});
describe("resolveSeededAppConfigPaths", () => {
it("uses workspace .od by default", () => {
const config = makeConfig("/work");
expect(resolveSeededAppConfigPaths(config)).toEqual({
sourcePath: join("/work", ".od", "app-config.json"),
targetPath: join("/work", ".tmp", "tools-pack", "runtime", "mac", "namespaces", "local-test", "data", "app-config.json"),
});
});
it("prefers OD_DATA_DIR when provided", () => {
process.env.OD_DATA_DIR = "/custom/data";
const config = makeConfig("/work");
expect(resolveSeededAppConfigPaths(config)).toEqual({
sourcePath: join("/custom/data", "app-config.json"),
targetPath: join("/work", ".tmp", "tools-pack", "runtime", "mac", "namespaces", "local-test", "data", "app-config.json"),
});
});
it("resolves relative OD_DATA_DIR against the workspace root", () => {
process.env.OD_DATA_DIR = "e2e/ui/.od-data";
const config = makeConfig("/work");
expect(resolveSeededAppConfigPaths(config)).toEqual({
sourcePath: resolve("/work", "e2e", "ui", ".od-data", "app-config.json"),
targetPath: join("/work", ".tmp", "tools-pack", "runtime", "mac", "namespaces", "local-test", "data", "app-config.json"),
});
});
it("expands $HOME-style OD_DATA_DIR values", () => {
process.env.OD_DATA_DIR = "$HOME/.open-design";
const config = makeConfig("/work");
expect(resolveSeededAppConfigPaths(config)).toEqual({
sourcePath: join(os.homedir(), ".open-design", "app-config.json"),
targetPath: join("/work", ".tmp", "tools-pack", "runtime", "mac", "namespaces", "local-test", "data", "app-config.json"),
});
});
});
describe("seedPackagedAppConfig", () => {
it("copies the current app-config into the packaged runtime namespace", async () => {
const root = await mkdtemp(join(tmpdir(), "open-design-tools-pack-mac-"));
try {
const config = makeConfig(root);
const sourceDir = join(root, ".od");
await mkdir(sourceDir, { recursive: true });
await writeFile(
join(sourceDir, "app-config.json"),
`${JSON.stringify({ onboardingCompleted: true, agentId: "codex", agentCliEnv: { codex: { CODEX_BIN: "/Applications/Codex.app/Contents/Resources/codex" } } }, null, 2)}\n`,
"utf8",
);
await seedPackagedAppConfig(config);
await expect(
readFile(join(config.roots.runtime.namespaceRoot, "data", "app-config.json"), "utf8"),
).resolves.toContain('"agentId": "codex"');
} finally {
await rm(root, { force: true, recursive: true });
}
});
it("skips seeding for portable builds", async () => {
const root = await mkdtemp(join(tmpdir(), "open-design-tools-pack-mac-"));
try {
const config = makeConfig(root, { portable: true });
const sourceDir = join(root, ".od");
await mkdir(sourceDir, { recursive: true });
await writeFile(join(sourceDir, "app-config.json"), "{\n \"agentId\": \"codex\"\n}\n", "utf8");
await seedPackagedAppConfig(config);
await expect(
readFile(join(config.roots.runtime.namespaceRoot, "data", "app-config.json"), "utf8"),
).rejects.toMatchObject({ code: "ENOENT" });
} finally {
await rm(root, { force: true, recursive: true });
}
});
});
describe("copyResourceTree", () => {
it("does not embed the build machine Node launcher into mac resources", async () => {
const root = await mkdtemp(join(tmpdir(), "open-design-tools-pack-mac-"));
try {
const config = makeConfig(root);
const paths = resolveMacPaths(config);
const resourceNames = [
"skills",
"design-templates",
"design-systems",
"craft",
"plugins/_official",
"plugins/registry",
"assets/frames",
"assets/community-pets",
"prompt-templates",
"data/plugin-previews",
];
for (const name of resourceNames) {
await mkdir(join(root, name), { recursive: true });
}
await copyResourceTree(config, paths);
expect(await pathExists(join(paths.resourceRoot, "bin", "node"))).toBe(false);
} finally {
await rm(root, { force: true, recursive: true });
}
});
});
describe("renderMacPackagedConfig", () => {
it("omits nodeCommandRelative so packaged mac sidecars use Electron as Node", async () => {
const root = await mkdtemp(join(tmpdir(), "open-design-tools-pack-mac-"));
try {
const config = makeConfig(root);
const packagedConfig = JSON.parse(
renderMacPackagedConfig({
appVersion: "1.2.3",
config,
usePrebundledStandaloneWeb: true,
}),
) as Record<string, unknown>;
expect(packagedConfig).not.toHaveProperty("nodeCommandRelative");
} finally {
await rm(root, { force: true, recursive: true });
}
});
it("bakes the configured updater metadata URL for mac beta validation", async () => {
const root = await mkdtemp(join(tmpdir(), "open-design-tools-pack-mac-"));
try {
const config = makeConfig(root, {
updateMetadataUrl: "http://127.0.0.1:4567/beta/latest/metadata.json",
});
const packagedConfig = JSON.parse(
renderMacPackagedConfig({
appVersion: "1.2.3-beta.0",
config,
usePrebundledStandaloneWeb: true,
}),
) as Record<string, unknown>;
expect(packagedConfig.updateMetadataUrl).toBe("http://127.0.0.1:4567/beta/latest/metadata.json");
} finally {
await rm(root, { force: true, recursive: true });
}
});
});
describe("runElectronBuilder", () => {
async function prepareElectronBuilderConfig(root: string, overrides: Partial<ToolPackConfig>) {
const cliPath = join(root, "fake-electron-builder.mjs");
await writeFile(cliPath, "process.exit(0);\n", "utf8");
const config = makeConfig(root, {
appVersion: "1.2.3.nightly.4",
electronBuilderCliPath: cliPath,
signed: true,
webOutputMode: "server",
...overrides,
});
const paths = resolveMacPaths(config);
await runElectronBuilder(config, paths, ["dir"]);
return JSON.parse(await readFile(paths.appBuilderConfigPath, "utf8")) as {
afterSign?: string;
mac?: {
notarize?: boolean;
};
};
}
it("does not explicitly disable electron-builder notarization for notarized mac builds", async () => {
const root = await mkdtemp(join(tmpdir(), "open-design-tools-pack-mac-"));
try {
const builderConfig = await prepareElectronBuilderConfig(root, { macNotarize: true });
expect(builderConfig.afterSign).toContain("notarize.cjs");
expect(builderConfig.mac).not.toHaveProperty("notarize");
} finally {
await rm(root, { force: true, recursive: true });
}
});
it("keeps signed-only mac builds from invoking electron-builder notarization", async () => {
const root = await mkdtemp(join(tmpdir(), "open-design-tools-pack-mac-"));
try {
const builderConfig = await prepareElectronBuilderConfig(root, { macNotarize: false });
expect(builderConfig.afterSign).toBeUndefined();
expect(builderConfig.mac?.notarize).toBe(false);
} finally {
await rm(root, { force: true, recursive: true });
}
});
});
describe("createMacElectronRebuildOptions", () => {
it("targets the packaged Electron ABI for required native modules", async () => {
const root = await mkdtemp(join(tmpdir(), "open-design-tools-pack-mac-"));
try {
const config = makeConfig(root, { electronVersion: "41.3.0" });
const appRoot = join(root, "assembled", "app");
expect(createMacElectronRebuildOptions(config, appRoot)).toMatchObject({
arch: process.arch,
buildFromSource: false,
buildPath: appRoot,
electronVersion: "41.3.0",
force: true,
mode: "sequential",
onlyModules: ["better-sqlite3"],
platform: "darwin",
projectRootPath: appRoot,
});
} finally {
await rm(root, { force: true, recursive: true });
}
});
});
describe("validateMacNativeRebuildOutput", () => {
it("reports a missing rebuilt native module as missing output", async () => {
const root = await mkdtemp(join(tmpdir(), "open-design-tools-pack-mac-"));
try {
await expect(validateMacNativeRebuildOutput(root)).resolves.toBe(
`native module output is missing: ${join(root, "node_modules", "better-sqlite3", "build", "Release", "better_sqlite3.node")}`,
);
} finally {
await rm(root, { force: true, recursive: true });
}
});
it("preserves non-ENOENT filesystem diagnostics from stat failures", async () => {
const root = await mkdtemp(join(tmpdir(), "open-design-tools-pack-mac-"));
try {
const buildPath = join(root, "node_modules", "better-sqlite3", "build");
const nativePath = join(buildPath, "Release", "better_sqlite3.node");
await mkdir(dirname(buildPath), { recursive: true });
await writeFile(buildPath, "not a directory", "utf8");
const result = await validateMacNativeRebuildOutput(root);
if (process.platform === "win32") {
await expect(Promise.resolve(result)).resolves.toBe(
`native module output is missing: ${nativePath}`,
);
} else {
await expect(Promise.resolve(result)).resolves.toContain(
`native module output could not be inspected: ${nativePath}: ENOTDIR: not a directory, stat '${nativePath}'`,
);
}
} finally {
await rm(root, { force: true, recursive: true });
}
});
});
describe("writeLaunchPackagedConfig", () => {
it("injects the tools-pack runtime namespace root without mutating the packaged app config", async () => {
const root = await mkdtemp(join(tmpdir(), "open-design-tools-pack-mac-"));
try {
const config = makeConfig(root, { namespace: "release-beta", portable: true });
const appPath = join(root, "Open Design.app");
const embeddedConfigPath = join(appPath, "Contents", "Resources", "open-design-config.json");
await mkdir(dirname(embeddedConfigPath), { recursive: true });
await writeFile(
embeddedConfigPath,
`${JSON.stringify(
{
appVersion: "0.5.1-beta.2",
namespace: "packaged-default",
nodeCommandRelative: "open-design/bin/node",
webOutputMode: "standalone",
},
null,
2,
)}\n`,
"utf8",
);
const launchConfigPath = await writeLaunchPackagedConfig(config, appPath);
const launchConfig = JSON.parse(await readFile(launchConfigPath, "utf8")) as Record<string, unknown>;
const embeddedConfig = JSON.parse(await readFile(embeddedConfigPath, "utf8")) as Record<string, unknown>;
expect(launchConfigPath).toBe(join(config.roots.runtime.namespaceRoot, "runtime", "open-design-config.json"));
expect(launchConfig).toMatchObject({
appVersion: "0.5.1-beta.2",
namespace: "release-beta",
namespaceBaseRoot: config.roots.runtime.namespaceBaseRoot,
nodeCommandRelative: "open-design/bin/node",
webOutputMode: "standalone",
});
expect(embeddedConfig).not.toHaveProperty("namespaceBaseRoot");
expect(embeddedConfig.namespace).toBe("packaged-default");
} finally {
await rm(root, { force: true, recursive: true });
}
});
});