2026-07-21 02:24:26 +00:00
package npcs
import (
"reflect"
"strings"
"testing"
"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 TestNewRequiresLLMClientAndRejectsAmbiguousReferences ( t * testing . T ) {
if _ , err := New ( nil , Options { } ) ; err == nil || ! strings . Contains ( err . Error ( ) , "LLM client" ) {
t . Fatalf ( "New(nil) error = %v, want dependency error" , err )
}
if _ , err := New ( & fakeNPCsLLMClient { } , Options { } , contracts . ReferenceSet { } , contracts . ReferenceSet { } ) ; err == nil || ! strings . Contains ( err . Error ( ) , "at most one reference set" ) {
t . Fatalf ( "New() error = %v, want reference-set error" , err )
}
if got := newExtractor ( t , & fakeNPCsLLMClient { } ) . Key ( ) ; got != Key {
t . Fatalf ( "Key() = %q, want %q" , got , Key )
}
}
func TestModuleSpecAndReferenceSlots ( t * testing . T ) {
got := ModuleSpec ( )
want := pipeline . ModuleSpec {
2026-08-03 16:49:51 +00:00
Key : Key ,
Stage : pipeline . StageExtract ,
ExecutionClass : contracts . ExecutionClassLLMBacked ,
Requires : [ ] string { "chunks" , "source.transcript" } ,
Provides : [ ] string { "dnd.npcs" } ,
ArtifactKind : dnd . NPCListKind ,
2026-07-21 02:24:26 +00:00
ReferenceSlots : [ ] contracts . ReferenceSlot {
{ Name : "glossary" , Description : "Optional campaign glossary reference material used only for NPC disambiguation." , AcceptedMediaTypes : [ ] string { "application/json" , "application/x-yaml" , "application/yaml" , "text/markdown" , "text/plain" } } ,
{ Name : "party" , Description : "Optional party roster reference material used only for NPC disambiguation." , AcceptedMediaTypes : [ ] string { "application/json" , "application/x-yaml" , "application/yaml" , "text/markdown" , "text/plain" } } ,
{ Name : "players" , Description : "Optional player list reference material used only for NPC disambiguation." , AcceptedMediaTypes : [ ] string { "application/json" , "application/x-yaml" , "application/yaml" , "text/markdown" , "text/plain" } } ,
{ Name : "roster" , Description : "Deprecated alias for party roster reference material used only for NPC disambiguation." , AcceptedMediaTypes : [ ] string { "application/json" , "application/x-yaml" , "application/yaml" , "text/markdown" , "text/plain" } } ,
} ,
}
if ! reflect . DeepEqual ( got , want ) {
t . Fatalf ( "ModuleSpec() = %#v, want %#v" , got , want )
}
got . Requires [ 0 ] = "changed"
got . Provides [ 0 ] = "changed"
got . ReferenceSlots [ 0 ] . AcceptedMediaTypes [ 0 ] = "changed"
if ! reflect . DeepEqual ( ModuleSpec ( ) , want ) {
t . Fatal ( "ModuleSpec() returned mutable shared slices" )
}
extractor := newExtractor ( t , & fakeNPCsLLMClient { } )
if ! reflect . DeepEqual ( extractor . ReferenceSlots ( ) , want . ReferenceSlots ) {
t . Fatalf ( "ReferenceSlots() = %#v, want %#v" , extractor . ReferenceSlots ( ) , want . ReferenceSlots )
}
}
func TestRegisterStoresTypedModuleSpec ( t * testing . T ) {
registry := pipeline . NewExtractorRegistry ( )
if err := Register ( registry ) ; err != nil {
t . Fatalf ( "Register() error = %v, want nil" , err )
}
got , ok := registry . Spec ( Key )
if ! ok || ! reflect . DeepEqual ( got , ModuleSpec ( ) ) {
t . Fatalf ( "registry.Spec(%q) = %#v, present = %t" , Key , got , ok )
}
if err := Register ( nil ) ; err == nil || ! strings . Contains ( err . Error ( ) , "extractor registry" ) {
t . Fatalf ( "Register(nil) error = %v, want registry error" , err )
}
if _ , err := DecodeOptions ( map [ string ] any { "unexpected" : true } ) ; err == nil || ! strings . Contains ( err . Error ( ) , "unknown option" ) {
t . Fatalf ( "DecodeOptions() error = %v, want strict options error" , err )
}
}
func TestExtractorMetadataAndCheckpointIdentity ( t * testing . T ) {
extractor := newExtractor ( t , & fakeNPCsLLMClient { } )
metadata := extractor . ManifestMetadata ( )
for key , want := range map [ string ] string {
"prompt_id" : PromptID , "prompt_version" : SchemaVersion ,
"response_schema_key" : string ( ResponseSchemaKey ) , "response_schema_id" : ResponseSchemaID ,
"response_schema_name" : ResponseSchemaName , "response_schema_version" : SchemaVersion ,
"identity_policy" : "dnd.npcs.identity.v1" ,
2026-07-24 14:23:35 +00:00
"mapping_policy" : mappingPolicy ,
2026-07-21 02:24:26 +00:00
} {
if metadata [ key ] != want {
t . Fatalf ( "metadata[%q] = %#v, want %q" , key , metadata [ key ] , want )
}
}
for _ , key := range [ ] string { "prompt_sha256" , "response_schema_sha256" } {
if value , ok := metadata [ key ] . ( string ) ; ! ok || ! strings . HasPrefix ( value , "sha256:" ) {
t . Fatalf ( "metadata[%q] = %#v, want hash" , key , metadata [ key ] )
}
}
fingerprints := extractor . CheckpointFingerprints ( )
2026-07-24 14:23:35 +00:00
want := map [ string ] string { "prompt" : metadata [ "prompt_sha256" ] . ( string ) , "response_schema" : metadata [ "response_schema_sha256" ] . ( string ) , "identity_policy" : "dnd.npcs.identity.v1" , "mapping_policy" : mappingPolicy }
2026-07-21 02:24:26 +00:00
if len ( fingerprints ) != len ( want ) {
t . Fatalf ( "CheckpointFingerprints() = %#v, want %d entries" , fingerprints , len ( want ) )
}
for _ , fingerprint := range fingerprints {
if fingerprint . Value != want [ fingerprint . Name ] {
t . Fatalf ( "fingerprint %q = %q, want %#v" , fingerprint . Name , fingerprint . Value , want )
}
}
}