1// @effect-diagnostics nodeBuiltinImport:off - Exercises actual staged build inputs.
2import * as NodeFSP from "node:fs/promises";
3import * as NodeOS from "node:os";
4import * as NodePath from "node:path";
5import { afterEach, beforeEach, expect, it } from "vite-plus/test";
6import { fingerprintForkRuntime, fingerprintForkServerRuntime } from "./fork-runtime.ts";
7
8let root: string;
9let signing: string;
10const fingerprint = () =>
11 fingerprintForkRuntime(root, { platform: "mac", arch: "arm64", signingFiles: [signing] });
12
13beforeEach(async () => {
14 root = await NodeFSP.mkdtemp(NodePath.join(NodeOS.tmpdir(), "knife-runtime-"));
15 signing = NodePath.join(root, "sign.ts");
16 await NodeFSP.mkdir(NodePath.join(root, "apps/server/dist/client"), { recursive: true });
17 await NodeFSP.mkdir(NodePath.join(root, "apps/desktop/dist-electron"), { recursive: true });
18 await NodeFSP.mkdir(NodePath.join(root, "node_modules/native"), { recursive: true });
19 await NodeFSP.writeFile(signing, "signing policy");
20 await NodeFSP.writeFile(
21 NodePath.join(root, "package.json"),
22 JSON.stringify({
23 version: "0.0.1",
24 buildVersion: "0.0.1",
25 t3codeCommitHash: "one",
26 devDependencies: { electron: "44.4.5" },
27 build: { mac: { sign: signing } },
28 }),
29 );
30 await NodeFSP.writeFile(NodePath.join(root, "apps/server/dist/bin.mjs"), "server");
31 await NodeFSP.writeFile(NodePath.join(root, "apps/desktop/dist-electron/preload.cjs"), "preload");
32 await NodeFSP.writeFile(NodePath.join(root, "apps/desktop/dist-electron/boot.cjs"), "bootstrap");
33 await NodeFSP.writeFile(NodePath.join(root, "node_modules/native/binary.node"), "native");
34 for (const file of [
35 "scripts/container-runtime.ts",
36 "infra/container/knife",
37 "packages/shared/src/forkUpdateDirectory.ts",
38 ]) {
39 await NodeFSP.mkdir(NodePath.dirname(NodePath.join(root, file)), { recursive: true });
40 await NodeFSP.writeFile(NodePath.join(root, file), "stable bootstrap");
41 }
42});
43
44it("keeps the installed shell for backend, renderer, and runtime dependency changes", async () => {
45 const input = { platform: "mac", arch: "arm64", signingFiles: [signing], shellOnly: true };
46 const initial = await fingerprintForkRuntime(root, input);
47 for (const file of [
48 "apps/server/dist/bin.mjs",
49 "apps/desktop/dist-electron/preload.cjs",
50 "node_modules/native/binary.node",
51 ])
52 await NodeFSP.appendFile(NodePath.join(root, file), "new code");
53 expect(await fingerprintForkRuntime(root, input)).toBe(initial);
54 await NodeFSP.appendFile(
55 NodePath.join(root, "apps/desktop/dist-electron/boot.cjs"),
56 "new bootstrap",
57 );
58 expect(await fingerprintForkRuntime(root, input)).not.toBe(initial);
59});
60
61it("ignores package-manager launchers that are excluded from the packaged runtime", async () => {
62 const initial = await fingerprint();
63 await NodeFSP.mkdir(NodePath.join(root, "node_modules/.bin"));
64 await NodeFSP.writeFile(
65 NodePath.join(root, "node_modules/.bin/yaml"),
66 `#!/bin/sh\nexport NODE_PATH=${root}/node_modules`,
67 );
68 expect(await fingerprint()).toBe(initial);
69});
70
71it("fingerprints Windows server code without making renderer changes replace the shell", async () => {
72 const serverDirectory = NodePath.join(root, "server-sidecar");
73 for (const directory of ["apps/server/dist/client", "node_modules/native"]) {
74 await NodeFSP.mkdir(NodePath.join(serverDirectory, directory), { recursive: true });
75 }
76 await NodeFSP.writeFile(NodePath.join(serverDirectory, "apps/server/dist/bin.mjs"), "server");
77 await NodeFSP.writeFile(
78 NodePath.join(serverDirectory, "node_modules/native/binary.node"),
79 "native",
80 );
81 const sidecar = NodePath.join(root, "apps/desktop/prod-resources/windows-server/server.asar");
82 await NodeFSP.mkdir(NodePath.dirname(sidecar), { recursive: true });
83 await NodeFSP.writeFile(sidecar, "packed server and renderer");
84 const input = { platform: "win", arch: "x64", signingFiles: [], serverDirectory };
85 const initial = await fingerprintForkRuntime(root, input);
86 const shell = await fingerprintForkRuntime(root, { ...input, shellOnly: true });
87
88 await NodeFSP.writeFile(
89 NodePath.join(serverDirectory, "apps/server/dist/client/index.html"),
90 "new UI",
91 );
92 await NodeFSP.writeFile(sidecar, "repacked server and new renderer");
93 expect(await fingerprintForkRuntime(root, input)).toBe(initial);
94 expect(await fingerprintForkRuntime(root, { ...input, shellOnly: true })).toBe(shell);
95
96 await NodeFSP.appendFile(NodePath.join(serverDirectory, "apps/server/dist/bin.mjs"), "new code");
97 const changed = await fingerprintForkRuntime(root, input);
98 expect(changed).not.toBe(initial);
99 expect(await fingerprintForkRuntime(root, { ...input, shellOnly: true })).toBe(shell);
100 await NodeFSP.appendFile(
101 NodePath.join(serverDirectory, "node_modules/native/binary.node"),
102 "new native",
103 );
104 expect(await fingerprintForkRuntime(root, input)).not.toBe(changed);
105});
106
107it("matches the image to locked external dependencies, including their transitives", async () => {
108 const lock = `importers:
109 apps/server:
110 dependencies:
111 node-pty: {version: 1.0.0}
112 effect: {version: 4.0.0}
113packages:
114 node-pty@1.0.0: {resolution: {integrity: pty}}
115 native-helper@2.0.0: {resolution: {integrity: helper}}
116 effect@4.0.0: {resolution: {integrity: effect}}
117snapshots:
118 node-pty@1.0.0:
119 dependencies: {native-helper: 2.0.0}
120 native-helper@2.0.0: {}
121 effect@4.0.0: {}
122`;
123 const path = NodePath.join(root, "pnpm-lock.yaml");
124 await NodeFSP.writeFile(path, lock);
125 const initial = await fingerprintForkServerRuntime(root);
126 await NodeFSP.writeFile(path, lock.replaceAll("4.0.0", "4.0.1"));
127 expect(await fingerprintForkServerRuntime(root)).toBe(initial);
128 await NodeFSP.writeFile(path, lock.replaceAll("2.0.0", "2.0.1"));
129 expect(await fingerprintForkServerRuntime(root)).not.toBe(initial);
130});
131
132it("refuses an incomplete external dependency closure", async () => {
133 await NodeFSP.writeFile(
134 NodePath.join(root, "pnpm-lock.yaml"),
135 `importers:
136 apps/server:
137 dependencies:
138 node-pty: {version: 1.0.0}
139packages: {}
140snapshots: {}
141`,
142 );
143 await expect(fingerprintForkServerRuntime(root)).rejects.toThrow(
144 "Missing locked runtime dependency",
145 );
146});
147
148afterEach(async () => NodeFSP.rm(root, { recursive: true, force: true }));
149
150it("keeps the runtime identity across consecutive UI-only release versions", async () => {
151 const initial = await fingerprint();
152 const metadata = JSON.parse(await NodeFSP.readFile(NodePath.join(root, "package.json"), "utf8"));
153 metadata.version = metadata.buildVersion = "0.0.2";
154 metadata.t3codeCommitHash = "two";
155 await NodeFSP.writeFile(NodePath.join(root, "package.json"), JSON.stringify(metadata));
156 await NodeFSP.writeFile(NodePath.join(root, "apps/server/dist/client/index.html"), "new UI");
157 await NodeFSP.writeFile(
158 NodePath.join(root, "apps/desktop/dist-electron/main.cjs.map"),
159 "new sources",
160 );
161 expect(await fingerprint()).toBe(initial);
162});
163
164it.each([
165 "apps/server/dist/bin.mjs",
166 "apps/desktop/dist-electron/preload.cjs",
167 "node_modules/native/binary.node",
168 "sign.ts",
169])("requires a restart for changed runtime bytes in %s", async (file) => {
170 const initial = await fingerprint();
171 await NodeFSP.appendFile(NodePath.join(root, file), "changed");
172 expect(await fingerprint()).not.toBe(initial);
173});
174
175it("includes architecture, Electron and executable permissions", async () => {
176 const initial = await fingerprint();
177 expect(
178 await fingerprintForkRuntime(root, { platform: "mac", arch: "x64", signingFiles: [signing] }),
179 ).not.toBe(initial);
180 const metadata = JSON.parse(await NodeFSP.readFile(NodePath.join(root, "package.json"), "utf8"));
181 metadata.devDependencies.electron = "45.0.0";
182 await NodeFSP.writeFile(NodePath.join(root, "package.json"), JSON.stringify(metadata));
183 const newerElectron = await fingerprint();
184 expect(newerElectron).not.toBe(initial);
185 await NodeFSP.chmod(NodePath.join(root, "node_modules/native/binary.node"), 0o755);
186 expect(await fingerprint()).not.toBe(newerElectron);
187});