2026-07-17 06:50:08 +00:00
package spells
import (
"bytes"
2026-07-17 15:13:19 +00:00
"context"
2026-07-17 06:50:08 +00:00
"encoding/json"
"os"
"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"
2026-07-17 15:13:19 +00:00
spellshape "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/spells/shape"
2026-07-17 06:50:08 +00:00
)
func TestCodecMatchesMaintainedDurableFixture ( t * testing . T ) {
raw , err := os . ReadFile ( "testdata/dnd_spells.v1.json" )
if err != nil {
t . Fatalf ( "read durable fixture: %v" , err )
}
codec := New ( )
value , err := codec . Decode ( raw )
if err != nil {
t . Fatalf ( "Decode() error = %v, want nil" , err )
}
want := dnd . SpellList { SpellCasts : [ ] dnd . SpellCast {
{ Caster : "Aria" , Spell : "Cure Wounds" , Effect : "Heals an injured ally." , NarrativeDescription : "Aria restores the fighter after the fight." , SourceRefs : [ ] source . SourceRef { { SourceID : "session-alpha" , StartUnitID : 1 , EndUnitID : 2 } } } ,
{ Caster : "Borin" , Spell : "Fire Bolt" , Effect : "Scorches the wight." , NarrativeDescription : "Borin hurls fire at the wight." , SourceRefs : [ ] source . SourceRef { { SourceID : "session-alpha" , StartUnitID : 3 , EndUnitID : 3 } } } ,
} }
if ! reflect . DeepEqual ( value , want ) {
t . Fatalf ( "Decode() = %#v, want %#v" , value , want )
}
encoded , err := codec . Encode ( value )
if err != nil {
t . Fatalf ( "Encode() error = %v, want nil" , err )
}
var compact bytes . Buffer
if err := json . Compact ( & compact , raw ) ; err != nil {
t . Fatalf ( "compact durable fixture: %v" , err )
}
if ! bytes . Equal ( encoded , compact . Bytes ( ) ) {
t . Fatalf ( "Encode() = %s, want stable durable JSON %s" , encoded , compact . Bytes ( ) )
}
second , err := codec . Encode ( value )
if err != nil || ! bytes . Equal ( second , encoded ) {
t . Fatalf ( "second Encode() = %s, %v; want deterministic bytes" , second , err )
}
}
func TestCodecOwnsDurableSchemaAndRegistersExactType ( t * testing . T ) {
codec := New ( )
schema := codec . Schema ( )
if codec . Kind ( ) != dnd . SpellListKind || codec . MediaType ( ) != MediaType {
t . Fatalf ( "codec identity = %q/%q" , codec . Kind ( ) , codec . MediaType ( ) )
}
if schema . ID != SchemaID || schema . Name != SchemaName || schema . Version != SchemaVersion || ! json . Valid ( schema . JSONSchema ) {
t . Fatalf ( "schema = %#v, want durable spell schema" , schema )
}
var document map [ string ] any
if err := json . Unmarshal ( schema . JSONSchema , & document ) ; err != nil || document [ "$id" ] != SchemaID {
t . Fatalf ( "durable schema document = %#v, %v" , document , err )
}
registry := pipeline . NewArtifactCodecRegistry ( )
if err := pipeline . RegisterArtifactCodec ( registry , codec ) ; err != nil {
t . Fatalf ( "RegisterArtifactCodec() error = %v" , err )
}
spec , ok := registry . Spec ( dnd . SpellListKind )
if ! ok || spec . SchemaDigest != contracts . DigestArtifactSchema ( schema ) {
t . Fatalf ( "registered spec = %#v, %t" , spec , ok )
}
}
func TestCodecStrictlyRejectsInvalidRepresentations ( t * testing . T ) {
codec := New ( )
tests := [ ] struct {
name string
raw string
want string
} {
{ name : "unknown" , raw : ` { "spell_casts":[],"unexpected":true} ` , want : "unknown field" } ,
{ name : "trailing" , raw : ` { "spell_casts":[]} { } ` , want : "multiple JSON values" } ,
{ name : "missing" , raw : ` { } ` , want : "spell_casts must be present" } ,
{ name : "invalid evidence" , raw : ` { "spell_casts":[ { "caster":"Aria","spell":"Cure Wounds","effect":"Heals","narrative_description":"Aria heals","source_refs":[ { "source_id":"session","start_unit_id":0,"end_unit_id":1}]}]} ` , want : "start_unit_id" } ,
}
for _ , test := range tests {
t . Run ( test . name , func ( t * testing . T ) {
_ , err := codec . Decode ( [ ] byte ( test . raw ) )
if err == nil || ! strings . Contains ( err . Error ( ) , test . want ) {
t . Fatalf ( "Decode() error = %v, want %q" , err , test . want )
}
} )
}
}
func TestCodecRejectsInvalidCanonicalValues ( t * testing . T ) {
_ , err := New ( ) . Encode ( dnd . SpellList { } )
if err == nil || ! strings . Contains ( err . Error ( ) , "spell_casts must be present" ) {
t . Fatalf ( "Encode() error = %v, want strict shape error" , err )
}
}
2026-07-17 15:13:19 +00:00
func TestCodecEncodesIncompleteCandidateWithoutWeakeningFinalEncoding ( t * testing . T ) {
codec := New ( )
candidate := dnd . SpellList { }
content , err := codec . EncodeCandidate ( candidate )
if err != nil {
t . Fatalf ( "EncodeCandidate() error = %v, want nil" , err )
}
if ! json . Valid ( content ) {
t . Fatalf ( "EncodeCandidate() = %q, want JSON" , content )
}
result , err := spellshape . New ( spellshape . Options { } ) . Validate ( context . Background ( ) , contracts . TypedValidationRequest [ dnd . SpellList ] { Value : candidate } )
if err != nil || result . Approved || result . ReasonCode != spellshape . ReasonCode {
t . Fatalf ( "shape validation = %#v, %v; want candidate rejection" , result , err )
}
if _ , err := codec . Encode ( candidate ) ; err == nil || ! strings . Contains ( err . Error ( ) , "spell_casts must be present" ) {
t . Fatalf ( "Encode() error = %v, want strict final shape error" , err )
}
}
2026-07-17 06:50:08 +00:00
func TestCodecSchemaIsMutationSafe ( t * testing . T ) {
first := New ( ) . Schema ( )
first . JSONSchema [ 0 ] = '['
second := New ( ) . Schema ( )
if ! json . Valid ( second . JSONSchema ) || second . JSONSchema [ 0 ] == '[' {
t . Fatalf ( "Schema() returned shared bytes: %s" , second . JSONSchema )
}
}