2026-07-21 02:24:26 +00:00
package npcs
import (
"context"
"errors"
"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/modules/dnd"
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/npcs/identity"
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared"
)
func TestExtractReturnsCanonicalNPCListFromPrivateResponse ( t * testing . T ) {
client := & fakeNPCsLLMClient { response : extractionResponse { NPCs : [ ] npcResponse {
{
Name : "Captain Vale" , Aliases : [ ] string { "The Captain" } , Description : "A road captain." ,
Relationships : [ ] npcRelationshipResponse { { Target : "Mira Thorn" , Relationship : "reports to" } } ,
SourceRefs : responseSourceRefs ( 3 , 3 ) ,
} ,
{
Name : "Mira Thorn" , Aliases : [ ] string { "The Greencloak" } , Description : "A guarded ranger." ,
Relationships : [ ] npcRelationshipResponse { { Target : "Captain Vale" , Relationship : "commands" } } ,
SourceRefs : [ ] npcSourceRefResponse {
{ StartUnitID : sharedUnitRef ( 2 ) , EndUnitID : sharedUnitRef ( 2 ) } ,
{ StartUnitID : sharedUnitRef ( 1 ) , EndUnitID : sharedUnitRef ( 2 ) } ,
{ StartUnitID : sharedUnitRef ( 1 ) , EndUnitID : sharedUnitRef ( 2 ) } ,
} ,
} ,
} } }
result , err := newExtractor ( t , client ) . Extract ( context . Background ( ) , extractionRequest ( ) )
if err != nil {
t . Fatalf ( "Extract() error = %v, want nil" , err )
}
want := dnd . NPCList { NPCs : [ ] dnd . NPC {
{ ID : identity . DeriveID ( "Mira Thorn" ) , Name : "Mira Thorn" , Aliases : [ ] string { "The Greencloak" } , Description : "A guarded ranger." , Relationships : [ ] dnd . NPCRelationship { { Target : "Captain Vale" , Relationship : "commands" } } , SourceRefs : [ ] source . SourceRef { { SourceID : "session-alpha" , StartUnitID : 1 , EndUnitID : 2 } , { SourceID : "session-alpha" , StartUnitID : 2 , EndUnitID : 2 } } } ,
{ ID : identity . DeriveID ( "Captain Vale" ) , Name : "Captain Vale" , Aliases : [ ] string { "The Captain" } , Description : "A road captain." , Relationships : [ ] dnd . NPCRelationship { { Target : "Mira Thorn" , Relationship : "reports to" } } , SourceRefs : [ ] source . SourceRef { { SourceID : "session-alpha" , StartUnitID : 3 , EndUnitID : 3 } } } ,
} }
if ! reflect . DeepEqual ( result . Value , want ) {
t . Fatalf ( "Value = %#v, want %#v" , result . Value , want )
}
if len ( client . requests ) != 1 {
t . Fatalf ( "LLM calls = %d, want 1" , len ( client . requests ) )
}
request := client . requests [ 0 ]
if request . StageName != Key || request . PromptID != PromptID || request . PromptVersion != SchemaVersion || request . ProfileID != "profile-npcs" || request . SessionID != "session-123" {
t . Fatalf ( "LLM request identity = %#v" , request )
}
transcript := request . Inputs [ "transcript" ]
if transcript . Name != "transcript" || transcript . MediaType != "application/json" || transcript . Digest != "sha256:chunk" || transcript . OriginURI != "file:///session-alpha.json" || string ( transcript . Content ) != string ( extractionRequest ( ) . Chunk . Content ) {
t . Fatalf ( "transcript input = %#v, want chunk-scoped material" , transcript )
}
}
2026-07-20 23:10:25 -05:00
func TestExtractOrdersNPCsBySourcePositionRatherThanUnitID ( t * testing . T ) {
client := & fakeNPCsLLMClient { response : extractionResponse { NPCs : [ ] npcResponse {
{
Name : "Later NPC" , Aliases : [ ] string { } , Description : "Appears later." , Relationships : [ ] npcRelationshipResponse { } ,
SourceRefs : responseSourceRefs ( 10 , 10 ) ,
} ,
{
Name : "Earlier NPC" , Aliases : [ ] string { } , Description : "Appears first." , Relationships : [ ] npcRelationshipResponse { } ,
SourceRefs : [ ] npcSourceRefResponse {
{ StartUnitID : sharedUnitRef ( 50 ) , EndUnitID : sharedUnitRef ( 50 ) } ,
{ StartUnitID : sharedUnitRef ( 100 ) , EndUnitID : sharedUnitRef ( 100 ) } ,
} ,
} ,
} } }
req := extractionRequest ( )
req . Source . Units = [ ] source . SourceUnit {
{ ID : 100 , Kind : "transcript_segment" , Text : "Earlier NPC appears." } ,
{ ID : 10 , Kind : "transcript_segment" , Text : "Later NPC appears." } ,
{ ID : 50 , Kind : "transcript_segment" , Text : "Earlier NPC appears again." } ,
}
req . Chunk . Units = append ( [ ] source . SourceUnit ( nil ) , req . Source . Units ... )
req . Chunk . Ref = source . SourceRef { SourceID : req . Source . ID , StartUnitID : 100 , EndUnitID : 50 }
result , err := newExtractor ( t , client ) . Extract ( context . Background ( ) , req )
if err != nil {
t . Fatalf ( "Extract() error = %v, want nil" , err )
}
if len ( result . Value . NPCs ) != 2 || result . Value . NPCs [ 0 ] . Name != "Earlier NPC" || result . Value . NPCs [ 1 ] . Name != "Later NPC" {
t . Fatalf ( "NPC order = %#v, want source-document order" , result . Value . NPCs )
}
}
2026-07-21 02:24:26 +00:00
func TestExtractPassesCampaignReferencesAsPromptInputs ( t * testing . T ) {
client := & fakeNPCsLLMClient { response : extractionResponse { NPCs : [ ] npcResponse { } } }
req := extractionRequest ( )
req . References = contracts . ReferenceSet { Slots : map [ string ] contracts . ResolvedReferenceSlot {
"players" : { Slot : contracts . ReferenceSlot { Name : "players" } , Items : [ ] contracts . ReferenceItem { { SlotName : "players" , Content : [ ] byte ( "Dana: Mira" ) } } } ,
"party" : { Slot : contracts . ReferenceSlot { Name : "party" } , Items : [ ] contracts . ReferenceItem { { SlotName : "party" , Content : [ ] byte ( "Mira: ranger" ) } } } ,
"glossary" : { Slot : contracts . ReferenceSlot { Name : "glossary" } , Items : [ ] contracts . ReferenceItem { { SlotName : "glossary" , Content : [ ] byte ( "Greencloak: local title" ) } } } ,
} }
if _ , err := newExtractor ( t , client ) . Extract ( context . Background ( ) , req ) ; err != nil {
t . Fatalf ( "Extract() error = %v, want nil" , err )
}
inputs := client . requests [ 0 ] . Inputs
if string ( inputs [ "players" ] . Content ) != "Dana: Mira" || string ( inputs [ "party" ] . Content ) != "Mira: ranger" || string ( inputs [ "glossary" ] . Content ) != "Greencloak: local title" {
t . Fatalf ( "reference inputs = %#v" , inputs )
}
if strings . Contains ( string ( inputs [ "transcript" ] . Content ) , "local title" ) {
t . Fatal ( "transcript input contains reference content" )
}
}
func TestExtractPreservesMalformedCandidatesForValidators ( t * testing . T ) {
client := & fakeNPCsLLMClient { response : extractionResponse { NPCs : [ ] npcResponse { {
Name : "" , Aliases : nil , Description : "" , Relationships : nil ,
SourceRefs : [ ] npcSourceRefResponse { { StartUnitID : sharedUnitRef ( 99 ) , EndUnitID : shared . UnitRefFromString ( "missing" ) } , { StartUnitID : sharedUnitRef ( 99 ) , EndUnitID : shared . UnitRefFromInt ( 0 ) } } ,
} } } }
result , err := newExtractor ( t , client ) . Extract ( context . Background ( ) , extractionRequest ( ) )
if err != nil {
t . Fatalf ( "Extract() error = %v, want nil" , err )
}
if len ( result . Value . NPCs ) != 1 || result . Value . NPCs [ 0 ] . ID != "" || result . Value . NPCs [ 0 ] . Name != "" || result . Value . NPCs [ 0 ] . Aliases != nil || result . Value . NPCs [ 0 ] . Relationships != nil {
t . Fatalf ( "malformed candidate = %#v, want invalid values preserved" , result . Value )
}
if refs := result . Value . NPCs [ 0 ] . SourceRefs ; len ( refs ) != 1 || refs [ 0 ] . SourceID != "session-alpha" || refs [ 0 ] . StartUnitID != 99 || refs [ 0 ] . EndUnitID != 0 {
t . Fatalf ( "malformed source refs = %#v, want invalid range preserved after exact deduplication" , refs )
}
}
func TestExtractHandlesCancellationAndProviderErrors ( t * testing . T ) {
request := extractionRequest ( )
extractor := newExtractor ( t , & fakeNPCsLLMClient { response : extractionResponse { NPCs : [ ] npcResponse { } } } )
canceled , cancel := context . WithCancel ( context . Background ( ) )
cancel ( )
if _ , err := extractor . Extract ( canceled , request ) ; err == nil || ! strings . Contains ( err . Error ( ) , "context" ) {
t . Fatalf ( "canceled Extract() error = %v, want context error" , err )
}
_ , err := newExtractor ( t , & fakeNPCsLLMClient { err : errors . New ( "provider unavailable" ) } ) . Extract ( context . Background ( ) , request )
if err == nil || ! strings . Contains ( err . Error ( ) , "dnd npcs" ) || ! strings . Contains ( err . Error ( ) , "provider unavailable" ) {
t . Fatalf ( "provider Extract() error = %v, want contextual provider error" , err )
}
}
func sharedUnitRef ( value int ) shared . UnitRef { return shared . UnitRefFromInt ( value ) }