2026-07-21 05:22:12 +00:00
package invariants
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"
)
func TestValidatorApprovesNormalizedCombatTurns ( t * testing . T ) {
result , err := New ( Options { } ) . Validate ( context . Background ( ) , contracts . TypedValidationRequest [ dnd . CombatTurnList ] { Source : invariantDocument ( ) , Value : normalizedList ( ) } )
if err != nil || ! result . Approved || len ( result . Warnings ) != 0 {
t . Fatalf ( "Validate() = %#v, %v; want approval without warnings" , result , err )
}
}
func TestValidateRejectsOwnedNormalizedInvariants ( t * testing . T ) {
tests := [ ] struct {
name string
mutate func ( * dnd . CombatTurnList , * source . SourceDocument )
want string
} {
{ name : "actor display whitespace" , mutate : func ( value * dnd . CombatTurnList , _ * source . SourceDocument ) { value . CombatTurns [ 0 ] . Actor = " Aria " } , want : "actor is not display-normalized" } ,
{ name : "reference order" , mutate : func ( value * dnd . CombatTurnList , _ * source . SourceDocument ) {
value . CombatTurns [ 0 ] . SourceRefs = [ ] source . SourceRef { { SourceID : "session" , StartUnitID : 20 , EndUnitID : 20 } , { SourceID : "session" , StartUnitID : 10 , EndUnitID : 10 } }
} , want : "not in canonical order" } ,
{ name : "duplicate reference" , mutate : func ( value * dnd . CombatTurnList , _ * source . SourceDocument ) {
value . CombatTurns [ 0 ] . SourceRefs = append ( value . CombatTurns [ 0 ] . SourceRefs , value . CombatTurns [ 0 ] . SourceRefs [ 0 ] )
} , want : "duplicates the previous reference" } ,
{ name : "chronology" , mutate : func ( value * dnd . CombatTurnList , _ * source . SourceDocument ) {
value . CombatTurns = [ ] dnd . CombatTurn { withRef ( value . CombatTurns [ 0 ] , source . SourceRef { SourceID : "session" , StartUnitID : 20 , EndUnitID : 20 } ) , value . CombatTurns [ 0 ] }
} , want : "out of chronological order" } ,
{ name : "duplicate identity" , mutate : func ( value * dnd . CombatTurnList , _ * source . SourceDocument ) {
2026-07-22 19:14:14 +00:00
value . CombatTurns = append ( value . CombatTurns , value . CombatTurns [ 0 ] )
2026-07-21 05:22:12 +00:00
} , want : "duplicates combat turn" } ,
}
for _ , test := range tests {
t . Run ( test . name , func ( t * testing . T ) {
value := normalizedList ( )
doc := invariantDocument ( )
test . mutate ( & value , doc )
err := Validate ( doc , value )
if err == nil || ! strings . Contains ( err . Error ( ) , test . want ) {
t . Fatalf ( "Validate() error = %v, want %q" , err , test . want )
}
result , err := New ( Options { } ) . Validate ( context . Background ( ) , contracts . TypedValidationRequest [ dnd . CombatTurnList ] { Source : doc , Value : value } )
if err != nil || result . Approved || result . ReasonCode != ReasonCode {
t . Fatalf ( "Validator result = %#v, %v; want normalized-invariant rejection" , result , err )
}
} )
}
}
2026-07-24 14:20:10 +00:00
func TestValidateUsesSourceDocumentOrder ( t * testing . T ) {
doc := & source . SourceDocument { ID : "session" , Units : [ ] source . SourceUnit { { ID : 30 } , { ID : 10 } } }
value := dnd . CombatTurnList { CombatTurns : [ ] dnd . CombatTurn {
{ Actor : "Aria" , TurnKind : dnd . CombatTurnKindTurn , SourceRefs : [ ] source . SourceRef { { SourceID : doc . ID , StartUnitID : 10 , EndUnitID : 10 } , { SourceID : doc . ID , StartUnitID : 30 , EndUnitID : 30 } } } ,
} }
if err := Validate ( doc , value ) ; err == nil || ! strings . Contains ( err . Error ( ) , "not in canonical order" ) {
t . Fatalf ( "Validate() error = %v, want document-order reference rejection" , err )
}
value = dnd . CombatTurnList { CombatTurns : [ ] dnd . CombatTurn {
{ Actor : "Aria" , TurnKind : dnd . CombatTurnKindTurn , SourceRefs : [ ] source . SourceRef { { SourceID : doc . ID , StartUnitID : 10 , EndUnitID : 10 } } } ,
{ Actor : "Borin" , TurnKind : dnd . CombatTurnKindTurn , SourceRefs : [ ] source . SourceRef { { SourceID : doc . ID , StartUnitID : 30 , EndUnitID : 30 } } } ,
} }
if err := Validate ( doc , value ) ; err == nil || ! strings . Contains ( err . Error ( ) , "out of chronological order" ) {
t . Fatalf ( "Validate() error = %v, want document-order chronology rejection" , err )
}
}
2026-07-21 05:22:12 +00:00
func TestValidatorDefersShapeAndSourceReferenceFailures ( t * testing . T ) {
doc := invariantDocument ( )
shapeInvalid := normalizedList ( )
shapeInvalid . CombatTurns [ 0 ] . Actor = " "
result , err := New ( Options { } ) . Validate ( context . Background ( ) , contracts . TypedValidationRequest [ dnd . CombatTurnList ] { Source : doc , Value : shapeInvalid } )
if err != nil || ! result . Approved {
t . Fatalf ( "shape-invalid result = %#v, %v; want deferral" , result , err )
}
sourceInvalid := normalizedList ( )
sourceInvalid . CombatTurns [ 0 ] . SourceRefs [ 0 ] . StartUnitID = 999
result , err = New ( Options { } ) . Validate ( context . Background ( ) , contracts . TypedValidationRequest [ dnd . CombatTurnList ] { Source : doc , Value : sourceInvalid } )
if err != nil || ! result . Approved {
t . Fatalf ( "source-invalid result = %#v, %v; want deferral" , result , err )
}
}
func TestValidatorBoundsDiagnosticsAndRegistration ( t * testing . T ) {
value := normalizedList ( )
value . CombatTurns = make ( [ ] dnd . CombatTurn , 30 )
for index := range value . CombatTurns {
value . CombatTurns [ index ] = normalizedList ( ) . CombatTurns [ 0 ]
value . CombatTurns [ index ] . Actor = strings . Repeat ( "火" , 300 )
}
result , err := New ( Options { } ) . Validate ( context . Background ( ) , contracts . TypedValidationRequest [ dnd . CombatTurnList ] { Source : invariantDocument ( ) , Value : value } )
if err != nil || result . Approved || ! utf8 . ValidString ( result . Message ) || len ( [ ] byte ( result . Message ) ) > 4096 || ! strings . Contains ( result . Message , "additional issue(s) omitted" ) {
t . Fatalf ( "bounded result = %#v, %v; want bounded rejection" , result , err )
}
if got := New ( Options { } ) . CheckpointFingerprints ( ) ; len ( got ) != 1 || got [ 0 ] . Name != "policy" || got [ 0 ] . Value != policy {
t . Fatalf ( "CheckpointFingerprints() = %#v, want policy fingerprint" , got )
}
if spec := Spec ( ) ; spec . Key != Key || spec . ExecutionClass != contracts . ExecutionClassDeterministic {
t . Fatalf ( "Spec() = %#v" , spec )
}
registry := pipeline . NewValidatorRegistry ( )
if err := Register ( registry ) ; err != nil {
t . Fatalf ( "Register() error = %v" , err )
}
if _ , err := DecodeOptions ( map [ string ] any { "unexpected" : true } ) ; err == nil {
t . Fatal ( "DecodeOptions() accepted unknown option" )
}
if ! reflect . DeepEqual ( New ( Options { } ) . CheckpointFingerprints ( ) , [ ] pipeline . CheckpointFingerprint { { Name : "policy" , Value : policy } } ) {
t . Fatal ( "policy fingerprint changed unexpectedly" )
}
}
func normalizedList ( ) dnd . CombatTurnList {
return dnd . CombatTurnList { CombatTurns : [ ] dnd . CombatTurn { {
2026-07-22 19:14:14 +00:00
Actor : "Aria" , TurnKind : dnd . CombatTurnKindTurn ,
SourceRefs : [ ] source . SourceRef { { SourceID : "session" , StartUnitID : 10 , EndUnitID : 10 } } ,
2026-07-21 05:22:12 +00:00
} } }
}
func invariantDocument ( ) * source . SourceDocument {
return & source . SourceDocument { ID : "session" , Units : [ ] source . SourceUnit { { ID : 10 } , { ID : 20 } } }
}
func withRef ( turn dnd . CombatTurn , ref source . SourceRef ) dnd . CombatTurn {
turn . SourceRefs = [ ] source . SourceRef { ref }
return turn
}