wulf-pulse/.planning/phases/24-aws-route-53-dns-sync-track-changes-crud-operations-full-aud/24-VALIDATION.md
lorentz 52afdca8a1 docs(24-07): resolve live end-to-end checkpoint — all 9 steps passed
Ran the phase-closing 9-step live verification against the real
wulfconsulting.com hosted zone. All steps confirmed passing:
sync, create/update/delete round-trip, audit completeness, NS-write
rejection + genuine AWS failure logging, admin/user auth gating,
drift detection, and D-12/D-10 health-check behavior.

One real bug found and fixed along the way (commit 4da5664): TXT
record values weren't RFC-1035-quoted, so AWS rejected every TXT
create/update with InvalidCharacterString. Fixed in
route53-change-submit.ts, 4 regression tests added, retried
successfully.

Logged one pre-existing, unrelated finding: /admin/integrations page
load is slow overall, confirmed not specific to Route 53's own health
check (which is bounded to a few seconds) — likely another
integration's untimeouts liveCheck() probe. Deferred, not fixed.

24-VALIDATION.md's three Manual-Only Verifications rows updated with
observed outcomes; status marked complete.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-05 22:55:27 -04:00

15 KiB
Raw Blame History

phase slug status nyquist_compliant wave_0_complete created updated
24 aws-route-53-dns-sync-track-changes-crud-operations-full-aud complete true true 2026-08-05 2026-08-06

Phase 24 — Validation Strategy

Per-phase validation contract for feedback sampling during execution.


Test Infrastructure

Property Value
Framework vitest 4.1.5
Config file vitest.config.ts (include: ['lib/**/*.test.ts'], environment: 'node', globals: false)
Quick run command npx vitest run <touched-test-file>
Full suite command npm test
Estimated runtime ~10 seconds (small existing suite)

Structural constraint driving the plan layout: vitest.config.ts includes only lib/**/*.test.ts. Nothing under app/api/** can be unit-tested. Every plan therefore places its testable logic (record-key derivation, drift classification, the D-01 allowlist, AWS error sanitization, change-batch construction, propagation polling, NS comparison) in a lib/services/ module that the route handlers call, rather than inline in a route file.


Sampling Rate

  • After every task commit: npx vitest run <touched-test-file>
  • After every plan wave: npm test
  • Before /gsd:verify-work: full suite green + npx tsc --noEmit --pretty clean
  • Max feedback latency: 15 seconds

Per-Task Verification Map

Task ID Plan Wave Requirement Threat Ref Secure Behavior Test Type Automated Command File Exists Status
24-01-T1 24-01 1 SC-3, SC-4, SC-5 T-24-06, T-24-SC Route 53 schema exists with source and status CHECK constraints; no AWS secret in committed .env schema/grep gate grep -c 'CREATE TABLE IF NOT EXISTS route53_' migrations/102_route53_tables.sql = 4 and grep -c '^AWS_' .env = 0 created by task pending
24-01-T2 24-01 1 SC-5 T-24-06 isRoute53Configured() false without creds; getRoute53Client() throws; no explicit credentials: object unit npx vitest run lib/services/route53-factory.test.ts Wave 0 (created by task) pending
24-01-T3 24-01 1 SC-5 T-24-08, T-24-09 BWS key names confirmed; creds present in container; DNS egress reachable; IAM least-privilege manual / checkpoint none automated — blocking checkpoint:human-verify manual by necessity pending
24-02-T1 24-02 2 SC-1, SC-4 Record-key derivation and recordset normalization are order-insensitive, so equal recordsets never register as drift unit npx vitest run lib/services/route53-record-key.test.ts Wave 0 (created by task) pending
24-02-T2 24-02 2 SC-1 T-24-03 Pagination + soft-delete; errors logged as error.message only, never the raw AWS error object grep gate + typecheck npx tsc --noEmit --pretty && test $(grep -c 'integration_settings' lib/services/route53-sync-service.ts) -eq 0 created by task pending
24-02-T3 24-02 2 SC-4 T-24-04, T-24-10 Drift produces exactly one correctly-tagged sync_detected_drift history row per changed record; equal recordsets and initial import produce none unit npx vitest run lib/services/route53-sync-service.test.ts Wave 0 (created by task) pending
24-03-T1 24-03 2 SC-3 T-24-01, T-24-03, T-24-12 Closed six-type allowlist rejects NS/SOA/ns/CAA with 400; sanitizeAwsError redacts AKIA key ids, arn:aws:*, and 12-digit account ids unit npx vitest run lib/services/route53-record-validation.test.ts Wave 0 (created by task) pending
24-03-T2 24-03 2 SC-3 T-24-04, T-24-07 Audit row inserted status='pending' before any AWS call; markAuditFailed binds a sanitized message; mirror deletes are soft-only unit (mocked pg) npx vitest run lib/services/route53-write-persistence.test.ts Wave 0 (created by task) pending
24-04-T1 24-04 2 SC-6 (D-12) T-24-13, T-24-15 NS normalization is case/trailing-dot insensitive; empty authoritative list yields no false alarm; empty live answer yields a mismatch; setServers only on a dedicated Resolver instance unit npx vitest run lib/services/route53-dns-delegation.test.ts Wave 0 (created by task) pending
24-04-T2 24-04 2 SC-6 T-24-03, T-24-16 route53 present in the health aggregate; auth-probe errors sanitized; delegation-step failure cannot abort Promise.all integration (curl) + typecheck npx tsc --noEmit --pretty && curl -s localhost:3100/api/admin/integration-health | grep -q '"key":"route53"' modified by task pending
24-05-T1 24-05 3 SC-2 T-24-01, T-24-19, T-24-20 buildChangeBatch throws on NS/SOA (defence in depth); pollChangeStatus is bounded and stops after timeout; the 30-minute SDK waiter is absent unit npx vitest run lib/services/route53-change-submit.test.ts Wave 0 (created by task) pending
24-05-T2 24-05 3 SC-4, SC-6 T-24-02, T-24-18 Every read route gated by requireAuth(); all SQL parameter-bound; no integration_settings gating (D-10) grep gate + curl test $(grep -rl 'requireAuth|requireAdmin' app/api/route53 | wc -l) -eq 4 and unauthenticated curl on /api/route53/zones returns 401 created by task pending
24-05-T3 24-05 3 SC-2, SC-3 T-24-01, T-24-02, T-24-03, T-24-04, T-24-17 validateRecordWrite and createPendingAuditLog both precede submitRecordChange; history written only on success; failures return 502 with a sanitized message; no staged-approval state source-order gate + typecheck npx tsc --noEmit --pretty && test $(grep -rc 'pending_approval|requiresConfirmation|confirmToken' app/api/route53/ | awk -F: '{s+=$2} END {print s}') -eq 0 created by task pending
24-05-T3m 24-05 3 SC-2 (D-04) T-24-02 requireAdmin() returns 403 for a user-role session hitting a write route directly manual / smoke none automated — see Manual-Only table row 1 manual by convention pending
24-06-T1 24-06 3 SC-1, SC-6 T-24-21, T-24-22, T-24-24 Both sync types dispatch via dynamic import and gate on isRoute53Configured() only, never on integration_settings (D-10) grep gate + typecheck npx tsc --noEmit --pretty && test $(grep -A12 "config.sync_type === 'route53" lib/services/sync-scheduler.ts | grep -c integration_settings) -eq 0 modified by task pending
24-06-T2 24-06 3 SC-6 T-24-23 Tile entry uses a valid COLOR_MAP key; logo asset contains no script or xlink:href grep gate + build npm run build && test $(grep -ci 'script|xlink:href' public/logos/route53.svg) -eq 0 created by task pending
24-07-T1 24-07 4 SC-4, SC-6 T-24-25 Page renders record values and history before/after as escaped text; no dangerouslySetInnerHTML; no SWR/react-query grep gate + build npm run build && test $(grep -c 'dangerouslySetInnerHTML' app/admin/sync/route53/page.tsx) -eq 0 created by task green
24-07-T2 24-07 4 SC-2 T-24-01, T-24-20, T-24-26 Type selector offers only the six writable types; submit disabled while in flight; no approval-workflow state introduced grep gate + build npm run build && test $(grep -c "'NS'|'SOA'" components/admin/route53/record-editor-dialog.tsx) -eq 0 created by task green
24-07-T3 24-07 4 SC-1..SC-6 all Full live round-trip: sync, create/update/delete against real AWS, audit completeness, failure logging, auth gating, drift detection, D-12 health, D-10 display-only disable manual / checkpoint none automated — blocking checkpoint:human-verify, 9 steps manual by necessity green (all 9 steps passed against wulfconsulting.com; TXT-quoting bug found in step 2, fixed in 4da5664, retried successfully)

Status: pending · green · red · ⚠️ flaky

Sampling continuity check: no 3 consecutive tasks lack an automated verify. The two checkpoint tasks (24-01-T3, 24-07-T3) are each adjacent to tasks carrying automated commands, and both are terminal within their plan.


Wave 0 Requirements

All Wave 0 test files are created by the task that needs them, inside the plan that owns the module under test — no separate scaffolding plan is required, because every test target is a new file rather than an extension of untested existing code.

  • lib/services/route53-factory.test.ts — plan 24-01 Task 2 (isRoute53Configured() branches)
  • lib/services/route53-record-key.test.ts — plan 24-02 Task 1 (key derivation, normalization, order-insensitive equality, drift classification)
  • lib/services/route53-sync-service.test.ts — plan 24-02 Task 3 (drift history row shape and source tagging)
  • lib/services/route53-record-validation.test.ts — plan 24-03 Task 1 (D-01 closed allowlist, TTL/value shape, AWS error sanitizer)
  • lib/services/route53-write-persistence.test.ts — plan 24-03 Task 2 (pending/committed/failed SQL contract, soft-delete only)
  • lib/services/route53-dns-delegation.test.ts — plan 24-04 Task 1 (NS normalization + mismatch detection; first health-check test file in this codebase)
  • lib/services/route53-change-submit.test.ts — plan 24-05 Task 1 (change-batch construction, retry classification, bounded poll)
  • No framework install needed — vitest is already configured project-wide

Manual-Only Verifications

Behavior Requirement Covered By Why Manual Test Instructions
Write-route auth gating (401/403 for non-admin) SC-2 / D-04 / T-24-02 24-07 Task 3, step 7 requireAdmin()/requireAuth() are Better Auth session-dependent; this codebase has no precedent for testing route auth gating in isolation — historically verified by manual click-through (see 22-VERIFICATION.md) Sign in as a user-role account, POST /api/route53/zones/<zone>/records with a valid payload, confirm 403. Confirm GET /api/route53/zones still returns 200 for the same session. Repeat the POST as admin and confirm success. Observed: PASS. user-role session (colleen@wulfconsulting.com) got 403 "Forbidden - Admin access required" on write; GET /api/route53/zones returned 200 for the same session. super-admin session (lorentz@wulfconsulting.com) succeeded on the equivalent write.
Live AWS write-back round-trip (create/update/delete a real record) SC-2 / SC-3 / SC-4 24-07 Task 3, steps 2-6 Cannot be safely automated against a real AWS account/hosted zone in CI; requires a real Route 53 zone and live credentials Using a disposable TXT record in a real hosted zone: create via the Pulse UI, confirm in the AWS console; update; delete. Confirm route53_audit_log has 3 committed rows with correct before/after and actor, and route53_record_history has 3 pulse_crud rows. Then force an AWS-side rejection (apex CNAME) and confirm a failed row with a sanitized error_message and an HTTP 502 response. Observed: PASS on retry. First create attempt failed with AWS's InvalidCharacterString (TXT values weren't RFC-1035-quoted) — real bug, fixed in 4da5664 (formatTxtValue() in route53-change-submit.ts), pulse-app rebuilt, retried successfully. Create/update/delete against pulse-phase24-test.wulfconsulting.com. all confirmed in the AWS console; route53_audit_log showed 3 committed rows (create/update/delete) attributed to lorentz@wulfconsulting.com. Apex-CNAME rejection confirmed 502 + a failed audit row with a clean, sanitized error_message (no account id/ARN/key id present).
DNS-egress-in-production assumption (D-12 health check) D-12 / T-24-13 24-01 Task 3, step 3 and 24-07 Task 3, step 9 Whether outbound UDP/53 to public resolvers is permitted from the production container network is unverifiable from the repo (Open Question 3 / Assumption A3 in 24-RESEARCH.md) Run the dns.Resolver().setServers(['1.1.1.1','8.8.8.8']).resolveNs('google.com') one-liner inside the container (exact command in plan 24-01 Task 3). EGRESS-OK means the Node dns path is viable; EGRESS-BLOCKED requires plan 24-04 to use the DoH-over-HTTPS fallback. Observed: EGRESS-OK — confirmed twice (24-01 checkpoint and again during 24-07's live testing via the D-12 delegation check running successfully). Node's dns module in use, no DoH fallback needed.
D-10 display-only disable behavior D-10 24-07 Task 3, step 9 The failure mode (disabling the toggle silently stopping sync, replicating PAX8's blocking exception) is only observable end-to-end across the health cache, the scheduler, and the sync route Toggle route53 off at /admin/integrations, wait for the 5-minute health cache (or let the PATCH clear it), confirm the health row reads disabled, then confirm POST /api/route53/sync still succeeds and returns { ok: true }. Observed: PASS. Toggling off showed disabled on the health row while a manual sync still succeeded. Developer noted /admin/integrations page load felt slow during this step overall (not specific to Route 53) — investigated, confirmed unrelated to this plan's code (see deferred-items.md), logged and not fixed here.
BWS secret key names / IAM least privilege SC-5 / T-24-08 24-01 Task 3, steps 1, 2, 4 BWS project contents live in Bitwarden's cloud, and IAM policy scope lives in the AWS console — neither is inspectable from this repo See plan 24-01 Task 3's how-to-verify steps 1, 2, and 4. Verification commands print only SET/unset presence markers, never secret values. Observed: RESOLVED. BWS key names initially mismatched (AWS_ACCESS_KEY/AWS_SECRET_KEY vs. the AWS-SDK-required AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY) — developer renamed the secrets directly in Bitwarden; re-verified present and correctly named. Credentials confirmed reaching the actual Node process (/proc/<pid>/environ, presence-only check). IAM least-privilege scope left as an open item for the developer to confirm from the AWS console side — not a blocker for this phase's code.

Validation Sign-Off

  • All tasks have an <automated> verify or an explicit manual/checkpoint justification
  • Sampling continuity: no 3 consecutive tasks without an automated verify
  • Wave 0 covers all MISSING references — every test file is created by the task that needs it
  • No watch-mode flags in any command
  • Feedback latency < 15s (all unit commands target a single file; the full suite is ~10s)
  • Every grep gate that counts occurrences targets source constructs, not comment prose
  • nyquist_compliant: true set in frontmatter

Approval: ready for execution