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