From d91f446e9e42bd450aecc5bca8885fa118e2e51c Mon Sep 17 00:00:00 2001 From: Rongsong Shen Date: Thu, 11 Jun 2026 14:56:31 +0000 Subject: [PATCH] test(ha-commit-engine): RED tests for WAIT mode, admission cap, own-term gate (task 2.1) 9 new cases (8-16): block-until-majority, deadline timeout, standalone fast path, zero-ACK regression, n_vcpus-relative cap with floor, ACK progress at saturation, gate as blocking predicate (NOT_READY is admission-cap-only per corrected contract). Main-thread-only assertions after joins; 16 tests / 9 RED failures; baseline unchanged. --- .kiro/specs/ha-commit-engine/tasks.md | 3 +- .wolf/anatomy.md | 6 +- .wolf/memory.md | 7 + src/controller/testsuite/test_commit_engine.c | 396 +++++++++++++++++- 4 files changed, 405 insertions(+), 7 deletions(-) diff --git a/.kiro/specs/ha-commit-engine/tasks.md b/.kiro/specs/ha-commit-engine/tasks.md index ebd444d..2adb849 100644 --- a/.kiro/specs/ha-commit-engine/tasks.md +++ b/.kiro/specs/ha-commit-engine/tasks.md @@ -18,7 +18,7 @@ - _Requirements: 1.2, 2.1, 2.2, 2.3, 5.1_ - [ ] 2. WAIT mode, self-quorum, zero-peer rule, admission cap, own-term gate storage -- [ ] 2.1 Write RED tests for blocking-wait semantics and admission control +- [x] 2.1 Write RED tests for blocking-wait semantics and admission control - Tests in `test_commit_engine.c`: WAIT blocks until majority ACK then returns 0; WAIT returns `-ETIMEDOUT` at deadline with `committed_lsn` unmoved past the entry; n_peers=1 → WAIT returns 0 immediately and `committed_lsn` advances to the entry; n_peers=3 with zero standby ACKs → `-ETIMEDOUT`, never success (short-circuit regression); with n_vcpus=8 and reserved=2, a 7th concurrent WAIT gets `-XCE_ERR_NOT_READY` while the 6 admitted waits proceed; cap floor `max(1, n_vcpus - reserved)` holds for tiny n_vcpus; while the cap is saturated, `commit_on_ack` still advances `committed_lsn` and completes admitted waiters; `writable_lsn` unset (0) → no gating; `commit_set_writable_lsn(G)` with G > entry → waiter completes only once `committed_lsn >= G` - Observable completion: new cases FAIL (red) against the task-1 engine - _Requirements: 1.1, 4.3, 4.4, 6.3, 7.1, 7.2, 7.3_ @@ -85,3 +85,4 @@ - (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. +- (2.1) WAIT/cap/gate tests (8-16) read `g_ce.inflight_waits` under `g_ce.lock` — task 2.2 must keep that field accurate while waiters block. Timing floors have ~20ms slack vs sleeps (reviewer advisory: watch for flake on loaded boxes). Unity assertions are main-thread-only after joins — keep that discipline in future threaded tests. diff --git a/.wolf/anatomy.md b/.wolf/anatomy.md index 1a1d32a..bf8c2de 100644 --- a/.wolf/anatomy.md +++ b/.wolf/anatomy.md @@ -1,6 +1,6 @@ # anatomy.md -> Auto-maintained by OpenWolf. Last scanned: 2026-06-11T14:36:56.598Z +> Auto-maintained by OpenWolf. Last scanned: 2026-06-11T14:56:17.297Z > 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 (~2947 tok) +- `tasks.md` — Implementation Plan: ha-commit-engine (~3031 tok) ## .kiro/specs/ha-config-bootstrap/ @@ -716,4 +716,4 @@ ## src/controller/testsuite/ - `controller_test.c` — include (~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) +- `test_commit_engine.c` — Commit engine tests: tasks 1.x majority/commit-index (green) + task 2.1 WAIT blocking/admission-cap/own-term-gate RED cases (~13200 tok) diff --git a/.wolf/memory.md b/.wolf/memory.md index c425dd2..7fe6119 100644 --- a/.wolf/memory.md +++ b/.wolf/memory.md @@ -873,3 +873,10 @@ | 07:22 | Created src/controller/commit.c | — | ~2108 | | 2026-06-11 | ha-commit-engine task 1.2: implemented commit engine core (majority median, monotonic clamped committed_lsn, RETAIN) — 7/7 tests green | src/controller/commit.c | success | ~9k | | 07:36 | Edited .kiro/specs/ha-commit-engine/tasks.md | inline fix | ~14 | +| 07:41 | Edited src/controller/testsuite/test_commit_engine.c | expanded (+17 lines) | ~339 | +| 07:41 | Edited src/controller/testsuite/test_commit_engine.c | modified engine_setup_vcpus() | ~303 | +| 07:43 | Edited src/controller/testsuite/test_commit_engine.c | modified struct() | ~3781 | +| 07:43 | Edited src/controller/testsuite/test_commit_engine.c | 4→3 lines | ~17 | +| 14:47 | task 2.1 RED tests: 9 WAIT/admission/gate cases added (16 total, 7 pass / 9 RED -ENOSYS) | src/controller/testsuite/test_commit_engine.c | RED verified, ASAN clean | ~9k | +| 07:56 | Edited .kiro/specs/ha-commit-engine/tasks.md | inline fix | ~20 | +| 07:56 | Edited .kiro/specs/ha-commit-engine/tasks.md | 1→2 lines | ~122 | diff --git a/src/controller/testsuite/test_commit_engine.c b/src/controller/testsuite/test_commit_engine.c index a8ee628..b4027f0 100644 --- a/src/controller/testsuite/test_commit_engine.c +++ b/src/controller/testsuite/test_commit_engine.c @@ -17,9 +17,23 @@ * 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 + * Task 2.1 extends the file with COMMIT_MODE_WAIT blocking-wait semantics, + * admission control, and own-term-gate cases. They FAIL (RED) against the + * task-1 engine (WAIT is -ENOSYS) and go green with task 2.2. Contract: + * 0 — entry committed (majority ACK, gate satisfied) + * -ETIMEDOUT — predicate unmet at deadline (incl. a gate that never + * opens: the own-term gate is part of the WAIT + * completion predicate, NOT an admission fast-reject) + * -XCE_ERR_NOT_READY — admission cap exhausted (the ONLY cause); + * cap = max(1, n_vcpus - COMMIT_WAIT_RESERVED) + * Blocking waits run on pthreads; ACKs are fed from the main thread via the + * registry + commit_on_ack. All threads are joined BEFORE any assertion so + * Unity's longjmp never strands a live thread (ASAN/LSan clean). + * + * Requirements: 1.1, 1.2, 2.1, 2.2, 2.3, 4.3, 4.4, 5.1, 6.3, 7.1, 7.2, 7.3 */ #include +#include #include #include #include @@ -27,6 +41,8 @@ #include #include +#include "xce/errnox.h" + #include "commit.h" #include "persist.h" #include "repl.h" @@ -44,8 +60,10 @@ 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. + * n_vcpus drives the WAIT admission cap: max(1, n_vcpus - COMMIT_WAIT_RESERVED). */ -static void engine_setup(int n_peers, int n_standbys, uint64_t appended_lsn) +static void engine_setup_vcpus(int n_peers, int n_standbys, uint64_t appended_lsn, + int n_vcpus) { int i; char name[16]; @@ -61,7 +79,12 @@ static void engine_setup(int n_peers, int n_standbys, uint64_t appended_lsn) 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)); + TEST_ASSERT_EQUAL_INT(0, commit_engine_init(&g_ce, &g_p, n_peers, n_vcpus)); +} + +static void engine_setup(int n_peers, int n_standbys, uint64_t appended_lsn) +{ + engine_setup_vcpus(n_peers, n_standbys, appended_lsn, 8); } static void engine_teardown(void) @@ -254,6 +277,363 @@ static void test_retain_mode_returns_zero_immediately(void) printf("test_retain_mode_returns_zero_immediately: PASS\n"); } +/* ════════════════════════════════════════════════════════════════════════ + * Task 2.1 — COMMIT_MODE_WAIT blocking semantics, admission control, + * own-term gate. RED against the task-1 engine (WAIT = -ENOSYS). + * ════════════════════════════════════════════════════════════════════════ */ + +/* Per-waiter pthread context. Static array: survives Unity's longjmp. */ +typedef struct { + uint64_t lsn; + uint32_t timeout_ms; + int rc; + uint64_t elapsed_ms; +} wait_arg_t; + +#define MAX_WAITERS 7 + +static wait_arg_t g_wargs[MAX_WAITERS]; +static pthread_t g_wtids[MAX_WAITERS]; + +/* Count of wait threads that have RETURNED from commit_submit — lets polls + * exit fast in the RED phase, where WAIT returns -ENOSYS immediately. */ +static atomic_int g_waiters_done; + +static void* wait_thread_fn(void* p) +{ + wait_arg_t* a = (wait_arg_t*)p; + uint64_t t0 = mono_ms(); + + a->rc = commit_submit(&g_ce, a->lsn, COMMIT_MODE_WAIT, a->timeout_ms); + a->elapsed_ms = mono_ms() - t0; + atomic_fetch_add(&g_waiters_done, 1); + return NULL; +} + +static void spawn_waiters(int n, uint64_t lsn, uint32_t timeout_ms) +{ + int i; + + atomic_store(&g_waiters_done, 0); + for (i = 0; i < n; i++) { + g_wargs[i].lsn = lsn; + g_wargs[i].timeout_ms = timeout_ms; + g_wargs[i].rc = -9999; + g_wargs[i].elapsed_ms = 0; + TEST_ASSERT_EQUAL_INT( + 0, pthread_create(&g_wtids[i], NULL, wait_thread_fn, &g_wargs[i])); + } +} + +/* Join ALL spawned threads before any assertion: a failing TEST_ASSERT + * longjmps out of the test, and a still-running waiter would then trip + * ASAN/LSan and corrupt the static fixture for the next case. */ +static void join_waiters(int n) +{ + int i; + + for (i = 0; i < n; i++) { + pthread_join(g_wtids[i], NULL); + } +} + +static void sleep_ms(uint32_t ms) +{ + struct timespec ts; + + ts.tv_sec = ms / 1000u; + ts.tv_nsec = (long)(ms % 1000u) * 1000000L; + nanosleep(&ts, NULL); +} + +static int inflight_waits(void) +{ + int v; + + pthread_mutex_lock(&g_ce.lock); + v = g_ce.inflight_waits; + pthread_mutex_unlock(&g_ce.lock); + return v; +} + +/* Poll until `want` waiters are blocked inside the engine, all `n_spawned` + * waiters have already returned (RED fast path), or the deadline expires. + * Returns the last observed inflight count. */ +static int poll_inflight(int want, int n_spawned, uint32_t deadline_ms) +{ + uint64_t t0 = mono_ms(); + int v = inflight_waits(); + + while (v < want && atomic_load(&g_waiters_done) < n_spawned && + mono_ms() - t0 < deadline_ms) { + sleep_ms(5); + v = inflight_waits(); + } + return v; +} + +/* ── Test 8: WAIT blocks until majority ACK, then returns 0 ─────────────── + * (Req 1.1) */ +static void test_wait_blocks_until_majority_ack_then_returns_zero(void) +{ + /* Arrange: 3-member cluster, entry 1 appended, no ACKs yet */ + engine_setup(3, 2, 1); + spawn_waiters(1, 1, 2000); + + /* Act: hold the waiter for >= 120 ms, then a standby ACK covers it */ + sleep_ms(120); + standby_ack(0, 1); + join_waiters(1); + + /* Assert: success only after the ACK — the wait really blocked */ + TEST_ASSERT_EQUAL_INT(0, g_wargs[0].rc); + TEST_ASSERT_TRUE_MESSAGE(g_wargs[0].elapsed_ms >= 100, + "WAIT must block until the majority ACK arrives"); + TEST_ASSERT_EQUAL_UINT64(1, committed()); + + engine_teardown(); + printf("test_wait_blocks_until_majority_ack_then_returns_zero: PASS\n"); +} + +/* ── Test 9: WAIT deadline → -ETIMEDOUT, committed_lsn unmoved past entry ─ + * (Req 2.1, 4.4) */ +static void test_wait_times_out_with_committed_lsn_unmoved_past_entry(void) +{ + /* Arrange: 3-member cluster, entries 1..2 appended; majority covers + * only entry 1 — entry 2 can never commit within the wait */ + engine_setup(3, 2, 2); + standby_ack(0, 1); + + /* Act: WAIT on entry 2 with a short deadline; no further ACKs */ + spawn_waiters(1, 2, 250); + join_waiters(1); + + /* Assert: timeout surfaced, deadline honored, commit index never + * advanced past the unacknowledged entry */ + TEST_ASSERT_EQUAL_INT(-ETIMEDOUT, g_wargs[0].rc); + TEST_ASSERT_TRUE_MESSAGE(g_wargs[0].elapsed_ms >= 200, + "WAIT must hold until its deadline"); + TEST_ASSERT_EQUAL_UINT64(1, committed()); + + engine_teardown(); + printf("test_wait_times_out_with_committed_lsn_unmoved_past_entry: PASS\n"); +} + +/* ── Test 10: n_peers=1 → WAIT returns 0 immediately, committed advances ── + * Standalone: self-ACK constitutes majority. (Req 6.3) */ +static void test_wait_single_member_returns_immediately_and_advances(void) +{ + int rc; + uint64_t t0, elapsed_ms; + + /* Arrange: single-member cluster, entry 1 appended */ + engine_setup(1, 0, 1); + + /* Act: WAIT with a large timeout that must NOT be consumed */ + t0 = mono_ms(); + rc = commit_submit(&g_ce, 1, COMMIT_MODE_WAIT, 5000); + elapsed_ms = mono_ms() - t0; + + /* Assert: immediate local confirmation + committed_lsn advance */ + TEST_ASSERT_EQUAL_INT(0, rc); + TEST_ASSERT_TRUE_MESSAGE(elapsed_ms < 1000, + "single-member WAIT must not block"); + TEST_ASSERT_EQUAL_UINT64(1, committed()); + + engine_teardown(); + printf("test_wait_single_member_returns_immediately_and_advances: PASS\n"); +} + +/* ── Test 11: n_peers=3, ZERO standby ACKs → -ETIMEDOUT, never success ──── + * Regression for the zero-active-standbys success short-circuit. + * (Req 4.3, 4.4) */ +static void test_wait_zero_standby_acks_never_succeeds(void) +{ + /* Arrange: 3-member cluster, entry 1 appended, standbys never ACK */ + engine_setup(3, 2, 1); + + /* Act */ + spawn_waiters(1, 1, 250); + join_waiters(1); + + /* Assert: failure result — zero peer ACKs are never a majority */ + TEST_ASSERT_TRUE_MESSAGE(g_wargs[0].rc != 0, + "zero standby ACKs must never be confirmed"); + TEST_ASSERT_EQUAL_INT(-ETIMEDOUT, g_wargs[0].rc); + TEST_ASSERT_EQUAL_UINT64(0, committed()); + + engine_teardown(); + printf("test_wait_zero_standby_acks_never_succeeds: PASS\n"); +} + +/* ── Test 12: admission cap — 7th concurrent WAIT rejected, 6 admitted ──── + * n_vcpus=8, COMMIT_WAIT_RESERVED=2 → cap = 6. The 7th gets + * -XCE_ERR_NOT_READY fast; the 6 admitted proceed; while the cap is + * saturated, commit_on_ack still advances committed_lsn and completes the + * admitted waiters. (Req 7.1, 7.2, 7.3) */ +static void test_wait_admission_cap_rejects_seventh_concurrent_wait(void) +{ + int i, rc7, observed_inflight; + uint64_t t0, elapsed7_ms; + + /* Arrange: 3-member cluster, entry 1 appended, cap = max(1, 8-2) = 6 */ + engine_setup_vcpus(3, 2, 1, 8); + spawn_waiters(6, 1, 5000); + observed_inflight = poll_inflight(6, 6, 2000); + + /* Act: 7th concurrent WAIT from this thread while 6 are blocked */ + t0 = mono_ms(); + rc7 = commit_submit(&g_ce, 1, COMMIT_MODE_WAIT, 5000); + elapsed7_ms = mono_ms() - t0; + + /* ACK arrives while the cap is still saturated (the 6 are blocked) */ + standby_ack(0, 1); + join_waiters(6); + + /* Assert (after all joins): exactly 6 admitted... */ + TEST_ASSERT_EQUAL_INT_MESSAGE(6, observed_inflight, + "all 6 admitted waits must block concurrently"); + /* ...7th rejected fast with the retriable admission error... */ + TEST_ASSERT_EQUAL_INT(-XCE_ERR_NOT_READY, rc7); + TEST_ASSERT_TRUE_MESSAGE(elapsed7_ms < 500, + "cap rejection must not block"); + /* ...ACK processing at saturation advanced committed_lsn (7.3)... */ + TEST_ASSERT_EQUAL_UINT64(1, committed()); + /* ...and completed every admitted waiter with success */ + for (i = 0; i < 6; i++) { + TEST_ASSERT_EQUAL_INT_MESSAGE(0, g_wargs[i].rc, + "admitted waits must proceed to commit"); + } + + engine_teardown(); + printf("test_wait_admission_cap_rejects_seventh_concurrent_wait: PASS\n"); +} + +/* ── Test 13: cap floor max(1, n_vcpus - reserved) for tiny n_vcpus ─────── + * n_vcpus=2, reserved=2 → cap = max(1, 0) = 1: one wait is ALWAYS + * admitted; the 2nd concurrent one is rejected. (Req 7.1, 7.2) */ +static void test_wait_admission_cap_floor_holds_for_tiny_vcpus(void) +{ + int rc2; + uint64_t t0, elapsed2_ms; + + /* Arrange: 3-member cluster, entry 1 appended, cap = 1 */ + engine_setup_vcpus(3, 2, 1, 2); + spawn_waiters(1, 1, 5000); + (void)poll_inflight(1, 1, 2000); + + /* Act: 2nd concurrent WAIT while the floor slot is occupied */ + t0 = mono_ms(); + rc2 = commit_submit(&g_ce, 1, COMMIT_MODE_WAIT, 5000); + elapsed2_ms = mono_ms() - t0; + + standby_ack(0, 1); + join_waiters(1); + + /* Assert: the floor admits at least one waiter to completion... */ + TEST_ASSERT_EQUAL_INT_MESSAGE(0, g_wargs[0].rc, + "cap floor must admit at least one wait"); + /* ...and the wait beyond the floor is rejected fast */ + TEST_ASSERT_EQUAL_INT(-XCE_ERR_NOT_READY, rc2); + TEST_ASSERT_TRUE_MESSAGE(elapsed2_ms < 500, "cap rejection must not block"); + + engine_teardown(); + printf("test_wait_admission_cap_floor_holds_for_tiny_vcpus: PASS\n"); +} + +/* ── Test 14: writable_lsn unset (0) → no gating ────────────────────────── + * Gate defaults to 0 = no gate: WAIT on an already-committed entry returns + * 0 immediately with no own-term condition applied. (Req 1.1) */ +static void test_wait_no_gate_when_writable_lsn_unset(void) +{ + int rc; + uint64_t t0, elapsed_ms; + + /* Arrange: 3-member cluster, entry 1 already majority-committed; + * commit_set_writable_lsn never called */ + engine_setup(3, 2, 1); + standby_ack(0, 1); + TEST_ASSERT_EQUAL_UINT64(1, committed()); + + /* Act */ + t0 = mono_ms(); + rc = commit_submit(&g_ce, 1, COMMIT_MODE_WAIT, 5000); + elapsed_ms = mono_ms() - t0; + + /* Assert: immediate success — no gate predicate when unset */ + TEST_ASSERT_EQUAL_INT(0, rc); + TEST_ASSERT_TRUE_MESSAGE(elapsed_ms < 1000, + "unset writable_lsn must not gate committed waits"); + + engine_teardown(); + printf("test_wait_no_gate_when_writable_lsn_unset: PASS\n"); +} + +/* ── Test 15: gate G > entry → waiter completes only once committed >= G ── + * The own-term gate is part of the WAIT completion predicate: the waiter + * stays blocked after its OWN entry commits, completing only when + * committed_lsn reaches the gate. (Req 1.1; design §Commit Engine) */ +static void test_wait_gate_blocks_until_committed_reaches_gate(void) +{ + int done_after_entry_commit; + + /* Arrange: 3-member cluster, entries 1..3 appended, gate G=3 > entry 1 */ + engine_setup(3, 2, 3); + commit_set_writable_lsn(&g_ce, 3); + spawn_waiters(1, 1, 3000); + + /* Act: entry 1 gains majority — gate (3) still unmet */ + sleep_ms(50); + standby_ack(0, 1); + sleep_ms(150); + done_after_entry_commit = atomic_load(&g_waiters_done); + + /* Gate opens: committed_lsn reaches G */ + standby_ack(0, 3); + join_waiters(1); + + /* Assert (after join): waiter held while committed_lsn < G... */ + TEST_ASSERT_EQUAL_INT_MESSAGE( + 0, done_after_entry_commit, + "waiter must not complete while committed_lsn < writable_lsn"); + /* ...then completed successfully once committed_lsn >= G */ + TEST_ASSERT_EQUAL_INT(0, g_wargs[0].rc); + TEST_ASSERT_TRUE_MESSAGE(g_wargs[0].elapsed_ms >= 180, + "waiter must block past its own entry's commit"); + TEST_ASSERT_EQUAL_UINT64(3, committed()); + + engine_teardown(); + printf("test_wait_gate_blocks_until_committed_reaches_gate: PASS\n"); +} + +/* ── Test 16: gate never opens → -ETIMEDOUT, NOT -XCE_ERR_NOT_READY ─────── + * CORRECTED contract: -XCE_ERR_NOT_READY is admission-cap-only. An unmet + * gate blocks within the timeout and surfaces -ETIMEDOUT if it never + * opens — never a NOT_READY fast-reject. (Req 1.1; design §Commit Engine) */ +static void test_wait_gate_never_opening_times_out_not_notready(void) +{ + /* Arrange: 3-member cluster, entry 1 committed, gate G=5 unreachable + * (only entry 1 is appended) */ + engine_setup(3, 2, 1); + commit_set_writable_lsn(&g_ce, 5); + standby_ack(0, 1); + TEST_ASSERT_EQUAL_UINT64(1, committed()); + + /* Act */ + spawn_waiters(1, 1, 250); + join_waiters(1); + + /* Assert: gate is a blocking predicate, not an admission fast-reject */ + TEST_ASSERT_TRUE_MESSAGE(g_wargs[0].rc != -XCE_ERR_NOT_READY, + "unmet gate must NOT fast-reject with NOT_READY"); + TEST_ASSERT_EQUAL_INT(-ETIMEDOUT, g_wargs[0].rc); + TEST_ASSERT_TRUE_MESSAGE(g_wargs[0].elapsed_ms >= 200, + "gated WAIT must block until its deadline"); + + engine_teardown(); + printf("test_wait_gate_never_opening_times_out_not_notready: PASS\n"); +} + /* ── Test group entry point ──────────────────────────────────────────────── */ void test_commit_engine(void) @@ -266,5 +646,15 @@ void test_commit_engine(void) 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); + /* Task 2.1 — WAIT semantics, admission control, own-term gate */ + RUN_TEST(test_wait_blocks_until_majority_ack_then_returns_zero); + RUN_TEST(test_wait_times_out_with_committed_lsn_unmoved_past_entry); + RUN_TEST(test_wait_single_member_returns_immediately_and_advances); + RUN_TEST(test_wait_zero_standby_acks_never_succeeds); + RUN_TEST(test_wait_admission_cap_rejects_seventh_concurrent_wait); + RUN_TEST(test_wait_admission_cap_floor_holds_for_tiny_vcpus); + RUN_TEST(test_wait_no_gate_when_writable_lsn_unset); + RUN_TEST(test_wait_gate_blocks_until_committed_reaches_gate); + RUN_TEST(test_wait_gate_never_opening_times_out_not_notready); UNITY_END(); }