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" } ,
2026-08-09 02:03:27 +00:00
{ "missing source start" , strings . Replace ( validJSON , ` "start_unit_id":1, ` , "" , 1 ) , "source_refs[0].start_unit_id must be present" } ,
{ "missing source end" , strings . Replace ( validJSON , ` ,"end_unit_id":1 ` , "" , 1 ) , "source_refs[0].end_unit_id must be present" } ,
2026-08-03 20:32:20 +00:00
{ "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 )
}
} )
}
}
2026-08-09 02:03:27 +00:00
func TestCodecDefensivelyOwnsCandidateValues ( t * testing . T ) {
2026-08-03 20:32:20 +00:00
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" )
}
2026-08-09 02:03:27 +00:00
if _ , err := codec . Decode ( content ) ; err == nil || ! strings . Contains ( err . Error ( ) , "name must not be empty" ) {
t . Fatalf ( "Decode() error = %v, want durable value validation" , 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" )
}
}
2026-08-09 02:03:27 +00:00
func TestCodecRejectsInvalidDurableValuesWhileCandidatesPreserveThem ( t * testing . T ) {
codec := New ( )
for _ , test := range [ ] struct {
name string
mutate func ( * dnd . EnemyEventList )
want string
} {
{ name : "blank name" , mutate : func ( value * dnd . EnemyEventList ) { value . Events [ 0 ] . Name = " " } , want : "name must not be empty" } ,
{ name : "unsupported kind" , mutate : func ( value * dnd . EnemyEventList ) { value . Events [ 0 ] . Kind = "unsupported" } , want : "kind must be supported" } ,
{ name : "empty source refs" , mutate : func ( value * dnd . EnemyEventList ) { value . Events [ 0 ] . SourceRefs = [ ] source . SourceRef { } } , want : "source_refs must contain" } ,
{ name : "blank source ID" , mutate : func ( value * dnd . EnemyEventList ) { value . Events [ 0 ] . SourceRefs [ 0 ] . SourceID = " " } , want : "source_id must not be empty" } ,
{ name : "nonpositive start" , mutate : func ( value * dnd . EnemyEventList ) { value . Events [ 0 ] . SourceRefs [ 0 ] . StartUnitID = 0 } , want : "start_unit_id" } ,
{ name : "nonpositive end" , mutate : func ( value * dnd . EnemyEventList ) { value . Events [ 0 ] . SourceRefs [ 0 ] . EndUnitID = 0 } , want : "end_unit_id" } ,
{ name : "reversed" , mutate : func ( value * dnd . EnemyEventList ) {
value . Events [ 0 ] . SourceRefs [ 0 ] . StartUnitID = 2
value . Events [ 0 ] . SourceRefs [ 0 ] . EndUnitID = 1
} , want : "must not exceed" } ,
} {
t . Run ( test . name , func ( t * testing . T ) {
candidate := dnd . EnemyEventList { Events : [ ] dnd . EnemyEvent { {
Name : "Ashfang" , Kind : dnd . EnemyEventKindEngaged ,
SourceRefs : [ ] source . SourceRef { { SourceID : "session" , StartUnitID : 1 , EndUnitID : 1 } } ,
} } }
test . mutate ( & candidate )
content , err := codec . EncodeCandidate ( candidate )
if err != nil {
t . Fatalf ( "EncodeCandidate() error = %v" , err )
}
decoded , err := codec . DecodeCandidate ( content )
if err != nil || ! reflect . DeepEqual ( decoded , candidate ) {
t . Fatalf ( "DecodeCandidate() = %#v, %v; want %#v" , decoded , err , candidate )
}
if _ , err := codec . Encode ( candidate ) ; err == nil || ! strings . Contains ( err . Error ( ) , test . want ) {
t . Fatalf ( "Encode() error = %v, want %q" , err , test . want )
}
if _ , err := codec . Decode ( content ) ; err == nil || ! strings . Contains ( err . Error ( ) , test . want ) {
t . Fatalf ( "Decode() error = %v, want %q" , err , test . want )
}
} )
}
}
func TestCodecAcceptsEmptyDurableEventList ( t * testing . T ) {
value := dnd . EnemyEventList { Events : [ ] dnd . EnemyEvent { } }
content , err := New ( ) . Encode ( value )
if err != nil || string ( content ) != ` { "events":[]} ` {
t . Fatalf ( "Encode() = %s, %v" , content , err )
}
decoded , err := New ( ) . Decode ( content )
if err != nil || ! reflect . DeepEqual ( decoded , value ) {
t . Fatalf ( "Decode() = %#v, %v; want %#v" , decoded , err , value )
}
}