2026-07-25 21:49:24 +00:00
package itemevents
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"
)
func TestExtractMapsAllEventKindsAndCanonicalizesEvidence ( t * testing . T ) {
quantity := 20
client := & fakeItemEventsLLMClient { response : extractionResponse { Events : [ ] itemEventResponse {
{ Name : "Torch" , Kind : "lost" , From : "party" , SourceRefs : responseRefs ( 5 , 5 ) } ,
{ Name : "Moonblade" , Kind : "transferred" , From : "Aria" , To : "Borin" , SourceRefs : responseRefs ( 3 , 3 ) } ,
{ Name : "Hidden Cache" , Kind : "discovered" , SourceRefs : [ ] itemEventSourceRefResponse { { StartSegment : 1 , EndSegment : 1 } , { StartSegment : 1 , EndSegment : 1 } } } ,
{ Name : "Gold Pieces" , Kind : "acquired" , Quantity : & quantity , To : "party" , SourceRefs : responseRefs ( 2 , 2 ) } ,
{ Name : "Healing Potion" , Kind : "consumed" , From : "party" , SourceRefs : responseRefs ( 4 , 4 ) } ,
} } }
result , err := newExtractor ( t , client ) . Extract ( context . Background ( ) , extractionRequest ( ) )
if err != nil {
t . Fatalf ( "Extract() error = %v" , err )
}
if got := [ ] dnd . ItemEventKind { result . Value . Events [ 0 ] . Kind , result . Value . Events [ 1 ] . Kind , result . Value . Events [ 2 ] . Kind , result . Value . Events [ 3 ] . Kind , result . Value . Events [ 4 ] . Kind } ; ! reflect . DeepEqual ( got , [ ] dnd . ItemEventKind {
dnd . ItemEventKindDiscovered , dnd . ItemEventKindAcquired , dnd . ItemEventKindTransferred , dnd . ItemEventKindConsumed , dnd . ItemEventKindLost ,
} ) {
t . Fatalf ( "event kinds = %#v, want source order" , got )
}
acquired := result . Value . Events [ 1 ]
if acquired . Name != "Gold Pieces" || acquired . To != "party" || acquired . Quantity == nil || * acquired . Quantity != 20 {
t . Fatalf ( "acquired event = %#v" , acquired )
}
if result . Value . Events [ 0 ] . Quantity != nil || result . Value . Events [ 0 ] . From != "" || result . Value . Events [ 0 ] . To != "" {
t . Fatalf ( "discovered event = %#v, want omitted optional fields" , result . Value . Events [ 0 ] )
}
if refs := result . Value . Events [ 0 ] . SourceRefs ; len ( refs ) != 1 || refs [ 0 ] != ( source . SourceRef { SourceID : "session-alpha" , StartUnitID : 1 , EndUnitID : 1 } ) {
t . Fatalf ( "source refs = %#v, want source ID attachment and deduplication" , refs )
}
if acquired . Quantity == client . response . Events [ 3 ] . Quantity {
t . Fatal ( "mapped quantity aliases private response" )
}
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-item-events" || request . SessionID != "session-123" {
t . Fatalf ( "LLM request identity = %#v" , request )
}
if transcript := request . Inputs [ "transcript" ] ; string ( transcript . Content ) != string ( extractionRequest ( ) . Chunk . Content ) || transcript . Name != "transcript" {
t . Fatalf ( "transcript input = %#v" , transcript )
}
for _ , name := range [ ] string { "players" , "party" , "glossary" } {
if input := request . Inputs [ name ] ; string ( input . Content ) != " " {
t . Fatalf ( "absent %s input = %#v, want retained empty prompt material" , name , input )
}
}
}
func TestExtractPreservesInvalidCandidatesAndEmptyResults ( t * testing . T ) {
client := & fakeItemEventsLLMClient { content : [ ] byte ( ` { "events":[ { "name":"","kind":"transferred","quantity":0,"from":"party","to":"Party","source_refs":[ { "start_segment":99,"end_segment":-1}]}]} ` ) }
result , err := newExtractor ( t , client ) . Extract ( context . Background ( ) , extractionRequest ( ) )
if err != nil {
t . Fatalf ( "Extract() error = %v" , err )
}
event := result . Value . Events [ 0 ]
if event . Name != "" || event . Kind != "transferred" || event . Quantity == nil || * event . Quantity != 0 || event . From != "party" || event . To != "Party" {
t . Fatalf ( "invalid candidate = %#v, want values preserved" , event )
}
if refs := event . SourceRefs ; len ( refs ) != 1 || refs [ 0 ] != ( source . SourceRef { SourceID : "session-alpha" , StartUnitID : 99 , EndUnitID : - 1 } ) {
t . Fatalf ( "source refs = %#v, want invalid range preserved" , refs )
}
empty , err := newExtractor ( t , & fakeItemEventsLLMClient { response : extractionResponse { Events : [ ] itemEventResponse { } } } ) . Extract ( context . Background ( ) , extractionRequest ( ) )
if err != nil || empty . Value . Events == nil || len ( empty . Value . Events ) != 0 {
t . Fatalf ( "empty provider result = %#v, %v; want valid empty list" , empty . Value , err )
}
}
2026-08-04 19:52:47 +00:00
func TestExtractMapsNullableResponseFieldsToAbsentArtifactFields ( t * testing . T ) {
client := & fakeItemEventsLLMClient { content : [ ] byte ( ` { "events":[ { "name":"Hidden Cache","kind":"discovered","quantity":null,"from":null,"to":null,"source_refs":[ { "start_segment":1,"end_segment":1}]}]} ` ) }
result , err := newExtractor ( t , client ) . Extract ( context . Background ( ) , extractionRequest ( ) )
if err != nil {
t . Fatal ( err )
}
if len ( result . Value . Events ) != 1 {
t . Fatalf ( "events = %#v, want one" , result . Value . Events )
}
event := result . Value . Events [ 0 ]
if event . Quantity != nil || event . From != "" || event . To != "" {
t . Fatalf ( "nullable response fields mapped to artifact values: %#v" , event )
}
}
2026-07-25 21:49:24 +00:00
func TestExtractUsesSourceDocumentOrderForCandidates ( t * testing . T ) {
client := & fakeItemEventsLLMClient { response : extractionResponse { Events : [ ] itemEventResponse {
{ Name : "Later" , Kind : "discovered" , SourceRefs : responseRefs ( 10 , 10 ) } ,
{ Name : "Earlier" , Kind : "discovered" , SourceRefs : responseRefs ( 100 , 100 ) } ,
} } }
req := extractionRequest ( )
req . Source . Units = [ ] source . SourceUnit { { ID : 100 } , { ID : 10 } }
req . Chunk . Units = append ( [ ] source . SourceUnit ( nil ) , req . Source . Units ... )
req . Chunk . Ref = source . SourceRef { SourceID : req . Source . ID , StartUnitID : 100 , EndUnitID : 10 }
result , err := newExtractor ( t , client ) . Extract ( context . Background ( ) , req )
if err != nil {
t . Fatal ( err )
}
if got := [ ] string { result . Value . Events [ 0 ] . Name , result . Value . Events [ 1 ] . Name } ; ! reflect . DeepEqual ( got , [ ] string { "Earlier" , "Later" } ) {
t . Fatalf ( "event order = %#v, want source document order" , got )
}
}
func TestExtractUsesOnlySupportedPromptInputs ( t * testing . T ) {
client := & fakeItemEventsLLMClient { response : extractionResponse { Events : [ ] itemEventResponse { } } }
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: Aria" ) } } } ,
"party" : { Slot : contracts . ReferenceSlot { Name : "party" } , Items : [ ] contracts . ReferenceItem { { SlotName : "party" , Content : [ ] byte ( "Aria: ranger" ) } } } ,
"glossary" : { Slot : contracts . ReferenceSlot { Name : "glossary" } , Items : [ ] contracts . ReferenceItem { { SlotName : "glossary" , Content : [ ] byte ( "Moonblade: heirloom" ) } } } ,
2026-08-05 18:38:22 +00:00
"npcs" : { Slot : contracts . ReferenceSlot { Name : "npc_registry" } , Items : [ ] contracts . ReferenceItem { { SlotName : "npc_registry" , Content : [ ] byte ( "must not be used" ) } } } ,
2026-07-25 21:49:24 +00:00
} }
if _ , err := newExtractor ( t , client ) . Extract ( context . Background ( ) , req ) ; err != nil {
t . Fatal ( err )
}
inputs := client . requests [ 0 ] . Inputs
if string ( inputs [ "players" ] . Content ) != "Dana: Aria" || string ( inputs [ "party" ] . Content ) != "Aria: ranger" || string ( inputs [ "glossary" ] . Content ) != "Moonblade: heirloom" {
t . Fatalf ( "reference inputs = %#v" , inputs )
}
if _ , ok := inputs [ "npcs" ] ; ok {
t . Fatalf ( "unexpected generated-lane input: %#v" , inputs )
}
}
func TestExtractRetainsLocalErrorContext ( t * testing . T ) {
request := extractionRequest ( )
extractor := newExtractor ( t , & fakeItemEventsLLMClient { } )
var nilExtractor * Extractor
request . SourceInput = contracts . NewLLMInputMaterial ( "source" , "application/json" , [ ] byte ( ` { "different":true} ` ) , "sha256:other" , "file:///other.json" )
for _ , test := range [ ] struct {
name string
extractor * Extractor
req contracts . TypedExtractionRequest
want string
} {
{ "nil extractor" , nilExtractor , extractionRequest ( ) , "extractor" } ,
{ "nil client" , & Extractor { } , extractionRequest ( ) , "LLM client" } ,
{ "preflight" , extractor , request , "must match chunk" } ,
} {
t . Run ( test . name , func ( t * testing . T ) {
if _ , err := test . extractor . Extract ( context . Background ( ) , test . req ) ; err == nil || ! strings . Contains ( err . Error ( ) , "dnd item events" ) || ! strings . Contains ( err . Error ( ) , test . want ) {
t . Fatalf ( "Extract() error = %v" , err )
}
} )
}
if _ , err := newExtractor ( t , & fakeItemEventsLLMClient { err : errors . New ( "provider unavailable" ) } ) . Extract ( context . Background ( ) , extractionRequest ( ) ) ; err == nil || ! strings . Contains ( err . Error ( ) , "provider unavailable" ) {
t . Fatalf ( "provider error = %v" , err )
}
}