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-28 14:37:22 +00:00
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd"
2026-08-05 19:55:34 +00:00
itemidentity "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/items/identity"
2026-08-28 14:37:22 +00:00
itemoccurrenceshape "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/itemoccurrences/shape"
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-28 13:45:21 +00:00
rawResponse := [ ] byte ( ` { "occurrences":[ { "name":"Torch","kind":"lost","from":"party","source_refs":[ { "start_unit_id":2,"end_unit_id":1}]}]} ` )
2026-08-26 23:59:05 +00:00
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-28 13:45:21 +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 != 2 {
2026-08-09 02:11:27 +00:00
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
}
}
2026-08-28 14:37:22 +00:00
func TestExtractCarriesRejectedHolderGuidanceIntoCorrectedAttempt ( t * testing . T ) {
references := itemRegistryReferences ( t )
req := extractionRequest ( )
req . References = references
defectiveResponse := [ ] byte ( ` { "occurrences":[ { "name":"Torch","kind":"acquired","quantity":null,"from":"Chest","to":"party","source_refs":[ { "start_unit_id":2,"end_unit_id":2}]}]} ` )
defective , err := newExtractor ( t , & fakeItemOccurrencesLLMClient { content : defectiveResponse } , references ) . Extract ( context . Background ( ) , req )
if err != nil {
t . Fatal ( err )
}
validation , err := itemoccurrenceshape . New ( itemoccurrenceshape . Options { } ) . Validate ( context . Background ( ) , contracts . TypedValidationRequest [ dnd . ItemOccurrenceList ] { Value : defective . Value } )
if err != nil || validation . Approved {
t . Fatalf ( "holder validation = %#v, %v; want rejection" , validation , err )
}
for _ , fragment := range [ ] string { "For `acquired` occurrences" , "`from` to JSON null" , "item \"Torch\"" , "source unit 2" } {
if ! strings . Contains ( validation . CorrectionGuidance , fragment ) {
t . Fatalf ( "CorrectionGuidance = %q, want %q" , validation . CorrectionGuidance , fragment )
}
}
for _ , forbidden := range [ ] string { defective . Value . Occurrences [ 0 ] . ItemID , "item_id" , "sha256" , itemoccurrenceshape . Key , itemoccurrenceshape . ReasonCode } {
if strings . Contains ( validation . CorrectionGuidance , forbidden ) {
t . Fatalf ( "CorrectionGuidance leaked implementation identifier %q: %q" , forbidden , validation . CorrectionGuidance )
}
}
correction , err := contracts . NewSemanticCorrection ( defective . ModelCandidate . Response , validation . CorrectionGuidance )
if err != nil {
t . Fatal ( err )
}
correctedResponse := [ ] byte ( ` { "occurrences":[ { "name":"Torch","kind":"acquired","quantity":null,"from":null,"to":"party","source_refs":[ { "start_unit_id":2,"end_unit_id":2}]}]} ` )
client := & fakeItemOccurrencesLLMClient { content : correctedResponse }
req . Correction = correction
corrected , err := newExtractor ( t , client , references ) . Extract ( context . Background ( ) , req )
if err != nil {
t . Fatal ( err )
}
accepted , err := itemoccurrenceshape . New ( itemoccurrenceshape . Options { } ) . Validate ( context . Background ( ) , contracts . TypedValidationRequest [ dnd . ItemOccurrenceList ] { Value : corrected . Value } )
if err != nil || ! accepted . Approved {
t . Fatalf ( "corrected holder validation = %#v, %v; want approval" , accepted , err )
}
if len ( client . requests ) != 1 || client . requests [ 0 ] . Correction == nil || string ( client . requests [ 0 ] . Correction . AssistantResponse ) != string ( defectiveResponse ) || client . requests [ 0 ] . Correction . UserGuidance != validation . CorrectionGuidance {
t . Fatalf ( "corrected request = %#v, want byte-faithful defective response and semantic guidance" , client . requests )
}
}