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/npcregistry" "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) } fingerprints := checkpointFingerprintMap(extractor.CheckpointFingerprints()) if fingerprints["npc_registry"] == "" { t.Fatalf("unbound checkpoint fingerprints = %#v, want empty-projection identity", fingerprints) } 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 != fingerprints["npc_registry"] || 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"] == "" || 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, []byte(`{"npcs":[{"name":"Mira Thorn"}]}`)) { t.Fatalf("NPC prompt input = %s, want names-only projection", input.Content) } 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 TestSpellExtractorResolvesOperationNPCOverrideWithoutSingletonMetadata(t *testing.T) { canonical, err := npccodec.New().Encode(registryFixture()) if err != nil { t.Fatalf("encode NPC registry: %v", err) } client := &fakeSpellsLLMClient{response: extractionResponse{SpellCasts: []spellCastResponse{}}} extractor := newExtractor(t, client) request := extractionRequest() request.References = spellNPCRegistryReference(canonical, "file:///generated.json") if _, err := extractor.Extract(context.Background(), request); err != nil { t.Fatalf("Extract() error = %v", err) } input := client.requests[0].Inputs[NPCRegistryReferenceSlot] if input.Digest == "" || string(input.Content) != `{"npcs":[{"name":"Mira Thorn"}]}` || input.OriginURI != "" { t.Fatalf("operation NPC input = %#v, want generated canonical grounding without provenance", input) } if metadata := extractor.ManifestMetadata(); metadata["npc_registry_digest"] != nil || metadata["npc_count"] != nil { t.Fatalf("singleton metadata = %#v, want no operation-varying NPC identity", metadata) } if checkpointFingerprintMap(extractor.CheckpointFingerprints())["npc_registry"] == "" { t.Fatalf("singleton fingerprints = %#v, want empty-projection identity", extractor.CheckpointFingerprints()) } } func registryFixture() dnd.NPCRegistry { return dnd.NPCRegistry{NPCs: []dnd.NPC{{ ID: identity.DeriveID("Mira Thorn"), Name: "Mira Thorn", 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 }