package engagements import ( "context" "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" ) func TestValidatorEnforcesOneEngagementPerSubject(t *testing.T) { for _, test := range []struct { name string value dnd.EnemyEventList approved bool }{ {name: "empty", value: dnd.EnemyEventList{Events: []dnd.EnemyEvent{}}, approved: true}, {name: "one engagement", value: events(event("Ashfang", dnd.EnemyEventKindEngaged, 1)), approved: true}, {name: "same subject different evidence", value: events(event("Ashfang", dnd.EnemyEventKindEngaged, 1), event("Ashfang", dnd.EnemyEventKindEngaged, 2))}, {name: "whitespace and case variant", value: events(event("Ashfang", dnd.EnemyEventKindEngaged, 1), event(" ASHFANG ", dnd.EnemyEventKindEngaged, 2))}, {name: "different subjects", value: events(event("Ashfang", dnd.EnemyEventKindEngaged, 1), event("Briar", dnd.EnemyEventKindEngaged, 2)), approved: true}, {name: "different kinds", value: events(event("Ashfang", dnd.EnemyEventKindEngaged, 1), event("Ashfang", dnd.EnemyEventKindFled, 2)), approved: true}, {name: "malformed shape", value: dnd.EnemyEventList{Events: []dnd.EnemyEvent{{Name: "Ashfang"}, {Name: "Ashfang", Kind: dnd.EnemyEventKindEngaged, SourceRefs: refs(2)}}}, approved: true}, } { t.Run(test.name, func(t *testing.T) { result, err := New(Options{}).Validate(context.Background(), contracts.TypedValidationRequest[dnd.EnemyEventList]{Value: test.value}) if err != nil || result.Approved != test.approved { t.Fatalf("Validate() = %#v, %v; want approved=%t", result, err, test.approved) } if !test.approved && result.ReasonCode != ReasonCode { t.Fatalf("reason code = %q, want %q", result.ReasonCode, ReasonCode) } }) } validator := New(Options{}) for _, unitID := range []int{1, 2} { value := events(event("Ashfang", dnd.EnemyEventKindEngaged, unitID)) before := events(event("Ashfang", dnd.EnemyEventKindEngaged, unitID)) result, err := validator.Validate(context.Background(), contracts.TypedValidationRequest[dnd.EnemyEventList]{Value: value}) if err != nil || !result.Approved { t.Fatalf("separate scene validation = %#v, %v; want approved", result, err) } if !reflect.DeepEqual(value, before) { t.Fatalf("Validate() mutated separate scene value: got %#v, want %#v", value, before) } } } func TestValidatorRegistersStrictOptionsAndPolicy(t *testing.T) { 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 got := New(Options{}).CheckpointFingerprints(); len(got) != 1 || got[0].Name != "policy" || got[0].Value != policy { t.Fatalf("CheckpointFingerprints() = %#v", got) } if _, err := DecodeOptions(map[string]any{"unexpected": true}); err == nil || !strings.Contains(err.Error(), "unknown option") { t.Fatalf("DecodeOptions() error = %v, want strict option rejection", err) } } func events(values ...dnd.EnemyEvent) dnd.EnemyEventList { return dnd.EnemyEventList{Events: values} } func event(name string, kind dnd.EnemyEventKind, unitID int) dnd.EnemyEvent { return dnd.EnemyEvent{Name: name, Kind: kind, SourceRefs: refs(unitID)} } func refs(unitID int) []source.SourceRef { return []source.SourceRef{{SourceID: "session", StartUnitID: unitID, EndUnitID: unitID}} }