package shape import ( "context" "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 TestValidatorApprovesWellFormedSpellPayload(t *testing.T) { result, err := New(Options{}).Validate(context.Background(), requestWithValue(validSpellList())) if err != nil { t.Fatalf("Validate() error = %v, want nil", err) } if !result.Approved { t.Fatalf("Validate() = %#v, want approved", result) } } func TestValidatorRejectsMissingSpellList(t *testing.T) { result, err := New(Options{}).Validate(context.Background(), requestWithValue(dnd.SpellList{})) if err != nil { t.Fatalf("Validate() error = %v, want nil", err) } if result.Approved { t.Fatalf("Approved = true, want false") } if result.ReasonCode != ReasonCode { t.Fatalf("ReasonCode = %q, want %q", result.ReasonCode, ReasonCode) } } func TestValidatorRejectsMissingRequiredSpellFields(t *testing.T) { tests := []struct { name string mutate func(*dnd.SpellCast) }{ {name: "blank caster", mutate: func(cast *dnd.SpellCast) { cast.Caster = " " }}, {name: "blank spell", mutate: func(cast *dnd.SpellCast) { cast.Spell = "" }}, {name: "empty evidence", mutate: func(cast *dnd.SpellCast) { cast.SourceRefs = nil }}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { value := validSpellList() test.mutate(&value.SpellCasts[0]) result, err := New(Options{}).Validate(context.Background(), requestWithValue(value)) if err != nil || result.Approved || result.ReasonCode != ReasonCode { t.Fatalf("Validate() = %#v, %v; want shape rejection", result, err) } }) } } func TestValidatorReportsAllSpellDefectsWithContextualGuidance(t *testing.T) { value := dnd.SpellList{SpellCasts: []dnd.SpellCast{ {Caster: "", Spell: "Fire Bolt", SourceRefs: refsAt(4)}, {Caster: "Aria", Spell: "", SourceRefs: nil}, }} result, err := New(Options{}).Validate(context.Background(), requestWithValue(value)) if err != nil || result.Approved { t.Fatalf("Validate() = %#v, %v; want rejection", result, err) } for _, want := range []string{"spell_casts[0].caster", "spell_casts[1].spell", "spell_casts[1].source_refs"} { if !strings.Contains(result.Message, want) { t.Fatalf("Message = %q, want %q", result.Message, want) } } for _, want := range []string{"Fire Bolt", "source unit 4", "Aria", "complete replacement"} { if !strings.Contains(result.CorrectionGuidance, want) { t.Fatalf("CorrectionGuidance = %q, want %q", result.CorrectionGuidance, want) } } if strings.Contains(result.CorrectionGuidance, "spell_casts[") { t.Fatalf("CorrectionGuidance exposed operator path: %q", result.CorrectionGuidance) } } func TestValidatorSpecCheckpointAndRegistration(t *testing.T) { if got := New(Options{}).CheckpointFingerprints(); len(got) != 1 || got[0].Name != "policy" || got[0].Value != policy { t.Fatalf("CheckpointFingerprints() = %#v, want local policy", got) } if spec := Spec(); spec.Key != Key || spec.ExecutionClass != contracts.ExecutionClassDeterministic { t.Fatalf("Spec() = %#v, want deterministic shape validator", spec) } registry := pipeline.NewValidatorRegistry() if err := Register(registry); err != nil { t.Fatalf("Register() error = %v, want nil", err) } if _, err := DecodeOptions(map[string]any{"unexpected": true}); err == nil { t.Fatal("DecodeOptions() error = nil, want unknown option error") } } func requestWithValue(value dnd.SpellList) contracts.TypedValidationRequest[dnd.SpellList] { return contracts.TypedValidationRequest[dnd.SpellList]{Value: value} } func validSpellList() dnd.SpellList { return dnd.SpellList{SpellCasts: []dnd.SpellCast{{Caster: "Aria", Spell: "Cure Wounds", SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 1, EndUnitID: 1}}}}} } func refsAt(unitID int) []source.SourceRef { return []source.SourceRef{{SourceID: "session", StartUnitID: unitID, EndUnitID: unitID}} }