From 5f67be4fa37dcc40dbb453cc11262437767c347a Mon Sep 17 00:00:00 2001 From: Rongsong Shen Date: Wed, 24 Jun 2026 14:05:19 +0000 Subject: [PATCH] test(raft-sim-conformance): conformance Restart persistence test (task 4.3) --- .kiro/specs/raft-sim-conformance/tasks.md | 10 +- src/lib/testsuite/test_raft.c | 108 ++++++++++++++++++++++ 2 files changed, 117 insertions(+), 1 deletion(-) diff --git a/.kiro/specs/raft-sim-conformance/tasks.md b/.kiro/specs/raft-sim-conformance/tasks.md index 8fbc5cf..38286c8 100644 --- a/.kiro/specs/raft-sim-conformance/tasks.md +++ b/.kiro/specs/raft-sim-conformance/tasks.md @@ -113,7 +113,7 @@ - _Boundary: raft_sim.c, test_raft.c_ - _Depends: 2.2, 3.5, 4.1_ -- [ ] 4.3 RED+GREEN: Restart preserves persisted `term`/`votedFor`/log +- [x] 4.3 RED+GREEN: Restart preserves persisted `term`/`votedFor`/log - Test: a Restart action re-inits a node while keeping `node.log[]`, recovered `log_len`, and last-persisted `(term, votedFor)`; volatile state resets per `raft.tla` `Restart` - Implement the Restart branch (feed `initial_log_len` + restored `(term, votedFor)` into `raft_init`) - Observable completion: a restarted node retains its log and term; no invariant violation results from the restart alone @@ -227,3 +227,11 @@ legitimate scenario surfaces it, report BLOCKED with the upstream cause rather than working around it. Seed 3 is clean over 600 steps and is used only to prove the 4.2 scheduler mechanism reaches all action kinds. +- **UPDATE (post-4.2): bug-1095/1096 FIXED in raft.c** (commit `fix(raft): vote-reply + per-peer dedup + stale-term drop`). `raft_on_vote_reply` now tracks grants in a per-peer + `vote_replies[]` set (idempotent under DuplicateMessage) and drops stale-term replies + (DropStaleResponse), mirroring the existing pre-vote idiom; 47 raft / 264 lib tests pass. + The library is now correct, so downstream tasks (5.x scenarios, 6.1 M-seed sweep, 7.2 + correct-direction) CAN and MUST assert zero invariant violations on the correct build. + The user authorized this cross-boundary fix. The deliberately-broken-build fault switch + (task 7) remains the way to deliberately re-introduce a divergence for the negative test. diff --git a/src/lib/testsuite/test_raft.c b/src/lib/testsuite/test_raft.c index 8663e0d..ebc4626 100644 --- a/src/lib/testsuite/test_raft.c +++ b/src/lib/testsuite/test_raft.c @@ -7854,6 +7854,113 @@ static void test_sim_4_2_step_halts_on_forced_violation(void) /* A step WAS recorded before applying (R1.4): the trace is non-empty. */ TEST_ASSERT_TRUE(c.trace_len >= 1); } + +/* + * test_sim_4_3_restart_preserves_persistence (raft-sim-conformance task 4.3; Req 2.1) + * + * Conformance-harness Restart semantics (raft.tla `Restart`): a Restart re-inits a + * node while keeping its DURABLE state — node->log[] entries, the recovered log_len + * (== log_count), and the last-persisted (term, votedFor) — and RESETS the VOLATILE + * consensus state (role -> FOLLOWER, votes, election bookkeeping, per-peer leader + * progress). raft.tla resets commitIndex to 0 on restart; XCE deliberately persists + * the committed LSN and restores it (a documented XCE durability deviation), so the + * restarted node's commit_index is the restored durable value, never a regression. + * + * The Restart IMPL (raft_sim_restart_node) pre-exists from raft-commit-safety, so the + * "RED" here is the ABSENCE of a conformance-restart test that pins these semantics. + * Every assertion below names a SPECIFIC retained/reset value so a regression that + * wiped the log, dropped the persisted term/votedFor, or failed to reset volatile + * state would fail this test: + * - a log-wiping restart fails the log_count / per-entry term assertions; + * - a term/votedFor-losing restart fails the persisted (term, voted_for) assertions; + * - a volatile-state-leaking restart (e.g. staying LEADER) fails the FOLLOWER / + * votes_granted / election_decided assertions. + * The post-restart raft_sim_check_all == RAFT_SIM_INV_NONE proves the restart alone + * produces no invariant violation. + */ +static void test_sim_4_3_restart_preserves_persistence(void) +{ + raft_sim_cluster_t c; + memset(&c, 0, sizeof(c)); + + /* 3-node conformance cluster so the restart helper restores via raft_init with + * the conformance callback set still wired (it preserves node->cbs). */ + TEST_ASSERT_EQUAL_INT(0, raft_sim_conformance_init(&c, 3, 2000, 0x4A33ULL)); + + /* Drive node 0 to LEADER so it holds genuine VOLATILE state (LEADER role, a + * decided election, per-peer next/match progress) that the restart must reset, + * plus a real persisted (term, voted_for) — its own self-vote in the won term. */ + raft_start_election(&c.nodes[0].state); + for (int g = 0; g < 64 && c.bag_count > 0; g++) + raft_sim_bag_deliver(&c, 0); /* drain election traffic; node 0 wins */ + TEST_ASSERT_EQUAL_INT(RAFT_LEADER, c.nodes[0].state.role); + + /* Leader appends a few client entries so node 0 owns a NON-EMPTY durable log. */ + for (int e = 0; e < 3; e++) { + uint64_t value = 0xC0DE0000ULL + (uint64_t)e; + uint64_t out_index = 0; + TEST_ASSERT_EQUAL_INT(RAFT_OK, + raft_log_append(&c.nodes[0].state, &value, sizeof(value), &out_index)); + } + TEST_ASSERT_EQUAL_INT(3, c.nodes[0].log_count); + + /* Snapshot the DURABLE state a correct Restart must retain, plus a copy of the + * log bytes/terms so a wipe is detectable byte-for-byte. */ + raft_sim_node_t *node = &c.nodes[0]; + uint64_t persisted_term = node->state.term; + int persisted_voted_for = node->state.voted_for; + int durable_log_count = node->log_count; + uint64_t durable_commit = node->state.commit_index; + raft_sim_entry_t durable_log[3]; + for (int i = 0; i < 3; i++) + durable_log[i] = node->log[i]; + + /* Sanity: node 0 self-voted in the won term (a real persisted vote to retain). */ + TEST_ASSERT_EQUAL_INT(0, persisted_voted_for); + TEST_ASSERT_TRUE(persisted_term > 0); + /* Sanity: VOLATILE leader bookkeeping is genuinely set pre-restart, so the reset + * assertions below are meaningful (not vacuously already-zero). */ + TEST_ASSERT_TRUE(node->state.election_decided == 1); + TEST_ASSERT_TRUE(node->state.votes_granted >= 1); + + /* ---- RESTART (raft.tla Restart) ---- */ + TEST_ASSERT_EQUAL_INT(0, raft_sim_restart_node(&c, 0)); + + /* DURABLE state RETAINED: log[] bytes/terms, log_count/log_len, (term, votedFor), + * and the restored committed LSN (XCE durability deviation — never regresses). */ + TEST_ASSERT_EQUAL_INT(durable_log_count, node->log_count); + TEST_ASSERT_EQUAL_UINT64((uint64_t)durable_log_count, node->state.log_len); + for (int i = 0; i < durable_log_count; i++) { + TEST_ASSERT_EQUAL_UINT64(durable_log[i].term, node->log[i].term); + TEST_ASSERT_EQUAL_UINT(durable_log[i].len, node->log[i].len); + TEST_ASSERT_EQUAL_INT(0, + memcmp(durable_log[i].bytes, node->log[i].bytes, durable_log[i].len)); + } + TEST_ASSERT_EQUAL_UINT64(persisted_term, node->state.term); + TEST_ASSERT_EQUAL_INT(persisted_voted_for, node->state.voted_for); + TEST_ASSERT_EQUAL_UINT64(durable_commit, node->state.commit_index); + + /* VOLATILE state RESET to a fresh FOLLOWER: role, votes, election bookkeeping, + * and per-peer leader progress all cleared by the re-init. */ + TEST_ASSERT_EQUAL_INT(RAFT_FOLLOWER, node->state.role); + TEST_ASSERT_EQUAL_INT(0, node->state.votes_granted); + TEST_ASSERT_EQUAL_INT(0, node->state.election_decided); + TEST_ASSERT_EQUAL_INT(0, node->state.replies_received); + TEST_ASSERT_EQUAL_INT(0, node->state.pre_vote_in_progress); + for (int p = 0; p < node->state.n_peers; p++) { + /* raft_init seeds next_index to log_len+1 and zeroes match_index (the + * leader-progress reset). match_index back to 0 proves the leader progress + * was discarded. */ + TEST_ASSERT_EQUAL_UINT64(0, node->state.match_index[p]); + } + + /* Partition flag and ctx wiring are preserved across the restart. */ + TEST_ASSERT_EQUAL_INT(0, node->partition); + TEST_ASSERT_TRUE(node->ctx == &c); + + /* The restart ALONE produces no invariant violation (Req 2.1). */ + TEST_ASSERT_EQUAL_INT(RAFT_SIM_INV_NONE, raft_sim_check_all(&c)); +} #endif /* XCE_TESTING */ /* ---------- test_raft entry point ---------- */ @@ -8062,6 +8169,7 @@ void test_raft(void) RUN_TEST(test_sim_4_1_trace_record_and_dump); RUN_TEST(test_sim_4_2_step_liveness_and_coverage); RUN_TEST(test_sim_4_2_step_halts_on_forced_violation); + RUN_TEST(test_sim_4_3_restart_preserves_persistence); /* Group A: basic election correctness (task 5.1) */ RUN_TEST(test_proto_A1_single_node_self_elects);