2026-07-23 20:17:18 +00:00
package scenedescriptions
import (
"context"
2026-08-09 02:22:23 +00:00
"fmt"
2026-07-23 20:17:18 +00:00
"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 )
}
2026-08-27 16:40:17 +00:00
if len ( result . Diagnostics ) != 3 || result . Diagnostics [ 0 ] . ReasonCode != ReasonCodeProseNormalized || result . Diagnostics [ 0 ] . OccurrenceCount != 4 || result . Diagnostics [ 1 ] . ReasonCode != ReasonCodeOrderNormalized || result . Diagnostics [ 1 ] . OccurrenceCount != 2 || result . Diagnostics [ 2 ] . ReasonCode != ReasonCodeDuplicateCollapsed || result . Diagnostics [ 2 ] . OccurrenceCount != 1 {
2026-08-27 16:04:32 +00:00
t . Fatalf ( "diagnostics = %#v, want grouped prose, order, and duplicate observations" , result . Diagnostics )
2026-08-09 02:22:23 +00:00
}
2026-07-23 20:17:18 +00:00
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" )
}
2026-08-09 02:22:23 +00:00
second , err := New ( Options { } ) . Normalize ( context . Background ( ) , normalizeRequest ( dnd . SceneDescriptionList { Scenes : want } , doc ) )
if err != nil {
t . Fatalf ( "second Normalize() error = %v" , err )
}
2026-08-27 16:04:32 +00:00
if ! reflect . DeepEqual ( second . Value . Scenes , want ) || len ( second . Diagnostics ) != 0 {
t . Fatalf ( "second normalization = %#v, want unchanged value without diagnostics" , second )
2026-08-09 02:22:23 +00:00
}
}
2026-08-27 18:41:59 +00:00
func TestNormalizeLimitsCombinedSceneMutationFindings ( t * testing . T ) {
2026-08-27 16:40:17 +00:00
count := contracts . MaxDiagnosticSamples + 1
2026-08-09 02:22:23 +00:00
doc := & source . SourceDocument { ID : "session" , Units : make ( [ ] source . SourceUnit , count ) }
input := dnd . SceneDescriptionList { Scenes : make ( [ ] dnd . SceneDescription , count ) }
for index := range input . Scenes {
unitID := count - index
doc . Units [ index ] = source . SourceUnit { ID : index + 1 }
input . Scenes [ index ] = scene ( fmt . Sprintf ( "scene-%02d" , unitID ) , unitID , unitID , 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 len ( result . Value . Scenes ) != count || result . Value . Scenes [ 0 ] . SourceRef . StartUnitID != 1 {
t . Fatalf ( "normalized scenes = %#v, want unchanged canonical values" , result . Value . Scenes )
}
2026-08-27 16:40:17 +00:00
if len ( result . Diagnostics ) != 2 || result . Diagnostics [ 0 ] . ReasonCode != ReasonCodeProseNormalized || result . Diagnostics [ 0 ] . Disposition != contracts . DiagnosticDispositionObservation || result . Diagnostics [ 0 ] . OccurrenceCount != count || len ( result . Diagnostics [ 0 ] . Samples ) != contracts . MaxDiagnosticSamples || result . Diagnostics [ 1 ] . ReasonCode != ReasonCodeOrderNormalized || result . Diagnostics [ 1 ] . OccurrenceCount != count {
2026-08-27 16:04:32 +00:00
t . Fatalf ( "diagnostics = %#v" , result . Diagnostics )
2026-08-09 02:22:23 +00:00
}
2026-07-23 20:17:18 +00:00
}
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
}