feat(raft-commit-safety): (lastLogTerm,lastLogIndex) vote+prevote freshness (tasks 1.3, 4.1-4.2, 5.1-5.2, 6.2)

logOk (raft.tla:285, closes G6): local_last_log_term = log_len>0 ? on_get_entry_term(log_len):0;
logOk = cand_last_log_term > local || (== && cand_last_log_index >= log_len). Lexicographic:
term dominates, tie decided by index. Shared raft_log_ok helper.

Vote (4.2): logOk deny inserted after stale-term/UpdateTerm/role-gate/one-vote-per-term and
BEFORE on_vote_eligible (denies even when the app gate would grant); persist-before-grant intact.
PreVote (5.2): identical logOk, fully side-effect-free (no term/vote/role/commit mutation, no
on_persist). Signatures extended (1.3) on raft_on_vote_request/pre_vote_request; all 39 lib call
sites + sim routing updated; sim captures the candidate's send-time last-log tuple (raft_sim_rpc_t).
Empty-log call sites pass (0,0) → logOk true → existing outcomes preserved. 9 freshness tests.

NOTE: controller cluster.c vote callers still use the old signatures and will NOT compile — an
accepted, design-documented revalidation trigger; the controller adapter plumbing is owned by the
downstream ha-term-safety spec (to be implemented next). Library suite ASAN green (239 tests).
This commit is contained in:
Rongsong Shen 2026-06-22 00:14:26 +00:00
parent 195f660f9a
commit 56485e2fa3
5 changed files with 449 additions and 62 deletions

View file

@ -23,7 +23,7 @@
- _Boundary: raft.h_
- _Depends: 1.1_
- [ ] 1.3 Declare `raft_advance_commit` and extend the vote/PreVote signatures with last_log_term/last_log_index
- [x] 1.3 Declare `raft_advance_commit` and extend the vote/PreVote signatures with last_log_term/last_log_index
- Declare `int raft_advance_commit(raft_state_t *s)`
- Extend `raft_on_vote_request` and `raft_on_pre_vote_request` to accept `cand_last_log_term` and `cand_last_log_index` (the chosen "library owns logOk" plumbing — Option B in design.md)
- Update the header doc comments to describe the new logOk gate and the commit-advance action
@ -74,7 +74,7 @@
## 4. (lastLogTerm, lastLogIndex) vote freshness (raft.tla:285, closes G6)
- [ ] 4.1 RED: binding-vote freshness tests
- [x] 4.1 RED: binding-vote freshness tests
- Test: candidate with stale `last_log_term` denied even when `last_log_index` is greater; candidate with fresher `last_log_term` granted with a smaller `last_log_index`; tie on term grants only when `cand_last_log_index >= log_len`; the voter's own last-log term/index come from `on_get_entry_term(log_len)`/`log_len` (term 0 when empty)
- Test: `!logOk` denies even when `on_vote_eligible` would grant (logOk applied BEFORE the application gate); persist-before-grant unchanged on the grant path
- Observable completion: tests compile and FAIL
@ -82,7 +82,7 @@
- _Boundary: test_raft.c_
- _Depends: 1.3_
- [ ] 4.2 GREEN: implement the logOk gate in `raft_on_vote_request`
- [x] 4.2 GREEN: implement the logOk gate in `raft_on_vote_request`
- Compute `local_last_log_term = (log_len > 0) ? on_get_entry_term(log_len) : 0`; `logOk = (cand_last_log_term > local_last_log_term) || (cand_last_log_term == local_last_log_term && cand_last_log_index >= log_len)`
- Insert the logOk deny BEFORE `on_vote_eligible`, after the existing stale-term/UpdateTerm/one-vote-per-term steps; leave persist-before-grant and all existing invariants in place
- Observable completion: 4.1 tests pass; no regression in existing vote tests; the logOk block is clearly before `on_vote_eligible` in `raft.c`
@ -92,7 +92,7 @@
## 5. (lastLogTerm, lastLogIndex) PreVote freshness (raft.tla:285, side-effect-free)
- [ ] 5.1 RED: PreVote freshness tests (identical rule, no side effects)
- [x] 5.1 RED: PreVote freshness tests (identical rule, no side effects)
- Test: `raft_on_pre_vote_request` applies the SAME `(lastLogTerm, lastLogIndex)` logOk as the binding vote; a stale tuple is denied; the existing stale-term deny and `on_vote_eligible` gate remain in force
- Test: across grant and deny, no mutation of `term`/`voted_for`/`voted_for_term`/`role`/`commit_index` and no `on_persist` call (PreVote invariant)
- Observable completion: tests compile and FAIL
@ -100,7 +100,7 @@
- _Boundary: test_raft.c_
- _Depends: 1.3_
- [ ] 5.2 GREEN: implement the logOk gate in `raft_on_pre_vote_request`
- [x] 5.2 GREEN: implement the logOk gate in `raft_on_pre_vote_request`
- Apply the identical logOk predicate (factored to match 4.2) before/with the existing eligibility gate; mutate nothing; no `on_persist`
- Observable completion: 5.1 tests pass; no regression in existing PreVote tests; the predicate matches the binding-vote path exactly
- _Requirements: 4.1, 4.2, 4.3, 4.4, 4.5_
@ -118,7 +118,7 @@
- _Boundary: raft_sim.c_
- _Depends: 3.2_
- [ ] 6.2 Plumb `(last_log_term, last_log_index)` through the sim vote/PreVote routing
- [x] 6.2 Plumb `(last_log_term, last_log_index)` through the sim vote/PreVote routing
- Update the sim's vote and PreVote delivery to read the candidate's last-log `(term, index)` from its in-memory log (`term at log_len` / `log_len`) and pass them to the extended `raft_on_vote_request` / `raft_on_pre_vote_request`
- Observable completion: existing election sim tests still pass with the extended signatures; a node with a stale last-log term is denied a vote it would previously have won on LSN alone
- _Requirements: 6.5, 3.2, 4.2_

View file

@ -148,7 +148,25 @@ int raft_start_election(raft_state_t *s)
return RAFT_OK;
}
/* (lastLogTerm, lastLogIndex) freshness predicate (raft.tla
* HandleRequestVoteRequest logOk, line 285, closes G6). The voter's own last-log
* tuple is (on_get_entry_term(log_len), log_len) — term 0 when the log is empty,
* matching the on_get_entry_term 0-means-absent sentinel. The library owns this
* decision (design "Option B: library owns logOk") and applies it identically on
* the binding vote and the PreVote eligibility paths. */
static int raft_log_ok(const raft_state_t *s, uint64_t cand_last_log_term,
uint64_t cand_last_log_index)
{
uint64_t local_last_log_term =
(s->log_len > 0) ? s->cbs->on_get_entry_term(s->ctx, s->log_len) : 0;
return (cand_last_log_term > local_last_log_term) ||
(cand_last_log_term == local_last_log_term &&
cand_last_log_index >= s->log_len);
}
int raft_on_vote_request(raft_state_t *s, int from_idx, uint64_t term,
uint64_t cand_last_log_term,
uint64_t cand_last_log_index,
uint64_t *reply_term)
{
/* Step 1: stale term — deny immediately, no state change */
@ -208,7 +226,18 @@ int raft_on_vote_request(raft_state_t *s, int from_idx, uint64_t term,
/* same candidate — fall through to re-persist and re-grant */
}
/* Step 5: application-level policy gate (freshness, log lsn, etc.) */
/* Step 5: (lastLogTerm, lastLogIndex) freshness gate (raft.tla
* HandleRequestVoteRequest logOk, 285, closes G6). This precedes the
* application gate: a !logOk denies regardless of on_vote_eligible (Req 3.4),
* without weakening the stale-term / one-vote-per-term / persist-before-grant
* invariants above (Req 3.5). */
if (!raft_log_ok(s, cand_last_log_term, cand_last_log_index)) {
if (s->cbs->on_trace)
s->cbs->on_trace(s->ctx, "vote_denied_log_stale", NULL);
return RAFT_DENY;
}
/* Step 6: application-level policy gate (freshness, log lsn, etc.) */
if (s->cbs->on_vote_eligible != NULL) {
if (s->cbs->on_vote_eligible(s->ctx, from_idx, term) == 0) {
if (s->cbs->on_trace)
@ -217,7 +246,7 @@ int raft_on_vote_request(raft_state_t *s, int from_idx, uint64_t term,
}
}
/* Step 6: persist before granting — durability guarantee */
/* Step 7: persist before granting — durability guarantee */
if (s->cbs->on_persist(s->ctx, term, from_idx) != 0) {
/* Persist failed — deny without updating voted_for */
return RAFT_DENY;
@ -231,15 +260,18 @@ int raft_on_vote_request(raft_state_t *s, int from_idx, uint64_t term,
return RAFT_GRANT;
}
int raft_on_pre_vote_request(raft_state_t *s, int from_idx, uint64_t cand_term)
int raft_on_pre_vote_request(raft_state_t *s, int from_idx, uint64_t cand_term,
uint64_t cand_last_log_term,
uint64_t cand_last_log_index)
{
/* PreVote is a side-effect-free straw poll: it answers "would you vote for
* me in cand_term?" WITHOUT changing any persistent state. Unlike
* raft_on_vote_request, this function never adopts cand_term (even when
* higher — that adoption is the very thing PreVote exists to prevent), never
* touches term / voted_for / voted_for_term / role, and never calls
* on_persist. The dispatcher applies the freshness floor before this call,
* exactly as on the real-vote path. */
* on_persist. The library applies the SAME (lastLogTerm, lastLogIndex)
* logOk freshness gate as the real-vote path (via raft_log_ok), so a stale
* candidate is rejected here exactly as it would be in a binding vote. */
/* Step 1: stale term — deny (mirrors raft_on_vote_request Step 1). A
* candidate proposing a term we already passed could never win a real
@ -251,10 +283,21 @@ int raft_on_pre_vote_request(raft_state_t *s, int from_idx, uint64_t cand_term)
return RAFT_DENY;
}
/* Step 2: application-level eligibility gate — reuse the EXISTING
/* Step 2: (lastLogTerm, lastLogIndex) freshness gate — the IDENTICAL logOk
* as the binding vote path (raft.tla 285, closes G6 for PreVote, Req 4.1).
* The library owns this predicate (the dispatcher no longer applies freshness
* before this call). Side-effect-free: a deny mutates nothing and never
* persists (Req 4.4). */
if (!raft_log_ok(s, cand_last_log_term, cand_last_log_index)) {
if (s->cbs->on_trace)
s->cbs->on_trace(s->ctx, "pre_vote_denied_log_stale", NULL);
return RAFT_DENY;
}
/* Step 3: application-level eligibility gate — reuse the EXISTING
* on_vote_eligible callback (primary-deny / fresh-heartbeat stickiness /
* catch-up deny), identical to raft_on_vote_request Step 5. If NULL, every
* Raft-eligible candidate is granted. */
* catch-up deny), identical to raft_on_vote_request's application gate. If
* NULL, every Raft-eligible candidate is granted. */
if (s->cbs->on_vote_eligible != NULL) {
if (s->cbs->on_vote_eligible(s->ctx, from_idx, cand_term) == 0) {
if (s->cbs->on_trace)

View file

@ -244,12 +244,29 @@ int raft_start_election(raft_state_t *s);
/*
* raft_on_vote_request: process an incoming RequestVote RPC.
* Applies Raft invariants (term comparison, one-vote-per-term).
* If invariants pass, calls on_vote_eligible to allow application policy.
* Applies Raft invariants (term comparison, one-vote-per-term), then the
* (lastLogTerm, lastLogIndex) freshness gate (raft.tla HandleRequestVoteRequest
* logOk, line 285, closes G6), then the application policy gate.
*
* The library owns the logOk decision (design "Option B: library owns logOk"):
* local_last_log_term = (log_len > 0) ? on_get_entry_term(log_len) : 0
* logOk = (cand_last_log_term > local_last_log_term)
* || (cand_last_log_term == local_last_log_term
* && cand_last_log_index >= log_len)
* The candidate advertises (cand_last_log_term, cand_last_log_index) in its wire
* RequestVote; the caller decodes and passes them here.
*
* Step order: stale-term deny → UpdateTerm/step-down → role gate →
* one-vote-per-term → logOk gate (NEW; precedes the application gate, so a
* !logOk denies regardless of on_vote_eligible) → on_vote_eligible →
* persist-before-grant.
*
* Fires on_persist before granting.
* Returns RAFT_GRANT (1) or RAFT_DENY (0); sets *reply_term to current term.
*/
int raft_on_vote_request(raft_state_t *s, int from_idx, uint64_t term,
uint64_t cand_last_log_term,
uint64_t cand_last_log_index,
uint64_t *reply_term);
/*
@ -301,13 +318,17 @@ int raft_begin_pre_vote(raft_state_t *s);
/*
* raft_on_pre_vote_request: answer an incoming PreVote probe.
* Denies if cand_term < s->term; otherwise returns on_vote_eligible(ctx,
* from_idx, cand_term) (granting when the gate is NULL). Never touches term,
* voted_for, role, or fires on_persist. Freshness is applied by the
* dispatcher before this call (same contract as the real vote path).
* Denies if cand_term < s->term; then applies the IDENTICAL (lastLogTerm,
* lastLogIndex) logOk as raft_on_vote_request (the library owns the predicate —
* it is NO LONGER applied by the dispatcher before this call), then consults
* on_vote_eligible (granting when the gate is NULL). Mutates NOTHING — never
* touches term, voted_for, voted_for_term, role, or commit_index, and never
* fires on_persist — preserving the PreVote side-effect-free invariant.
* Returns 1 = grant, 0 = deny.
*/
int raft_on_pre_vote_request(raft_state_t *s, int from_idx, uint64_t cand_term);
int raft_on_pre_vote_request(raft_state_t *s, int from_idx, uint64_t cand_term,
uint64_t cand_last_log_term,
uint64_t cand_last_log_index);
/*
* raft_on_pre_vote_reply: tally a reply to our PreVote.
@ -423,11 +444,17 @@ void raft_snapshot(const raft_state_t *s, raft_snapshot_t *snap);
/* Upper bound on buffered AppendEntries awaiting delivery (mirrors pending[]). */
#define RAFT_SIM_MAX_AE 64
/* Buffered RPC entry: one pending vote-request to route */
/* Buffered RPC entry: one pending vote-request to route.
* last_log_term/last_log_index capture the CANDIDATE's advertised last-log tuple
* at SEND time (mirroring raft_sim_ae_t.leader_term) so the (lastLogTerm,
* lastLogIndex) freshness gate (raft-commit-safety) is evaluated against the
* candidate's send-epoch log, not a value re-read at delivery. */
typedef struct {
int src;
int dst;
uint64_t term;
uint64_t last_log_term; /* candidate's last-log term at send (0 if empty) */
uint64_t last_log_index; /* candidate's last-log index at send (== log_count) */
} raft_sim_rpc_t;
/* One in-memory log entry: term stamp + fixed-size payload bytes.

View file

@ -53,6 +53,14 @@ static int sim_send_vote_request(void *ctx, int peer_idx, uint64_t term)
rpc->src = node->idx;
rpc->dst = peer_idx;
rpc->term = term;
/* Capture the CANDIDATE's last-log tuple at SEND time from its own in-memory
* log (the library holds no entry bytes): last_log_index == log_count,
* last_log_term == term of the tail entry (0 when the log is empty). This
* feeds the (lastLogTerm, lastLogIndex) freshness gate at delivery. */
rpc->last_log_index = (uint64_t)node->log_count;
rpc->last_log_term = (node->log_count > 0)
? node->log[node->log_count - 1].term
: 0;
return 0;
}
@ -319,9 +327,12 @@ void raft_sim_deliver_pending(raft_sim_cluster_t *c)
if (c->nodes[src].partition || c->nodes[dst].partition)
continue;
/* Deliver vote request to dst */
/* Deliver vote request to dst, passing the candidate's send-time last-log
* tuple so dst evaluates the (lastLogTerm, lastLogIndex) freshness gate. */
uint64_t reply_term = 0;
int granted = raft_on_vote_request(&c->nodes[dst].state, src, term,
rpc->last_log_term,
rpc->last_log_index,
&reply_term);
/* Route reply back to src */

View file

@ -130,6 +130,13 @@ typedef struct {
int vote_eligible_return_value; /* configurable: 1=grant, 0=deny; default 1 */
int vote_eligible_call_count;
/* on_get_entry_term mock (raft-commit-safety freshness, task 4.1/5.1).
* The voter's local last-log term is returned for index == get_entry_term_log_len;
* any other index returns 0 (absent). Default 0 keeps empty-log scenarios at the
* pre-existing local_last_log_term == 0. */
uint64_t local_last_log_term; /* term reported at index == get_entry_term_log_len */
uint64_t get_entry_term_log_len; /* the index treated as the local last log */
/* on_trace mock */
int trace_call_count;
char last_trace_event[64];
@ -241,6 +248,19 @@ static int mock_vote_eligible(void *ctx, int from_idx, uint64_t term)
return g_mock.vote_eligible_return_value;
}
/* Configurable local-log term reporter for freshness tests (task 4.1/5.1).
* Returns g_mock.local_last_log_term for the configured last-log index, 0
* otherwise. Defaults (both 0) reproduce the previous always-0 behaviour, so
* empty-log call sites are unaffected. */
static uint64_t mock_get_entry_term(void *ctx, uint64_t index)
{
(void)ctx;
if (g_mock.get_entry_term_log_len != 0 &&
index == g_mock.get_entry_term_log_len)
return g_mock.local_last_log_term;
return 0;
}
static void mock_trace(void *ctx, const char *event, const char *detail)
{
(void)ctx; (void)detail;
@ -940,7 +960,7 @@ static void test_vote_req_stale_term(void)
TEST_ASSERT_EQUAL_INT(RAFT_FOLLOWER, s.role);
TEST_ASSERT_EQUAL_INT(-1, s.voted_for);
int rc = raft_on_vote_request(&s, 1, 3, &reply_term);
int rc = raft_on_vote_request(&s, 1, 3, 0, 0, &reply_term);
TEST_ASSERT_EQUAL_INT(RAFT_DENY, rc);
TEST_ASSERT_EQUAL_UINT64(5, reply_term);
@ -981,7 +1001,7 @@ static void test_vote_req_higher_term_updates_state(void)
s.voted_for = 0; /* voted for self */
s.voted_for_term = 2;
int rc = raft_on_vote_request(&s, 1, 5, &reply_term);
int rc = raft_on_vote_request(&s, 1, 5, 0, 0, &reply_term);
/* Term upgrade must happen: term updated, role stepped down */
TEST_ASSERT_EQUAL_UINT64(5, s.term);
@ -1021,7 +1041,7 @@ static void test_vote_req_same_term_already_voted_different(void)
s.voted_for = 1;
s.voted_for_term = 5;
int rc = raft_on_vote_request(&s, 2, 5, &reply_term);
int rc = raft_on_vote_request(&s, 2, 5, 0, 0, &reply_term);
TEST_ASSERT_EQUAL_INT(RAFT_DENY, rc);
TEST_ASSERT_EQUAL_UINT64(5, reply_term);
@ -1058,7 +1078,7 @@ static void test_vote_req_same_term_idempotent_same_candidate(void)
s.voted_for = 1;
s.voted_for_term = 5;
int rc = raft_on_vote_request(&s, 1, 5, &reply_term);
int rc = raft_on_vote_request(&s, 1, 5, 0, 0, &reply_term);
TEST_ASSERT_EQUAL_INT(RAFT_GRANT, rc);
TEST_ASSERT_EQUAL_UINT64(5, reply_term);
@ -1089,7 +1109,7 @@ static void test_vote_req_leader_denies(void)
TEST_ASSERT_EQUAL_INT(0, init_vote_node(&s, &cbs, 3, 0, 5));
s.role = RAFT_LEADER;
int rc = raft_on_vote_request(&s, 1, 5, &reply_term);
int rc = raft_on_vote_request(&s, 1, 5, 0, 0, &reply_term);
TEST_ASSERT_EQUAL_INT(RAFT_DENY, rc);
TEST_ASSERT_EQUAL_UINT64(5, reply_term);
@ -1124,7 +1144,7 @@ static void test_vote_req_on_vote_eligible_denies(void)
TEST_ASSERT_EQUAL_INT(0, init_vote_node(&s, &cbs, 3, 0, 5));
int rc = raft_on_vote_request(&s, 1, 5, &reply_term);
int rc = raft_on_vote_request(&s, 1, 5, 0, 0, &reply_term);
TEST_ASSERT_EQUAL_INT(RAFT_DENY, rc);
TEST_ASSERT_EQUAL_UINT64(5, reply_term);
@ -1161,7 +1181,7 @@ static void test_vote_req_on_vote_eligible_grants(void)
TEST_ASSERT_EQUAL_INT(0, init_vote_node(&s, &cbs, 3, 0, 5));
int rc = raft_on_vote_request(&s, 1, 5, &reply_term);
int rc = raft_on_vote_request(&s, 1, 5, 0, 0, &reply_term);
TEST_ASSERT_EQUAL_INT(RAFT_GRANT, rc);
TEST_ASSERT_EQUAL_UINT64(5, reply_term);
@ -1198,7 +1218,7 @@ static void test_vote_req_on_vote_eligible_null(void)
TEST_ASSERT_EQUAL_INT(0, init_vote_node(&s, &cbs, 3, 0, 5));
int rc = raft_on_vote_request(&s, 1, 5, &reply_term);
int rc = raft_on_vote_request(&s, 1, 5, 0, 0, &reply_term);
TEST_ASSERT_EQUAL_INT(RAFT_GRANT, rc);
TEST_ASSERT_EQUAL_UINT64(5, reply_term);
@ -1228,7 +1248,7 @@ static void test_vote_req_persist_fails(void)
TEST_ASSERT_EQUAL_INT(0, init_vote_node(&s, &cbs, 3, 0, 5));
int rc = raft_on_vote_request(&s, 1, 5, &reply_term);
int rc = raft_on_vote_request(&s, 1, 5, 0, 0, &reply_term);
TEST_ASSERT_EQUAL_INT(RAFT_DENY, rc);
TEST_ASSERT_EQUAL_UINT64(5, reply_term);
@ -1257,14 +1277,14 @@ static void test_vote_req_reply_term_always_set(void)
reset_mock();
TEST_ASSERT_EQUAL_INT(0, init_vote_node(&s, &cbs, 3, 0, 7));
reply_term = 0;
raft_on_vote_request(&s, 1, 3, &reply_term);
raft_on_vote_request(&s, 1, 3, 0, 0, &reply_term);
TEST_ASSERT_EQUAL_UINT64(s.term, reply_term); /* must equal current term=7 */
/* Path 2: term upgrade + grant */
reset_mock();
TEST_ASSERT_EQUAL_INT(0, init_vote_node(&s, &cbs, 3, 0, 3));
reply_term = 0;
raft_on_vote_request(&s, 1, 7, &reply_term);
raft_on_vote_request(&s, 1, 7, 0, 0, &reply_term);
TEST_ASSERT_EQUAL_UINT64(s.term, reply_term); /* must equal upgraded term=7 */
/* Path 3: already voted for different — deny */
@ -1272,7 +1292,7 @@ static void test_vote_req_reply_term_always_set(void)
TEST_ASSERT_EQUAL_INT(0, init_vote_node(&s, &cbs, 3, 0, 5));
s.voted_for = 1; s.voted_for_term = 5;
reply_term = 0;
raft_on_vote_request(&s, 2, 5, &reply_term);
raft_on_vote_request(&s, 2, 5, 0, 0, &reply_term);
TEST_ASSERT_EQUAL_UINT64(s.term, reply_term); /* must equal 5 */
/* Path 4: persist fails — deny */
@ -1281,7 +1301,7 @@ static void test_vote_req_reply_term_always_set(void)
g_mock.persist_return_value = -1;
TEST_ASSERT_EQUAL_INT(0, init_vote_node(&s, &cbs, 3, 0, 5));
reply_term = 0;
raft_on_vote_request(&s, 1, 5, &reply_term);
raft_on_vote_request(&s, 1, 5, 0, 0, &reply_term);
TEST_ASSERT_EQUAL_UINT64(s.term, reply_term); /* must equal 5 */
}
@ -4098,7 +4118,7 @@ static void test_proto_A3_late_election_denied(void)
/* Now node 2 tries to request a vote from node 1 at the same term=1 */
uint64_t reply_term = 0;
int rc = raft_on_vote_request(&c.nodes[1].state, 2, 1, &reply_term);
int rc = raft_on_vote_request(&c.nodes[1].state, 2, 1, 0, 0, &reply_term);
/* Node 1 must deny — already voted for node 0 this term */
TEST_ASSERT_EQUAL_INT(RAFT_DENY, rc);
@ -4153,7 +4173,7 @@ static void test_proto_A5_leader_denies_vote_request(void)
/* Send a vote request to the leader at the same term */
uint64_t reply_term = 0;
int rc = raft_on_vote_request(&c.nodes[0].state, 1, term_before, &reply_term);
int rc = raft_on_vote_request(&c.nodes[0].state, 1, term_before, 0, 0, &reply_term);
/* Must be denied */
TEST_ASSERT_EQUAL_INT(RAFT_DENY, rc);
@ -4187,7 +4207,7 @@ static void test_proto_C1_eligible_denies(void)
s.term = 1;
s.voted_for = -1;
int rc = raft_on_vote_request(&s, 1, 1, &reply_term);
int rc = raft_on_vote_request(&s, 1, 1, 0, 0, &reply_term);
TEST_ASSERT_EQUAL_INT(RAFT_DENY, rc);
TEST_ASSERT_EQUAL_INT(-1, s.voted_for);
@ -4214,7 +4234,7 @@ static void test_proto_C2_eligible_grants(void)
s.term = 1;
s.voted_for = -1;
int rc = raft_on_vote_request(&s, 1, 1, &reply_term);
int rc = raft_on_vote_request(&s, 1, 1, 0, 0, &reply_term);
TEST_ASSERT_EQUAL_INT(RAFT_GRANT, rc);
TEST_ASSERT_EQUAL_INT(1, s.voted_for);
@ -4240,7 +4260,7 @@ static void test_proto_C3_eligible_null_grants(void)
s.term = 1;
s.voted_for = -1;
int rc = raft_on_vote_request(&s, 1, 1, &reply_term);
int rc = raft_on_vote_request(&s, 1, 1, 0, 0, &reply_term);
TEST_ASSERT_EQUAL_INT(RAFT_GRANT, rc);
/* vote_eligible not called (NULL) */
@ -4267,7 +4287,7 @@ static void test_proto_C4_stale_term_no_eligible_call(void)
s.term = 5;
s.voted_for = -1;
int rc = raft_on_vote_request(&s, 1, 3, &reply_term);
int rc = raft_on_vote_request(&s, 1, 3, 0, 0, &reply_term);
TEST_ASSERT_EQUAL_INT(RAFT_DENY, rc);
TEST_ASSERT_EQUAL_INT(0, g_mock.vote_eligible_call_count);
@ -4295,7 +4315,7 @@ static void test_proto_C5_already_voted_no_eligible_call(void)
s.voted_for = 1;
s.voted_for_term = 3;
int rc = raft_on_vote_request(&s, 2, 3, &reply_term);
int rc = raft_on_vote_request(&s, 2, 3, 0, 0, &reply_term);
TEST_ASSERT_EQUAL_INT(RAFT_DENY, rc);
TEST_ASSERT_EQUAL_INT(0, g_mock.vote_eligible_call_count);
@ -4395,7 +4415,7 @@ static void test_proto_H6_restored_vote_blocks_second_vote(void)
TEST_ASSERT_EQUAL_INT(0, raft_init(&s, &cfg));
/* Different candidate (idx=2) requests a vote in the SAME term 5 */
int rc = raft_on_vote_request(&s, 2, 5, &reply_term);
int rc = raft_on_vote_request(&s, 2, 5, 0, 0, &reply_term);
TEST_ASSERT_EQUAL_INT(RAFT_DENY, rc);
TEST_ASSERT_EQUAL_UINT64(5, reply_term);
@ -4405,7 +4425,7 @@ static void test_proto_H6_restored_vote_blocks_second_vote(void)
TEST_ASSERT_EQUAL_UINT64(5, s.voted_for_term);
/* Same candidate (idx=1) retransmitting must still be re-granted */
rc = raft_on_vote_request(&s, 1, 5, &reply_term);
rc = raft_on_vote_request(&s, 1, 5, 0, 0, &reply_term);
TEST_ASSERT_EQUAL_INT(RAFT_GRANT, rc);
}
@ -4445,7 +4465,7 @@ static void test_proto_H3_persist_before_vote_recorded(void)
s.term = 1;
s.voted_for = -1;
int rc = raft_on_vote_request(&s, 1, 1, &reply_term);
int rc = raft_on_vote_request(&s, 1, 1, 0, 0, &reply_term);
/* Must grant */
TEST_ASSERT_EQUAL_INT(RAFT_GRANT, rc);
@ -4481,7 +4501,7 @@ static void test_proto_H4_persist_fail_denies(void)
s.term = 1;
s.voted_for = -1;
int rc = raft_on_vote_request(&s, 1, 1, &reply_term);
int rc = raft_on_vote_request(&s, 1, 1, 0, 0, &reply_term);
TEST_ASSERT_EQUAL_INT(RAFT_DENY, rc);
TEST_ASSERT_EQUAL_INT(-1, s.voted_for);
@ -4602,7 +4622,7 @@ static void test_proto_B2_higher_term_updates_state(void)
s.voted_for = 1; /* node voted for itself in an earlier election */
s.voted_for_term = 1; /* that vote was in term 1 */
int rc = raft_on_vote_request(&s, 0, 5, &reply_term);
int rc = raft_on_vote_request(&s, 0, 5, 0, 0, &reply_term);
/* Term must be upgraded to 5 */
TEST_ASSERT_EQUAL_UINT64(5, s.term);
@ -4632,7 +4652,7 @@ static void test_proto_B3_stale_term_denied(void)
reset_mock();
TEST_ASSERT_EQUAL_INT(0, init_vote_node(&s, &cbs, 3, 0, 5));
int rc = raft_on_vote_request(&s, 0, 3, &reply_term);
int rc = raft_on_vote_request(&s, 0, 3, 0, 0, &reply_term);
TEST_ASSERT_EQUAL_INT(RAFT_DENY, rc);
TEST_ASSERT_EQUAL_UINT64(5, reply_term);
@ -4660,7 +4680,7 @@ static void test_proto_B4_different_candidate_denied(void)
s.voted_for = 1;
s.voted_for_term = 3;
int rc = raft_on_vote_request(&s, 2, 3, &reply_term);
int rc = raft_on_vote_request(&s, 2, 3, 0, 0, &reply_term);
TEST_ASSERT_EQUAL_INT(RAFT_DENY, rc);
TEST_ASSERT_EQUAL_UINT64(3, reply_term);
@ -4691,7 +4711,7 @@ static void test_proto_B5_idempotent_same_candidate(void)
s.voted_for = 1;
s.voted_for_term = 3;
int rc = raft_on_vote_request(&s, 1, 3, &reply_term);
int rc = raft_on_vote_request(&s, 1, 3, 0, 0, &reply_term);
TEST_ASSERT_EQUAL_INT(RAFT_GRANT, rc);
TEST_ASSERT_EQUAL_UINT64(3, reply_term);
@ -5055,7 +5075,7 @@ static void test_proto_F2_voted_during_delay_skip(void)
TEST_ASSERT_EQUAL_INT(-1, s.voted_for);
/* During the delay, a vote request arrives from node 0 at term=1 */
int rc = raft_on_vote_request(&s, 0, 1, &reply_term);
int rc = raft_on_vote_request(&s, 0, 1, 0, 0, &reply_term);
TEST_ASSERT_EQUAL_INT(RAFT_GRANT, rc);
TEST_ASSERT_EQUAL_INT(0, s.voted_for);
@ -5220,6 +5240,279 @@ static void test_proto_G2_partition_minority_no_quorum(void)
#endif /* XCE_TESTING */
/* ---------- (lastLogTerm,lastLogIndex) vote freshness tests (task 4.1) ----------
*
* raft.tla HandleRequestVoteRequest logOk (285), closes G6. The library now
* computes logOk itself from the candidate's advertised (cand_last_log_term,
* cand_last_log_index) against its own (on_get_entry_term(log_len), log_len),
* BEFORE the application gate. */
/* Configure the voter's local log: last index = log_len, last term = last_term. */
static void set_voter_log(raft_state_t *s, uint64_t log_len, uint64_t last_term)
{
s->log_len = log_len;
g_mock.get_entry_term_log_len = log_len;
g_mock.local_last_log_term = last_term;
}
/*
* test_vote_req_freshness_stale_term_denied_higher_index
* Voter log = (term 5, index 3). Candidate advertises a STALE last-log term (4)
* but a HIGHER index (10). logOk false -> deny, even though the app gate grants.
* (Req 3.1, 3.3, 3.4.)
*/
static void test_vote_req_freshness_stale_term_denied_higher_index(void)
{
raft_state_t s;
raft_callbacks_t cbs = make_mock_cbs();
uint64_t reply_term = 0;
cbs.on_trace = mock_trace;
cbs.on_vote_eligible = mock_vote_eligible;
cbs.on_get_entry_term = mock_get_entry_term;
reset_mock();
g_mock.vote_eligible_return_value = 1;
TEST_ASSERT_EQUAL_INT(0, init_vote_node(&s, &cbs, 3, 0, 5));
set_voter_log(&s, 3, 5);
int rc = raft_on_vote_request(&s, 1, 5, 4, 10, &reply_term);
TEST_ASSERT_EQUAL_INT(RAFT_DENY, rc);
TEST_ASSERT_EQUAL_UINT64(5, reply_term);
TEST_ASSERT_EQUAL_INT(0, g_mock.vote_eligible_call_count);
TEST_ASSERT_EQUAL_INT(0, g_mock.persist_count);
TEST_ASSERT_EQUAL_INT(-1, s.voted_for);
}
/*
* test_vote_req_freshness_fresher_term_granted_smaller_index
* Voter log = (term 5, index 10). Candidate (term 6 fresher, index 2 smaller).
* logOk = (6 > 5) -> grant. (Req 3.1, 3.6.)
*/
static void test_vote_req_freshness_fresher_term_granted_smaller_index(void)
{
raft_state_t s;
raft_callbacks_t cbs = make_mock_cbs();
uint64_t reply_term = 0;
cbs.on_trace = mock_trace;
cbs.on_vote_eligible = mock_vote_eligible;
cbs.on_get_entry_term = mock_get_entry_term;
reset_mock();
g_mock.vote_eligible_return_value = 1;
TEST_ASSERT_EQUAL_INT(0, init_vote_node(&s, &cbs, 3, 0, 5));
set_voter_log(&s, 10, 5);
int rc = raft_on_vote_request(&s, 1, 5, 6, 2, &reply_term);
TEST_ASSERT_EQUAL_INT(RAFT_GRANT, rc);
TEST_ASSERT_EQUAL_INT(1, g_mock.persist_count);
TEST_ASSERT_EQUAL_INT(1, s.voted_for);
TEST_ASSERT_EQUAL_UINT64(5, s.voted_for_term);
TEST_ASSERT_EQUAL_INT(1, g_mock.vote_eligible_call_count);
}
/*
* test_vote_req_freshness_tie_term_index_boundary
* Voter log = (term 5, index 4). On a term TIE, grant requires cand index >=
* voter log_len. index 3 (<4) -> deny; index 4 (==4) -> grant. (Req 3.1.)
*/
static void test_vote_req_freshness_tie_term_index_boundary(void)
{
raft_state_t s;
raft_callbacks_t cbs = make_mock_cbs();
uint64_t reply_term = 0;
cbs.on_vote_eligible = mock_vote_eligible;
cbs.on_get_entry_term = mock_get_entry_term;
reset_mock();
g_mock.vote_eligible_return_value = 1;
TEST_ASSERT_EQUAL_INT(0, init_vote_node(&s, &cbs, 3, 0, 5));
set_voter_log(&s, 4, 5);
int rc = raft_on_vote_request(&s, 1, 5, 5, 3, &reply_term); /* idx 3 < 4 */
TEST_ASSERT_EQUAL_INT(RAFT_DENY, rc);
TEST_ASSERT_EQUAL_INT(0, g_mock.persist_count);
reset_mock();
g_mock.vote_eligible_return_value = 1;
TEST_ASSERT_EQUAL_INT(0, init_vote_node(&s, &cbs, 3, 0, 5));
set_voter_log(&s, 4, 5);
rc = raft_on_vote_request(&s, 1, 5, 5, 4, &reply_term); /* idx 4 == 4 */
TEST_ASSERT_EQUAL_INT(RAFT_GRANT, rc);
TEST_ASSERT_EQUAL_INT(1, g_mock.persist_count);
}
/*
* test_vote_req_freshness_empty_voter_log_term_zero
* Empty voter log (log_len 0): local last-log term 0. Candidate (0,0) satisfies
* logOk = (0>0)||(0==0 && 0>=0) = true -> grant. (Req 3.1.)
*/
static void test_vote_req_freshness_empty_voter_log_term_zero(void)
{
raft_state_t s;
raft_callbacks_t cbs = make_mock_cbs();
uint64_t reply_term = 0;
cbs.on_vote_eligible = mock_vote_eligible;
cbs.on_get_entry_term = mock_get_entry_term;
reset_mock();
g_mock.vote_eligible_return_value = 1;
TEST_ASSERT_EQUAL_INT(0, init_vote_node(&s, &cbs, 3, 0, 5));
int rc = raft_on_vote_request(&s, 1, 5, 0, 0, &reply_term);
TEST_ASSERT_EQUAL_INT(RAFT_GRANT, rc);
TEST_ASSERT_EQUAL_INT(1, g_mock.persist_count);
}
/*
* test_vote_req_freshness_logok_precedes_app_gate
* !logOk denies even when on_vote_eligible WOULD grant, and the app gate is not
* consulted (logOk strictly precedes it). (Req 3.4.)
*/
static void test_vote_req_freshness_logok_precedes_app_gate(void)
{
raft_state_t s;
raft_callbacks_t cbs = make_mock_cbs();
uint64_t reply_term = 0;
cbs.on_vote_eligible = mock_vote_eligible;
cbs.on_get_entry_term = mock_get_entry_term;
reset_mock();
g_mock.vote_eligible_return_value = 1;
TEST_ASSERT_EQUAL_INT(0, init_vote_node(&s, &cbs, 3, 0, 5));
set_voter_log(&s, 8, 7);
int rc = raft_on_vote_request(&s, 1, 5, 7, 1, &reply_term);
TEST_ASSERT_EQUAL_INT(RAFT_DENY, rc);
TEST_ASSERT_EQUAL_INT(0, g_mock.vote_eligible_call_count);
TEST_ASSERT_EQUAL_INT(0, g_mock.persist_count);
}
/* ---------- (lastLogTerm,lastLogIndex) PreVote freshness tests (task 5.1) ----------
*
* The PreVote path applies the IDENTICAL logOk, and mutates NOTHING. (Req 4.1-4.5.) */
/*
* test_pre_vote_req_freshness_stale_tuple_denied
* Voter (term 5, index 3). PreVote tuple (term 4 stale, index 10) -> deny, no
* mutation, no persist. (Req 4.1, 4.3.)
*/
static void test_pre_vote_req_freshness_stale_tuple_denied(void)
{
raft_state_t s;
raft_callbacks_t cbs = make_mock_cbs();
cbs.on_vote_eligible = mock_vote_eligible;
cbs.on_get_entry_term = mock_get_entry_term;
reset_mock();
g_mock.vote_eligible_return_value = 1;
TEST_ASSERT_EQUAL_INT(0, init_vote_node(&s, &cbs, 3, 0, 5));
set_voter_log(&s, 3, 5);
int rc = raft_on_pre_vote_request(&s, 1, 5, 4, 10);
TEST_ASSERT_EQUAL_INT(RAFT_DENY, rc);
TEST_ASSERT_EQUAL_UINT64(5, s.term);
TEST_ASSERT_EQUAL_INT(-1, s.voted_for);
TEST_ASSERT_EQUAL_UINT64(0, s.voted_for_term);
TEST_ASSERT_EQUAL_INT(RAFT_FOLLOWER, s.role);
TEST_ASSERT_EQUAL_UINT64(0, s.commit_index);
TEST_ASSERT_EQUAL_INT(0, g_mock.persist_count);
}
/*
* test_pre_vote_req_freshness_fresher_tuple_granted
* Voter (term 5, index 10). PreVote (term 6 fresher, index 2) -> grant, still
* side-effect-free. (Req 4.1.)
*/
static void test_pre_vote_req_freshness_fresher_tuple_granted(void)
{
raft_state_t s;
raft_callbacks_t cbs = make_mock_cbs();
cbs.on_vote_eligible = mock_vote_eligible;
cbs.on_get_entry_term = mock_get_entry_term;
reset_mock();
g_mock.vote_eligible_return_value = 1;
TEST_ASSERT_EQUAL_INT(0, init_vote_node(&s, &cbs, 3, 0, 5));
set_voter_log(&s, 10, 5);
int rc = raft_on_pre_vote_request(&s, 1, 5, 6, 2);
TEST_ASSERT_EQUAL_INT(RAFT_GRANT, rc);
TEST_ASSERT_EQUAL_INT(0, g_mock.persist_count);
TEST_ASSERT_EQUAL_INT(-1, s.voted_for);
}
/*
* test_pre_vote_req_freshness_no_mutation_grant_and_deny
* Across BOTH a grant and a deny, no change to term/voted_for/voted_for_term/
* role/commit_index and no on_persist. (Req 4.4.)
*/
static void test_pre_vote_req_freshness_no_mutation_grant_and_deny(void)
{
raft_state_t s;
raft_callbacks_t cbs = make_mock_cbs();
cbs.on_vote_eligible = mock_vote_eligible;
cbs.on_get_entry_term = mock_get_entry_term;
reset_mock();
g_mock.vote_eligible_return_value = 1;
TEST_ASSERT_EQUAL_INT(0, init_vote_node(&s, &cbs, 3, 0, 5));
s.commit_index = 2;
set_voter_log(&s, 4, 5);
TEST_ASSERT_EQUAL_INT(RAFT_GRANT, raft_on_pre_vote_request(&s, 1, 5, 5, 9));
TEST_ASSERT_EQUAL_UINT64(5, s.term);
TEST_ASSERT_EQUAL_INT(-1, s.voted_for);
TEST_ASSERT_EQUAL_UINT64(0, s.voted_for_term);
TEST_ASSERT_EQUAL_INT(RAFT_FOLLOWER, s.role);
TEST_ASSERT_EQUAL_UINT64(2, s.commit_index);
TEST_ASSERT_EQUAL_INT(0, g_mock.persist_count);
reset_mock();
g_mock.vote_eligible_return_value = 1;
TEST_ASSERT_EQUAL_INT(0, init_vote_node(&s, &cbs, 3, 0, 5));
s.commit_index = 2;
set_voter_log(&s, 4, 5);
TEST_ASSERT_EQUAL_INT(RAFT_DENY, raft_on_pre_vote_request(&s, 1, 5, 5, 1));
TEST_ASSERT_EQUAL_UINT64(5, s.term);
TEST_ASSERT_EQUAL_INT(-1, s.voted_for);
TEST_ASSERT_EQUAL_UINT64(0, s.voted_for_term);
TEST_ASSERT_EQUAL_INT(RAFT_FOLLOWER, s.role);
TEST_ASSERT_EQUAL_UINT64(2, s.commit_index);
TEST_ASSERT_EQUAL_INT(0, g_mock.persist_count);
}
/*
* test_pre_vote_req_freshness_stale_term_still_denied
* The pre-existing stale-term deny stays in force even with a fresh tuple:
* cand_term < s.term denies. (Req 4.5.)
*/
static void test_pre_vote_req_freshness_stale_term_still_denied(void)
{
raft_state_t s;
raft_callbacks_t cbs = make_mock_cbs();
cbs.on_vote_eligible = mock_vote_eligible;
cbs.on_get_entry_term = mock_get_entry_term;
reset_mock();
g_mock.vote_eligible_return_value = 1;
TEST_ASSERT_EQUAL_INT(0, init_vote_node(&s, &cbs, 3, 0, 5));
set_voter_log(&s, 3, 5);
int rc = raft_on_pre_vote_request(&s, 1, 4, 99, 99);
TEST_ASSERT_EQUAL_INT(RAFT_DENY, rc);
TEST_ASSERT_EQUAL_INT(0, g_mock.persist_count);
}
/* ---------- raft_on_pre_vote_request tests (task 2.1) ---------- */
/*
@ -5249,7 +5542,7 @@ static void test_pre_vote_req_grant_higher_term_eligible(void)
TEST_ASSERT_EQUAL_INT(0, init_vote_node(&s, &cbs, 3, 0, 5));
int rc = raft_on_pre_vote_request(&s, 1, 6);
int rc = raft_on_pre_vote_request(&s, 1, 6, 0, 0);
TEST_ASSERT_EQUAL_INT(RAFT_GRANT, rc);
@ -5283,7 +5576,7 @@ static void test_pre_vote_req_grant_equal_term_eligible(void)
TEST_ASSERT_EQUAL_INT(0, init_vote_node(&s, &cbs, 3, 0, 5));
int rc = raft_on_pre_vote_request(&s, 1, 5); /* equal term */
int rc = raft_on_pre_vote_request(&s, 1, 5, 0, 0); /* equal term */
TEST_ASSERT_EQUAL_INT(RAFT_GRANT, rc);
@ -5312,7 +5605,7 @@ static void test_pre_vote_req_deny_stale_term(void)
TEST_ASSERT_EQUAL_INT(0, init_vote_node(&s, &cbs, 3, 0, 5));
int rc = raft_on_pre_vote_request(&s, 1, 4); /* stale */
int rc = raft_on_pre_vote_request(&s, 1, 4, 0, 0); /* stale */
TEST_ASSERT_EQUAL_INT(RAFT_DENY, rc);
@ -5346,7 +5639,7 @@ static void test_pre_vote_req_deny_eligibility_rejects(void)
TEST_ASSERT_EQUAL_INT(0, init_vote_node(&s, &cbs, 3, 0, 5));
int rc = raft_on_pre_vote_request(&s, 1, 6);
int rc = raft_on_pre_vote_request(&s, 1, 6, 0, 0);
TEST_ASSERT_EQUAL_INT(RAFT_DENY, rc);
TEST_ASSERT_EQUAL_INT(1, g_mock.vote_eligible_call_count);
@ -5375,7 +5668,7 @@ static void test_pre_vote_req_null_eligible_grants(void)
TEST_ASSERT_EQUAL_INT(0, init_vote_node(&s, &cbs, 3, 0, 5));
int rc = raft_on_pre_vote_request(&s, 1, 6);
int rc = raft_on_pre_vote_request(&s, 1, 6, 0, 0);
TEST_ASSERT_EQUAL_INT(RAFT_GRANT, rc);
@ -5409,7 +5702,7 @@ static void test_pre_vote_req_grant_preserves_existing_vote(void)
s.voted_for_term = 5;
/* A different candidate (peer 2) probes a PreVote for term 6. */
int rc = raft_on_pre_vote_request(&s, 2, 6);
int rc = raft_on_pre_vote_request(&s, 2, 6, 0, 0);
TEST_ASSERT_EQUAL_INT(RAFT_GRANT, rc);
@ -5940,14 +6233,14 @@ static void test_invariant_prevote_sequence_pure_no_persist(void)
TEST_ASSERT_EQUAL_INT(0, raft_begin_pre_vote(&s));
/* (b) answer an incoming PreVote request that PASSES eligibility — grant */
TEST_ASSERT_EQUAL_INT(1, raft_on_pre_vote_request(&s, 2, 6 /* higher */));
TEST_ASSERT_EQUAL_INT(1, raft_on_pre_vote_request(&s, 2, 6 /* higher */, 0, 0));
/* (c) answer a STALE-term incoming request — deny */
TEST_ASSERT_EQUAL_INT(0, raft_on_pre_vote_request(&s, 2, 3 /* stale */));
TEST_ASSERT_EQUAL_INT(0, raft_on_pre_vote_request(&s, 2, 3 /* stale */, 0, 0));
/* (d) answer an incoming request the eligibility gate REJECTS — deny */
g_mock.vote_eligible_return_value = 0;
TEST_ASSERT_EQUAL_INT(0, raft_on_pre_vote_request(&s, 2, 6));
TEST_ASSERT_EQUAL_INT(0, raft_on_pre_vote_request(&s, 2, 6, 0, 0));
g_mock.vote_eligible_return_value = 1;
/* (e) tally replies: a grant (drives to WON quorum on 3-node), then a
@ -6103,7 +6396,7 @@ static void test_invariant_real_vote_higher_term_adopt_unchanged(void)
s.voted_for_term = 2;
reset_mock();
int rc = raft_on_vote_request(&s, 1, 5, &reply_term);
int rc = raft_on_vote_request(&s, 1, 5, 0, 0, &reply_term);
/* Higher-term adopt + leader step-down, exactly as bug-339/340 require. */
TEST_ASSERT_EQUAL_UINT64(5, s.term);
@ -6229,6 +6522,19 @@ void test_raft(void)
RUN_TEST(test_vote_req_persist_fails);
RUN_TEST(test_vote_req_reply_term_always_set);
/* (lastLogTerm,lastLogIndex) binding-vote freshness (task 4.1, Req 3.1-3.6) */
RUN_TEST(test_vote_req_freshness_stale_term_denied_higher_index);
RUN_TEST(test_vote_req_freshness_fresher_term_granted_smaller_index);
RUN_TEST(test_vote_req_freshness_tie_term_index_boundary);
RUN_TEST(test_vote_req_freshness_empty_voter_log_term_zero);
RUN_TEST(test_vote_req_freshness_logok_precedes_app_gate);
/* (lastLogTerm,lastLogIndex) PreVote freshness (task 5.1, Req 4.1-4.5) */
RUN_TEST(test_pre_vote_req_freshness_stale_tuple_denied);
RUN_TEST(test_pre_vote_req_freshness_fresher_tuple_granted);
RUN_TEST(test_pre_vote_req_freshness_no_mutation_grant_and_deny);
RUN_TEST(test_pre_vote_req_freshness_stale_term_still_denied);
/* raft_on_election_timeout, raft_on_leader_lost, raft_check_and_start_election (task 3.5) */
RUN_TEST(test_election_timeout_undecided);
RUN_TEST(test_election_timeout_already_decided);