package validator import "testing" // ── Schema ── func TestEmail_WrongArtifactType_FailsSchema(t *testing.T) { v := NewEmailValidator(mkLookup()) _, err := v.Validate(Artifact{FillProposal: map[string]any{}}) ve, _ := asValidationError(err) if ve == nil || ve.Kind != ErrSchema { t.Errorf("expected schema error on wrong artifact, got %+v", ve) } } func TestEmail_MissingTo_FailsSchema(t *testing.T) { v := NewEmailValidator(mkLookup()) _, err := v.Validate(Artifact{EmailDraft: map[string]any{"body": "hi"}}) ve, _ := asValidationError(err) if ve == nil || ve.Kind != ErrSchema || ve.Field != "to" { t.Errorf("expected schema/to error, got %+v", ve) } } func TestEmail_MissingBody_FailsSchema(t *testing.T) { v := NewEmailValidator(mkLookup()) _, err := v.Validate(Artifact{EmailDraft: map[string]any{"to": "a@b.com"}}) ve, _ := asValidationError(err) if ve == nil || ve.Kind != ErrSchema || ve.Field != "body" { t.Errorf("expected schema/body error, got %+v", ve) } } // ── Length limits ── func TestEmail_LongSMS_FailsCompleteness(t *testing.T) { v := NewEmailValidator(mkLookup()) body := make([]byte, 200) for i := range body { body[i] = 'x' } _, err := v.Validate(Artifact{EmailDraft: map[string]any{ "to": "+15555550123", "body": string(body), "kind": "sms", }}) ve, _ := asValidationError(err) if ve == nil || ve.Kind != ErrCompleteness { t.Errorf("expected completeness error on long SMS, got %+v", ve) } } func TestEmail_LongSubject_FailsCompleteness(t *testing.T) { v := NewEmailValidator(mkLookup()) subject := make([]byte, 100) for i := range subject { subject[i] = 'x' } _, err := v.Validate(Artifact{EmailDraft: map[string]any{ "to": "a@b.com", "body": "hi", "subject": string(subject), }}) ve, _ := asValidationError(err) if ve == nil || ve.Kind != ErrCompleteness { t.Errorf("expected completeness error on long subject, got %+v", ve) } } // ── PII: SSN ── func TestEmail_SSNInBody_FailsPolicy(t *testing.T) { v := NewEmailValidator(mkLookup()) _, err := v.Validate(Artifact{EmailDraft: map[string]any{ "to": "a@b.com", "body": "Their SSN is 123-45-6789, please file accordingly.", }}) ve, _ := asValidationError(err) if ve == nil || ve.Kind != ErrPolicy { t.Errorf("expected policy error on SSN, got %+v", ve) } } func TestEmail_PhonePatternNotFlaggedAsSSN(t *testing.T) { // NNN-NNN-NNNN (phone) must NOT trigger the NNN-NN-NNNN check. // Critical false-positive case from Rust phone-pattern test. v := NewEmailValidator(mkLookup()) _, err := v.Validate(Artifact{EmailDraft: map[string]any{ "to": "a@b.com", "body": "Call me at 555-123-4567 to confirm.", }}) if err != nil { t.Errorf("phone pattern should NOT trigger SSN policy, got %v", err) } } func TestEmail_SSNInsideLongerNumericRun_NotFlagged(t *testing.T) { // 1234-56-78901 has the right shape pattern at offset 0 but // flanking digits → not an SSN. Mirrors Rust's flanking-digit // guard test. v := NewEmailValidator(mkLookup()) _, err := v.Validate(Artifact{EmailDraft: map[string]any{ "to": "a@b.com", "body": "ID 1234-56-78901 is the new format.", }}) if err != nil { t.Errorf("flanking-digit guard should reject this, got %v", err) } } // ── PII: salary ── func TestEmail_SalaryDisclosure_FailsPolicy(t *testing.T) { v := NewEmailValidator(mkLookup()) _, err := v.Validate(Artifact{EmailDraft: map[string]any{ "to": "a@b.com", "body": "Their salary is $45000 — please confirm before sending offer.", }}) ve, _ := asValidationError(err) if ve == nil || ve.Kind != ErrPolicy { t.Errorf("expected policy error on salary disclosure, got %+v", ve) } } func TestEmail_HourlyRateDisclosure_FailsPolicy(t *testing.T) { v := NewEmailValidator(mkLookup()) _, err := v.Validate(Artifact{EmailDraft: map[string]any{ "to": "a@b.com", "body": "Discuss your hourly rate of $30 with the client when you arrive.", }}) ve, _ := asValidationError(err) if ve == nil || ve.Kind != ErrPolicy { t.Errorf("expected policy error on hourly rate, got %+v", ve) } } func TestEmail_DollarFar_NotFlagged(t *testing.T) { // $ amount > 40 chars from the keyword → not flagged. v := NewEmailValidator(mkLookup()) body := "We're paid by salary, but the parking validation costs " + "about three more sentences worth of text appearing in between, " + "and then much later at $50 the trip is too expensive." _, err := v.Validate(Artifact{EmailDraft: map[string]any{ "to": "a@b.com", "body": body, }}) if err != nil { t.Errorf("salary keyword far from $ amount should not flag, got %v", err) } } // ── Worker-name consistency ── func TestEmail_NameMissingFromBody_EmitsWarning(t *testing.T) { v := NewEmailValidator(mkLookup(mkWorker("w1", "Alice Smith", "active", "Toledo", "OH", "Welder"))) report, err := v.Validate(Artifact{EmailDraft: map[string]any{ "to": "a@b.com", "body": "Hello, please confirm your shift tomorrow.", "_context": map[string]any{"candidate_id": "w1"}, }}) if err != nil { t.Fatalf("name mismatch should NOT error (warning only), got %v", err) } if len(report.Findings) != 1 || report.Findings[0].Severity != SeverityWarning { t.Errorf("expected 1 warning finding, got %v", report.Findings) } } func TestEmail_NameInBody_NoFinding(t *testing.T) { v := NewEmailValidator(mkLookup(mkWorker("w1", "Alice Smith", "active", "Toledo", "OH", "Welder"))) report, err := v.Validate(Artifact{EmailDraft: map[string]any{ "to": "a@b.com", "body": "Hi Alice, please confirm tomorrow.", "_context": map[string]any{"candidate_id": "w1"}, }}) if err != nil { t.Fatalf("expected pass, got %v", err) } if len(report.Findings) != 0 { t.Errorf("expected zero findings, got %v", report.Findings) } } func TestEmail_PhantomCandidateID_FailsConsistency(t *testing.T) { v := NewEmailValidator(mkLookup()) _, err := v.Validate(Artifact{EmailDraft: map[string]any{ "to": "a@b.com", "body": "Hi Alice", "_context": map[string]any{"candidate_id": "phantom"}, }}) ve, _ := asValidationError(err) if ve == nil || ve.Kind != ErrConsistency { t.Errorf("expected consistency error on phantom ID, got %+v", ve) } } // ── Happy path ── func TestEmail_WellFormed_Passes(t *testing.T) { v := NewEmailValidator(mkLookup()) report, err := v.Validate(Artifact{EmailDraft: map[string]any{ "to": "alice@example.com", "subject": "Shift confirmation", "body": "Please confirm your shift starts at 9am tomorrow.", }}) if err != nil { t.Errorf("well-formed email should pass, got %v", err) } if len(report.Findings) != 0 { t.Errorf("expected zero findings, got %v", report.Findings) } } // ── Validator name is stable ── func TestEmail_NameMatchesRust(t *testing.T) { v := NewEmailValidator(mkLookup()) if v.Name() != "staffing.email" { t.Errorf("name should match Rust 'staffing.email', got %q", v.Name()) } }