2026-07-27 20:14:57 +00:00
package register
import (
"context"
"reflect"
"strings"
"testing"
"time"
"gitea.maximumdirect.net/eric/notarius/internal/framework/llm"
combatextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/combatturns"
2026-08-03 21:01:23 +00:00
enemyeventextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/enemyevents"
2026-08-05 20:00:20 +00:00
itemoccurrenceextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/itemoccurrences"
2026-08-05 19:39:06 +00:00
itemregistryextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/itemregistry"
2026-08-05 14:33:00 +00:00
locationoccurrenceextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/locationoccurrences"
2026-08-05 19:13:37 +00:00
locationextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/locationregistry"
2026-08-05 19:00:55 +00:00
occurrenceextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/npcoccurrences"
2026-08-05 18:38:22 +00:00
npcextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/npcregistry"
2026-07-27 20:14:57 +00:00
scenedescriptionextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/scenedescriptions"
spellextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/spells"
2026-07-28 16:31:36 +00:00
"gitea.maximumdirect.net/eric/promptkit"
2026-07-27 20:14:57 +00:00
)
2026-08-05 17:38:51 +00:00
func TestExtractionPromptComposition ( t * testing . T ) {
2026-07-27 20:14:57 +00:00
const (
2026-08-28 00:13:44 +00:00
transcriptSentinel = "shared-transcript-sentinel"
playersSentinel = "shared-players-sentinel"
partySentinel = "shared-party-sentinel"
glossarySentinel = "shared-glossary-sentinel"
npcSentinel = "npc-registry-sentinel"
catalogSentinel = "spell-catalog-sentinel"
evidenceSentinel = "Transcript units are the only evidence"
combatPolicySentinel = "substantive active combat materially organizes the"
2026-07-27 20:14:57 +00:00
)
registry := llm . NewAssetRegistry ( )
if err := registerPromptAssets ( registry ) ; err != nil {
t . Fatalf ( "registerPromptAssets() error = %v" , err )
}
engine := newPromptCacheEngine ( t , registry )
2026-07-28 16:31:36 +00:00
commonInputs := map [ string ] promptkit . ArtifactRef {
"transcript" : promptkit . InlineWithURI ( "file:///session.json" , ` { "sentinel":" ` + transcriptSentinel + ` "} ` ) ,
"players" : promptkit . Inline ( playersSentinel ) ,
"party" : promptkit . Inline ( partySentinel ) ,
"glossary" : promptkit . Inline ( glossarySentinel ) ,
2026-07-27 20:14:57 +00:00
}
cases := [ ] struct {
2026-08-28 00:13:44 +00:00
name string
promptID string
promptVersion string
inputs map [ string ] promptkit . ArtifactRef
suffixGroups [ ] [ ] string
middleSentinel string
2026-07-27 20:14:57 +00:00
} {
2026-08-05 17:38:51 +00:00
{ name : "npcs" , promptID : npcextract . PromptID , promptVersion : npcextract . SchemaVersion , inputs : commonInputs , suffixGroups : [ ] [ ] string { { evidenceSentinel } } } ,
{ name : "locations" , promptID : locationextract . PromptID , promptVersion : locationextract . SchemaVersion , inputs : commonInputs , suffixGroups : [ ] [ ] string { { evidenceSentinel } } } ,
2026-08-05 20:00:20 +00:00
{ name : "item occurrences" , promptID : itemoccurrenceextract . PromptID , promptVersion : itemoccurrenceextract . SchemaVersion , inputs : withPromptInputs ( commonInputs , map [ string ] promptkit . ArtifactRef {
2026-08-05 19:55:34 +00:00
"item_registry" : promptkit . Inline ( ` { "items":[ { "id":"item-registry-sentinel","name":"Torch"}]} ` ) ,
} ) , suffixGroups : [ ] [ ] string { { evidenceSentinel } , { "item-registry-sentinel" } } } ,
2026-08-05 19:39:06 +00:00
{ name : "item registry" , promptID : itemregistryextract . PromptID , promptVersion : itemregistryextract . SchemaVersion , inputs : commonInputs , suffixGroups : [ ] [ ] string { { evidenceSentinel } } } ,
2026-08-28 00:13:44 +00:00
{ name : "scene descriptions" , promptID : scenedescriptionextract . PromptID , promptVersion : scenedescriptionextract . SchemaVersion , inputs : commonInputs , middleSentinel : combatPolicySentinel } ,
2026-07-27 20:14:57 +00:00
{
name : "combat turns" ,
promptID : combatextract . PromptID ,
promptVersion : combatextract . SchemaVersion ,
2026-07-28 16:31:36 +00:00
inputs : withPromptInputs ( commonInputs , map [ string ] promptkit . ArtifactRef {
2026-08-05 18:38:22 +00:00
"npc_registry" : promptkit . Inline ( ` { "sentinel":" ` + npcSentinel + ` "} ` ) ,
2026-07-27 20:14:57 +00:00
} ) ,
2026-08-05 17:38:51 +00:00
suffixGroups : [ ] [ ] string { { evidenceSentinel } , { npcSentinel } } ,
2026-07-27 20:14:57 +00:00
} ,
2026-08-03 21:01:23 +00:00
{
name : "enemy events" ,
promptID : enemyeventextract . PromptID ,
promptVersion : enemyeventextract . SchemaVersion ,
inputs : withPromptInputs ( commonInputs , map [ string ] promptkit . ArtifactRef {
2026-08-05 19:00:55 +00:00
"npc_registry" : promptkit . Inline ( ` { "sentinel":" ` + npcSentinel + ` "} ` ) ,
"combat_turns" : promptkit . Inline ( ` { "sentinel":"combat-turns-sentinel"} ` ) ,
"npc_occurrences" : promptkit . Inline ( ` { "sentinel":"npc-occurrences-sentinel"} ` ) ,
2026-08-03 21:01:23 +00:00
} ) ,
2026-08-05 17:38:51 +00:00
suffixGroups : [ ] [ ] string {
{ evidenceSentinel } ,
{ npcSentinel } ,
2026-08-05 19:00:55 +00:00
{ "combat-turns-sentinel" , "npc-occurrences-sentinel" } ,
2026-08-05 17:38:51 +00:00
} ,
2026-08-03 21:01:23 +00:00
} ,
2026-07-27 20:14:57 +00:00
{
2026-08-05 19:00:55 +00:00
name : "npc occurrences" ,
promptID : occurrenceextract . PromptID ,
promptVersion : occurrenceextract . SchemaVersion ,
2026-07-28 16:31:36 +00:00
inputs : withPromptInputs ( commonInputs , map [ string ] promptkit . ArtifactRef {
2026-08-05 18:38:22 +00:00
"npc_registry" : promptkit . Inline ( ` { "sentinel":" ` + npcSentinel + ` "} ` ) ,
2026-07-27 20:14:57 +00:00
} ) ,
2026-08-05 17:38:51 +00:00
suffixGroups : [ ] [ ] string { { evidenceSentinel } , { npcSentinel } } ,
2026-07-27 20:14:57 +00:00
} ,
2026-08-05 14:33:00 +00:00
{
name : "location occurrences" ,
promptID : locationoccurrenceextract . PromptID ,
promptVersion : locationoccurrenceextract . SchemaVersion ,
inputs : withPromptInputs ( commonInputs , map [ string ] promptkit . ArtifactRef {
2026-08-05 19:13:37 +00:00
"location_registry" : promptkit . Inline ( ` { "sentinel":"location-registry-sentinel"} ` ) ,
2026-08-05 14:33:00 +00:00
} ) ,
2026-08-05 17:38:51 +00:00
suffixGroups : [ ] [ ] string { { evidenceSentinel } , { "location-registry-sentinel" } } ,
2026-08-05 14:33:00 +00:00
} ,
2026-07-27 20:14:57 +00:00
{
name : "spells" ,
promptID : spellextract . PromptID ,
promptVersion : spellextract . SchemaVersion ,
2026-07-28 16:31:36 +00:00
inputs : withPromptInputs ( commonInputs , map [ string ] promptkit . ArtifactRef {
2026-08-05 18:38:22 +00:00
"npc_registry" : promptkit . Inline ( ` { "sentinel":" ` + npcSentinel + ` "} ` ) ,
2026-07-28 16:31:36 +00:00
"spell_catalog" : promptkit . Inline ( ` { "sentinel":" ` + catalogSentinel + ` "} ` ) ,
2026-07-27 20:14:57 +00:00
} ) ,
2026-08-05 17:38:51 +00:00
suffixGroups : [ ] [ ] string { { evidenceSentinel } , { npcSentinel } , { catalogSentinel } } ,
2026-07-27 20:14:57 +00:00
} ,
}
2026-07-28 16:31:36 +00:00
var sharedPrefix [ ] promptkit . RenderedMessage
2026-07-27 20:14:57 +00:00
for _ , testCase := range cases {
t . Run ( testCase . name , func ( t * testing . T ) {
2026-07-28 16:31:36 +00:00
prepared , err := engine . Prepare ( context . Background ( ) , promptkit . RunRequest {
2026-07-27 20:14:57 +00:00
PromptID : testCase . promptID ,
PromptVersion : testCase . promptVersion ,
ProfileID : "prompt-cache-test-profile" ,
Inputs : testCase . inputs ,
} )
if err != nil {
t . Fatalf ( "Prepare() error = %v" , err )
}
transcriptIndex := renderedInputMessageIndex ( t , prepared . Messages , transcriptSentinel )
2026-08-05 17:38:51 +00:00
referenceIndex := renderedInputMessageIndex ( t , prepared . Messages , playersSentinel )
if referenceIndex >= transcriptIndex {
t . Fatalf ( "campaign references rendered at message %d, want before transcript message %d" , referenceIndex , transcriptIndex )
}
assertEphemeralCache ( t , prepared . Messages , referenceIndex )
assertEphemeralCache ( t , prepared . Messages , transcriptIndex )
2026-07-27 20:14:57 +00:00
prefix := prepared . Messages [ : transcriptIndex + 1 ]
if len ( prepared . Messages ) <= len ( prefix ) {
t . Fatalf ( "prepared prompt has %d messages, want lane-specific suffix after transcript" , len ( prepared . Messages ) )
}
if sharedPrefix == nil {
2026-07-28 16:31:36 +00:00
sharedPrefix = append ( [ ] promptkit . RenderedMessage ( nil ) , prefix ... )
2026-07-27 20:14:57 +00:00
} else if ! reflect . DeepEqual ( prefix , sharedPrefix ) {
t . Fatalf ( "rendered prefix = %#v, want %#v" , prefix , sharedPrefix )
}
2026-08-05 17:38:51 +00:00
previousIndex := transcriptIndex
for _ , group := range testCase . suffixGroups {
groupIndex := - 1
for _ , sentinel := range group {
inputIndex := renderedInputMessageIndex ( t , prepared . Messages , sentinel )
if groupIndex < 0 {
groupIndex = inputIndex
} else if inputIndex != groupIndex {
t . Fatalf ( "input sentinel %q rendered at message %d, want grouped at message %d" , sentinel , inputIndex , groupIndex )
}
}
if groupIndex <= previousIndex {
t . Fatalf ( "prompt suffix group rendered at message %d, want after message %d" , groupIndex , previousIndex )
}
previousIndex = groupIndex
}
instructionIndex := len ( prepared . Messages ) - 1
2026-08-28 00:13:44 +00:00
if testCase . middleSentinel != "" {
policyIndex := renderedInputMessageIndex ( t , prepared . Messages , testCase . middleSentinel )
if policyIndex <= transcriptIndex || policyIndex >= instructionIndex {
t . Fatalf ( "combat policy rendered at message %d, want after transcript message %d and before instructions message %d" , policyIndex , transcriptIndex , instructionIndex )
}
}
2026-08-05 17:38:51 +00:00
if instructionIndex <= previousIndex {
t . Fatalf ( "instructions rendered at message %d, want after lane input message %d" , instructionIndex , previousIndex )
2026-07-27 20:14:57 +00:00
}
2026-08-05 17:38:51 +00:00
assertEphemeralCache ( t , prepared . Messages , instructionIndex )
2026-07-27 20:14:57 +00:00
} )
}
}
2026-07-28 16:31:36 +00:00
func newPromptCacheEngine ( t * testing . T , registry * llm . AssetRegistry ) * promptkit . Engine {
2026-07-27 20:14:57 +00:00
t . Helper ( )
2026-07-28 16:31:36 +00:00
options , err := registry . PromptKitOptions ( )
2026-07-27 20:14:57 +00:00
if err != nil {
2026-07-28 16:31:36 +00:00
t . Fatalf ( "PromptKitOptions() error = %v" , err )
2026-07-27 20:14:57 +00:00
}
2026-07-28 16:31:36 +00:00
options = append ( options , promptkit . WithProfiles ( promptkit . OpenAICompatibleProfile ( promptkit . OpenAICompatibleProfileConfig {
2026-07-27 20:14:57 +00:00
ID : "prompt-cache-test-profile" , Endpoint : "http://127.0.0.1:1/v1" , Model : "prompt-cache-test-model" ,
} ) ) )
2026-07-28 16:31:36 +00:00
engine , err := promptkit . NewEngine ( promptkit . Config { Timeout : time . Second } , options ... )
2026-07-27 20:14:57 +00:00
if err != nil {
t . Fatalf ( "NewEngine() error = %v" , err )
}
return engine
}
2026-07-28 16:31:36 +00:00
func withPromptInputs ( inputs , extras map [ string ] promptkit . ArtifactRef ) map [ string ] promptkit . ArtifactRef {
merged := make ( map [ string ] promptkit . ArtifactRef , len ( inputs ) + len ( extras ) )
2026-07-27 20:14:57 +00:00
for name , input := range inputs {
merged [ name ] = input
}
for name , input := range extras {
merged [ name ] = input
}
return merged
}
2026-08-05 17:38:51 +00:00
func assertEphemeralCache ( t * testing . T , messages [ ] promptkit . RenderedMessage , index int ) {
2026-07-27 20:14:57 +00:00
t . Helper ( )
2026-08-05 17:38:51 +00:00
if cache := messages [ index ] . CacheControl ; cache == nil || cache . Type != promptkit . CacheControlEphemeral {
t . Fatalf ( "message %d cache control = %#v, want ephemeral" , index , cache )
2026-07-27 20:14:57 +00:00
}
}
2026-07-28 16:31:36 +00:00
func renderedInputMessageIndex ( t * testing . T , messages [ ] promptkit . RenderedMessage , sentinel string ) int {
2026-07-27 20:14:57 +00:00
t . Helper ( )
index := - 1
occurrences := 0
for messageIndex , message := range messages {
count := strings . Count ( message . Content , sentinel )
if count > 0 {
index = messageIndex
occurrences += count
}
}
if occurrences != 1 {
t . Fatalf ( "input sentinel %q rendered %d times, want exactly once" , sentinel , occurrences )
}
return index
}