@@ -2,6 +2,7 @@ import { assert, describe, it } from "@effect/vitest";
22import * as Cause from "effect/Cause" ;
33import * as Effect from "effect/Effect" ;
44import * as Layer from "effect/Layer" ;
5+ import * as Ref from "effect/Ref" ;
56import { beforeEach , vi } from "vite-plus/test" ;
67
78const { createClerkBridgeMock, storageAdapter, storageMock } = vi . hoisted ( ( ) => ( {
@@ -22,13 +23,16 @@ vi.mock("@clerk/electron/storage", () => ({
2223 storage : storageMock ,
2324} ) ) ;
2425
26+ import * as ElectronApp from "../electron/ElectronApp.ts" ;
2527import * as DesktopClerk from "./DesktopClerk.ts" ;
28+ import * as DesktopDeepLinks from "./DesktopDeepLinks.ts" ;
2629import * as DesktopEnvironment from "./DesktopEnvironment.ts" ;
2730
28- const makeDesktopClerkLayer = ( isDevelopment = true ) => {
31+ const makeDesktopClerkLayer = ( isDevelopment = true , isPackaged = false ) => {
2932 const environment = DesktopEnvironment . DesktopEnvironment . of ( {
3033 stateDir : "/tmp/t3-state" ,
3134 isDevelopment,
35+ isPackaged,
3236 } as unknown as DesktopEnvironment . DesktopEnvironment [ "Service" ] ) ;
3337
3438 return DesktopClerk . layer . pipe (
@@ -146,4 +150,150 @@ describe("DesktopClerk", () => {
146150 storageMock . mockClear ( ) ;
147151 createClerkBridgeMock . mockClear ( ) ;
148152 } ) ;
153+
154+ it . effect (
155+ "wires second-instance argv into deep links and registers the protocol when packaged" ,
156+ ( ) => {
157+ storageMock . mockReturnValue ( storageAdapter ) ;
158+ createClerkBridgeMock . mockReturnValue ( { cleanup : vi . fn ( ) } ) ;
159+
160+ return Effect . gen ( function * ( ) {
161+ const handledArgv = yield * Ref . make < Array < readonly string [ ] > > ( [ ] ) ;
162+ const handledUrls = yield * Ref . make < string [ ] > ( [ ] ) ;
163+ const listeners = new Map < string , ( ...args : readonly unknown [ ] ) => void > ( ) ;
164+ let protocolClientRegistered = false ;
165+
166+ const deepLinksLayer = Layer . succeed ( DesktopDeepLinks . DesktopDeepLinks , {
167+ handleArgv : ( argv ) =>
168+ Ref . update ( handledArgv , ( items ) => [ ...items , argv ] ) . pipe ( Effect . asVoid ) ,
169+ handleUrl : ( url ) =>
170+ Ref . update ( handledUrls , ( items ) => [ ...items , url ] ) . pipe ( Effect . asVoid ) ,
171+ start : Effect . void ,
172+ } satisfies DesktopDeepLinks . DesktopDeepLinks [ "Service" ] ) ;
173+
174+ const electronAppLayer = Layer . succeed ( ElectronApp . ElectronApp , {
175+ metadata : Effect . die ( "unexpected metadata" ) ,
176+ name : Effect . succeed ( "T3 Code" ) ,
177+ whenReady : Effect . void ,
178+ quit : Effect . void ,
179+ exit : ( ) => Effect . void ,
180+ relaunch : ( ) => Effect . void ,
181+ setPath : ( ) => Effect . void ,
182+ setName : ( ) => Effect . void ,
183+ setAboutPanelOptions : ( ) => Effect . void ,
184+ setAppUserModelId : ( ) => Effect . void ,
185+ requestSingleInstanceLock : Effect . succeed ( true ) ,
186+ isDefaultProtocolClient : ( ) => Effect . succeed ( false ) ,
187+ setAsDefaultProtocolClient : ( protocol : string ) =>
188+ Effect . sync ( ( ) => {
189+ protocolClientRegistered = protocol === "t3code" ;
190+ return true ;
191+ } ) ,
192+ setDesktopName : ( ) => Effect . void ,
193+ setDockIcon : ( ) => Effect . void ,
194+ appendCommandLineSwitch : ( ) => Effect . void ,
195+ on : < Args extends ReadonlyArray < unknown > > (
196+ eventName : string ,
197+ listener : ( ...args : Args ) => void ,
198+ ) =>
199+ Effect . sync ( ( ) => {
200+ listeners . set ( eventName , listener as ( ...args : readonly unknown [ ] ) => void ) ;
201+ } ) . pipe ( Effect . asVoid ) ,
202+ } as unknown as ElectronApp . ElectronApp [ "Service" ] ) ;
203+
204+ const runtimeLayer = Layer . mergeAll (
205+ makeDesktopClerkLayer ( false , true ) ,
206+ deepLinksLayer ,
207+ electronAppLayer ,
208+ ) ;
209+
210+ yield * Effect . scoped (
211+ Effect . gen ( function * ( ) {
212+ const clerk = yield * DesktopClerk . DesktopClerk ;
213+ yield * clerk . configure ;
214+
215+ assert . isTrue ( protocolClientRegistered ) ;
216+ assert . isTrue ( listeners . has ( "second-instance" ) ) ;
217+ assert . isTrue ( listeners . has ( "open-url" ) ) ;
218+
219+ // Initial process.argv is captured during configure.
220+ const initialHandled = yield * Ref . get ( handledArgv ) ;
221+ assert . isTrue ( initialHandled . length >= 1 ) ;
222+
223+ const secondInstance = listeners . get ( "second-instance" ) ;
224+ assert . isDefined ( secondInstance ) ;
225+ secondInstance ?.( { } , [
226+ "t3code" ,
227+ "t3code://open/thread?connection=t3vm&thread=ebf3a84d-7f60-4809-a5e0-bbd574275463" ,
228+ ] ) ;
229+ // Allow the fire-and-forget runPromise callback to settle.
230+ yield * Effect . yieldNow ;
231+ yield * Effect . yieldNow ;
232+
233+ const afterSecond = yield * Ref . get ( handledArgv ) ;
234+ assert . isTrue (
235+ afterSecond . some ( ( argv ) =>
236+ argv . some ( ( entry ) => entry . startsWith ( "t3code://open/thread" ) ) ,
237+ ) ,
238+ ) ;
239+
240+ const openUrl = listeners . get ( "open-url" ) ;
241+ assert . isDefined ( openUrl ) ;
242+ const preventDefault = vi . fn ( ) ;
243+ openUrl ?.(
244+ { preventDefault } ,
245+ "t3code://open/thread?connection=t3vm&thread=aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" ,
246+ ) ;
247+ yield * Effect . yieldNow ;
248+ yield * Effect . yieldNow ;
249+ assert . equal ( preventDefault . mock . calls . length , 1 ) ;
250+ const urls = yield * Ref . get ( handledUrls ) ;
251+ assert . deepEqual ( urls , [
252+ "t3code://open/thread?connection=t3vm&thread=aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" ,
253+ ] ) ;
254+ } ) . pipe ( Effect . provide ( runtimeLayer ) ) ,
255+ ) ;
256+ } ) ;
257+ } ,
258+ ) ;
259+
260+ it . effect ( "does not register the OS protocol client in development" , ( ) => {
261+ storageMock . mockReturnValue ( storageAdapter ) ;
262+ createClerkBridgeMock . mockReturnValue ( { cleanup : vi . fn ( ) } ) ;
263+
264+ return Effect . gen ( function * ( ) {
265+ let protocolClientRegistered = false ;
266+
267+ const deepLinksLayer = Layer . succeed ( DesktopDeepLinks . DesktopDeepLinks , {
268+ handleArgv : ( ) => Effect . void ,
269+ handleUrl : ( ) => Effect . void ,
270+ start : Effect . void ,
271+ } satisfies DesktopDeepLinks . DesktopDeepLinks [ "Service" ] ) ;
272+
273+ const electronAppLayer = Layer . succeed ( ElectronApp . ElectronApp , {
274+ requestSingleInstanceLock : Effect . succeed ( true ) ,
275+ setAsDefaultProtocolClient : ( ) =>
276+ Effect . sync ( ( ) => {
277+ protocolClientRegistered = true ;
278+ return true ;
279+ } ) ,
280+ on : ( ) => Effect . void ,
281+ quit : Effect . void ,
282+ } as unknown as ElectronApp . ElectronApp [ "Service" ] ) ;
283+
284+ const runtimeLayer = Layer . mergeAll (
285+ makeDesktopClerkLayer ( true , false ) ,
286+ deepLinksLayer ,
287+ electronAppLayer ,
288+ ) ;
289+
290+ yield * Effect . scoped (
291+ Effect . gen ( function * ( ) {
292+ const clerk = yield * DesktopClerk . DesktopClerk ;
293+ yield * clerk . configure ;
294+ assert . isFalse ( protocolClientRegistered ) ;
295+ } ) . pipe ( Effect . provide ( runtimeLayer ) ) ,
296+ ) ;
297+ } ) ;
298+ } ) ;
149299} ) ;
0 commit comments