diff --git a/assets/dnd/npc-interactions/prompts/instructions.md b/assets/dnd/npc-interactions/prompts/instructions.md index a64db944..56cea7a2 100644 --- a/assets/dnd/npc-interactions/prompts/instructions.md +++ b/assets/dnd/npc-interactions/prompts/instructions.md @@ -5,9 +5,9 @@ Use only names from the supplied NPC registry. Do not summarize, infer relationships, sentiment, factions, motives, aliases, or persistent state. Do not identify player characters, anonymous groups, or -invented NPCs. Keep occurrences within this transcript chunk. Split records -when an NPC's interaction kind changes, when combat alignment changes, or when -an NPC is first mentioned and later becomes present. +invented NPCs. Split records when an NPC's interaction kind changes, when +combat alignment changes, or when an NPC is first mentioned and later becomes +present. Use exactly one kind per occurrence: diff --git a/assets/dnd/npcs/normalize/prompts/candidates.md b/assets/dnd/npcs/normalize/prompts/candidates.md index 7ba9fa84..87d35ce5 100644 --- a/assets/dnd/npcs/normalize/prompts/candidates.md +++ b/assets/dnd/npcs/normalize/prompts/candidates.md @@ -1,4 +1,3 @@ -The supplied NPC candidates are below. Use only these display names in the -response. +NPC candidates for identity comparison: {{ input "candidates" }} diff --git a/assets/dnd/scene-descriptions/prompts/instructions.md b/assets/dnd/scene-descriptions/prompts/instructions.md index f1058094..e019bcef 100644 --- a/assets/dnd/scene-descriptions/prompts/instructions.md +++ b/assets/dnd/scene-descriptions/prompts/instructions.md @@ -42,5 +42,4 @@ must not invent a proper noun. The summary must briefly state the main activity and material transition or outcome established within the accepted chunk. Do not add analysis, inferred motives, hidden state, future consequences, relationship claims, or facts from -outside the chunk. Campaign references may disambiguate names but never add -events or lore. +outside the chunk. diff --git a/internal/framework/llm/asset_registry_test.go b/internal/framework/llm/asset_registry_test.go index 0c59a24e..bbe45de1 100644 --- a/internal/framework/llm/asset_registry_test.go +++ b/internal/framework/llm/asset_registry_test.go @@ -185,11 +185,13 @@ func TestAssetRegistryFallbackProfileDigestTracksContentWithoutLeakingIt(t *test func TestAssetRegistryCombinesNamespacedPromptSources(t *testing.T) { registry := NewAssetRegistry() mustRegisterPromptFS(t, registry, fstest.MapFS{ - "dnd.spells/prompt.yaml": {Data: []byte(validPromptYAML("schema.json"))}, + "dnd.spells/dnd.spells.yaml": {Data: []byte(validPromptYAML("schema.json"))}, + "dnd.spells/task.md": {Data: []byte("spell task")}, "dnd.spells/instructions.md": {Data: []byte("spell instructions")}, }, ".") mustRegisterPromptFS(t, registry, fstest.MapFS{ - "dnd.scenes/prompt.yaml": {Data: []byte(validPromptYAML("schema.json"))}, + "dnd.scenes/dnd.scenes.yaml": {Data: []byte(validPromptYAML("schema.json"))}, + "dnd.scenes/task.md": {Data: []byte("scene task")}, "dnd.scenes/instructions.md": {Data: []byte("scene instructions")}, }, ".") @@ -198,9 +200,11 @@ func TestAssetRegistryCombinesNamespacedPromptSources(t *testing.T) { t.Fatalf("PromptFS() error = %v, want nil", err) } for _, name := range []string{ - "dnd.spells/prompt.yaml", + "dnd.spells/dnd.spells.yaml", + "dnd.spells/task.md", "dnd.spells/instructions.md", - "dnd.scenes/prompt.yaml", + "dnd.scenes/dnd.scenes.yaml", + "dnd.scenes/task.md", "dnd.scenes/instructions.md", } { if _, err := fsys.Open(name); err != nil { diff --git a/internal/framework/promptfs/prompt_fs_test.go b/internal/framework/promptfs/prompt_fs_test.go index a894daf0..38577a92 100644 --- a/internal/framework/promptfs/prompt_fs_test.go +++ b/internal/framework/promptfs/prompt_fs_test.go @@ -14,11 +14,11 @@ func TestModulePromptFSCombinesModuleAndSharedPrompts(t *testing.T) { "nested/transcript.md": {Data: []byte("transcript")}, } fsys, err := ModulePromptFS("module.test", fstest.MapFS{ - "assets/prompts/prompt.yaml": {Data: []byte("id: module.test")}, - "assets/prompts/instructions.md": {Data: []byte("instructions")}, + "assets/prompts/module.test.yaml": {Data: []byte("id: module.test")}, + "assets/prompts/task.md": {Data: []byte("task")}, }, []ModulePromptFile{ - {Name: "prompt.yaml", Path: "assets/prompts/prompt.yaml"}, - {Name: "instructions.md", Path: "assets/prompts/instructions.md"}, + {Name: "module.test.yaml", Path: "assets/prompts/module.test.yaml"}, + {Name: "task.md", Path: "assets/prompts/task.md"}, }, SharedPromptFile{Name: "system.md", FS: sharedFS, Path: "shared/system.md"}, SharedPromptFile{Name: "reference.md", FS: sharedFS, Path: "shared/reference.md"}, @@ -29,8 +29,8 @@ func TestModulePromptFSCombinesModuleAndSharedPrompts(t *testing.T) { } tests := map[string]string{ - "assets/prompts/module.test/prompt.yaml": "id: module.test", - "assets/prompts/module.test/instructions.md": "instructions", + "assets/prompts/module.test/module.test.yaml": "id: module.test", + "assets/prompts/module.test/task.md": "task", "assets/prompts/module.test/sharedassets/system.md": "system", "assets/prompts/module.test/sharedassets/reference.md": "reference", "assets/prompts/module.test/sharedassets/transcript.md": "transcript", @@ -63,18 +63,18 @@ func TestModulePromptFSCombinesModuleAndSharedPrompts(t *testing.T) { func TestModulePromptFSRejectsMissingModuleFile(t *testing.T) { _, err := ModulePromptFS("module.test", fstest.MapFS{}, []ModulePromptFile{ - {Name: "instructions.md", Path: "assets/prompts/instructions.md"}, + {Name: "task.md", Path: "assets/prompts/task.md"}, }) - if err == nil || !strings.Contains(err.Error(), "read module prompt asset assets/prompts/instructions.md") { + if err == nil || !strings.Contains(err.Error(), "read module prompt asset assets/prompts/task.md") { t.Fatalf("ModulePromptFS() error = %v, want missing module asset context", err) } } func TestModulePromptFSRejectsMissingSharedFile(t *testing.T) { _, err := ModulePromptFS("module.test", fstest.MapFS{ - "assets/prompts/instructions.md": {Data: []byte("instructions")}, + "assets/prompts/task.md": {Data: []byte("task")}, }, []ModulePromptFile{ - {Name: "instructions.md", Path: "assets/prompts/instructions.md"}, + {Name: "task.md", Path: "assets/prompts/task.md"}, }, SharedPromptFile{Name: "system.md", FS: fstest.MapFS{}, Path: "shared/system.md"}) if err == nil || !strings.Contains(err.Error(), "read shared prompt asset shared/system.md") { t.Fatalf("ModulePromptFS() error = %v, want missing shared asset context", err) @@ -83,9 +83,9 @@ func TestModulePromptFSRejectsMissingSharedFile(t *testing.T) { func TestModulePromptFSRejectsNestedVirtualFileName(t *testing.T) { _, err := ModulePromptFS("module.test", fstest.MapFS{ - "assets/prompts/instructions.md": {Data: []byte("instructions")}, + "assets/prompts/task.md": {Data: []byte("task")}, }, []ModulePromptFile{ - {Name: "nested/instructions.md", Path: "assets/prompts/instructions.md"}, + {Name: "nested/task.md", Path: "assets/prompts/task.md"}, }) if err == nil || !strings.Contains(err.Error(), "must not contain path separators") { t.Fatalf("ModulePromptFS() error = %v, want nested file name error", err) @@ -94,9 +94,9 @@ func TestModulePromptFSRejectsNestedVirtualFileName(t *testing.T) { func TestModulePromptFSRejectsNestedSharedFileName(t *testing.T) { _, err := ModulePromptFS("module.test", fstest.MapFS{ - "assets/prompts/instructions.md": {Data: []byte("instructions")}, + "assets/prompts/task.md": {Data: []byte("task")}, }, []ModulePromptFile{ - {Name: "instructions.md", Path: "assets/prompts/instructions.md"}, + {Name: "task.md", Path: "assets/prompts/task.md"}, }, SharedPromptFile{Name: "nested/system.md", FS: fstest.MapFS{}, Path: "shared/system.md"}) if err == nil || !strings.Contains(err.Error(), "shared prompt file name") || !strings.Contains(err.Error(), "must not contain path separators") { t.Fatalf("ModulePromptFS() error = %v, want nested shared file name error", err) diff --git a/internal/modules/dnd/extract/combatturns/prompt_assets_test.go b/internal/modules/dnd/extract/combatturns/prompt_assets_test.go index 737085b3..c4283dac 100644 --- a/internal/modules/dnd/extract/combatturns/prompt_assets_test.go +++ b/internal/modules/dnd/extract/combatturns/prompt_assets_test.go @@ -62,38 +62,4 @@ func TestPromptAssetsPrepareRequiredInputs(t *testing.T) { if prepared.PromptID != PromptID || prepared.OutputContract.SchemaPath != "dnd_combat_turns_llm.v1.json" { t.Fatalf("prepared prompt = %#v, want combat prompt identity and schema", prepared) } - referenceIndex := -1 - transcriptIndex := -1 - npcIndex := -1 - for index, message := range prepared.Messages { - if strings.Contains(message.Content, "combat-player") { - referenceIndex = index - } - if strings.Contains(message.Content, "combat-transcript") { - transcriptIndex = index - } - if strings.Contains(message.Content, "combat-npc") { - npcIndex = index - } - } - instructionsIndex := len(prepared.Messages) - 1 - if referenceIndex < 0 || transcriptIndex <= referenceIndex || npcIndex <= transcriptIndex || instructionsIndex <= npcIndex { - t.Fatalf("message order = references %d, transcript %d, NPCs %d, instructions %d; want that order", referenceIndex, transcriptIndex, npcIndex, instructionsIndex) - } - for _, index := range []int{referenceIndex, transcriptIndex, instructionsIndex} { - if cache := prepared.Messages[index].CacheControl; cache == nil || cache.Type != promptkit.CacheControlEphemeral { - t.Fatalf("message %d cache control = %#v, want ephemeral", index, cache) - } - } - for index, message := range prepared.Messages { - if index != referenceIndex && strings.Contains(message.Content, "combat-player") { - t.Errorf("message %d unexpectedly rendered campaign-reference input", index) - } - if index != transcriptIndex && strings.Contains(message.Content, "combat-transcript") { - t.Errorf("message %d unexpectedly rendered transcript input", index) - } - if index != npcIndex && strings.Contains(message.Content, "combat-npc") { - t.Errorf("message %d unexpectedly rendered NPC grounding input", index) - } - } } diff --git a/internal/modules/dnd/extract/enemyevents/prompt_assets_test.go b/internal/modules/dnd/extract/enemyevents/prompt_assets_test.go index f3f82e88..632603f7 100644 --- a/internal/modules/dnd/extract/enemyevents/prompt_assets_test.go +++ b/internal/modules/dnd/extract/enemyevents/prompt_assets_test.go @@ -28,33 +28,6 @@ func TestRegisterPromptAssetsAndPrepareEnemyEventPrompt(t *testing.T) { if prepared.PromptID != PromptID || prepared.OutputContract.SchemaPath != "dnd_enemy_events_llm.v1.json" || prepared.SelectedProfileID != "dnd-extraction" { t.Fatalf("prepared prompt = %#v", prepared) } - referenceIndex := renderedMessageIndex(t, prepared.Messages, "enemy-player") - transcriptIndex := renderedMessageIndex(t, prepared.Messages, "enemy-transcript") - if transcriptIndex <= referenceIndex { - t.Fatalf("transcript message index = %d, want after references index %d", transcriptIndex, referenceIndex) - } - groundingIndices := make([]int, 0, 3) - for _, sentinel := range []string{"enemy-npc", "enemy-turn", "enemy-opponent"} { - if index := renderedMessageIndex(t, prepared.Messages, sentinel); index <= transcriptIndex { - t.Fatalf("message containing %q has index %d, want after transcript index %d", sentinel, index, transcriptIndex) - } else { - groundingIndices = append(groundingIndices, index) - } - } - instructionIndex := len(prepared.Messages) - 1 - for _, index := range groundingIndices { - if instructionIndex <= index { - t.Fatalf("instruction index = %d, want after grounding index %d", instructionIndex, index) - } - } - if instructionIndex != len(prepared.Messages)-1 { - t.Fatalf("instruction message index = %d, want final message", instructionIndex) - } - for _, index := range []int{referenceIndex, transcriptIndex, instructionIndex} { - if cache := prepared.Messages[index].CacheControl; cache == nil || cache.Type != promptkit.CacheControlEphemeral { - t.Fatalf("message %d cache control = %#v, want ephemeral", index, cache) - } - } } func TestEnemyEventPromptRequiresGroundingInputs(t *testing.T) { @@ -115,20 +88,3 @@ func enemyEventPromptInputs() map[string]promptkit.ArtifactRef { "npc_interactions": promptkit.Inline(`{"npc_interactions":[{"name":"enemy-opponent","kind":"combat_opponent"}]}`), } } - -func renderedMessageIndex(t *testing.T, messages []promptkit.RenderedMessage, sentinel string) int { - 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("message sentinel %q rendered %d times, want exactly once", sentinel, occurrences) - } - return index -} diff --git a/internal/modules/dnd/extract/itemevents/prompt_assets_test.go b/internal/modules/dnd/extract/itemevents/prompt_assets_test.go index 5e0858e2..7cf7c7d6 100644 --- a/internal/modules/dnd/extract/itemevents/prompt_assets_test.go +++ b/internal/modules/dnd/extract/itemevents/prompt_assets_test.go @@ -42,33 +42,6 @@ func TestPromptAssetsPrepareItemEventPrompt(t *testing.T) { if prepared.PromptID != PromptID || prepared.OutputContract.SchemaPath != "dnd_item_events_llm.v1.json" { t.Fatalf("prepared prompt = %#v", prepared) } - referenceIndex := -1 - transcriptIndex := -1 - for index, message := range prepared.Messages { - if strings.Contains(message.Content, "item-event-player") { - referenceIndex = index - } - if strings.Contains(message.Content, "item-event-transcript") { - transcriptIndex = index - } - } - instructionsIndex := len(prepared.Messages) - 1 - if referenceIndex < 0 || transcriptIndex <= referenceIndex || transcriptIndex >= instructionsIndex { - t.Fatalf("message order = references %d, transcript %d, instructions %d; want that order", referenceIndex, transcriptIndex, instructionsIndex) - } - for _, index := range []int{referenceIndex, transcriptIndex, instructionsIndex} { - if cache := prepared.Messages[index].CacheControl; cache == nil || cache.Type != promptkit.CacheControlEphemeral { - t.Fatalf("message %d cache control = %#v, want ephemeral", index, cache) - } - } - for index, message := range prepared.Messages { - if index != referenceIndex && strings.Contains(message.Content, "item-event-player") { - t.Errorf("message %d unexpectedly rendered campaign-reference input", index) - } - if index != transcriptIndex && strings.Contains(message.Content, "item-event-transcript") { - t.Errorf("message %d unexpectedly rendered transcript input", index) - } - } } func TestPromptAssetsDoNotLeakIntoMetadata(t *testing.T) { diff --git a/internal/modules/dnd/extract/locationoccurrences/prompt_assets_test.go b/internal/modules/dnd/extract/locationoccurrences/prompt_assets_test.go index 4c57d1d3..73beab5d 100644 --- a/internal/modules/dnd/extract/locationoccurrences/prompt_assets_test.go +++ b/internal/modules/dnd/extract/locationoccurrences/prompt_assets_test.go @@ -40,46 +40,12 @@ func TestRegisterPromptAssetsPreparesLocationOccurrencePrompt(t *testing.T) { t.Fatalf("prepared prompt = %#v", prepared) } var registryMessage string - referenceIndex := -1 - transcriptIndex := -1 - registryIndex := -1 - evidenceIndex := -1 - for index, message := range prepared.Messages { - if strings.Contains(message.Content, "location-occurrence-player") { - referenceIndex = index - } - if strings.Contains(message.Content, "location-occurrence-transcript") { - transcriptIndex = index - } + for _, message := range prepared.Messages { if strings.Contains(message.Content, "normalized location registry") { registryMessage = message.Content - registryIndex = index - } - if strings.Contains(message.Content, "Transcript units are the only evidence") { - evidenceIndex = index } } if !strings.Contains(registryMessage, "location:sha256:test") || !strings.Contains(registryMessage, "location-occurrence-registry") || strings.Contains(registryMessage, "source_refs") { t.Fatalf("rendered prompt did not preserve source-free registry grounding: %s", registryMessage) } - instructionsIndex := len(prepared.Messages) - 1 - if referenceIndex < 0 || transcriptIndex <= referenceIndex || evidenceIndex <= transcriptIndex || registryIndex <= evidenceIndex || instructionsIndex <= registryIndex { - t.Fatalf("message order = references %d, transcript %d, evidence %d, registry %d, instructions %d; want that order", referenceIndex, transcriptIndex, evidenceIndex, registryIndex, instructionsIndex) - } - for _, index := range []int{referenceIndex, transcriptIndex, instructionsIndex} { - if cache := prepared.Messages[index].CacheControl; cache == nil || cache.Type != promptkit.CacheControlEphemeral { - t.Fatalf("message %d cache control = %#v, want ephemeral", index, cache) - } - } - for index, message := range prepared.Messages { - if index != referenceIndex && strings.Contains(message.Content, "location-occurrence-player") { - t.Errorf("message %d unexpectedly rendered campaign-reference input", index) - } - if index != transcriptIndex && strings.Contains(message.Content, "location-occurrence-transcript") { - t.Errorf("message %d unexpectedly rendered transcript input", index) - } - if index != registryIndex && strings.Contains(message.Content, "location-occurrence-registry") { - t.Errorf("message %d unexpectedly rendered registry grounding input", index) - } - } } diff --git a/internal/modules/dnd/extract/locations/prompt_assets_test.go b/internal/modules/dnd/extract/locations/prompt_assets_test.go index 64697c84..5b9f629f 100644 --- a/internal/modules/dnd/extract/locations/prompt_assets_test.go +++ b/internal/modules/dnd/extract/locations/prompt_assets_test.go @@ -2,7 +2,6 @@ package locations import ( "context" - "strings" "testing" "time" @@ -30,34 +29,7 @@ func TestRegisterPromptAssetsPreparesLocationPrompt(t *testing.T) { if err != nil { t.Fatalf("Prepare() error = %v", err) } - if prepared.OutputContract.SchemaPath != "dnd_locations_llm.v1.json" { - t.Fatalf("output contract = %#v", prepared.OutputContract) - } - referenceIndex := -1 - transcriptIndex := -1 - for index, message := range prepared.Messages { - if strings.Contains(message.Content, "location-player") { - referenceIndex = index - } - if strings.Contains(message.Content, "location-transcript") { - transcriptIndex = index - } - } - instructionsIndex := len(prepared.Messages) - 1 - if referenceIndex < 0 || transcriptIndex <= referenceIndex || transcriptIndex >= instructionsIndex { - t.Fatalf("message order = references %d, transcript %d, instructions %d; want that order", referenceIndex, transcriptIndex, instructionsIndex) - } - for _, index := range []int{referenceIndex, transcriptIndex, instructionsIndex} { - if prepared.Messages[index].CacheControl == nil || prepared.Messages[index].CacheControl.Type != promptkit.CacheControlEphemeral { - t.Fatalf("message %d cache control = %#v, want ephemeral", index, prepared.Messages[index].CacheControl) - } - } - for index, message := range prepared.Messages { - if index != referenceIndex && strings.Contains(message.Content, "location-player") { - t.Errorf("message %d unexpectedly rendered campaign-reference input", index) - } - if index != transcriptIndex && strings.Contains(message.Content, "location-transcript") { - t.Errorf("message %d unexpectedly rendered transcript input", index) - } + if prepared.PromptID != PromptID || prepared.OutputContract.SchemaPath != "dnd_locations_llm.v1.json" { + t.Fatalf("prepared prompt = %#v, want location prompt identity and schema wiring", prepared) } } diff --git a/internal/modules/dnd/extract/npcinteractions/prompt_assets_test.go b/internal/modules/dnd/extract/npcinteractions/prompt_assets_test.go index b6c0baed..48c1105a 100644 --- a/internal/modules/dnd/extract/npcinteractions/prompt_assets_test.go +++ b/internal/modules/dnd/extract/npcinteractions/prompt_assets_test.go @@ -51,40 +51,6 @@ func TestRegisterPromptAssetsAndPrepareInteractionPrompt(t *testing.T) { if prepared.PromptID != PromptID || prepared.OutputContract.SchemaPath != "dnd_npc_interactions_llm.v1.json" { t.Fatalf("prepared prompt = %#v", prepared) } - referenceIndex := -1 - transcriptIndex := -1 - npcIndex := -1 - for index, message := range prepared.Messages { - if strings.Contains(message.Content, "interaction-player") { - referenceIndex = index - } - if strings.Contains(message.Content, "interaction-transcript") { - transcriptIndex = index - } - if strings.Contains(message.Content, "interaction-npc") { - npcIndex = index - } - } - instructionsIndex := len(prepared.Messages) - 1 - if referenceIndex < 0 || transcriptIndex <= referenceIndex || npcIndex <= transcriptIndex || instructionsIndex <= npcIndex { - t.Fatalf("message order = references %d, transcript %d, NPCs %d, instructions %d; want that order", referenceIndex, transcriptIndex, npcIndex, instructionsIndex) - } - for _, index := range []int{referenceIndex, transcriptIndex, instructionsIndex} { - if cache := prepared.Messages[index].CacheControl; cache == nil || cache.Type != promptkit.CacheControlEphemeral { - t.Fatalf("message %d cache control = %#v, want ephemeral", index, cache) - } - } - for index, message := range prepared.Messages { - if index != referenceIndex && strings.Contains(message.Content, "interaction-player") { - t.Errorf("message %d unexpectedly rendered campaign-reference input", index) - } - if index != transcriptIndex && strings.Contains(message.Content, "interaction-transcript") { - t.Errorf("message %d unexpectedly rendered transcript input", index) - } - if index != npcIndex && strings.Contains(message.Content, "interaction-npc") { - t.Errorf("message %d unexpectedly rendered NPC grounding input", index) - } - } } func TestPromptMetadataDoesNotExposeAssetContent(t *testing.T) { diff --git a/internal/modules/dnd/extract/npcs/prompt_assets_test.go b/internal/modules/dnd/extract/npcs/prompt_assets_test.go index f63b722f..58074486 100644 --- a/internal/modules/dnd/extract/npcs/prompt_assets_test.go +++ b/internal/modules/dnd/extract/npcs/prompt_assets_test.go @@ -42,33 +42,6 @@ func TestRegisterPromptAssetsAndPrepareNPCPrompt(t *testing.T) { if prepared.PromptID != PromptID || prepared.OutputContract.SchemaPath != "dnd_npcs_llm.v1.json" { t.Fatalf("prepared prompt = %#v, want NPC prompt identity and wiring", prepared) } - referenceIndex := -1 - transcriptIndex := -1 - for index, message := range prepared.Messages { - if strings.Contains(message.Content, "npc-player") { - referenceIndex = index - } - if strings.Contains(message.Content, "npc-transcript") { - transcriptIndex = index - } - } - instructionsIndex := len(prepared.Messages) - 1 - if referenceIndex < 0 || transcriptIndex <= referenceIndex || transcriptIndex >= instructionsIndex { - t.Fatalf("message order = references %d, transcript %d, instructions %d; want that order", referenceIndex, transcriptIndex, instructionsIndex) - } - for _, index := range []int{referenceIndex, transcriptIndex, instructionsIndex} { - if cache := prepared.Messages[index].CacheControl; cache == nil || cache.Type != promptkit.CacheControlEphemeral { - t.Fatalf("message %d cache control = %#v, want ephemeral", index, cache) - } - } - for index, message := range prepared.Messages { - if index != referenceIndex && strings.Contains(message.Content, "npc-player") { - t.Errorf("message %d unexpectedly rendered campaign-reference input", index) - } - if index != transcriptIndex && strings.Contains(message.Content, "npc-transcript") { - t.Errorf("message %d unexpectedly rendered transcript input", index) - } - } } func TestPromptMetadataAndDiagnosticsDoNotContainRawAssets(t *testing.T) { diff --git a/internal/modules/dnd/extract/scenedescriptions/prompt_assets_test.go b/internal/modules/dnd/extract/scenedescriptions/prompt_assets_test.go index c92608f9..1ae1f18f 100644 --- a/internal/modules/dnd/extract/scenedescriptions/prompt_assets_test.go +++ b/internal/modules/dnd/extract/scenedescriptions/prompt_assets_test.go @@ -42,33 +42,6 @@ func TestRegisterPromptAssetsPreparesSceneDescriptionPrompt(t *testing.T) { if prepared.PromptID != PromptID || prepared.OutputContract.SchemaPath != "dnd_scene_descriptions_llm.v1.json" { t.Fatalf("prepared prompt = %#v, want scene-description prompt identity and schema wiring", prepared) } - referenceIndex := -1 - transcriptIndex := -1 - for index, message := range prepared.Messages { - if strings.Contains(message.Content, "scene-description-player") { - referenceIndex = index - } - if strings.Contains(message.Content, "scene-description-transcript") { - transcriptIndex = index - } - } - instructionsIndex := len(prepared.Messages) - 1 - if referenceIndex < 0 || transcriptIndex <= referenceIndex || transcriptIndex >= instructionsIndex { - t.Fatalf("message order = references %d, transcript %d, instructions %d; want that order", referenceIndex, transcriptIndex, instructionsIndex) - } - for _, index := range []int{referenceIndex, transcriptIndex, instructionsIndex} { - if cache := prepared.Messages[index].CacheControl; cache == nil || cache.Type != promptkit.CacheControlEphemeral { - t.Fatalf("message %d cache control = %#v, want ephemeral", index, cache) - } - } - for index, message := range prepared.Messages { - if index != referenceIndex && strings.Contains(message.Content, "scene-description-player") { - t.Errorf("message %d unexpectedly rendered campaign-reference input", index) - } - if index != transcriptIndex && strings.Contains(message.Content, "scene-description-transcript") { - t.Errorf("message %d unexpectedly rendered transcript input", index) - } - } } func TestPromptMetadataAndDiagnosticsDoNotContainRawAssets(t *testing.T) { diff --git a/internal/modules/dnd/extract/spells/prompt_assets_test.go b/internal/modules/dnd/extract/spells/prompt_assets_test.go index fa6a7981..56ed650d 100644 --- a/internal/modules/dnd/extract/spells/prompt_assets_test.go +++ b/internal/modules/dnd/extract/spells/prompt_assets_test.go @@ -21,35 +21,6 @@ func TestPromptAssetsPrepareSpellPrompt(t *testing.T) { if prepared.OutputContract.SchemaPath != "dnd_spells_llm.v1.json" { t.Fatalf("schema path = %q, want LLM-only schema", prepared.OutputContract.SchemaPath) } - indices := map[string]int{ - "spell-player": -1, - "spell-transcript": -1, - "spell-npc-sentinel": -1, - "spell-catalog-sentinel": -1, - } - for index, message := range prepared.Messages { - for sentinel := range indices { - if strings.Contains(message.Content, sentinel) { - indices[sentinel] = index - } - } - } - instructionsIndex := len(prepared.Messages) - 1 - if indices["spell-player"] < 0 || indices["spell-transcript"] <= indices["spell-player"] || indices["spell-npc-sentinel"] <= indices["spell-transcript"] || indices["spell-catalog-sentinel"] <= indices["spell-npc-sentinel"] || instructionsIndex <= indices["spell-catalog-sentinel"] { - t.Fatalf("message order = %#v with instructions at %d, want references, transcript, NPCs, catalog, instructions", indices, instructionsIndex) - } - for _, index := range []int{indices["spell-player"], indices["spell-transcript"], instructionsIndex} { - if cache := prepared.Messages[index].CacheControl; cache == nil || cache.Type != promptkit.CacheControlEphemeral { - t.Fatalf("message %d cache control = %#v, want ephemeral", index, cache) - } - } - for sentinel, index := range indices { - for messageIndex, message := range prepared.Messages { - if messageIndex != index && strings.Contains(message.Content, sentinel) { - t.Errorf("message %d unexpectedly rendered %q", messageIndex, sentinel) - } - } - } } func TestPromptAssetsPrepareWithMissingOptionalReferences(t *testing.T) { diff --git a/internal/modules/dnd/register/prompt_cache_test.go b/internal/modules/dnd/register/prompt_cache_test.go index b0466c90..7041cd7f 100644 --- a/internal/modules/dnd/register/prompt_cache_test.go +++ b/internal/modules/dnd/register/prompt_cache_test.go @@ -20,7 +20,7 @@ import ( "gitea.maximumdirect.net/eric/promptkit" ) -func TestExtractionPromptsShareRenderedPrefix(t *testing.T) { +func TestExtractionPromptComposition(t *testing.T) { const ( transcriptSentinel = "shared-transcript-sentinel" playersSentinel = "shared-players-sentinel" @@ -28,6 +28,7 @@ func TestExtractionPromptsShareRenderedPrefix(t *testing.T) { glossarySentinel = "shared-glossary-sentinel" npcSentinel = "npc-registry-sentinel" catalogSentinel = "spell-catalog-sentinel" + evidenceSentinel = "Transcript units are the only evidence" ) registry := llm.NewAssetRegistry() if err := registerPromptAssets(registry); err != nil { @@ -41,15 +42,15 @@ func TestExtractionPromptsShareRenderedPrefix(t *testing.T) { "glossary": promptkit.Inline(glossarySentinel), } cases := []struct { - name string - promptID string - promptVersion string - inputs map[string]promptkit.ArtifactRef - inputSentinels []string + name string + promptID string + promptVersion string + inputs map[string]promptkit.ArtifactRef + suffixGroups [][]string }{ - {name: "npcs", promptID: npcextract.PromptID, promptVersion: npcextract.SchemaVersion, inputs: commonInputs}, - {name: "locations", promptID: locationextract.PromptID, promptVersion: locationextract.SchemaVersion, inputs: commonInputs}, - {name: "item events", promptID: itemeventextract.PromptID, promptVersion: itemeventextract.SchemaVersion, inputs: commonInputs}, + {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}}}, + {name: "item events", promptID: itemeventextract.PromptID, promptVersion: itemeventextract.SchemaVersion, inputs: commonInputs, suffixGroups: [][]string{{evidenceSentinel}}}, {name: "scene descriptions", promptID: scenedescriptionextract.PromptID, promptVersion: scenedescriptionextract.SchemaVersion, inputs: commonInputs}, { name: "combat turns", @@ -58,7 +59,7 @@ func TestExtractionPromptsShareRenderedPrefix(t *testing.T) { inputs: withPromptInputs(commonInputs, map[string]promptkit.ArtifactRef{ "npcs": promptkit.Inline(`{"sentinel":"` + npcSentinel + `"}`), }), - inputSentinels: []string{npcSentinel}, + suffixGroups: [][]string{{evidenceSentinel}, {npcSentinel}}, }, { name: "enemy events", @@ -69,7 +70,11 @@ func TestExtractionPromptsShareRenderedPrefix(t *testing.T) { "combat_turns": promptkit.Inline(`{"sentinel":"combat-turns-sentinel"}`), "npc_interactions": promptkit.Inline(`{"sentinel":"npc-interactions-sentinel"}`), }), - inputSentinels: []string{npcSentinel, "combat-turns-sentinel", "npc-interactions-sentinel"}, + suffixGroups: [][]string{ + {evidenceSentinel}, + {npcSentinel}, + {"combat-turns-sentinel", "npc-interactions-sentinel"}, + }, }, { name: "npc interactions", @@ -78,7 +83,7 @@ func TestExtractionPromptsShareRenderedPrefix(t *testing.T) { inputs: withPromptInputs(commonInputs, map[string]promptkit.ArtifactRef{ "npcs": promptkit.Inline(`{"sentinel":"` + npcSentinel + `"}`), }), - inputSentinels: []string{npcSentinel}, + suffixGroups: [][]string{{evidenceSentinel}, {npcSentinel}}, }, { name: "location occurrences", @@ -87,7 +92,7 @@ func TestExtractionPromptsShareRenderedPrefix(t *testing.T) { inputs: withPromptInputs(commonInputs, map[string]promptkit.ArtifactRef{ "locations": promptkit.Inline(`{"sentinel":"location-registry-sentinel"}`), }), - inputSentinels: []string{"location-registry-sentinel"}, + suffixGroups: [][]string{{evidenceSentinel}, {"location-registry-sentinel"}}, }, { name: "spells", @@ -97,7 +102,7 @@ func TestExtractionPromptsShareRenderedPrefix(t *testing.T) { "npcs": promptkit.Inline(`{"sentinel":"` + npcSentinel + `"}`), "spell_catalog": promptkit.Inline(`{"sentinel":"` + catalogSentinel + `"}`), }), - inputSentinels: []string{npcSentinel, catalogSentinel}, + suffixGroups: [][]string{{evidenceSentinel}, {npcSentinel}, {catalogSentinel}}, }, } @@ -114,6 +119,12 @@ func TestExtractionPromptsShareRenderedPrefix(t *testing.T) { t.Fatalf("Prepare() error = %v", err) } transcriptIndex := renderedInputMessageIndex(t, prepared.Messages, transcriptSentinel) + 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) 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)) @@ -123,9 +134,27 @@ func TestExtractionPromptsShareRenderedPrefix(t *testing.T) { } else if !reflect.DeepEqual(prefix, sharedPrefix) { t.Fatalf("rendered prefix = %#v, want %#v", prefix, sharedPrefix) } - for _, sentinel := range testCase.inputSentinels { - assertRenderedInputAfter(t, prepared.Messages, sentinel, transcriptIndex) + 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 + if instructionIndex <= previousIndex { + t.Fatalf("instructions rendered at message %d, want after lane input message %d", instructionIndex, previousIndex) + } + assertEphemeralCache(t, prepared.Messages, instructionIndex) }) } } @@ -157,10 +186,10 @@ func withPromptInputs(inputs, extras map[string]promptkit.ArtifactRef) map[strin return merged } -func assertRenderedInputAfter(t *testing.T, messages []promptkit.RenderedMessage, sentinel string, index int) { +func assertEphemeralCache(t *testing.T, messages []promptkit.RenderedMessage, index int) { t.Helper() - if inputIndex := renderedInputMessageIndex(t, messages, sentinel); inputIndex <= index { - t.Fatalf("input sentinel %q rendered at message %d, want after transcript message %d", sentinel, inputIndex, index) + if cache := messages[index].CacheControl; cache == nil || cache.Type != promptkit.CacheControlEphemeral { + t.Fatalf("message %d cache control = %#v, want ephemeral", index, cache) } }