test(ha-commit-engine): RED tests for per-LSN majority evaluation (task 1.1)

commit.h umbrella-verbatim interface + inert stub commit.c (test target
only); 7 Unity tests covering per-LSN vs acked_lsn>0 counting, majority
median, monotonicity, append clamp, lagging standby, RETAIN immediacy.
All 7 fail RED as intended. Documented 3 pre-existing masked suite
failures (bug-411) and the per-group exit-code masking root cause.
This commit is contained in:
Rongsong Shen 2026-06-11 14:17:09 +00:00
parent 06135954ff
commit d5a0f9c64d
10 changed files with 757 additions and 283 deletions

View file

@ -4,7 +4,7 @@
> Verify exact line numbers with grep before editing (research.md table has the verified anchors; files drift).
- [ ] 1. Commit engine core: per-LSN evaluation and commit-index integrity
- [ ] 1.1 Write RED tests for per-LSN majority evaluation and commit-index integrity
- [x] 1.1 Write RED tests for per-LSN majority evaluation and commit-index integrity
- Create `testsuite/test_commit_engine.c` against a stub `persist_t` + `replicator_t` (XCE_TESTING pattern, AAA structure) and register it in `controller_unit_test_src` (meson.build); create `src/controller/include/commit.h` with the umbrella-verbatim interface (types + prototypes only) so tests compile
- Tests: with n_peers=3, a standby ACK for LSN M does not commit entry N>M (kills `acked_lsn > 0` counting); ACK watermark >= N from one standby plus self commits N; cumulative/out-of-order watermarks advance `committed_lsn` to the majority median, never past the highest appended LSN, never backwards; one far-lagging standby does not block majority commit; RETAIN mode returns 0 immediately without blocking
- Observable completion: build succeeds with the new header and test file; all new test cases FAIL (red) against a stub `commit.c` or unlinked implementation
@ -78,3 +78,10 @@
- Audit: grep confirms no production (engine-wired) walAck path reaches the `acked_lsn > 0` counting or the `n_active == 0 → success` branch; both survive only under the deprecated `ce == NULL` test fallback
- Observable completion: full testsuite green under ASAN; audit results recorded in the task completion notes
- _Requirements: 1.1, 1.2, 1.3, 2.1, 2.2, 2.3, 3.1, 3.2, 3.3, 4.1, 4.2, 4.3, 4.4, 5.1, 6.1, 6.2, 6.3, 7.1, 7.2, 7.3_
## Implementation Notes
- (1.1) THREE PRE-EXISTING suite failures, masked until now (bug-411): test_committed_lsn_does_not_advance_on_timeout (-110), test_repl_ack_after_promote_gate_allows_jobsub (-10013), test_rdhup_primary_dispatch_gate_opens (-10013). Masking cause: each test group runs its own UNITY_BEGIN/END so the process exit code reflects only the LAST group. They live in repl/persist sync-append and quorum-gate paths — tasks 1.2/2.x/5.x touch those areas and 6.1 requires full green, so they must be fixed (or proven fixed by the engine) along the way.
- (1.1) Full-suite output under default ASAN dies at exit via LeakSanitizer (pre-existing leaks in worker_join/rdhup_integration/start_election_async/liveness groups) without flushing stdout — use ASAN_OPTIONS=detect_leaks=0 for full-run logs or run groups standalone with -t.
- (1.1) repl_add_standby returns the SLOT INDEX, not 0 (bug-410 note for fixture writers).
- (1.1) commit.c is registered ONLY in controller_unit_test_src for now; production target linkage lands with task 5.3.

View file

@ -1,7 +1,7 @@
# anatomy.md
> Auto-maintained by OpenWolf. Last scanned: 2026-06-11T13:00:00.755Z
> Files: 506 tracked | Anatomy hits: 0 | Misses: 0
> Auto-maintained by OpenWolf. Last scanned: 2026-06-11T14:16:52.677Z
> Files: 511 tracked | Anatomy hits: 0 | Misses: 0
## ./
@ -262,7 +262,7 @@
- `requirements.md` — Requirements Document (~2715 tok)
- `research.md` — Research Log: ha-commit-engine (~1345 tok)
- `spec.json` (~140 tok)
- `tasks.md` — Implementation Plan: ha-commit-engine (~2682 tok)
- `tasks.md` — Implementation Plan: ha-commit-engine (~2947 tok)
## .kiro/specs/ha-config-bootstrap/
@ -703,3 +703,17 @@
- `6664f5dc160a6acfc03af4d9881c53f8f6c650ac384b1ad9b3db47d896800af9.json` (~656 tok)
- `669931b38fdf0060e974d68dd28abc43e49a1907405848ce9d8b3b3901518313.json` (~376 tok)
- `66cc2d1ad85554725cecb1f215d375511474a58829464647a0d8c109818d9804.json` (~57 tok)
## src/controller/
- `commit.c` — STUB commit engine (ha-commit-engine task 1.1 RED): init/fini real, commit_submit -ENOSYS, commit_on_ack no-op; real impl in task 1.2 (~474 tok)
- `meson.build` (~1094 tok)
## src/controller/include/
- `commit.h` — commit engine service interface (umbrella-verbatim): commit_engine_t, WAIT/RETAIN modes, init/fini/submit/on_ack/set_writable_lsn/on_membership_change (~771 tok)
## src/controller/testsuite/
- `controller_test.c` — include <argp.h> (~1323 tok)
- `test_commit_engine.c` — RED tests for per-LSN majority + commit-index integrity (Reqs 1.2, 2.1-2.3, 5.1): 7 cases against stub persist_t+replicator_t (~2459 tok)

File diff suppressed because it is too large Load diff

View file

@ -34,6 +34,10 @@
- Heartbeat freshness source of truth (2026-06-09): `ha_heartbeat_is_fresh` derives from `ha->last_activity` (whole CLOCK_MONOTONIC seconds, written by ha_on_heartbeat_received, lock-free time_t read). The old `last_heartbeat` timespec field was REMOVED from ha_state_t — do not reintroduce it. The standby vote-deny gate is `role==CTRL_STANDBY && primary_ctrl_fd >= 0 && ha_heartbeat_is_fresh(ha)`; RDHUP resets primary_ctrl_fd to -1 so a standby whose primary died can grant votes immediately even if last_activity is still fresh.
- raft_init restore contract (2026-06-09): persistence writes (term, voted_for) as a pair, so `raft_init` must set `voted_for_term = initial_term` when `initial_voted_for != -1`. Leaving it 0 lets a restarted node double-vote in the same term (Election Safety violation). Covered by test_proto_H6.
- repl_add_standby returns the SLOT INDEX (>=0) on success, not 0 (2026-06-11, ha-commit-engine task 1.1): asserting `== 0` breaks on the second standby. Assert `>= 0` or the expected index.
- Unity harness quirks in controller_unit_test (2026-06-11): (a) failure lines misattribute the FILE — Unity.TestFile comes from whichever group's UNITY_BEGIN ran, so a failure printed as test_repl_unit.c:294 may live in test_p2_integration.c; locate the actual test by grepping for the test NAME. (b) Each group's UNITY_BEGIN resets counters, so the process exit code only reflects the LAST group — failures in earlier groups are masked (exit=0); meson reports green despite real failures. Grep output for FAIL with `grep FAIL` on the raw log (ANSI codes sit between ':' and 'FAIL', so `grep ":FAIL"` misses them).
- 3 pre-existing controller suite failures on persistent_ha baseline (2026-06-11, verified by stash/rebuild without ha-commit-engine changes, see bug-411): test_committed_lsn_does_not_advance_on_timeout (rc -110 vs expected 0 — persist_wal_sync_append now returns -ETIMEDOUT, test expects old rc==0+sync_rc contract), test_repl_ack_after_promote_gate_allows_jobsub and test_rdhup_primary_dispatch_gate_opens (both -10013: ha_has_write_quorum false, standby_endpoints empty with n_peers=2).
## Do-Not-Repeat
<!-- Mistakes made and corrected. Each entry prevents the same mistake recurring. -->

View file

@ -860,3 +860,13 @@
| 05:57 | Edited .kiro/steering/roadmap.md | 6→8 lines | ~365 |
| 11:40 | kiro-spec-batch complete: 4 sub-specs generated (3 waves), cross-spec review passed after 6 doc fixes (1 by fix-agent before spend limit, rest verified/applied); roadmap marked [x] | 4 spec dirs, roadmap.md, umbrella design.md | 51 sub-tasks total, ready for /kiro-impl ha-commit-engine | ~700k |
| 05:57 | Session end: 99 writes across 12 files (brief.md, project_ha_consistency_invariants.md, product.md, spec.json, requirements.md) | 69 reads | ~240145 tok |
| 06:26 | Created src/controller/include/commit.h | — | ~771 |
| 06:27 | Created src/controller/commit.c | — | ~474 |
| 06:28 | Created src/controller/testsuite/test_commit_engine.c | — | ~2440 |
| 06:28 | Edited src/controller/meson.build | 2→4 lines | ~30 |
| 06:28 | Edited src/controller/testsuite/controller_test.c | 1→2 lines | ~20 |
| 06:28 | Edited src/controller/testsuite/controller_test.c | 3→4 lines | ~22 |
| 06:32 | Edited src/controller/testsuite/test_commit_engine.c | modified for() | ~75 |
| 13:30 | Task 1.1 ha-commit-engine RED: commit.h + stub commit.c + test_commit_engine.c (7 failing tests) | src/controller/{include/commit.h,commit.c,testsuite/test_commit_engine.c,meson.build,testsuite/controller_test.c} | 7/7 RED for right reasons; 3 suite failures proven pre-existing via baseline stash run | ~60k |
| 07:16 | Edited .kiro/specs/ha-commit-engine/tasks.md | inline fix | ~23 |
| 07:16 | Edited .kiro/specs/ha-commit-engine/tasks.md | modified now() | ~344 |

68
src/controller/commit.c Normal file
View file

@ -0,0 +1,68 @@
/*
* commit.c — STUB for ha-commit-engine task 1.1 (RED phase).
*
* Behavioral functions are deliberately inert (-ENOSYS / no-op) so the
* task 1.1 tests in testsuite/test_commit_engine.c compile, link, and FAIL
* on their assertions. init/fini are real enough that tests exercise the
* engine's behavioral contract rather than failing at fixture setup.
*
* The real implementation lands in task 1.2 (per-LSN majority evaluation,
* monotonic/contiguous committed_lsn advancement).
*/
#include <errno.h>
#include "commit.h"
int commit_engine_init(commit_engine_t* ce, persist_t* p, int n_peers, int n_vcpus)
{
if (ce == NULL || p == NULL || n_peers < 0 || n_vcpus < 1) {
return -EINVAL;
}
pthread_mutex_init(&ce->lock, NULL);
pthread_cond_init(&ce->cond, NULL);
ce->p = p;
ce->n_peers = n_peers;
ce->n_vcpus = n_vcpus;
ce->inflight_waits = 0;
ce->writable_lsn = 0;
return 0;
}
void commit_engine_fini(commit_engine_t* ce)
{
if (ce == NULL) {
return;
}
pthread_cond_destroy(&ce->cond);
pthread_mutex_destroy(&ce->lock);
ce->p = NULL;
}
int commit_submit(commit_engine_t* ce, uint64_t lsn, commit_mode_t mode,
uint32_t timeout_ms)
{
(void)ce;
(void)lsn;
(void)mode;
(void)timeout_ms;
return -ENOSYS; /* stub: real WAIT/RETAIN semantics in task 1.2/2.2 */
}
void commit_on_ack(commit_engine_t* ce, int standby_idx, uint64_t acked_lsn)
{
(void)ce;
(void)standby_idx;
(void)acked_lsn;
/* stub: never advances committed_lsn */
}
void commit_set_writable_lsn(commit_engine_t* ce, uint64_t noop_lsn)
{
(void)ce;
(void)noop_lsn;
}
void commit_on_membership_change(commit_engine_t* ce)
{
(void)ce;
}

View file

@ -0,0 +1,61 @@
#pragma once
/*
* commit.h — Commit engine: single authority for per-entry commit evaluation,
* committed_lsn advancement, and waiter completion.
*
* Service interface is umbrella-verbatim from
* .kiro/specs/ha-consistency-invariants/design.md (sliced by ha-commit-engine).
*
* All functions thread-safe; commit_on_ack callable from iomgr (non-blocking).
*/
#include <pthread.h>
#include <stdint.h>
#include "persist.h" /* persist_t (owns committed_lsn atomic + replicator_t registry) */
/* Headroom kept free of blocking commit waits; cap = max(1, n_vcpus - reserved) */
#define COMMIT_WAIT_RESERVED 2
typedef enum { COMMIT_MODE_WAIT, COMMIT_MODE_RETAIN } commit_mode_t;
/*
* Commit engine state — in-memory only (commit state volatile, Raft-conformant).
* Per-standby view delegated to standby_conn_t.acked_lsn / in_catch_up via
* p->repl (no duplication).
*/
typedef struct commit_engine {
pthread_mutex_t lock;
pthread_cond_t cond; /* broadcast on committed_lsn advance / membership change */
persist_t* p; /* back-reference: committed_lsn store + repl registry */
int n_peers;
int n_vcpus;
int inflight_waits; /* <= max(1, n_vcpus - COMMIT_WAIT_RESERVED) */
uint64_t writable_lsn; /* own-term gate; 0 = no gate */
uint64_t scratch[MAX_STANDBYS]; /* sorted matchIndex scratch array */
} commit_engine_t;
/* Lifecycle. init validates n_peers >= 0, n_vcpus >= 1, non-NULL persist_t;
* returns -EINVAL otherwise. Call after n_peers and n_vcpus are final. */
int commit_engine_init(commit_engine_t* ce, persist_t* p, int n_peers, int n_vcpus);
void commit_engine_fini(commit_engine_t* ce);
/* Register entry at append time. RETAIN returns 0 immediately.
* WAIT blocks up to timeout_ms: 0 = committed; -ETIMEDOUT = not committed
* (entry remains in log; outcome resolves per umbrella 2.5); -XCE_ERR_NOT_READY =
* admission cap exhausted (only cause). An unmet own-term gate does NOT fail
* fast: the gate is part of the completion predicate, so WAIT blocks within
* timeout_ms until the gate opens, surfacing -ETIMEDOUT if it never does. */
int commit_submit(commit_engine_t* ce, uint64_t lsn, commit_mode_t mode,
uint32_t timeout_ms);
/* iomgr path: record standby ACK, evaluate majority, advance committed_lsn
* contiguously, wake satisfied waiters. Never blocks. */
void commit_on_ack(commit_engine_t* ce, int standby_idx, uint64_t acked_lsn);
/* Promotion: set own-term gate to the no-op entry's LSN.
* (API ships here; no caller in this spec — unset = no gate.) */
void commit_set_writable_lsn(commit_engine_t* ce, uint64_t noop_lsn);
/* Quorum-membership change (standby add/remove/catch-up transition):
* re-evaluate pending entries; standbys in catch-up are excluded. */
void commit_on_membership_change(commit_engine_t* ce);

View file

@ -119,6 +119,8 @@ controller_unit_test_src = [
'testsuite/test_ha_connection_reuse.c',
'testsuite/test_ha_rdhup_integration.c',
'testsuite/test_ha_liveness.c',
'testsuite/test_commit_engine.c',
'commit.c',
'startup_validation.c',
'worker.c',
'persist.c',

View file

@ -50,6 +50,7 @@ extern void test_ha_repl_ack(void);
extern void test_ha_connection_reuse(void);
extern void test_ha_rdhup_integration(void);
extern void test_ha_liveness(void);
extern void test_commit_engine(void);
void setUp(void) { return; }
@ -169,6 +170,7 @@ int main(int argc, char* argv[])
TEST(test_ha_connection_reuse);
TEST(test_ha_rdhup_integration);
TEST(test_ha_liveness);
TEST(test_commit_engine);
return UNITY_END();
}

View file

@ -0,0 +1,270 @@
/*
* test_commit_engine.c — RED-phase tests for the commit engine
* (ha-commit-engine task 1.1: per-LSN majority evaluation and
* commit-index integrity).
*
* The engine is constructed standalone against a stub persist_t +
* replicator_t (XCE_TESTING pattern): the persist_t is a zeroed stack
* struct whose only live parts are committed_lsn, current_lsn, and the
* embedded replicator registry. Standby ACK watermarks are written
* directly into standby_conn_t.acked_lsn before feeding commit_on_ack,
* exactly as repl_on_ack does in production.
*
* Cluster model: n_peers = 3 means primary (self) + 2 standbys.
* Majority for entry N = self + at least n_peers/2 standbys with
* acked_lsn >= N (Raft §5.3, umbrella-verbatim commit rule).
*
* These cases FAIL against the task-1.1 stub commit.c and go green with
* the task-1.2 implementation.
*
* Requirements: 1.2, 2.1, 2.2, 2.3, 5.1
*/
#include <errno.h>
#include <stdatomic.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <time.h>
#include <unity/unity.h>
#include "commit.h"
#include "persist.h"
#include "repl.h"
/* ── Fixture ──────────────────────────────────────────────────────────────
* Static: persist_t embeds large path buffers, and statics survive Unity's
* longjmp on assertion failure (pattern from test_repl_majority.c). */
static persist_t g_p;
static commit_engine_t g_ce;
/* Dummy endpoints — the replicator only compares pointers */
static int g_eps[MAX_STANDBYS];
/*
* Arrange a fresh engine over a stub persist_t + replicator_t.
* appended_lsn = highest locally appended WAL entry; persist invariant is
* committed_lsn <= current_lsn - 1, so current_lsn = appended_lsn + 1.
*/
static void engine_setup(int n_peers, int n_standbys, uint64_t appended_lsn)
{
int i;
char name[16];
memset(&g_p, 0, sizeof(g_p));
TEST_ASSERT_EQUAL_INT(0, repl_init(&g_p.repl));
for (i = 0; i < n_standbys; i++) {
snprintf(name, sizeof(name), "sb%d", i);
/* repl_add_standby returns the slot index (>= 0) on success */
TEST_ASSERT_EQUAL_INT(
i, repl_add_standby(&g_p.repl, &g_eps[i], name, 7000 + (uint32_t)i));
}
g_p.current_lsn = appended_lsn + 1;
atomic_store(&g_p.committed_lsn, 0);
TEST_ASSERT_EQUAL_INT(0, commit_engine_init(&g_ce, &g_p, n_peers, 8));
}
static void engine_teardown(void)
{
commit_engine_fini(&g_ce);
repl_fini(&g_p.repl);
}
/* Register appended entries 1..up_to with the engine (RETAIN = bookkeeping
* only, no blocking). Return codes asserted in the dedicated RETAIN test. */
static void append_entries(uint64_t up_to)
{
uint64_t lsn;
for (lsn = 1; lsn <= up_to; lsn++) {
(void)commit_submit(&g_ce, lsn, COMMIT_MODE_RETAIN, 0);
}
}
/* Standby ACK watermark arrives: update the registry (what repl_on_ack
* does in production) and feed the engine. */
static void standby_ack(int idx, uint64_t acked_lsn)
{
atomic_store(&g_p.repl.standbys[idx].acked_lsn, acked_lsn);
commit_on_ack(&g_ce, idx, acked_lsn);
}
static uint64_t committed(void)
{
return atomic_load(&g_p.committed_lsn);
}
static uint64_t mono_ms(void)
{
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (uint64_t)ts.tv_sec * 1000u + (uint64_t)ts.tv_nsec / 1000000u;
}
/* ── Test 1: per-LSN evaluation — ACK for LSN M never commits N > M ───────
* Kills the `acked_lsn > 0` ("ACKed anything ever") counting. (Req 1.2) */
static void test_ack_for_lower_lsn_does_not_commit_higher_entry(void)
{
/* Arrange: 3-member cluster, entries 1..3 appended */
engine_setup(3, 2, 3);
append_entries(3);
/* Act: standby 0 ACKs only LSN M=1 */
standby_ack(0, 1);
/* Assert: entry N=3 is NOT committed (dishonest counting would have
* counted "ACKed anything" and committed 3); entry 1 IS committed
* (self + standby 0 cover it) */
TEST_ASSERT_TRUE_MESSAGE(committed() < 3,
"ACK for LSN 1 must not commit entry 3");
TEST_ASSERT_EQUAL_UINT64(1, committed());
engine_teardown();
printf("test_ack_for_lower_lsn_does_not_commit_higher_entry: PASS\n");
}
/* ── Test 2: ACK watermark >= N from one standby plus self commits N ──────
* (Req 1.2, 2.1) */
static void test_ack_at_entry_plus_self_commits_entry(void)
{
/* Arrange: 3-member cluster, entries 1..2 appended */
engine_setup(3, 2, 2);
append_entries(2);
/* Act: standby 0's watermark covers entry 2 (self + sb0 = 2/3) */
standby_ack(0, 2);
/* Assert: entry 2 committed */
TEST_ASSERT_EQUAL_UINT64(2, committed());
engine_teardown();
printf("test_ack_at_entry_plus_self_commits_entry: PASS\n");
}
/* ── Test 3: cumulative / out-of-order watermarks → majority median ───────
* committed_lsn always advances to the highest LSN covered by self + a
* majority of standby watermarks, contiguously. (Req 2.1, 2.3) */
static void test_out_of_order_watermarks_advance_to_majority_median(void)
{
/* Arrange: 3-member cluster, entries 1..5 appended */
engine_setup(3, 2, 5);
append_entries(5);
/* Act + Assert, step by step (cumulative watermarks, mixed order) */
/* sb1 ACKs watermark 2 → matchIndex {self=5, sb0=0, sb1=2}: median 2 */
standby_ack(1, 2);
TEST_ASSERT_EQUAL_UINT64(2, committed());
/* sb0 ACKs watermark 4 → {self=5, sb0=4, sb1=2}: median 4 — entry 5
* still lacks majority and must NOT be committed */
standby_ack(0, 4);
TEST_ASSERT_EQUAL_UINT64(4, committed());
/* sb0 ACKs watermark 5 → {self=5, sb0=5, sb1=2}: median 5 */
standby_ack(0, 5);
TEST_ASSERT_EQUAL_UINT64(5, committed());
engine_teardown();
printf("test_out_of_order_watermarks_advance_to_majority_median: PASS\n");
}
/* ── Test 4: committed_lsn never exceeds the highest appended LSN ─────────
* (Req 2.3) */
static void test_committed_lsn_never_exceeds_appended_log(void)
{
/* Arrange: 3-member cluster, only entries 1..3 appended locally */
engine_setup(3, 2, 3);
append_entries(3);
/* Act: both standbys report watermarks beyond our appended log */
standby_ack(0, 10);
standby_ack(1, 10);
/* Assert: clamped to the highest appended LSN */
TEST_ASSERT_EQUAL_UINT64(3, committed());
engine_teardown();
printf("test_committed_lsn_never_exceeds_appended_log: PASS\n");
}
/* ── Test 5: committed_lsn is monotonic — never moves backwards ───────────
* (Req 2.2) */
static void test_committed_lsn_never_regresses(void)
{
/* Arrange: 3-member cluster, entries 1..4 appended; commit up to 4 */
engine_setup(3, 2, 4);
append_entries(4);
standby_ack(0, 4);
TEST_ASSERT_EQUAL_UINT64(4, committed());
/* Act: a regressing watermark arrives (stale/duplicate ACK) — the
* majority median over current registry state would now be 1 */
standby_ack(0, 1);
/* Assert: commit index never decreases */
TEST_ASSERT_EQUAL_UINT64(4, committed());
engine_teardown();
printf("test_committed_lsn_never_regresses: PASS\n");
}
/* ── Test 6: one far-lagging standby does not block majority commit ───────
* (Req 5.1) */
static void test_lagging_standby_does_not_block_majority_commit(void)
{
/* Arrange: 3-member cluster, entries 1..3 appended; standby 1 is far
* behind and never ACKs (watermark stays 0) */
engine_setup(3, 2, 3);
append_entries(3);
/* Act: standby 0 alone covers entry 3 — self + sb0 = majority */
standby_ack(0, 3);
/* Assert: full replication is not a confirmation precondition */
TEST_ASSERT_EQUAL_UINT64(3, committed());
engine_teardown();
printf("test_lagging_standby_does_not_block_majority_commit: PASS\n");
}
/* ── Test 7: RETAIN mode returns 0 immediately without blocking ───────────
* (interface contract; commit happens later via ACKs, Req 2.1) */
static void test_retain_mode_returns_zero_immediately(void)
{
int rc;
uint64_t t0, elapsed_ms;
/* Arrange: 3-member cluster, entry 1 appended, no ACKs outstanding */
engine_setup(3, 2, 1);
/* Act: RETAIN submit with a large timeout that must NOT be honored */
t0 = mono_ms();
rc = commit_submit(&g_ce, 1, COMMIT_MODE_RETAIN, 5000);
elapsed_ms = mono_ms() - t0;
/* Assert: immediate success, no blocking, and no commit without ACKs */
TEST_ASSERT_EQUAL_INT(0, rc);
TEST_ASSERT_TRUE_MESSAGE(elapsed_ms < 1000,
"RETAIN submit must not block on the timeout");
TEST_ASSERT_EQUAL_UINT64(0, committed());
engine_teardown();
printf("test_retain_mode_returns_zero_immediately: PASS\n");
}
/* ── Test group entry point ──────────────────────────────────────────────── */
void test_commit_engine(void)
{
UNITY_BEGIN();
RUN_TEST(test_ack_for_lower_lsn_does_not_commit_higher_entry);
RUN_TEST(test_ack_at_entry_plus_self_commits_entry);
RUN_TEST(test_out_of_order_watermarks_advance_to_majority_median);
RUN_TEST(test_committed_lsn_never_exceeds_appended_log);
RUN_TEST(test_committed_lsn_never_regresses);
RUN_TEST(test_lagging_standby_does_not_block_majority_commit);
RUN_TEST(test_retain_mode_returns_zero_immediately);
UNITY_END();
}