--- id: "bug-1818" status: "fixed" status_updated: "2026-09-24" status_note: "" component: "harness" severity: "unknown" timestamp: "2026-09-16T08:20:00Z" last_seen: "2026-09-19T09:00:00Z" occurrences: 1 files: ["src/harness/test.c (cmd_ctrlstatus -- fix)", ".kiro/specs/controller-status/design.md (Design Delta -- Requirement 12", "Locked Decision 3 -- corrected)"] tags: ["controller-status", "ctrlstatus", "cmd_ctrlstatus", "ha", "election", "live-e2e-found", "display-layer", "dead-code-gate", "primaryName"] related: [] legacy: true --- # bug-1818: Live 3-node HA E2E validation (controller-status spec, Requirement 12 delta, task 7.6)… ## Description Live 3-node HA E2E validation (controller-status spec, Requirement 12 delta, task 7.6): cmd_ctrlstatus (xce_test client) printed electedTerm: unknown / electedAt: unknown for the new primary-election-term/time fields on EVERY standby query, both before and after a real primary kill/failover (confirmed after ~4 heartbeat intervals, ruling out a convergence-timing gap) -- even though the same query correctly showed non-stale, converged term/committedLsn/appliedLsn values for the same standby at the same moment. ## Environment _Not recorded (migrated from buglog.json)._ ## Reproduction _Not recorded (migrated from buglog.json)._ ## Root cause cmd_ctrlstatus gated the new electedTerm/electedAt print lines on NULLSTR(status->primaryName), reusing the same condition the pre-existing Primary: line already uses -- per the delta design's own Locked Decision 3, which assumed primaryName would be "known" on a standby that has learned the primary. But primaryName is UNCONDITIONALLY empty on every non-primary responder in this build: ctrlstatus_fill (src/controller/ctrlstatus.c) only self-reports the primary's own name when the responder IS the primary (ha->primary_host is vestigial/never written, a pre-existing behavior from Requirement 2 / commit ade0081, predating this delta). So Requirement 2.3's "standby knows the primary" branch is dead code via the plain ctrlstatus command, and gating the new fields on the same condition made them permanently unreachable on any standby -- even though status->elected_at_ms (populated unconditionally by ctrlstatus_fill, task 7.4, and correctly heartbeat-learned by task 7.3) was present and correct the whole time. Caught only by a real live cluster run; all unit tests (which test ctrlstatus_fill directly, not cmd_ctrlstatus's print gate) passed throughout, since the bug was entirely in the untested CLI display layer. ## Fix FIXED (2026-09-16, controller-status Requirement 12 delta). Changed cmd_ctrlstatus's gate from NULLSTR(status->primaryName) to status->elected_at_ms == 0. This is both necessary and sufficient: elected_at_ms stays exactly 0 until a real promotion stamps it (either promotion road, ha_do_promote / raft_cb_become_leader) or a heartbeat carries a learned value from an actual primary (ha_consume_heartbeat_committed) -- unlike term, which can legitimately bump from mere vote/candidacy activity before any primary is actually elected, so term == 0 would have been the wrong gate (verified via a dedicated candidacy-in-progress test case: elected_at_ms == 0 but term != 0 must still print unknown). ctrlstatus_fill itself was NOT touched -- it correctly remained an unconditional, no-role-branch read (Locked Decision 2), unaffected by this bug. Verified via a standalone scratch program (against the real ctrlstatus_fmt.h header, 4 cases: standby/bug case, never-elected, candidacy-in-progress, primary/regression) plus a repeat live 3-node E2E run confirming the fix converges end-to-end after a real primary kill. design.md's Locked Decision 3 and its dependent traceability/service-contract text were corrected in the same commit to document this live-E2E finding, mirroring the spec's existing Req 10/11 delta convention for recording live findings. ## Verification _Not recorded (migrated from buglog.json)._ ## References _Not recorded (migrated from buglog.json)._