Files
paseo/packages/desktop/src/daemon/desktop-packaging.test.ts
T
efd7ab3420 Fix macOS packaged CLI daemon Dock icons (#1759)
* Fix macOS CLI daemon relaunch path

* test(cli): mock helper existence in daemon launch test

* fix(desktop): launch packaged CLI through Helper

The packaged macOS CLI shim entered through the main app executable, so daemon supervision inherited app lifecycle behavior and surfaced Dock icons. Make Helper the required macOS CLI runtime, keep daemon relaunches on process.execPath, and cover cold bundled CLI daemon starts in release smoke.

---------

Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
2026-06-29 20:45:53 +02:00

143 lines
5.0 KiB
TypeScript

import { spawnSync } from "node:child_process";
import {
chmodSync,
copyFileSync,
mkdirSync,
mkdtempSync,
readFileSync,
rmSync,
writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { describe, expect, it } from "vitest";
const packageRoot = join(dirname(fileURLToPath(import.meta.url)), "..", "..");
function writeExecutable(filePath: string, contents: string): void {
writeFileSync(filePath, contents, "utf8");
chmodSync(filePath, 0o755);
}
function createFakeMacBundle(options: { includeHelper: boolean }): {
root: string;
shimPath: string;
} {
const root = mkdtempSync(join(tmpdir(), "paseo-cli-shim-test-"));
const appPath = join(root, "Paseo.app");
const contentsPath = join(appPath, "Contents");
const resourcesPath = join(contentsPath, "Resources");
const shimPath = join(resourcesPath, "bin", "paseo");
const mainPath = join(contentsPath, "MacOS", "Paseo");
const helperPath = join(
contentsPath,
"Frameworks",
"Paseo Helper.app",
"Contents",
"MacOS",
"Paseo Helper",
);
mkdirSync(dirname(shimPath), { recursive: true });
mkdirSync(dirname(mainPath), { recursive: true });
copyFileSync(join(packageRoot, "bin", "paseo"), shimPath);
chmodSync(shimPath, 0o755);
writeExecutable(mainPath, "#!/bin/sh\necho main-executable\n");
if (options.includeHelper) {
mkdirSync(dirname(helperPath), { recursive: true });
writeExecutable(
helperPath,
[
"#!/bin/sh",
'printf "helper env=%s/%s\\n" "$ELECTRON_RUN_AS_NODE" "$PASEO_NODE_ENV"',
'printf "args=%s\\n" "$*"',
"",
].join("\n"),
);
}
return { root, shimPath };
}
describe("desktop packaging", () => {
it("unpacks server zsh shell integration files for external shells", () => {
const config = readFileSync(join(packageRoot, "electron-builder.yml"), "utf8");
expect(config).toContain(
"node_modules/@getpaseo/server/dist/server/terminal/shell-integration/**/*",
);
expect(config).not.toContain(
"node_modules/@getpaseo/server/dist/src/terminal/shell-integration/**/*",
);
});
it("excludes package debug/source files from the packaged app", () => {
const config = readFileSync(join(packageRoot, "electron-builder.yml"), "utf8");
expect(config).toContain("!**/*.map");
expect(config).toContain("!node_modules/@getpaseo/*/src/**");
expect(config).toContain("!node_modules/@getpaseo/**/*.test.*");
expect(config).toContain("!node_modules/@getpaseo/**/*.spec.*");
});
it("excludes the bundled daemon web UI from the packaged app", () => {
const config = readFileSync(join(packageRoot, "electron-builder.yml"), "utf8");
expect(config).toContain("!node_modules/@getpaseo/server/dist/server/web-ui/**");
});
// electron-builder packs production dependencies declared in package.json into
// app.asar. Runtime code in runtime-paths.ts and bin/paseo dynamically resolves
// these workspace packages by string, so static analysis (TypeScript, Knip) cannot
// see the link. If a runtime-required workspace dep is dropped from
// dependencies, the build still succeeds but ships a broken bundle. This
// assertion is the safety net.
it("declares all workspace packages required at runtime", () => {
const pkg = JSON.parse(readFileSync(join(packageRoot, "package.json"), "utf8")) as {
dependencies?: Record<string, string>;
};
const deps = pkg.dependencies ?? {};
for (const required of ["@getpaseo/cli", "@getpaseo/server"]) {
expect(deps[required], `${required} must be declared in dependencies`).toBe("*");
}
});
it("launches the packaged macOS CLI through Helper instead of the main app executable", () => {
if (process.platform === "win32") return;
const bundle = createFakeMacBundle({ includeHelper: true });
try {
const result = spawnSync(bundle.shimPath, ["--version"], { encoding: "utf8" });
expect(result.status).toBe(0);
expect(result.stdout).toContain("helper env=1/production");
expect(result.stdout).toContain("node-entrypoint-runner.js");
expect(result.stdout).toContain("node-script");
expect(result.stdout).toContain("@getpaseo/cli/dist/index.js");
expect(result.stdout).toContain("--version");
expect(result.stdout).not.toContain("main-executable");
} finally {
rmSync(bundle.root, { recursive: true, force: true });
}
});
it("fails packaged macOS CLI startup when Helper is missing", () => {
if (process.platform === "win32") return;
const bundle = createFakeMacBundle({ includeHelper: false });
try {
const result = spawnSync(bundle.shimPath, ["--version"], { encoding: "utf8" });
expect(result.status).toBe(1);
expect(result.stderr).toContain("Bundled Paseo Helper executable not found");
expect(result.stdout).not.toContain("main-executable");
} finally {
rmSync(bundle.root, { recursive: true, force: true });
}
});
});