Closes the last "Go primary" backlog item in docs/ARCHITECTURE_COMPARISON.md. Go now owns the entire validator path end-to-end — no Rust dep for staffing safety net. Architecture: cmd/validatord on :3221 hosts both endpoints. Calls chatd directly for the iterate loop's LLM hop (no gateway self-loopback like the Rust shape). Gateway proxies /v1/validate + /v1/iterate to validatord. What's in: - internal/validator/playbook.go — 3rd validator kind (PRD checks: fill: prefix, endorsed_names ≤ target_count×2, fingerprint required) - internal/validator/lookup_jsonl.go — JSONL roster loader (Parquet deferred; producer one-liner documented in package comment) - internal/validator/iterate.go — ExtractJSON helper + Iterate orchestrator with ChatCaller seam for unit tests - cmd/validatord/main.go — HTTP routes, roster load, chat client - internal/shared/config.go — ValidatordConfig + gateway URL field - lakehouse.toml — [validatord] section - cmd/gateway/main.go — proxy routes for /v1/validate + /v1/iterate Smoke: 5/5 PASS through gateway :3110: ✓ playbook happy path ✓ playbook missing fingerprint → 422 schema/fingerprint ✓ phantom candidate W-PHANTOM → 422 consistency ✓ unknown kind → 400 ✓ roster loaded with 3 records go test ./... green across 33 packages. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
65 lines
1.8 KiB
Go
65 lines
1.8 KiB
Go
package validator
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
)
|
|
|
|
func TestLoadJSONLRoster_RoundTripFields(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "roster.jsonl")
|
|
body := `{"candidate_id":"W-1","name":"Ada","status":"active","city":"Toledo","state":"OH","role":"Welder","blacklisted_clients":["C-1"]}
|
|
{"candidate_id":"W-2","name":"Bea","status":"inactive","city":null,"state":null,"role":null,"blacklisted_clients":[]}
|
|
malformed line that should be skipped
|
|
{"candidate_id":"","name":"empty id","status":"active"}
|
|
`
|
|
if err := os.WriteFile(path, []byte(body), 0o644); err != nil {
|
|
t.Fatalf("write fixture: %v", err)
|
|
}
|
|
|
|
l, err := LoadJSONLRoster(path)
|
|
if err != nil {
|
|
t.Fatalf("load: %v", err)
|
|
}
|
|
if l.Len() != 2 {
|
|
t.Fatalf("expected 2 records (skip malformed + empty id), got %d", l.Len())
|
|
}
|
|
|
|
w1, ok := l.Find("W-1")
|
|
if !ok {
|
|
t.Fatal("missing W-1")
|
|
}
|
|
if w1.City == nil || *w1.City != "Toledo" || w1.Role == nil || *w1.Role != "Welder" {
|
|
t.Errorf("W-1 fields: %+v", w1)
|
|
}
|
|
if len(w1.BlacklistedClients) != 1 || w1.BlacklistedClients[0] != "C-1" {
|
|
t.Errorf("W-1 blacklist: %+v", w1.BlacklistedClients)
|
|
}
|
|
|
|
w2, ok := l.Find("w-2") // case-insensitive
|
|
if !ok {
|
|
t.Fatal("missing W-2 (case-insensitive)")
|
|
}
|
|
if w2.City != nil || w2.State != nil || w2.Role != nil {
|
|
t.Errorf("W-2 should have nil pointers for missing fields: %+v", w2)
|
|
}
|
|
}
|
|
|
|
func TestLoadJSONLRoster_EmptyPathReturnsEmptyLookup(t *testing.T) {
|
|
l, err := LoadJSONLRoster("")
|
|
if err != nil {
|
|
t.Fatalf("empty path should not error: %v", err)
|
|
}
|
|
if l.Len() != 0 {
|
|
t.Errorf("expected empty lookup, got len=%d", l.Len())
|
|
}
|
|
}
|
|
|
|
func TestLoadJSONLRoster_MissingFileErrors(t *testing.T) {
|
|
_, err := LoadJSONLRoster("/nonexistent/path/roster.jsonl")
|
|
if err == nil {
|
|
t.Fatal("expected error for missing path")
|
|
}
|
|
}
|