root b03521a506 validator: port FillValidator + EmailValidator from Rust validator crate
Per architecture_comparison.md universal-win for Go side: ports the
Rust crates/validator/src/staffing/ to internal/validator/. Production
safety net Go was missing — FillValidator catches phantom worker IDs
+ status/blacklist/geo/role mismatches; EmailValidator catches
SSN-shape PII + salary disclosure + wrong-target name in email/SMS
drafts.

Files:
- types.go: Artifact (FillProposal | EmailDraft), Validator interface,
  WorkerLookup interface, ValidationError + Finding + Severity
- lookup.go: InMemoryWorkerLookup with case-insensitive ID lookup
- fill.go: FillValidator — schema → completeness → cross-roster
  (phantom ID / status / blacklist / geo / role)
- email.go: EmailValidator — schema → length → PII (SSN + salary)
  → worker-name consistency
- fill_test.go + email_test.go: 24 tests covering happy path +
  every error variant + the load-bearing edge cases (phone-pattern
  not flagged as SSN, flanking-digit guard rejects extended
  numeric runs)

Validator names match Rust (staffing.fill / staffing.email) so
cross-runtime audit logs share the same identifier. PII scanners
(containsSSNPattern, containsSalaryDisclosure) ported byte-for-byte
so a draft flagged by one runtime is flagged by the other.

Caveat: the Rust validator crate also has parquet_lookup.rs (loads
workers_500k.parquet at startup) and playbook.rs (additional
checks). Those weren't ported in this wave — only the two
load-bearing validators that were named in the comparison doc.

Closes one of the two universal-win items for Go side. The other
(materializer port) remains deferred — it's a bigger surface change
and depends on transforms.ts source-class adapters.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 04:49:55 -05:00

227 lines
7.3 KiB
Go

package validator
import (
"errors"
"testing"
)
// Helpers — mirror the Rust test helpers.
func mkLookup(records ...WorkerRecord) WorkerLookup {
return NewInMemoryWorkerLookup(records)
}
func mkWorker(id, name, status, city, state, role string) WorkerRecord {
return WorkerRecord{
CandidateID: id,
Name: name,
Status: status,
City: strPtr(city),
State: strPtr(state),
Role: strPtr(role),
}
}
func asValidationError(err error) (*ValidationError, bool) {
var ve *ValidationError
if errors.As(err, &ve) {
return ve, true
}
return nil, false
}
// ── Schema-level errors ──
func TestFill_WrongArtifactType_FailsSchema(t *testing.T) {
v := NewFillValidator(mkLookup())
_, err := v.Validate(Artifact{EmailDraft: map[string]any{}})
ve, ok := asValidationError(err)
if !ok {
t.Fatalf("expected ValidationError, got %v", err)
}
if ve.Kind != ErrSchema || ve.Field != "artifact" {
t.Errorf("expected schema/artifact error, got %+v", ve)
}
}
func TestFill_MissingFillsArray_FailsSchema(t *testing.T) {
v := NewFillValidator(mkLookup())
_, err := v.Validate(Artifact{FillProposal: map[string]any{}})
ve, _ := asValidationError(err)
if ve == nil || ve.Kind != ErrSchema || ve.Field != "fills" {
t.Errorf("expected schema/fills error, got %+v", ve)
}
}
func TestFill_MissingCandidateID_FailsSchema(t *testing.T) {
v := NewFillValidator(mkLookup())
_, err := v.Validate(Artifact{FillProposal: map[string]any{
"fills": []any{
map[string]any{"name": "Alice"},
},
}})
ve, _ := asValidationError(err)
if ve == nil || ve.Kind != ErrSchema || ve.Field != "fills[0].candidate_id" {
t.Errorf("expected schema/fills[0].candidate_id error, got %+v", ve)
}
}
// ── Completeness ──
func TestFill_TargetCountMismatch_FailsCompleteness(t *testing.T) {
v := NewFillValidator(mkLookup(mkWorker("w1", "Alice", "active", "Toledo", "OH", "Welder")))
_, err := v.Validate(Artifact{FillProposal: map[string]any{
"_context": map[string]any{"target_count": float64(2)},
"fills": []any{map[string]any{"candidate_id": "w1", "name": "Alice"}},
}})
ve, _ := asValidationError(err)
if ve == nil || ve.Kind != ErrCompleteness {
t.Errorf("expected completeness error, got %+v", ve)
}
}
// ── Cross-roster checks ──
func TestFill_PhantomID_FailsConsistency(t *testing.T) {
// Lookup is empty → any candidate_id is "phantom" — the
// load-bearing check for the 0→85% pattern.
v := NewFillValidator(mkLookup())
_, err := v.Validate(Artifact{FillProposal: map[string]any{
"fills": []any{map[string]any{"candidate_id": "phantom-id", "name": "Alice"}},
}})
ve, _ := asValidationError(err)
if ve == nil || ve.Kind != ErrConsistency {
t.Errorf("expected consistency error on phantom ID, got %+v", ve)
}
}
func TestFill_DuplicateID_FailsConsistency(t *testing.T) {
v := NewFillValidator(mkLookup(mkWorker("w1", "Alice", "active", "Toledo", "OH", "Welder")))
_, err := v.Validate(Artifact{FillProposal: map[string]any{
"fills": []any{
map[string]any{"candidate_id": "w1", "name": "Alice"},
map[string]any{"candidate_id": "w1", "name": "Alice"},
},
}})
ve, _ := asValidationError(err)
if ve == nil || ve.Kind != ErrConsistency {
t.Errorf("expected consistency error on duplicate ID, got %+v", ve)
}
}
func TestFill_InactiveStatus_FailsConsistency(t *testing.T) {
v := NewFillValidator(mkLookup(mkWorker("w1", "Alice", "inactive", "Toledo", "OH", "Welder")))
_, err := v.Validate(Artifact{FillProposal: map[string]any{
"fills": []any{map[string]any{"candidate_id": "w1", "name": "Alice"}},
}})
ve, _ := asValidationError(err)
if ve == nil || ve.Kind != ErrConsistency {
t.Errorf("expected consistency error on inactive status, got %+v", ve)
}
}
func TestFill_Blacklist_FailsPolicy(t *testing.T) {
w := mkWorker("w1", "Alice", "active", "Toledo", "OH", "Welder")
w.BlacklistedClients = []string{"CLI-99"}
v := NewFillValidator(mkLookup(w))
_, err := v.Validate(Artifact{FillProposal: map[string]any{
"_context": map[string]any{"client_id": "cli-99"}, // case-insensitive
"fills": []any{map[string]any{"candidate_id": "w1", "name": "Alice"}},
}})
ve, _ := asValidationError(err)
if ve == nil || ve.Kind != ErrPolicy {
t.Errorf("expected policy error on blacklist, got %+v", ve)
}
}
func TestFill_GeoMismatch_FailsConsistency(t *testing.T) {
// Worker in Detroit, contract says Toledo.
v := NewFillValidator(mkLookup(mkWorker("w1", "Alice", "active", "Detroit", "MI", "Welder")))
_, err := v.Validate(Artifact{FillProposal: map[string]any{
"_context": map[string]any{"city": "Toledo", "state": "OH"},
"fills": []any{map[string]any{"candidate_id": "w1", "name": "Alice"}},
}})
ve, _ := asValidationError(err)
if ve == nil || ve.Kind != ErrConsistency {
t.Errorf("expected consistency error on geo mismatch, got %+v", ve)
}
}
func TestFill_RoleMismatch_FailsConsistency(t *testing.T) {
v := NewFillValidator(mkLookup(mkWorker("w1", "Alice", "active", "Toledo", "OH", "Forklift Operator")))
_, err := v.Validate(Artifact{FillProposal: map[string]any{
"_context": map[string]any{"role": "Welder"},
"fills": []any{map[string]any{"candidate_id": "w1", "name": "Alice"}},
}})
ve, _ := asValidationError(err)
if ve == nil || ve.Kind != ErrConsistency {
t.Errorf("expected consistency error on role mismatch, got %+v", ve)
}
}
// ── Happy path ──
func TestFill_WellFormed_Passes(t *testing.T) {
v := NewFillValidator(mkLookup(
mkWorker("w1", "Alice", "active", "Toledo", "OH", "Welder"),
mkWorker("w2", "Bob", "active", "Toledo", "OH", "Welder"),
))
report, err := v.Validate(Artifact{FillProposal: map[string]any{
"_context": map[string]any{
"target_count": float64(2),
"city": "Toledo",
"state": "OH",
"role": "Welder",
},
"fills": []any{
map[string]any{"candidate_id": "w1", "name": "Alice"},
map[string]any{"candidate_id": "w2", "name": "Bob"},
},
}})
if err != nil {
t.Fatalf("expected pass, got %v", err)
}
if len(report.Findings) != 0 {
t.Errorf("expected zero findings, got %v", report.Findings)
}
}
// ── Name mismatch is a Finding (warning), not an error ──
func TestFill_NameMismatch_EmitsWarning(t *testing.T) {
v := NewFillValidator(mkLookup(mkWorker("w1", "Alice Smith", "active", "Toledo", "OH", "Welder")))
report, err := v.Validate(Artifact{FillProposal: map[string]any{
"fills": []any{
map[string]any{"candidate_id": "w1", "name": "Alyssa Smith"}, // typo / outdated
},
}})
if err != nil {
t.Fatalf("name mismatch should NOT error, got %v", err)
}
if len(report.Findings) != 1 || report.Findings[0].Severity != SeverityWarning {
t.Errorf("expected 1 warning finding, got %v", report.Findings)
}
}
// ── Case-insensitive matches ──
func TestFill_CaseInsensitiveMatch_Passes(t *testing.T) {
v := NewFillValidator(mkLookup(mkWorker("w1", "Alice", "ACTIVE", "TOLEDO", "oh", "Welder")))
_, err := v.Validate(Artifact{FillProposal: map[string]any{
"_context": map[string]any{"city": "Toledo", "state": "OH"},
"fills": []any{map[string]any{"candidate_id": "w1", "name": "Alice"}},
}})
if err != nil {
t.Errorf("case-insensitive comparisons should pass, got %v", err)
}
}
// ── Validator name is stable ──
func TestFill_NameMatchesRust(t *testing.T) {
v := NewFillValidator(mkLookup())
if v.Name() != "staffing.fill" {
t.Errorf("name should match Rust 'staffing.fill', got %q", v.Name())
}
}