package registry import ( "context" "encoding/json" "reflect" "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" npcregistry "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/npcs/registry" ) func TestValidatorRecognizesRegistryNamesAndRejectsUnknownNames(t *testing.T) { references := registryReferences(t, "Mira Thorn") validator := newValidator(t, references) value := validList(" mira thorn ") result, err := validator.Validate(context.Background(), request(references, value)) if err != nil || !result.Approved { t.Fatalf("recognized result = %#v, %v", result, err) } value.Interactions[0].Name = "Unknown NPC" result, err = validator.Validate(context.Background(), request(references, value)) if err != nil || result.Approved || result.ReasonCode != ReasonCode || !strings.Contains(result.Message, "interactions[0].name") { t.Fatalf("unknown result = %#v, %v", result, err) } } func TestValidatorRequiresRegistryAndResolvesGeneratedReferenceAtOperationTime(t *testing.T) { validator := newValidator(t) value := validList("Mira Thorn") result, err := validator.Validate(context.Background(), request(contracts.ReferenceSet{}, value)) if err != nil || result.Approved || result.ReasonCode != ReasonCode || !strings.Contains(result.Message, "required") { t.Fatalf("unbound result = %#v, %v", result, err) } references := registryReferences(t, "Mira Thorn") result, err = validator.Validate(context.Background(), request(references, value)) if err != nil || !result.Approved { t.Fatalf("generated result = %#v, %v", result, err) } empty := registryReferences(t) result, err = newValidator(t, empty).Validate(context.Background(), request(empty, dnd.NPCInteractionList{Interactions: []dnd.NPCInteraction{}})) if err != nil || !result.Approved { t.Fatalf("empty registry result = %#v, %v", result, err) } } func TestValidatorRejectsMalformedRegistryWithoutContentAndKeepsMetadataSafe(t *testing.T) { malformed := contracts.ReferenceSet{Slots: map[string]contracts.ResolvedReferenceSlot{ npcregistry.ReferenceSlot: { Slot: contracts.ReferenceSlot{Name: npcregistry.ReferenceSlot}, Items: []contracts.ReferenceItem{{SlotName: npcregistry.ReferenceSlot, MediaType: "application/json", Content: []byte(`{"secret":"private source"}`)}}, }, }} if _, err := New(Options{}, malformed); err == nil || strings.Contains(err.Error(), "private source") { t.Fatalf("New() error = %v", err) } references := registryReferences(t, "Mira Thorn") validator := newValidator(t, references) metadata, err := json.Marshal(validator.ManifestMetadata()) if err != nil { t.Fatal(err) } if strings.Contains(string(metadata), "Mira Thorn") || strings.Contains(string(metadata), "other-session") { t.Fatalf("metadata leaked registry content: %s", metadata) } if got := validator.CheckpointFingerprints(); len(got) != 2 || got[0].Value != policy || !strings.HasPrefix(got[1].Value, "sha256:") { t.Fatalf("CheckpointFingerprints() = %#v", got) } } func TestValidatorDefersShapeAndDoesNotMutateOrMisregister(t *testing.T) { references := registryReferences(t, "Mira Thorn") validator := newValidator(t, references) malformed := dnd.NPCInteractionList{Interactions: []dnd.NPCInteraction{{Name: "Mira Thorn"}}} result, err := validator.Validate(context.Background(), request(references, malformed)) if err != nil || !result.Approved { t.Fatalf("shape deferral = %#v, %v", result, err) } value := validList("Mira Thorn") before := value _, err = validator.Validate(context.Background(), request(references, value)) if err != nil || !reflect.DeepEqual(value, before) { t.Fatalf("Validate() mutated value: %#v", value) } registry := pipeline.NewValidatorRegistry() if err := Register(registry); err != nil { t.Fatal(err) } if _, err := DecodeOptions(map[string]any{"unexpected": true}); err == nil { t.Fatal("DecodeOptions() accepted unknown option") } } func newValidator(t *testing.T, references ...contracts.ReferenceSet) *Validator { t.Helper() validator, err := New(Options{}, references...) if err != nil { t.Fatal(err) } return validator } func request(references contracts.ReferenceSet, value dnd.NPCInteractionList) contracts.TypedValidationRequest[dnd.NPCInteractionList] { return contracts.TypedValidationRequest[dnd.NPCInteractionList]{References: references, Value: value} } func validList(name string) dnd.NPCInteractionList { return dnd.NPCInteractionList{Interactions: []dnd.NPCInteraction{{ Name: name, Kind: dnd.NPCInteractionKindDialogue, SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 1, EndUnitID: 1}}, }}} } func registryReferences(t *testing.T, names ...string) contracts.ReferenceSet { t.Helper() npcs := make([]dnd.NPC, len(names)) for index, name := range names { npcs[index] = dnd.NPC{ID: identity.DeriveID(name), Name: name, SourceRefs: []source.SourceRef{{SourceID: "other-session", StartUnitID: index + 1, EndUnitID: index + 1}}} } content, err := npccodec.New().Encode(dnd.NPCRegistry{NPCs: npcs}) if err != nil { t.Fatal(err) } return contracts.ReferenceSet{Slots: map[string]contracts.ResolvedReferenceSlot{ npcregistry.ReferenceSlot: { Slot: contracts.ReferenceSlot{Name: npcregistry.ReferenceSlot}, Items: []contracts.ReferenceItem{{SlotName: npcregistry.ReferenceSlot, MediaType: npccodec.MediaType, Content: content, Origin: contracts.ReferenceOrigin{Type: "generated"}}}, }, }} }