fix(web): close StatusBadge enum drift + Certificate TS phantom fields (D-1 master)

Five audit findings, all category cat-d or cat-f, all rooted in two
frontend files. The dashboard silently lied:

  cat-d-359e92c20cbf [P1, primary] — Agent: 'Stale' dead key + 'Degraded'
                                     neutral fallthrough
  cat-d-9f4c8e4a91f1 [P2]          — Notification: 'dead' missing
  cat-d-1447e04732e7 [P3]          — Cert: 'PendingIssuance' dead key
  cat-f-cert_detail_page_key_render_fallback [P2] — render-site reads
                                                    cert.key_algorithm directly
  cat-f-ae0d06b6588f [P2]          — Certificate TS phantom fields (root cause)

Pre-D-1, agents in the only Go AgentStatus that means 'needs operator
attention' (Degraded) rendered as default neutral grey because StatusBadge
mapped 'Stale' (a key Go has never emitted) to yellow. Dead-letter
notifications visually equated with 'read' (operator-acknowledged). The
Certificate badge map carried a 'PendingIssuance' key no Go enum emits.
CertificateDetailPage's Key Algorithm and Key Size rows always rendered
'—' even when the data was a single fetch away — the lookup went through
cert.key_algorithm / cert.key_size directly, both phantom Certificate TS
fields. Trim the TS type so the missing-data case is explicit; fix the
render site to use latestVersion?.field; pin the contract with a 38-case
Vitest property test that walks every Go enum.

StatusBadge (web/src/components/StatusBadge.tsx)
- Drop 'Stale' (Agent dead key) + 'PendingIssuance' (Cert dead key).
- Add 'Degraded' (Agent → badge-warning) + 'dead' (Notification → badge-danger).
- Add leading docblock naming Go-side source-of-truth file for every
  status family and pointing at the property test as regression vector.

Property test (web/src/components/StatusBadge.test.tsx — 38 cases)
- Iterates every Go-emitted enum value (AgentStatus, CertificateStatus,
  JobStatus, NotificationStatus, DiscoveryStatus, HealthStatus) plus the
  two frontend-synthesized Enabled/Disabled labels, asserts every value
  gets a non-default class (or an explicit 'badge badge-neutral' for the
  five intentionally-neutral terminal values: Archived, Cancelled,
  Dismissed, read, unknown).
- Negative assertions: 'Stale' and 'PendingIssuance' must fall through
  to the dictionary default — re-adding either key surfaces here.
- Specific UX-correctness assertions: 'dead' → badge-danger,
  'Degraded' → badge-warning.
- Unknown-status fallthrough preserves label text.

Certificate TS trim (web/src/api/types.ts)
- Drop serial_number?, fingerprint_sha256?, key_algorithm?, key_size?,
  issued_at? from Certificate. Go's ManagedCertificate has never carried
  these — they live on CertificateVersion. Post-trim a cert.X access for
  any of the five fields is a TS compile error.
- Leading docblock cross-references the closure rationale and the
  latestVersion fallback pattern.

Render-site fix (web/src/pages/CertificateDetailPage.tsx)
- Key Algorithm / Key Size rows now read latestVersion?.key_algorithm /
  latestVersion?.key_size, mirroring the existing latestVersion fallback
  used a few lines above for serial_number / fingerprint_sha256.
- The same edit also tightened the serial / fingerprint / issued_at
  derivations to drop the now-impossible 'cert.X || latestVersion?.X'
  cert-side leg (cert.serial_number is a TS error post-trim).

Type-test regression (web/src/api/types.test.ts)
- Certificate literal construction pinned post-trim — adding any of the
  five fields back makes the literal an excess-property TS error.
- Sibling CertificateVersion literal pinning the trimmed fields still
  live on the version envelope (so the CertificateDetailPage fallback
  path can't break).

OpenAPI (api/openapi.yaml)
- ManagedCertificate schema unchanged — was already correct (no phantom
  fields). Added a leading comment cross-referencing the D-5 closure for
  future readers.

CI guardrail (.github/workflows/ci.yml)
- 'Forbidden StatusBadge dead-key + Certificate phantom-field regression
  guard (D-1)'. Two grep blocks: catches Stale/PendingIssuance map
  literals in StatusBadge.tsx; uses an awk-scoped window over the
  'export interface Certificate {' block in types.ts to catch the five
  phantom fields reappearing while explicitly excluding CertificateVersion
  (which legitimately carries them). Comments + test files exempt.

Verification
- Backend build/vet/test -short -race all clean across handler/router/
  middleware packages.
- Frontend tsc --noEmit clean.
- Vitest 256 → 296 tests (+40: 38 from new StatusBadge test, 2 from D-5
  Certificate trim regression in types.test.ts).
- OpenAPI YAML parses (87 paths).
- Both CI guardrail patterns clear on the post-fix tree; both fire
  against synthetic regression patterns (re-add Stale → fires; re-add
  serial_number? to Certificate → fires).

Out of scope (deferred)
- diff-05x06-* type drifts for Agent/DeploymentTarget/Notification/
  DiscoveredCertificate/Issuer TS interfaces. Per-type field-by-field
  Go ↔ TS diff is codegen-shaped, not edit-shaped — warrants its own
  D-2 master prompt. Noted in CHANGELOG follow-ups section.
This commit is contained in:
Shankar Reddy
2026-04-25 13:52:54 +00:00
parent fab92551c3
commit b554d8104b
8 changed files with 413 additions and 22 deletions
+74
View File
@@ -270,6 +270,80 @@ jobs:
exit 1
fi
- name: Forbidden StatusBadge dead-key + Certificate phantom-field regression guard (D-1)
# D-1 master closed cat-d-359e92c20cbf (Agent: 'Stale' dead key,
# 'Degraded' missing), cat-d-9f4c8e4a91f1 (Notification: 'dead'
# missing), cat-d-1447e04732e7 (Cert: 'PendingIssuance' dead
# key), cat-f-cert_detail_page_key_render_fallback (render-site
# uses cert.X directly), and cat-f-ae0d06b6588f (Certificate
# TS phantom fields). This step grep-fails the build if either
# half of the closure is reverted:
#
# 1. The dead StatusBadge keys ('Stale' for Agent, 'PendingIssuance'
# for Cert) reappearing as map literals, OR
# 2. The five phantom Certificate TS fields (serial_number,
# fingerprint_sha256, key_algorithm, key_size, issued_at)
# reappearing on the `Certificate` interface in types.ts
# (CertificateVersion legitimately carries them and is
# explicitly excluded by the awk pre-filter below).
#
# Comments are exempt so the closure prose in StatusBadge.tsx +
# types.ts can stay. Test files are exempt so negative tests
# asserting the dead keys fall through to neutral keep working.
#
# See coverage-gap-audit-2026-04-24-v5/unified-audit.md
# cat-d-* / cat-f-* for the closure rationale, or
# web/src/components/StatusBadge.test.tsx for the live
# enum-coverage contract.
run: |
set -e
BAD_BADGE=$(grep -nE "^\s*(Stale|PendingIssuance)\s*:\s*'badge-" \
web/src/components/StatusBadge.tsx 2>/dev/null \
| grep -v '\.test\.' \
| grep -vE '^\s*[^:]+:[0-9]+:\s*//' \
|| true)
if [ -n "$BAD_BADGE" ]; then
echo "D-1 regression: dead StatusBadge key reappeared:"
echo "$BAD_BADGE"
echo ""
echo "Allowed surface: comment lines naming the removed key in"
echo "the file's preamble. The Go-side AgentStatus values are"
echo "Online/Offline/Degraded (no Stale); CertificateStatus values"
echo "are Pending/Active/... (no PendingIssuance). See"
echo "web/src/components/StatusBadge.test.tsx for the contract."
exit 1
fi
# Certificate TS phantom-field check. Scoped to the
# `export interface Certificate {` block in web/src/api/types.ts
# — CertificateVersion legitimately declares these fields and
# must NOT trip the guardrail. The awk window opens on the
# exact `Certificate {` header (not `CertificateVersion {`,
# not `CertificateProfile {`) and closes at the first `}`,
# then the grep matches a phantom-field declaration anywhere
# in that window.
BAD_TS=$(awk '
/^export interface Certificate \{/ { flag=1; next }
flag && /^\}/ { flag=0 }
flag { print FILENAME":"NR":"$0 }
' web/src/api/types.ts \
| grep -E '\b(serial_number|fingerprint_sha256|key_algorithm|key_size|issued_at)\??\s*:' \
|| true)
if [ -n "$BAD_TS" ]; then
echo "D-1 regression: Certificate TS interface re-added a phantom field:"
echo "$BAD_TS"
echo ""
echo "These fields live on CertificateVersion, not ManagedCertificate."
echo "The Go-side ManagedCertificate has never carried them; the"
echo "TS optional declarations were silently undefined on every"
echo "list response. Render-site consumers (e.g. CertificateDetailPage)"
echo "use latestVersion?.field as the canonical access path."
echo "See coverage-gap-audit-2026-04-24-v5/unified-audit.md"
echo "cat-f-ae0d06b6588f for the closure rationale."
exit 1
fi
- name: Race Detection
run: go test -race ./internal/service/... ./internal/api/handler/... ./internal/api/middleware/... ./internal/scheduler/... ./internal/connector/... ./internal/crypto/... ./internal/domain/... ./internal/validation/... ./internal/tlsprobe/... -count=1 -timeout 300s