Files
open-design/tools/pack/tests/launcher-payload.test.ts
T
0bf1b6d6b8 [codex] converge release workflows and stable dry-runs (#4390)
* fix(tools-pack): use junctions for Windows standalone peer deps

* fix(desktop): expose IPC during startup

* fix(tools-pack): preserve Windows inspect diagnostics

* fix(tools-pack): report Windows inspect status errors

* fix(packaged): use Electron net fetch for app protocol

* fix(packaged): load Windows renderer from web sidecar

* fix(desktop): show Windows packaged window during startup

* fix(packaged): disable Windows GPU startup

* fix(tools-pack): keep Windows core smoke observable

* fix(packaged): remove Windows startup probes

* fix(tools-pack): trace Windows desktop IPC status

* fix(tools-pack): add Windows IPC diagnose loop

* fix(release): default beta-s Windows updater feed

* chore: clean merged test eof

* refactor(release): unify prerelease channel model

* chore(release): close prerelease doc escape hatches

* refactor(release): converge release channel workflows

* fix(release): install toolchain in metadata jobs

* fix(release): build release package before contracts

* chore(release): bump development version to 0.10.1

* fix(e2e): seed windows packaged smoke runtime config

* fix(release): install toolchain for metadata publish

* fix(release): materialize betas metadata checkout

* chore(release): bump development version to 0.10.2

* fix(release): allow betas metadata cold start from s3

* fix(e2e): support betas packaged update scenarios

* fix(release): pass betas channel into packaged smoke

* fix(release): set betas channel during self-hosted builds

* fix(release): verify counted channel reservations

* fix(release): use pnpm cmd for betas windows publish

* fix(release): add betas manifest artifact fallback

* fix(release): skip beta-s public metadata fetch

* fix(release): read beta-s manifests from storage

* fix(release): cache beta windows tools-pack builds

* fix(release): inline beta mac tools-pack builds

* fix(pack): deep sign unsigned mac bundles

* docs(pack): document payload-first beta updater validation

* fix(release): align preview tools-pack cache flow

* fix(release): align prerelease tools-pack cache flow

* fix(release): pass github token to prerelease metadata

* fix(release): setup pnpm before feishu notify

* fix(release): add stable dry-run prepublish flow

* fix(release): accept completed prerelease metadata gate

* fix(release): require stable release branches

* fix(release): converge r2 access checks

* fix(updater): use release channel parser for defaults

* fix(updater): harden windows payload relaunch

* fix(release): converge updater smoke fixture contract

* test(e2e): require silent updater fixture output

* fix(release): align stable windows smoke build path

* fix(ci): include release workspace in validation

* fix(ci): repair release validation lanes

Generated-By: looper 0.9.10+codex.autoclean (runner=fixer, agent=codex)

* fix(ci): restore zero-install Feishu notification

Generated-By: looper 0.9.10+codex.autoclean (runner=fixer, agent=codex)

---------

Co-authored-by: Looper <looper@noreply.github.com>
2026-06-23 06:13:21 +00:00

432 lines
20 KiB
TypeScript

import { execFile } from "node:child_process";
import { access, chmod, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { promisify } from "node:util";
import { LAUNCHER_SCHEMA_VERSION } from "@open-design/launcher-proto";
import { describe, expect, it } from "vitest";
import type { ToolPackConfig, ToolPackPlatform } from "../src/config.js";
import { ToolPackCache } from "../src/cache.js";
import { resolveMacInstallIdentity } from "../src/mac/identity.js";
import {
buildMacLauncherPayloadManifest,
createMacLauncherPayloadArchive,
} from "../src/mac/payload.js";
import { resolveMacPaths } from "../src/mac/paths.js";
import { winResources } from "../src/resources.js";
import {
buildWinLauncherPayloadArchive,
buildWinLauncherPayloadManifest,
validateWinLauncherPayloadArchive,
} from "../src/win/payload.js";
import type { WinBuiltAppManifest, WinPaths } from "../src/win/types.js";
const execFileAsync = promisify(execFile);
function makeConfig(root: string, platform: ToolPackPlatform, namespace: string, appVersion: string): ToolPackConfig {
return {
appVersion,
containerized: false,
electronBuilderCliPath: "/x/electron-builder/cli.js",
electronDistPath: "/x/electron/dist",
electronVersion: "41.3.0",
macCompression: "normal",
namespace,
platform,
portable: false,
removeData: false,
removeLogs: false,
removeProductUserData: false,
removeSidecars: false,
requireVelaCli: false,
roots: {
cacheRoot: join(root, ".tmp", "tools-pack", "cache"),
output: {
appBuilderRoot: join(root, ".tmp", "tools-pack", "out", platform, "namespaces", namespace, "builder"),
namespaceRoot: join(root, ".tmp", "tools-pack", "out", platform, "namespaces", namespace),
platformRoot: join(root, ".tmp", "tools-pack", "out", platform),
root: join(root, ".tmp", "tools-pack", "out"),
},
runtime: {
namespaceBaseRoot: join(root, ".tmp", "tools-pack", "runtime", platform, "namespaces"),
namespaceRoot: join(root, ".tmp", "tools-pack", "runtime", platform, "namespaces", namespace),
},
toolPackRoot: join(root, ".tmp", "tools-pack"),
},
signed: false,
silent: true,
to: platform === "win" ? "nsis" : "app",
webOutputMode: "standalone",
workspaceRoot: root,
};
}
async function expectPathExists(path: string): Promise<void> {
await expect(access(path)).resolves.toBeUndefined();
}
async function writeFakeMacApp(config: ToolPackConfig): Promise<ReturnType<typeof resolveMacPaths>> {
const paths = resolveMacPaths(config);
const identity = resolveMacInstallIdentity(config);
const resourcesRoot = join(paths.appPath, "Contents", "Resources");
const executablePath = join(paths.appPath, "Contents", "MacOS", identity.executableName);
await mkdir(join(paths.appPath, "Contents", "MacOS"), { recursive: true });
await mkdir(join(resourcesRoot, "open-design", "bin"), { recursive: true });
await mkdir(join(resourcesRoot, "open-design", "prebundled", "daemon"), { recursive: true });
await mkdir(join(resourcesRoot, "open-design", "prebundled", "web"), { recursive: true });
await mkdir(join(resourcesRoot, "open-design-web-standalone"), { recursive: true });
await writeFile(executablePath, "#!/bin/sh\nexit 0\n", "utf8");
await chmod(executablePath, 0o755);
await writeFile(join(resourcesRoot, "open-design", "bin", "node"), "#!/bin/sh\nexit 0\n", "utf8");
await writeFile(join(resourcesRoot, "open-design", "prebundled", "daemon", "daemon-sidecar.mjs"), "export {};\n", "utf8");
await writeFile(join(resourcesRoot, "open-design", "prebundled", "web", "web-sidecar.mjs"), "export {};\n", "utf8");
await writeFile(
join(resourcesRoot, "open-design-config.json"),
`${JSON.stringify({
appVersion: config.appVersion,
daemonSidecarEntryRelative: "open-design/prebundled/daemon/daemon-sidecar.mjs",
namespace: config.namespace,
nodeCommandRelative: "open-design/bin/node",
webOutputMode: "standalone",
webSidecarEntryRelative: "open-design/prebundled/web/web-sidecar.mjs",
}, null, 2)}\n`,
"utf8",
);
return paths;
}
function createWinPaths(root: string, namespace: string): WinPaths {
const namespaceRoot = join(root, ".tmp", "tools-pack", "out", "win", "namespaces", namespace);
return {
appBuilderConfigPath: join(namespaceRoot, "builder-config.json"),
appBuilderOutputRoot: join(namespaceRoot, "builder"),
assembledAppRoot: join(namespaceRoot, "assembled", "app"),
assembledMainEntryPath: join(namespaceRoot, "assembled", "app", "main.cjs"),
assembledPackageJsonPath: join(namespaceRoot, "assembled", "app", "package.json"),
assembledPrebundledRoot: join(namespaceRoot, "assembled", "app", "prebundled"),
blockmapPath: join(namespaceRoot, "builder", "Open Design-release-beta-win-setup.exe.blockmap"),
builtManifestPath: join(namespaceRoot, "built-app.json"),
daemonCliPrebundleEntrypointPath: join(namespaceRoot, "prebundle-entrypoints", "daemon-cli.js"),
daemonCliPrebundlePath: join(namespaceRoot, "assembled", "app", "prebundled", "daemon", "daemon-cli.mjs"),
daemonPrebundleMetaPath: join(namespaceRoot, "prebundle-meta", "daemon.meta.json"),
daemonPrebundleRoot: join(namespaceRoot, "assembled", "app", "prebundled", "daemon"),
daemonSidecarPrebundleEntrypointPath: join(namespaceRoot, "prebundle-entrypoints", "daemon-sidecar.js"),
daemonSidecarPrebundlePath: join(namespaceRoot, "assembled", "app", "prebundled", "daemon", "daemon-sidecar.mjs"),
exePath: join(namespaceRoot, "builder", "Open Design-release-beta-win.exe"),
installDir: join(namespaceRoot, "runtime", "install", "Open Design Beta"),
installedExePath: join(namespaceRoot, "runtime", "install", "Open Design Beta", "Open Design.exe"),
installerBasePayloadPath: join(namespaceRoot, "installer", "payload-base.7z"),
installerOverlayPayloadPath: join(namespaceRoot, "installer", "payload-overlay.7z"),
installerScriptPath: join(namespaceRoot, "installer", "installer.nsi"),
launcherPayloadPath: join(namespaceRoot, "payload", "Open Design-release-beta-win-payload.7z"),
publicDesktopShortcutPath: join(namespaceRoot, "desktop", "public.lnk"),
latestYmlPath: join(namespaceRoot, "builder", "latest.yml"),
installMarkerPath: join(namespaceRoot, "logs", "install.marker.json"),
installTimingPath: join(namespaceRoot, "logs", "install.timing.json"),
nsisLogPath: join(namespaceRoot, "logs", "nsis.log"),
nsisIncludePath: join(namespaceRoot, "nsis", "installer.nsh"),
packagedConfigPath: join(namespaceRoot, "open-design-config.json"),
packagedMainPrebundleMetaPath: join(namespaceRoot, "prebundle-meta", "packaged-main.meta.json"),
packagedMainPrebundlePath: join(namespaceRoot, "assembled", "app", "prebundled", "packaged-main.mjs"),
resourceRoot: join(namespaceRoot, "resources", "open-design"),
setupPath: join(namespaceRoot, "builder", "Open Design-release-beta-win-setup.exe"),
setupZipPath: join(namespaceRoot, "builder", "Open Design-release-beta-win-portable.zip"),
startMenuShortcutPath: join(namespaceRoot, "start-menu.lnk"),
tarballsRoot: join(namespaceRoot, "tarballs"),
userDesktopShortcutPath: join(namespaceRoot, "desktop", "user.lnk"),
uninstallMarkerPath: join(namespaceRoot, "logs", "uninstall.marker.json"),
uninstallTimingPath: join(namespaceRoot, "logs", "uninstall.timing.json"),
uninstallerPath: join(namespaceRoot, "runtime", "install", "Open Design Beta", "Uninstall.exe"),
webStandaloneHookAuditPath: join(namespaceRoot, "web-standalone-after-pack-audit.json"),
webStandaloneHookConfigPath: join(namespaceRoot, "web-standalone-after-pack-config.json"),
webSidecarPrebundleMetaPath: join(namespaceRoot, "prebundle-meta", "web-sidecar.meta.json"),
webSidecarPrebundlePath: join(namespaceRoot, "assembled", "app", "prebundled", "web-sidecar.mjs"),
winIconPath: join(namespaceRoot, "resources", "win", "icon.ico"),
unpackedExePath: join(namespaceRoot, "builder", "win-unpacked", "Open Design.exe"),
unpackedRoot: join(namespaceRoot, "builder", "win-unpacked"),
};
}
async function writeFakeWinUnpackedApp(root: string, namespace: string, version: string): Promise<{
builtApp: WinBuiltAppManifest;
paths: WinPaths;
}> {
const paths = createWinPaths(root, namespace);
await mkdir(join(paths.unpackedRoot, "resources"), { recursive: true });
await writeFile(join(paths.unpackedRoot, "Open Design.exe"), "fake executable\n", "utf8");
await writeFile(
join(paths.unpackedRoot, "resources", "open-design-config.json"),
`${JSON.stringify({
appVersion: version,
daemonSidecarEntryRelative: "open-design/prebundled/daemon/daemon-sidecar.mjs",
namespace,
nodeCommandRelative: "open-design/bin/node",
webOutputMode: "standalone",
webSidecarEntryRelative: "open-design/prebundled/web/web-sidecar.mjs",
}, null, 2)}\n`,
"utf8",
);
await mkdir(join(paths.unpackedRoot, "resources", "app"), { recursive: true });
await writeFile(
join(paths.unpackedRoot, "resources", "app", "package.json"),
`${JSON.stringify({ name: "open-design-packaged-app", version })}\n`,
"utf8",
);
await mkdir(join(paths.packagedConfigPath, ".."), { recursive: true });
await writeFile(
paths.packagedConfigPath,
`${JSON.stringify({
appVersion: version,
daemonSidecarEntryRelative: "open-design/prebundled/daemon/daemon-sidecar.mjs",
namespace,
nodeCommandRelative: "open-design/bin/node",
webOutputMode: "standalone",
webSidecarEntryRelative: "open-design/prebundled/web/web-sidecar.mjs",
}, null, 2)}\n`,
"utf8",
);
return {
builtApp: {
appBuilderOutputRoot: paths.appBuilderOutputRoot,
cacheEntryPath: null,
configPath: paths.packagedConfigPath,
executablePath: join(paths.unpackedRoot, "Open Design.exe"),
source: "namespace",
unpackedRoot: paths.unpackedRoot,
version: 1,
webStandaloneHookAuditPath: null,
},
paths,
};
}
describe("tools-pack launcher payload archives", () => {
it("builds channel and namespace scoped payload manifests for both desktop platforms", () => {
const macIdentity = resolveMacInstallIdentity({ appVersion: "0.9.0-beta.2", namespace: "release-beta" });
expect(buildMacLauncherPayloadManifest({
channel: "beta",
executableName: macIdentity.executableName,
namespace: "release-beta",
publicAppBundleName: macIdentity.publicAppBundleName,
version: "0.9.0-beta.2",
})).toEqual({
appBundleName: "Open Design Beta.app",
channel: "beta",
entry: {
cwd: "payload/Open Design Beta.app",
executable: "payload/Open Design Beta.app/Contents/MacOS/Open Design Beta",
},
namespace: "release-beta",
payloadRoot: "payload",
platform: "darwin",
schemaVersion: LAUNCHER_SCHEMA_VERSION,
version: "0.9.0-beta.2",
});
expect(buildWinLauncherPayloadManifest({
channel: "beta",
namespace: "release-beta-win",
version: "0.9.0-beta.2",
})).toEqual({
channel: "beta",
entry: {
cwd: "payload",
executable: "payload/Open Design.exe",
},
namespace: "release-beta-win",
payloadRoot: "payload",
platform: "win32",
schemaVersion: LAUNCHER_SCHEMA_VERSION,
version: "0.9.0-beta.2",
});
});
it.skipIf(process.platform !== "darwin")("creates a mac payload zip with bootstrap-readable contents", async () => {
const root = await mkdtemp(join(tmpdir(), "od-tools-pack-mac-payload-"));
try {
const config = makeConfig(root, "mac", "release-beta", "0.9.0-beta.2");
const paths = await writeFakeMacApp(config);
const archivePath = await createMacLauncherPayloadArchive(config, paths);
const extractRoot = join(root, "extracted");
await mkdir(extractRoot, { recursive: true });
await execFileAsync("ditto", ["-x", "-k", archivePath, extractRoot]);
const manifest = JSON.parse(await readFile(join(extractRoot, "manifest.json"), "utf8")) as {
appBundleName: string;
entry: { executable: string };
version: string;
};
expect(manifest.appBundleName).toBe("Open Design Beta.app");
expect(manifest.entry.executable).toBe("payload/Open Design Beta.app/Contents/MacOS/Open Design Beta");
expect(manifest.version).toBe("0.9.0-beta.2");
await expectPathExists(join(extractRoot, manifest.entry.executable));
await expectPathExists(join(
extractRoot,
"payload",
"Open Design Beta.app",
"Contents",
"Resources",
"open-design-config.json",
));
} finally {
await rm(root, { force: true, recursive: true });
}
});
it.skipIf(process.platform !== "win32")("creates a Windows payload 7z with bootstrap-readable contents", async () => {
const root = await mkdtemp(join(tmpdir(), "od-tools-pack-win-payload-"));
try {
const namespace = "release-beta-win";
const version = "0.9.0-beta.2";
const config = makeConfig(root, "win", namespace, version);
const { builtApp, paths } = await writeFakeWinUnpackedApp(root, namespace, version);
await buildWinLauncherPayloadArchive(config, paths, builtApp);
const extractRoot = join(root, "extracted");
await mkdir(extractRoot, { recursive: true });
await execFileAsync(winResources.sevenZipExe, ["x", paths.launcherPayloadPath, `-o${extractRoot}`, "-y"]);
const manifest = JSON.parse(await readFile(join(extractRoot, "manifest.json"), "utf8")) as {
entry: { executable: string };
namespace: string;
platform: string;
version: string;
};
expect(manifest.namespace).toBe(namespace);
expect(manifest.platform).toBe("win32");
expect(manifest.entry.executable).toBe("payload/Open Design.exe");
expect(manifest.version).toBe(version);
await expectPathExists(join(extractRoot, "payload", "Open Design.exe"));
await expectPathExists(join(extractRoot, "payload", "resources", "open-design-config.json"));
} finally {
await rm(root, { force: true, recursive: true });
}
});
it.skipIf(process.platform !== "win32")("validates Windows launcher payload archives", async () => {
const root = await mkdtemp(join(tmpdir(), "od-tools-pack-win-payload-validate-"));
try {
const namespace = "release-beta-win";
const version = "0.9.0-beta.2";
const config = makeConfig(root, "win", namespace, version);
const { builtApp, paths } = await writeFakeWinUnpackedApp(root, namespace, version);
await buildWinLauncherPayloadArchive(config, paths, builtApp);
await expect(validateWinLauncherPayloadArchive({
expectedVersion: version,
namespace,
payloadPath: paths.launcherPayloadPath,
workspaceRoot: root,
})).resolves.toMatchObject({
manifest: {
namespace,
version,
},
valid: true,
});
await expect(validateWinLauncherPayloadArchive({
expectedVersion: "0.9.0-beta.3",
namespace,
payloadPath: paths.launcherPayloadPath,
workspaceRoot: root,
})).rejects.toThrow("launcher payload manifest version expected");
} finally {
await rm(root, { force: true, recursive: true });
}
});
it.skipIf(process.platform !== "win32")("reuses the Windows payload archive when base and overlay metadata are unchanged", async () => {
const root = await mkdtemp(join(tmpdir(), "od-tools-pack-win-payload-cache-"));
try {
const namespace = "release-beta-win";
const initialVersion = "0.9.0-beta.0";
const { builtApp, paths } = await writeFakeWinUnpackedApp(root, namespace, initialVersion);
const cache = new ToolPackCache(join(root, "cache"));
for (const version of ["0.9.0-beta.1", "0.9.0-beta.2", "0.9.0-beta.2"]) {
const config = makeConfig(root, "win", namespace, version);
await writeFile(
paths.packagedConfigPath,
`${JSON.stringify({
appVersion: version,
daemonSidecarEntryRelative: "open-design/prebundled/daemon/daemon-sidecar.mjs",
namespace,
nodeCommandRelative: "open-design/bin/node",
webOutputMode: "standalone",
webSidecarEntryRelative: "open-design/prebundled/web/web-sidecar.mjs",
}, null, 2)}\n`,
"utf8",
);
await buildWinLauncherPayloadArchive(config, paths, builtApp, cache);
}
const payloadCacheEntries = cache.report().entries.filter((entry) => entry.nodeId === "win.launcher-payload-base");
expect(payloadCacheEntries.map((entry) => entry.status)).toEqual(["miss", "hit", "hit"]);
const archiveCacheEntries = cache.report().entries.filter((entry) => entry.nodeId === "win.launcher-payload");
expect(archiveCacheEntries.map((entry) => entry.status)).toEqual(["miss", "miss", "hit"]);
expect(archiveCacheEntries.at(-1)?.materialized).toEqual([
expect.objectContaining({ from: "payload.7z", to: paths.launcherPayloadPath }),
]);
const extractRoot = join(root, "extracted");
await mkdir(extractRoot, { recursive: true });
await execFileAsync(winResources.sevenZipExe, ["x", paths.launcherPayloadPath, `-o${extractRoot}`, "-y"]);
const manifest = JSON.parse(await readFile(join(extractRoot, "manifest.json"), "utf8")) as { version: string };
const config = JSON.parse(
await readFile(join(extractRoot, "payload", "resources", "open-design-config.json"), "utf8"),
) as { appVersion: string };
const packageJson = JSON.parse(
await readFile(join(extractRoot, "payload", "resources", "app", "package.json"), "utf8"),
) as { version: string };
expect(manifest.version).toBe("0.9.0-beta.2");
expect(config.appVersion).toBe("0.9.0-beta.2");
expect(packageJson.version).toBe("0.9.0-beta.2");
} finally {
await rm(root, { force: true, recursive: true });
}
});
it.skipIf(process.platform !== "win32")("can seed the Windows launcher payload from the NSIS base archive", async () => {
const root = await mkdtemp(join(tmpdir(), "od-tools-pack-win-payload-nsis-seed-"));
try {
const namespace = "release-beta-win";
const version = "0.9.0-beta.3";
const config = makeConfig(root, "win", namespace, version);
const { builtApp, paths } = await writeFakeWinUnpackedApp(root, namespace, version);
const cache = new ToolPackCache(join(root, "cache"));
await mkdir(join(paths.installerBasePayloadPath, ".."), { recursive: true });
await execFileAsync(winResources.sevenZipExe, [
"a",
"-t7z",
paths.installerBasePayloadPath,
"resources",
], { cwd: paths.unpackedRoot, windowsHide: true });
await buildWinLauncherPayloadArchive(config, paths, builtApp, cache, { seedFromInstallerPayload: true });
expect(cache.report().entries.some((entry) => entry.nodeId === "win.launcher-payload-base")).toBe(false);
expect(cache.report().entries.some((entry) => entry.nodeId === "win.launcher-payload")).toBe(true);
const extractRoot = join(root, "extracted");
await mkdir(extractRoot, { recursive: true });
await execFileAsync(winResources.sevenZipExe, ["x", paths.launcherPayloadPath, `-o${extractRoot}`, "-y"]);
const manifest = JSON.parse(await readFile(join(extractRoot, "manifest.json"), "utf8")) as { version: string };
const configJson = JSON.parse(
await readFile(join(extractRoot, "payload", "resources", "open-design-config.json"), "utf8"),
) as { appVersion: string };
expect(manifest.version).toBe(version);
expect(configJson.appVersion).toBe(version);
await expectPathExists(join(extractRoot, "payload", "Open Design.exe"));
await expectPathExists(join(extractRoot, "payload", "resources"));
} finally {
await rm(root, { force: true, recursive: true });
}
});
});