package validator import ( "context" "errors" "regexp" "testing" ) func mustRegexps(patterns ...string) []*regexp.Regexp { out := make([]*regexp.Regexp, 0, len(patterns)) for _, p := range patterns { out = append(out, regexp.MustCompile(p)) } return out } // fakeValidator is a configurable Validator stub for composite tests. type fakeValidator struct { name string verdict Verdict err error calls int } func (f *fakeValidator) Name() string { return f.name } func (f *fakeValidator) Validate(_ context.Context, _ DiffInput) (Verdict, error) { f.calls++ return f.verdict, f.err } var errFakeBackend = errors.New("fake backend down") // -- AllMustApprove -- func TestComposite_AllMustApprove_Empty(t *testing.T) { c := NewCompositeValidator("all", "v1", ModeAllMustApprove) got, err := c.Validate(context.Background(), DiffInput{}) if err != nil { t.Fatalf("empty composite: %v", err) } if !got.Approved { t.Errorf("empty composite should approve, got %+v", got) } if got.Score != 1.0 { t.Errorf("empty composite score should be 1.0, got %v", got.Score) } if got.ValidatorName != "all" { t.Errorf("ValidatorName mismatch: got %q", got.ValidatorName) } } func TestComposite_AllMustApprove_AllApprove(t *testing.T) { a := &fakeValidator{name: "a", verdict: Verdict{Approved: true, Score: 0.9}} b := &fakeValidator{name: "b", verdict: Verdict{Approved: true, Score: 0.8}} c := NewCompositeValidator("all", "v1", ModeAllMustApprove, a, b) got, err := c.Validate(context.Background(), DiffInput{}) if err != nil { t.Fatalf("all-approve: %v", err) } if !got.Approved { t.Errorf("should approve, got %+v", got) } if got.Score != 0.8 { t.Errorf("score should be min (0.8), got %v", got.Score) } if a.calls != 1 || b.calls != 1 { t.Errorf("both children should run once, got a=%d b=%d", a.calls, b.calls) } } func TestComposite_AllMustApprove_OneRejectsAllStillRun(t *testing.T) { // AllMustApprove runs every child even after one rejects (need full // audit breakdown). a := &fakeValidator{name: "a", verdict: Verdict{Approved: true, Score: 1}} b := &fakeValidator{name: "b", verdict: Verdict{Approved: false, Score: 0.2, Severity: SeverityBlock, Reason: "boom"}} co := &fakeValidator{name: "c", verdict: Verdict{Approved: true, Score: 1}} comp := NewCompositeValidator("all", "v1", ModeAllMustApprove, a, b, co) got, err := comp.Validate(context.Background(), DiffInput{}) if err != nil { t.Fatalf("unexpected error: %v", err) } if got.Approved { t.Errorf("should block, got %+v", got) } if got.Severity != SeverityBlock { t.Errorf("severity should be Block, got %v", got.Severity) } if got.Score != 0.2 { t.Errorf("score should be min (0.2), got %v", got.Score) } if co.calls != 1 { t.Errorf("AllMustApprove should run every child, c should be called once, got co=%d", co.calls) } if _, ok := got.Details["b"]; !ok { t.Errorf("details should include rejecting child b, got %v", got.Details) } } func TestComposite_AllMustApprove_ChildError(t *testing.T) { a := &fakeValidator{name: "a", verdict: Verdict{Approved: true, Score: 1}} b := &fakeValidator{name: "b", err: errFakeBackend} c := &fakeValidator{name: "c", verdict: Verdict{Approved: true, Score: 1}} comp := NewCompositeValidator("all", "v1", ModeAllMustApprove, a, b, c) got, err := comp.Validate(context.Background(), DiffInput{}) if !errors.Is(err, errFakeBackend) { t.Fatalf("expected errFakeBackend via errors.Is, got %v", err) } if got.Approved { t.Errorf("should block on child error, got %+v", got) } if got.Severity != SeverityBlock { t.Errorf("child error should escalate to Block, got %v", got.Severity) } if got.Score != 0.0 { t.Errorf("child error should set minScore to 0, got %v", got.Score) } if c.calls != 1 { t.Errorf("AllMustApprove should still run c after b errors, got c=%d", c.calls) } } func TestComposite_AllMustApprove_WarnDoesNotEscalate(t *testing.T) { // A child that rejects with Warn severity should not escalate the // composite's severity to Block; composite's Approved stays false. a := &fakeValidator{name: "a", verdict: Verdict{Approved: false, Severity: SeverityWarn, Reason: "minor"}} b := &fakeValidator{name: "b", verdict: Verdict{Approved: true}} c := NewCompositeValidator("all", "v1", ModeAllMustApprove, a, b) got, _ := c.Validate(context.Background(), DiffInput{}) if got.Approved { t.Errorf("warn reject should not approve overall, got %+v", got) } if got.Severity != SeverityWarn { t.Errorf("warn reject should stay Warn, got %v", got.Severity) } } // -- Waterfall -- func TestComposite_Waterfall_Empty(t *testing.T) { c := NewCompositeValidator("w", "v1", ModeWaterfall) got, err := c.Validate(context.Background(), DiffInput{}) if err != nil { t.Fatalf("empty waterfall: %v", err) } if !got.Approved { t.Errorf("empty waterfall should approve, got %+v", got) } } func TestComposite_Waterfall_AllApprove(t *testing.T) { a := &fakeValidator{name: "a", verdict: Verdict{Approved: true, Score: 1}} b := &fakeValidator{name: "b", verdict: Verdict{Approved: true, Score: 0.8}} c := NewCompositeValidator("w", "v1", ModeWaterfall, a, b) got, err := c.Validate(context.Background(), DiffInput{}) if err != nil { t.Fatalf("all-approve: %v", err) } if !got.Approved { t.Errorf("should approve, got %+v", got) } if got.Score != 0.8 { t.Errorf("score should be min (0.8), got %v", got.Score) } if a.calls != 1 || b.calls != 1 { t.Errorf("both should run when all approve, got a=%d b=%d", a.calls, b.calls) } } func TestComposite_Waterfall_ShortCircuitsOnReject(t *testing.T) { a := &fakeValidator{name: "a", verdict: Verdict{Approved: false, Severity: SeverityBlock, Reason: "no go"}} b := &fakeValidator{name: "b", verdict: Verdict{Approved: true}} c := NewCompositeValidator("w", "v1", ModeWaterfall, a, b) got, err := c.Validate(context.Background(), DiffInput{}) if err != nil { t.Fatalf("waterfall reject: %v", err) } if got.Approved { t.Errorf("should block, got %+v", got) } if a.calls != 1 { t.Errorf("a should run, got a=%d", a.calls) } if b.calls != 0 { t.Errorf("Waterfall should NOT run b after a rejects, got b=%d", b.calls) } if got.Severity != SeverityBlock { t.Errorf("waterfall reject should carry child severity, got %v", got.Severity) } } func TestComposite_Waterfall_ShortCircuitsOnError(t *testing.T) { a := &fakeValidator{name: "a", err: errFakeBackend} b := &fakeValidator{name: "b", verdict: Verdict{Approved: true}} c := NewCompositeValidator("w", "v1", ModeWaterfall, a, b) got, err := c.Validate(context.Background(), DiffInput{}) if !errors.Is(err, errFakeBackend) { t.Fatalf("expected errFakeBackend, got %v", err) } if got.Approved { t.Errorf("should block on error, got %+v", got) } if b.calls != 0 { t.Errorf("Waterfall should NOT run b after a errors, got b=%d", b.calls) } if got.Severity != SeverityBlock { t.Errorf("error should escalate to Block, got %v", got.Severity) } } // -- shared -- func TestComposite_StampsIdentity(t *testing.T) { a := &fakeValidator{name: "a", verdict: Verdict{Approved: true}} c := NewCompositeValidator("my-comp", "my-v1", ModeAllMustApprove, a) got, _ := c.Validate(context.Background(), DiffInput{}) if got.ValidatorName != "my-comp" { t.Errorf("ValidatorName should be composite's, got %q", got.ValidatorName) } if got.PolicyVersion != "my-v1" { t.Errorf("PolicyVersion should be composite's, got %q", got.PolicyVersion) } } func TestComposite_CancelledContext(t *testing.T) { a := &fakeValidator{name: "a", verdict: Verdict{Approved: true}} c := NewCompositeValidator("w", "v1", ModeWaterfall, a) ctx, cancel := context.WithCancel(context.Background()) cancel() got, err := c.Validate(ctx, DiffInput{}) if !errors.Is(err, context.Canceled) { t.Errorf("expected context.Canceled, got %v", err) } if got.Approved { t.Errorf("cancelled should block, got %+v", got) } if a.calls != 0 { t.Errorf("cancelled should not run any child, got a=%d", a.calls) } if got.Severity != SeverityBlock { t.Errorf("cancelled should be Block, got %v", got.Severity) } } func TestComposite_DetailsIncludeChildren(t *testing.T) { a := &fakeValidator{name: "alpha", verdict: Verdict{Approved: true, Score: 0.9}} b := &fakeValidator{name: "beta", verdict: Verdict{Approved: false, Severity: SeverityBlock, Reason: "bad"}} c := NewCompositeValidator("w", "v1", ModeWaterfall, a, b) got, _ := c.Validate(context.Background(), DiffInput{}) if _, ok := got.Details["alpha"]; !ok { t.Errorf("alpha should be in details, got %v", got.Details) } if _, ok := got.Details["beta"]; !ok { t.Errorf("beta should be in details, got %v", got.Details) } // alpha should contain its approved / score / severity / reason alpha, _ := got.Details["alpha"].(map[string]any) if alpha["approved"] != true { t.Errorf("alpha.approved should be true, got %v", alpha["approved"]) } if alpha["score"] != 0.9 { t.Errorf("alpha.score should be 0.9, got %v", alpha["score"]) } } func TestComposite_UnknownMode(t *testing.T) { a := &fakeValidator{name: "a", verdict: Verdict{Approved: true}} c := NewCompositeValidator("bad", "v1", CompositionMode(99), a) got, err := c.Validate(context.Background(), DiffInput{}) if err == nil { t.Fatalf("unknown mode should error") } if got.Approved { t.Errorf("unknown mode should block, got %+v", got) } if got.Severity != SeverityBlock { t.Errorf("unknown mode should be Block, got %v", got.Severity) } if a.calls != 0 { t.Errorf("unknown mode should not run children, got a=%d", a.calls) } } func TestComposite_WaterfallRuleBeforeLLM(t *testing.T) { // Realistic scenario: RuleValidator catches obvious bad SQL before // the expensive LLM ever runs. ruleV := NewRuleValidator("rules", "v1", &PatternRule{Patterns: mustRegexps(`(?i)\bDROP\s+TABLE\b`)}) llmV := &fakeValidator{name: "llm", verdict: Verdict{Approved: true}} comp := NewCompositeValidator("waterfall", "v1", ModeWaterfall, ruleV, llmV) got, err := comp.Validate(context.Background(), DiffInput{Raw: []byte("DROP TABLE orders")}) if err != nil { t.Fatalf("unexpected error: %v", err) } if got.Approved { t.Errorf("DROP TABLE should be rejected by rule, got %+v", got) } if llmV.calls != 0 { t.Errorf("LLM should NOT have been called (rule caught it), got llm calls=%d", llmV.calls) } }