Align validator identity and span domain checks
This commit is contained in:
@@ -109,6 +109,26 @@ func TestApplyProposalsStaleProposalSkippedAfterEarlierChange(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyProposalsRechecksBlankResultAfterEarlierDeletion(t *testing.T) {
|
||||
transcript := &schema.Transcript{Segments: []schema.Segment{{ID: 1, Text: "a b"}}}
|
||||
proposed := []EnrichedCorrectionProposal{
|
||||
{CorrectionProposal: CorrectionProposal{TargetSegmentID: 1, OriginalText: "a", CorrectedText: "", Confidence: 1}, ProposalMetadata: ProposalMetadata{ProposalIndex: 0}},
|
||||
{CorrectionProposal: CorrectionProposal{TargetSegmentID: 1, OriginalText: "b", CorrectedText: "", Confidence: 1}, ProposalMetadata: ProposalMetadata{ProposalIndex: 1}},
|
||||
}
|
||||
for _, candidate := range proposed {
|
||||
if preview := PreviewProposalForSegment(&transcript.Segments[0], candidate.CorrectionProposal, ReplacementPolicyRequireUnique); !preview.Applicable {
|
||||
t.Fatalf("proposal must be initially applicable: %+v", preview)
|
||||
}
|
||||
}
|
||||
result := ApplyProposals(transcript, proposed, ReplacementPolicyRequireUnique)
|
||||
if got := result.Transcript.Segments[0].Text; got != " b" {
|
||||
t.Fatalf("expected literal surviving text, got %q", got)
|
||||
}
|
||||
if len(result.Applied) != 1 || len(result.Skipped) != 1 || result.Skipped[0].SkipReason != SkipReasonEmptyResultingText {
|
||||
t.Fatalf("expected second deletion skipped after reevaluation: %+v", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyProposalsMissingSegmentSkip(t *testing.T) {
|
||||
transcript := &schema.Transcript{Segments: []schema.Segment{{ID: 1, Text: "hello"}}}
|
||||
proposals := []EnrichedCorrectionProposal{
|
||||
|
||||
@@ -139,6 +139,30 @@ func TestPreviewProposalForSegmentAllowsEmptyCorrectedTextWhenSegmentRemainsNonE
|
||||
}
|
||||
}
|
||||
|
||||
func TestPreviewProposalForSegmentWhitespaceSpan(t *testing.T) {
|
||||
proposal := CorrectionProposal{TargetSegmentID: 8, OriginalText: " ", CorrectedText: " ", Confidence: 0.9}
|
||||
unique := PreviewProposalForSegment(&schema.Segment{ID: 8, Text: "hello world"}, proposal, ReplacementPolicyRequireUnique)
|
||||
if !unique.Applicable || unique.CorrectedSegmentText != "hello world" || unique.ReplacementCount != 1 {
|
||||
t.Fatalf("unexpected spacing preview: %+v", unique)
|
||||
}
|
||||
segment := &schema.Segment{ID: 8, Text: "a b c"}
|
||||
ambiguous := PreviewProposalForSegment(segment, proposal, ReplacementPolicyRequireUnique)
|
||||
if ambiguous.Applicable || ambiguous.SkipReason != SkipReasonAmbiguousOriginal {
|
||||
t.Fatalf("expected ambiguity under require_unique: %+v", ambiguous)
|
||||
}
|
||||
all := PreviewProposalForSegment(segment, proposal, ReplacementPolicyReplaceAll)
|
||||
if !all.Applicable || all.CorrectedSegmentText != "a b c" || all.ReplacementCount != 2 {
|
||||
t.Fatalf("expected replace_all to correct both spans: %+v", all)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPreviewProposalForSegmentRejectsEmptyOriginalText(t *testing.T) {
|
||||
preview := PreviewProposalForSegment(&schema.Segment{ID: 8, Text: "hello"}, CorrectionProposal{TargetSegmentID: 8, OriginalText: "", CorrectedText: "x", Confidence: 0.9}, ReplacementPolicyRequireUnique)
|
||||
if preview.Applicable || preview.SkipReason != SkipReasonInvalidProposal {
|
||||
t.Fatalf("expected invalid empty original: %+v", preview)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPreviewProposalForSegmentRejectsEmptyResultingSegment(t *testing.T) {
|
||||
segment := &schema.Segment{ID: 8, Text: "uh"}
|
||||
proposal := CorrectionProposal{
|
||||
|
||||
@@ -2,7 +2,6 @@ package proposals
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// CorrectionProposal is the base proposal emitted by correction modules.
|
||||
@@ -35,7 +34,7 @@ func (p CorrectionProposal) Validate() error {
|
||||
if p.TargetSegmentID <= 0 {
|
||||
return fmt.Errorf("proposal id must be positive")
|
||||
}
|
||||
if strings.TrimSpace(p.OriginalText) == "" {
|
||||
if p.OriginalText == "" {
|
||||
return fmt.Errorf("proposal original_text must not be empty")
|
||||
}
|
||||
if p.Confidence < 0.0 || p.Confidence > 1.0 {
|
||||
|
||||
@@ -48,6 +48,13 @@ func TestCorrectionProposalValidate_AllowsEmptyCorrectedText(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCorrectionProposalValidate_AllowsWhitespaceOriginalText(t *testing.T) {
|
||||
proposal := CorrectionProposal{TargetSegmentID: 42, OriginalText: " ", CorrectedText: " ", Confidence: 0.95}
|
||||
if err := proposal.Validate(); err != nil {
|
||||
t.Fatalf("expected whitespace span to be valid, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCorrectionProposalValidate_InvalidConfidence(t *testing.T) {
|
||||
proposal := CorrectionProposal{
|
||||
TargetSegmentID: 42,
|
||||
|
||||
@@ -873,6 +873,36 @@ func TestRunnerApplicationSkipAfterValidatorApprovalReported(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunnerApplyConflictAfterTwoInitiallyValidDeletions(t *testing.T) {
|
||||
transcript := &schema.Transcript{Segments: []schema.Segment{{ID: 1, Text: "a b"}}}
|
||||
r := New(fakeFactory{modules: map[string]contracts.TranscriptModule{
|
||||
"m": fakeModule{key: "m", policy: proposals.ReplacementPolicyRequireUnique, validators: []contracts.Validator{validators.ProposalShapeValidator{}, validators.NonEmptyResultingSegmentValidator{}}, proposeF: func(req contracts.ProposalRequest) ([]proposals.CorrectionProposal, error) {
|
||||
return []proposals.CorrectionProposal{
|
||||
{TargetSegmentID: 1, OriginalText: "a", CorrectedText: "", Confidence: 1},
|
||||
{TargetSegmentID: 1, OriginalText: "b", CorrectedText: "", Confidence: 1},
|
||||
}, nil
|
||||
}},
|
||||
}})
|
||||
out, err := r.Run(context.Background(), RunInput{Transcript: transcript, ModuleSpecs: []contracts.ModuleRunSpec{{ModuleKey: "m", InstanceName: "m"}}, CaptureCandidates: true})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := out.FinalTranscript.Segments[0].Text; got != " b" {
|
||||
t.Fatalf("expected first deletion only, got %q", got)
|
||||
}
|
||||
if len(out.ModuleResults) != 1 || len(out.ModuleResults[0].SkippedChanges) != 1 || out.ModuleResults[0].SkippedChanges[0].SkipReason != proposals.SkipReasonEmptyResultingText {
|
||||
t.Fatalf("expected skipped debug evidence: %+v", out.ModuleResults)
|
||||
}
|
||||
if len(out.ModuleResults[0].ValidatorRejected) != 0 || len(out.WorkUnits) != 1 || out.WorkUnits[0].CorrectionsApplied != 1 {
|
||||
t.Fatalf("unexpected validation or application outcome: %+v", out)
|
||||
}
|
||||
for _, operation := range out.WorkUnits[0].Operations {
|
||||
if operation.FailureReason != "" || len(operation.RecoveryReasons) != 0 {
|
||||
t.Fatalf("apply-time conflict must not count as model error or filtering recovery: %+v", operation)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ptrConfig(c config.Config) *config.Config { return &c }
|
||||
|
||||
type fakeStructuredClient struct {
|
||||
|
||||
@@ -19,7 +19,7 @@ func (v ProposalShapeValidator) Validate(_ context.Context, req Request) (Result
|
||||
switch {
|
||||
case c.TargetSegmentID <= 0:
|
||||
decisions = append(decisions, rejection(c.ProposalIndex, ReasonInvalidTargetSegment, "proposal target segment id must be positive"))
|
||||
case strings.TrimSpace(c.OriginalText) == "":
|
||||
case c.OriginalText == "":
|
||||
decisions = append(decisions, rejection(c.ProposalIndex, ReasonEmptyOriginalText, "proposal original_text must not be empty"))
|
||||
case c.Confidence < 0.0 || c.Confidence > 1.0:
|
||||
decisions = append(decisions, rejection(c.ProposalIndex, ReasonInvalidConfidence, "proposal confidence must be between 0.0 and 1.0"))
|
||||
@@ -84,11 +84,11 @@ func (v OriginalTextPresenceValidator) Validate(_ context.Context, req Request)
|
||||
return Result{ValidatorName: v.Name(), Decisions: decisions}, nil
|
||||
}
|
||||
|
||||
type NonEmptyCorrectionValidator struct{}
|
||||
type NonEmptyResultingSegmentValidator struct{}
|
||||
|
||||
func (v NonEmptyCorrectionValidator) Name() string { return "non_empty_corrected_text" }
|
||||
func (v NonEmptyResultingSegmentValidator) Name() string { return "non_empty_resulting_segment" }
|
||||
|
||||
func (v NonEmptyCorrectionValidator) Validate(_ context.Context, req Request) (Result, error) {
|
||||
func (v NonEmptyResultingSegmentValidator) Validate(_ context.Context, req Request) (Result, error) {
|
||||
segmentsByID := make(map[int]schema.Segment)
|
||||
if req.WorkingTranscript != nil {
|
||||
for _, seg := range req.WorkingTranscript.Segments {
|
||||
|
||||
@@ -46,13 +46,17 @@ func TestValidateProtectedTermsAppliesToNonGlossaryModule(t *testing.T) {
|
||||
},
|
||||
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, false)
|
||||
if err != nil {
|
||||
t.Fatalf("validator error: %v", err)
|
||||
}
|
||||
if len(res.Decisions) != 1 || res.Decisions[0].Approved {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,8 +72,9 @@ 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(2, 1, "", "the", 0.9),
|
||||
mkCandidate(3, 1, "teh", "the", 1.5),
|
||||
mkCandidate(4, 1, " ", " ", 0.9),
|
||||
}}
|
||||
res, err := (ProposalShapeValidator{}).Validate(context.Background(), req)
|
||||
if err != nil {
|
||||
@@ -91,6 +92,9 @@ func TestProposalShapeValidator(t *testing.T) {
|
||||
if res.Decisions[3].ReasonCode != ReasonInvalidConfidence {
|
||||
t.Fatalf("expected invalid confidence rejection, got %+v", res.Decisions[3])
|
||||
}
|
||||
if !res.Decisions[4].Approved {
|
||||
t.Fatalf("expected whitespace original approved, got %+v", res.Decisions[4])
|
||||
}
|
||||
}
|
||||
|
||||
func TestOriginalTextPresenceValidator(t *testing.T) {
|
||||
@@ -114,15 +118,17 @@ func TestOriginalTextPresenceValidator(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestNonEmptyCorrectionValidator(t *testing.T) {
|
||||
func TestNonEmptyResultingSegmentValidator(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),
|
||||
mkCandidate(2, 1, "hello ", "", 0.9),
|
||||
mkCandidate(3, 1, "hello world", "", 0.9),
|
||||
}}
|
||||
res, err := (NonEmptyCorrectionValidator{}).Validate(context.Background(), req)
|
||||
res, err := (NonEmptyResultingSegmentValidator{}).Validate(context.Background(), req)
|
||||
if err != nil {
|
||||
t.Fatalf("Validate error: %v", err)
|
||||
}
|
||||
@@ -132,6 +138,9 @@ func TestNonEmptyCorrectionValidator(t *testing.T) {
|
||||
if res.Decisions[1].ReasonCode != ReasonEmptyResultingText {
|
||||
t.Fatalf("expected empty_resulting_segment, got %+v", res.Decisions[1])
|
||||
}
|
||||
if !res.Decisions[2].Approved || res.Decisions[3].ReasonCode != ReasonEmptyResultingText {
|
||||
t.Fatalf("unexpected deletion decisions: %+v", res.Decisions)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNoEffectValidator(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user