2026-08-03 20:32:20 +00:00
package enemyevents
import (
"encoding/json"
"reflect"
"strings"
"testing"
"gitea.maximumdirect.net/eric/notarius/internal/core/source"
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd"
)
func validList ( ) dnd . EnemyEventList {
return dnd . EnemyEventList { Events : [ ] dnd . EnemyEvent {
{ Name : "Ashfang" , Kind : dnd . EnemyEventKindEngaged , SourceRefs : refs ( 1 , 2 ) } ,
{ Name : "Ashfang" , Kind : dnd . EnemyEventKindFled , SourceRefs : refs ( 5 , 6 ) } ,
} }
}
func refs ( start , end int ) [ ] source . SourceRef {
return [ ] source . SourceRef { { SourceID : "session" , StartUnitID : start , EndUnitID : end } }
}
func TestCodecRoundTripAndIdentity ( t * testing . T ) {
codec := New ( )
value := validList ( )
content , err := codec . Encode ( value )
if err != nil {
t . Fatalf ( "Encode() error = %v" , err )
}
decoded , err := codec . Decode ( content )
if err != nil || ! reflect . DeepEqual ( decoded , value ) {
t . Fatalf ( "Decode() = %#v, %v; want %#v" , decoded , err , value )
}
schema := codec . Schema ( )
if codec . Kind ( ) != dnd . EnemyEventListKind || codec . MediaType ( ) != MediaType || schema . ID != SchemaID || schema . Name != SchemaName || schema . Version != SchemaVersion || ! json . Valid ( schema . JSONSchema ) {
t . Fatalf ( "codec identity/schema = %q/%q %#v" , codec . Kind ( ) , codec . MediaType ( ) , schema )
}
registry := pipeline . NewArtifactCodecRegistry ( )
if err := pipeline . RegisterArtifactCodec ( registry , codec ) ; err != nil {
t . Fatal ( err )
}
spec , ok := registry . Spec ( dnd . EnemyEventListKind )
if ! ok || spec . SchemaDigest != contracts . DigestArtifactSchema ( schema ) {
t . Fatalf ( "registered spec = %#v, %t" , spec , ok )
}
}
func TestCodecRejectsStrictJSONAndMissingRequiredFields ( t * testing . T ) {
validJSON := ` { "events":[ { "name":"Ashfang","kind":"engaged","source_refs":[ { "source_id":"session","start_unit_id":1,"end_unit_id":1}]}]} `
tests := [ ] struct {
name , raw , want string
} {
{ "invalid JSON" , ` { ` , "decode dnd enemy event list" } ,
{ "unknown root field" , ` { "events":[],"unexpected":true} ` , "unknown field" } ,
{ "unknown event field" , strings . Replace ( validJSON , ` "kind":"engaged" ` , ` "kind":"engaged","unexpected":true ` , 1 ) , "unknown field" } ,
{ "missing events" , ` { } ` , "events must be present" } ,
{ "missing name" , strings . Replace ( validJSON , ` "name":"Ashfang", ` , "" , 1 ) , "events[0].name must be present" } ,
{ "missing kind" , strings . Replace ( validJSON , ` "kind":"engaged", ` , "" , 1 ) , "events[0].kind must be present" } ,
{ "missing refs" , strings . Replace ( validJSON , ` ,"source_refs":[ { "source_id":"session","start_unit_id":1,"end_unit_id":1}] ` , "" , 1 ) , "events[0].source_refs must be present" } ,
{ "missing source ID" , strings . Replace ( validJSON , ` "source_id":"session", ` , "" , 1 ) , "source_refs[0].source_id must be present" } ,
{ "trailing JSON" , ` { "events":[]} { } ` , "multiple JSON values" } ,
}
for _ , test := range tests {
t . Run ( test . name , func ( t * testing . T ) {
if _ , err := New ( ) . Decode ( [ ] byte ( test . raw ) ) ; err == nil || ! strings . Contains ( err . Error ( ) , test . want ) {
t . Fatalf ( "Decode() error = %v, want %q" , err , test . want )
}
} )
}
}
func TestCodecDefensivelyOwnsValuesAndDefersSemanticValidation ( t * testing . T ) {
codec := New ( )
candidate := dnd . EnemyEventList { Events : [ ] dnd . EnemyEvent { {
Name : " " , Kind : "unsupported" , SourceRefs : [ ] source . SourceRef { { SourceID : "" , StartUnitID : 0 , EndUnitID : - 1 } } ,
} } }
content , err := codec . EncodeCandidate ( candidate )
if err != nil {
t . Fatal ( err )
}
2026-08-09 01:03:50 +00:00
decoded , err := codec . DecodeCandidate ( content )
2026-08-03 20:32:20 +00:00
if err != nil || ! reflect . DeepEqual ( decoded , candidate ) {
2026-08-09 01:03:50 +00:00
t . Fatalf ( "DecodeCandidate() = %#v, %v; want semantic candidate preservation" , decoded , err )
2026-08-03 20:32:20 +00:00
}
decoded . Events [ 0 ] . SourceRefs [ 0 ] . SourceID = "changed"
if candidate . Events [ 0 ] . SourceRefs [ 0 ] . SourceID != "" {
2026-08-09 01:03:50 +00:00
t . Fatal ( "DecodeCandidate() retained caller-owned source references" )
}
if decoded , err := codec . Decode ( content ) ; err != nil || ! reflect . DeepEqual ( decoded , candidate ) {
t . Fatalf ( "Decode() = %#v, %v; want durable decode behavior" , decoded , err )
2026-08-03 20:32:20 +00:00
}
first := codec . Schema ( )
first . JSONSchema [ 0 ] = '['
if second := codec . Schema ( ) ; ! json . Valid ( second . JSONSchema ) || second . JSONSchema [ 0 ] == '[' {
t . Fatal ( "Schema() returned shared bytes" )
}
}