diff --git a/.kiro/specs/ha-config-bootstrap/tasks.md b/.kiro/specs/ha-config-bootstrap/tasks.md index f7a8580..84e5a5e 100644 --- a/.kiro/specs/ha-config-bootstrap/tasks.md +++ b/.kiro/specs/ha-config-bootstrap/tasks.md @@ -54,7 +54,7 @@ Dependency order: startup validation (D) unblocks vtaskmgr before election (A); - _Boundary: Startup Validation (main.c, ha.c)_ - [ ] 3. Change A: Listener and iomgr/vtaskmgr before election -- [ ] 3.1 Write failing tests for dispatch gate and FSM init state +- [x] 3.1 Write failing tests for dispatch gate and FSM init state - In `src/controller/testsuite/test_ha_startup.c` (extend): test that requests other than `OP_requestVote` and `OP_probeConfigTag` receive `XCE_ERR_NOT_READY` when role is `CTRL_INIT`; test `CTRL_STANDBY` returns `XCE_ERR_STANDBY` for primary-only ops; test `heartbeat_seen` semaphore initialized in `ha_init()` - `make test` fails - _Requirements: 1.1, 1.2, 1.3, 1.4_ diff --git a/src/controller/testsuite/test_ha_startup.c b/src/controller/testsuite/test_ha_startup.c index 02d76e8..9aa2b1e 100644 --- a/src/controller/testsuite/test_ha_startup.c +++ b/src/controller/testsuite/test_ha_startup.c @@ -1,18 +1,26 @@ -/* test_ha_startup.c — Unity RED-phase tests for startup validation +/* test_ha_startup.c — Unity RED-phase tests for startup validation and + * dispatch gate / FSM init state. * - * Tests verify: even-N peer count rejection, cert pre-flight -ENOENT with path - * in error message, --vcpus 0 rejection, and vcpus key in config logs WARN - * (via caller-supplied warn_buf) without failing. + * Tests verify: + * Req 4.1-4.4: even-N peer count rejection, cert pre-flight -ENOENT with + * path in error message, --vcpus 0 rejection, and vcpus key in + * config logs WARN (via caller-supplied warn_buf) without failing. + * Req 1.3-1.4: dispatch gate rejects non-vote/non-probe ops in CTRL_INIT + * and CTRL_ELECTING states with XCE_ERR_NOT_READY; returns + * XCE_ERR_STANDBY for primary-only ops in CTRL_STANDBY state; + * Req 1.x: heartbeat_seen semaphore is initialized in ha_init(). * - * Requirements: 4.1, 4.2, 4.3, 4.4 * Functions under test: validate_peer_count(), validate_cert_files(), - * validate_vcpus(), validate_startup_config() - * These functions do NOT exist yet — this file establishes the contract - * for tasks 2.2 and 2.3. + * validate_vcpus(), validate_startup_config(), + * ha_dispatch_gate() + * These functions do NOT all exist yet — this file establishes contracts for + * tasks 2.2/2.3 (validation) and 3.3 (dispatch gate). */ #define _GNU_SOURCE #include #include +#include +#include #include #include #include @@ -22,6 +30,9 @@ #include +#include "controller.capnp.h" +#include "ha.h" + /* Functions under test — declared here; implementations come in tasks 2.2/2.3 */ int validate_peer_count(int n_peers); int validate_cert_files(const char *cert_path, const char *key_path, @@ -30,6 +41,40 @@ int validate_vcpus(int vcpus); int validate_startup_config(const char *config_path, char *warn_buf, size_t warn_len); +/* ── Dispatch gate (task 3.3) — does NOT exist yet; linker failure expected ── + * + * ha_dispatch_gate: role-based operation filter for the vtaskmgr dispatch loop. + * + * Parameters: + * ha - pointer to the controller's ha_state_t (role is read atomically) + * opcode - the OPCode integer value from the incoming request + * + * Returns: + * 0 — operation is permitted for the current role + * -XCE_ERR_NOT_READY — role is CTRL_INIT or CTRL_ELECTING and opcode is not + * OP_requestVote or OP_probeConfigTag + * -XCE_ERR_STANDBY — role is CTRL_STANDBY and opcode is a primary-only op + * + * Design ref: design.md "Dispatch gate" section (req 1.3, 1.4). + * This declaration exists to make the RED-phase tests compile and fail to link. + */ +int ha_dispatch_gate(ha_state_t *ha, int opcode); + +/* ── Error codes (task 3.3) — do NOT exist in errnox.h yet ────────────────── + * + * XCE_ERR_NOT_READY: returned when controller is in CTRL_INIT or CTRL_ELECTING + * state and receives an opcode that is not allowed during + * startup (req 1.3). + * XCE_ERR_STANDBY: returned when controller is in CTRL_STANDBY state and + * receives a primary-only operation (req 1.4). + * + * These are declared as extern int to force a linker error when the + * corresponding symbols are not yet defined in any translation unit. + * Tasks 3.3 implementation will add them to errnox.h and provide definitions. + */ +extern int XCE_ERR_NOT_READY; +extern int XCE_ERR_STANDBY; + /* ── helpers ──────────────────────────────────────────────────────────── */ static char g_tmpdir[256]; @@ -218,6 +263,171 @@ static void test_config_without_vcpus_key_has_no_warning(void) printf("test_config_without_vcpus_key_has_no_warning: PASS\n"); } +/* ── Req 1.3: dispatch gate — CTRL_INIT blocks non-election opcodes ──────── */ + +/* Shared ha_config_t for dispatch gate tests — minimal valid config */ +static ha_config_t g_dispatch_cfg = { + .heartbeat_interval_ms = 3000, + .heartbeat_timeout_ms = 10000, + .election_timeout_ms = 2000, + .probe_connect_timeout_ms = 500, + .max_promotion_retries = 5, +}; + +static void test_dispatch_gate_init_blocks_non_election_ops(void) +{ + /* Req 1.3: When role is CTRL_INIT, any opcode that is not OP_requestVote + * or OP_probeConfigTag must receive XCE_ERR_NOT_READY. + * + * OPCode_jobsub (11) is a representative primary-only opcode. */ + ha_state_t ha; + ha_init(&ha, &g_dispatch_cfg, 0); + atomic_store_explicit(&ha.role, (int32_t)CTRL_INIT, memory_order_release); + + int rc = ha_dispatch_gate(&ha, OPCode_jobsub); + TEST_ASSERT_EQUAL_INT(-XCE_ERR_NOT_READY, rc); + + ha_fini(&ha); + printf("test_dispatch_gate_init_blocks_non_election_ops: PASS\n"); +} + +static void test_dispatch_gate_electing_blocks_non_election_ops(void) +{ + /* Req 1.3: Same gate applies when role is CTRL_ELECTING. + * OPCode_get (5) is another representative non-election opcode. */ + ha_state_t ha; + ha_init(&ha, &g_dispatch_cfg, 0); + atomic_store_explicit(&ha.role, (int32_t)CTRL_ELECTING, memory_order_release); + + int rc = ha_dispatch_gate(&ha, OPCode_get); + TEST_ASSERT_EQUAL_INT(-XCE_ERR_NOT_READY, rc); + + ha_fini(&ha); + printf("test_dispatch_gate_electing_blocks_non_election_ops: PASS\n"); +} + +static void test_dispatch_gate_init_allows_request_vote(void) +{ + /* Req 1.3: OP_requestVote must pass through in CTRL_INIT state. + * Returns 0 (permitted). */ + ha_state_t ha; + ha_init(&ha, &g_dispatch_cfg, 0); + atomic_store_explicit(&ha.role, (int32_t)CTRL_INIT, memory_order_release); + + int rc = ha_dispatch_gate(&ha, OPCode_requestVote); + TEST_ASSERT_EQUAL_INT(0, rc); + + ha_fini(&ha); + printf("test_dispatch_gate_init_allows_request_vote: PASS\n"); +} + +static void test_dispatch_gate_init_allows_probe_config_tag(void) +{ + /* Req 1.3: OP_probeConfigTag must pass through in CTRL_INIT state. + * Returns 0 (permitted). */ + ha_state_t ha; + ha_init(&ha, &g_dispatch_cfg, 0); + atomic_store_explicit(&ha.role, (int32_t)CTRL_INIT, memory_order_release); + + int rc = ha_dispatch_gate(&ha, OPCode_probeConfigTag); + TEST_ASSERT_EQUAL_INT(0, rc); + + ha_fini(&ha); + printf("test_dispatch_gate_init_allows_probe_config_tag: PASS\n"); +} + +static void test_dispatch_gate_electing_allows_request_vote(void) +{ + /* Req 1.3: OP_requestVote must pass through in CTRL_ELECTING state. */ + ha_state_t ha; + ha_init(&ha, &g_dispatch_cfg, 0); + atomic_store_explicit(&ha.role, (int32_t)CTRL_ELECTING, memory_order_release); + + int rc = ha_dispatch_gate(&ha, OPCode_requestVote); + TEST_ASSERT_EQUAL_INT(0, rc); + + ha_fini(&ha); + printf("test_dispatch_gate_electing_allows_request_vote: PASS\n"); +} + +static void test_dispatch_gate_electing_allows_probe_config_tag(void) +{ + /* Req 1.3: OP_probeConfigTag must pass through in CTRL_ELECTING state. */ + ha_state_t ha; + ha_init(&ha, &g_dispatch_cfg, 0); + atomic_store_explicit(&ha.role, (int32_t)CTRL_ELECTING, memory_order_release); + + int rc = ha_dispatch_gate(&ha, OPCode_probeConfigTag); + TEST_ASSERT_EQUAL_INT(0, rc); + + ha_fini(&ha); + printf("test_dispatch_gate_electing_allows_probe_config_tag: PASS\n"); +} + +/* ── Req 1.4: dispatch gate — CTRL_STANDBY blocks primary-only ops ───────── */ + +static void test_dispatch_gate_standby_blocks_primary_only_ops(void) +{ + /* Req 1.4: When role is CTRL_STANDBY, primary-only operations must + * receive XCE_ERR_STANDBY. + * + * OPCode_jobsub (11) is a representative primary-only opcode. + * The implementation populates redirect_host/port when a primary is known + * (tested separately in join tests); here we only verify the error code. */ + ha_state_t ha; + ha_init(&ha, &g_dispatch_cfg, 0); + atomic_store_explicit(&ha.role, (int32_t)CTRL_STANDBY, memory_order_release); + + int rc = ha_dispatch_gate(&ha, OPCode_jobsub); + TEST_ASSERT_EQUAL_INT(-XCE_ERR_STANDBY, rc); + + ha_fini(&ha); + printf("test_dispatch_gate_standby_blocks_primary_only_ops: PASS\n"); +} + +static void test_dispatch_gate_primary_allows_all_ops(void) +{ + /* Sanity: when role is CTRL_PRIMARY, all ops including primary-only ones + * must be permitted (return 0). */ + ha_state_t ha; + ha_init(&ha, &g_dispatch_cfg, 0); + atomic_store_explicit(&ha.role, (int32_t)CTRL_PRIMARY, memory_order_release); + + TEST_ASSERT_EQUAL_INT(0, ha_dispatch_gate(&ha, OPCode_jobsub)); + TEST_ASSERT_EQUAL_INT(0, ha_dispatch_gate(&ha, OPCode_get)); + TEST_ASSERT_EQUAL_INT(0, ha_dispatch_gate(&ha, OPCode_requestVote)); + + ha_fini(&ha); + printf("test_dispatch_gate_primary_allows_all_ops: PASS\n"); +} + +/* ── Req 1.x: ha_init() initializes heartbeat_seen semaphore ────────────── */ + +static void test_ha_init_initializes_heartbeat_semaphore(void) +{ + /* Req 1: ha_init() must initialize ha->heartbeat_seen so that + * ha_wait_for_heartbeat_or_timeout() can safely call sem_timedwait on it. + * + * Verify the semaphore is initialized at value 0 (no heartbeat yet seen): + * sem_trywait() on a zero-value semaphore must return -1 with errno=EAGAIN. + * Also verify sem_post() + sem_wait() round-trip works (semaphore is live). */ + ha_state_t ha; + ha_init(&ha, &g_dispatch_cfg, 0); + + /* sem_trywait on a freshly initialized semaphore (value=0) must fail with + * EAGAIN — confirming it was initialized at 0, not garbage. */ + int rc = sem_trywait(&ha.heartbeat_seen); + TEST_ASSERT_EQUAL_INT(-1, rc); + TEST_ASSERT_EQUAL_INT(EAGAIN, errno); + + /* Round-trip: post then wait must succeed immediately */ + TEST_ASSERT_EQUAL_INT(0, sem_post(&ha.heartbeat_seen)); + TEST_ASSERT_EQUAL_INT(0, sem_wait(&ha.heartbeat_seen)); + + ha_fini(&ha); + printf("test_ha_init_initializes_heartbeat_semaphore: PASS\n"); +} + /* ── Test suite entry point ───────────────────────────────────────────── */ void test_ha_startup(void) @@ -241,4 +451,19 @@ void test_ha_startup(void) /* Req 4.4 — vcpus key WARN via warn_buf; no failure */ RUN_TEST(test_vcpus_key_in_config_warns_but_does_not_fail); RUN_TEST(test_config_without_vcpus_key_has_no_warning); + + /* Req 1.3 — dispatch gate: CTRL_INIT and CTRL_ELECTING block non-election ops */ + RUN_TEST(test_dispatch_gate_init_blocks_non_election_ops); + RUN_TEST(test_dispatch_gate_electing_blocks_non_election_ops); + RUN_TEST(test_dispatch_gate_init_allows_request_vote); + RUN_TEST(test_dispatch_gate_init_allows_probe_config_tag); + RUN_TEST(test_dispatch_gate_electing_allows_request_vote); + RUN_TEST(test_dispatch_gate_electing_allows_probe_config_tag); + + /* Req 1.4 — dispatch gate: CTRL_STANDBY blocks primary-only ops */ + RUN_TEST(test_dispatch_gate_standby_blocks_primary_only_ops); + RUN_TEST(test_dispatch_gate_primary_allows_all_ops); + + /* Req 1.x — ha_init() initializes heartbeat_seen semaphore */ + RUN_TEST(test_ha_init_initializes_heartbeat_semaphore); }