@@ -218,109 +218,104 @@ describe('collectStaticRpcDump', () => {
218218 } )
219219
220220 describe ( 'error-bearing records' , ( ) => {
221- it ( 'promotes error-bearing query records to structured-clone for jsonSerializable: true' , async ( ) => {
221+ it ( 'writes JSON-safe error shape (message + name + cause) for jsonSerializable: true' , async ( ) => {
222222 const flaky = defineRpcFunction ( {
223223 name : 'test:flaky' ,
224224 type : 'query' ,
225225 jsonSerializable : true ,
226- handler : ( id : string ) => {
227- if ( id === 'bad' )
228- throw new Error ( 'boom' )
229- return { id }
226+ handler : ( ) => {
227+ throw new TypeError ( 'boom' , { cause : new Error ( 'inner' ) } )
230228 } ,
231229 dump : {
232- inputs : [ [ 'good' ] , [ 'bad' ] ] ,
230+ inputs : [ [ ] ] as [ ] [ ] ,
233231 } ,
234232 } )
235233
236234 const result = await collectStaticRpcDump ( [ flaky ] , { } )
237235 const entry = result . manifest [ 'test:flaky' ] as {
238- type : 'query'
239236 records : Record < string , string >
240237 serialization : 'json'
241- recordSerializations ?: Record < string , 'json' | 'structured-clone' >
242238 }
243239
244240 expect ( entry . serialization ) . toBe ( 'json' )
245- expect ( entry . recordSerializations ) . toBeDefined ( )
246241
247- const erroredKeys = Object . keys ( entry . recordSerializations ! )
248- expect ( erroredKeys ) . toHaveLength ( 1 )
249-
250- const erroredPath = entry . records [ erroredKeys [ 0 ] ! ] !
251- expect ( result . files [ erroredPath ] ! . serialization ) . toBe ( 'structured-clone' )
242+ const recordPath = Object . values ( entry . records ) [ 0 ] !
243+ const file = result . files [ recordPath ] !
244+ expect ( file . serialization ) . toBe ( 'json' )
252245
253- const goodPath = entry . records [ Object . keys ( entry . records ) . find ( k => k !== erroredKeys [ 0 ] ) ! ] !
254- expect ( result . files [ goodPath ] ! . serialization ) . toBe ( 'json' )
246+ // serializeDumpError flattens Error.cause into a plain object, so
247+ // strict-JSON encoding succeeds without any per-record promotion.
248+ const wireText = strictJsonStringify ( file . data , file . fnName )
249+ const parsed = JSON . parse ( wireText ) as {
250+ error : { name : string , message : string , cause : { name : string , message : string } }
251+ }
252+ expect ( parsed . error . name ) . toBe ( 'TypeError' )
253+ expect ( parsed . error . message ) . toBe ( 'boom' )
254+ expect ( parsed . error . cause . name ) . toBe ( 'Error' )
255+ expect ( parsed . error . cause . message ) . toBe ( 'inner' )
255256 } )
256257
257- it ( 'keeps the entry serialization on error-bearing records when default is structured-clone' , async ( ) => {
258+ it ( 'preserves rich error info end-to-end for default (structured-clone) entries' , async ( ) => {
259+ const tags = new Map < string , number > ( [ [ 'a' , 1 ] ] )
258260 const flaky = defineRpcFunction ( {
259- name : 'test:flaky-sc ' ,
261+ name : 'test:flaky-roundtrip ' ,
260262 type : 'query' ,
261263 // default jsonSerializable: false → structured-clone shards
262- handler : ( id : string ) => {
263- if ( id === 'bad' )
264- throw new Error ( 'boom' )
265- return { id }
264+ handler : ( ) => {
265+ const err = new TypeError ( 'boom' , { cause : new Error ( 'inner' ) } ) as Error & { tags ?: unknown }
266+ err . tags = tags
267+ throw err
266268 } ,
267269 dump : {
268- inputs : [ [ 'good' ] , [ 'bad' ] ] ,
270+ inputs : [ [ ] ] as [ ] [ ] ,
269271 } ,
270272 } )
271273
272274 const result = await collectStaticRpcDump ( [ flaky ] , { } )
273- const entry = result . manifest [ 'test:flaky-sc ' ] as {
274- serialization : 'json' | 'structured-clone'
275- recordSerializations ?: Record < string , 'json' | ' structured-clone'>
275+ const entry = result . manifest [ 'test:flaky-roundtrip ' ] as {
276+ records : Record < string , string >
277+ serialization : ' structured-clone'
276278 }
277-
278279 expect ( entry . serialization ) . toBe ( 'structured-clone' )
279- // No overrides — everything is already SC.
280- expect ( entry . recordSerializations ) . toBeUndefined ( )
280+
281+ const recordPath = Object . values ( entry . records ) [ 0 ] !
282+ const file = result . files [ recordPath ] !
283+ const revived = structuredCloneDeserialize ( JSON . parse ( structuredCloneStringify ( file . data ) ) ) as {
284+ error : { name : string , message : string , cause : { message : string } , tags : Map < string , number > }
285+ }
286+ expect ( revived . error . name ) . toBe ( 'TypeError' )
287+ expect ( revived . error . message ) . toBe ( 'boom' )
288+ expect ( revived . error . cause . message ) . toBe ( 'inner' )
289+ expect ( revived . error . tags ) . toBeInstanceOf ( Map )
290+ expect ( revived . error . tags . get ( 'a' ) ) . toBe ( 1 )
281291 } )
282292
283- it ( 'preserves rich error info (cause + custom props) through a full read round-trip' , async ( ) => {
284- const tags = new Map < string , number > ( [ [ 'a' , 1 ] ] )
293+ it ( 'throws DF0020 when a jsonSerializable: true function attaches non-JSON to an error' , async ( ) => {
285294 const flaky = defineRpcFunction ( {
286- name : 'test:flaky-roundtrip ' ,
295+ name : 'test:flaky-non-json ' ,
287296 type : 'query' ,
288297 jsonSerializable : true ,
289- handler : ( id : string ) => {
290- if ( id === 'bad' ) {
291- const err = new TypeError ( 'boom' , { cause : new Error ( 'inner' ) } ) as Error & { tags ?: unknown }
292- err . tags = tags
293- throw err
294- }
295- return { id }
298+ handler : ( ) => {
299+ const err = new Error ( 'boom' ) as Error & { tags ?: unknown }
300+ err . tags = new Map ( [ [ 'a' , 1 ] ] )
301+ throw err
296302 } ,
297303 dump : {
298- inputs : [ [ 'bad' ] ] ,
304+ inputs : [ [ ] ] as [ ] [ ] ,
299305 } ,
300306 } )
301307
302308 const result = await collectStaticRpcDump ( [ flaky ] , { } )
303- const entry = result . manifest [ 'test:flaky-roundtrip' ] as {
304- records : Record < string , string >
305- recordSerializations ?: Record < string , 'json' | 'structured-clone' >
306- }
307-
308- const recordKey = Object . keys ( entry . records ) [ 0 ] !
309- const recordPath = entry . records [ recordKey ] !
309+ const recordPath = Object . values (
310+ ( result . manifest [ 'test:flaky-non-json' ] as { records : Record < string , string > } ) . records ,
311+ ) [ 0 ] !
310312 const file = result . files [ recordPath ] !
311313
312- // The shard was promoted to structured-clone — verify the Error
313- // (and its Map property) round-trips losslessly through the wire format.
314- expect ( file . serialization ) . toBe ( 'structured-clone' )
315- const wireText = structuredCloneStringify ( file . data )
316- const revived = structuredCloneDeserialize ( JSON . parse ( wireText ) ) as {
317- error : { name : string , message : string , cause : { message : string } , tags : Map < string , number > }
318- }
319- expect ( revived . error . name ) . toBe ( 'TypeError' )
320- expect ( revived . error . message ) . toBe ( 'boom' )
321- expect ( revived . error . cause . message ) . toBe ( 'inner' )
322- expect ( revived . error . tags ) . toBeInstanceOf ( Map )
323- expect ( revived . error . tags . get ( 'a' ) ) . toBe ( 1 )
314+ // Attaching a Map to the thrown Error violates the `jsonSerializable: true`
315+ // contract; the strict serializer surfaces this at build time, same as
316+ // if the function had returned a Map.
317+ expect ( ( ) => strictJsonStringify ( file . data , file . fnName ) )
318+ . toThrowError ( / j s o n S e r i a l i z a b l e : t r u e .* i s a M a p / )
324319 } )
325320 } )
326321} )
0 commit comments