import { describe, expect, it } from "vitest"; import { appImageBootstrapJs, appImageWrapperScript, CLI_ENTRY_RELPATH, cliInstallKind, LINUX_EXECUTABLE, MAC_EXECUTABLE, mergeWindowsUserPath, posixLauncherScript, WIN_EXECUTABLE, windowsLauncherScript, } from "../src/launcher.js"; describe("posixLauncherScript", () => { const script = posixLauncherScript(); it("is a /bin/sh script that resolves symlinks before locating the runtime", () => { expect(script.startsWith("#!/bin/sh\n")).toBe(true); expect(script).toContain('while [ -h "$SOURCE" ]'); expect(script).toContain("readlink"); }); it("targets the bundled CLI entry and both platform runtime locations", () => { expect(script).toContain(`CLI_ENTRY="$APP_DIR/${CLI_ENTRY_RELPATH}"`); // macOS: resources/app -> ../../MacOS/; Linux: -> ../../. expect(script).toContain(`"$APP_DIR/../../MacOS/${MAC_EXECUTABLE}"`); expect(script).toContain(`"$APP_DIR/../../${LINUX_EXECUTABLE}"`); }); it("execs as Node and forwards all arguments", () => { expect(script).toContain("export ELECTRON_RUN_AS_NODE=1"); expect(script).toContain('exec "$CANDIDATE" "$CLI_ENTRY" "$@"'); }); it("fails with a message when no runtime is found", () => { expect(script).toContain("exit 1"); }); }); describe("windowsLauncherScript", () => { const script = windowsLauncherScript(); it("uses CRLF line endings (cmd.exe is unreliable with bare LF)", () => { expect(script).toContain("\r\n"); expect(script.split("\r\n").join("")).not.toContain("\n"); }); it("finds the exe three levels up from bin\\ and the CLI entry in the app dir", () => { expect(script).toContain(`"%~dp0..\\..\\..\\${WIN_EXECUTABLE}"`); expect(script).toContain(`"%~dp0..\\${CLI_ENTRY_RELPATH.replaceAll("/", "\\")}"`); }); it("runs as Node, forwards arguments and propagates the exit code", () => { expect(script).toContain('set "ELECTRON_RUN_AS_NODE=1"'); expect(script).toContain(" %*"); expect(script).toContain("exit /b %errorlevel%"); }); }); describe("appImageWrapperScript", () => { const appImage = "/home/user/Apps/penguin-desktop-linux-x86_64.AppImage"; const script = appImageWrapperScript(appImage); it("bakes in the AppImage path and guards against it disappearing", () => { expect(script).toContain(`APPIMAGE_PATH='${appImage}'`); expect(script).toContain('if [ ! -x "$APPIMAGE_PATH" ]'); }); it("invokes the AppImage as Node with the bootstrap and forwards arguments", () => { expect(script).toContain("export ELECTRON_RUN_AS_NODE=1"); // The -- is load-bearing: without it Node swallows leading-dash arguments // (penguin --help) as its own flags instead of passing them to the CLI. expect(script).toContain(`exec "$APPIMAGE_PATH" -e '${appImageBootstrapJs()}' -- "$@"`); }); it("refuses paths containing single quotes (they would break the quoting)", () => { expect(() => appImageWrapperScript("/tmp/it's.AppImage")).toThrow(/single quote/); }); }); describe("appImageBootstrapJs", () => { const js = appImageBootstrapJs(); it("contains no single quotes (embedded in a single-quoted shell string)", () => { expect(js).not.toContain("'"); }); it("resolves the CLI entry relative to process.execPath and fixes argv", () => { expect(js).toContain("process.execPath"); expect(js).toContain('"resources","app"'); expect(js).toContain('"penguin-cli","dist","index.js"'); // node -e argv is [execPath, ...args]; the CLI slices argv from index 2, so the // entry path must be spliced in at index 1. expect(js).toContain("process.argv.splice(1,0,cli)"); expect(js).toContain("import(cli)"); }); it("is valid JavaScript", () => { // eslint-disable-next-line @typescript-eslint/no-implied-eval expect(() => new Function(js)).not.toThrow(); }); }); describe("mergeWindowsUserPath", () => { const bin = "C:\\Program Files\\PenguinHarness\\resources\\app\\bin"; it("appends to an existing value with a semicolon", () => { expect(mergeWindowsUserPath("C:\\other", bin)).toBe(`C:\\other;${bin}`); }); it("does not double the separator when the value ends with one", () => { expect(mergeWindowsUserPath("C:\\other;", bin)).toBe(`C:\\other;${bin}`); }); it("starts a missing or empty value with just the bin dir", () => { expect(mergeWindowsUserPath(null, bin)).toBe(bin); expect(mergeWindowsUserPath("", bin)).toBe(bin); expect(mergeWindowsUserPath(" ", bin)).toBe(bin); }); it("is idempotent: returns null when already present", () => { expect(mergeWindowsUserPath(`C:\\other;${bin}`, bin)).toBeNull(); }); it("matches case-insensitively and ignores quotes and trailing slashes", () => { expect( mergeWindowsUserPath(`c:\\program files\\penguinharness\\RESOURCES\\app\\BIN`, bin), ).toBeNull(); expect(mergeWindowsUserPath(`"${bin}"`, bin)).toBeNull(); expect(mergeWindowsUserPath(`${bin}\\`, bin)).toBeNull(); }); it("preserves the existing value verbatim (including %VAR% entries)", () => { const current = "%USERPROFILE%\\bin;C:\\tools"; expect(mergeWindowsUserPath(current, bin)).toBe(`${current};${bin}`); }); }); describe("cliInstallKind", () => { it("is null for dev runs regardless of platform", () => { expect(cliInstallKind({ packaged: false, platform: "darwin", appImagePath: null })).toBeNull(); expect(cliInstallKind({ packaged: false, platform: "win32", appImagePath: null })).toBeNull(); }); it("maps packaged macOS and Windows to their installers", () => { expect(cliInstallKind({ packaged: true, platform: "darwin", appImagePath: null })).toBe( "darwin", ); expect(cliInstallKind({ packaged: true, platform: "win32", appImagePath: null })).toBe( "windows", ); }); it("on Linux only the AppImage form installs (deb ships /usr/bin/penguin itself)", () => { expect(cliInstallKind({ packaged: true, platform: "linux", appImagePath: null })).toBeNull(); expect(cliInstallKind({ packaged: true, platform: "linux", appImagePath: "" })).toBeNull(); expect( cliInstallKind({ packaged: true, platform: "linux", appImagePath: "/x/y.AppImage" }), ).toBe("appimage"); }); });