SCRUM-214 — Config: draft validation endpoint¶
Plan ref: CFG-B4 (docs/11-admin-plane-plan.md). Stacked on SCRUM-213.
What changed¶
POST /api/admin/config/namespaces/{ns}/draft/validate (live_ops), body
{"document": {…}} (same strict reading and 1 MiB cap as the draft PUT; the body
reader is now shared). Validates against the namespace's latest schema and saves
nothing (no audit row, draft revision unchanged).
{"valid": false, "schema_version": 2,
"errors": [{"pointer": "/weapons/0/damage", "message": "got string, want integer"}]}
errorsis[]when valid, nevernull; capped at 100, library order.- Pointers are RFC 6901 and name the failing instance location, so the UI can
mark the exact field; a missing
requiredproperty points at its parent object and the message names the property. - Compiled validators are cached per
(namespace, schema_version)inschema.Cache(added toapi.Handlers; versions are immutable, so no invalidation). A stored schema that no longer compiles → 500. - Numbers are decoded exactly (
jsonschema.UnmarshalJSON), so integer checks are not fooled by float conversion.
How to verify¶
cd services/config
export CONFIG_TEST_DATABASE_URL='postgres://USER:PASS@127.0.0.1:5433/config_test?sslmode=disable'
go vet ./... && go test -race -count=1 ./...
go test -count=1 -v -run 'Validate' ./internal/schema/ ./internal/server/
| Test | Proves |
|---|---|
schema.TestValidateAcceptsAValidDocument |
valid → no issues |
schema.TestValidateNamesTheFailingInstance |
damage:"lots" → /weapons/0/damage |
schema.TestValidateRequiredPointsAtTheParent |
missing required → parent pointer, property named |
schema.TestValidateCapsIssues |
>100 failures → exactly 100 |
server.TestDraftValidateRoutesThroughTheLiveServer |
real schema PUT then: valid → valid:true, errors:[]; "lots" and -1 → /weapons/0/damage; viewer 403; unknown ns 404; draft revision unchanged |
By hand (live_ops token $T, after giving ui.motd a schema):
curl -s -H "Authorization: Bearer $T" -d '{"document":{"weapons":[{"damage":"lots"}]}}' \
localhost:8080/api/admin/config/namespaces/balance.weapons/draft/validate
Results at time of writing¶
go vet,go test -race ./...against Postgres 16: pass (7 packages).
How it was built¶
DeepSeek run scoped (Landlock) to services/config (106 s, ~18k output tokens).
Claude review: handler and cache wiring read, suite re-run. No changes needed.