Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
375 lines
13 KiB
JavaScript
375 lines
13 KiB
JavaScript
const test = require("node:test");
|
|
const assert = require("node:assert/strict");
|
|
const { readFileSync } = require("node:fs");
|
|
const path = require("node:path");
|
|
|
|
const config = require("../electron-builder.config.cjs");
|
|
|
|
test("unpacked MCP server includes its shared CommonJS dependencies", () => {
|
|
assert.ok(
|
|
config.asarUnpack.includes("electron/mcp/**/*"),
|
|
"MCP server must stay unpacked so Codex can launch it as a child process",
|
|
);
|
|
assert.ok(
|
|
config.asarUnpack.includes("lib/**/*.cjs"),
|
|
"MCP server requires ../../lib/commandBlocklist.cjs from the unpacked runtime path",
|
|
);
|
|
assert.ok(
|
|
config.asarUnpack.includes("lib/**/*.json"),
|
|
"unpacked lib CommonJS modules require sibling JSON data files at runtime",
|
|
);
|
|
});
|
|
|
|
test("build.files includes shared terminal flow constants for main process", () => {
|
|
assert.ok(
|
|
config.files.includes("infrastructure/config/terminalFlowConstants.cjs"),
|
|
"terminalFlowAck.cjs requires infrastructure/config/terminalFlowConstants.cjs at packaged startup",
|
|
);
|
|
assert.ok(
|
|
config.files.includes("infrastructure/config/terminalFlowConstants.json"),
|
|
"terminalFlowConstants.cjs requires sibling terminalFlowConstants.json at packaged startup",
|
|
);
|
|
});
|
|
|
|
test("build.files includes the prompt classifier required by Mosh", () => {
|
|
assert.ok(
|
|
config.files.includes("domain/terminalPromptSecurity.shared.cjs"),
|
|
"packaged Mosh bootstrap requires the shared prompt classifier",
|
|
);
|
|
const promptSecurity = require("../domain/terminalPromptSecurity.shared.cjs");
|
|
assert.equal(promptSecurity.isUntrustedTerminalInputPrompt("验证码:"), true);
|
|
});
|
|
|
|
test("unpacked Tool CLI includes capability runtime dependencies", () => {
|
|
assert.ok(
|
|
config.asarUnpack.includes("electron/cli/**/*"),
|
|
"Tool CLI launcher and scripts must stay unpacked so agents can launch them as child processes",
|
|
);
|
|
assert.ok(
|
|
config.asarUnpack.includes("electron/capabilities/**/*"),
|
|
"Tool CLI requires capability catalog, registry, policy, timeout, and RPC transport modules from the unpacked runtime path",
|
|
);
|
|
assert.ok(
|
|
config.asarUnpack.includes("electron/shared/**/*"),
|
|
"Capability policy may load shared permission grant helpers from the unpacked runtime path",
|
|
);
|
|
});
|
|
|
|
test("build.files excludes per-platform agent binaries", () => {
|
|
const files = config.files;
|
|
const expectExclusions = [
|
|
"!**/@anthropic-ai/claude-agent-sdk-*/**/*",
|
|
"!node_modules/@anthropic-ai/claude-code-*/**/*",
|
|
"!node_modules/@openai/codex-{darwin,linux,linuxmusl,win32}-*/**/*",
|
|
"!node_modules/@github/copilot-{darwin,linux,linuxmusl,win32}-*/**/*",
|
|
"!node_modules/@github/copilot/**/*",
|
|
"!node_modules/opencode-{darwin,linux,linuxmusl,windows}-*/**/*",
|
|
"!node_modules/opencode-ai/**/*",
|
|
];
|
|
for (const glob of expectExclusions) {
|
|
assert.ok(
|
|
files.includes(glob),
|
|
`build.files must exclude platform binary glob: ${glob}`,
|
|
);
|
|
}
|
|
});
|
|
|
|
test("asarUnpack no longer references removed legacy agent packages", () => {
|
|
const unpack = config.asarUnpack.join("\n");
|
|
for (const stale of [
|
|
"@agentclientprotocol/claude-agent-acp",
|
|
"@agentclientprotocol/sdk",
|
|
"@zed-industries/codex-acp",
|
|
]) {
|
|
assert.ok(
|
|
!unpack.includes(stale),
|
|
`asarUnpack must not reference removed package: ${stale}`,
|
|
);
|
|
}
|
|
});
|
|
|
|
test("asarUnpack keeps MCP server runtime deps unpacked", () => {
|
|
// @modelcontextprotocol/sdk is now a direct dep and the MCP server hard-requires it.
|
|
assert.ok(config.asarUnpack.includes("node_modules/@modelcontextprotocol/sdk/**/*"));
|
|
});
|
|
|
|
test("asarUnpack keeps Cursor SDK runtime deps unpacked", () => {
|
|
assert.ok(
|
|
!config.asarUnpack.includes("node_modules/@cursor/sdk/**/*"),
|
|
"Cursor SDK JavaScript can load from app.asar and should not be duplicated into app.asar.unpacked",
|
|
);
|
|
assert.ok(config.asarUnpack.includes("node_modules/@cursor/sdk-*/**/*"));
|
|
assert.ok(config.asarUnpack.includes("node_modules/sqlite3/**/*"));
|
|
});
|
|
|
|
test("beforePack installs missing Cursor SDK packages and builds Windows Hello helper", () => {
|
|
assert.equal(config.beforePack, "./scripts/beforePackCursorSdk.cjs");
|
|
});
|
|
|
|
test("Windows packaging includes the Windows Hello helper executable", () => {
|
|
assert.ok(
|
|
Array.isArray(config.win.extraResources),
|
|
"win.extraResources must be an array",
|
|
);
|
|
assert.ok(
|
|
config.win.extraResources.some((entry) => (
|
|
entry &&
|
|
entry.from === "electron/bridges/windowsHelloHelper/build/${arch}/NetcattyWindowsHello.exe" &&
|
|
entry.to === "windowsHello/NetcattyWindowsHello.exe"
|
|
)),
|
|
"Windows package must include the Windows Hello helper executable for the target arch",
|
|
);
|
|
assert.ok(
|
|
config.files.includes("!electron/bridges/windowsHelloHelper/build/**/*"),
|
|
"Windows Hello build output must not also be copied into app.asar",
|
|
);
|
|
});
|
|
|
|
test("Windows package arch is controlled by pack script CLI flags", () => {
|
|
assert.deepEqual(
|
|
config.win.target,
|
|
["nsis", "portable", "zip"],
|
|
"win.target must not hard-code x64 and arm64 or pack:win-x64 will still invoke arm64 beforePack hooks",
|
|
);
|
|
});
|
|
|
|
test("packaged app declares ssh, telnet, and jms URL protocol support", () => {
|
|
assert.deepEqual(config.protocols, [
|
|
{
|
|
name: "SSH URL",
|
|
schemes: ["ssh"],
|
|
},
|
|
{
|
|
name: "Telnet URL",
|
|
schemes: ["telnet"],
|
|
},
|
|
{
|
|
name: "JumpServer URL",
|
|
schemes: ["jms"],
|
|
},
|
|
]);
|
|
});
|
|
|
|
test("build.files trims release-only dependency payloads", () => {
|
|
const files = config.files;
|
|
for (const glob of [
|
|
"!node_modules/@cursor/sdk/dist/cjs/**/*",
|
|
"!node_modules/@cursor/sdk/dist/**/*.d.ts",
|
|
"!node_modules/@cursor/sdk/dist/**/*.d.ts.map",
|
|
"!node_modules/sqlite3/deps/**/*",
|
|
"!node_modules/**/docs/**/*",
|
|
"!node_modules/**/doc/**/*",
|
|
"!node_modules/**/benchmark/**/*",
|
|
"!node_modules/**/benchmarks/**/*",
|
|
]) {
|
|
assert.ok(files.includes(glob), `build.files must exclude release-only payload: ${glob}`);
|
|
}
|
|
});
|
|
|
|
test("build.files excludes Vite-bundled renderer-only packages", () => {
|
|
const files = config.files;
|
|
for (const glob of [
|
|
"!node_modules/react/**/*",
|
|
"!node_modules/react-dom/**/*",
|
|
"!node_modules/@radix-ui/**/*",
|
|
"!node_modules/ai/**/*",
|
|
"!node_modules/@ai-sdk/**/*",
|
|
"!node_modules/@mdxeditor/**/*",
|
|
"!node_modules/streamdown/**/*",
|
|
"!node_modules/@streamdown/**/*",
|
|
"!node_modules/@tanstack/react-virtual/**/*",
|
|
"!node_modules/pinyin-pro/**/*",
|
|
"!node_modules/re2js/**/*",
|
|
"!node_modules/@eslint-community/regexpp/**/*",
|
|
"!node_modules/clsx/**/*",
|
|
"!node_modules/tailwind-merge/**/*",
|
|
"!node_modules/use-stick-to-bottom/**/*",
|
|
"!node_modules/lexical/**/*",
|
|
"!node_modules/@lexical/**/*",
|
|
"!node_modules/@codemirror/**/*",
|
|
"!node_modules/katex/**/*",
|
|
"!node_modules/shiki/**/*",
|
|
"!node_modules/@shiki/**/*",
|
|
]) {
|
|
assert.ok(
|
|
files.includes(glob),
|
|
`build.files must exclude Vite-bundled renderer package: ${glob}`,
|
|
);
|
|
}
|
|
});
|
|
|
|
test("KaTeX distribution keeps its license notice", () => {
|
|
const notice = readFileSync(
|
|
path.join(__dirname, "..", "public", "licenses", "KaTeX-LICENSE.txt"),
|
|
"utf8",
|
|
);
|
|
assert.match(notice, /Copyright \(c\) 2013-2020 Khan Academy and other contributors/);
|
|
});
|
|
|
|
test("linux packaging uses multi-size build/icons instead of a single 1024px override", async () => {
|
|
assert.equal(
|
|
config.linux.icon,
|
|
"icons",
|
|
"linux.icon must point at build/icons so electron-builder installs hicolor/* sizes",
|
|
);
|
|
assert.equal(config.directories.buildResources, "build");
|
|
|
|
const fs = require("node:fs");
|
|
const path = require("node:path");
|
|
const iconsDir = path.join(__dirname, "..", "build", "icons");
|
|
for (const size of [16, 32, 48, 64, 128, 256, 512]) {
|
|
const file = path.join(iconsDir, `${size}x${size}.png`);
|
|
assert.ok(fs.existsSync(file), `expected Linux icon: build/icons/${size}x${size}.png`);
|
|
}
|
|
|
|
const { convertIcon } = require("app-builder-lib/out/util/iconConverter");
|
|
const projectDir = path.join(__dirname, "..");
|
|
const buildResources = path.join(projectDir, config.directories.buildResources);
|
|
const sources = [config.linux.icon, config.mac?.icon ?? config.icon].filter(Boolean);
|
|
const result = await convertIcon({
|
|
sources,
|
|
fallbackSources: [buildResources],
|
|
roots: [buildResources, projectDir],
|
|
format: "set",
|
|
outDir: path.join(projectDir, "release", ".icon-config-test"),
|
|
});
|
|
const sizes = result.icons.map((icon) => icon.size);
|
|
assert.ok(
|
|
sizes.includes(48) && sizes.includes(256) && !sizes.every((size) => size === 1024),
|
|
`expected standard hicolor sizes, got: ${sizes.join(", ")}`,
|
|
);
|
|
});
|
|
|
|
test("linux packaging includes an Arch Linux pacman package target", () => {
|
|
assert.deepEqual(
|
|
config.linux.target,
|
|
["AppImage", "deb", "rpm", "pacman"],
|
|
"linux package builds must publish AppImage, Debian, RPM, and Arch pacman artifacts",
|
|
);
|
|
});
|
|
|
|
test("rpm packaging disables generated build-id symlinks", () => {
|
|
assert.deepEqual(
|
|
config.rpm?.fpm,
|
|
[
|
|
"--rpm-rpmbuild-define",
|
|
"_build_id_links none",
|
|
"--rpm-rpmbuild-define",
|
|
"__os_install_post %{nil}",
|
|
],
|
|
"RPM packages must skip build-id links and host brp post scripts on RHEL builders",
|
|
);
|
|
});
|
|
|
|
test("rpm packaging uses gzip compression for RHEL-family package hosts", () => {
|
|
// Default electron-builder/fpm RPM compression is xzmt. AlmaLinux/RHEL 8 CI
|
|
// images provide `xz` but not the `xzmt` shim, which makes rpmbuild exit 127.
|
|
assert.equal(
|
|
config.rpm?.compression,
|
|
"gzip",
|
|
"RPM compression must avoid xzmt on RHEL 8 / AlmaLinux 8 package builders",
|
|
);
|
|
});
|
|
|
|
test("Windows package arch is controlled by pack script CLI flags", () => {
|
|
assert.deepEqual(
|
|
config.win.target,
|
|
["nsis", "portable", "zip"],
|
|
"win.target must not hard-code x64 and arm64 or pack:win-x64 will still emit broken arm64 installers",
|
|
);
|
|
});
|
|
|
|
test("windows packaging includes a zip archive target", () => {
|
|
assert.ok(
|
|
config.win.target.includes("zip"),
|
|
"windows package builds must publish a zip archive for no-install environments",
|
|
);
|
|
});
|
|
|
|
test("windows installer registers and removes Explorer folder context menu entries", () => {
|
|
const fs = require("node:fs");
|
|
const path = require("node:path");
|
|
|
|
assert.equal(
|
|
config.nsis.include,
|
|
"build/installer.nsh",
|
|
"NSIS packaging must include the Explorer context-menu installer hooks",
|
|
);
|
|
|
|
const installerScript = fs.readFileSync(
|
|
path.join(__dirname, "..", config.nsis.include),
|
|
"utf8",
|
|
);
|
|
const folderKey = String.raw`Software\Classes\Directory\shell\Netcatty`;
|
|
const backgroundKey = String.raw`Software\Classes\Directory\Background\shell\Netcatty`;
|
|
|
|
assert.match(installerScript, /!macro customInstall\b/);
|
|
assert.match(installerScript, new RegExp(`WriteRegStr SHCTX "${folderKey.replaceAll("\\", "\\\\")}"`));
|
|
assert.match(installerScript, new RegExp(`WriteRegStr SHCTX "${backgroundKey.replaceAll("\\", "\\\\")}"`));
|
|
assert.match(installerScript, /-- --open-terminal-path="%1\."/);
|
|
assert.match(installerScript, /-- --open-terminal-path="%V\."/);
|
|
|
|
assert.match(installerScript, /!macro customUnInstall\b/);
|
|
assert.match(installerScript, new RegExp(`DeleteRegKey SHCTX "${folderKey.replaceAll("\\", "\\\\")}"`));
|
|
assert.match(installerScript, new RegExp(`DeleteRegKey SHCTX "${backgroundKey.replaceAll("\\", "\\\\")}"`));
|
|
});
|
|
|
|
test("windows zip follows the requested build architecture", () => {
|
|
const { Arch } = require("builder-util");
|
|
const { Platform } = require("app-builder-lib");
|
|
const { computeArchToTargetNamesMap } = require("app-builder-lib/out/targets/targetFactory");
|
|
|
|
const rawCliTargets = new Map([[Arch.x64, []]]);
|
|
const targetsByArch = computeArchToTargetNamesMap(
|
|
rawCliTargets,
|
|
{
|
|
platformSpecificBuildOptions: config.win,
|
|
defaultTarget: ["nsis"],
|
|
},
|
|
Platform.WINDOWS,
|
|
);
|
|
|
|
assert.deepEqual(
|
|
targetsByArch.get(Arch.x64)?.slice().sort(),
|
|
["nsis", "portable", "zip"].sort(),
|
|
"pack:win-x64 must publish x64 nsis, portable, and zip",
|
|
);
|
|
assert.equal(
|
|
targetsByArch.has(Arch.arm64),
|
|
false,
|
|
"pack:win-x64 must not publish arm64 nsis/portable/zip without a dedicated arm64 job",
|
|
);
|
|
});
|
|
|
|
test("linux FPM packages use the custom post-install template", () => {
|
|
const fs = require("node:fs");
|
|
const path = require("node:path");
|
|
|
|
for (const [target, afterInstall] of Object.entries({
|
|
deb: config.deb.afterInstall,
|
|
rpm: config.rpm.afterInstall,
|
|
pacman: config.pacman.afterInstall,
|
|
})) {
|
|
assert.equal(
|
|
afterInstall,
|
|
"scripts/linux/after-install.tpl",
|
|
`${target}.afterInstall must point at the custom FPM post-install template`,
|
|
);
|
|
}
|
|
assert.equal(
|
|
config.pacman.afterRemove,
|
|
"scripts/linux/after-remove.tpl",
|
|
"pacman.afterRemove must point at the custom FPM post-remove template",
|
|
);
|
|
|
|
for (const relPath of [config.deb.afterInstall, config.pacman.afterRemove]) {
|
|
const file = path.join(__dirname, "..", relPath);
|
|
const contents = fs.readFileSync(file, "utf8");
|
|
assert.match(
|
|
contents,
|
|
/gtk-update-icon-cache.*\/usr\/share\/icons\/hicolor/,
|
|
`${relPath} must refresh the hicolor icon cache`,
|
|
);
|
|
}
|
|
});
|