Skip to content
Source
Sign inSign up
Open History to see the last commit

DesktopClerk.test.ts

485 lines19.2 KB

1// @effect-diagnostics nodeBuiltinImport:off globalFetchInEffect:off - Hosted handoff test uses a real localhost listener without an OpenAI account.
2import * as NodeFS from "node:fs";
3import * as NodeHttp from "node:http";
4import * as NodeOS from "node:os";
5import * as NodePath from "node:path";
6import * as NodePathLayer from "@effect/platform-node/NodePath";
7import { codexAuthHandoffUrl, readCodexAuthDelivery } from "@t3tools/shared/codexAuthHandoff";
8import { EnvironmentId, ProviderInstanceId } from "@t3tools/contracts";
9import * as HostProcess from "@t3tools/shared/HostProcess";
10import { assert, describe, it } from "@effect/vitest";
11import * as Effect from "effect/Effect";
12import * as Layer from "effect/Layer";
13import { afterAll, beforeEach, vi } from "vite-plus/test";
14​
15const clerkStateDir = NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "t3-desktop-clerk-"));
16afterAll(() => {
17 NodeFS.rmSync(clerkStateDir, { recursive: true, force: true });
18});
19​
20const { createClerkBridgeMock, requestSingleInstanceLockMock, storageAdapter, storageMock } =
21 vi.hoisted(() => ({
22 createClerkBridgeMock: vi.fn(),
23 requestSingleInstanceLockMock: vi.fn(() => true),
24 storageAdapter: {
25 getItem: vi.fn(),
26 setItem: vi.fn(),
27 removeItem: vi.fn(),
28 },
29 storageMock: vi.fn(),
30 }));
31​
32vi.mock("@clerk/electron", () => ({
33 createClerkBridge: createClerkBridgeMock,
34}));
35​
36vi.mock("@clerk/electron/storage", () => ({
37 storage: storageMock,
38}));
39import * as Exit from "effect/Exit";
40import * as FileSystem from "effect/FileSystem";
41import * as ElectronApp from "../electron/ElectronApp.ts";
42import * as ElectronShell from "../electron/ElectronShell.ts";
43import * as ElectronWindow from "../electron/ElectronWindow.ts";
44import * as DesktopClerk from "./DesktopClerk.ts";
45import * as DesktopWebLinks from "./DesktopWebLinks.ts";
46import * as DesktopEnvironment from "./DesktopEnvironment.ts";
47import * as DesktopPreReadyFileSystem from "./DesktopPreReadyFileSystem.ts";
48​
49const ignoreWebLinks = DesktopWebLinks.DesktopWebLinks.of({
50 receive: () => Effect.void,
51 setRendererReady: () => Effect.void,
52});
53​
54const layerDesktopClerk = (
55 isDevelopment = true,
56 events: string[] = [],
57 platform: NodeJS.Platform = "darwin",
58 fileSystemLayer: Layer.Layer<FileSystem.FileSystem> = FileSystem.layerNoop({
59 exists: () => Effect.succeed(false),
60 }),
61 shell: ElectronShell.ElectronShell["Service"] = {
62 openExternal: () => Effect.succeed(true),
63 openSystemSettings: () => Effect.succeed(false),
64 copyText: () => Effect.void,
65 },
66 listeners = new Map<string, (...args: unknown[]) => void>(),
67) => {
68 const environment = DesktopEnvironment.DesktopEnvironment.of({
69 stateDir: clerkStateDir,
70 isDevelopment,
71 appDataDirectory: "/tmp/app-data",
72 platform,
73 } as unknown as DesktopEnvironment.DesktopEnvironment["Service"]);
74​
75 const electronApp = {
76 setPath: (name: string, value: string) =>
77 Effect.sync(() => {
78 events.push(`setPath:${name}:${value}`);
79 }),
80 requestSingleInstanceLock: Effect.sync(() => {
81 events.push("requestSingleInstanceLock");
82 return requestSingleInstanceLockMock();
83 }),
84 quit: Effect.sync(() => {
85 events.push("quit");
86 }),
87 on: (name: string, listener: (...args: unknown[]) => void) =>
88 Effect.acquireRelease(
89 Effect.sync(() => {
90 events.push(`on:${name}`);
91 listeners.set(name, listener);
92 }),
93 () =>
94 Effect.sync(() => {
95 listeners.delete(name);
96 }),
97 ).pipe(Effect.asVoid),
98 } as unknown as ElectronApp.ElectronApp["Service"];
99​
100 return DesktopClerk.layer.pipe(
101 Layer.provide(
102 Layer.mergeAll(
103 NodePathLayer.layerPosix,
104 Layer.succeed(DesktopEnvironment.DesktopEnvironment, environment),
105 Layer.succeed(ElectronApp.ElectronApp, electronApp),
106 Layer.succeed(ElectronShell.ElectronShell, shell),
107 fileSystemLayer,
108 ),
109 ),
110 );
111};
112​
113describe("DesktopClerk", () => {
114 beforeEach(() => {
115 createClerkBridgeMock.mockReset();
116 requestSingleInstanceLockMock.mockReset().mockReturnValue(true);
117 storageMock.mockReset();
118 });
119​
120 it.each([
121 {
122 name: "packaged macOS",
123 isDevelopment: false,
124 platform: "darwin" as const,
125 userData: "/tmp/app-data/t3code-v2",
126 },
127 {
128 name: "packaged Linux",
129 isDevelopment: false,
130 platform: "linux" as const,
131 userData: "/tmp/app-data/t3code-v2",
132 },
133 {
134 name: "packaged Windows",
135 isDevelopment: false,
136 platform: "win32" as const,
137 userData: "/tmp/app-data/t3code-v2",
138 },
139 {
140 name: "development",
141 isDevelopment: true,
142 platform: "win32" as const,
143 userData: "/tmp/app-data/t3code-dev",
144 },
145 ])(
146 "creates the bridge before startup can yield to the event loop ($name)",
147 ({ isDevelopment, platform, userData }) => {
148 const events: string[] = [];
149 storageMock.mockReturnValue(storageAdapter);
150 createClerkBridgeMock.mockImplementation(() => {
151 events.push("createClerkBridge");
152 return { cleanup: vi.fn(), isPrimaryInstance: true };
153 });
154 // oxlint-disable-next-line t3code/no-manual-effect-runtime-in-tests -- The assertion IS that the layer builds synchronously; it.effect would mask a regression to async.
155 Effect.runSync(
156 Effect.scoped(
157 Layer.build(
158 layerDesktopClerk(isDevelopment, events, platform, DesktopPreReadyFileSystem.layer),
159 ),
160 ),
161 );
162​
163 assert.deepEqual(events, [
164 `setPath:userData:${userData}`,
165 "requestSingleInstanceLock",
166 "on:second-instance",
167 "on:open-url",
168 "createClerkBridge",
169 ]);
170 assert.deepEqual(storageMock.mock.calls, [
171 [{ path: NodePath.join(NodePath.resolve(clerkStateDir), "clerk") }],
172 ]);
173 assert.notStrictEqual(createClerkBridgeMock.mock.calls[0]?.[0]?.storage, storageAdapter);
174 },
175 );
176​
177 it.effect.each(["before configure", "after configure"] as const)(
178 "reveals the running window when a second instance launches %s",
179 (entry) => {
180 storageMock.mockReturnValue(storageAdapter);
181 createClerkBridgeMock.mockReturnValue({ cleanup: vi.fn(), isPrimaryInstance: true });
182 const quit = vi.fn();
183 const listeners = new Map<string, (...args: unknown[]) => void>();
184 const revealed = Promise.withResolvers<unknown>();
185 const window = { id: 1 };
186 const electronApp = {
187 quit: Effect.sync(quit),
188 on: (eventName: string, listener: (...args: unknown[]) => void) =>
189 Effect.sync(() => {
190 listeners.set(eventName, listener);
191 }),
192 } as unknown as ElectronApp.ElectronApp["Service"];
193 const electronWindow = {
194 currentMainOrFirst: Effect.succeedSome(window),
195 reveal: (value: unknown) => Effect.sync(() => revealed.resolve(value)),
196 } as unknown as ElectronWindow.ElectronWindow["Service"];
197​
198 return Effect.gen(function* () {
199 const clerk = yield* DesktopClerk.DesktopClerk;
200 if (entry === "before configure") listeners.get("second-instance")?.({}, ["t3"]);
201 yield* clerk.configure;
202​
203 assert.equal(quit.mock.calls.length, 0);
204 if (entry === "after configure") listeners.get("second-instance")!({}, ["t3"]);
205 assert.strictEqual(yield* Effect.promise(() => revealed.promise), window);
206 }).pipe(
207 Effect.scoped,
208 Effect.provide(layerDesktopClerk(true, [], "darwin", undefined, undefined, listeners)),
209 Effect.provideService(ElectronApp.ElectronApp, electronApp),
210 Effect.provideService(ElectronWindow.ElectronWindow, electronWindow),
211 Effect.provideService(DesktopWebLinks.DesktopWebLinks, ignoreWebLinks),
212 );
213 },
214 );
215​
216 it.effect("delivers each queued startup link once, before later links", () => {
217 storageMock.mockReturnValue(storageAdapter);
218 createClerkBridgeMock.mockReturnValue({ cleanup: vi.fn(), isPrimaryInstance: true });
219 const listeners = new Map<string, (...args: unknown[]) => void>();
220 const received: string[] = [];
221 const first = "t3code://new?prompt=first";
222 const second = "t3code://threads/env-1/thread-1";
223 const later = "t3code://new?prompt=later";
224 const launchPair = "t3code://pair?pairingUrl=https%3A%2F%2Fvps.example.com%2F%23token%3Dabc";
225 const delivered = Promise.withResolvers<void>();
226 const electronApp = {
227 on: (name: string, listener: (...args: unknown[]) => void) =>
228 Effect.sync(() => {
229 listeners.set(name, listener);
230 }),
231 } as unknown as ElectronApp.ElectronApp["Service"];
232​
233 return Effect.gen(function* () {
234 const clerk = yield* DesktopClerk.DesktopClerk;
235 listeners.get("second-instance")?.({}, ["t3", first]);
236 listeners.get("second-instance")?.({}, ["t3", second]);
237 listeners.get("open-url")?.({ preventDefault: () => undefined }, launchPair);
238 assert.deepEqual(received, []);
239​
240 yield* clerk.configure;
241 listeners.get("second-instance")!({}, ["t3", later]);
242 yield* Effect.promise(() => delivered.promise);
243​
244 assert.deepEqual(received, [launchPair, first, second, later]);
245 }).pipe(
246 Effect.scoped,
247 Effect.provide(layerDesktopClerk(true, [], "darwin", undefined, undefined, listeners)),
248 Effect.provideService(ElectronApp.ElectronApp, electronApp),
249 Effect.provideService(
250 ElectronWindow.ElectronWindow,
251 {} as ElectronWindow.ElectronWindow["Service"],
252 ),
253 Effect.provideService(
254 DesktopWebLinks.DesktopWebLinks,
255 DesktopWebLinks.DesktopWebLinks.of({
256 receive: (url) =>
257 Effect.sync(() => {
258 received.push(url);
259 if (url === later) delivered.resolve();
260 }),
261 setRendererReady: () => Effect.void,
262 }),
263 ),
264 );
265 });
266​
267 it.effect.each(["darwin", "win32", "linux"] as const)(
268 "quits a second launch before initializing Clerk or the application runtime on %s",
269 (platform) =>
270 Effect.gen(function* () {
271 requestSingleInstanceLockMock.mockReturnValueOnce(false);
272 createClerkBridgeMock.mockReturnValue({ cleanup: vi.fn(), isPrimaryInstance: true });
273 const events: string[] = [];
274 const runtime = layerDesktopClerk(false, events, platform).pipe(
275 Layer.flatMap(() =>
276 Layer.effectDiscard(
277 Effect.sync(() => {
278 events.push("initializeApplicationRuntime");
279 }),
280 ),
281 ),
282 );
283 const exit = yield* Effect.exit(Effect.scoped(Layer.build(runtime)));
284​
285 assert.isTrue(Exit.hasInterrupts(exit));
286 assert.deepEqual(events, [
287 "setPath:userData:/tmp/app-data/t3code-v2",
288 "requestSingleInstanceLock",
289 "quit",
290 ]);
291 assert.equal(createClerkBridgeMock.mock.calls.length, 0);
292 assert.equal(storageMock.mock.calls.length, 0);
293 }),
294 );
295});
296​
297it.effect(
298 "provider auth deep links navigate and reveal the running desktop without handling Clerk URLs",
299 () => {
300 storageMock.mockReturnValue(storageAdapter);
301 createClerkBridgeMock.mockReturnValue({ cleanup: vi.fn(), isPrimaryInstance: true });
302 const listeners = new Map<string, (...args: unknown[]) => void>();
303 const revealed = Promise.withResolvers<void>();
304 const loadURL = vi.fn(async (_url: string) => undefined);
305 const window = { loadURL };
306 const electronApp = {
307 on: (name: string, listener: (...args: unknown[]) => void) =>
308 Effect.sync(() => {
309 listeners.set(name, listener);
310 }),
311 } as unknown as ElectronApp.ElectronApp["Service"];
312 const electronWindow = {
313 currentMainOrFirst: Effect.succeedSome(window),
314 reveal: () => Effect.sync(() => revealed.resolve()),
315 } as unknown as ElectronWindow.ElectronWindow["Service"];
316 return Effect.gen(function* () {
317 const clerk = yield* DesktopClerk.DesktopClerk;
318 yield* clerk.configure;
319 const event = { preventDefault: vi.fn() };
320 listeners.get("open-url")!(event, "t3code-dev://app/auth/callback?code=clerk-code");
321 listeners.get("open-url")!(event, "t3code://app/welcome");
322 assert.equal(loadURL.mock.calls.length, 0);
323 assert.equal(event.preventDefault.mock.calls.length, 0);
324 listeners.get("second-instance")!({}, [
325 "t3",
326 "t3code-dev://app/settings/providers?instanceId=work&code=never-forward",
327 ]);
328 yield* Effect.promise(() => revealed.promise);
329 assert.deepEqual(loadURL.mock.calls, [
330 ["t3code-dev://app/settings/providers?instanceId=work"],
331 ]);
332 listeners.get("open-url")!(event, "t3code-dev://app/welcome#agents:machine-id");
333 assert.equal(event.preventDefault.mock.calls.length, 1);
334 }).pipe(
335 Effect.scoped,
336 Effect.provide(layerDesktopClerk(true, [], "darwin", undefined, undefined, listeners)),
337 Effect.provideService(ElectronApp.ElectronApp, electronApp),
338 Effect.provideService(ElectronWindow.ElectronWindow, electronWindow),
339 Effect.provideService(DesktopWebLinks.DesktopWebLinks, ignoreWebLinks),
340 );
341 },
342);
343​
344it.effect.each(["startup", "open-url", "early-second-instance"] as const)(
345 "receives hosted web sign-in through the desktop %s handler",
346 (entry) =>
347 Effect.gen(function* () {
348 storageMock.mockReturnValue(storageAdapter);
349 createClerkBridgeMock.mockReturnValue({ cleanup: vi.fn(), isPrimaryInstance: true });
350 const port = yield* Effect.promise(async () => {
351 const server = NodeHttp.createServer();
352 await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
353 const address = server.address();
354 if (!address || typeof address === "string") throw new Error("address");
355 await new Promise<void>((resolve) => server.close(() => resolve()));
356 return address.port;
357 });
358 const authorize = new URL("https://auth.openai.com/api/accounts/authorize");
359 authorize.search = new URLSearchParams({
360 client_id: "dynamic_agent_client",
361 response_type: "code",
362 redirect_uri: `http://127.0.0.1:${port}/auth/callback`,
363 state: "a".repeat(43),
364 code_challenge_method: "S256",
365 code_challenge: "b".repeat(43),
366 }).toString();
367 const request = {
368 authorizationUrl: authorize.toString(),
369 returnUrl: "https://app.t3.codes/welcome#agents:remote-one",
370 environmentId: EnvironmentId.make("remote-one"),
371 instanceId: ProviderInstanceId.make("work"),
372 flowId: "flow-one",
373 };
374 const link = codexAuthHandoffUrl(request, true);
375 const delivered = Promise.withResolvers<string>();
376 const shell = ElectronShell.ElectronShell.of({
377 openExternal: (value) =>
378 Effect.promise(async () => {
379 const url = new URL(String(value));
380 const callback = new URL(url.searchParams.get("redirect_uri")!);
381 callback.search = new URLSearchParams({
382 state: url.searchParams.get("state")!,
383 code: "test-code",
384 client_id: "oaiapp_test",
385 }).toString();
386 const response = await fetch(callback, { redirect: "manual" });
387 delivered.resolve(response.headers.get("location")!);
388 return true;
389 }),
390 openSystemSettings: () => Effect.succeed(false),
391 copyText: () => Effect.void,
392 });
393 const listeners = new Map<string, (...args: unknown[]) => void>();
394 const electronApp = {
395 whenReady: Effect.void,
396 on: (name: string, listener: (...args: unknown[]) => void) =>
397 Effect.sync(() => {
398 listeners.set(name, listener);
399 }),
400 } as unknown as ElectronApp.ElectronApp["Service"];
401 yield* Effect.gen(function* () {
402 const clerk = yield* DesktopClerk.DesktopClerk;
403 if (entry === "early-second-instance") {
404 listeners.get("second-instance")?.({}, ["t3", link]);
405 }
406 yield* clerk.configure;
407 if (entry === "open-url") {
408 const event = { preventDefault: vi.fn() };
409 listeners.get("open-url")!(event, link);
410 assert.strictEqual(event.preventDefault.mock.calls.length, 1);
411 }
412 const delivery = readCodexAuthDelivery(yield* Effect.promise(() => delivered.promise));
413 assert.strictEqual(delivery?.environmentId, request.environmentId);
414 assert.strictEqual(delivery?.instanceId, request.instanceId);
415 assert.strictEqual(delivery?.flowId, request.flowId);
416 assert.strictEqual(delivery?.returnUrl, request.returnUrl);
417 }).pipe(
418 Effect.provide(layerDesktopClerk(true, [], "darwin", undefined, shell, listeners)),
419 Effect.provideService(HostProcess.Arguments, entry === "startup" ? ["t3", link] : ["t3"]),
420 Effect.provideService(ElectronApp.ElectronApp, electronApp),
421 Effect.provideService(
422 ElectronWindow.ElectronWindow,
423 {} as ElectronWindow.ElectronWindow["Service"],
424 ),
425 Effect.provideService(DesktopWebLinks.DesktopWebLinks, ignoreWebLinks),
426 );
427 }),
428);
429​
430it.effect("hands web and app links to the renderer and leaves other links alone", () =>
431 Effect.gen(function* () {
432 storageMock.mockReturnValue(storageAdapter);
433 createClerkBridgeMock.mockReturnValue({ cleanup: vi.fn(), isPrimaryInstance: true });
434 const listeners = new Map<string, (...args: unknown[]) => void>();
435 const electronApp = {
436 whenReady: Effect.void,
437 on: (name: string, listener: (...args: unknown[]) => void) =>
438 Effect.sync(() => {
439 listeners.set(name, listener);
440 }),
441 } as unknown as ElectronApp.ElectronApp["Service"];
442 const received: Array<string> = [];
443 const pairLink = "t3code://pair?pairingUrl=https%3A%2F%2Fvps.example.com%2F%23token%3Dabc";
444 yield* Effect.gen(function* () {
445 const clerk = yield* DesktopClerk.DesktopClerk;
446 yield* clerk.configure;
447 const open = (url: string) => {
448 const event = { preventDefault: vi.fn() };
449 listeners.get("open-url")!(event, url);
450 return event.preventDefault.mock.calls.length;
451 };
452 assert.strictEqual(open("https://example.com/page"), 1);
453 assert.strictEqual(open("http://localhost:3000/"), 1);
454 assert.strictEqual(open("t3code://new?prompt=hi"), 1);
455 assert.strictEqual(open("t3code://threads/env-1/thread-1"), 1);
456 assert.strictEqual(open(pairLink), 1);
457 assert.strictEqual(open("t3code://pair"), 0);
458 assert.strictEqual(open("mailto:hello@example.com"), 0);
459 assert.strictEqual(open("t3code://somewhere-else"), 0);
460 yield* Effect.yieldNow;
461 assert.deepStrictEqual(received, [
462 "https://example.com/page",
463 "http://localhost:3000/",
464 "t3code://new?prompt=hi",
465 "t3code://threads/env-1/thread-1",
466 pairLink,
467 ]);
468 }).pipe(
469 Effect.provide(layerDesktopClerk(true, [], "darwin", undefined, undefined, listeners)),
470 Effect.provideService(HostProcess.Arguments, ["t3"]),
471 Effect.provideService(ElectronApp.ElectronApp, electronApp),
472 Effect.provideService(
473 ElectronWindow.ElectronWindow,
474 {} as ElectronWindow.ElectronWindow["Service"],
475 ),
476 Effect.provideService(
477 DesktopWebLinks.DesktopWebLinks,
478 DesktopWebLinks.DesktopWebLinks.of({
479 receive: (url) => Effect.sync(() => void received.push(url)),
480 setRendererReady: () => Effect.void,
481 }),
482 ),
483 );
484 }),
485);