package validators import ( "reflect" "sort" "strings" "testing" domain "gitea.maximumdirect.net/eric/audita/v3/internal/core/glossary" "gitea.maximumdirect.net/eric/audita/v3/internal/core/schema" "gitea.maximumdirect.net/eric/audita/v3/internal/framework/proposals" ) func TestRecognizedTermsDeduplicateAndIncludePlurals(t *testing.T) { plural := "Hranks" entries := []domain.Entry{{ID: "jesters", Name: "Jesters", Aliases: []string{"Jester", " "}}, {ID: "hrank", Name: "Hrank", Aliases: []string{"hrank", "Hrank"}, Plural: &plural}, {ID: "lyra", Name: "Lyra"}} var got []string for _, term := range domain.Vocabulary(entries, nil) { got = append(got, term.Spelling) } sort.Strings(got) want := []string{"Hrank", "Hranks", "Jester", "Jesters", "Jesterss", "Lyra", "Lyras"} if !reflect.DeepEqual(got, want) { t.Fatalf("got %v want %v", got, want) } if len(domain.Vocabulary(nil, nil)) != 0 { t.Fatal("empty input produced terms") } } func TestValidateProtectedTermsAppliesToNonGlossaryModule(t *testing.T) { req := Request{ ModuleKey: "grammar", Glossary: []domain.Entry{{ID: "jesters", Name: "Jesters", Category: "faction"}}, ProtectedTerms: domain.Vocabulary([]domain.Entry{{ID: "jesters", Name: "Jesters"}}, nil), CandidateProposal: []proposals.EnrichedCorrectionProposal{ { CorrectionProposal: proposals.CorrectionProposal{ TargetSegmentID: 1, OriginalText: "Jesters", CorrectedText: "Gestures", Confidence: 0.9, }, ProposalMetadata: proposals.ProposalMetadata{ProposalIndex: 0}, }, { CorrectionProposal: proposals.CorrectionProposal{TargetSegmentID: 1, OriginalText: "Jesters", CorrectedText: "", Confidence: 0.9}, ProposalMetadata: proposals.ProposalMetadata{ProposalIndex: 1}, }, }, } res, err := ValidateProtectedTerms(req) if err != nil { t.Fatalf("validator error: %v", err) } if len(res.Decisions) != 2 || res.Decisions[0].Approved || res.Decisions[1].Approved || res.Decisions[1].ReasonCode != ReasonProtectedGlossaryTerm { t.Fatalf("expected protected-term rejection, got %+v", res.Decisions) } } func TestValidateProtectedTermsGlossaryStageAllowsProtectedTermSwap(t *testing.T) { req := Request{ ModuleKey: "glossary", Glossary: []domain.Entry{{ID: "jesters", Name: "Jesters", Category: "faction"}}, CandidateProposal: []proposals.EnrichedCorrectionProposal{ { CorrectionProposal: proposals.CorrectionProposal{ TargetSegmentID: 1, OriginalText: "gestures", CorrectedText: "Jesters", Confidence: 0.9, }, ProposalMetadata: proposals.ProposalMetadata{ProposalIndex: 0}, }, }, } res, err := ValidateProtectedTerms(req) if err != nil { t.Fatalf("validator error: %v", err) } if len(res.Decisions) != 1 || !res.Decisions[0].Approved { t.Fatalf("expected approval for glossary-stage protected term correction, got %+v", res.Decisions) } } func TestProtectedNameRepetitionRequiresSurvivingIdentityAndSpokenWordScope(t *testing.T) { for _, tc := range []struct { name, module, before, original, replacement string allowed bool }{ {"whole repetition", "spoken_word", "Jesters Jesters arrived", "Jesters Jesters", "Jesters", true}, {"span deletion", "spoken_word", "Jesters, Jesters arrived", "Jesters, ", "", true}, {"last reference", "spoken_word", "Jesters arrived", "Jesters ", "", false}, {"different identity", "spoken_word", "Jesters Jesters arrived", "Jesters Jesters", "Lyra", false}, {"spelling degraded", "spoken_word", "Jesters Jesters arrived", "Jesters Jesters", "jestERS", false}, {"grammar unchanged", "grammar", "Jesters Jesters arrived", "Jesters Jesters", "Jesters", false}, } { t.Run(tc.name, func(t *testing.T) { req := Request{ModuleKey: tc.module, ReplacementPolicy: proposals.ReplacementPolicyRequireUnique, WorkingTranscript: &schema.Transcript{Segments: []schema.Segment{{ID: 1, Text: tc.before}}}, ProtectedTerms: domain.Vocabulary([]domain.Entry{{ID: "jesters", Name: "Jesters"}, {ID: "lyra", Name: "Lyra"}}, nil), CandidateProposal: []proposals.EnrichedCorrectionProposal{{CorrectionProposal: proposals.CorrectionProposal{TargetSegmentID: 1, OriginalText: tc.original, CorrectedText: tc.replacement, Confidence: 1}}}, } result, err := ValidateProtectedTerms(req) if err != nil || len(result.Decisions) != 1 || result.Decisions[0].Approved != tc.allowed { t.Fatalf("result=%+v err=%v", result, err) } }) } } func TestProtectedNameFindingIdentifiesRemovedAlias(t *testing.T) { req := Request{ ModuleKey: "glossary", IndependentEvidence: true, ReplacementPolicy: proposals.ReplacementPolicyReplaceAll, WorkingTranscript: &schema.Transcript{Segments: []schema.Segment{ {ID: 1, Text: "Mira spoke to Captain Mira"}, }}, ProtectedTerms: domain.Vocabulary([]domain.Entry{{ID: "mira", Name: "Mira", Aliases: []string{"Captain Mira"}}}, nil), CandidateProposal: []proposals.EnrichedCorrectionProposal{{ CorrectionProposal: proposals.CorrectionProposal{ TargetSegmentID: 1, OriginalText: "Captain Mira", CorrectedText: "the captain", Confidence: 0.9, }, }}, } result, err := ValidateProtectedTerms(req) if err != nil || len(result.Decisions) != 1 { t.Fatalf("result=%+v err=%v", result, err) } decision := result.Decisions[0] if decision.Approved || decision.ReasonCode != ReasonProtectedGlossaryTerm || !strings.Contains(decision.Message, `"Captain Mira"`) { t.Fatalf("finding did not identify the removed alias: %+v", decision) } }