2026-08-05 20:00:20 +00:00
package itemoccurrences
2026-07-25 21:49:24 +00:00
import (
"context"
2026-08-06 13:37:21 +00:00
"strings"
2026-07-25 21:49:24 +00:00
"testing"
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
2026-08-05 19:55:34 +00:00
itemidentity "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/items/identity"
2026-07-25 21:49:24 +00:00
)
2026-08-05 19:55:34 +00:00
func TestExtractGroundsOccurrencesInRequiredRegistry ( t * testing . T ) {
id := itemidentity . DeriveID ( "Torch" )
2026-08-26 23:59:05 +00:00
rawResponse := [ ] byte ( ` { "occurrences":[ { "name":"Torch","kind":"lost","from":"party","source_refs":[ { "start_unit_id":1,"end_unit_id":1}]}]} ` )
client := & fakeItemOccurrencesLLMClient { content : append ( [ ] byte ( nil ) , rawResponse ... ) }
2026-08-05 19:55:34 +00:00
req := extractionRequest ( )
req . References = itemRegistryReferences ( t )
2026-08-26 23:59:05 +00:00
correction , err := contracts . NewSemanticCorrection ( [ ] byte ( ` { "occurrences":[]} ` ) , "Keep the transcript-grounded item occurrence." )
if err != nil {
t . Fatalf ( "NewSemanticCorrection() error = %v" , err )
}
req . Correction = correction
2026-08-05 19:55:34 +00:00
result , err := newExtractor ( t , client , req . References ) . Extract ( context . Background ( ) , req )
2026-07-25 21:49:24 +00:00
if err != nil {
2026-08-05 19:55:34 +00:00
t . Fatal ( err )
2026-07-25 21:49:24 +00:00
}
2026-08-05 19:55:34 +00:00
if len ( result . Value . Occurrences ) != 1 || result . Value . Occurrences [ 0 ] . ItemID != id || result . Value . Occurrences [ 0 ] . Name != "Torch" {
t . Fatalf ( "occurrences = %#v" , result . Value . Occurrences )
2026-07-25 21:49:24 +00:00
}
2026-08-09 02:11:27 +00:00
if refs := result . Value . Occurrences [ 0 ] . SourceRefs ; len ( refs ) != 1 || refs [ 0 ] . SourceID != req . Source . ID || refs [ 0 ] . StartUnitID != 1 || refs [ 0 ] . EndUnitID != 1 {
t . Fatalf ( "occurrence evidence = %#v, want current-source unit range" , refs )
}
2026-08-26 23:59:05 +00:00
request := client . requests [ 0 ]
input := request . Inputs [ ItemRegistryReferenceSlot ]
2026-08-08 14:43:04 +00:00
if input . Name != ItemRegistryReferenceSlot || string ( input . Content ) != ` { "items":[ { "name":"Torch"}]} ` || strings . Contains ( string ( input . Content ) , "item:sha256:" ) {
t . Fatalf ( "registry prompt input = %#v, want names-only projection" , input )
2026-07-25 21:49:24 +00:00
}
2026-08-26 23:59:05 +00:00
if request . Correction == nil || string ( request . Correction . AssistantResponse ) != ` { "occurrences":[]} ` || request . Correction . UserGuidance != "Keep the transcript-grounded item occurrence." {
t . Fatalf ( "correction = %#v, want exact request correction" , request . Correction )
}
for _ , material := range [ ] string { string ( request . Correction . AssistantResponse ) , request . Correction . UserGuidance } {
if strings . Contains ( material , id ) || strings . Contains ( material , "item:sha256:" ) {
t . Fatalf ( "correction material leaked opaque item identity: %q" , material )
}
}
correction . AssistantResponse [ 0 ] = '['
if got := string ( request . Correction . AssistantResponse ) ; got != ` { "occurrences":[]} ` {
t . Fatalf ( "captured correction changed after caller mutation: %q" , got )
}
if result . ModelCandidate == nil || result . ModelCandidate . Protocol != contracts . CorrectionProtocolSingleResponseV1 || string ( result . ModelCandidate . Response ) != string ( rawResponse ) {
t . Fatalf ( "model candidate = %#v, want exact validated response" , result . ModelCandidate )
}
client . content [ 0 ] = '['
if got := string ( result . ModelCandidate . Response ) ; got != string ( rawResponse ) {
t . Fatalf ( "model candidate changed after provider buffer mutation: %q" , got )
}
2026-07-25 21:49:24 +00:00
}
2026-08-08 14:43:04 +00:00
func TestExtractCanonicalizesComparisonEquivalentNames ( t * testing . T ) {
2026-08-06 13:37:21 +00:00
id := itemidentity . DeriveID ( "Torch" )
2026-08-08 14:43:04 +00:00
client := & fakeItemOccurrencesLLMClient { response : extractionResponse { Occurrences : [ ] itemOccurrenceResponse {
{ Name : " tORCH " , Kind : "lost" , From : "party" , SourceRefs : responseRefs ( 1 , 1 ) } ,
} } }
req := extractionRequest ( )
req . References = itemRegistryReferences ( t )
result , err := newExtractor ( t , client , req . References ) . Extract ( context . Background ( ) , req )
if err != nil {
t . Fatal ( err )
}
occurrence := result . Value . Occurrences [ 0 ]
if occurrence . ItemID != id || occurrence . Name != "Torch" {
t . Fatalf ( "canonical occurrence = %#v" , occurrence )
2026-08-06 13:37:21 +00:00
}
}
2026-08-08 14:43:04 +00:00
func TestExtractRejectsUnknownNameAfterValidOccurrence ( t * testing . T ) {
2026-08-06 13:37:21 +00:00
client := & fakeItemOccurrencesLLMClient { response : extractionResponse { Occurrences : [ ] itemOccurrenceResponse {
2026-08-08 14:43:04 +00:00
{ Name : "Torch" , Kind : "lost" , From : "party" , SourceRefs : responseRefs ( 1 , 1 ) } ,
{ Name : "Unknown" , Kind : "lost" , From : "party" , SourceRefs : responseRefs ( 2 , 2 ) } ,
2026-08-06 13:37:21 +00:00
} } }
req := extractionRequest ( )
req . References = itemRegistryReferences ( t )
result , err := newExtractor ( t , client , req . References ) . Extract ( context . Background ( ) , req )
2026-08-08 14:43:04 +00:00
if err == nil || ! strings . Contains ( err . Error ( ) , "occurrences[1].name is not in the item registry" ) {
t . Fatalf ( "Extract() error = %v, want occurrence index and unknown name" , err )
2026-08-06 13:37:21 +00:00
}
if len ( result . Value . Occurrences ) != 0 {
t . Fatalf ( "Extract() returned partial occurrences: %#v" , result . Value . Occurrences )
}
}
2026-08-05 19:55:34 +00:00
func TestExtractRequiresItemRegistry ( t * testing . T ) {
_ , err := newExtractor ( t , & fakeItemOccurrencesLLMClient { } ) . Extract ( context . Background ( ) , extractionRequest ( ) )
if err == nil {
t . Fatal ( "Extract() error = nil, want required registry error" )
2026-08-04 19:52:47 +00:00
}
}
2026-08-08 14:43:04 +00:00
func TestExtractAcceptsOnlyEmptyResponseForEmptyRegistry ( t * testing . T ) {
references := emptyItemRegistryReferences ( t )
for _ , test := range [ ] struct {
name string
response extractionResponse
wantErr bool
} {
{ name : "empty" , response : extractionResponse { Occurrences : [ ] itemOccurrenceResponse { } } } ,
{ name : "selection" , response : extractionResponse { Occurrences : [ ] itemOccurrenceResponse { { Name : "Torch" , Kind : "lost" , From : "party" , SourceRefs : responseRefs ( 1 , 1 ) } } } , wantErr : true } ,
} {
t . Run ( test . name , func ( t * testing . T ) {
client := & fakeItemOccurrencesLLMClient { response : test . response }
req := extractionRequest ( )
req . References = references
result , err := newExtractor ( t , client , references ) . Extract ( context . Background ( ) , req )
if test . wantErr {
if err == nil || ! strings . Contains ( err . Error ( ) , "name is not in the item registry" ) || len ( result . Value . Occurrences ) != 0 {
t . Fatalf ( "Extract() = %#v, %v; want no accepted occurrences" , result , err )
}
return
}
if err != nil || result . Value . Occurrences == nil || len ( result . Value . Occurrences ) != 0 {
t . Fatalf ( "Extract() = %#v, %v; want present empty occurrences" , result , err )
}
} )
}
}
2026-08-05 19:55:34 +00:00
func TestExtractPreservesNullableFields ( t * testing . T ) {
2026-08-09 02:11:27 +00:00
client := & fakeItemOccurrencesLLMClient { content : [ ] byte ( ` { "occurrences":[ { "name":"Torch","kind":"discovered","quantity":null,"from":null,"to":null,"source_refs":[ { "start_unit_id":1,"end_unit_id":1}]}]} ` ) }
2026-07-25 21:49:24 +00:00
req := extractionRequest ( )
2026-08-05 19:55:34 +00:00
req . References = itemRegistryReferences ( t )
result , err := newExtractor ( t , client , req . References ) . Extract ( context . Background ( ) , req )
if err != nil || len ( result . Value . Occurrences ) != 1 || result . Value . Occurrences [ 0 ] . Quantity != nil || result . Value . Occurrences [ 0 ] . From != "" || result . Value . Occurrences [ 0 ] . To != "" {
t . Fatalf ( "result = %#v, %v" , result , err )
2026-07-25 21:49:24 +00:00
}
}
func TestExtractUsesOnlySupportedPromptInputs ( t * testing . T ) {
2026-08-05 19:55:34 +00:00
client := & fakeItemOccurrencesLLMClient { response : extractionResponse { Occurrences : [ ] itemOccurrenceResponse { } } }
2026-07-25 21:49:24 +00:00
req := extractionRequest ( )
2026-08-05 19:55:34 +00:00
req . References = itemRegistryReferences ( t )
req . References . Slots [ "unrelated" ] = contracts . ResolvedReferenceSlot { Slot : contracts . ReferenceSlot { Name : "unrelated" } , Items : [ ] contracts . ReferenceItem { { SlotName : "unrelated" , Content : [ ] byte ( "ignored" ) } } }
if _ , err := newExtractor ( t , client , req . References ) . Extract ( context . Background ( ) , req ) ; err != nil {
2026-07-25 21:49:24 +00:00
t . Fatal ( err )
}
2026-08-05 19:55:34 +00:00
if _ , ok := client . requests [ 0 ] . Inputs [ "unrelated" ] ; ok {
t . Fatalf ( "unexpected prompt input: %#v" , client . requests [ 0 ] . Inputs )
2026-07-25 21:49:24 +00:00
}
}