@@ -5,9 +5,51 @@ import type { MessagePortMain, WebContents } from "electron"
55import { Context , Effect , Layer , ManagedRuntime , Option , Queue , Schema , Stream } from "effect"
66import { Rpc , RpcClient , RpcClientError , RpcGroup , RpcMessage , RpcServer } from "effect/unstable/rpc"
77import { Transferable } from "effect/unstable/workers"
8+ import { omitUndefined } from "../shared/ipc-transport"
9+ import { FilesOpenFilePicker } from "../shared/ipc-rpc/files"
810import { IpcPortHandoff , IpcServerProtocolLive } from "./ipc-transport"
911
1012describe ( "desktop RPC transport" , ( ) => {
13+ test ( "decodes renderer payloads whose optional fields are undefined" , async ( ) => {
14+ let received : unknown
15+ const rpcs = RpcGroup . make ( FilesOpenFilePicker )
16+ const handlers = rpcs . toLayer ( {
17+ FilesOpenFilePicker : ( { options } ) =>
18+ Effect . sync ( ( ) => {
19+ received = options
20+ return null
21+ } ) ,
22+ } )
23+ const live = RpcServer . layer ( rpcs ) . pipe ( Layer . provide ( handlers ) , Layer . provideMerge ( IpcServerProtocolLive ) )
24+ const runtime = ManagedRuntime . make ( live )
25+ const handoff = await runtime . runPromise ( IpcPortHandoff )
26+ const channel = new MessageChannel ( )
27+ handoff . bind ( sender ( 1 ) , serverPort ( channel . port1 ) )
28+ // What openAttachmentPickerDialog sends for the composer's attach button.
29+ const payload = { options : { multiple : true , title : undefined , defaultPath : "C:\\project" , extensions : undefined } }
30+
31+ // The renderer posts the wire format itself, so a present-but-undefined key reaches the JSON codec.
32+ const rejected = await rawRequest ( channel . port2 , 0 , payload )
33+ expect ( rejected ) . toMatchObject ( {
34+ _tag : "Exit" ,
35+ exit : { _tag : "Failure" , cause : [ { _tag : "Die" , defect : expect . stringContaining ( '["options"]["title"]' ) } ] } ,
36+ } )
37+
38+ const accepted = await rawRequest ( channel . port2 , 1 , omitUndefined ( payload ) )
39+ expect ( accepted ) . toMatchObject ( { _tag : "Exit" , exit : { _tag : "Success" , value : null } } )
40+ expect ( received ) . toEqual ( { multiple : true , defaultPath : "C:\\project" } )
41+
42+ channel . port2 . close ( )
43+ await runtime . dispose ( )
44+ } )
45+
46+ test ( "omitting undefined fields leaves bytes and defined values alone" , ( ) => {
47+ const data = new Uint8Array ( [ 0 , 255 , 2 ] )
48+ const result = omitUndefined ( { data, nested : [ { keep : null , drop : undefined } ] , count : 0 } ) as { data : Uint8Array }
49+ expect ( result ) . toEqual ( { data, nested : [ { keep : null } ] , count : 0 } )
50+ expect ( result . data ) . toBe ( data )
51+ } )
52+
1153 test ( "keeps multiple renderer ports independent" , async ( ) => {
1254 let received : unknown
1355 const handlers = TestRpcs . toLayer (
@@ -145,6 +187,12 @@ function clientProtocol(port: MessagePort) {
145187 )
146188}
147189
190+ function rawRequest ( port : MessageChannel [ "port2" ] , id : number , payload : unknown ) {
191+ const response = new Promise < RpcMessage . FromServerEncoded > ( ( resolve ) => port . once ( "message" , resolve ) )
192+ port . postMessage ( { _tag : "Request" , id, tag : "FilesOpenFilePicker" , payload, headers : [ ] } )
193+ return response
194+ }
195+
148196function sender ( id : number ) {
149197 const events = new EventEmitter ( )
150198 return {
0 commit comments