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 cli=%s\\n" "$ELECTRON_RUN_AS_NODE" "$PASEO_NODE_ENV" "$PASEO_CLI"', '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/**"); }); it("registers Paseo agent links with the operating system", () => { const config = readFileSync(join(packageRoot, "electron-builder.yml"), "utf8"); expect(config).toContain("name: Paseo agent link"); expect(config).toContain("- paseo"); }); // 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; }; 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 cli=${bundle.shimPath}`); 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 }); } }); });