1// @effect-diagnostics nodeBuiltinImport:off - Tests use Node's glob matcher to verify electron-builder exclusions.
2import * as NodeCrypto from "node:crypto";
3import * as NodePath from "node:path";
4
5import * as NodeServices from "@effect/platform-node/NodeServices";
6import { extractFile } from "@electron/asar";
7import { assert, it } from "@effect/vitest";
8import * as ConfigProvider from "effect/ConfigProvider";
9import * as FileSystem from "effect/FileSystem";
10import * as Effect from "effect/Effect";
11import * as Layer from "effect/Layer";
12import * as Option from "effect/Option";
13import * as Path from "effect/Path";
14import * as Sink from "effect/Sink";
15import * as Stream from "effect/Stream";
16import { ChildProcess, ChildProcessSpawner } from "effect/process";
17
18import {
19 BundleNotSelfContainedError,
20 BuildCommandFailedError,
21 parseWslRuntimeArchiveMembers,
22 DesktopDmgBackgroundSourceMissingError,
23 createStageWorkspaceConfig,
24 createStagePatchedDependencies,
25 readWorkspaceConfig,
26 createBuildConfig,
27 exportForkResources,
28 DESKTOP_ELECTRON_LANGUAGES,
29 DESKTOP_FILE_EXCLUSIONS,
30 DESKTOP_EXTRA_RESOURCES,
31 LINUX_CAPTURE_EXTRA_RESOURCES,
32 LINUX_BROWSER_SECRET_EXTRA_RESOURCES,
33 LINUX_FILE_EXCLUSIONS,
34 MAC_FILE_EXCLUSIONS,
35 InvalidMacPasskeyRpDomainError,
36 InvalidMacPasskeyPublishableKeyError,
37 InvalidMockUpdateServerPortError,
38 UnsupportedDesktopBuildArchitectureError,
39 isMacPasskeySigningConfigurationError,
40 LinuxIconResizeError,
41 LinuxDesktopBuildPrerequisitesMissingError,
42 MacDesktopBuildPrerequisitesMissingError,
43 MacPasskeySigningConfigurationResolutionError,
44 MissingMacPasskeyProvisioningProfileError,
45 packWindowsServerAsar,
46 preflightLinuxDesktopBuild,
47 preflightMacDesktopBuild,
48 preflightWindowsDesktopBuild,
49 renderMacPasskeyEntitlements,
50 resolveMacWebAuthnEntitlements,
51 resolveClerkPasskeyNativeArtifacts,
52 resolveMacPasskeySigningConfiguration,
53 resolveDesktopRuntimeDependencies,
54 resolveMergedStageDependencies,
55 resolveFffNativeDependencies,
56 resolveBuildOptions,
57 resolveDesktopBuildIconAssets,
58 resolveDesktopProductName,
59 resolveDesktopUpdateChannel,
60 resolveDesktopWebAssetBrand,
61 resolveResourceMonitorRustTargets,
62 resolveWindowsServerAsarIgnoreGlobs,
63 resourceMonitorExecutableName,
64 resolveGitHubPublishConfig,
65 resolveMockUpdateServerPort,
66 resolveMockUpdateServerUrl,
67 resolvePackageManagerUserAgent,
68 stageLinuxIconSize,
69 stageDesktopDmgBackground,
70 stageResourceMonitor,
71 stageLinuxCaptureHelper,
72 stageWslRuntimeArchive,
73 bundlesWslRuntime,
74 STAGE_INSTALL_ARGS,
75 ancestorNodeModulesPaths,
76 copyDirectoryPreservingSymlinks,
77 LinuxBrowserSecretHostError,
78 stageBrowserSecret,
79 validateWindowsPackagedPayload,
80 WindowsPrimaryNativeProbeError,
81 WindowsDesktopBuildPrerequisitesMissingError,
82 WindowsPackagedPayloadValidationError,
83 WINDOWS_NATIVE_ASAR_UNPACK_GLOB,
84 stageCursorSdkPlatformPackages,
85 WINDOWS_PACKAGED_PAYLOAD_FILE_LIMIT,
86 WINDOWS_SERVER_ASAR_IGNORE_GLOBS,
87 WINDOWS_SERVER_EXTRA_RESOURCES,
88 WINDOWS_SERVER_ASAR_RESOURCE,
89 WINDOWS_SERVER_RESOURCE_SOURCE_DIR,
90 WSL_RUNTIME_ARCHIVE_EXTRA_RESOURCE,
91 WSL_RUNTIME_ARCHIVE_HASH_EXTRA_RESOURCE,
92 WSL_RUNTIME_ARCHIVE_HASH_NAME,
93 WSL_RUNTIME_ARCHIVE_NAME,
94 WSL_RUNTIME_EXTRA_RESOURCES,
95 WslRuntimeArchiveMissingError,
96 wslRuntimeArchiveStem,
97} from "./build-desktop-artifact.ts";
98import { BRAND_ASSET_PATHS } from "./lib/brand-assets.ts";
99import * as HostProcess from "@t3tools/shared/HostProcess";
100import { symlinksSupported } from "@t3tools/shared/testing/symlinks";
101
102it.effect.each(["linux", "win"] as const)(
103 "exports the actual %s packaged archives and their native sidecars",
104 (platform) =>
105 Effect.scoped(
106 Effect.gen(function* () {
107 const fs = yield* FileSystem.FileSystem;
108 const path = yield* Path.Path;
109 const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
110 const root = yield* fs.makeTempDirectoryScoped();
111 const stageDistDir = path.join(root, "dist");
112 const resources = path.join(stageDistDir, `${platform}-unpacked/resources`);
113 const files = [
114 "app.asar",
115 "app.asar.unpacked/native.node",
116 ...(platform === "win" ? ["server.asar", "server.asar.unpacked/server.node"] : []),
117 ];
118 for (const file of [...files, "unrelated-electron-resource"]) {
119 yield* fs.makeDirectory(path.dirname(path.join(resources, file)), { recursive: true });
120 yield* fs.writeFileString(path.join(resources, file), file);
121 }
122 const archive = yield* exportForkResources({
123 stageDistDir,
124 outputDir: path.join(root, "output"),
125 platform,
126 arch: "x64",
127 serverRuntime: "a".repeat(64),
128 });
129 assert.equal(
130 path.basename(archive),
131 `knife-resources-${platform === "win" ? "win32" : platform}-x64.tar.gz`,
132 );
133 const extracted = path.join(root, "extracted");
134 yield* fs.makeDirectory(extracted);
135 const child = yield* spawner.spawn(
136 ChildProcess.make("tar", ["-xzf", archive, "-C", extracted]),
137 );
138 assert.equal(Number(yield* child.exitCode), 0);
139 for (const file of files)
140 assert.equal(yield* fs.readFileString(path.join(extracted, file)), file);
141 assert.equal(
142 yield* fs.readFileString(path.join(extracted, "knife-server-runtime")),
143 "a".repeat(64),
144 );
145 assert.equal(yield* fs.exists(path.join(extracted, "unrelated-electron-resource")), false);
146 }),
147 ).pipe(Effect.provide(NodeServices.layer)),
148);
149
150// Keeps pnpm from auto-installing TypeScript, a types-only peer, into the app.
151const stagePackageExtensions = {
152 "electron-webauthn": { peerDependenciesMeta: { typescript: { optional: true } } },
153 "@electron-webauthn/macos": { peerDependenciesMeta: { typescript: { optional: true } } },
154};
155
156// A minimal stand-in for the Linux CLI release archive: one top-level
157// directory named after the archive stem holding the executable, the web
158// client, and the runtime externals with node-pty built from source.
159const makeLinuxCliArchiveFixture = Effect.fn("test.makeLinuxCliArchiveFixture")(function* (input: {
160 readonly root: string;
161 readonly stem: string;
162 readonly extraMembers?: ReadonlyArray<string>;
163 readonly omitMembers?: ReadonlyArray<string>;
164}) {
165 const fs = yield* FileSystem.FileSystem;
166 const path = yield* Path.Path;
167 const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
168 const contentRoot = path.join(input.root, "content");
169 const members = [
170 `${input.stem}/t3`,
171 `${input.stem}/client/index.html`,
172 `${input.stem}/node_modules/node-pty/package.json`,
173 `${input.stem}/node_modules/node-pty/build/Release/pty.node`,
174 ...(input.extraMembers ?? []),
175 ].filter((member) => !(input.omitMembers ?? []).includes(member));
176 for (const member of members) {
177 const memberPath = path.join(contentRoot, member);
178 yield* fs.makeDirectory(path.dirname(memberPath), { recursive: true });
179 yield* fs.writeFileString(memberPath, member);
180 }
181 const archivePath = path.join(input.root, `${input.stem}.tar.gz`);
182 const tar = yield* spawner.spawn(
183 ChildProcess.make("tar", ["-czf", archivePath, "-C", contentRoot, "."], {
184 stdin: "ignore",
185 stdout: "ignore",
186 stderr: "pipe",
187 }),
188 );
189 assert.equal(Number(yield* tar.exitCode), 0);
190 return archivePath;
191});
192
193function mockProcess(exitCode: number, stdout = "") {
194 const encodedStdout = new TextEncoder().encode(stdout);
195 return ChildProcessSpawner.makeHandle({
196 pid: ChildProcessSpawner.ProcessId(1),
197 exitCode: Effect.succeed(ChildProcessSpawner.ExitCode(exitCode)),
198 isRunning: Effect.succeed(false),
199 kill: () => Effect.void,
200 unref: Effect.succeed(Effect.void),
201 stdin: Sink.drain,
202 stdout: stdout ? Stream.make(encodedStdout) : Stream.empty,
203 stderr: Stream.empty,
204 all: Stream.empty,
205 getInputFd: () => Sink.drain,
206 getOutputFd: () => Stream.empty,
207 });
208}
209
210function iconResizeSpawnerLayer(
211 commands: Array<{ readonly command: string; readonly args: ReadonlyArray<string> }>,
212 exitCodes: ReadonlyArray<number>,
213) {
214 let commandIndex = 0;
215 return Layer.succeed(
216 ChildProcessSpawner.ChildProcessSpawner,
217 ChildProcessSpawner.make((command) => {
218 const childProcess = command as unknown as {
219 readonly command: string;
220 readonly args: ReadonlyArray<string>;
221 };
222 commands.push({
223 command: childProcess.command,
224 args: childProcess.args,
225 });
226 return Effect.succeed(mockProcess(exitCodes[commandIndex++] ?? 0));
227 }),
228 );
229}
230
231const WINDOWS_PAYLOAD_FIXTURE_VERSION = "1.2.3";
232
233const makeWindowsPayloadFixture = Effect.fn("test.makeWindowsPayloadFixture")(function* (input: {
234 readonly copyUnpackedNatives: boolean;
235 readonly serverEntrySource?: string;
236 readonly wslRuntime?: "valid" | "loose-server-tree" | "missing-pty" | "bad-digest";
237 readonly targetArch?: "x64" | "arm64";
238 readonly ptyPrebuildArch?: "x64" | "arm64";
239}) {
240 const fs = yield* FileSystem.FileSystem;
241 const path = yield* Path.Path;
242 const tempDir = yield* fs.makeTempDirectoryScoped({
243 prefix: "t3-windows-payload-test-",
244 });
245 const sourceDir = path.join(tempDir, "server-source");
246 const serverEntryPath = path.join(sourceDir, "apps/server/dist/bin.mjs");
247 const nativePath = path.join(sourceDir, "node_modules/native/addon.node");
248 yield* fs.makeDirectory(path.dirname(serverEntryPath), { recursive: true });
249 yield* fs.makeDirectory(path.dirname(nativePath), { recursive: true });
250 yield* fs.writeFileString(serverEntryPath, input.serverEntrySource ?? "console.log('server');\n");
251 yield* fs.writeFileString(nativePath, "MZ native-binary");
252
253 const generatedAsarPath = path.join(tempDir, WINDOWS_SERVER_ASAR_RESOURCE);
254 yield* packWindowsServerAsar({ sourceDir, asarPath: generatedAsarPath, arch: "x64" });
255
256 const stageDistDir = path.join(tempDir, "dist");
257 const packagedAppDir = path.join(stageDistDir, "win-unpacked");
258 const resourcesDir = path.join(packagedAppDir, "resources");
259 yield* fs.makeDirectory(path.join(resourcesDir, "resource-monitor"), { recursive: true });
260 yield* fs.copyFile(generatedAsarPath, path.join(resourcesDir, WINDOWS_SERVER_ASAR_RESOURCE));
261 if (input.copyUnpackedNatives) {
262 yield* fs.copy(
263 `${generatedAsarPath}.unpacked`,
264 path.join(resourcesDir, `${WINDOWS_SERVER_ASAR_RESOURCE}.unpacked`),
265 );
266 }
267 yield* fs.writeFileString(
268 path.join(resourcesDir, "resource-monitor/t3-resource-monitor.exe"),
269 "monitor",
270 );
271 const appExecutableName = "t3code.exe";
272 yield* fs.writeFileString(path.join(packagedAppDir, appExecutableName), "electron");
273 yield* fs.writeFileString(path.join(packagedAppDir, "chrome_crashpad_handler.exe"), "crashpad");
274
275 if (input.wslRuntime !== undefined) {
276 const stem = wslRuntimeArchiveStem(WINDOWS_PAYLOAD_FIXTURE_VERSION, input.targetArch ?? "x64");
277 const sourceArchivePath =
278 input.wslRuntime === "loose-server-tree"
279 ? // The old hand-rolled runtime: apps/server/dist + node_modules at the
280 // archive root, no single stem directory, no `t3` executable.
281 yield* makeLinuxCliArchiveFixture({
282 root: path.join(tempDir, "wsl-runtime"),
283 stem: "apps",
284 omitMembers: ["apps/t3", "apps/client/index.html"],
285 extraMembers: ["apps/server/dist/bin.mjs", "node_modules/node-pty/package.json"],
286 })
287 : yield* makeLinuxCliArchiveFixture({
288 root: path.join(tempDir, "wsl-runtime"),
289 stem,
290 ...(input.wslRuntime === "missing-pty" || input.ptyPrebuildArch !== undefined
291 ? { omitMembers: [`${stem}/node_modules/node-pty/build/Release/pty.node`] }
292 : {}),
293 ...(input.ptyPrebuildArch !== undefined
294 ? {
295 extraMembers: [
296 `${stem}/node_modules/node-pty/prebuilds/linux-${input.ptyPrebuildArch}/pty.node`,
297 ],
298 }
299 : {}),
300 });
301 const archivePath = path.join(resourcesDir, WSL_RUNTIME_ARCHIVE_NAME);
302 const hashPath = path.join(resourcesDir, WSL_RUNTIME_ARCHIVE_HASH_NAME);
303 yield* stageWslRuntimeArchive({ sourceArchivePath, archivePath, hashPath });
304 if (input.wslRuntime === "bad-digest") {
305 yield* fs.writeFileString(hashPath, `${"0".repeat(64)}\n`);
306 }
307 }
308
309 return {
310 stageDistDir,
311 packagedAppDir,
312 sourceDir,
313 generatedAsarPath,
314 appExecutableName,
315 } as const;
316});
317
318it.layer(NodeServices.layer)("build-desktop-artifact", (it) => {
319 it.effect("packages a signed private feed instead of the upstream release channel", () =>
320 Effect.gen(function* () {
321 const config = yield* createBuildConfig(
322 "mac",
323 "zip",
324 "0.0.1791936000",
325 false,
326 false,
327 undefined,
328 undefined,
329 );
330 assert.deepStrictEqual(config.publish, [
331 { provider: "generic", url: "https://updates.example.test/fork/?ls=" },
332 ]);
333 assert.equal(config.forceCodeSigning, true);
334 assert.containsSubset(config.mac, {
335 identity: "Apple Development: Private Owner (TESTTEAM)",
336 type: "development",
337 });
338 assert.property(config.mac, "sign");
339 assert.notProperty(config.mac, "provisioningProfile");
340 }).pipe(
341 Effect.provide(
342 ConfigProvider.layer(
343 ConfigProvider.fromEnv({
344 env: {
345 GITHUB_REPOSITORY: "pingdotgg/t3code",
346 T3CODE_DESKTOP_UPDATE_URL: "https://updates.example.test/fork/",
347 T3CODE_DESKTOP_MAC_SIGN_IDENTITY: "Apple Development: Private Owner (TESTTEAM)",
348 },
349 }),
350 ),
351 ),
352 ),
353 );
354
355 it.effect("notarizes Developer ID fork builds without requiring passkey provisioning", () =>
356 Effect.gen(function* () {
357 const config = yield* createBuildConfig(
358 "mac",
359 "zip",
360 "0.0.1791936000",
361 false,
362 false,
363 undefined,
364 undefined,
365 );
366
367 assert.equal(config.forceCodeSigning, true);
368 assert.containsSubset(config.mac, {
369 identity: "A".repeat(40),
370 type: "distribution",
371 hardenedRuntime: true,
372 notarize: true,
373 });
374 assert.property(config.mac, "sign");
375 assert.notProperty(config.mac, "provisioningProfile");
376 }).pipe(
377 Effect.provide(
378 ConfigProvider.layer(
379 ConfigProvider.fromEnv({
380 env: {
381 T3CODE_DESKTOP_MAC_SIGN_IDENTITY: "A".repeat(40),
382 T3CODE_DESKTOP_MAC_SIGN_TYPE: "distribution",
383 },
384 }),
385 ),
386 ),
387 ),
388 );
389
390 it("resolves the dedicated nightly updater channel from nightly versions", () => {
391 assert.equal(resolveDesktopUpdateChannel("0.0.17-nightly.20260413.42"), "nightly");
392 assert.equal(resolveDesktopUpdateChannel("0.0.17"), "latest");
393 });
394
395 it("switches desktop packaging product names to nightly for nightly builds", () => {
396 assert.equal(resolveDesktopProductName("0.0.17"), "The Knife");
397 assert.equal(resolveDesktopProductName("0.0.17-nightly.20260413.42"), "The Knife (Nightly)");
398 });
399
400 it("switches desktop packaging icons to the nightly artwork for nightly versions", () => {
401 assert.deepStrictEqual(resolveDesktopBuildIconAssets("0.0.17"), {
402 macIconPng: BRAND_ASSET_PATHS.productionMacIconPng,
403 linuxIconPng: BRAND_ASSET_PATHS.productionLinuxIconPng,
404 windowsIconIco: BRAND_ASSET_PATHS.productionWindowsIconIco,
405 });
406
407 assert.deepStrictEqual(resolveDesktopBuildIconAssets("0.0.17-nightly.20260413.42"), {
408 macIconPng: BRAND_ASSET_PATHS.nightlyMacIconPng,
409 linuxIconPng: BRAND_ASSET_PATHS.nightlyLinuxIconPng,
410 windowsIconIco: BRAND_ASSET_PATHS.nightlyWindowsIconIco,
411 });
412 });
413
414 it("switches the bundled splash and favicon branding for nightly versions", () => {
415 assert.equal(resolveDesktopWebAssetBrand("0.0.17"), "production");
416 assert.equal(resolveDesktopWebAssetBrand("0.0.17-nightly.20260413.42"), "nightly");
417 });
418
419 it.effect("resolves GitHub desktop publish config from Effect config", () =>
420 Effect.gen(function* () {
421 const latestConfig = yield* resolveGitHubPublishConfig("latest").pipe(
422 Effect.provide(
423 ConfigProvider.layer(
424 ConfigProvider.fromEnv({
425 env: {
426 T3CODE_DESKTOP_UPDATE_REPOSITORY: "pingdotgg/t3code",
427 },
428 }),
429 ),
430 ),
431 );
432 const nightlyConfig = yield* resolveGitHubPublishConfig("nightly").pipe(
433 Effect.provide(
434 ConfigProvider.layer(
435 ConfigProvider.fromEnv({
436 env: {
437 GITHUB_REPOSITORY: "pingdotgg/t3code",
438 },
439 }),
440 ),
441 ),
442 );
443
444 assert.deepStrictEqual(latestConfig, {
445 provider: "github",
446 owner: "pingdotgg",
447 repo: "t3code",
448 releaseType: "release",
449 });
450 assert.deepStrictEqual(nightlyConfig, {
451 provider: "github",
452 owner: "pingdotgg",
453 repo: "t3code",
454 releaseType: "prerelease",
455 channel: "nightly",
456 });
457 }),
458 );
459
460 it.effect("omits update feeds for pull request preview builds", () =>
461 Effect.gen(function* () {
462 const preview = yield* createBuildConfig(
463 "mac",
464 "dmg",
465 "0.0.33-pr.8182.1",
466 false,
467 false,
468 undefined,
469 undefined,
470 );
471 const release = yield* createBuildConfig(
472 "mac",
473 "dmg",
474 "0.0.33",
475 false,
476 false,
477 undefined,
478 undefined,
479 );
480
481 const previewChannel = yield* createBuildConfig(
482 "mac",
483 "dmg",
484 "0.0.41-preview.20260912.1589",
485 false,
486 false,
487 undefined,
488 undefined,
489 );
490
491 assert.notProperty(preview, "publish");
492 assert.notProperty(previewChannel, "publish");
493 assert.deepStrictEqual(release.publish, [
494 {
495 provider: "github",
496 owner: "pingdotgg",
497 repo: "t3code",
498 releaseType: "release",
499 },
500 ]);
501 }).pipe(
502 Effect.provide(
503 ConfigProvider.layer(
504 ConfigProvider.fromEnv({ env: { GITHUB_REPOSITORY: "pingdotgg/t3code" } }),
505 ),
506 ),
507 ),
508 );
509
510 it("stages only the desktop main-process externals", () => {
511 assert.deepStrictEqual(
512 resolveDesktopRuntimeDependencies(
513 {
514 "@clerk/electron": "catalog:",
515 "@clerk/electron-passkeys": "catalog:",
516 "@crowecawcaw/xa11y": "0.13.0",
517 "@effect/platform-node": "catalog:",
518 "@napi-rs/keyring": "^1.3.0",
519 "@t3tools/contracts": "workspace:*",
520 "@t3tools/shared": "workspace:*",
521 "dbus-next": "0.10.2",
522 effect: "catalog:",
523 electron: "41.5.0",
524 "electron-updater": "^6.6.2",
525 "ffi-rs": "1.3.2",
526 "playwright-core": "1.60.0",
527 },
528 {
529 "@clerk/electron": "0.0.37",
530 "@clerk/electron-passkeys": "0.0.3",
531 "@effect/platform-node": "4.0.0-beta.59",
532 effect: "4.0.0-beta.59",
533 },
534 ),
535 {
536 "@clerk/electron-passkeys": "0.0.3",
537 "@crowecawcaw/xa11y": "0.13.0",
538 "@napi-rs/keyring": "^1.3.0",
539 "ffi-rs": "1.3.2",
540 "playwright-core": "1.60.0",
541 },
542 );
543 });
544
545 it("carries only staged dependency patch metadata into staged desktop installs", () => {
546 assert.deepStrictEqual(
547 createStagePatchedDependencies(
548 {
549 "@expo/metro-config@56.0.13": "patches/@expo%2Fmetro-config@56.0.13.patch",
550 "@ff-labs/fff-node@0.9.4": "patches/@ff-labs__fff-node@0.9.4.patch",
551 "@pierre/diffs@1.1.20": "patches/@pierre%2Fdiffs@1.1.20.patch",
552 "alchemy@2.0.0-beta.49": "patches/alchemy@2.0.0-beta.49.patch",
553 "effect@4.0.0-beta.73": "patches/effect@4.0.0-beta.73.patch",
554 },
555 {
556 "@ff-labs/fff-node": "0.9.4",
557 "@pierre/diffs": "1.1.20",
558 effect: "4.0.0-beta.73",
559 },
560 ),
561 {
562 "@ff-labs/fff-node@0.9.4": "patches/@ff-labs__fff-node@0.9.4.patch",
563 "@pierre/diffs@1.1.20": "patches/@pierre%2Fdiffs@1.1.20.patch",
564 "effect@4.0.0-beta.73": "patches/effect@4.0.0-beta.73.patch",
565 },
566 );
567
568 assert.deepStrictEqual(
569 createStagePatchedDependencies(
570 {
571 "@expo/metro-config@56.0.13": "patches/@expo%2Fmetro-config@56.0.13.patch",
572 },
573 { effect: "4.0.0-beta.73" },
574 ),
575 {},
576 );
577 });
578
579 it("installs optional native dependencies for the target desktop architecture", () => {
580 assert.deepStrictEqual(STAGE_INSTALL_ARGS, ["install", "--prod"]);
581 assert.deepStrictEqual(createStageWorkspaceConfig({ platform: "mac", arch: "x64" }), {
582 supportedArchitectures: {
583 os: ["darwin"],
584 cpu: ["x64"],
585 },
586 packageExtensions: stagePackageExtensions,
587 });
588 assert.deepStrictEqual(createStageWorkspaceConfig({ platform: "linux", arch: "x64" }), {
589 supportedArchitectures: {
590 os: ["linux"],
591 cpu: ["x64"],
592 libc: ["glibc"],
593 },
594 packageExtensions: stagePackageExtensions,
595 });
596 // Windows stages only win32 natives; WSL runs the separately built Linux
597 // CLI archive rather than anything installed here.
598 assert.deepStrictEqual(createStageWorkspaceConfig({ platform: "win", arch: "x64" }), {
599 supportedArchitectures: {
600 os: ["win32"],
601 cpu: ["x64"],
602 },
603 packageExtensions: stagePackageExtensions,
604 });
605 assert.deepStrictEqual(createStageWorkspaceConfig({ platform: "win", arch: "arm64" }), {
606 supportedArchitectures: {
607 os: ["win32"],
608 cpu: ["arm64"],
609 },
610 packageExtensions: stagePackageExtensions,
611 });
612 assert.deepStrictEqual(createStageWorkspaceConfig({ platform: "mac", arch: "universal" }), {
613 supportedArchitectures: {
614 os: ["darwin"],
615 cpu: ["arm64", "x64"],
616 },
617 packageExtensions: stagePackageExtensions,
618 });
619 });
620
621 // pnpm writes "set this to true or false" placeholders into allowBuilds when a
622 // tarball changes. pnpm ignores them, but the release build decode rejects them.
623 it.effect("decodes the repo allowBuilds and every file: tarball it names exists", () =>
624 Effect.gen(function* () {
625 const fs = yield* FileSystem.FileSystem;
626 const path = yield* Path.Path;
627 const { allowBuilds = {} } = yield* readWorkspaceConfig();
628 const repoRoot = path.join(import.meta.dirname, "..");
629 for (const key of Object.keys(allowBuilds)) {
630 const tarball = key.split("@file:")[1];
631 if (tarball === undefined) continue;
632 assert.isTrue(
633 yield* fs.exists(path.join(repoRoot, tarball)),
634 `allowBuilds names a missing tarball: ${key}`,
635 );
636 }
637 }),
638 );
639
640 it("stages pnpm 11 allowBuilds and patchedDependencies in the workspace yaml", () => {
641 assert.deepStrictEqual(
642 createStageWorkspaceConfig({
643 platform: "linux",
644 arch: "x64",
645 allowBuilds: {
646 electron: true,
647 "node-pty": true,
648 "browser-tabs-lock": false,
649 },
650 patchedDependencies: {
651 "effect@4.0.0-beta.73": "patches/effect@4.0.0-beta.73.patch",
652 },
653 overrides: {
654 effect: "4.0.0-beta.73",
655 },
656 }),
657 {
658 supportedArchitectures: {
659 os: ["linux"],
660 cpu: ["x64"],
661 libc: ["glibc"],
662 },
663 packageExtensions: stagePackageExtensions,
664 allowBuilds: {
665 electron: true,
666 "node-pty": true,
667 "browser-tabs-lock": false,
668 },
669 patchedDependencies: {
670 "effect@4.0.0-beta.73": "patches/effect@4.0.0-beta.73.patch",
671 },
672 overrides: {
673 effect: "4.0.0-beta.73",
674 },
675 },
676 );
677
678 // Empty maps must not be written — pnpm would still require reviewed
679 // packages if allowBuilds is present but incomplete, and omitting empty
680 // patchedDependencies keeps the stage yaml minimal.
681 assert.deepStrictEqual(
682 createStageWorkspaceConfig({
683 platform: "mac",
684 arch: "arm64",
685 allowBuilds: {},
686 patchedDependencies: {},
687 overrides: {},
688 }),
689 {
690 supportedArchitectures: {
691 os: ["darwin"],
692 cpu: ["arm64"],
693 },
694 packageExtensions: stagePackageExtensions,
695 },
696 );
697 });
698
699 it("limits Electron locales and excludes separately packaged resources", () => {
700 assert.deepStrictEqual(DESKTOP_ELECTRON_LANGUAGES, ["en-US"]);
701 // Every platform staging input is emitted once at resources/, so adding one
702 // without its exclusion silently packs a second copy into app.asar. The
703 // snapshot below cannot catch that on its own: adding a resource and
704 // forgetting the exclusion leaves the exclusion list untouched, so it still
705 // matches. Assert the invariant first, where the failure names the culprit.
706 for (const resource of [
707 ...WSL_RUNTIME_EXTRA_RESOURCES,
708 ...LINUX_BROWSER_SECRET_EXTRA_RESOURCES,
709 ]) {
710 assert.include(
711 DESKTOP_FILE_EXCLUSIONS,
712 `!${resource.from}`,
713 `${resource.from} ships via extraResources and must be excluded from app.asar`,
714 );
715 }
716
717 assert.deepStrictEqual(DESKTOP_FILE_EXCLUSIONS, [
718 "!**/node_modules/@cursor/sdk-*/**/*",
719 "!apps/desktop/prod-resources/cursor-sdk",
720 "!apps/desktop/prod-resources/cursor-sdk/**/*",
721 "!**/node_modules/@anthropic-ai/claude-agent-sdk-*/**/*",
722 "!**/*.map",
723 "!**/*.d.cts",
724 "!apps/desktop/resources/browser-secret",
725 "!apps/desktop/resources/browser-secret/**/*",
726 "!apps/desktop/prod-resources/browser-secret",
727 "!apps/desktop/prod-resources/browser-secret/**/*",
728 "!apps/desktop/prod-resources/windows-server",
729 "!apps/desktop/prod-resources/windows-server/**/*",
730 "!apps/desktop/prod-resources/wsl-runtime.tar.gz",
731 "!apps/desktop/prod-resources/wsl-runtime.tar.gz.sha256",
732 "!apps/desktop/gnome-extension",
733 "!apps/desktop/gnome-extension/**/*",
734 ]);
735 assert.equal(WINDOWS_SERVER_RESOURCE_SOURCE_DIR, "apps/desktop/prod-resources/windows-server");
736 assert.deepStrictEqual(WINDOWS_SERVER_EXTRA_RESOURCES, [
737 {
738 from: "apps/desktop/prod-resources/windows-server",
739 to: ".",
740 filter: ["server.asar", "server.asar.unpacked/**/*"],
741 },
742 ]);
743 });
744
745 it.effect("applies platform-specific packaging to the build config", () =>
746 Effect.gen(function* () {
747 const mac = yield* createBuildConfig(
748 "mac",
749 "dmg",
750 "1.2.3",
751 false,
752 false,
753 undefined,
754 undefined,
755 );
756 const linux = yield* createBuildConfig(
757 "linux",
758 "AppImage",
759 "1.2.3",
760 false,
761 false,
762 undefined,
763 undefined,
764 );
765 const win = yield* createBuildConfig(
766 "win",
767 "nsis",
768 "1.2.3",
769 false,
770 false,
771 undefined,
772 undefined,
773 true,
774 );
775 const winWithoutWslRuntime = yield* createBuildConfig(
776 "win",
777 "nsis",
778 "1.2.3",
779 false,
780 false,
781 undefined,
782 undefined,
783 false,
784 );
785
786 // Windows unpacks native files explicitly so their JavaScript and metadata
787 // stay archived. Other platforms retain electron-builder's defaults.
788 assert.notProperty(mac, "asar");
789 assert.notProperty(linux, "asar");
790 assert.notProperty(mac, "asarUnpack");
791 assert.notProperty(linux, "asarUnpack");
792 assert.deepStrictEqual(win.asar, { smartUnpack: false });
793 assert.deepStrictEqual(win.asarUnpack, [WINDOWS_NATIVE_ASAR_UNPACK_GLOB]);
794 assert.deepStrictEqual(winWithoutWslRuntime.asar, win.asar);
795 assert.deepStrictEqual(winWithoutWslRuntime.asarUnpack, win.asarUnpack);
796 assert.deepStrictEqual(mac.extraResources, DESKTOP_EXTRA_RESOURCES);
797 assert.deepStrictEqual(linux.extraResources, [
798 ...DESKTOP_EXTRA_RESOURCES,
799 ...LINUX_CAPTURE_EXTRA_RESOURCES,
800 { from: "apps/desktop/prod-resources/browser-secret", to: "browser-secret" },
801 ]);
802 assert.deepStrictEqual(win.extraResources, [
803 ...DESKTOP_EXTRA_RESOURCES,
804 ...WINDOWS_SERVER_EXTRA_RESOURCES,
805 ...WSL_RUNTIME_EXTRA_RESOURCES,
806 ]);
807 // No Linux CLI archive means staging never writes the runtime, so
808 // listing it here would fail the build on a missing source file.
809 assert.deepStrictEqual(winWithoutWslRuntime.extraResources, [
810 ...DESKTOP_EXTRA_RESOURCES,
811 ...WINDOWS_SERVER_EXTRA_RESOURCES,
812 ]);
813 assert.deepStrictEqual(win.nsis, { differentialPackage: true });
814 // The Claude SDK platform packages and .bin shims never ship.
815 assert.deepStrictEqual(WINDOWS_SERVER_ASAR_IGNORE_GLOBS, [
816 "**/node_modules/@cursor/sdk-*",
817 "**/node_modules/@cursor/sdk-*/**",
818 "**/node_modules/@anthropic-ai/claude-agent-sdk-*",
819 "**/node_modules/@anthropic-ai/claude-agent-sdk-*/**",
820 "**/node_modules/.bin",
821 "**/node_modules/.bin/**",
822 "**/*.map",
823 ]);
824 assert.deepStrictEqual(mac.dmg, {
825 title: "The Knife 1.2.3 Installer",
826 background: "dmg/dmg-background-latest.png",
827 window: { width: 640, height: 432 },
828 contents: [
829 { x: 166, y: 214, type: "file" },
830 { x: 474, y: 214, type: "link", path: "/Applications" },
831 ],
832 iconSize: 120,
833 iconTextSize: 12,
834 });
835 // A Linux AppImage build also emits the .deb from the same run.
836 assert.deepStrictEqual((linux.linux as Record<string, unknown>).target, ["AppImage", "deb"]);
837 // Linux must register the renderer schemes so the generated .desktop
838 // entry advertises MimeType=x-scheme-handler/t3code; for OAuth deep links.
839 assert.deepStrictEqual((linux.linux as Record<string, unknown>).protocols, [
840 { name: "The Knife", schemes: ["t3code", "t3code-dev"] },
841 ]);
842 // macOS also offers itself as a web browser, so it can be chosen as the default.
843 assert.deepStrictEqual((mac.mac as Record<string, unknown>).protocols, [
844 { name: "The Knife", schemes: ["t3code", "t3code-dev"] },
845 { name: "Web site URL", schemes: ["http", "https"], role: "Viewer" },
846 ]);
847 // macOS lists a default browser only when it also opens web pages as documents.
848 const macInfo = (mac.mac as { extendInfo: Record<string, unknown> }).extendInfo;
849 assert.strictEqual(macInfo.NSAutoFillRequiresTextContentTypeForOneTimeCodeOnMac, true);
850 assert.deepStrictEqual(macInfo.CFBundleDocumentTypes, [
851 {
852 CFBundleTypeName: "Web page",
853 CFBundleTypeRole: "Viewer",
854 LSHandlerRank: "Alternate",
855 LSItemContentTypes: ["public.html", "public.xhtml"],
856 },
857 ]);
858 assert.deepStrictEqual(linux.toolsets, { appimage: "1.0.3" });
859 assert.notProperty(mac, "toolsets");
860 assert.notProperty(win, "toolsets");
861 assert.deepStrictEqual(mac.files, [...DESKTOP_FILE_EXCLUSIONS, ...MAC_FILE_EXCLUSIONS]);
862 assert.deepStrictEqual(linux.files, [...DESKTOP_FILE_EXCLUSIONS, ...LINUX_FILE_EXCLUSIONS]);
863 assert.deepStrictEqual(win.files, DESKTOP_FILE_EXCLUSIONS);
864 assert.deepStrictEqual(winWithoutWslRuntime.files, win.files);
865 assert.notProperty(mac.mac as Record<string, unknown>, "sign");
866 assert.propertyVal(
867 (mac.mac as Record<string, unknown>).extendInfo as Record<string, unknown>,
868 "NSLocalNetworkUsageDescription",
869 "The Knife connects to devices on your local network for remote environments and commands run by terminals and coding agents.",
870 );
871 for (const config of [linux, win]) {
872 assert.deepStrictEqual(config.electronLanguages, DESKTOP_ELECTRON_LANGUAGES);
873 }
874 assert.deepStrictEqual(mac.electronLanguages, DESKTOP_ELECTRON_LANGUAGES);
875 }).pipe(Effect.provide(ConfigProvider.layer(ConfigProvider.fromEnv({ env: {} })))),
876 );
877
878 it("excludes foreign node-pty prebuilds from macOS and Linux packages", () => {
879 assert.deepStrictEqual(MAC_FILE_EXCLUSIONS, [
880 "!**/node_modules/node-pty/prebuilds/win32-*/**/*",
881 "!**/node_modules/node-pty/third_party/conpty/**/*",
882 ]);
883 assert.deepStrictEqual(LINUX_FILE_EXCLUSIONS, [
884 ...MAC_FILE_EXCLUSIONS,
885 "!**/node_modules/node-pty/prebuilds/darwin-*/**/*",
886 ]);
887 });
888
889 it("unpacks native binaries while keeping their JavaScript and metadata archived", () => {
890 for (const file of [
891 "node_modules/@napi-rs/keyring/keyring.win32-x64-msvc.node",
892 "node_modules/@clerk/electron-passkeys/electron-passkeys.win32-x64-msvc.node",
893 "node_modules/@ff-labs/fff-bin-win32-x64/fff_c.dll",
894 "node_modules/node-pty/prebuilds/win32-x64/conpty/OpenConsole.exe",
895 "node_modules/native/addon.so",
896 "node_modules/native/addon.so.1",
897 "node_modules/native/addon.dylib",
898 ]) {
899 assert.isTrue(
900 NodePath.matchesGlob(file, WINDOWS_NATIVE_ASAR_UNPACK_GLOB),
901 `${file} must be available as a real file`,
902 );
903 }
904
905 for (const file of [
906 "node_modules/@napi-rs/keyring/index.js",
907 "node_modules/@napi-rs/keyring/keytar.js",
908 "node_modules/@clerk/electron-passkeys/index.js",
909 ]) {
910 assert.isFalse(
911 NodePath.matchesGlob(file, WINDOWS_NATIVE_ASAR_UNPACK_GLOB),
912 `${file} should remain inside the archive`,
913 );
914 }
915 });
916
917 it("stages only the externals of both bundles in merged packages", () => {
918 assert.deepStrictEqual(
919 resolveMergedStageDependencies({
920 platform: "mac",
921 serverDependencies: {
922 "@cursor/sdk": "1.0.22",
923 "@anthropic-ai/claude-agent-sdk": "^0.3.170",
924 "@ff-labs/fff-node": "0.9.4",
925 "@opencode-ai/sdk": "^1.3.15",
926 "@pierre/diffs": "1.3.0",
927 "node-pty": "1.1.0",
928 },
929 desktopDependencies: {
930 "@napi-rs/keyring": "1.3.0",
931 "playwright-core": "1.60.0",
932 },
933 arch: "arm64",
934 fffNodeVersion: "0.9.4",
935 }),
936 {
937 "@cursor/sdk": "1.0.22",
938 "@ff-labs/fff-node": "0.9.4",
939 "node-pty": "1.1.0",
940 "@napi-rs/keyring": "1.3.0",
941 "playwright-core": "1.60.0",
942 "@ff-labs/fff-bin-darwin-arm64": "0.9.4",
943 },
944 );
945
946 assert.deepStrictEqual(
947 resolveMergedStageDependencies({
948 platform: "linux",
949 serverDependencies: { "@ff-labs/fff-node": "0.9.4", "node-pty": "1.1.0", effect: "4.0.0" },
950 desktopDependencies: { "@crowecawcaw/xa11y": "0.13.0" },
951 arch: "x64",
952 fffNodeVersion: "0.9.4",
953 }),
954 {
955 "@ff-labs/fff-node": "0.9.4",
956 "node-pty": "1.1.0",
957 "@crowecawcaw/xa11y": "0.13.0",
958 "@ff-labs/fff-bin-linux-x64-gnu": "0.9.4",
959 "@ff-labs/fff-bin-linux-x64-musl": "0.9.4",
960 },
961 );
962 });
963
964 it.effect("ships Cursor platform assets outside asar for spawning and native loading", () =>
965 Effect.scoped(
966 Effect.gen(function* () {
967 const fs = yield* FileSystem.FileSystem;
968 const path = yield* Path.Path;
969 const root = yield* fs.makeTempDirectoryScoped({ prefix: "t3-cursor-helpers-" });
970 const nodeModules = path.join(root, "node_modules");
971 const destination = path.join(root, "resources/node_modules/@cursor");
972 const cursorDirectory = symlinksSupported
973 ? path.join(root, "store/@cursor")
974 : path.join(nodeModules, "@cursor");
975 yield* fs.makeDirectory(path.join(cursorDirectory, "sdk"), { recursive: true });
976 if (symlinksSupported) {
977 yield* fs.makeDirectory(path.join(nodeModules, "@cursor"), { recursive: true });
978 yield* fs.symlink(
979 path.join(cursorDirectory, "sdk"),
980 path.join(nodeModules, "@cursor/sdk"),
981 );
982 }
983 const helpers = [
984 "sdk-darwin-arm64/bin/rg",
985 "sdk-darwin-arm64/bin/cursorsandbox",
986 "sdk-darwin-arm64/vendor/tree-sitter/index.js",
987 "sdk-darwin-arm64/vendor/tree-sitter/binding.node",
988 "sdk-darwin-arm64/vendor/tree-sitter-bash/binding.node",
989 "sdk-darwin-arm64/package.json",
990 "sdk-win32-x64/bin/rg.exe",
991 ];
992 for (const helper of helpers) {
993 const source = path.join(cursorDirectory, helper);
994 yield* fs.makeDirectory(path.dirname(source), { recursive: true });
995 yield* fs.writeFileString(source, "fixture helper", { mode: 0o755 });
996 }
997 yield* stageCursorSdkPlatformPackages(nodeModules, destination);
998 for (const helper of helpers) {
999 assert.equal(yield* fs.readFileString(path.join(destination, helper)), "fixture helper");
1000 const packagedPath = `node_modules/@cursor/${helper}`;
1001 assert.isTrue(
1002 DESKTOP_FILE_EXCLUSIONS.some((glob) =>
1003 NodePath.matchesGlob(packagedPath, glob.slice(1)),
1004 ),
1005 );
1006 assert.isTrue(
1007 WINDOWS_SERVER_ASAR_IGNORE_GLOBS.some((glob) =>
1008 NodePath.matchesGlob(packagedPath, glob),
1009 ),
1010 );
1011 }
1012 }),
1013 ),
1014 );
1015
1016 it("excludes node-pty binaries for the other Windows architecture", () => {
1017 assert.deepStrictEqual(resolveWindowsServerAsarIgnoreGlobs("x64"), [
1018 ...WINDOWS_SERVER_ASAR_IGNORE_GLOBS,
1019 "**/node_modules/node-pty/prebuilds/win32-arm64",
1020 "**/node_modules/node-pty/prebuilds/win32-arm64/**",
1021 "**/node_modules/node-pty/third_party/conpty/*/win10-arm64",
1022 "**/node_modules/node-pty/third_party/conpty/*/win10-arm64/**",
1023 ]);
1024 assert.deepStrictEqual(resolveWindowsServerAsarIgnoreGlobs("arm64"), [
1025 ...WINDOWS_SERVER_ASAR_IGNORE_GLOBS,
1026 "**/node_modules/node-pty/prebuilds/win32-x64",
1027 "**/node_modules/node-pty/prebuilds/win32-x64/**",
1028 "**/node_modules/node-pty/third_party/conpty/*/win10-x64",
1029 "**/node_modules/node-pty/third_party/conpty/*/win10-x64/**",
1030 ]);
1031 });
1032
1033 it.effect("unpacks target Windows natives while excluding the other architecture", () =>
1034 Effect.scoped(
1035 Effect.gen(function* () {
1036 const fs = yield* FileSystem.FileSystem;
1037 const path = yield* Path.Path;
1038 const tempDir = yield* fs.makeTempDirectoryScoped({
1039 prefix: "t3-windows-architecture-test-",
1040 });
1041 const sourceDir = path.join(tempDir, "server");
1042 const nativeFiles = [
1043 "node_modules/node-pty/prebuilds/win32-x64/conpty/OpenConsole.exe",
1044 "node_modules/node-pty/prebuilds/win32-arm64/conpty/OpenConsole.exe",
1045 "node_modules/node-pty/third_party/conpty/1.0.0/win10-x64/OpenConsole.exe",
1046 "node_modules/node-pty/third_party/conpty/1.0.0/win10-arm64/OpenConsole.exe",
1047 "node_modules/node-pty/prebuilds/win32-x64/pty.node",
1048 "node_modules/node-pty/prebuilds/linux-x64/pty.node",
1049 ];
1050
1051 for (const nativeFile of nativeFiles) {
1052 const nativePath = path.join(sourceDir, nativeFile);
1053 yield* fs.makeDirectory(path.dirname(nativePath), { recursive: true });
1054 yield* fs.writeFileString(nativePath, nativeFile.includes("linux") ? "\x7fELF" : "MZ");
1055 }
1056
1057 const asarPath = path.join(tempDir, "server.asar");
1058 yield* packWindowsServerAsar({ sourceDir, asarPath, arch: "x64" });
1059 const unpackedRoot = `${asarPath}.unpacked`;
1060
1061 assert.isTrue(
1062 yield* fs.exists(
1063 path.join(
1064 unpackedRoot,
1065 "node_modules/node-pty/prebuilds/win32-x64/conpty/OpenConsole.exe",
1066 ),
1067 ),
1068 );
1069 assert.isTrue(
1070 yield* fs.exists(
1071 path.join(unpackedRoot, "node_modules/node-pty/prebuilds/win32-x64/pty.node"),
1072 ),
1073 );
1074 // Signed builds sign every unpacked .node, so the ELF prebuild stays packed.
1075 assert.isFalse(
1076 yield* fs.exists(path.join(unpackedRoot, "node_modules/node-pty/prebuilds/linux-x64")),
1077 );
1078 assert.equal(
1079 extractFile(
1080 asarPath,
1081 path.join("node_modules", "node-pty", "prebuilds", "linux-x64", "pty.node"),
1082 ).toString(),
1083 "\x7fELF",
1084 );
1085 assert.isFalse(
1086 yield* fs.exists(path.join(unpackedRoot, "node_modules/node-pty/prebuilds/win32-arm64")),
1087 );
1088 assert.isFalse(
1089 yield* fs.exists(
1090 path.join(unpackedRoot, "node_modules/node-pty/third_party/conpty/1.0.0/win10-arm64"),
1091 ),
1092 );
1093 }),
1094 ),
1095 );
1096
1097 it.effect("stages a cached resource monitor without invoking Cargo", () =>
1098 Effect.scoped(
1099 Effect.gen(function* () {
1100 const fs = yield* FileSystem.FileSystem;
1101 const path = yield* Path.Path;
1102 const repoRoot = yield* fs.makeTempDirectoryScoped({
1103 prefix: "t3-resource-monitor-cache-test-",
1104 });
1105 const binaryPath = path.join(
1106 repoRoot,
1107 "native/resource-monitor/target/x86_64-unknown-linux-gnu/release/t3-resource-monitor",
1108 );
1109 const stageResourcesDir = path.join(repoRoot, "stage");
1110 yield* fs.makeDirectory(path.dirname(binaryPath), { recursive: true });
1111 yield* fs.writeFileString(binaryPath, "cached monitor");
1112
1113 yield* stageResourceMonitor({
1114 repoRoot,
1115 stageResourcesDir,
1116 platform: "linux",
1117 arch: "x64",
1118 verbose: false,
1119 }).pipe(
1120 Effect.provide(
1121 ConfigProvider.layer(
1122 ConfigProvider.fromEnv({
1123 env: { T3CODE_DESKTOP_REUSE_RESOURCE_MONITOR: "true" },
1124 }),
1125 ),
1126 ),
1127 );
1128
1129 assert.equal(
1130 yield* fs.readFileString(
1131 path.join(stageResourcesDir, "resource-monitor/t3-resource-monitor"),
1132 ),
1133 "cached monitor",
1134 );
1135 }),
1136 ),
1137 );
1138
1139 it.effect("reports every missing Linux desktop build prerequisite with an install command", () =>
1140 Effect.scoped(
1141 Effect.gen(function* () {
1142 const commands: Array<{ readonly command: string; readonly args: ReadonlyArray<string> }> =
1143 [];
1144 const spawner = Layer.succeed(
1145 ChildProcessSpawner.ChildProcessSpawner,
1146 ChildProcessSpawner.make((command) => {
1147 const childProcess = command as unknown as {
1148 readonly command: string;
1149 readonly args: ReadonlyArray<string>;
1150 };
1151 commands.push(childProcess);
1152 const fails =
1153 childProcess.command === "cargo" ||
1154 childProcess.command === "rustc" ||
1155 childProcess.command === "pkg-config";
1156 return Effect.succeed(mockProcess(fails ? 1 : 0));
1157 }),
1158 );
1159
1160 const error = yield* preflightLinuxDesktopBuild("arm64").pipe(
1161 Effect.provide(spawner),
1162 Effect.flip,
1163 );
1164
1165 assert.instanceOf(error, LinuxDesktopBuildPrerequisitesMissingError);
1166 assert.deepStrictEqual(error.missing, ["cargo", "rust-target", "libsecret"]);
1167 assert.include(error.message, "Rust compiler and Cargo (cargo, rustc)");
1168 assert.include(error.message, "Requested Rust standard library");
1169 assert.include(
1170 error.message,
1171 "sudo apt-get install cargo rustc libsecret-1-dev pkg-config",
1172 );
1173 assert.include(error.message, "rustup target add aarch64-unknown-linux-gnu");
1174 assert.isTrue(
1175 commands.some(
1176 (command) =>
1177 command.command === "rustc" && command.args.includes("aarch64-unknown-linux-gnu"),
1178 ),
1179 );
1180 }),
1181 ),
1182 );
1183
1184 it.effect("reports missing macOS tools and Rust targets before building", () =>
1185 Effect.scoped(
1186 Effect.gen(function* () {
1187 const spawner = Layer.succeed(
1188 ChildProcessSpawner.ChildProcessSpawner,
1189 ChildProcessSpawner.make((command) => {
1190 const childProcess = command as unknown as {
1191 readonly command: string;
1192 readonly args: ReadonlyArray<string>;
1193 };
1194 const fails =
1195 childProcess.command === "rustc" ||
1196 (childProcess.command === "xcrun" && childProcess.args.includes("iconutil"));
1197 return Effect.succeed(mockProcess(fails ? 1 : 0));
1198 }),
1199 );
1200 const error = yield* preflightMacDesktopBuild("universal").pipe(
1201 Effect.provide(spawner),
1202 Effect.flip,
1203 );
1204
1205 assert.instanceOf(error, MacDesktopBuildPrerequisitesMissingError);
1206 assert.deepStrictEqual(error.missing, ["rust", "iconutil"]);
1207 assert.deepStrictEqual(error.rustTargets, ["aarch64-apple-darwin", "x86_64-apple-darwin"]);
1208 assert.include(error.message, "xcode-select --install");
1209 assert.include(error.message, "rustup target add aarch64-apple-darwin x86_64-apple-darwin");
1210 }),
1211 ),
1212 );
1213
1214 it.effect("reports missing Windows toolchain capabilities before building", () =>
1215 Effect.scoped(
1216 Effect.gen(function* () {
1217 const fs = yield* FileSystem.FileSystem;
1218 const path = yield* Path.Path;
1219 const tempDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-windows-preflight-" });
1220 const pythonPath = path.join(tempDir, "python.exe");
1221 yield* fs.writeFileString(pythonPath, "python");
1222 const spawner = Layer.succeed(
1223 ChildProcessSpawner.ChildProcessSpawner,
1224 ChildProcessSpawner.make((command) => {
1225 const childProcess = command as unknown as { readonly command: string };
1226 const fails =
1227 childProcess.command === "rustc" ||
1228 childProcess.command === "powershell.exe" ||
1229 childProcess.command === pythonPath;
1230 return Effect.succeed(mockProcess(fails ? 1 : 0));
1231 }),
1232 );
1233 const error = yield* preflightWindowsDesktopBuild({
1234 arch: "x64",
1235 bundlesWslRuntime: true,
1236 }).pipe(
1237 Effect.provide(
1238 Layer.merge(
1239 spawner,
1240 ConfigProvider.layer(
1241 ConfigProvider.fromEnv({ env: { npm_config_python: pythonPath } }),
1242 ),
1243 ),
1244 ),
1245 Effect.flip,
1246 );
1247
1248 assert.instanceOf(error, WindowsDesktopBuildPrerequisitesMissingError);
1249 assert.deepStrictEqual(error.missing, ["rust", "python", "msvc"]);
1250 assert.equal(error.rustTarget, "x86_64-pc-windows-msvc");
1251 assert.include(error.message, "Visual Studio Build Tools components");
1252 }),
1253 ),
1254 );
1255
1256 it.effect("does not require MSVC when reusing a prebuilt Windows resource monitor", () =>
1257 Effect.scoped(
1258 Effect.gen(function* () {
1259 const fs = yield* FileSystem.FileSystem;
1260 const path = yield* Path.Path;
1261 const tempDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-windows-preflight-" });
1262 const pythonPath = path.join(tempDir, "python.exe");
1263 yield* fs.writeFileString(pythonPath, "python");
1264 const commands: string[] = [];
1265 const spawner = Layer.succeed(
1266 ChildProcessSpawner.ChildProcessSpawner,
1267 ChildProcessSpawner.make((command) => {
1268 const childProcess = command as unknown as { readonly command: string };
1269 commands.push(childProcess.command);
1270 return Effect.succeed(mockProcess(childProcess.command === "powershell.exe" ? 1 : 0));
1271 }),
1272 );
1273
1274 yield* preflightWindowsDesktopBuild({
1275 arch: "x64",
1276 bundlesWslRuntime: true,
1277 }).pipe(
1278 Effect.provide(
1279 Layer.merge(
1280 spawner,
1281 ConfigProvider.layer(
1282 ConfigProvider.fromEnv({
1283 env: {
1284 npm_config_python: pythonPath,
1285 T3CODE_DESKTOP_REUSE_RESOURCE_MONITOR: "true",
1286 },
1287 }),
1288 ),
1289 ),
1290 ),
1291 );
1292
1293 assert.notInclude(commands, "powershell.exe");
1294 }),
1295 ),
1296 );
1297
1298 it.effect("rejects a PATH-discovered Python executable that is not Python 3", () =>
1299 Effect.scoped(
1300 Effect.gen(function* () {
1301 const fs = yield* FileSystem.FileSystem;
1302 const path = yield* Path.Path;
1303 const tempDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-python2-preflight-" });
1304 const pythonPath = path.join(tempDir, "python");
1305 yield* fs.writeFileString(pythonPath, "python2");
1306 const spawner = Layer.succeed(
1307 ChildProcessSpawner.ChildProcessSpawner,
1308 ChildProcessSpawner.make((command) => {
1309 const childProcess = command as unknown as {
1310 readonly command: string;
1311 readonly args: ReadonlyArray<string>;
1312 };
1313 if (childProcess.command === "python") {
1314 return Effect.succeed(mockProcess(0, `${pythonPath}\n`));
1315 }
1316 if (childProcess.command === pythonPath) {
1317 return Effect.succeed(mockProcess(1));
1318 }
1319 return Effect.succeed(mockProcess(0));
1320 }),
1321 );
1322 const error = yield* preflightWindowsDesktopBuild({
1323 arch: "x64",
1324 bundlesWslRuntime: false,
1325 }).pipe(
1326 Effect.provide(
1327 Layer.merge(
1328 spawner,
1329 ConfigProvider.layer(
1330 ConfigProvider.fromEnv({
1331 env: { T3CODE_DESKTOP_REUSE_RESOURCE_MONITOR: "true" },
1332 }),
1333 ),
1334 ),
1335 ),
1336 Effect.flip,
1337 );
1338
1339 assert.instanceOf(error, WindowsDesktopBuildPrerequisitesMissingError);
1340 assert.deepStrictEqual(error.missing, ["python"]);
1341 }),
1342 ),
1343 );
1344
1345 it.effect("builds and stages native capture helpers for each Linux architecture", () =>
1346 Effect.scoped(
1347 Effect.gen(function* () {
1348 const fs = yield* FileSystem.FileSystem;
1349 const path = yield* Path.Path;
1350 const repoRoot = yield* fs.makeTempDirectoryScoped({ prefix: "t3-kde-stage-test-" });
1351 const protocols = path.join(repoRoot, "native/hyprland-snap-shot/protocols");
1352 yield* fs.makeDirectory(protocols, { recursive: true });
1353 yield* fs.writeFileString(path.join(protocols, "capture.xml"), "BSD protocol notice");
1354 for (const backend of ["kde", "hyprland"] as const) {
1355 for (const [arch, target] of [
1356 ["x64", "x86_64-unknown-linux-gnu"],
1357 ["arm64", "aarch64-unknown-linux-gnu"],
1358 ] as const) {
1359 const binary = path.join(
1360 repoRoot,
1361 `native/${backend}-snap-shot/target`,
1362 target,
1363 `release/t3-${backend}-snap-shot`,
1364 );
1365 const stageResourcesDir = path.join(repoRoot, "stage", backend, arch);
1366 const spawner = Layer.succeed(
1367 ChildProcessSpawner.ChildProcessSpawner,
1368 ChildProcessSpawner.make((command) =>
1369 Effect.gen(function* () {
1370 assert.equal(command._tag, "StandardCommand");
1371 if (command._tag !== "StandardCommand") return mockProcess(1);
1372 assert.equal(command.command, "cargo");
1373 assert.deepEqual(command.args, [
1374 "build",
1375 "--locked",
1376 "--release",
1377 "--manifest-path",
1378 path.join(repoRoot, `native/${backend}-snap-shot/Cargo.toml`),
1379 "--target",
1380 target,
1381 ]);
1382 yield* fs.makeDirectory(path.dirname(binary), { recursive: true });
1383 yield* fs.writeFileString(binary, `helper-${arch}`);
1384 return mockProcess(0);
1385 }),
1386 ),
1387 );
1388 yield* stageLinuxCaptureHelper({
1389 backend,
1390 repoRoot,
1391 stageResourcesDir,
1392 arch,
1393 verbose: false,
1394 }).pipe(Effect.provide(spawner));
1395 const installed = path.join(
1396 stageResourcesDir,
1397 `${backend}-capture/t3-${backend}-snap-shot`,
1398 );
1399 assert.equal(yield* fs.readFileString(installed), `helper-${arch}`);
1400 assert.equal((yield* fs.stat(installed)).mode & 0o777, 0o755);
1401 if (backend === "hyprland")
1402 assert.equal(
1403 yield* fs.readFileString(
1404 path.join(stageResourcesDir, "hyprland-capture/protocols/capture.xml"),
1405 ),
1406 "BSD protocol notice",
1407 );
1408 }
1409 }
1410 }),
1411 ),
1412 );
1413
1414 // The fixture's t3code.exe is a text placeholder, not an executable. These
1415 // cases reach the native-load probe, so pin only that host-platform check to
1416 // Linux. Host-native paths and the real Windows tar/archive checks still run.
1417 it.effect("validates every ASAR-unpacked native in the packaged Windows payload", () =>
1418 Effect.scoped(
1419 Effect.gen(function* () {
1420 const fs = yield* FileSystem.FileSystem;
1421 const path = yield* Path.Path;
1422 const fixture = yield* makeWindowsPayloadFixture({ copyUnpackedNatives: true });
1423 const result = yield* validateWindowsPackagedPayload({
1424 stageDistDir: fixture.stageDistDir,
1425 appExecutableName: fixture.appExecutableName,
1426 targetArch: "x64",
1427 appVersion: WINDOWS_PAYLOAD_FIXTURE_VERSION,
1428 });
1429
1430 const secondAsarPath = path.join(path.dirname(fixture.generatedAsarPath), "second.asar");
1431 yield* packWindowsServerAsar({
1432 sourceDir: fixture.sourceDir,
1433 asarPath: secondAsarPath,
1434 arch: "x64",
1435 });
1436 const [firstAsar, secondAsar] = yield* Effect.all([
1437 fs.readFile(fixture.generatedAsarPath),
1438 fs.readFile(secondAsarPath),
1439 ]);
1440
1441 assert.equal(result.packagedAppDir, fixture.packagedAppDir);
1442 assert.deepStrictEqual(result.unpackedFiles, ["node_modules/native/addon.node"]);
1443 assert.isBelow(result.fileCount, WINDOWS_PACKAGED_PAYLOAD_FILE_LIMIT);
1444 assert.deepStrictEqual(secondAsar, firstAsar);
1445 }),
1446 ).pipe(Effect.provideService(HostProcess.Platform, "linux")),
1447 );
1448
1449 it.effect("accepts an embedded Linux CLI release archive with a matching digest", () =>
1450 Effect.scoped(
1451 Effect.gen(function* () {
1452 const fixture = yield* makeWindowsPayloadFixture({
1453 copyUnpackedNatives: true,
1454 wslRuntime: "valid",
1455 });
1456 const result = yield* validateWindowsPackagedPayload({
1457 stageDistDir: fixture.stageDistDir,
1458 appExecutableName: fixture.appExecutableName,
1459 targetArch: "x64",
1460 appVersion: WINDOWS_PAYLOAD_FIXTURE_VERSION,
1461 expectWslRuntime: true,
1462 });
1463
1464 assert.equal(result.packagedAppDir, fixture.packagedAppDir);
1465 }),
1466 ).pipe(Effect.provideService(HostProcess.Platform, "linux")),
1467 );
1468
1469 it.effect.each(["x64", "arm64"] as const)(
1470 "accepts an embedded archive with the Linux %s node-pty prebuild",
1471 (targetArch) =>
1472 Effect.scoped(
1473 Effect.gen(function* () {
1474 const fixture = yield* makeWindowsPayloadFixture({
1475 copyUnpackedNatives: true,
1476 wslRuntime: "valid",
1477 targetArch,
1478 ptyPrebuildArch: targetArch,
1479 });
1480 const result = yield* validateWindowsPackagedPayload({
1481 stageDistDir: fixture.stageDistDir,
1482 appExecutableName: fixture.appExecutableName,
1483 targetArch,
1484 appVersion: WINDOWS_PAYLOAD_FIXTURE_VERSION,
1485 expectWslRuntime: true,
1486 });
1487
1488 assert.equal(result.packagedAppDir, fixture.packagedAppDir);
1489 }),
1490 ).pipe(Effect.provideService(HostProcess.Platform, "linux")),
1491 );
1492
1493 it.effect.each(["x64", "arm64"] as const)(
1494 "rejects a node-pty prebuild for the wrong architecture in a Linux %s archive",
1495 (targetArch) =>
1496 Effect.scoped(
1497 Effect.gen(function* () {
1498 const fixture = yield* makeWindowsPayloadFixture({
1499 copyUnpackedNatives: true,
1500 wslRuntime: "valid",
1501 targetArch,
1502 ptyPrebuildArch: targetArch === "x64" ? "arm64" : "x64",
1503 });
1504 const error = yield* validateWindowsPackagedPayload({
1505 stageDistDir: fixture.stageDistDir,
1506 appExecutableName: fixture.appExecutableName,
1507 targetArch,
1508 appVersion: WINDOWS_PAYLOAD_FIXTURE_VERSION,
1509 expectWslRuntime: true,
1510 }).pipe(Effect.flip);
1511
1512 assert.instanceOf(error, WindowsPackagedPayloadValidationError);
1513 assert.equal(error.reason, "wsl-runtime-invalid");
1514 }),
1515 ),
1516 );
1517
1518 it.effect("rejects an embedded archive built for a different release version", () =>
1519 Effect.scoped(
1520 Effect.gen(function* () {
1521 const fixture = yield* makeWindowsPayloadFixture({
1522 copyUnpackedNatives: true,
1523 wslRuntime: "valid",
1524 });
1525 const error = yield* validateWindowsPackagedPayload({
1526 stageDistDir: fixture.stageDistDir,
1527 appExecutableName: fixture.appExecutableName,
1528 targetArch: "x64",
1529 appVersion: "9.9.9",
1530 expectWslRuntime: true,
1531 }).pipe(Effect.flip);
1532
1533 assert.instanceOf(error, WindowsPackagedPayloadValidationError);
1534 assert.equal(error.reason, "wsl-runtime-invalid");
1535 }),
1536 ),
1537 );
1538
1539 it.effect("rejects a Windows package missing its expected WSL runtime", () =>
1540 Effect.scoped(
1541 Effect.gen(function* () {
1542 const fixture = yield* makeWindowsPayloadFixture({ copyUnpackedNatives: true });
1543 const error = yield* validateWindowsPackagedPayload({
1544 stageDistDir: fixture.stageDistDir,
1545 appExecutableName: fixture.appExecutableName,
1546 targetArch: "x64",
1547 appVersion: WINDOWS_PAYLOAD_FIXTURE_VERSION,
1548 expectWslRuntime: true,
1549 }).pipe(Effect.flip);
1550
1551 assert.instanceOf(error, WindowsPackagedPayloadValidationError);
1552 assert.equal(error.reason, "wsl-runtime-missing");
1553 }),
1554 ),
1555 );
1556
1557 it.effect("rejects a loose server tree that is not a Linux CLI release archive", () =>
1558 Effect.scoped(
1559 Effect.gen(function* () {
1560 const fixture = yield* makeWindowsPayloadFixture({
1561 copyUnpackedNatives: true,
1562 wslRuntime: "loose-server-tree",
1563 });
1564 const error = yield* validateWindowsPackagedPayload({
1565 stageDistDir: fixture.stageDistDir,
1566 appExecutableName: fixture.appExecutableName,
1567 targetArch: "x64",
1568 appVersion: WINDOWS_PAYLOAD_FIXTURE_VERSION,
1569 expectWslRuntime: true,
1570 }).pipe(Effect.flip);
1571
1572 assert.instanceOf(error, WindowsPackagedPayloadValidationError);
1573 assert.equal(error.reason, "wsl-runtime-invalid");
1574 }),
1575 ),
1576 );
1577
1578 it.effect("rejects an embedded archive without the Linux node-pty binary", () =>
1579 Effect.scoped(
1580 Effect.gen(function* () {
1581 const fixture = yield* makeWindowsPayloadFixture({
1582 copyUnpackedNatives: true,
1583 wslRuntime: "missing-pty",
1584 });
1585 const error = yield* validateWindowsPackagedPayload({
1586 stageDistDir: fixture.stageDistDir,
1587 appExecutableName: fixture.appExecutableName,
1588 targetArch: "x64",
1589 appVersion: WINDOWS_PAYLOAD_FIXTURE_VERSION,
1590 expectWslRuntime: true,
1591 }).pipe(Effect.flip);
1592
1593 assert.instanceOf(error, WindowsPackagedPayloadValidationError);
1594 assert.equal(error.reason, "wsl-runtime-invalid");
1595 assert.deepStrictEqual(error.missingFiles, [
1596 `${wslRuntimeArchiveStem(WINDOWS_PAYLOAD_FIXTURE_VERSION, "x64")}/node_modules/node-pty/build/Release/pty.node`,
1597 `${wslRuntimeArchiveStem(WINDOWS_PAYLOAD_FIXTURE_VERSION, "x64")}/node_modules/node-pty/prebuilds/linux-x64/pty.node`,
1598 ]);
1599 }),
1600 ),
1601 );
1602
1603 it.effect("rejects an emitted WSL archive whose sidecar digest does not match", () =>
1604 Effect.scoped(
1605 Effect.gen(function* () {
1606 const fixture = yield* makeWindowsPayloadFixture({
1607 copyUnpackedNatives: true,
1608 wslRuntime: "bad-digest",
1609 });
1610 const error = yield* validateWindowsPackagedPayload({
1611 stageDistDir: fixture.stageDistDir,
1612 appExecutableName: fixture.appExecutableName,
1613 targetArch: "x64",
1614 appVersion: WINDOWS_PAYLOAD_FIXTURE_VERSION,
1615 expectWslRuntime: true,
1616 }).pipe(Effect.flip);
1617
1618 assert.instanceOf(error, WindowsPackagedPayloadValidationError);
1619 assert.equal(error.reason, "wsl-runtime-invalid");
1620 }),
1621 ),
1622 );
1623
1624 it.effect("probes fff through the packaged Windows primary instead of helper executables", () => {
1625 const commands: Array<{
1626 readonly command: string;
1627 readonly args: ReadonlyArray<string>;
1628 readonly options: {
1629 readonly cwd?: string;
1630 readonly env?: Readonly<Record<string, string | undefined>>;
1631 };
1632 }> = [];
1633 const spawnerLayer = Layer.succeed(
1634 ChildProcessSpawner.ChildProcessSpawner,
1635 ChildProcessSpawner.make((command) => {
1636 commands.push(command as unknown as (typeof commands)[number]);
1637 return Effect.succeed(mockProcess(0));
1638 }),
1639 );
1640
1641 return Effect.scoped(
1642 Effect.gen(function* () {
1643 const path = yield* Path.Path;
1644 const fixture = yield* makeWindowsPayloadFixture({ copyUnpackedNatives: true });
1645 yield* validateWindowsPackagedPayload({
1646 stageDistDir: fixture.stageDistDir,
1647 appExecutableName: fixture.appExecutableName,
1648 targetArch: "x64",
1649 appVersion: WINDOWS_PAYLOAD_FIXTURE_VERSION,
1650 });
1651
1652 const primaryProbe = commands.find(
1653 (command) => command.options.env?.ELECTRON_RUN_AS_NODE === "1",
1654 );
1655 if (primaryProbe === undefined) return assert.fail("Windows primary probe was not spawned");
1656
1657 assert.equal(
1658 primaryProbe.command,
1659 path.join(fixture.packagedAppDir, fixture.appExecutableName),
1660 );
1661 assert.deepStrictEqual(primaryProbe.args.slice(0, 3), [
1662 "--no-global-search-paths",
1663 "--input-type=module",
1664 "--eval",
1665 ]);
1666 assert.include(primaryProbe.args[3], "FileFinder.create");
1667 assert.equal(
1668 primaryProbe.args[4],
1669 path.join(
1670 fixture.packagedAppDir,
1671 "resources/server.asar/node_modules/@ff-labs/fff-node/dist/index.js",
1672 ),
1673 );
1674 assert.equal(primaryProbe.options.cwd, fixture.packagedAppDir);
1675 assert.equal(primaryProbe.options.env?.NODE_PATH, "");
1676 }),
1677 ).pipe(
1678 Effect.provide(
1679 Layer.mergeAll(
1680 spawnerLayer,
1681 Layer.succeed(HostProcess.Platform, "win32"),
1682 Layer.succeed(HostProcess.Architecture, "x64"),
1683 ),
1684 ),
1685 );
1686 });
1687
1688 it.effect("builds the Linux browser secret helper for a concrete architecture", () => {
1689 const commands: Array<{ readonly command: string; readonly args: ReadonlyArray<string> }> = [];
1690 const spawnerLayer = Layer.succeed(
1691 ChildProcessSpawner.ChildProcessSpawner,
1692 ChildProcessSpawner.make((command) => {
1693 commands.push(command as unknown as (typeof commands)[number]);
1694 return Effect.succeed(mockProcess(0));
1695 }),
1696 );
1697
1698 return Effect.gen(function* () {
1699 // `universal` is a mac-only arch the option type still admits. The helper
1700 // script only knows x64 and arm64, so the request maps to x64, the same
1701 // concrete target the Linux resource monitor resolves it to.
1702 yield* stageBrowserSecret({
1703 repoRoot: "/repo",
1704 stageResourcesDir: "/stage/resources",
1705 platform: "linux",
1706 arch: "universal",
1707 verbose: false,
1708 });
1709 const helper = commands.find((command) =>
1710 command.args.some((arg) => arg.endsWith("build-browser-secret.mjs")),
1711 );
1712 assert.isDefined(helper);
1713 const path = yield* Path.Path;
1714 assert.deepStrictEqual(helper.args.slice(-4), [
1715 "--arch",
1716 "x64",
1717 "--output",
1718 path.join("/stage/resources", "browser-secret", "t3-browser-secret"),
1719 ]);
1720 }).pipe(
1721 Effect.provide(
1722 Layer.mergeAll(
1723 spawnerLayer,
1724 Layer.succeed(HostProcess.Platform, "linux"),
1725 Layer.succeed(HostProcess.Architecture, "x64"),
1726 ),
1727 ),
1728 );
1729 });
1730
1731 it.effect("checks the isolated Windows bundle through an explicit Wine runtime", () => {
1732 const commands: Array<{ readonly command: string; readonly args: ReadonlyArray<string> }> = [];
1733 const spawnerLayer = Layer.succeed(
1734 ChildProcessSpawner.ChildProcessSpawner,
1735 ChildProcessSpawner.make((command) => {
1736 if (!ChildProcess.isStandardCommand(command)) {
1737 return Effect.die(new Error("Expected a standard command"));
1738 }
1739 commands.push(command);
1740 return Effect.succeed(mockProcess(0));
1741 }),
1742 );
1743 return Effect.scoped(
1744 Effect.gen(function* () {
1745 const fixture = yield* makeWindowsPayloadFixture({ copyUnpackedNatives: true });
1746 yield* validateWindowsPackagedPayload({
1747 stageDistDir: fixture.stageDistDir,
1748 appExecutableName: fixture.appExecutableName,
1749 targetArch: "x64",
1750 appVersion: WINDOWS_PAYLOAD_FIXTURE_VERSION,
1751 });
1752 const probes = commands.filter((command) => command.command === "/tools/wine");
1753 assert.lengthOf(probes, 1);
1754 const [isolated] = probes;
1755 if (isolated === undefined) return assert.fail("The Windows bundle probe must run");
1756 assert.isTrue(
1757 probes.every((command) => command.args[0]?.endsWith(fixture.appExecutableName)),
1758 );
1759 assert.equal(isolated.args.at(-1), "--version");
1760 assert.isTrue(
1761 isolated.args.some((arg) => arg.startsWith("Z:\\") && arg.endsWith("bin.mjs")),
1762 );
1763 }),
1764 ).pipe(
1765 Effect.provide(
1766 Layer.mergeAll(
1767 spawnerLayer,
1768 Layer.succeed(HostProcess.Platform, "darwin"),
1769 Layer.succeed(HostProcess.Architecture, "arm64"),
1770 ConfigProvider.layer(
1771 ConfigProvider.fromEnv({ env: { T3CODE_DESKTOP_WINDOWS_RUNTIME: "/tools/wine" } }),
1772 ),
1773 ),
1774 ),
1775 );
1776 });
1777
1778 it.effect("refuses a Linux build on a host that cannot build the browser secret helper", () => {
1779 const commands: Array<{ readonly command: string }> = [];
1780 const spawnerLayer = Layer.succeed(
1781 ChildProcessSpawner.ChildProcessSpawner,
1782 ChildProcessSpawner.make((command) => {
1783 commands.push(command as unknown as (typeof commands)[number]);
1784 return Effect.succeed(mockProcess(0));
1785 }),
1786 );
1787
1788 return Effect.gen(function* () {
1789 // The helper links against the host's libsecret and its build script is
1790 // a no-op elsewhere, so a Linux artifact built on macOS would ship
1791 // without it and report the keyring as unavailable on every import.
1792 const error = yield* stageBrowserSecret({
1793 repoRoot: "/repo",
1794 stageResourcesDir: "/stage/resources",
1795 platform: "linux",
1796 arch: "x64",
1797 verbose: false,
1798 }).pipe(Effect.flip);
1799 assert.instanceOf(error, LinuxBrowserSecretHostError);
1800 assert.equal(error.hostPlatform, "darwin");
1801 assert.include(error.message, "Linux host");
1802 assert.lengthOf(commands, 0);
1803 }).pipe(
1804 Effect.provide(
1805 Layer.mergeAll(
1806 spawnerLayer,
1807 Layer.succeed(HostProcess.Platform, "darwin"),
1808 Layer.succeed(HostProcess.Architecture, "arm64"),
1809 ),
1810 ),
1811 );
1812 });
1813
1814 it.effect("skips the primary native probe for cross-architecture Windows payloads", () => {
1815 const commands: Array<{
1816 readonly command: string;
1817 readonly options: {
1818 readonly env?: Readonly<Record<string, string | undefined>>;
1819 };
1820 }> = [];
1821 const spawnerLayer = Layer.succeed(
1822 ChildProcessSpawner.ChildProcessSpawner,
1823 ChildProcessSpawner.make((command) => {
1824 commands.push(command as unknown as (typeof commands)[number]);
1825 return Effect.succeed(mockProcess(0));
1826 }),
1827 );
1828
1829 return Effect.scoped(
1830 Effect.gen(function* () {
1831 const fixture = yield* makeWindowsPayloadFixture({ copyUnpackedNatives: true });
1832 yield* validateWindowsPackagedPayload({
1833 stageDistDir: fixture.stageDistDir,
1834 appExecutableName: fixture.appExecutableName,
1835 targetArch: "arm64",
1836 appVersion: WINDOWS_PAYLOAD_FIXTURE_VERSION,
1837 });
1838
1839 assert.isFalse(
1840 commands.some((command) => command.command.endsWith(fixture.appExecutableName)),
1841 );
1842 assert.isTrue(
1843 commands.some(
1844 (command) =>
1845 command.command === process.execPath && command.options.env?.NODE_PATH === "",
1846 ),
1847 );
1848 }),
1849 ).pipe(
1850 Effect.provide(
1851 Layer.mergeAll(
1852 spawnerLayer,
1853 Layer.succeed(HostProcess.Platform, "win32"),
1854 Layer.succeed(HostProcess.Architecture, "x64"),
1855 ),
1856 ),
1857 );
1858 });
1859
1860 it.effect("rejects a cross-architecture Windows payload without its primary executable", () =>
1861 Effect.scoped(
1862 Effect.gen(function* () {
1863 const fs = yield* FileSystem.FileSystem;
1864 const path = yield* Path.Path;
1865 const fixture = yield* makeWindowsPayloadFixture({ copyUnpackedNatives: true });
1866 const executablePath = path.join(fixture.packagedAppDir, fixture.appExecutableName);
1867 yield* fs.remove(executablePath);
1868
1869 const error = yield* validateWindowsPackagedPayload({
1870 stageDistDir: fixture.stageDistDir,
1871 appExecutableName: fixture.appExecutableName,
1872 targetArch: "arm64",
1873 appVersion: WINDOWS_PAYLOAD_FIXTURE_VERSION,
1874 }).pipe(Effect.flip);
1875
1876 assert.instanceOf(error, WindowsPrimaryNativeProbeError);
1877 assert.equal(error.executablePath, executablePath);
1878 }),
1879 ).pipe(
1880 Effect.provide(
1881 Layer.mergeAll(
1882 Layer.succeed(HostProcess.Platform, "win32"),
1883 Layer.succeed(HostProcess.Architecture, "x64"),
1884 ),
1885 ),
1886 ),
1887 );
1888
1889 it.effect("rejects a packaged sidecar whose ASAR-unpacked native is missing", () =>
1890 Effect.scoped(
1891 Effect.gen(function* () {
1892 const fixture = yield* makeWindowsPayloadFixture({ copyUnpackedNatives: false });
1893 const error = yield* validateWindowsPackagedPayload({
1894 stageDistDir: fixture.stageDistDir,
1895 appExecutableName: fixture.appExecutableName,
1896 targetArch: "x64",
1897 appVersion: WINDOWS_PAYLOAD_FIXTURE_VERSION,
1898 }).pipe(Effect.flip);
1899
1900 assert.instanceOf(error, WindowsPackagedPayloadValidationError);
1901 assert.equal(error.reason, "unpacked-native-missing");
1902 assert.deepStrictEqual(error.missingFiles, [
1903 "server.asar.unpacked/node_modules/native/addon.node",
1904 ]);
1905 }),
1906 ),
1907 );
1908
1909 it.effect("rejects directories in place of packaged executable files", () =>
1910 Effect.scoped(
1911 Effect.gen(function* () {
1912 const fs = yield* FileSystem.FileSystem;
1913 const path = yield* Path.Path;
1914 const fixture = yield* makeWindowsPayloadFixture({ copyUnpackedNatives: true });
1915 const nativePath = path.join(
1916 fixture.packagedAppDir,
1917 "resources/server.asar.unpacked/node_modules/native/addon.node",
1918 );
1919 yield* fs.remove(nativePath);
1920 yield* fs.makeDirectory(nativePath);
1921
1922 const nativeError = yield* validateWindowsPackagedPayload({
1923 stageDistDir: fixture.stageDistDir,
1924 appExecutableName: fixture.appExecutableName,
1925 targetArch: "x64",
1926 appVersion: WINDOWS_PAYLOAD_FIXTURE_VERSION,
1927 }).pipe(Effect.flip);
1928 assert.instanceOf(nativeError, WindowsPackagedPayloadValidationError);
1929 assert.equal(nativeError.reason, "unpacked-native-missing");
1930 assert.deepStrictEqual(nativeError.missingFiles, [
1931 "server.asar.unpacked/node_modules/native/addon.node",
1932 ]);
1933
1934 yield* fs.remove(nativePath, { recursive: true });
1935 yield* fs.writeFileString(nativePath, "native-binary");
1936 const resourceMonitorPath = path.join(
1937 fixture.packagedAppDir,
1938 "resources/resource-monitor/t3-resource-monitor.exe",
1939 );
1940 yield* fs.remove(resourceMonitorPath);
1941 yield* fs.makeDirectory(resourceMonitorPath);
1942
1943 const resourceMonitorError = yield* validateWindowsPackagedPayload({
1944 stageDistDir: fixture.stageDistDir,
1945 appExecutableName: fixture.appExecutableName,
1946 targetArch: "x64",
1947 appVersion: WINDOWS_PAYLOAD_FIXTURE_VERSION,
1948 }).pipe(Effect.flip);
1949 assert.instanceOf(resourceMonitorError, WindowsPackagedPayloadValidationError);
1950 assert.equal(resourceMonitorError.reason, "resource-monitor-missing");
1951 assert.deepStrictEqual(resourceMonitorError.missingFiles, [
1952 "resource-monitor/t3-resource-monitor.exe",
1953 ]);
1954 }),
1955 ),
1956 );
1957
1958 it.effect("rejects a Windows payload that regresses above the file-count budget", () =>
1959 Effect.scoped(
1960 Effect.gen(function* () {
1961 const fixture = yield* makeWindowsPayloadFixture({ copyUnpackedNatives: true });
1962 const error = yield* validateWindowsPackagedPayload({
1963 stageDistDir: fixture.stageDistDir,
1964 appExecutableName: fixture.appExecutableName,
1965 targetArch: "x64",
1966 appVersion: WINDOWS_PAYLOAD_FIXTURE_VERSION,
1967 fileLimit: 2,
1968 }).pipe(Effect.flip);
1969
1970 assert.instanceOf(error, WindowsPackagedPayloadValidationError);
1971 assert.equal(error.reason, "file-limit-exceeded");
1972 assert.isAbove(error.fileCount ?? 0, 2);
1973 }),
1974 ),
1975 );
1976
1977 it.effect("rejects a sidecar whose extracted server bundle cannot resolve", () =>
1978 Effect.scoped(
1979 Effect.gen(function* () {
1980 const fixture = yield* makeWindowsPayloadFixture({
1981 copyUnpackedNatives: true,
1982 serverEntrySource: 'import "t3code-deliberately-missing-package";\n',
1983 });
1984 const error = yield* validateWindowsPackagedPayload({
1985 stageDistDir: fixture.stageDistDir,
1986 appExecutableName: fixture.appExecutableName,
1987 targetArch: "x64",
1988 appVersion: WINDOWS_PAYLOAD_FIXTURE_VERSION,
1989 }).pipe(Effect.flip);
1990
1991 assert.instanceOf(error, BundleNotSelfContainedError);
1992 assert.include(error.output, "t3code-deliberately-missing-package");
1993 }),
1994 ).pipe(Effect.provideService(HostProcess.Platform, "linux")),
1995 );
1996
1997 it.effect("preserves both Linux icon resize failures with structural context", () => {
1998 const commands: Array<{ readonly command: string; readonly args: ReadonlyArray<string> }> = [];
1999
2000 return Effect.gen(function* () {
2001 const error = yield* stageLinuxIconSize("source.png", "target.png", 512, false).pipe(
2002 Effect.provide(iconResizeSpawnerLayer(commands, [1, 2])),
2003 Effect.flip,
2004 );
2005
2006 assert.instanceOf(error, LinuxIconResizeError);
2007 assert.equal(error.operation, "resize");
2008 assert.equal(error.iconSize, 512);
2009 assert.equal(error.primaryTool, "magick");
2010 assert.equal(error.fallbackTool, "convert");
2011 assert.include(error.message, "512x512");
2012 assert.include(error.message, "`magick`");
2013 assert.include(error.message, "`convert`");
2014 assert.notInclude(error.message, "non-zero exit code");
2015
2016 assert.instanceOf(error.cause, AggregateError);
2017 const aggregateCause = error.cause as AggregateError;
2018 assert.lengthOf(aggregateCause.errors, 2);
2019 assert.strictEqual(aggregateCause.cause, aggregateCause.errors[0]);
2020 assert.instanceOf(aggregateCause.errors[0], BuildCommandFailedError);
2021 assert.instanceOf(aggregateCause.errors[1], BuildCommandFailedError);
2022 const primaryError = aggregateCause.errors[0] as BuildCommandFailedError;
2023 const fallbackError = aggregateCause.errors[1] as BuildCommandFailedError;
2024 assert.equal(primaryError.command, "magick linux icon 512x512");
2025 assert.equal(primaryError.exitCode, 1);
2026 assert.include(primaryError.message, "magick linux icon");
2027 assert.equal(fallbackError.command, "convert linux icon 512x512");
2028 assert.equal(fallbackError.exitCode, 2);
2029 assert.include(fallbackError.message, "convert linux icon");
2030 assert.deepStrictEqual(
2031 commands.map(({ command }) => command),
2032 ["magick", "convert"],
2033 );
2034 });
2035 });
2036
2037 it.effect("rasterizes staged DMG backgrounds at standard and Retina sizes", () =>
2038 Effect.scoped(
2039 Effect.gen(function* () {
2040 const fs = yield* FileSystem.FileSystem;
2041 const path = yield* Path.Path;
2042 const stageResourcesDir = yield* fs.makeTempDirectoryScoped({
2043 prefix: "t3code-dmg-background-",
2044 });
2045 const dmgDir = path.join(stageResourcesDir, "dmg");
2046 yield* fs.makeDirectory(dmgDir, { recursive: true });
2047 const sourcePath = path.join(dmgDir, "dmg-background-nightly.svg");
2048 yield* fs.writeFileString(sourcePath, '<svg xmlns="http://www.w3.org/2000/svg"/>');
2049 const commands: Array<{ readonly command: string; readonly args: ReadonlyArray<string> }> =
2050 [];
2051
2052 yield* stageDesktopDmgBackground(stageResourcesDir, "nightly", false).pipe(
2053 Effect.provide(iconResizeSpawnerLayer(commands, [0, 0])),
2054 );
2055
2056 assert.deepStrictEqual(
2057 commands.map((command) => [command.command, ...command.args]),
2058 [
2059 [
2060 "sips",
2061 "-s",
2062 "format",
2063 "png",
2064 "-z",
2065 "432",
2066 "640",
2067 sourcePath,
2068 "--out",
2069 path.join(dmgDir, "dmg-background-nightly.png"),
2070 ],
2071 [
2072 "sips",
2073 "-s",
2074 "format",
2075 "png",
2076 "-z",
2077 "864",
2078 "1280",
2079 sourcePath,
2080 "--out",
2081 path.join(dmgDir, "dmg-background-nightly@2x.png"),
2082 ],
2083 ],
2084 );
2085 }),
2086 ),
2087 );
2088
2089 it.effect("fails clearly when the selected DMG background source is missing", () =>
2090 Effect.scoped(
2091 Effect.gen(function* () {
2092 const fs = yield* FileSystem.FileSystem;
2093 const stageResourcesDir = yield* fs.makeTempDirectoryScoped({
2094 prefix: "t3code-dmg-background-missing-",
2095 });
2096
2097 const error = yield* stageDesktopDmgBackground(stageResourcesDir, "latest", false).pipe(
2098 Effect.flip,
2099 );
2100
2101 assert.instanceOf(error, DesktopDmgBackgroundSourceMissingError);
2102 assert.equal(error.channel, "latest");
2103 assert.include(error.sourcePath, "dmg-background-latest.svg");
2104 }),
2105 ),
2106 );
2107
2108 it("derives macOS passkey signing configuration from the Clerk publishable key", () => {
2109 const configuration = resolveMacPasskeySigningConfiguration({
2110 T3CODE_APPLE_TEAM_ID: "abc1234567",
2111 T3CODE_MACOS_PROVISIONING_PROFILE: "/tmp/t3code.provisionprofile",
2112 T3CODE_CLERK_PUBLISHABLE_KEY: `pk_test_${btoa("example.clerk.accounts.dev$")}`,
2113 });
2114
2115 assert.deepStrictEqual(configuration, {
2116 appId: "com.t3tools.t3code",
2117 teamId: "ABC1234567",
2118 rpDomains: ["example.clerk.accounts.dev"],
2119 provisioningProfilePath: "/tmp/t3code.provisionprofile",
2120 });
2121 });
2122
2123 it("normalizes explicit macOS passkey RP domains and renders required entitlements", () => {
2124 const configuration = resolveMacPasskeySigningConfiguration({
2125 T3CODE_APPLE_TEAM_ID: "ABC1234567",
2126 T3CODE_MACOS_PROVISIONING_PROFILE: "/tmp/t3code.provisionprofile",
2127 T3CODE_CLERK_PASSKEY_RP_DOMAINS:
2128 " Clerk.Example.com,example.clerk.accounts.dev,clerk.example.com ",
2129 });
2130 const entitlements = renderMacPasskeyEntitlements(configuration, {
2131 touchIdKeychainAccessGroup: undefined,
2132 browserPasskeys: false,
2133 });
2134
2135 assert.deepStrictEqual(configuration.rpDomains, [
2136 "clerk.example.com",
2137 "example.clerk.accounts.dev",
2138 ]);
2139 assert.include(entitlements, "<string>ABC1234567.com.t3tools.t3code</string>");
2140 assert.include(entitlements, "<string>webcredentials:clerk.example.com</string>");
2141 assert.include(entitlements, "<string>webcredentials:example.clerk.accounts.dev</string>");
2142 assert.include(entitlements, "<key>com.apple.security.cs.allow-jit</key>");
2143 assert.notInclude(entitlements, "keychain-access-groups");
2144 assert.notInclude(entitlements, "com.apple.developer.web-browser.public-key-credential");
2145 });
2146
2147 it("grants in-app browser passkey entitlements only when the provisioning profile does", () => {
2148 const configuration = { appId: "com.t3tools.t3code", teamId: "ABC1234567" };
2149 // Profiles are CMS envelopes around a plain XML plist.
2150 const profile = (entitlements: string, outside = "") =>
2151 `0\x82\x1f\x9a\x06\t*\x86H<?xml version="1.0"?><plist version="1.0"><dict>${outside}<key>Entitlements</key><dict>${entitlements}</dict></dict></plist>\x00\x01`;
2152 const teamWildcardGroups = `<key>keychain-access-groups</key>
2153 <array>
2154 <string>ABC1234567.*</string>
2155 <string>com.apple.token</string>
2156 </array>`;
2157
2158 assert.deepStrictEqual(resolveMacWebAuthnEntitlements(profile(""), configuration), {
2159 touchIdKeychainAccessGroup: undefined,
2160 browserPasskeys: false,
2161 });
2162 assert.deepStrictEqual(
2163 resolveMacWebAuthnEntitlements(
2164 profile("<key>keychain-access-groups</key><array><string>OTHERTEAM1.*</string></array>"),
2165 configuration,
2166 ),
2167 { touchIdKeychainAccessGroup: undefined, browserPasskeys: false },
2168 );
2169 // Only real values inside the Entitlements dict count: not comments, not
2170 // explicit false, not keys elsewhere in the profile.
2171 assert.deepStrictEqual(
2172 resolveMacWebAuthnEntitlements(
2173 profile(
2174 `<!-- ${teamWildcardGroups}
2175 <key>com.apple.developer.web-browser.public-key-credential</key><true/> -->
2176 <key>com.apple.developer.web-browser.public-key-credential</key><false/>`,
2177 teamWildcardGroups,
2178 ),
2179 configuration,
2180 ),
2181 { touchIdKeychainAccessGroup: undefined, browserPasskeys: false },
2182 );
2183 assert.deepStrictEqual(
2184 resolveMacWebAuthnEntitlements(
2185 profile(
2186 `${teamWildcardGroups}
2187 <key>com.apple.developer.web-browser.public-key-credential</key>
2188 <true/>`,
2189 ),
2190 configuration,
2191 ),
2192 {
2193 touchIdKeychainAccessGroup: "ABC1234567.com.t3tools.t3code.webauthn",
2194 browserPasskeys: true,
2195 },
2196 );
2197
2198 const entitlements = renderMacPasskeyEntitlements(
2199 { ...configuration, rpDomains: ["clerk.example.com"], provisioningProfilePath: "" },
2200 resolveMacWebAuthnEntitlements(
2201 profile(
2202 `${teamWildcardGroups}<key>com.apple.developer.web-browser.public-key-credential</key><true/>`,
2203 ),
2204 configuration,
2205 ),
2206 );
2207 assert.match(
2208 entitlements,
2209 /<key>keychain-access-groups<\/key>\s*<array>\s*<string>ABC1234567\.com\.t3tools\.t3code\.webauthn<\/string>\s*<\/array>/u,
2210 );
2211 assert.match(
2212 entitlements,
2213 /<key>com\.apple\.developer\.web-browser\.public-key-credential<\/key>\s*<true\/>/u,
2214 );
2215 });
2216
2217 it("rejects incomplete macOS passkey signing configuration", () => {
2218 const captureError = (env: Readonly<Record<string, string | undefined>>) => {
2219 try {
2220 resolveMacPasskeySigningConfiguration(env);
2221 } catch (error) {
2222 return error;
2223 }
2224 return assert.fail("Expected passkey signing configuration to fail.");
2225 };
2226
2227 const missingProfileError = captureError({
2228 T3CODE_APPLE_TEAM_ID: "ABC1234567",
2229 T3CODE_CLERK_PASSKEY_RP_DOMAINS: "example.clerk.accounts.dev",
2230 });
2231 assert.instanceOf(missingProfileError, MissingMacPasskeyProvisioningProfileError);
2232 assert.equal(
2233 missingProfileError.message,
2234 "T3CODE_MACOS_PROVISIONING_PROFILE must point to an Associated Domains provisioning profile.",
2235 );
2236
2237 const unsafeDomain =
2238 "https://domain-user:domain-secret@example.clerk.accounts.dev/path?token=query-secret";
2239 const invalidDomainError = captureError({
2240 T3CODE_APPLE_TEAM_ID: "ABC1234567",
2241 T3CODE_MACOS_PROVISIONING_PROFILE: "/tmp/t3code.provisionprofile",
2242 T3CODE_CLERK_PASSKEY_RP_DOMAINS: unsafeDomain,
2243 });
2244 assert.instanceOf(invalidDomainError, InvalidMacPasskeyRpDomainError);
2245 assert.equal(invalidDomainError.reason, "scheme-not-allowed");
2246 assert.equal(invalidDomainError.inputLength, unsafeDomain.length);
2247 assert.equal(invalidDomainError.message, "Invalid passkey RP domain (scheme-not-allowed).");
2248 assert.notProperty(invalidDomainError, "domain");
2249 assert.notProperty(invalidDomainError, "cause");
2250 const serializedInvalidDomainError = JSON.stringify(invalidDomainError);
2251 assert.notInclude(serializedInvalidDomainError, unsafeDomain);
2252 assert.notInclude(serializedInvalidDomainError, "domain-user");
2253 assert.notInclude(serializedInvalidDomainError, "domain-secret");
2254 assert.notInclude(serializedInvalidDomainError, "query-secret");
2255 assert.throws(
2256 () =>
2257 resolveMacPasskeySigningConfiguration({
2258 T3CODE_APPLE_TEAM_ID: "ABC1234567",
2259 T3CODE_MACOS_PROVISIONING_PROFILE: "/tmp/t3code.provisionprofile",
2260 T3CODE_CLERK_PASSKEY_RP_DOMAINS: "example.clerk.accounts.dev:8443",
2261 }),
2262 /Invalid passkey RP domain/u,
2263 );
2264 const invalidPublishableKeyError = captureError({
2265 T3CODE_APPLE_TEAM_ID: "ABC1234567",
2266 T3CODE_MACOS_PROVISIONING_PROFILE: "/tmp/t3code.provisionprofile",
2267 T3CODE_CLERK_PUBLISHABLE_KEY: "pk_test_%",
2268 });
2269 assert.instanceOf(invalidPublishableKeyError, InvalidMacPasskeyPublishableKeyError);
2270 assert.ok(invalidPublishableKeyError.cause);
2271 assert.equal(invalidPublishableKeyError.message, "T3CODE_CLERK_PUBLISHABLE_KEY is invalid.");
2272 assert.notProperty(invalidPublishableKeyError, "publishableKey");
2273 assert.notInclude(invalidPublishableKeyError.message, "pk_test_%");
2274 });
2275
2276 it("preserves known passkey signing configuration errors at the build boundary", () => {
2277 const decodingCause = new Error("publishable-key-decode-failed");
2278 const knownError = new InvalidMacPasskeyPublishableKeyError({ cause: decodingCause });
2279 const error = MacPasskeySigningConfigurationResolutionError.fromCause(knownError);
2280
2281 assert.strictEqual(error, knownError);
2282 assert.instanceOf(error, InvalidMacPasskeyPublishableKeyError);
2283 assert.strictEqual(error.cause, decodingCause);
2284 assert.isTrue(isMacPasskeySigningConfigurationError(error));
2285 });
2286
2287 it("wraps unknown passkey signing configuration defects without copying cause text", () => {
2288 const secret = "pk_test_do-not-retain";
2289 const cause = new Error(secret);
2290 const error = MacPasskeySigningConfigurationResolutionError.fromCause(cause);
2291
2292 assert.instanceOf(error, MacPasskeySigningConfigurationResolutionError);
2293 assert.strictEqual(error.cause, cause);
2294 assert.equal(error.message, "Failed to resolve macOS passkey signing configuration.");
2295 assert.notInclude(error.message, secret);
2296 });
2297
2298 it.effect("adds passkey entitlements and both renderer protocols to signed macOS builds", () =>
2299 Effect.gen(function* () {
2300 const config = yield* createBuildConfig("mac", "dmg", "1.2.3", true, false, undefined, {
2301 entitlementsPath: "/tmp/entitlements.mac.plist",
2302 provisioningProfilePath: "/tmp/t3code.provisionprofile",
2303 });
2304
2305 const mac = config.mac as Record<string, unknown>;
2306 assert.equal(config.appId, "com.t3tools.t3code");
2307 assert.equal(mac.entitlements, "/tmp/entitlements.mac.plist");
2308 assert.equal(mac.provisioningProfile, "/tmp/t3code.provisionprofile");
2309 assert.match(String(mac.sign), /[\\/]scripts[\\/]sign-macos\.ts$/);
2310 assert.deepStrictEqual(mac.protocols, [
2311 { name: "The Knife", schemes: ["t3code", "t3code-dev"] },
2312 { name: "Web site URL", schemes: ["http", "https"], role: "Viewer" },
2313 ]);
2314 }).pipe(Effect.provide(ConfigProvider.layer(ConfigProvider.fromEnv({ env: {} })))),
2315 );
2316
2317 it.effect("uses the nightly DMG background for nightly macOS builds", () =>
2318 Effect.gen(function* () {
2319 const config = yield* createBuildConfig(
2320 "mac",
2321 "dmg",
2322 "1.2.3-nightly.20260815.1",
2323 false,
2324 false,
2325 undefined,
2326 undefined,
2327 );
2328
2329 assert.equal(
2330 (config.dmg as Record<string, unknown>).background,
2331 "dmg/dmg-background-nightly.png",
2332 );
2333 }).pipe(Effect.provide(ConfigProvider.layer(ConfigProvider.fromEnv({ env: {} })))),
2334 );
2335
2336 it.effect("configures Windows executable metadata and native signing extensions", () =>
2337 Effect.gen(function* () {
2338 const config = yield* createBuildConfig(
2339 "win",
2340 "nsis",
2341 "1.2.3",
2342 false,
2343 false,
2344 undefined,
2345 undefined,
2346 );
2347
2348 const win = config.win as Record<string, unknown>;
2349 assert.equal(win.icon, "icon.ico");
2350 assert.deepStrictEqual(win.signExts, [".node", ".dll"]);
2351 assert.equal(win.signAndEditExecutable, true);
2352 assert.notProperty(win, "azureSignOptions");
2353 }).pipe(Effect.provide(ConfigProvider.layer(ConfigProvider.fromEnv({ env: {} })))),
2354 );
2355
2356 it("stages the resource monitor as an external executable resource", () => {
2357 assert.deepStrictEqual(DESKTOP_EXTRA_RESOURCES, [
2358 {
2359 from: "apps/desktop/prod-resources/cursor-sdk",
2360 to: "node_modules/@cursor",
2361 },
2362 {
2363 from: "apps/desktop/prod-resources/resource-monitor",
2364 to: "resource-monitor",
2365 },
2366 ]);
2367 assert.deepStrictEqual(resolveResourceMonitorRustTargets("mac", "universal"), [
2368 "aarch64-apple-darwin",
2369 "x86_64-apple-darwin",
2370 ]);
2371 assert.deepStrictEqual(resolveResourceMonitorRustTargets("linux", "x64"), [
2372 "x86_64-unknown-linux-gnu",
2373 ]);
2374 assert.deepStrictEqual(resolveResourceMonitorRustTargets("win", "arm64"), [
2375 "aarch64-pc-windows-msvc",
2376 ]);
2377 assert.equal(resourceMonitorExecutableName("mac"), "t3-resource-monitor");
2378 assert.equal(resourceMonitorExecutableName("win"), "t3-resource-monitor.exe");
2379 });
2380
2381 it("ships the Linux CLI release archive as the WSL runtime", () => {
2382 assert.equal(WSL_RUNTIME_ARCHIVE_NAME, "wsl-runtime.tar.gz");
2383 assert.equal(WSL_RUNTIME_ARCHIVE_HASH_NAME, "wsl-runtime.tar.gz.sha256");
2384 assert.deepStrictEqual(WSL_RUNTIME_ARCHIVE_EXTRA_RESOURCE, {
2385 from: "apps/desktop/prod-resources/wsl-runtime.tar.gz",
2386 to: "wsl-runtime.tar.gz",
2387 });
2388 assert.deepStrictEqual(WSL_RUNTIME_ARCHIVE_HASH_EXTRA_RESOURCE, {
2389 from: "apps/desktop/prod-resources/wsl-runtime.tar.gz.sha256",
2390 to: "wsl-runtime.tar.gz.sha256",
2391 });
2392 // Both the staging and the packaging config hang off this one decision:
2393 // Windows only, and only when CI handed the build a Linux CLI archive.
2394 const runtimeArchivePath = "/tmp/t3-1.2.3-linux-x64.tar.gz";
2395 assert.isTrue(bundlesWslRuntime({ platform: "win", runtimeArchivePath }));
2396 assert.isFalse(bundlesWslRuntime({ platform: "win", runtimeArchivePath: undefined }));
2397 assert.isFalse(bundlesWslRuntime({ platform: "linux", runtimeArchivePath }));
2398 assert.isFalse(bundlesWslRuntime({ platform: "mac", runtimeArchivePath }));
2399 assert.equal(wslRuntimeArchiveStem("1.2.3", "x64"), "t3-1.2.3-linux-x64");
2400 });
2401
2402 it("parses Windows bsdtar member listings with CRLF line endings", () => {
2403 assert.deepStrictEqual(
2404 parseWslRuntimeArchiveMembers("./t3-1.2.3-linux-x64/t3\r\nt3-1.2.3-linux-x64/client/\r\n"),
2405 ["t3-1.2.3-linux-x64/t3", "t3-1.2.3-linux-x64/client"],
2406 );
2407 });
2408
2409 it.effect("stages the Linux CLI archive verbatim with its SHA-256 sidecar", () =>
2410 Effect.scoped(
2411 Effect.gen(function* () {
2412 const fs = yield* FileSystem.FileSystem;
2413 const path = yield* Path.Path;
2414 const root = yield* fs.makeTempDirectoryScoped({ prefix: "t3-wsl-runtime-stage-" });
2415 const sourceArchivePath = yield* makeLinuxCliArchiveFixture({
2416 root,
2417 stem: "t3-1.2.3-linux-x64",
2418 });
2419 const stageAppDir = path.join(root, "app");
2420 const archivePath = path.join(stageAppDir, WSL_RUNTIME_ARCHIVE_EXTRA_RESOURCE.from);
2421 const hashPath = path.join(stageAppDir, WSL_RUNTIME_ARCHIVE_HASH_EXTRA_RESOURCE.from);
2422
2423 yield* stageWslRuntimeArchive({ sourceArchivePath, archivePath, hashPath });
2424
2425 const [source, staged] = yield* Effect.all([
2426 fs.readFile(sourceArchivePath),
2427 fs.readFile(archivePath),
2428 ]);
2429 assert.deepStrictEqual(staged, source);
2430 // The digest both gates installation inside the distro and names the
2431 // extracted runtime's cache directory.
2432 const hash = yield* fs.readFileString(hashPath);
2433 assert.equal(hash, `${NodeCrypto.createHash("sha256").update(source).digest("hex")}\n`);
2434 }),
2435 ),
2436 );
2437
2438 it.effect("fails when the Linux CLI archive handed to the build does not exist", () =>
2439 Effect.scoped(
2440 Effect.gen(function* () {
2441 const fs = yield* FileSystem.FileSystem;
2442 const path = yield* Path.Path;
2443 const root = yield* fs.makeTempDirectoryScoped({ prefix: "t3-wsl-runtime-missing-" });
2444 const error = yield* stageWslRuntimeArchive({
2445 sourceArchivePath: path.join(root, "t3-1.2.3-linux-x64.tar.gz"),
2446 archivePath: path.join(root, WSL_RUNTIME_ARCHIVE_NAME),
2447 hashPath: path.join(root, WSL_RUNTIME_ARCHIVE_HASH_NAME),
2448 }).pipe(Effect.flip);
2449
2450 assert.instanceOf(error, WslRuntimeArchiveMissingError);
2451 }),
2452 ),
2453 );
2454
2455 it("promotes target fff binaries to direct staged dependencies", () => {
2456 assert.deepStrictEqual(resolveFffNativeDependencies("mac", "arm64", "0.9.4"), {
2457 "@ff-labs/fff-bin-darwin-arm64": "0.9.4",
2458 });
2459 assert.deepStrictEqual(resolveFffNativeDependencies("mac", "universal", "0.9.4"), {
2460 "@ff-labs/fff-bin-darwin-arm64": "0.9.4",
2461 "@ff-labs/fff-bin-darwin-x64": "0.9.4",
2462 });
2463 assert.deepStrictEqual(resolveFffNativeDependencies("win", "x64", "0.9.4"), {
2464 "@ff-labs/fff-bin-win32-x64": "0.9.4",
2465 });
2466 assert.deepStrictEqual(resolveFffNativeDependencies("linux", "x64", "0.9.4"), {
2467 "@ff-labs/fff-bin-linux-x64-gnu": "0.9.4",
2468 "@ff-labs/fff-bin-linux-x64-musl": "0.9.4",
2469 });
2470 assert.deepStrictEqual(resolveFffNativeDependencies("linux", "arm64", "0.9.4"), {
2471 "@ff-labs/fff-bin-linux-arm64-gnu": "0.9.4",
2472 "@ff-labs/fff-bin-linux-arm64-musl": "0.9.4",
2473 });
2474 });
2475
2476 it("resolves target Clerk passkey native artifacts", () => {
2477 assert.deepStrictEqual(resolveClerkPasskeyNativeArtifacts("mac", "universal"), [
2478 {
2479 packageName: "@clerk/electron-passkeys-darwin-arm64",
2480 binaryFileName: "electron-passkeys.darwin-arm64.node",
2481 },
2482 {
2483 packageName: "@clerk/electron-passkeys-darwin-x64",
2484 binaryFileName: "electron-passkeys.darwin-x64.node",
2485 },
2486 ]);
2487 assert.deepStrictEqual(resolveClerkPasskeyNativeArtifacts("win", "x64"), [
2488 {
2489 packageName: "@clerk/electron-passkeys-win32-x64-msvc",
2490 binaryFileName: "electron-passkeys.win32-x64-msvc.node",
2491 },
2492 ]);
2493 assert.deepStrictEqual(resolveClerkPasskeyNativeArtifacts("linux", "x64"), []);
2494 });
2495
2496 it("falls back to the default mock update port when the configured port is blank", () => {
2497 assert.equal(resolveMockUpdateServerUrl(undefined), "http://localhost:3000");
2498 assert.equal(resolveMockUpdateServerUrl(4123), "http://localhost:4123");
2499 });
2500
2501 it("derives the electron-builder package manager user agent from packageManager", () => {
2502 assert.equal(resolvePackageManagerUserAgent("pnpm@11.10.0"), "pnpm/11.10.0");
2503 assert.equal(resolvePackageManagerUserAgent(" yarn@4.9.2 "), "yarn/4.9.2");
2504 assert.equal(resolvePackageManagerUserAgent("pnpm"), "pnpm");
2505 });
2506
2507 it.effect("normalizes mock update server ports from env-style strings", () =>
2508 Effect.gen(function* () {
2509 assert.equal(yield* resolveMockUpdateServerPort(undefined), undefined);
2510 assert.equal(yield* resolveMockUpdateServerPort(""), undefined);
2511 assert.equal(yield* resolveMockUpdateServerPort(" "), undefined);
2512 assert.equal(yield* resolveMockUpdateServerPort("4123"), 4123);
2513 }),
2514 );
2515
2516 it.effect("rejects non-numeric or out-of-range mock update ports", () =>
2517 Effect.gen(function* () {
2518 const invalidPorts = ["abc", "12.5", "0", "65536"];
2519 for (const port of invalidPorts) {
2520 const exit = yield* Effect.exit(resolveMockUpdateServerPort(port));
2521 assert.equal(exit._tag, "Failure");
2522 }
2523 }),
2524 );
2525
2526 it("classifies invalid configured ports with the decoder's number grammar", () => {
2527 const cause = new Error("invalid configured port");
2528
2529 assert.equal(
2530 InvalidMockUpdateServerPortError.fromConfigValue("0x10", cause).reason,
2531 "not-numeric",
2532 );
2533 assert.equal(
2534 InvalidMockUpdateServerPortError.fromConfigValue("12.5", cause).reason,
2535 "not-integer",
2536 );
2537 assert.equal(
2538 InvalidMockUpdateServerPortError.fromConfigValue("65536", cause).reason,
2539 "out-of-range",
2540 );
2541 assert.strictEqual(
2542 InvalidMockUpdateServerPortError.fromConfigValue("0x10", cause).cause,
2543 cause,
2544 );
2545 });
2546
2547 it.effect("resolves default platform and architecture from host references", () =>
2548 Effect.gen(function* () {
2549 const resolved = yield* resolveBuildOptions({
2550 platform: Option.none(),
2551 target: Option.none(),
2552 arch: Option.none(),
2553 buildVersion: Option.none(),
2554 outputDir: Option.none(),
2555 skipBuild: Option.none(),
2556 keepStage: Option.none(),
2557 signed: Option.none(),
2558 verbose: Option.none(),
2559 mockUpdates: Option.none(),
2560 mockUpdateServerPort: Option.none(),
2561 wslRuntime: Option.none(),
2562 }).pipe(
2563 Effect.provide(
2564 Layer.mergeAll(
2565 Layer.succeed(HostProcess.Platform, "win32"),
2566 Layer.succeed(HostProcess.Architecture, "x64"),
2567 ConfigProvider.layer(
2568 ConfigProvider.fromEnv({
2569 env: {
2570 PROCESSOR_ARCHITECTURE: "AMD64",
2571 PROCESSOR_ARCHITEW6432: "ARM64",
2572 },
2573 }),
2574 ),
2575 ),
2576 ),
2577 );
2578
2579 assert.equal(resolved.platform, "win");
2580 assert.equal(resolved.target, "nsis");
2581 assert.equal(resolved.arch, "arm64");
2582 }),
2583 );
2584
2585 it.effect("rejects universal builds on Linux and Windows before staging binaries", () =>
2586 Effect.gen(function* () {
2587 for (const platform of ["linux", "win"] as const) {
2588 const error = yield* Effect.flip(
2589 resolveBuildOptions({
2590 platform: Option.some(platform),
2591 target: Option.none(),
2592 arch: Option.some("universal"),
2593 buildVersion: Option.none(),
2594 outputDir: Option.none(),
2595 skipBuild: Option.none(),
2596 keepStage: Option.none(),
2597 signed: Option.none(),
2598 verbose: Option.none(),
2599 mockUpdates: Option.none(),
2600 mockUpdateServerPort: Option.none(),
2601 wslRuntime: Option.none(),
2602 }),
2603 );
2604
2605 assert.instanceOf(error, UnsupportedDesktopBuildArchitectureError);
2606 assert.deepStrictEqual(error.supportedArchitectures, ["x64", "arm64"]);
2607 }
2608 }),
2609 );
2610
2611 it.effect("preserves explicit false boolean flags over true env defaults", () =>
2612 Effect.gen(function* () {
2613 const resolved = yield* resolveBuildOptions({
2614 platform: Option.some("mac"),
2615 target: Option.none(),
2616 arch: Option.some("arm64"),
2617 buildVersion: Option.none(),
2618 outputDir: Option.some("release-test"),
2619 skipBuild: Option.some(false),
2620 keepStage: Option.some(false),
2621 signed: Option.some(false),
2622 verbose: Option.some(false),
2623 mockUpdates: Option.some(false),
2624 mockUpdateServerPort: Option.none(),
2625 wslRuntime: Option.none(),
2626 }).pipe(
2627 Effect.provide(
2628 ConfigProvider.layer(
2629 ConfigProvider.fromEnv({
2630 env: {
2631 T3CODE_DESKTOP_SKIP_BUILD: "true",
2632 T3CODE_DESKTOP_KEEP_STAGE: "true",
2633 T3CODE_DESKTOP_SIGNED: "true",
2634 T3CODE_DESKTOP_VERBOSE: "true",
2635 T3CODE_DESKTOP_MOCK_UPDATES: "true",
2636 },
2637 }),
2638 ),
2639 ),
2640 );
2641
2642 assert.equal(resolved.skipBuild, false);
2643 assert.equal(resolved.keepStage, false);
2644 assert.equal(resolved.signed, false);
2645 assert.equal(resolved.verbose, false);
2646 assert.equal(resolved.mockUpdates, false);
2647 }),
2648 );
2649});
2650
2651// The self-containment check runs the packaged tree in a scratch directory. Its
2652// own node_modules holds the sidecar externals and must be ignored, but any
2653// node_modules *above* it would let Node's parent walk satisfy an import that is
2654// missing from the package, so the probe refuses to run in that case.
2655it("lists ancestor node_modules, nearest first, excluding the start directory", () => {
2656 assert.deepStrictEqual(ancestorNodeModulesPaths("C:\\tmp\\probe\\app", "\\"), [
2657 "C:\\tmp\\probe\\node_modules",
2658 "C:\\tmp\\node_modules",
2659 "C:\\node_modules",
2660 ]);
2661});
2662
2663it("includes the filesystem root for posix paths", () => {
2664 assert.deepStrictEqual(ancestorNodeModulesPaths("/tmp/probe", "/"), [
2665 "/tmp/node_modules",
2666 "/node_modules",
2667 ]);
2668});
2669
2670// A UNC root must keep its \\server\share prefix. Rebuilding it from segments
2671// produced relative paths, which fs.exists resolves against the build cwd, so
2672// the guard checked directories that do not exist and silently passed.
2673it("keeps the prefix of a UNC path instead of going relative", () => {
2674 const paths = ancestorNodeModulesPaths("\\\\server\\share\\tmp\\app", "\\");
2675 for (const candidate of paths) {
2676 assert.ok(candidate.startsWith("\\\\server\\share"), candidate);
2677 }
2678 assert.deepStrictEqual(paths[0], "\\\\server\\share\\tmp\\node_modules");
2679});
2680
2681it.effect.skipIf(!symlinksSupported)("rebases packaged links into the isolated tree", () =>
2682 Effect.gen(function* () {
2683 const fs = yield* FileSystem.FileSystem;
2684 const path = yield* Path.Path;
2685 const root = yield* fs.makeTempDirectoryScoped({ prefix: "t3code-copy-symlinks-" });
2686 const source = path.join(root, "source");
2687 const destination = path.join(root, "destination");
2688 const packageDir = path.join(source, "node_modules/.pnpm/example@1/node_modules/example");
2689 const relativePackageLink = path.join(source, "node_modules/example-relative");
2690 const absolutePackageLink = path.join(source, "node_modules/example-absolute");
2691
2692 yield* fs.makeDirectory(packageDir, { recursive: true });
2693 yield* fs.writeFileString(path.join(packageDir, "index.js"), "module.exports = true;\n");
2694 yield* fs.symlink(
2695 path.join(".pnpm", "example@1", "node_modules", "example"),
2696 relativePackageLink,
2697 );
2698 yield* fs.symlink(packageDir, absolutePackageLink);
2699
2700 yield* copyDirectoryPreservingSymlinks(source, destination);
2701
2702 const copiedPackage = path.join(
2703 destination,
2704 "node_modules/.pnpm/example@1/node_modules/example",
2705 );
2706 const resolvedCopiedPackage = yield* fs.realPath(copiedPackage);
2707 assert.equal(
2708 yield* fs.readLink(path.join(destination, "node_modules/example-relative")),
2709 copiedPackage,
2710 );
2711 assert.equal(
2712 yield* fs.readLink(path.join(destination, "node_modules/example-absolute")),
2713 copiedPackage,
2714 );
2715 assert.equal(
2716 yield* fs.realPath(path.join(destination, "node_modules/example-relative")),
2717 resolvedCopiedPackage,
2718 );
2719 assert.equal(
2720 yield* fs.realPath(path.join(destination, "node_modules/example-absolute")),
2721 resolvedCopiedPackage,
2722 );
2723 }).pipe(Effect.provide(NodeServices.layer)),
2724);
2725
2726it("ignores trailing separators", () => {
2727 assert.deepStrictEqual(
2728 ancestorNodeModulesPaths("C:\\tmp\\probe\\app\\", "\\"),
2729 ancestorNodeModulesPaths("C:\\tmp\\probe\\app", "\\"),
2730 );
2731});