121 lines
5.3 KiB
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
|
|
}
|