2026-07-25 21:52:59 +00:00
package shape
import (
"context"
"reflect"
"strings"
"testing"
"unicode/utf8"
"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-08-05 20:00:20 +00:00
func TestValidatorAcceptsEveryOccurrenceRuleAndNormalizableWhitespace ( t * testing . T ) {
2026-07-25 21:52:59 +00:00
quantity := 1
2026-08-05 19:55:34 +00:00
value := dnd . ItemOccurrenceList { Occurrences : [ ] dnd . ItemOccurrence { { ItemID : "item" , Name : " Hidden Cache " , Kind : dnd . ItemOccurrenceKindDiscovered , From : " " , To : " " , SourceRefs : refs ( 1 , 1 ) } ,
{ ItemID : "item" , Name : "Gold Pieces" , Kind : dnd . ItemOccurrenceKindAcquired , Quantity : & quantity , To : " party " , SourceRefs : refs ( 2 , 2 ) } ,
{ ItemID : "item" , Name : "Torch" , Kind : dnd . ItemOccurrenceKindLost , From : " party " , SourceRefs : refs ( 3 , 3 ) } ,
{ ItemID : "item" , Name : "Potion" , Kind : dnd . ItemOccurrenceKindConsumed , From : " Aria " , SourceRefs : refs ( 4 , 4 ) } ,
{ ItemID : "item" , Name : "Moonblade" , Kind : dnd . ItemOccurrenceKindTransferred , From : "Aria" , To : "Borin" , SourceRefs : refs ( 5 , 5 ) } ,
2026-07-25 21:52:59 +00:00
} }
2026-08-05 19:55:34 +00:00
result , err := New ( Options { } ) . Validate ( context . Background ( ) , contracts . TypedValidationRequest [ dnd . ItemOccurrenceList ] { Value : value } )
2026-07-25 21:52:59 +00:00
if err != nil || ! result . Approved {
t . Fatalf ( "Validate() = %#v, %v" , result , err )
}
2026-08-05 19:55:34 +00:00
empty , err := New ( Options { } ) . Validate ( context . Background ( ) , contracts . TypedValidationRequest [ dnd . ItemOccurrenceList ] { Value : dnd . ItemOccurrenceList { Occurrences : [ ] dnd . ItemOccurrence { } } } )
2026-07-25 21:52:59 +00:00
if err != nil || ! empty . Approved {
t . Fatalf ( "empty list result = %#v, %v" , empty , err )
}
}
func TestValidatorRejectsOwnedSemanticBoundaries ( t * testing . T ) {
valid := validList ( )
zero := 0
negative := - 1
tests := [ ] struct {
name string
2026-08-05 19:55:34 +00:00
value dnd . ItemOccurrenceList
2026-07-25 21:52:59 +00:00
want string
} {
2026-08-05 20:00:20 +00:00
{ "missing occurrences" , dnd . ItemOccurrenceList { } , "occurrences must be present" } ,
2026-08-05 19:55:34 +00:00
{ "blank name" , listWith ( dnd . ItemOccurrence { ItemID : "item" , Name : " \t" , Kind : dnd . ItemOccurrenceKindDiscovered , SourceRefs : refs ( 1 , 1 ) } ) , "name must not be empty" } ,
{ "unsupported kind" , listWith ( dnd . ItemOccurrence { ItemID : "item" , Name : "Ring" , Kind : "unknown" , SourceRefs : refs ( 1 , 1 ) } ) , "kind is unsupported" } ,
{ "discovered holder" , listWith ( dnd . ItemOccurrence { ItemID : "item" , Name : "Ring" , Kind : dnd . ItemOccurrenceKindDiscovered , From : "Aria" , SourceRefs : refs ( 1 , 1 ) } ) , "incompatible" } ,
{ "acquired without holder" , listWith ( dnd . ItemOccurrence { ItemID : "item" , Name : "Ring" , Kind : dnd . ItemOccurrenceKindAcquired , SourceRefs : refs ( 1 , 1 ) } ) , "incompatible" } ,
{ "lost destination" , listWith ( dnd . ItemOccurrence { ItemID : "item" , Name : "Ring" , Kind : dnd . ItemOccurrenceKindLost , From : "Aria" , To : "Borin" , SourceRefs : refs ( 1 , 1 ) } ) , "incompatible" } ,
{ "consumed without holder" , listWith ( dnd . ItemOccurrence { ItemID : "item" , Name : "Ring" , Kind : dnd . ItemOccurrenceKindConsumed , SourceRefs : refs ( 1 , 1 ) } ) , "incompatible" } ,
{ "party transfer from" , listWith ( dnd . ItemOccurrence { ItemID : "item" , Name : "Ring" , Kind : dnd . ItemOccurrenceKindTransferred , From : "party" , To : "Borin" , SourceRefs : refs ( 1 , 1 ) } ) , "incompatible" } ,
{ "party transfer to" , listWith ( dnd . ItemOccurrence { ItemID : "item" , Name : "Ring" , Kind : dnd . ItemOccurrenceKindTransferred , From : "Aria" , To : " PARTY " , SourceRefs : refs ( 1 , 1 ) } ) , "incompatible" } ,
{ "case equivalent transfer" , listWith ( dnd . ItemOccurrence { ItemID : "item" , Name : "Ring" , Kind : dnd . ItemOccurrenceKindTransferred , From : "Aria" , To : "aria" , SourceRefs : refs ( 1 , 1 ) } ) , "incompatible" } ,
{ "unicode equivalent transfer" , listWith ( dnd . ItemOccurrence { ItemID : "item" , Name : "Ring" , Kind : dnd . ItemOccurrenceKindTransferred , From : "Åria" , To : "Åria" , SourceRefs : refs ( 1 , 1 ) } ) , "incompatible" } ,
{ "zero quantity" , listWith ( dnd . ItemOccurrence { ItemID : "item" , Name : "Ring" , Kind : dnd . ItemOccurrenceKindAcquired , Quantity : & zero , To : "party" , SourceRefs : refs ( 1 , 1 ) } ) , "quantity must be positive" } ,
{ "negative quantity" , listWith ( dnd . ItemOccurrence { ItemID : "item" , Name : "Ring" , Kind : dnd . ItemOccurrenceKindAcquired , Quantity : & negative , To : "party" , SourceRefs : refs ( 1 , 1 ) } ) , "quantity must be positive" } ,
{ "missing source refs" , listWith ( dnd . ItemOccurrence { ItemID : "item" , Name : "Ring" , Kind : dnd . ItemOccurrenceKindDiscovered } ) , "source_refs must contain" } ,
2026-07-25 21:52:59 +00:00
}
for _ , test := range tests {
t . Run ( test . name , func ( t * testing . T ) {
2026-08-05 19:55:34 +00:00
result , err := New ( Options { } ) . Validate ( context . Background ( ) , contracts . TypedValidationRequest [ dnd . ItemOccurrenceList ] { Value : test . value } )
2026-07-25 21:52:59 +00:00
if err != nil || result . Approved || result . ReasonCode != ReasonCode || ! strings . Contains ( result . Message , test . want ) {
t . Fatalf ( "Validate() = %#v, %v; want %q" , result , err , test . want )
}
} )
}
2026-08-05 19:55:34 +00:00
if result , err := New ( Options { } ) . Validate ( context . Background ( ) , contracts . TypedValidationRequest [ dnd . ItemOccurrenceList ] { Value : valid } ) ; err != nil || ! result . Approved {
2026-07-25 21:52:59 +00:00
t . Fatalf ( "valid result = %#v, %v" , result , err )
}
}
func TestValidatorAggregatesBoundedIndexedDiagnosticsAndRegistration ( t * testing . T ) {
2026-08-05 19:55:34 +00:00
value := dnd . ItemOccurrenceList { Occurrences : make ( [ ] dnd . ItemOccurrence , 24 ) }
for index := range value . Occurrences {
value . Occurrences [ index ] = dnd . ItemOccurrence { ItemID : "item" , Name : " \n" , Kind : "unsupported" }
2026-07-25 21:52:59 +00:00
}
2026-08-05 19:55:34 +00:00
result , err := New ( Options { } ) . Validate ( context . Background ( ) , contracts . TypedValidationRequest [ dnd . ItemOccurrenceList ] { Value : value } )
if err != nil || result . Approved || result . ReasonCode != ReasonCode || len ( [ ] byte ( result . Message ) ) > 4096 || ! utf8 . ValidString ( result . Message ) || ! strings . Contains ( result . Message , "occurrences[0]" ) || ! strings . Contains ( result . Message , "additional issue(s) omitted" ) {
2026-07-25 21:52:59 +00:00
t . Fatalf ( "Validate() = %#v, %v" , result , err )
}
2026-08-05 19:55:34 +00:00
if value . Occurrences [ 0 ] . Name != " \n" {
2026-07-25 21:52:59 +00:00
t . Fatal ( "Validate() mutated input" )
}
if got := New ( Options { } ) . CheckpointFingerprints ( ) ; ! reflect . DeepEqual ( got , [ ] pipeline . CheckpointFingerprint { { Name : "policy" , Value : policy } } ) {
t . Fatalf ( "CheckpointFingerprints() = %#v" , got )
}
registry := pipeline . NewValidatorRegistry ( )
if err := Register ( registry ) ; err != nil {
t . Fatal ( err )
}
if got , ok := registry . Spec ( Key ) ; ! ok || ! reflect . DeepEqual ( got , Spec ( ) ) {
t . Fatalf ( "registry spec = %#v, %t" , got , ok )
}
if _ , err := DecodeOptions ( map [ string ] any { "unexpected" : true } ) ; err == nil {
t . Fatal ( "DecodeOptions() accepted unknown option" )
}
}
2026-08-05 19:55:34 +00:00
func validList ( ) dnd . ItemOccurrenceList {
return dnd . ItemOccurrenceList { Occurrences : [ ] dnd . ItemOccurrence { { ItemID : "item" , Name : "Ring" , Kind : dnd . ItemOccurrenceKindAcquired , To : "party" , SourceRefs : refs ( 1 , 1 ) } } }
2026-07-25 21:52:59 +00:00
}
2026-08-05 20:00:20 +00:00
func listWith ( occurrence dnd . ItemOccurrence ) dnd . ItemOccurrenceList {
return dnd . ItemOccurrenceList { Occurrences : [ ] dnd . ItemOccurrence { occurrence } }
2026-07-25 21:52:59 +00:00
}
func refs ( start , end int ) [ ] source . SourceRef {
return [ ] source . SourceRef { { SourceID : "session" , StartUnitID : start , EndUnitID : end } }
}