Files
notarius/internal/modules/dnd/extract/spells/npc_registry_wiring_test.go

121 lines
5.3 KiB
Go

package spells
import (
"bytes"
"context"
"encoding/json"
"strings"
"testing"
"gitea.maximumdirect.net/eric/notarius/internal/core/source"
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd"
npccodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/npcs"
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/npcs/identity"
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/npcs/registry"
)
func TestSpellExtractorUsesExactUnboundNPCPromptAndOmitsRegistryIdentity(t *testing.T) {
extractor := newExtractor(t, &fakeSpellsLLMClient{})
metadata := extractor.ManifestMetadata()
if metadata["npc_registry_digest"] != nil || metadata["npc_count"] != nil {
t.Fatalf("unbound extractor metadata = %#v, want no NPC registry fields", metadata)
}
for _, fingerprint := range extractor.CheckpointFingerprints() {
if fingerprint.Name == "npc_registry" {
t.Fatalf("unbound checkpoint fingerprints = %#v, want no NPC registry fingerprint", extractor.CheckpointFingerprints())
}
}
client := &fakeSpellsLLMClient{response: extractionResponse{SpellCasts: []spellCastResponse{}}}
extractor = newExtractor(t, client)
if _, err := extractor.Extract(context.Background(), extractionRequest()); err != nil {
t.Fatalf("Extract() error = %v, want nil", err)
}
input := client.requests[0].Inputs[NPCRegistryReferenceSlot]
if input.Name != NPCRegistryReferenceSlot || input.MediaType != npccodec.MediaType || input.Digest != "" || input.OriginURI != "" || string(input.Content) != `{"npcs":[]}` {
t.Fatalf("NPC prompt input = %#v, want exact empty registry material", input)
}
}
func TestSpellExtractorPreservesSemanticNPCRegistryFingerprintAndPromptWiring(t *testing.T) {
value := registryFixture()
canonical, err := npccodec.New().Encode(value)
if err != nil {
t.Fatalf("encode NPC registry: %v", err)
}
pretty, err := json.MarshalIndent(value, "", " ")
if err != nil {
t.Fatalf("MarshalIndent() error = %v", err)
}
first := newExtractor(t, &fakeSpellsLLMClient{}, spellNPCRegistryReference(canonical, "file:///one.json"))
second := newExtractor(t, &fakeSpellsLLMClient{}, spellNPCRegistryReference(pretty, "file:///two.json"))
firstFingerprints := checkpointFingerprintMap(first.CheckpointFingerprints())
secondFingerprints := checkpointFingerprintMap(second.CheckpointFingerprints())
if firstFingerprints["npc_registry"] == "" || firstFingerprints["npc_registry"] != secondFingerprints["npc_registry"] {
t.Fatalf("semantic NPC fingerprints = %#v and %#v, want same npc_registry value", firstFingerprints, secondFingerprints)
}
metadata := first.ManifestMetadata()
if metadata["npc_registry_digest"] != firstFingerprints["npc_registry"] || metadata["npc_count"] != 1 {
t.Fatalf("NPC registry metadata = %#v, want digest and count only", metadata)
}
client := &fakeSpellsLLMClient{response: extractionResponse{SpellCasts: []spellCastResponse{}}}
extractor := newExtractor(t, client, spellNPCRegistryReference(append([]byte("\n"), canonical...), "file:///npc-session.json"))
if _, err := extractor.Extract(context.Background(), extractionRequest()); err != nil {
t.Fatalf("Extract() error = %v, want nil", err)
}
input := client.requests[0].Inputs[NPCRegistryReferenceSlot]
if input.Name != NPCRegistryReferenceSlot || input.MediaType != npccodec.MediaType || input.Digest != firstFingerprints["npc_registry"] || input.OriginURI != "" {
t.Fatalf("NPC prompt input metadata = %#v, want semantic metadata without provenance", input)
}
if !bytes.Equal(input.Content, canonical) {
t.Fatalf("NPC prompt input = %s, want canonical JSON %s", input.Content, canonical)
}
encoded, err := json.Marshal(map[string]any{"metadata": metadata, "fingerprints": firstFingerprints})
if err != nil {
t.Fatalf("marshal metadata: %v", err)
}
for _, forbidden := range []string{"Mira Thorn", "The Greencloak", "one.json", "two.json"} {
if strings.Contains(string(encoded), forbidden) {
t.Fatalf("metadata or fingerprints leaked %q: %s", forbidden, encoded)
}
}
}
func registryFixture() dnd.NPCList {
return dnd.NPCList{NPCs: []dnd.NPC{{
ID: identity.DeriveID("Mira Thorn"),
Name: "Mira Thorn",
Aliases: []string{"The Greencloak"},
Description: "A guarded ranger who watches the northern road.",
Relationships: []dnd.NPCRelationship{{
Target: "Captain Vale", Relationship: "reports to",
}},
SourceRefs: []source.SourceRef{{SourceID: "npc-session", StartUnitID: 41, EndUnitID: 43}},
}}}
}
func spellNPCRegistryReference(content []byte, origin string) contracts.ReferenceSet {
return contracts.ReferenceSet{Slots: map[string]contracts.ResolvedReferenceSlot{
registry.ReferenceSlot: {
Slot: contracts.ReferenceSlot{Name: registry.ReferenceSlot, AcceptedMediaTypes: []string{npccodec.MediaType}, MaxBytes: registry.MaxBytes},
Items: []contracts.ReferenceItem{{
SlotName: registry.ReferenceSlot,
MediaType: npccodec.MediaType,
Content: append([]byte(nil), content...),
Origin: contracts.ReferenceOrigin{Type: "file", URI: origin},
}},
},
}}
}
func checkpointFingerprintMap(values []pipeline.CheckpointFingerprint) map[string]string {
result := make(map[string]string, len(values))
for _, value := range values {
result[value.Name] = value.Value
}
return result
}