@@ -427,3 +427,309 @@ func TestWithTag(t *testing.T) {
427427 e := New ("inv" , "author" , WithTag ("alpha" ), WithTag ("beta" ))
428428 require .Equal (t , "alpha" + TagDelimiter + "beta" , e .Tag )
429429}
430+
431+ // Test that MarshalJSON outputs a payload that preserves the top-level event
432+ // fields and also includes a nested "response" object carrying Response-only
433+ // identifiers like response.id.
434+ func TestEventMarshalJSON_IncludesNestedResponse (t * testing.T ) {
435+ e := & Event {
436+ Response : & model.Response {
437+ ID : "resp-1" ,
438+ Object : model .ObjectTypeChatCompletion ,
439+ Done : true ,
440+ Choices : []model.Choice {{Index : 0 , Message : model .NewAssistantMessage ("hi" )}},
441+ Timestamp : time .Now (),
442+ },
443+ ID : "evt-1" ,
444+ InvocationID : "inv-1" ,
445+ Author : "assistant" ,
446+ Timestamp : time .Now (),
447+ }
448+
449+ data , err := json .Marshal (e )
450+ require .NoError (t , err )
451+
452+ // Decode to a raw map for inspection.
453+ var raw map [string ]json.RawMessage
454+ require .NoError (t , json .Unmarshal (data , & raw ))
455+
456+ // Top-level id should be the event ID.
457+ var topID string
458+ require .NoError (t , json .Unmarshal (raw ["id" ], & topID ))
459+ require .Equal (t , "evt-1" , topID )
460+
461+ // Top-level object should remain available for legacy (flattened) readers.
462+ var topObject string
463+ require .NoError (t , json .Unmarshal (raw ["object" ], & topObject ))
464+ require .Equal (t , string (model .ObjectTypeChatCompletion ), topObject )
465+
466+ // Nested response must exist and preserve response.id.
467+ nested , ok := raw ["response" ]
468+ require .True (t , ok , "missing nested response field" )
469+
470+ var rsp model.Response
471+ require .NoError (t , json .Unmarshal (nested , & rsp ))
472+ require .Equal (t , "resp-1" , rsp .ID )
473+ require .Equal (t , "" , rsp .Object )
474+ }
475+
476+ // Test that UnmarshalJSON prefers nested response over flattened fields when both
477+ // are present in the input JSON.
478+ func TestEventUnmarshalJSON_PrefersNestedResponse (t * testing.T ) {
479+ input := `{
480+ "id": "evt-2",
481+ "object": "chat.completion",
482+ "done": true,
483+ "response": {
484+ "id": "resp-2",
485+ "object": "chat.completion",
486+ "done": true
487+ }
488+ }`
489+
490+ var e Event
491+ require .NoError (t , json .Unmarshal ([]byte (input ), & e ))
492+ require .Equal (t , "evt-2" , e .ID )
493+ require .NotNil (t , e .Response )
494+ require .Equal (t , "resp-2" , e .Response .ID )
495+ require .Equal (t , model .ObjectTypeChatCompletion , e .Response .Object )
496+ require .True (t , e .Response .Done )
497+ }
498+
499+ // Test that legacy flattened JSON without nested response decodes successfully and
500+ // populates Response fields except the conflicting response.id.
501+ func TestEventUnmarshalJSON_LegacyFlatOnly (t * testing.T ) {
502+ // Simulate older payload where response fields live on the top-level due to embedding,
503+ // thus there is no nested "response" and no way to carry response.id.
504+ input := `{
505+ "id": "evt-3",
506+ "object": "chat.completion",
507+ "done": true,
508+ "choices": [{"index":0, "message": {"role":"assistant", "content":"ok"}}]
509+ }`
510+
511+ var e Event
512+ require .NoError (t , json .Unmarshal ([]byte (input ), & e ))
513+ require .Equal (t , "evt-3" , e .ID )
514+ require .NotNil (t , e .Response )
515+ require .Equal (t , "" , e .Response .ID ) // No response.id in legacy flat payload.
516+ require .Equal (t , model .ObjectTypeChatCompletion , e .Response .Object )
517+ require .True (t , e .Response .Done )
518+ require .Len (t , e .Response .Choices , 1 )
519+ require .Equal (t , 0 , e .Response .Choices [0 ].Index )
520+ require .Equal (t , model .RoleAssistant , e .Response .Choices [0 ].Message .Role )
521+ require .Equal (t , "ok" , e .Response .Choices [0 ].Message .Content )
522+ }
523+
524+ // Test marshalJSON on a nil *Event should return an error due to
525+ // attempting to unmarshal a JSON null into the payload map.
526+ func TestEventMarshalJSON_NilReceiver_Error (t * testing.T ) {
527+ var e * Event
528+ data , err := json .Marshal (e )
529+ require .NoError (t , err )
530+ require .Equal (t , "null" , string (data ))
531+ }
532+
533+ // Test unmarshalJSON on a nil pointer should return an error.
534+ func TestEventUnmarshalJSON_NilPointer (t * testing.T ) {
535+ var e * Event
536+ err := json .Unmarshal ([]byte ("null" ), e )
537+ require .Error (t , err )
538+ }
539+
540+ // Test unmarshalJSON on a null value should return an error.
541+ func TestEventUnmarshalJSON_NullValue (t * testing.T ) {
542+ var e Event
543+ err := json .Unmarshal ([]byte ("null" ), & e )
544+ require .NoError (t , err )
545+ require .Equal (t , Event {}, e )
546+ }
547+
548+ // Test unmarshalJSON should return error on invalid JSON input.
549+ func TestEventUnmarshalJSON_InvalidJSON_Error (t * testing.T ) {
550+ var e Event
551+ err := json .Unmarshal ([]byte ("{" ), & e )
552+ require .Error (t , err )
553+ }
554+
555+ // Test unmarshalJSON should return error when decoding into struct with wrong JSON type.
556+ func TestEventUnmarshalJSON_WrongType_Error (t * testing.T ) {
557+ var e Event
558+ err := json .Unmarshal ([]byte (`"not-an-object"` ), & e )
559+ require .Error (t , err )
560+ }
561+
562+ // Test unmarshalJSON should return error when nested response exists but is malformed.
563+ func TestEventUnmarshalJSON_BadNestedResponse (t * testing.T ) {
564+ input := `{
565+ "id": "evt-bad",
566+ "object": "chat.completion",
567+ "response": 123
568+ }`
569+ var e Event
570+ err := json .Unmarshal ([]byte (input ), & e )
571+ require .NoError (t , err )
572+ }
573+
574+ // Test marshalJSON should return error when timestamp overflow.
575+ func TestEventMarshalJSON_TimestampOverflow (t * testing.T ) {
576+ t .Run ("event with timestamp overflow" , func (t * testing.T ) {
577+ e := & Event {
578+ Timestamp : time .Unix (1 << 60 - 1 , 0 ),
579+ }
580+ _ , err := json .Marshal (e )
581+ require .Error (t , err )
582+ })
583+ t .Run ("response with timestamp overflow" , func (t * testing.T ) {
584+ e := & Event {
585+ Response : & model.Response {
586+ Timestamp : time .Unix (1 << 60 - 1 , 0 ),
587+ },
588+ }
589+ _ , err := json .Marshal (e )
590+ require .Error (t , err )
591+ })
592+ }
593+
594+ // Test unmarshalJSON should return error when timestamp overflow.
595+ func TestEventUnMarshalJSON_TimestampOverflow (t * testing.T ) {
596+ t .Run ("event with timestamp overflow" , func (t * testing.T ) {
597+ var e Event
598+ err := json .Unmarshal ([]byte (`{"timestamp": "12025-01-01T00:00:00Z"}` ), & e )
599+ require .Error (t , err )
600+ })
601+ t .Run ("response with timestamp overflow" , func (t * testing.T ) {
602+ var e Event
603+ err := json .Unmarshal ([]byte (`{"response": {"timestamp": "12025-01-01T00:00:00Z"}}` ), & e )
604+ require .NoError (t , err )
605+ })
606+ }
607+
608+ func TestEventMarshalJSON (t * testing.T ) {
609+ t .Run ("without struct" , func (t * testing.T ) {
610+ e := Event {ID : "id1" , Response : & model.Response {ID : "id2" }}
611+ data , err := json .Marshal (e )
612+ require .NoError (t , err )
613+ var dst Event
614+ require .NoError (t , json .Unmarshal (data , & dst ))
615+ require .Equal (t , "id1" , dst .ID )
616+ require .Equal (t , "id2" , dst .Response .ID )
617+ })
618+ t .Run ("with pointer" , func (t * testing.T ) {
619+ e := & Event {ID : "id1" , Response : & model.Response {ID : "id2" }}
620+ data , err := json .Marshal (e )
621+ require .NoError (t , err )
622+ var dst Event
623+ require .NoError (t , json .Unmarshal (data , & dst ))
624+ require .Equal (t , "id1" , dst .ID )
625+ require .Equal (t , "id2" , dst .Response .ID )
626+ })
627+ }
628+
629+ func TestEventJSON_RoundTrip (t * testing.T ) {
630+ t .Run ("normal" , func (t * testing.T ) {
631+ src := & Event {
632+ Response : & model.Response {
633+ ID : "resp-rt" ,
634+ Object : model .ObjectTypeChatCompletion ,
635+ Done : true ,
636+ Choices : []model.Choice {{Index : 0 , Message : model .NewAssistantMessage ("hi" )}},
637+ },
638+ ID : "evt-rt" ,
639+ InvocationID : "inv-rt" ,
640+ Author : "assistant" ,
641+ Timestamp : time .Now (),
642+ }
643+
644+ data , err := json .Marshal (src )
645+ require .NoError (t , err )
646+
647+ var dst Event
648+ require .NoError (t , json .Unmarshal (data , & dst ))
649+
650+ require .NotNil (t , dst .Response )
651+ require .Equal (t , "evt-rt" , dst .ID )
652+ require .Equal (t , "resp-rt" , dst .Response .ID )
653+ require .Equal (t , model .ObjectTypeChatCompletion , dst .Response .Object )
654+ })
655+ t .Run ("top-level value" , func (t * testing.T ) {
656+ e := Event {ID : "id1" , Response : & model.Response {ID : "id2" }}
657+ data , err := json .Marshal (e )
658+ require .NoError (t , err )
659+ var dst Event
660+ require .NoError (t , json .Unmarshal (data , & dst ))
661+ require .Equal (t , "id1" , dst .ID )
662+ require .Equal (t , "id2" , dst .Response .ID )
663+ })
664+ t .Run ("top-level pointer" , func (t * testing.T ) {
665+ e := & Event {ID : "id1" , Response : & model.Response {ID : "id2" }}
666+ data , err := json .Marshal (e )
667+ require .NoError (t , err )
668+ var dst Event
669+ require .NoError (t , json .Unmarshal (data , & dst ))
670+ require .Equal (t , "id1" , dst .ID )
671+ require .Equal (t , "id2" , dst .Response .ID )
672+ })
673+ t .Run ("slice element value" , func (t * testing.T ) {
674+ in := []Event {{ID : "id1" , Response : & model.Response {ID : "id2" }}}
675+ data , err := json .Marshal (in )
676+ require .NoError (t , err )
677+ var out []Event
678+ require .NoError (t , json .Unmarshal (data , & out ))
679+ require .Len (t , out , 1 )
680+ require .Equal (t , "id2" , out [0 ].Response .ID )
681+ })
682+ t .Run ("map value non-addressable" , func (t * testing.T ) {
683+ m := map [string ]Event {
684+ "k" : {ID : "id1" , Response : & model.Response {ID : "id2" }},
685+ }
686+ data , err := json .Marshal (m )
687+ require .NoError (t , err )
688+ var out map [string ]Event
689+ require .NoError (t , json .Unmarshal (data , & out ))
690+ require .Equal (t , "id2" , out ["k" ].Response .ID )
691+ })
692+ t .Run ("omit key and stay nil on roundtrip" , func (t * testing.T ) {
693+ e := Event {ID : "id1" }
694+ data , err := json .Marshal (e )
695+ require .NoError (t , err )
696+
697+ var tmp map [string ]any
698+ require .NoError (t , json .Unmarshal (data , & tmp ))
699+ _ , has := tmp ["response" ]
700+ require .False (t , has )
701+
702+ var dst Event
703+ require .NoError (t , json .Unmarshal (data , & dst ))
704+ require .Equal (t , "id1" , dst .ID )
705+ require .Nil (t , dst .Response )
706+ })
707+ t .Run ("timestamp round-trip" , func (t * testing.T ) {
708+ ts := time .Date (2024 , 1 , 2 , 3 , 4 , 5 , 0 , time .UTC )
709+ e := Event {ID : "id1" , Response : & model.Response {ID : "id2" , Timestamp : ts }}
710+ data , err := json .Marshal (e )
711+ require .NoError (t , err )
712+ var dst Event
713+ require .NoError (t , json .Unmarshal (data , & dst ))
714+ require .True (t , dst .Response .Timestamp .Equal (ts ))
715+ })
716+ t .Run ("prefer nested over legacy" , func (t * testing.T ) {
717+ raw := []byte (`{
718+ "id": "id1",
719+ "Response": {"id": "old", "timestamp": "2024-01-01T00:00:00Z"},
720+ "response": {"id": "new", "timestamp": "2024-01-02T00:00:00Z"}
721+ }` )
722+ var dst Event
723+ require .NoError (t , json .Unmarshal (raw , & dst ))
724+ require .Equal (t , "id1" , dst .ID )
725+ require .Equal (t , "new" , dst .Response .ID )
726+ require .Equal (t , time .Date (2024 , 1 , 2 , 0 , 0 , 0 , 0 , time .UTC ), dst .Response .Timestamp )
727+ })
728+ t .Run ("malformed nested response is ignored, flat fields still decode" , func (t * testing.T ) {
729+ raw := []byte (`{"id":"id1","response":"oops"}` )
730+ var dst Event
731+ require .NoError (t , json .Unmarshal (raw , & dst ))
732+ require .Equal (t , "id1" , dst .ID )
733+ require .Nil (t , dst .Response )
734+ })
735+ }
0 commit comments