package validators import ( "context" "strings" "testing" "gitea.maximumdirect.net/eric/audita/internal/core/config" "gitea.maximumdirect.net/eric/audita/internal/core/schema" "gitea.maximumdirect.net/eric/audita/internal/framework/proposals" ) func mkCandidate(index int, segID int, orig, corr string, conf float64) proposals.EnrichedCorrectionProposal { return proposals.EnrichedCorrectionProposal{ CorrectionProposal: proposals.CorrectionProposal{TargetSegmentID: segID, OriginalText: orig, CorrectedText: corr, Confidence: conf}, ProposalMetadata: proposals.ProposalMetadata{ProposalIndex: index, ModuleKey: "grammar", ModuleInstance: "grammar_1"}, } } func TestEnforceDecisionCardinalitySuccess(t *testing.T) { candidates := []proposals.EnrichedCorrectionProposal{mkCandidate(0, 1, "teh", "the", 0.9), mkCandidate(1, 1, "recieve", "receive", 0.9)} decisions := []Decision{{ProposalIndex: 0, Approved: true}, {ProposalIndex: 1, Approved: false}} if err := EnforceDecisionCardinality(candidates, decisions); err != nil { t.Fatalf("unexpected error: %v", err) } } func TestEnforceDecisionCardinalityMissingDecision(t *testing.T) { candidates := []proposals.EnrichedCorrectionProposal{mkCandidate(0, 1, "teh", "the", 0.9), mkCandidate(1, 1, "recieve", "receive", 0.9)} err := EnforceDecisionCardinality(candidates, []Decision{{ProposalIndex: 0, Approved: true}}) if err == nil || !strings.Contains(err.Error(), "missing decision") { t.Fatalf("expected missing decision error, got %v", err) } } func TestEnforceDecisionCardinalityDuplicateDecision(t *testing.T) { candidates := []proposals.EnrichedCorrectionProposal{mkCandidate(0, 1, "teh", "the", 0.9)} err := EnforceDecisionCardinality(candidates, []Decision{{ProposalIndex: 0, Approved: true}, {ProposalIndex: 0, Approved: false}}) if err == nil || !strings.Contains(err.Error(), "duplicate decision") { t.Fatalf("expected duplicate decision error, got %v", err) } } func TestEnforceDecisionCardinalityUnknownDecision(t *testing.T) { candidates := []proposals.EnrichedCorrectionProposal{mkCandidate(0, 1, "teh", "the", 0.9)} err := EnforceDecisionCardinality(candidates, []Decision{{ProposalIndex: 99, Approved: true}}) if err == nil || !strings.Contains(err.Error(), "unknown decision") { t.Fatalf("expected unknown decision error, got %v", err) } } func TestConfidenceThresholdValidator(t *testing.T) { cfg := config.Default() cfg.Thresholds.Grammar = 0.8 req := Request{ModuleKey: "grammar", Config: &cfg, CandidateProposal: []proposals.EnrichedCorrectionProposal{ mkCandidate(0, 1, "teh", "the", 0.9), mkCandidate(1, 1, "recieve", "receive", 0.7), }} res, err := (ConfidenceThresholdValidator{}).Validate(context.Background(), req) if err != nil { t.Fatalf("Validate error: %v", err) } if !res.Decisions[0].Approved || res.Decisions[0].ReasonCode != ReasonApproved { t.Fatalf("expected first decision approved, got %+v", res.Decisions[0]) } if res.Decisions[1].Approved || res.Decisions[1].ReasonCode != ReasonLowConfidence { t.Fatalf("expected second decision low confidence reject, got %+v", res.Decisions[1]) } } func TestProposalShapeValidator(t *testing.T) { req := Request{CandidateProposal: []proposals.EnrichedCorrectionProposal{ mkCandidate(0, 1, "teh", "the", 0.9), mkCandidate(1, 0, "teh", "the", 0.9), mkCandidate(2, 1, " ", "the", 0.9), mkCandidate(3, 1, "teh", "the", 1.5), }} res, err := (ProposalShapeValidator{}).Validate(context.Background(), req) if err != nil { t.Fatalf("Validate error: %v", err) } if !res.Decisions[0].Approved { t.Fatalf("expected proposal 0 approved") } if res.Decisions[1].ReasonCode != ReasonInvalidTargetSegment { t.Fatalf("expected invalid target segment rejection, got %+v", res.Decisions[1]) } if res.Decisions[2].ReasonCode != ReasonEmptyOriginalText { t.Fatalf("expected empty original rejection, got %+v", res.Decisions[2]) } if res.Decisions[3].ReasonCode != ReasonInvalidConfidence { t.Fatalf("expected invalid confidence rejection, got %+v", res.Decisions[3]) } } func TestOriginalTextPresenceValidator(t *testing.T) { req := Request{WorkingTranscript: &schema.Transcript{Segments: []schema.Segment{{ID: 1, Text: "hello world"}}}, CandidateProposal: []proposals.EnrichedCorrectionProposal{ mkCandidate(0, 1, "hello", "hi", 0.9), mkCandidate(1, 1, "missing", "x", 0.9), mkCandidate(2, 5, "hello", "hi", 0.9), }} res, err := (OriginalTextPresenceValidator{}).Validate(context.Background(), req) if err != nil { t.Fatalf("Validate error: %v", err) } if !res.Decisions[0].Approved { t.Fatalf("expected proposal 0 approved") } if res.Decisions[1].ReasonCode != ReasonMissingOriginalText { t.Fatalf("expected missing_original_text, got %+v", res.Decisions[1]) } if res.Decisions[2].ReasonCode != ReasonMissingTargetSegment { t.Fatalf("expected missing_target_segment, got %+v", res.Decisions[2]) } } func TestNonEmptyCorrectionValidator(t *testing.T) { req := Request{ WorkingTranscript: &schema.Transcript{Segments: []schema.Segment{{ID: 1, Text: "hello world"}}}, ReplacementPolicy: proposals.ReplacementPolicyRequireUnique, CandidateProposal: []proposals.EnrichedCorrectionProposal{ mkCandidate(0, 1, "hello", "hi", 0.9), mkCandidate(1, 1, "hello world", " ", 0.9), }} res, err := (NonEmptyCorrectionValidator{}).Validate(context.Background(), req) if err != nil { t.Fatalf("Validate error: %v", err) } if !res.Decisions[0].Approved { t.Fatalf("expected proposal 0 approved") } if res.Decisions[1].ReasonCode != ReasonEmptyResultingText { t.Fatalf("expected empty_resulting_segment, got %+v", res.Decisions[1]) } } func TestNoEffectValidator(t *testing.T) { req := Request{CandidateProposal: []proposals.EnrichedCorrectionProposal{ mkCandidate(0, 1, "hello", "hello", 0.9), mkCandidate(1, 1, "hello", "hi", 0.9), }} res, err := (NoEffectValidator{}).Validate(context.Background(), req) if err != nil { t.Fatalf("Validate error: %v", err) } if res.Decisions[0].ReasonCode != ReasonNoEffect || res.Decisions[0].Approved { t.Fatalf("expected no_effect rejection, got %+v", res.Decisions[0]) } if !res.Decisions[1].Approved { t.Fatalf("expected proposal 1 approved") } } func TestValidateProtectedTerms(t *testing.T) { glossary := &schema.Glossary{Entries: []schema.GlossaryEntry{{Name: "OpenAI", Aliases: []string{"Open AI"}, Plural: "OpenAIs", Category: "brand", Summary: "brand"}}} req := Request{ Glossary: glossary, ModuleKey: "grammar", CandidateProposal: []proposals.EnrichedCorrectionProposal{ mkCandidate(0, 1, "OpenAI", "Open A I", 0.9), mkCandidate(1, 1, "teh", "the", 0.9), }, } res, err := ValidateProtectedTerms(req, false) if err != nil { t.Fatalf("Validate error: %v", err) } if res.Decisions[0].ReasonCode != ReasonProtectedGlossaryTerm || res.Decisions[0].Approved { t.Fatalf("expected protected glossary rejection, got %+v", res.Decisions[0]) } if !res.Decisions[1].Approved { t.Fatalf("expected non-glossary proposal approved") } } func TestStableReasonCodes(t *testing.T) { codes := []string{ ReasonApproved, ReasonLowConfidence, ReasonMissingOriginalText, ReasonMissingTargetSegment, ReasonEmptyResultingText, ReasonNoEffect, ReasonProtectedGlossaryTerm, ReasonInvalidTargetSegment, ReasonEmptyOriginalText, ReasonInvalidConfidence, ReasonValidatorMalformed, ReasonValidatorInputTooLarge, } for _, code := range codes { if strings.TrimSpace(code) == "" { t.Fatalf("reason code must not be empty") } } }