2026-07-03 23:38:41 +00:00
package spells
import (
"context"
2026-07-20 19:20:56 +00:00
"encoding/json"
2026-07-03 23:38:41 +00:00
"errors"
2026-07-17 06:50:08 +00:00
"reflect"
2026-07-20 19:20:56 +00:00
"sort"
2026-07-03 23:38:41 +00:00
"strings"
"testing"
2026-07-17 05:32:11 +00:00
"gitea.maximumdirect.net/eric/notarius/internal/core/source"
2026-07-03 23:38:41 +00:00
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
2026-07-17 06:50:08 +00:00
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd"
2026-07-17 05:05:48 +00:00
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared"
2026-07-20 19:20:56 +00:00
spellcatalog "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/spells/catalog"
2026-07-03 23:38:41 +00:00
)
2026-07-17 06:50:08 +00:00
func TestExtractReturnsCanonicalSpellListFromPrivateResponse ( t * testing . T ) {
client := & fakeSpellsLLMClient { response : extractionResponse { SpellCasts : [ ] spellCastResponse {
{
2026-07-22 19:01:58 +00:00
Caster : " Aria " ,
Spell : " Cure Wounds " ,
2026-08-28 13:45:21 +00:00
SourceRefs : responseSourceRefs ( 2 , 1 ) ,
2026-07-03 23:38:41 +00:00
} ,
2026-07-17 06:50:08 +00:00
} } }
req := extractionRequest ( )
2026-07-03 23:38:41 +00:00
2026-07-17 06:50:08 +00:00
result , err := newExtractor ( t , client ) . Extract ( context . Background ( ) , req )
2026-07-03 23:38:41 +00:00
if err != nil {
t . Fatalf ( "Extract() error = %v, want nil" , err )
}
2026-07-17 06:50:08 +00:00
want := dnd . SpellList { SpellCasts : [ ] dnd . SpellCast {
{
2026-07-22 19:01:58 +00:00
Caster : " Aria " ,
Spell : " Cure Wounds " ,
SourceRefs : [ ] source . SourceRef { { SourceID : "session-alpha" , StartUnitID : 1 , EndUnitID : 2 } } ,
2026-07-17 06:50:08 +00:00
} ,
} }
if ! reflect . DeepEqual ( result . Value , want ) {
t . Fatalf ( "Value = %#v, want %#v" , result . Value , want )
}
2026-07-03 23:38:41 +00:00
if len ( client . requests ) != 1 {
t . Fatalf ( "LLM calls = %d, want 1" , len ( client . requests ) )
}
2026-07-07 15:32:35 -05:00
llmReq := client . requests [ 0 ]
2026-07-17 06:50:08 +00:00
if llmReq . StageName != Key || llmReq . PromptID != PromptID || llmReq . PromptVersion != SchemaVersion {
t . Fatalf ( "LLM request identity = %#v, want spell prompt" , llmReq )
2026-07-03 23:38:41 +00:00
}
2026-07-07 15:32:35 -05:00
if llmReq . SessionID != "session-123" || llmReq . ProfileID != "profile-spells" {
t . Fatalf ( "session/profile = %q/%q, want session-123/profile-spells" , llmReq . SessionID , llmReq . ProfileID )
2026-07-03 23:38:41 +00:00
}
2026-07-07 15:32:35 -05:00
transcript := llmReq . Inputs [ "transcript" ]
if transcript . Name != "transcript" || transcript . MediaType != "application/json" || transcript . Digest != "sha256:chunk" || transcript . OriginURI != "file:///session-alpha.json" {
2026-07-05 18:27:07 +00:00
t . Fatalf ( "transcript metadata = %#v" , transcript )
2026-07-03 23:38:41 +00:00
}
2026-07-17 06:50:08 +00:00
if got := string ( transcript . Content ) ; got != string ( req . Chunk . Content ) {
t . Fatalf ( "transcript content = %q, want chunk content %q" , got , req . Chunk . Content )
2026-07-03 23:38:41 +00:00
}
2026-07-20 19:20:56 +00:00
catalogInput := llmReq . Inputs [ spellcatalog . SpellCatalogReferenceSlot ]
if catalogInput . Name != spellcatalog . SpellCatalogReferenceSlot || catalogInput . MediaType != "application/json" || catalogInput . OriginURI != "" || ! strings . HasPrefix ( catalogInput . Digest , "sha256:" ) {
t . Fatalf ( "catalog prompt input metadata = %#v" , catalogInput )
}
var catalogPayload struct {
2026-08-09 02:17:15 +00:00
Spells [ ] spellcatalog . PromptSpell ` json:"spells" `
2026-07-20 19:20:56 +00:00
}
if err := json . Unmarshal ( catalogInput . Content , & catalogPayload ) ; err != nil {
t . Fatalf ( "decode catalog prompt input: %v" , err )
}
base , err := spellcatalog . LoadSRD5E2014 ( )
if err != nil {
t . Fatal ( err )
}
wantNames := make ( [ ] string , 0 , len ( base . Spells ( ) ) )
for _ , spell := range base . Spells ( ) {
wantNames = append ( wantNames , spell . Name )
}
sort . Strings ( wantNames )
2026-08-09 02:17:15 +00:00
gotNames := make ( [ ] string , len ( catalogPayload . Spells ) )
for index , spell := range catalogPayload . Spells {
gotNames [ index ] = spell . CanonicalName
if ! sort . StringsAreSorted ( spell . Aliases ) {
t . Fatalf ( "catalog prompt aliases for %q are not sorted: %#v" , spell . CanonicalName , spell . Aliases )
}
}
if ! reflect . DeepEqual ( gotNames , wantNames ) || ! sort . StringsAreSorted ( gotNames ) {
t . Fatalf ( "catalog prompt names = %d entries, want sorted base catalog" , len ( gotNames ) )
2026-07-20 19:20:56 +00:00
}
}
2026-08-09 02:17:15 +00:00
func TestExtractPromptProjectsCanonicalNamesAndAliasesWithoutMetadata ( t * testing . T ) {
2026-07-20 19:20:56 +00:00
client := & fakeSpellsLLMClient { response : extractionResponse { SpellCasts : [ ] spellCastResponse { } } }
if _ , err := newExtractor ( t , client , overlaySpellCatalogReference ( ) ) . Extract ( context . Background ( ) , extractionRequest ( ) ) ; err != nil {
t . Fatalf ( "Extract() error = %v, want nil" , err )
}
input := client . requests [ 0 ] . Inputs [ spellcatalog . SpellCatalogReferenceSlot ]
content := string ( input . Content )
2026-08-09 02:17:15 +00:00
var payload struct {
Spells [ ] spellcatalog . PromptSpell ` json:"spells" `
}
if err := json . Unmarshal ( input . Content , & payload ) ; err != nil {
t . Fatalf ( "decode catalog prompt input: %v" , err )
}
var aegis * spellcatalog . PromptSpell
for index := range payload . Spells {
if payload . Spells [ index ] . CanonicalName == "Aegis of Emberfall" {
aegis = & payload . Spells [ index ]
break
2026-07-20 19:20:56 +00:00
}
}
2026-08-09 02:17:15 +00:00
if aegis == nil || ! reflect . DeepEqual ( aegis . Aliases , [ ] string { "Emberfall Aegis" } ) {
t . Fatalf ( "Aegis prompt projection = %#v, want canonical name and alias" , aegis )
}
for _ , forbidden := range [ ] string { "Private campaign source" , "file:///private-source.json" , "private" , "license" , "ruleset" , "provenance" } {
2026-07-20 19:20:56 +00:00
if strings . Contains ( content , forbidden ) {
t . Fatalf ( "catalog prompt input leaked %q: %s" , forbidden , content )
}
}
metadata := newExtractor ( t , & fakeSpellsLLMClient { } , overlaySpellCatalogReference ( ) ) . ManifestMetadata ( )
2026-07-24 14:23:35 +00:00
if metadata [ "mapping_policy" ] != mappingPolicy {
t . Fatalf ( "mapping policy metadata = %#v, want %q" , metadata [ "mapping_policy" ] , mappingPolicy )
}
2026-07-20 19:20:56 +00:00
if metadata [ "catalog_base_id" ] != spellcatalog . SRD5E2014ID {
t . Fatalf ( "catalog base metadata = %#v" , metadata [ "catalog_base_id" ] )
}
if digest , ok := metadata [ "catalog_digest" ] . ( string ) ; ! ok || ! strings . HasPrefix ( digest , "sha256:" ) {
t . Fatalf ( "catalog digest metadata = %#v" , metadata [ "catalog_digest" ] )
}
if got , ok := metadata [ "catalog_overlay_ids" ] . ( [ ] string ) ; ! ok || ! reflect . DeepEqual ( got , [ ] string { "campaign.example" } ) {
t . Fatalf ( "catalog overlay metadata = %#v" , metadata [ "catalog_overlay_ids" ] )
}
2026-07-20 15:14:41 -05:00
fingerprints := newExtractor ( t , & fakeSpellsLLMClient { } , overlaySpellCatalogReference ( ) ) . CheckpointFingerprints ( )
2026-07-20 20:25:23 -05:00
wantFingerprints := map [ string ] any {
"effective_catalog" : metadata [ "catalog_digest" ] ,
2026-07-24 14:23:35 +00:00
"mapping_policy" : mappingPolicy ,
2026-07-20 20:25:23 -05:00
"prompt" : metadata [ "prompt_sha256" ] ,
"response_schema" : metadata [ "response_schema_sha256" ] ,
2026-07-22 18:51:26 +00:00
"npc_registry" : checkpointFingerprintMap ( newExtractor ( t , & fakeSpellsLLMClient { } ) . CheckpointFingerprints ( ) ) [ "npc_registry" ] ,
2026-07-20 20:25:23 -05:00
}
if len ( fingerprints ) != len ( wantFingerprints ) {
2026-07-22 18:51:26 +00:00
t . Fatalf ( "checkpoint fingerprints = %#v, want prompt, response schema, catalog, and NPC projection identities" , fingerprints )
2026-07-20 20:25:23 -05:00
}
for _ , fingerprint := range fingerprints {
if want , ok := wantFingerprints [ fingerprint . Name ] ; ! ok || fingerprint . Value != want {
t . Fatalf ( "checkpoint fingerprint %q = %q, want %#v" , fingerprint . Name , fingerprint . Value , want )
}
2026-07-20 15:14:41 -05:00
}
2026-07-20 19:20:56 +00:00
encoded , err := json . Marshal ( metadata )
if err != nil {
t . Fatal ( err )
}
for _ , forbidden := range [ ] string { "Aegis of Emberfall" , "Emberfall Aegis" , "Private campaign source" , "file:///private-source.json" } {
if strings . Contains ( string ( encoded ) , forbidden ) {
t . Fatalf ( "manifest metadata leaked %q: %s" , forbidden , encoded )
}
}
}
2026-08-09 02:17:15 +00:00
func TestExtractUsesAliasRecognitionToRequestCanonicalSpellNames ( t * testing . T ) {
client := & fakeSpellsLLMClient { response : extractionResponse { SpellCasts : [ ] spellCastResponse { {
Caster : "Aria" ,
Spell : "Aegis of Emberfall" ,
SourceRefs : responseSourceRefs ( 1 , 1 ) ,
} } } }
request := extractionRequest ( )
request . Chunk . Content = [ ] byte ( ` { "segments":[ { "id":1,"text":"Aria invokes Emberfall Aegis."}]} ` )
request . SourceInput = spellChunkInput ( request . Chunk )
result , err := newExtractor ( t , client , overlaySpellCatalogReference ( ) ) . Extract ( context . Background ( ) , request )
if err != nil {
t . Fatalf ( "Extract() error = %v, want nil" , err )
}
if got := result . Value . SpellCasts ; len ( got ) != 1 || got [ 0 ] . Spell != "Aegis of Emberfall" {
t . Fatalf ( "spell casts = %#v, want canonical spell name" , got )
}
input := client . requests [ 0 ] . Inputs [ spellcatalog . SpellCatalogReferenceSlot ]
if ! strings . Contains ( string ( input . Content ) , ` "aliases":["Emberfall Aegis"] ` ) {
t . Fatalf ( "catalog prompt input = %s, want transcript alias recognition" , input . Content )
}
}
func TestExtractAliasOnlyCatalogChangesPromptMaterialAndCheckpointFingerprint ( t * testing . T ) {
aliasReference := spellCatalogReference ( ` { "schema_version":"notarius.dnd.spell-catalog-overlay.v1","catalogs":[ { "id":"campaign.example","ruleset":"dnd-5e-2014","source": { "title":"Private campaign source","version":"1","url":"file:///private-source.json","license":"private"},"spells":[ { "name":"Cure Wounds","aliases":["Campaign Woundweave"]}]}]} ` )
baseClient := & fakeSpellsLLMClient { response : extractionResponse { SpellCasts : [ ] spellCastResponse { } } }
aliasClient := & fakeSpellsLLMClient { response : extractionResponse { SpellCasts : [ ] spellCastResponse { } } }
baseExtractor := newExtractor ( t , baseClient )
aliasExtractor := newExtractor ( t , aliasClient , aliasReference )
if _ , err := baseExtractor . Extract ( context . Background ( ) , extractionRequest ( ) ) ; err != nil {
t . Fatalf ( "base Extract() error = %v" , err )
}
if _ , err := aliasExtractor . Extract ( context . Background ( ) , extractionRequest ( ) ) ; err != nil {
t . Fatalf ( "alias Extract() error = %v" , err )
}
baseInput := baseClient . requests [ 0 ] . Inputs [ spellcatalog . SpellCatalogReferenceSlot ]
aliasInput := aliasClient . requests [ 0 ] . Inputs [ spellcatalog . SpellCatalogReferenceSlot ]
if baseInput . Digest == aliasInput . Digest || string ( baseInput . Content ) == string ( aliasInput . Content ) {
t . Fatalf ( "alias-only catalog did not change prompt material: %q / %q" , baseInput . Digest , aliasInput . Digest )
}
if ! strings . Contains ( string ( aliasInput . Content ) , "Campaign Woundweave" ) {
t . Fatalf ( "alias prompt input = %s, want alias recognition" , aliasInput . Content )
}
baseFingerprints := checkpointFingerprintMap ( baseExtractor . CheckpointFingerprints ( ) )
aliasFingerprints := checkpointFingerprintMap ( aliasExtractor . CheckpointFingerprints ( ) )
if baseFingerprints [ "effective_catalog" ] == aliasFingerprints [ "effective_catalog" ] {
t . Fatalf ( "effective catalog fingerprint did not change: %#v" , aliasFingerprints )
}
if baseExtractor . ManifestMetadata ( ) [ "prompt_id" ] != aliasExtractor . ManifestMetadata ( ) [ "prompt_id" ] || baseExtractor . ManifestMetadata ( ) [ "prompt_version" ] != aliasExtractor . ManifestMetadata ( ) [ "prompt_version" ] {
t . Fatal ( "catalog-only change altered prompt identity" )
}
}
2026-07-20 19:20:56 +00:00
func TestNewRejectsMalformedCatalogBeforeLLMCall ( t * testing . T ) {
client := & fakeSpellsLLMClient { }
_ , err := New ( client , Options { } , spellCatalogReference ( ` { "schema_version":"notarius.dnd.spell-catalog-overlay.v2","catalogs":[]} ` ) )
if err == nil || ! strings . Contains ( err . Error ( ) , "resolve effective spell catalog" ) {
t . Fatalf ( "New() error = %v, want effective catalog error" , err )
}
if len ( client . requests ) != 0 {
t . Fatalf ( "LLM calls = %d, want none during failed construction" , len ( client . requests ) )
}
2026-07-03 23:38:41 +00:00
}
2026-07-17 06:50:08 +00:00
func TestExtractorManifestMetadataIncludesLLMSchemaProvenance ( t * testing . T ) {
metadata := newExtractor ( t , & fakeSpellsLLMClient { } ) . ManifestMetadata ( )
2026-07-04 00:45:21 +00:00
tests := map [ string ] string {
2026-07-17 06:50:08 +00:00
"prompt_id" : PromptID , "prompt_version" : SchemaVersion ,
"response_schema_key" : string ( ResponseSchemaKey ) , "response_schema_id" : ResponseSchemaID ,
"response_schema_name" : ResponseSchemaName , "response_schema_version" : SchemaVersion ,
2026-07-04 00:45:21 +00:00
}
for key , want := range tests {
if metadata [ key ] != want {
t . Fatalf ( "metadata[%q] = %#v, want %q" , key , metadata [ key ] , want )
}
}
for _ , key := range [ ] string { "prompt_sha256" , "response_schema_sha256" } {
value , ok := metadata [ key ] . ( string )
if ! ok || ! strings . HasPrefix ( value , "sha256:" ) {
t . Fatalf ( "metadata[%q] = %#v, want sha256 value" , key , metadata [ key ] )
}
}
}
2026-07-24 14:46:19 +00:00
func TestNilExtractorManifestMetadata ( t * testing . T ) {
var extractor * Extractor
if metadata := extractor . ManifestMetadata ( ) ; metadata != nil {
t . Fatalf ( "nil extractor metadata = %#v, want nil" , metadata )
}
}
2026-07-05 18:27:07 +00:00
func TestExtractPassesReferencesAsPromptInputs ( t * testing . T ) {
2026-07-05 14:49:17 +00:00
client := & fakeSpellsLLMClient { response : extractionResponse { SpellCasts : [ ] spellCastResponse { } } }
2026-07-17 06:50:08 +00:00
req := extractionRequest ( )
req . References = contracts . ReferenceSet { Slots : map [ string ] contracts . ResolvedReferenceSlot {
"players" : { Slot : contracts . ReferenceSlot { Name : "players" } , Items : [ ] contracts . ReferenceItem { { SlotName : "players" , Content : [ ] byte ( "Alice: Aria Brightmantle" ) } } } ,
"party" : { Slot : contracts . ReferenceSlot { Name : "party" } , Items : [ ] contracts . ReferenceItem { { SlotName : "party" , Content : [ ] byte ( "Aria Brightmantle: party cleric" ) } } } ,
"glossary" : { Slot : contracts . ReferenceSlot { Name : "glossary" } , Items : [ ] contracts . ReferenceItem { { SlotName : "glossary" , Content : [ ] byte ( "Brightmantle: local temple name" ) } } } ,
} }
if _ , err := newExtractor ( t , client ) . Extract ( context . Background ( ) , req ) ; err != nil {
2026-07-05 14:49:17 +00:00
t . Fatalf ( "Extract() error = %v, want nil" , err )
}
2026-07-17 06:50:08 +00:00
inputs := client . requests [ 0 ] . Inputs
if string ( inputs [ "players" ] . Content ) != "Alice: Aria Brightmantle" || string ( inputs [ "party" ] . Content ) != "Aria Brightmantle: party cleric" || string ( inputs [ "glossary" ] . Content ) != "Brightmantle: local temple name" {
t . Fatalf ( "reference inputs = %#v, want configured content" , inputs )
2026-07-05 18:27:07 +00:00
}
2026-07-17 06:50:08 +00:00
if strings . Contains ( string ( inputs [ "transcript" ] . Content ) , "party cleric" ) {
t . Fatal ( "transcript input contains reference content" )
2026-07-05 14:49:17 +00:00
}
}
2026-07-06 11:26:21 -05:00
func TestPromptInputsMapLegacyRosterReferenceToParty ( t * testing . T ) {
2026-07-17 06:50:08 +00:00
inputs := shared . PromptInputs ( spellSourceInput ( ) , contracts . ReferenceSet { Slots : map [ string ] contracts . ResolvedReferenceSlot {
"roster" : { Slot : contracts . ReferenceSlot { Name : "roster" } , Items : [ ] contracts . ReferenceItem { { SlotName : "roster" , Content : [ ] byte ( "Legacy roster text" ) } } } ,
} } )
2026-07-06 11:26:21 -05:00
if got := string ( inputs [ "party" ] . Content ) ; got != "Legacy roster text" {
t . Fatalf ( "party input = %q, want legacy roster content" , got )
}
if _ , ok := inputs [ "roster" ] ; ok {
2026-07-17 06:50:08 +00:00
t . Fatal ( "roster prompt input was present; want only party input" )
2026-07-06 11:26:21 -05:00
}
}
2026-07-17 06:50:08 +00:00
func TestExtractPreservesEmptyAndMalformedValuesForTypedValidators ( t * testing . T ) {
tests := [ ] struct {
name string
response extractionResponse
wantNil bool
} {
{ name : "empty" , response : extractionResponse { SpellCasts : [ ] spellCastResponse { } } } ,
{ name : "missing" , response : extractionResponse { } , wantNil : true } ,
}
for _ , test := range tests {
t . Run ( test . name , func ( t * testing . T ) {
result , err := newExtractor ( t , & fakeSpellsLLMClient { response : test . response } ) . Extract ( context . Background ( ) , extractionRequest ( ) )
if err != nil {
t . Fatalf ( "Extract() error = %v, want nil" , err )
}
if ( result . Value . SpellCasts == nil ) != test . wantNil || len ( result . Value . SpellCasts ) != 0 {
t . Fatalf ( "SpellCasts = %#v, want empty with nil=%t" , result . Value . SpellCasts , test . wantNil )
}
} )
2026-07-03 23:38:41 +00:00
}
}
func TestExtractWrapsLLMClientError ( t * testing . T ) {
2026-07-17 06:50:08 +00:00
_ , err := newExtractor ( t , & fakeSpellsLLMClient { err : errors . New ( "provider unavailable" ) } ) . Extract ( context . Background ( ) , extractionRequest ( ) )
if err == nil || ! strings . Contains ( err . Error ( ) , "dnd spells" ) || ! strings . Contains ( err . Error ( ) , "provider unavailable" ) {
t . Fatalf ( "Extract() error = %v, want wrapped provider error" , err )
2026-07-03 23:38:41 +00:00
}
}
func TestExtractRejectsInvalidRequests ( t * testing . T ) {
2026-07-17 06:50:08 +00:00
validReq := extractionRequest ( )
validExtractor := newExtractor ( t , & fakeSpellsLLMClient { response : extractionResponse { SpellCasts : [ ] spellCastResponse { } } } )
2026-07-24 14:38:42 +00:00
var nilExtractor * Extractor
2026-07-03 23:38:41 +00:00
tests := [ ] struct {
name string
extractor * Extractor
ctx context . Context
2026-07-17 06:50:08 +00:00
req contracts . TypedExtractionRequest
2026-07-03 23:38:41 +00:00
want string
} {
2026-07-24 14:38:42 +00:00
{ name : "nil extractor" , extractor : nilExtractor , ctx : context . Background ( ) , req : validReq , want : "extractor" } ,
{ name : "nil LLM client" , extractor : & Extractor { } , ctx : context . Background ( ) , req : validReq , want : "LLM client" } ,
{ name : "wrapped preflight failure" , extractor : validExtractor , ctx : context . Background ( ) , req : mismatchedSourceInputRequest ( validReq ) , want : "must match chunk" } ,
2026-07-17 06:50:08 +00:00
}
for _ , test := range tests {
t . Run ( test . name , func ( t * testing . T ) {
_ , err := test . extractor . Extract ( test . ctx , test . req )
if err == nil || ! strings . Contains ( err . Error ( ) , "dnd spells" ) || ! strings . Contains ( err . Error ( ) , test . want ) {
t . Fatalf ( "Extract() error = %v, want %q context" , err , test . want )
2026-07-03 23:38:41 +00:00
}
} )
}
}
2026-07-17 06:50:08 +00:00
func TestExtractOrdersAndDeduplicatesEvidence ( t * testing . T ) {
client := & fakeSpellsLLMClient { response : extractionResponse { SpellCasts : [ ] spellCastResponse {
2026-07-22 19:01:58 +00:00
{ Caster : "Borin" , Spell : "Fire Bolt" , SourceRefs : responseSourceRefs ( 2 , 2 ) } ,
{ Caster : "Aria" , Spell : "Cure Wounds" , SourceRefs : [ ] spellSourceRefResponse { { StartUnitID : 1 , EndUnitID : 2 } , { StartUnitID : 1 , EndUnitID : 2 } } } ,
{ Caster : "Narrator" , Spell : "Unknown" } ,
2026-07-17 06:50:08 +00:00
} } }
result , err := newExtractor ( t , client ) . Extract ( context . Background ( ) , extractionRequest ( ) )
2026-07-03 23:38:41 +00:00
if err != nil {
t . Fatalf ( "Extract() error = %v, want nil" , err )
}
2026-07-17 06:50:08 +00:00
if got := [ ] string { result . Value . SpellCasts [ 0 ] . Spell , result . Value . SpellCasts [ 1 ] . Spell , result . Value . SpellCasts [ 2 ] . Spell } ; ! reflect . DeepEqual ( got , [ ] string { "Cure Wounds" , "Fire Bolt" , "Unknown" } ) {
t . Fatalf ( "spell order = %#v, want evidence order" , got )
2026-07-03 23:38:41 +00:00
}
2026-07-17 06:50:08 +00:00
if refs := result . Value . SpellCasts [ 0 ] . SourceRefs ; len ( refs ) != 1 || refs [ 0 ] != ( source . SourceRef { SourceID : "session-alpha" , StartUnitID : 1 , EndUnitID : 2 } ) {
t . Fatalf ( "source refs = %#v, want one canonical ref" , refs )
2026-07-08 10:43:39 -05:00
}
}
2026-07-24 14:29:19 +00:00
func TestExtractUsesDocumentOrderForReferencesAndSpellCasts ( t * testing . T ) {
client := & fakeSpellsLLMClient { response : extractionResponse { SpellCasts : [ ] spellCastResponse {
{ Caster : "Later" , Spell : "Fire Bolt" , SourceRefs : responseSourceRefs ( 10 , 10 ) } ,
{ Caster : "Earlier" , Spell : "Cure Wounds" , SourceRefs : [ ] spellSourceRefResponse {
{ StartUnitID : 10 , EndUnitID : 10 } ,
{ StartUnitID : 30 , EndUnitID : 30 } ,
{ StartUnitID : 30 , EndUnitID : 30 } ,
{ StartUnitID : 999 , EndUnitID : 0 } ,
} } ,
{ Caster : "Unavailable" , Spell : "Healing Word" , SourceRefs : [ ] spellSourceRefResponse { { StartUnitID : 999 , EndUnitID : 0 } } } ,
} } }
req := extractionRequest ( )
req . Source . Units = [ ] source . SourceUnit { { ID : 30 } , { ID : 10 } }
req . Chunk . Units = append ( [ ] source . SourceUnit ( nil ) , req . Source . Units ... )
req . Chunk . Ref = source . SourceRef { SourceID : req . Source . ID , StartUnitID : 30 , EndUnitID : 10 }
result , err := newExtractor ( t , client ) . Extract ( context . Background ( ) , req )
if err != nil {
t . Fatalf ( "Extract() error = %v" , err )
}
if got := [ ] string { result . Value . SpellCasts [ 0 ] . Spell , result . Value . SpellCasts [ 1 ] . Spell , result . Value . SpellCasts [ 2 ] . Spell } ; ! reflect . DeepEqual ( got , [ ] string { "Cure Wounds" , "Fire Bolt" , "Healing Word" } ) {
t . Fatalf ( "spell order = %#v, want document chronology followed by invalid evidence" , got )
}
refs := result . Value . SpellCasts [ 0 ] . SourceRefs
if got := [ ] int { refs [ 0 ] . StartUnitID , refs [ 1 ] . StartUnitID , refs [ 2 ] . StartUnitID } ; ! reflect . DeepEqual ( got , [ ] int { 30 , 10 , 999 } ) {
t . Fatalf ( "source refs = %#v, want document order with exact duplicate removed" , refs )
}
refs [ 0 ] . StartUnitID = 777
for _ , spell := range client . response . SpellCasts {
for _ , ref := range spell . SourceRefs {
if ref . StartUnitID == 777 {
t . Fatal ( "result source references alias the model response" )
}
}
}
}
func TestExtractPreservesStableSpellOrderForEqualEvidence ( t * testing . T ) {
client := & fakeSpellsLLMClient { response : extractionResponse { SpellCasts : [ ] spellCastResponse {
{ Caster : "First" , Spell : "Cure Wounds" , SourceRefs : responseSourceRefs ( 2 , 2 ) } ,
{ Caster : "Second" , Spell : "Fire Bolt" , SourceRefs : responseSourceRefs ( 2 , 2 ) } ,
} } }
result , err := newExtractor ( t , client ) . Extract ( context . Background ( ) , extractionRequest ( ) )
if err != nil {
t . Fatalf ( "Extract() error = %v" , err )
}
if got := [ ] string { result . Value . SpellCasts [ 0 ] . Caster , result . Value . SpellCasts [ 1 ] . Caster } ; ! reflect . DeepEqual ( got , [ ] string { "First" , "Second" } ) {
t . Fatalf ( "equal-evidence order = %#v, want stable response order" , got )
}
}
2026-07-17 06:50:08 +00:00
func TestExtractPreservesInvalidEvidenceForValidators ( t * testing . T ) {
client := & fakeSpellsLLMClient { response : extractionResponse { SpellCasts : [ ] spellCastResponse { {
2026-07-22 19:01:58 +00:00
Caster : "Aria" , Spell : "Cure Wounds" ,
2026-07-22 14:38:35 +00:00
SourceRefs : [ ] spellSourceRefResponse { { StartUnitID : 99 , EndUnitID : 0 } } ,
2026-07-17 06:50:08 +00:00
} } } }
result , err := newExtractor ( t , client ) . Extract ( context . Background ( ) , extractionRequest ( ) )
2026-07-08 10:43:39 -05:00
if err != nil {
t . Fatalf ( "Extract() error = %v, want nil" , err )
}
2026-07-17 06:50:08 +00:00
ref := result . Value . SpellCasts [ 0 ] . SourceRefs [ 0 ]
if ref != ( source . SourceRef { SourceID : "session-alpha" , StartUnitID : 99 } ) {
t . Fatalf ( "source ref = %#v, want canonical source with invalid range preserved" , ref )
2026-07-05 18:26:31 -05:00
}
}
2026-07-22 14:38:35 +00:00
func TestExtractMapsRawSemanticCandidatesWithoutRepair ( t * testing . T ) {
2026-07-22 19:01:58 +00:00
client := & fakeSpellsLLMClient { content : [ ] byte ( ` { "spell_casts":[ { "caster":"","spell":"Cure Wounds","source_refs":[ { "start_unit_id":0,"end_unit_id":-1}]}]} ` ) }
2026-07-22 14:38:35 +00:00
result , err := newExtractor ( t , client ) . Extract ( context . Background ( ) , extractionRequest ( ) )
if err != nil {
t . Fatalf ( "Extract() error = %v, want nil" , err )
}
spell := result . Value . SpellCasts [ 0 ]
2026-07-22 19:01:58 +00:00
if spell . Caster != "" {
2026-07-22 14:38:35 +00:00
t . Fatalf ( "spell = %#v, want blank semantic values preserved" , spell )
}
if refs := spell . SourceRefs ; len ( refs ) != 1 || refs [ 0 ] != ( source . SourceRef { SourceID : "session-alpha" , StartUnitID : 0 , EndUnitID : - 1 } ) {
t . Fatalf ( "source refs = %#v, want raw nonpositive candidates preserved" , refs )
}
}