package invariants 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/npcregistry" "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/npcs/identity" npcregistry "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/npcs/registry" ) func TestValidatorApprovesNormalizedEventsAndGenericGroups(t *testing.T) { references := registryReferences(t, "Ária") value := normalizedList() result, err := newValidator(t, references).Validate(context.Background(), request(references, value)) if err != nil || !result.Approved { t.Fatalf("Validate() = %#v, %v", result, err) } } func TestValidatorRejectsNormalizationDrift(t *testing.T) { references := registryReferences(t, "Ária") for _, test := range []struct { name string mutate func(*dnd.EnemyEventList) want string }{ {"registry display", func(value *dnd.EnemyEventList) { value.Events[0].Name = " ária " }, "canonical NPC display name"}, {"generic whitespace", func(value *dnd.EnemyEventList) { value.Events[1].Name = " Remaining\tOrcs " }, "whitespace-normalized"}, {"evidence order", func(value *dnd.EnemyEventList) { value.Events[0].SourceRefs = []source.SourceRef{{SourceID: "session", StartUnitID: 20, EndUnitID: 20}, {SourceID: "session", StartUnitID: 10, EndUnitID: 10}} }, "not in canonical order"}, {"duplicate evidence", func(value *dnd.EnemyEventList) { value.Events[0].SourceRefs = append(value.Events[0].SourceRefs, value.Events[0].SourceRefs[0]) }, "duplicates the previous reference"}, {"event order", func(value *dnd.EnemyEventList) { value.Events[0], value.Events[1] = value.Events[1], value.Events[0] }, "events are not in canonical order"}, {"exact duplicate", func(value *dnd.EnemyEventList) { value.Events = append(value.Events, value.Events[0]) }, "duplicates event"}, } { t.Run(test.name, func(t *testing.T) { value := normalizedList() test.mutate(&value) result, err := newValidator(t, references).Validate(context.Background(), request(references, value)) if err != nil || result.Approved || result.ReasonCode != ReasonCode || !strings.Contains(result.Message, test.want) { t.Fatalf("Validate() = %#v, %v; want %q", result, err, test.want) } }) } } func TestValidatorDefersEarlierFailuresAndResolvesOperationRegistryWithoutMutation(t *testing.T) { references := registryReferences(t, "Ária") for _, value := range []dnd.EnemyEventList{ {Events: []dnd.EnemyEvent{{Name: "Ária"}}}, {Events: []dnd.EnemyEvent{{Name: "Ária", Kind: dnd.EnemyEventKindEngaged, SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 99, EndUnitID: 99}}}}}, } { result, err := newValidator(t, references).Validate(context.Background(), request(references, value)) if err != nil || !result.Approved { t.Fatalf("deferral = %#v, %v", result, err) } } value := normalizedList() before := cloneList(value) result, err := newValidator(t).Validate(context.Background(), request(references, value)) if err != nil || !result.Approved || !reflect.DeepEqual(value, before) { t.Fatalf("operation registry = %#v, %v; value=%#v", result, err, value) } result, err = newValidator(t).Validate(context.Background(), request(contracts.ReferenceSet{}, normalizedList())) if err != nil || result.Approved || result.ReasonCode != ReasonCode || !strings.Contains(result.Message, "required") { t.Fatalf("unbound registry = %#v, %v", result, err) } } func TestValidatorMetadataFingerprintsAndRegistrationAreSafe(t *testing.T) { references := registryReferences(t, "Ária") metadata, err := json.Marshal(newValidator(t, references).ManifestMetadata()) if err != nil || strings.Contains(string(metadata), "Ária") { t.Fatalf("ManifestMetadata() = %s, %v", metadata, err) } if got := newValidator(t, references).CheckpointFingerprints(); len(got) != 2 || got[0].Value != policy || !strings.HasPrefix(got[1].Value, "sha256:") { t.Fatalf("CheckpointFingerprints() = %#v", got) } registry := pipeline.NewValidatorRegistry() if err := Register(registry); err != nil { t.Fatal(err) } if Spec().Key != Key || Spec().ExecutionClass != contracts.ExecutionClassDeterministic { t.Fatalf("Spec() = %#v", Spec()) } 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.EnemyEventList) contracts.TypedValidationRequest[dnd.EnemyEventList] { return contracts.TypedValidationRequest[dnd.EnemyEventList]{Source: document(), References: references, Value: value} } func document() *source.SourceDocument { return &source.SourceDocument{ID: "session", Units: []source.SourceUnit{{ID: 10}, {ID: 20}}} } func normalizedList() dnd.EnemyEventList { return dnd.EnemyEventList{Events: []dnd.EnemyEvent{ {Name: "Ária", Kind: dnd.EnemyEventKindEngaged, SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 10, EndUnitID: 10}}}, {Name: "Remaining Orcs", Kind: dnd.EnemyEventKindFled, SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 20, EndUnitID: 20}}}, }} } func cloneList(value dnd.EnemyEventList) dnd.EnemyEventList { clone := dnd.EnemyEventList{Events: make([]dnd.EnemyEvent, len(value.Events))} for index, event := range value.Events { clone.Events[index] = event clone.Events[index].SourceRefs = append([]source.SourceRef(nil), event.SourceRefs...) } return clone } 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", 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"}}}}}} }