1// @effect-diagnostics nodeBuiltinImport:off
2
3import { it } from "@effect/vitest";
4import * as NodeTls from "node:tls";
5import * as NodeCrypto from "node:crypto";
6import * as NodeServices from "@effect/platform-node/NodeServices";
7import * as Effect from "effect/Effect";
8import * as Cause from "effect/Cause";
9import * as FileSystem from "effect/FileSystem";
10import * as Logger from "effect/Logger";
11import * as Path from "effect/Path";
12import * as Stream from "effect/Stream";
13import { ChildProcess, ChildProcessSpawner } from "effect/process";
14import { describe, expect } from "vite-plus/test";
15
16import {
17 makeApplyDesktopShellEnvironmentSnapshot,
18 requireDesktopTelemetryWriteProgress,
19} from "./DesktopTelemetryReceiver.ts";
20
21describe("DesktopTelemetryReceiver", () => {
22 it.live(
23 "preserves system and bundled roots when the first snapshot withdraws overlapping extras",
24 () =>
25 Effect.gen(function* () {
26 const fs = yield* FileSystem.FileSystem;
27 const path = yield* Path.Path;
28 const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
29 const directory = yield* fs.makeTempDirectoryScoped();
30 const certificatePath = path.join(directory, "startup-extra-ca.pem");
31 yield* fs.writeFileString(
32 certificatePath,
33 [...NodeTls.getCACertificates("system"), ...NodeTls.rootCertificates].join("\n"),
34 );
35 const child = yield* spawner.spawn(
36 ChildProcess.make(
37 process.execPath,
38 [
39 "--use-bundled-ca",
40 "--use-system-ca",
41 "--input-type=module",
42 "--eval",
43 `
44import * as NodeAssert from "node:assert/strict";
45import * as NodeCrypto from "node:crypto";
46import * as NodeTls from "node:tls";
47import * as Effect from "effect/Effect";
48import * as NodeServices from "@effect/platform-node/NodeServices";
49import { makeApplyDesktopShellEnvironmentSnapshot } from ${JSON.stringify(new URL("./DesktopTelemetryReceiver.ts", import.meta.url).href)};
50const fingerprints = (certificates) => [...new Set(certificates.map((pem) => new NodeCrypto.X509Certificate(pem).fingerprint256))].sort();
51const expected = fingerprints([...NodeTls.getCACertificates("bundled"), ...NodeTls.getCACertificates("system")]);
52const applySnapshot = makeApplyDesktopShellEnvironmentSnapshot(process.env, process.platform);
53await Effect.runPromise(applySnapshot({
54 version: 1,
55 type: "shellEnvironment",
56 platform: process.platform,
57 discovered: true,
58 environment: { NODE_EXTRA_CA_CERTS: null },
59}).pipe(Effect.provide(NodeServices.layer)));
60NodeAssert.equal(process.env.NODE_EXTRA_CA_CERTS, undefined);
61NodeAssert.deepEqual(fingerprints(NodeTls.getCACertificates("default")), expected);
62`,
63 ],
64 {
65 cwd: path.resolve(import.meta.dirname, "../.."),
66 env: { NODE_OPTIONS: "", NODE_EXTRA_CA_CERTS: certificatePath },
67 },
68 ),
69 );
70 const result = yield* Effect.all(
71 {
72 stderr: child.stderr.pipe(Stream.decodeText(), Stream.mkString),
73 exitCode: child.exitCode.pipe(Effect.map(Number)),
74 },
75 { concurrency: "unbounded" },
76 );
77 expect(result.exitCode, result.stderr).toBe(0);
78 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
79 );
80
81 it.effect("updates live TLS trust when shell certificates arrive after startup", () =>
82 Effect.gen(function* () {
83 const fs = yield* FileSystem.FileSystem;
84 const path = yield* Path.Path;
85 const directory = yield* fs.makeTempDirectoryScoped();
86 const certificatePath = path.join(directory, "extra-ca.pem");
87 const original = NodeTls.getCACertificates("default");
88 yield* Effect.addFinalizer(() =>
89 Effect.sync(() => NodeTls.setDefaultCACertificates(original)),
90 );
91 const baseline = NodeTls.rootCertificates[0]!;
92 const extra = NodeTls.rootCertificates[1]!;
93 const replacement = NodeTls.rootCertificates[2]!;
94 NodeTls.setDefaultCACertificates([baseline]);
95 yield* fs.writeFileString(certificatePath, `${extra}\n${replacement}`);
96 const environment: NodeJS.ProcessEnv = { T3CODE_DESKTOP_SHELL_DISCOVERY_PENDING: "1" };
97 const applySnapshot = makeApplyDesktopShellEnvironmentSnapshot(environment, "darwin");
98 const message = {
99 version: 1,
100 type: "shellEnvironment",
101 platform: "darwin",
102 discovered: true,
103 environment: { NODE_EXTRA_CA_CERTS: certificatePath },
104 } as const;
105 const fingerprints = (certificates: ReadonlyArray<string>) =>
106 certificates.map((pem) => new NodeCrypto.X509Certificate(pem).fingerprint256).sort();
107 yield* applySnapshot({ ...message, platform: "win32" });
108 expect(fingerprints(NodeTls.getCACertificates("default"))).toEqual(fingerprints([baseline]));
109 expect(environment.T3CODE_DESKTOP_SHELL_DISCOVERY_PENDING).toBe("1");
110 yield* applySnapshot(message);
111 expect(fingerprints(NodeTls.getCACertificates("default"))).toEqual(
112 fingerprints([baseline, extra, replacement]),
113 );
114 expect(environment.NODE_EXTRA_CA_CERTS).toBe(certificatePath);
115 expect(environment.T3CODE_DESKTOP_SHELL_DISCOVERY_PENDING).toBeUndefined();
116
117 yield* fs.writeFileString(certificatePath, replacement);
118 yield* applySnapshot(message);
119 expect(fingerprints(NodeTls.getCACertificates("default"))).toEqual(
120 fingerprints([baseline, replacement]),
121 );
122
123 yield* fs.writeFileString(
124 certificatePath,
125 "-----BEGIN CERTIFICATE-----\ninvalid\n-----END CERTIFICATE-----",
126 );
127 yield* applySnapshot(message);
128 expect(fingerprints(NodeTls.getCACertificates("default"))).toEqual(
129 fingerprints([baseline, replacement]),
130 );
131 yield* fs.remove(certificatePath);
132 const warnings: Array<Logger.Options<unknown>> = [];
133 yield* applySnapshot(message).pipe(
134 Effect.provide(
135 Logger.layer([Logger.make((entry) => warnings.push(entry))], {
136 mergeWithExisting: false,
137 }),
138 ),
139 );
140 expect(warnings).toHaveLength(1);
141 expect(warnings[0]!.message).toEqual(["Failed to update desktop shell TLS certificates."]);
142 expect(Cause.squash(warnings[0]!.cause)).toMatchObject({ _tag: "PlatformError" });
143 expect(fingerprints(NodeTls.getCACertificates("default"))).toEqual(
144 fingerprints([baseline, replacement]),
145 );
146
147 yield* applySnapshot({ ...message, environment: { NODE_EXTRA_CA_CERTS: null } });
148 expect(fingerprints(NodeTls.getCACertificates("default"))).toEqual(fingerprints([baseline]));
149 expect(environment.NODE_EXTRA_CA_CERTS).toBeUndefined();
150
151 NodeTls.setDefaultCACertificates([]);
152 yield* fs.writeFileString(certificatePath, extra);
153 const applyOpaqueTrust = makeApplyDesktopShellEnvironmentSnapshot(environment, "darwin");
154 yield* applyOpaqueTrust(message);
155 expect(NodeTls.getCACertificates("default")).toEqual([]);
156 expect(environment.NODE_EXTRA_CA_CERTS).toBe(certificatePath);
157 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
158 );
159
160 it.effect.each([
161 { platform: "darwin", discovered: true, applied: true, pending: false },
162 { platform: "win32", discovered: true, applied: false, pending: true },
163 { platform: "darwin", discovered: false, applied: true, pending: true },
164 ])(
165 "applies platform=$platform discovered=$discovered with pending=$pending",
166 ({ platform, discovered, applied, pending }) =>
167 Effect.gen(function* () {
168 const environment: NodeJS.ProcessEnv = {
169 PATH: "/old/bin",
170 JAVA_HOME: "/old/java",
171 UNRELATED: "keep",
172 T3CODE_DESKTOP_SHELL_DISCOVERY_PENDING: "1",
173 };
174 yield* makeApplyDesktopShellEnvironmentSnapshot(
175 environment,
176 "darwin",
177 )({
178 version: 1,
179 type: "shellEnvironment",
180 platform,
181 discovered,
182 environment: { PATH: "/fresh/bin", JAVA_HOME: null },
183 });
184 expect(environment).toEqual({
185 PATH: applied ? "/fresh/bin" : "/old/bin",
186 ...(applied ? {} : { JAVA_HOME: "/old/java" }),
187 UNRELATED: "keep",
188 ...(pending ? { T3CODE_DESKTOP_SHELL_DISCOVERY_PENDING: "1" } : {}),
189 });
190 }).pipe(Effect.provide(NodeServices.layer)),
191 );
192
193 it.effect("models a zero-byte control write as a stalled descriptor", () =>
194 Effect.gen(function* () {
195 const error = yield* requireDesktopTelemetryWriteProgress(7, 42, 0).pipe(Effect.flip);
196
197 expect(error._tag).toBe("DesktopTelemetryControlStalled");
198 expect(error.fd).toBe(7);
199 expect(error.remainingBytes).toBe(42);
200 expect(error.message).toBe(
201 "Desktop telemetry control stalled on fd 7 with 42 bytes remaining.",
202 );
203 yield* requireDesktopTelemetryWriteProgress(7, 42, 1);
204 }),
205 );
206});