2026-07-23 20:17:18 +00:00
package scenedescriptions
import (
"context"
"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 TestNormalizeTrimsOrdersDeduplicatesAndOwnsOutput ( t * testing . T ) {
doc := testDocument ( )
input := dnd . SceneDescriptionList { Scenes : [ ] dnd . SceneDescription {
scene ( "later" , 3 , 3 , dnd . SceneKindNarrative , " Return " , " The party returns. " ) ,
scene ( "first-b" , 1 , 1 , dnd . SceneKindMeta , " Rules " , " The table checks rules. " ) ,
scene ( "first-a" , 1 , 1 , dnd . SceneKindMeta , " Rules " , " The table checks rules. " ) ,
scene ( "later" , 3 , 3 , dnd . SceneKindNarrative , " Return " , " The party returns. " ) ,
} }
before := cloneList ( input )
result , err := New ( Options { } ) . Normalize ( context . Background ( ) , normalizeRequest ( input , doc ) )
if err != nil {
t . Fatalf ( "Normalize() error = %v, want nil" , err )
}
want := [ ] dnd . SceneDescription {
scene ( "first-a" , 1 , 1 , dnd . SceneKindMeta , "Rules" , "The table checks rules." ) ,
scene ( "first-b" , 1 , 1 , dnd . SceneKindMeta , "Rules" , "The table checks rules." ) ,
scene ( "later" , 3 , 3 , dnd . SceneKindNarrative , "Return" , "The party returns." ) ,
}
if ! reflect . DeepEqual ( result . Value . Scenes , want ) {
t . Fatalf ( "scenes = %#v, want %#v" , result . Value . Scenes , want )
}
if ! reflect . DeepEqual ( input , before ) {
t . Fatalf ( "Normalize() mutated input: %#v" , input )
}
result . Value . Scenes [ 0 ] . Title = "changed"
if input . Scenes [ 0 ] . Title == "changed" {
t . Fatal ( "normalized output aliases input storage" )
}
}
func TestNormalizeRejectsInvalidCandidatesAndConflicts ( t * testing . T ) {
doc := testDocument ( )
valid := scene ( "one" , 1 , 1 , dnd . SceneKindNarrative , "Arrival" , "The party arrives." )
for _ , test := range [ ] struct {
name string
input dnd . SceneDescriptionList
} {
{ name : "nil list" , input : dnd . SceneDescriptionList { } } ,
{ name : "empty list" , input : dnd . SceneDescriptionList { Scenes : [ ] dnd . SceneDescription { } } } ,
{ name : "blank ID" , input : dnd . SceneDescriptionList { Scenes : [ ] dnd . SceneDescription { { Kind : dnd . SceneKindNarrative , Title : "Arrival" , Summary : "The party arrives." , SourceRef : valid . SourceRef } } } } ,
{ name : "invalid kind" , input : dnd . SceneDescriptionList { Scenes : [ ] dnd . SceneDescription { { ID : "one" , Kind : "other" , Title : "Arrival" , Summary : "The party arrives." , SourceRef : valid . SourceRef } } } } ,
{ name : "invalid range" , input : dnd . SceneDescriptionList { Scenes : [ ] dnd . SceneDescription { { ID : "one" , Kind : dnd . SceneKindNarrative , Title : "Arrival" , Summary : "The party arrives." , SourceRef : source . SourceRef { SourceID : doc . ID , StartUnitID : 9 , EndUnitID : 9 } } } } } ,
{ name : "same ID conflict" , input : dnd . SceneDescriptionList { Scenes : [ ] dnd . SceneDescription { valid , scene ( "one" , 2 , 2 , dnd . SceneKindNarrative , "Departure" , "The party leaves." ) } } } ,
{ name : "same range conflict" , input : dnd . SceneDescriptionList { Scenes : [ ] dnd . SceneDescription { valid , scene ( "two" , 1 , 1 , dnd . SceneKindCombat , "Ambush" , "Bandits strike." ) } } } ,
} {
t . Run ( test . name , func ( t * testing . T ) {
if _ , err := New ( Options { } ) . Normalize ( context . Background ( ) , normalizeRequest ( test . input , doc ) ) ; err == nil {
t . Fatal ( "Normalize() error = nil, want rejection" )
}
} )
}
}
2026-07-23 23:03:37 +00:00
func TestNormalizeUsesDocumentOrderRatherThanNumericUnitID ( t * testing . T ) {
doc := & source . SourceDocument { ID : "session" , Units : [ ] source . SourceUnit {
{ ID : 20 , Text : "The party arrives." } ,
{ ID : 10 , Text : "The party departs." } ,
} }
input := dnd . SceneDescriptionList { Scenes : [ ] dnd . SceneDescription {
scene ( "later" , 10 , 10 , dnd . SceneKindNarrative , "Departure" , "The party departs." ) ,
scene ( "first" , 20 , 20 , dnd . SceneKindNarrative , "Arrival" , "The party arrives." ) ,
} }
result , err := New ( Options { } ) . Normalize ( context . Background ( ) , normalizeRequest ( input , doc ) )
if err != nil {
t . Fatalf ( "Normalize() error = %v" , err )
}
if got , want := [ ] string { result . Value . Scenes [ 0 ] . ID , result . Value . Scenes [ 1 ] . ID } , [ ] string { "first" , "later" } ; ! reflect . DeepEqual ( got , want ) {
t . Fatalf ( "scene order = %#v, want %#v" , got , want )
}
}
2026-07-23 20:17:18 +00:00
func TestNormalizerContractAndCancellation ( t * testing . T ) {
if _ , err := DecodeOptions ( nil ) ; err != nil {
t . Fatalf ( "DecodeOptions(nil) error = %v" , err )
}
if _ , err := DecodeOptions ( map [ string ] any { "unexpected" : true } ) ; err == nil {
t . Fatal ( "DecodeOptions() accepted unknown options" )
}
2026-08-03 16:49:51 +00:00
want := pipeline . ModuleSpec { Key : Key , Stage : pipeline . StageNormalize , ExecutionClass : contracts . ExecutionClassDeterministic , Requires : [ ] string { "merged" } , Provides : [ ] string { "normalized" } , ArtifactKind : dnd . SceneDescriptionListKind }
2026-07-23 20:17:18 +00:00
if got := ModuleSpec ( ) ; ! reflect . DeepEqual ( got , want ) {
t . Fatalf ( "ModuleSpec() = %#v, want %#v" , got , want )
}
registry := pipeline . NewNormalizerRegistry ( )
if err := Register ( registry ) ; err != nil {
t . Fatalf ( "Register() error = %v" , err )
}
normalizer := New ( Options { } )
if metadata := normalizer . ManifestMetadata ( ) ; metadata [ "normalizer_policy" ] != normalizerPolicy {
t . Fatalf ( "metadata = %#v" , metadata )
}
if got , want := normalizer . CheckpointFingerprints ( ) , [ ] pipeline . CheckpointFingerprint { { Name : "normalizer_policy" , Value : normalizerPolicy } } ; ! reflect . DeepEqual ( got , want ) {
t . Fatalf ( "fingerprints = %#v, want %#v" , got , want )
}
canceled , cancel := context . WithCancel ( context . Background ( ) )
cancel ( )
if _ , err := normalizer . Normalize ( canceled , normalizeRequest ( dnd . SceneDescriptionList { } , testDocument ( ) ) ) ; err == nil || ! strings . Contains ( err . Error ( ) , "context" ) {
t . Fatalf ( "canceled Normalize() error = %v" , err )
}
if _ , err := normalizer . Normalize ( context . Background ( ) , normalizeRequest ( dnd . SceneDescriptionList { Scenes : [ ] dnd . SceneDescription { scene ( "one" , 1 , 1 , dnd . SceneKindNarrative , "Arrival" , "The party arrives." ) } } , nil ) ) ; err == nil || ! strings . Contains ( err . Error ( ) , "source document" ) {
t . Fatalf ( "nil source Normalize() error = %v" , err )
}
}
func normalizeRequest ( value dnd . SceneDescriptionList , doc * source . SourceDocument ) contracts . TypedNormalizeRequest [ dnd . SceneDescriptionList ] {
return contracts . TypedNormalizeRequest [ dnd . SceneDescriptionList ] { Source : doc , MergeOutput : contracts . MergeArtifact [ dnd . SceneDescriptionList ] { Value : value } }
}
func testDocument ( ) * source . SourceDocument {
return & source . SourceDocument { ID : "session" , Units : [ ] source . SourceUnit { { ID : 1 , Text : "Rules are discussed." } , { ID : 2 , Text : "The party travels." } , { ID : 3 , Text : "The party returns." } } }
}
func scene ( id string , start , end int , kind dnd . SceneKind , title , summary string ) dnd . SceneDescription {
return dnd . SceneDescription { ID : id , SourceRef : source . SourceRef { SourceID : "session" , StartUnitID : start , EndUnitID : end } , Kind : kind , Title : title , Summary : summary }
}
func cloneList ( value dnd . SceneDescriptionList ) dnd . SceneDescriptionList {
if value . Scenes != nil {
value . Scenes = append ( [ ] dnd . SceneDescription ( nil ) , value . Scenes ... )
}
return value
}