| 1 | // @effect-diagnostics nodeBuiltinImport:off globalFetchInEffect:off - Hosted handoff test uses a real localhost listener without an OpenAI account. |
| 2 | import * as NodeHttp from "node:http"; |
| 3 | import * as NodePath from "@effect/platform-node/NodePath"; |
| 4 | import { codexAuthHandoffUrl, readCodexAuthDelivery } from "@t3tools/shared/codexAuthHandoff"; |
| 5 | import { EnvironmentId, ProviderInstanceId } from "@t3tools/contracts"; |
| 6 | import * as HostProcess from "@t3tools/shared/HostProcess"; |
| 7 | import { assert, describe, it } from "@effect/vitest"; |
| 8 | import * as Cause from "effect/Cause"; |
| 9 | import * as Effect from "effect/Effect"; |
| 10 | import * as Layer from "effect/Layer"; |
| 11 | import { beforeEach, vi } from "vite-plus/test"; |
| 12 | |
| 13 | const { 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 | |
| 23 | vi.mock("@clerk/electron", () => ({ |
| 24 | createClerkBridge: createClerkBridgeMock, |
| 25 | })); |
| 26 | |
| 27 | vi.mock("@clerk/electron/storage", () => ({ |
| 28 | storage: storageMock, |
| 29 | })); |
| 30 | |
| 31 | import * as Option from "effect/Option"; |
| 32 | import * as Exit from "effect/Exit"; |
| 33 | import * as FileSystem from "effect/FileSystem"; |
| 34 | import * as ElectronApp from "../electron/ElectronApp.ts"; |
| 35 | import * as ElectronShell from "../electron/ElectronShell.ts"; |
| 36 | import * as ElectronWindow from "../electron/ElectronWindow.ts"; |
| 37 | import * as DesktopClerk from "./DesktopClerk.ts"; |
| 38 | import * as DesktopWebLinks from "./DesktopWebLinks.ts"; |
| 39 | import * as DesktopEnvironment from "./DesktopEnvironment.ts"; |
| 40 | import * as DesktopPreReadyFileSystem from "./DesktopPreReadyFileSystem.ts"; |
| 41 | |
| 42 | /** Clerk forwards web links; tests that are not about links ignore them. */ |
| 43 | const ignoreWebLinks = DesktopWebLinks.DesktopWebLinks.of({ |
| 44 | receive: () => Effect.void, |
| 45 | setRendererReady: () => Effect.void, |
| 46 | }); |
| 47 | |
| 48 | const 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 | |
| 88 | describe("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 | |
| 269 | it.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 | |
| 316 | it.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 | |
| 399 | it.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 | ); |