197 lines
7.4 KiB
Go
197 lines
7.4 KiB
Go
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")
|
|
}
|
|
}
|
|
}
|