root f9e72412c1 validatord: /v1/validate + /v1/iterate HTTP surface (port 3221)
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>
2026-05-02 03:53:20 -05:00

78 lines
2.3 KiB
Go

package validator
import (
"errors"
"testing"
)
func TestPlaybook_WellFormedPasses(t *testing.T) {
r, err := PlaybookValidator{}.Validate(Artifact{Playbook: map[string]any{
"operation": "fill: Welder x2 in Toledo, OH",
"endorsed_names": []any{"W-123", "W-456"},
"target_count": 2.0,
"fingerprint": "abc123",
}})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if r.ElapsedMs < 0 {
t.Errorf("elapsed_ms negative: %d", r.ElapsedMs)
}
}
func TestPlaybook_EmptyEndorsedNamesFailsCompleteness(t *testing.T) {
_, err := PlaybookValidator{}.Validate(Artifact{Playbook: map[string]any{
"operation": "fill: Welder x2 in Toledo, OH",
"endorsed_names": []any{},
"fingerprint": "abc",
}})
var ve *ValidationError
if !errors.As(err, &ve) || ve.Kind != ErrCompleteness {
t.Fatalf("expected Completeness, got %v", err)
}
}
func TestPlaybook_OverfullEndorsedNamesFailsCompleteness(t *testing.T) {
_, err := PlaybookValidator{}.Validate(Artifact{Playbook: map[string]any{
"operation": "fill: Welder x1 in Toledo, OH",
"endorsed_names": []any{"a", "b", "c"},
"target_count": 1.0,
"fingerprint": "abc",
}})
var ve *ValidationError
if !errors.As(err, &ve) || ve.Kind != ErrCompleteness {
t.Fatalf("expected Completeness, got %v", err)
}
}
func TestPlaybook_MissingFingerprintFailsSchema(t *testing.T) {
_, err := PlaybookValidator{}.Validate(Artifact{Playbook: map[string]any{
"operation": "fill: X x1 in A, B",
"endorsed_names": []any{"a"},
}})
var ve *ValidationError
if !errors.As(err, &ve) || ve.Kind != ErrSchema || ve.Field != "fingerprint" {
t.Fatalf("expected Schema/fingerprint, got %+v", err)
}
}
func TestPlaybook_WrongOperationPrefixFailsSchema(t *testing.T) {
_, err := PlaybookValidator{}.Validate(Artifact{Playbook: map[string]any{
"operation": "sms_draft: hello",
"endorsed_names": []any{"a"},
"fingerprint": "x",
}})
var ve *ValidationError
if !errors.As(err, &ve) || ve.Kind != ErrSchema {
t.Fatalf("expected Schema, got %v", err)
}
}
func TestPlaybook_WrongArtifactKindFailsSchema(t *testing.T) {
_, err := PlaybookValidator{}.Validate(Artifact{FillProposal: map[string]any{}})
var ve *ValidationError
if !errors.As(err, &ve) || ve.Kind != ErrSchema || ve.Field != "artifact" {
t.Fatalf("expected Schema/artifact, got %+v", err)
}
}