1import * as NodeFS from "node:fs";
2import * as NodeOS from "node:os";
3import * as NodePath from "node:path";
4
5import { assert, describe, it } from "vite-plus/test";
6
7import {
8 makeDevelopmentEnvironmentScript,
9 makeDevelopmentLauncherScript,
10 resolveElectronBinaryPath,
11 resolveMacBundleInfoPlistStrings,
12 resolveMacCodeSignArguments,
13 resolveMacLauncherIconPaths,
14 resolveMacLauncherPaths,
15 writeDevelopmentLauncherScript,
16} from "./electron-launcher.mjs";
17
18describe("electron development launcher", () => {
19 it("uses captured values only as fallbacks for a live runner environment", () => {
20 const environmentScript = makeDevelopmentEnvironmentScript({
21 VITE_DEV_SERVER_URL: "http://127.0.0.1:8526",
22 T3CODE_PORT: "16566",
23 T3CODE_HOME: "/tmp/t3",
24 T3CODE_OTLP_PROTOCOL: "http/protobuf",
25 });
26
27 assert.include(
28 environmentScript,
29 "if [ -z \"${VITE_DEV_SERVER_URL:-}\" ]; then export VITE_DEV_SERVER_URL='http://127.0.0.1:8526'; fi",
30 );
31 assert.include(
32 environmentScript,
33 "if [ -z \"${T3CODE_OTLP_PROTOCOL:-}\" ]; then export T3CODE_OTLP_PROTOCOL='http/protobuf'; fi",
34 );
35 assert.notInclude(environmentScript, "\nexport VITE_DEV_SERVER_URL=");
36 });
37
38 it("keeps the launcher script free of volatile environment values", () => {
39 const script = makeDevelopmentLauncherScript({
40 electronBinaryPath: "/repo/node_modules/electron/Electron",
41 mainEntryPath: "/repo/apps/desktop/dist-electron/main.cjs",
42 desktopRoot: "/repo/apps/desktop",
43 environmentFilePath: "/repo/apps/desktop/.electron-runtime/dev-environment.sh",
44 });
45
46 assert.include(
47 script,
48 "if [ -f '/repo/apps/desktop/.electron-runtime/dev-environment.sh' ]; then . '/repo/apps/desktop/.electron-runtime/dev-environment.sh'; fi",
49 );
50 assert.notInclude(script, "VITE_DEV_SERVER_URL");
51 assert.include(
52 script,
53 "exec '/repo/node_modules/electron/Electron' --t3code-dev-root='/repo/apps/desktop' '/repo/apps/desktop/dist-electron/main.cjs' \"$@\"",
54 );
55 });
56
57 it("repairs Electron before loading the package entrypoint", () => {
58 const calls = [];
59 const electronPath = resolveElectronBinaryPath({
60 ensureRuntime: () => {
61 calls.push("ensure");
62 },
63 createRequire: () => (specifier) => {
64 calls.push(`require:${specifier}`);
65 return "/repo/node_modules/electron/dist/Electron.app/Contents/MacOS/Electron";
66 },
67 moduleUrl: import.meta.url,
68 });
69
70 assert.equal(
71 electronPath,
72 "/repo/node_modules/electron/dist/Electron.app/Contents/MacOS/Electron",
73 );
74 assert.deepEqual(calls, ["ensure", "require:electron"]);
75 });
76
77 it("keeps the native Electron executable name inside the branded macOS bundle", () => {
78 const paths = resolveMacLauncherPaths(
79 "/repo/apps/desktop/.electron-runtime/The Knife (Dev).app",
80 "The Knife (Dev)",
81 );
82
83 assert.equal(paths.launcherExecutableName, "The Knife (Dev) Launcher");
84 assert.equal(
85 paths.launcherBinaryPath,
86 "/repo/apps/desktop/.electron-runtime/The Knife (Dev).app/Contents/MacOS/The Knife (Dev) Launcher",
87 );
88 assert.equal(
89 paths.runtimeElectronBinaryPath,
90 "/repo/apps/desktop/.electron-runtime/The Knife (Dev).app/Contents/MacOS/Electron",
91 );
92
93 const script = makeDevelopmentLauncherScript({
94 electronBinaryPath: paths.runtimeElectronBinaryPath,
95 mainEntryPath: "/repo/apps/desktop/dist-electron/main.cjs",
96 desktopRoot: "/repo/apps/desktop",
97 environmentFilePath: "/repo/apps/desktop/.electron-runtime/dev-environment.sh",
98 });
99 assert.include(
100 script,
101 "exec '/repo/apps/desktop/.electron-runtime/The Knife (Dev).app/Contents/MacOS/Electron'",
102 );
103 assert.notInclude(script, "node_modules/electron");
104 });
105
106 it("declares why the macOS app needs protected access", () => {
107 const values = resolveMacBundleInfoPlistStrings("The Knife (Dev) Launcher");
108
109 assert.equal(
110 values.NSScreenCaptureUsageDescription,
111 "The Knife captures the active window when you use the snapshot shortcut.",
112 );
113 assert.equal(
114 values.NSDocumentsFolderUsageDescription,
115 "The Knife reads project files you open in the desktop app.",
116 );
117 });
118
119 it("ad-hoc signs the complete development app bundle", () => {
120 assert.deepEqual(resolveMacCodeSignArguments("/runtime/The Knife (Dev).app"), [
121 "--force",
122 "--deep",
123 "--sign",
124 "-",
125 "--timestamp=none",
126 "/runtime/The Knife (Dev).app",
127 ]);
128 });
129
130 it("restores execute permissions on an unchanged launcher", () => {
131 const directory = NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "t3-launcher-"));
132 const launcherPath = NodePath.join(directory, "launcher");
133 try {
134 writeDevelopmentLauncherScript(launcherPath, "/runtime/Electron");
135 NodeFS.chmodSync(launcherPath, 0o644);
136
137 assert.isFalse(writeDevelopmentLauncherScript(launcherPath, "/runtime/Electron"));
138 assert.equal(NodeFS.statSync(launcherPath).mode & 0o777, 0o755);
139 } finally {
140 NodeFS.rmSync(directory, { recursive: true, force: true });
141 }
142 });
143
144 it("derives launcher icons from the canonical Knife artwork", () => {
145 const icon = resolveMacLauncherIconPaths("/runtime");
146 assert.match(icon.sourceIconPath, /assets[\\/]knife[\\/]macos-1024\.png$/);
147 assert.equal(icon.generatedIconPath, "/runtime/icon.icns");
148 });
149});