test(ha-commit-barrier-rollout): RED tests for config barrier (task 5.1)

6 RED carriers pinning the three configAck defects (previous-tag acks
count, n_active==0 success shortcut, no-self-count denominator) and the
push/upgrade failure path (logged-only result, unconditional success
ACK + restart). 2 sanctioned pins (single-member, success-unchanged).
Behavior-preserving signature change: push_config_to_standbys gains
acked_out (return still legacy push count; 7 call sites NULL'd,
body-diff verified). 29/6 in group; no collateral failures.
This commit is contained in:
Rongsong Shen 2026-06-12 16:31:42 +00:00
parent 732e836868
commit 9cdeba5816
9 changed files with 582 additions and 19 deletions

View file

@ -77,11 +77,17 @@
* CTRL_PRIMARY_COMMITTING is intentionally NOT tested (deleted wholesale
* by ha-single-leader-role).
*
* Task 5.1 adds the config-barrier cases (Req 4.1-4.4) — configAck
* per-push arithmetic, push_config_to_standbys barrier-result contract,
* and the upgradeConfig requester failure/success paths; see the task-5.1
* section banner below for the full RED rationale.
*
* Requirements: 1.1, 1.2, 1.3 (task 2.1); 2.1, 2.2, 3.1 (task 1.1);
* 3.2 (task 4.1)
* 3.2 (task 4.1); 4.1, 4.2, 4.3, 4.4 (task 5.1)
*/
#include <errno.h>
#include <fcntl.h>
#include <openssl/sha.h> /* SHA256 (task 5.1 config blob hashing) */
#include <pthread.h>
#include <signal.h>
#include <stdarg.h>
@ -90,16 +96,21 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h> /* mkdir (task 5.1 upgrade fixture) */
#include <sys/wait.h>
#include <time.h>
#include <unistd.h>
#include <unity/unity.h>
#include <capnp_c.h> /* raw reply parsing (task 5.1) */
#include "xce/cluster_def.h" /* HOOK_DETAIL_*, XCE_CONTROLLER_HOOK_JOB */
#include "xce/errnox.h" /* XCE_ERR_NOT_READY (task 2.1 passthrough) */
#include "xce/types.h" /* job_t */
#include "commit.h"
#include "config_repl.h" /* push_config_to_standbys, handle_upgrade_config
* (+ netio.h, request.h, myconf.h, clusterconfig.h) */
#include "controller.capnp.h" /* OPCode_* (task 4.1 gate cases) */
#include "ha.h" /* ha_state_t, ha_config_t, zarray (task 4.1) */
#include "hook.h" /* HOOK_AFTER_EVENT */
@ -987,6 +998,500 @@ static void test_gate_with_quorum_admits_all_opcodes(void)
printf("test_gate_with_quorum_admits_all_opcodes: PASS\n");
}
/* ════════════════════════════════════════════════════════════════════════
* Task 5.1 — config barrier: configAck per-push arithmetic + upgrade
* failure path (Req 4.1, 4.2, 4.3, 4.4; design §Config Barrier).
*
* UNIT half (repl.c configAck branch via repl_wait_majority_ack): the
* hardened branch must take the in-flight push tag through the EXISTING
* entry_lsn parameter (added by ha-commit-engine; no second parameter)
* and count standbys with acked_config_lsn >= push_tag; majority =
* 1 (self) + count > n_peers / 2 over configured members (engine
* n_peers, the fixture's wired commit_engine_init value); n_peers <= 1 →
* immediate success. RED carriers against the current branch
* (repl.c:444-510, re-verified this session): it ignores entry_lsn,
* counts acked_config_lsn > 0 ("ACKed anything ever"), uses denominator
* n_active, and short-circuits n_active == 0 → success.
*
* INTEGRATION half (config_repl.c): push_config_to_standbys must return
* the barrier result (-ETIMEDOUT with acks withheld / 0 with majority)
* with the transport push count via the acked_out out-param (the task-5.1
* behavior-preserving signature change keeps the legacy push-count RETURN
* until task 5.2 — both rc assertions are RED against it), and the
* upgradeConfig requester must observe the barrier outcome: on failure an
* error reply in the send_upgrade_error shape (status < 0, nodata) with
* committed_config_tag unmoved and restart_requested clear (RED against
* the unconditional success ACK + restart, config_repl.c:1073-1106
* post-edit anchors); on success the existing reply + restart behavior is
* unchanged (PIN — both standbys pre-ack with UINT64_MAX so the case is
* green under both the legacy counting and the per-push arithmetic).
*
* Fixtures: the unit cases reuse barrier_setup (stub persist_t + repl
* registry + wired engine — the engine carries n_peers for the configAck
* denominator). The integration cases use a minimal cluster_t with stack
* standby endpoints (zeroed, no iomgr — pushes and replies stay on
* ep->outq, the test_config_repl.c:2210 / test_commit_barrier_reply.c
* capture precedent) and parse replies with the raw capnp reader.
* ════════════════════════════════════════════════════════════════════════ */
/* ── Unit: configAck per-push arithmetic (Req 4.1, 4.2) ─────────────────── */
/* A standby's acked_config_lsn from a PREVIOUS push tag must not count
* toward the current push: both standbys acked tag 0x50, the in-flight
* push is 0x100 → no covering ack → -ETIMEDOUT. RED: the current branch
* counts acked_config_lsn > 0, sees 2 >= quorum 2 and returns 0. */
static void test_configack_previous_tag_ack_does_not_count(void)
{
int rc;
/* Arrange: n_peers=3, both standbys acked an OLDER push tag */
barrier_setup(3, 2, 0);
repl_on_config_ack(&g_bp.repl, &g_bep_a, 0x50);
repl_on_config_ack(&g_bp.repl, &g_bep_b, 0x50);
/* Act: wait on push tag 0x100 (entry_lsn carries the push tag) */
rc = repl_wait_majority_ack(&g_bp.repl, 0x100, 150,
(uint8_t)OPCode_upgradeConfigAck);
/* Assert */
TEST_ASSERT_EQUAL_INT_MESSAGE(-ETIMEDOUT, rc,
"a previous push's config ack must not count toward the current tag");
barrier_teardown();
printf("test_configack_previous_tag_ack_does_not_count: PASS\n");
}
/* n_peers=3 with ZERO covering acks (here: zero connected standbys) must
* time out — a 3-member cluster can never reach majority on self alone.
* RED: the current `n_active == 0 → success` short-circuit returns 0. */
static void test_configack_no_active_standbys_times_out(void)
{
int rc;
/* Arrange: n_peers=3, no standby connected at all */
barrier_setup(3, 0, 0);
/* Act */
rc = repl_wait_majority_ack(&g_bp.repl, 0x100, 150,
(uint8_t)OPCode_upgradeConfigAck);
/* Assert */
TEST_ASSERT_EQUAL_INT_MESSAGE(-ETIMEDOUT, rc,
"n_peers=3 with zero covering config acks must never succeed "
"(the n_active == 0 success short-circuit must die on this branch)");
barrier_teardown();
printf("test_configack_no_active_standbys_times_out: PASS\n");
}
/* Self + one covering standby is a majority of n_peers=3 (1 + 1 = 2 > 1).
* RED: the current branch uses denominator n_active with NO self-count —
* quorum (2+2)/2 = 2 vs acked 1 → it times out. */
static void test_configack_self_plus_one_covering_is_majority_of_three(void)
{
int rc;
/* Arrange: n_peers=3, two standbys, one acks the in-flight tag */
barrier_setup(3, 2, 0);
repl_on_config_ack(&g_bp.repl, &g_bep_a, 0x100);
/* Act */
rc = repl_wait_majority_ack(&g_bp.repl, 0x100, 400,
(uint8_t)OPCode_upgradeConfigAck);
/* Assert */
TEST_ASSERT_EQUAL_INT_MESSAGE(0, rc,
"self + one covering standby must satisfy majority of n_peers=3");
barrier_teardown();
printf("test_configack_self_plus_one_covering_is_majority_of_three: PASS\n");
}
/* n_peers=1: single-member cluster — immediate success without consuming
* the timeout. PIN: green today (via the n_active == 0 short-circuit) and
* green after task 5.2 (explicit n_peers <= 1 rule). */
static void test_configack_single_member_immediate_success(void)
{
int rc;
uint64_t t0, elapsed_ms;
/* Arrange */
barrier_setup(1, 0, 0);
/* Act */
t0 = mono_ms();
rc = repl_wait_majority_ack(&g_bp.repl, 0x100, 500,
(uint8_t)OPCode_upgradeConfigAck);
elapsed_ms = mono_ms() - t0;
/* Assert */
TEST_ASSERT_EQUAL_INT_MESSAGE(0, rc,
"single-member cluster must confirm a config push immediately");
TEST_ASSERT_TRUE_MESSAGE(elapsed_ms < 200,
"single-member success must not consume the ack timeout");
barrier_teardown();
printf("test_configack_single_member_immediate_success: PASS\n");
}
/* ── Integration fixture: minimal cluster_t for the config push path ────── */
/* Statics survive Unity's longjmp on assertion failure. */
static cluster_t g_cb5_c;
static commit_engine_t g_cb5_eng;
static endpoint_t g_cb5_sb_ep[2]; /* standby push endpoints */
static ep_private_t g_cb5_sb_priv[2];
static endpoint_t g_cb5_req_ep; /* upgradeConfig requester */
static ep_private_t g_cb5_req_priv;
static zarray_t g_cb5_standbys;
static myconf_t g_cb5_myconf;
static char g_cb5_tmpdir[256];
static int g_cb5_have_tmpdir;
static void cb5_setup(void)
{
int i;
memset(&g_cb5_c, 0, sizeof(g_cb5_c));
memset(g_cb5_sb_ep, 0, sizeof(g_cb5_sb_ep));
memset(g_cb5_sb_priv, 0, sizeof(g_cb5_sb_priv));
memset(&g_cb5_req_ep, 0, sizeof(g_cb5_req_ep));
memset(&g_cb5_req_priv, 0, sizeof(g_cb5_req_priv));
memset(&g_cb5_myconf, 0, sizeof(g_cb5_myconf));
g_cb5_have_tmpdir = 0;
g_cb5_c.myhost = &g_cb5_c.myhost_info;
g_cb5_c.myhost_info.name = (char *)"ctrl-cb5";
g_cb5_c.myhost_info.key = (char *)"";
/* Zeroed stack endpoints, no iomgr: sends stay queued on ep->outq
* (endpoint_update_epoll no-ops; zeroed pthread mutexes are valid). */
g_cb5_req_ep.fd = -1;
g_cb5_req_ep.data = &g_cb5_req_priv;
TEST_ASSERT_EQUAL_INT(0, zarray_init(&g_cb5_standbys, 4, NULL));
for (i = 0; i < 2; i++) {
g_cb5_sb_ep[i].fd = -1;
g_cb5_sb_ep[i].data = &g_cb5_sb_priv[i];
zarray_store(&g_cb5_standbys, &g_cb5_sb_ep[i]);
}
g_cb5_c.ha.standby_endpoints = &g_cb5_standbys;
/* Replicator registry mirrors the push endpoints; wired engine carries
* n_peers=3 for the configAck majority denominator. */
TEST_ASSERT_EQUAL_INT(0, repl_init(&g_cb5_c.persist.repl));
TEST_ASSERT_TRUE(repl_add_standby(&g_cb5_c.persist.repl,
&g_cb5_sb_ep[0], "cb5_a", 7500) >= 0);
TEST_ASSERT_TRUE(repl_add_standby(&g_cb5_c.persist.repl,
&g_cb5_sb_ep[1], "cb5_b", 7501) >= 0);
TEST_ASSERT_EQUAL_INT(0,
commit_engine_init(&g_cb5_eng, &g_cb5_c.persist, 3, 8));
repl_set_commit_engine(&g_cb5_c.persist.repl, &g_cb5_eng);
/* The current do_upgrade_config_with_bytes restart path does
* cbuf_put(&c->wbuf, …) — must be a real cbuf or it blocks forever. */
TEST_ASSERT_EQUAL_INT(0, cbuf_init(&g_cb5_c.wbuf, 4, sizeof(int)));
}
/* Upgrade-path extension: tmpdir-backed myconf so conf_save_config works
* (it writes to <workdir>/<name>/conf — the parent must exist). */
static void cb5_setup_upgrade(void)
{
char inst[300];
cb5_setup();
snprintf(g_cb5_tmpdir, sizeof(g_cb5_tmpdir), "/tmp/xce_cb51_XXXXXX");
TEST_ASSERT_NOT_NULL(mkdtemp(g_cb5_tmpdir));
g_cb5_have_tmpdir = 1;
snprintf(inst, sizeof(inst), "%s/ctrl", g_cb5_tmpdir);
TEST_ASSERT_EQUAL_INT(0, mkdir(inst, 0700));
g_cb5_myconf.workdir = g_cb5_tmpdir;
g_cb5_myconf.name = (char *)"ctrl";
g_cb5_myconf.conf_keep_n = 2;
g_cb5_c.myconf = &g_cb5_myconf;
}
/* Drain and free every packet queued on an endpoint's outq. */
static void cb5_drain_ep(endpoint_t *ep)
{
data_item_t *it;
while ((it = endpoint_deq(ep)) != NULL) {
io_packet_t *pkt = ITPTR_TO_PACKET(it);
free_packet(pkt);
free(pkt);
}
}
static void cb5_teardown(void)
{
cb5_drain_ep(&g_cb5_sb_ep[0]);
cb5_drain_ep(&g_cb5_sb_ep[1]);
cb5_drain_ep(&g_cb5_req_ep);
commit_engine_fini(&g_cb5_eng);
repl_fini(&g_cb5_c.persist.repl);
zarray_fini(&g_cb5_standbys);
cbuf_fini(&g_cb5_c.wbuf);
free(g_cb5_myconf.conffile);
g_cb5_myconf.conffile = NULL;
if (g_cb5_have_tmpdir) {
char cmd[300];
snprintf(cmd, sizeof(cmd), "rm -rf %s", g_cb5_tmpdir);
system(cmd);
g_cb5_have_tmpdir = 0;
}
}
/* ── Reply observation (raw capnp parse of ep->outq packets) ────────────── */
typedef struct {
int n_replies; /* replymsg packets seen */
int32_t last_status; /* status of the last replymsg observed */
int last_data_which; /* RequestReply_data_which discriminant */
} cb5_reply_obs_t;
static void cb5_scan_replies(endpoint_t *ep, cb5_reply_obs_t *obs)
{
data_item_t *it;
while ((it = endpoint_deq(ep)) != NULL) {
io_packet_t *pkt = ITPTR_TO_PACKET(it);
struct capn c;
struct Request rq;
Request_ptr rp;
capn_init_mem(&c, (const uint8_t *)pkt->data, pkt->sz, 0);
rp.p = capn_getp(capn_root(&c), 0, 1);
read_Request(&rq, rp);
if (rq.body_which == Request_body_replymsg) {
struct RequestReply rr;
read_RequestReply(&rr, rq.body.replymsg);
obs->n_replies += 1;
obs->last_status = rr.status;
obs->last_data_which = (int)rr.data_which;
}
capn_free(&c);
free_packet(pkt);
free(pkt);
}
}
/* Build a minimal valid serialised ClusterConfig blob (caller frees). */
static uint8_t *cb5_make_config_blob(size_t *len_out)
{
cluster_config_t cfg;
struct capn cap;
struct capn_segment *cs;
ClusterConfig_ptr p;
uint8_t *buf;
size_t sz;
memset(&cfg, 0, sizeof(cfg));
cfg.clustername = (char *)"cb5-upgrade";
cfg.port = 9990;
cfg.config_generation = 1;
capn_init_malloc(&cap);
cs = capn_root(&cap).seg;
encode_ClusterConfig_ptr(cs, &p, &cfg);
capn_setp(capn_root(&cap), 0, p.p);
sz = (size_t)capn_size(&cap);
buf = (uint8_t *)malloc(sz);
TEST_ASSERT_NOT_NULL(buf);
capn_write_mem(&cap, buf, sz, 0);
capn_free(&cap);
*len_out = sz;
return buf;
}
/* upgradeConfig request scaffolding (statics survive Unity longjmp).
* handle_upgrade_config reads the raw_* fields xce_decode would have
* populated; the test owns the buffers, so xce_free_request is NOT used. */
static upgrade_config_req_t g_cb5_ucr;
static xce_request_t g_cb5_req;
static void cb5_arrange_upgrade_req(uint8_t *blob, size_t blen,
uint8_t sha[32])
{
memset(&g_cb5_ucr, 0, sizeof(g_cb5_ucr));
memset(&g_cb5_req, 0, sizeof(g_cb5_req));
g_cb5_ucr.raw_sha256 = sha;
g_cb5_ucr.raw_bytes = blob;
g_cb5_ucr.raw_bytes_len = blen;
g_cb5_req.opcode = OPCode_upgradeConfig;
g_cb5_req.seq = 1;
g_cb5_req.body_which = Request_body_upgradeconfigrequestmsg;
g_cb5_req.body.upgradeconfigrequestmsg = &g_cb5_ucr;
}
/* ── push_config_to_standbys: barrier result + push count (Req 4.2, 4.3) ── */
/* ACKs withheld: the push barrier must report -ETIMEDOUT (return value),
* the transport push count via acked_out, and the tag must never commit.
* RED: the current return value IS the push count (2), and the internal
* majority wait only logs its timeout. */
static void test_push_config_withheld_acks_returns_etimedout(void)
{
static const uint8_t blob[] = "cb5-push-blob";
uint8_t sha[32];
int pushed = -1;
int rc;
/* Arrange */
cb5_setup();
SHA256(blob, sizeof(blob) - 1, sha);
/* Act: no standby ever acks tag 0xAB12 */
rc = push_config_to_standbys(&g_cb5_c, blob, sizeof(blob) - 1, sha,
0xAB12, &pushed);
/* Assert */
TEST_ASSERT_EQUAL_INT_MESSAGE(-ETIMEDOUT, rc,
"withheld config ACKs must surface as -ETIMEDOUT from the push (Req 4.2)");
TEST_ASSERT_EQUAL_INT_MESSAGE(2, pushed,
"acked_out must report the per-standby transport push count");
TEST_ASSERT_EQUAL_UINT64_MESSAGE(0,
atomic_load_explicit(&g_cb5_c.ha.committed_config_tag,
memory_order_acquire),
"an unacked config must never become committed (Req 4.3)");
cb5_teardown();
printf("test_push_config_withheld_acks_returns_etimedout: PASS\n");
}
/* Majority acks (self + 1 covering standby of n_peers=3): the push must
* return 0 and commit the tag. RED twice over: the return value is the
* push count (2 != 0), and the legacy internal wait (quorum 2 over
* n_active, no self-count) times out so the tag never advances. */
static void test_push_config_majority_acks_returns_zero_and_commits(void)
{
static const uint8_t blob[] = "cb5-push-blob";
uint8_t sha[32];
int pushed = -1;
int rc;
/* Arrange: one standby has already acked the in-flight tag */
cb5_setup();
SHA256(blob, sizeof(blob) - 1, sha);
repl_on_config_ack(&g_cb5_c.persist.repl, &g_cb5_sb_ep[0], 0xAB12);
/* Act */
rc = push_config_to_standbys(&g_cb5_c, blob, sizeof(blob) - 1, sha,
0xAB12, &pushed);
/* Assert */
TEST_ASSERT_EQUAL_INT_MESSAGE(0, rc,
"majority config ACK must surface as success from the push (Req 4.1)");
TEST_ASSERT_EQUAL_INT_MESSAGE(2, pushed,
"acked_out must report the per-standby transport push count");
TEST_ASSERT_EQUAL_UINT64_MESSAGE(0xAB12,
atomic_load_explicit(&g_cb5_c.ha.committed_config_tag,
memory_order_acquire),
"majority-acked config must advance committed_config_tag (Req 4.1)");
cb5_teardown();
printf("test_push_config_majority_acks_returns_zero_and_commits: PASS\n");
}
/* ── upgradeConfig requester: failure and success paths (Req 4.1-4.4) ───── */
/* Barrier failure (ACKs withheld) must reach the requester as an error
* reply in the send_upgrade_error shape (status < 0, nodata), leave
* committed_config_tag unmoved, and keep restart_requested clear.
* RED: today the handler unconditionally ACKs status=0 with the
* upgradeConfigReply payload and requests the restart. */
static void test_upgrade_config_barrier_failure_error_reply_no_restart(void)
{
uint8_t *blob;
size_t blen = 0;
uint8_t sha[32];
cb5_reply_obs_t obs = { 0 };
/* Arrange */
cb5_setup_upgrade();
blob = cb5_make_config_blob(&blen);
SHA256(blob, blen, sha);
cb5_arrange_upgrade_req(blob, blen, sha);
/* Act: no standby ever acks the new tag */
(void)handle_upgrade_config(&g_cb5_c, &g_cb5_req_ep, &g_cb5_req);
/* Assert: error reply, no commit, no restart */
cb5_scan_replies(&g_cb5_req_ep, &obs);
TEST_ASSERT_EQUAL_INT_MESSAGE(1, obs.n_replies,
"the requester must receive exactly one reply");
TEST_ASSERT_TRUE_MESSAGE(obs.last_status < 0,
"a failed config commit must reach the requester as an error reply"
" (Req 4.2)");
TEST_ASSERT_EQUAL_INT_MESSAGE((int)RequestReply_data_nodata,
obs.last_data_which,
"the failure reply must use the send_upgrade_error shape (nodata)");
TEST_ASSERT_EQUAL_UINT64_MESSAGE(0,
atomic_load_explicit(&g_cb5_c.ha.committed_config_tag,
memory_order_acquire),
"a failed upgrade must leave committed_config_tag unmoved (Req 4.3)");
TEST_ASSERT_EQUAL_INT_MESSAGE(0, atomic_load(&g_cb5_c.restart_requested),
"a failed upgrade must not request the activation restart (Req 4.4)");
free(blob);
cb5_teardown();
printf("test_upgrade_config_barrier_failure_error_reply_no_restart: PASS\n");
}
/* Success path PIN: with a full majority of covering ACKs the existing
* behavior is unchanged — status-0 reply with the upgradeConfigReply
* payload, committed_config_tag at the saved tag, restart requested.
* Both standbys pre-ack UINT64_MAX (covers any tag — the new tag is only
* known after conf_save_config hashes the re-encoded blob), so the case
* is green under both the legacy counting and the per-push arithmetic. */
static void test_upgrade_config_majority_ack_success_unchanged(void)
{
uint8_t *blob;
size_t blen = 0;
uint8_t sha[32];
int rc;
cb5_reply_obs_t obs = { 0 };
/* Arrange: full covering pre-ack from both standbys */
cb5_setup_upgrade();
blob = cb5_make_config_blob(&blen);
SHA256(blob, blen, sha);
cb5_arrange_upgrade_req(blob, blen, sha);
repl_on_config_ack(&g_cb5_c.persist.repl, &g_cb5_sb_ep[0], UINT64_MAX);
repl_on_config_ack(&g_cb5_c.persist.repl, &g_cb5_sb_ep[1], UINT64_MAX);
/* Act */
rc = handle_upgrade_config(&g_cb5_c, &g_cb5_req_ep, &g_cb5_req);
/* Assert: existing success reply + restart behavior unchanged */
TEST_ASSERT_EQUAL_INT_MESSAGE(0, rc,
"a majority-acked upgrade must succeed");
cb5_scan_replies(&g_cb5_req_ep, &obs);
TEST_ASSERT_EQUAL_INT_MESSAGE(1, obs.n_replies,
"the requester must receive exactly one reply");
TEST_ASSERT_EQUAL_INT_MESSAGE(0, obs.last_status,
"the success ACK must carry status 0 (Req 4.1)");
TEST_ASSERT_EQUAL_INT_MESSAGE((int)RequestReply_data_upgradeconfigreplymsg,
obs.last_data_which,
"the success ACK must carry the upgradeConfigReply payload");
TEST_ASSERT_TRUE_MESSAGE(g_cb5_myconf.config_version_tag != 0,
"conf_save_config must have derived the new version tag");
TEST_ASSERT_EQUAL_UINT64_MESSAGE(g_cb5_myconf.config_version_tag,
atomic_load_explicit(&g_cb5_c.ha.committed_config_tag,
memory_order_acquire),
"a majority-acked config must be committed at the saved tag (Req 4.1)");
TEST_ASSERT_EQUAL_INT_MESSAGE(1, atomic_load(&g_cb5_c.restart_requested),
"a successful upgrade must request the activation restart");
free(blob);
cb5_teardown();
printf("test_upgrade_config_majority_ack_success_unchanged: PASS\n");
}
/* ── Test group entry point ──────────────────────────────────────────────── */
void test_commit_barrier_rollout(void)
@ -1016,5 +1521,15 @@ void test_commit_barrier_rollout(void)
RUN_TEST(test_gate_no_quorum_admits_clrpreason_fact);
RUN_TEST(test_gate_no_quorum_still_rejects_request_opcodes);
RUN_TEST(test_gate_with_quorum_admits_all_opcodes);
/* Task 5.1 — config barrier: configAck arithmetic + upgrade failure
* path (Req 4.1, 4.2, 4.3, 4.4) */
RUN_TEST(test_configack_previous_tag_ack_does_not_count);
RUN_TEST(test_configack_no_active_standbys_times_out);
RUN_TEST(test_configack_self_plus_one_covering_is_majority_of_three);
RUN_TEST(test_configack_single_member_immediate_success);
RUN_TEST(test_push_config_withheld_acks_returns_etimedout);
RUN_TEST(test_push_config_majority_acks_returns_zero_and_commits);
RUN_TEST(test_upgrade_config_barrier_failure_error_reply_no_restart);
RUN_TEST(test_upgrade_config_majority_ack_success_unchanged);
UNITY_END();
}

View file

@ -13,9 +13,16 @@
* confirmation advances committed_lsn through the engine. They are RED
* against the unwired legacy counting (`acked_lsn > 0`, n_active
* denominator, zero-active success short-circuit) and go green with tasks
* 5.2/5.3. The configAck expectations are untouched (config barrier is
* owned by ha-commit-barrier-rollout); only the mechanical entry_lsn=0
* argument was added to the call.
* 5.2/5.3.
*
* ha-commit-barrier-rollout task 5.1: the configAck case now follows the
* per-push arithmetic (design §Config Barrier): the call passes the config
* PUSH TAG as entry_lsn (no longer a vacuous 0 — the hardened branch counts
* standbys with acked_config_lsn >= push_tag, majority 1 (self) + count >
* n_peers / 2 over configured members). The expectation below (-ETIMEDOUT
* with zero covering acks) holds under BOTH the legacy counting and the
* per-push arithmetic; the RED carriers for the arithmetic change live in
* test_commit_barrier_rollout.c (task 5.1 section).
*
* Requirements: ha-commit-engine 1.1, 1.2, 2.1, 4.2, 4.4, 6.3
* (original repl coverage: 5.1, 5.4, 5.5, 5.8, 8.2)
@ -213,8 +220,11 @@ static void test_wait_majority_ack_catchup_excluded_from_quorum(void)
}
/* ── Test 5: OP_walAck vs OP_upgradeConfigAck dispatched independently ─────
* walAck half is engine-wired per-LSN; the configAck expectation is
* byte-identical legacy behavior (config barrier owned downstream). */
* walAck half is engine-wired per-LSN; the configAck half passes a real
* push tag as entry_lsn (per-push arithmetic, barrier-rollout task 5.1):
* n_peers=2, one active standby with NO covering config ack → timeout under
* both the legacy counting (acked_config_lsn 0, not > 0) and the hardened
* arithmetic (count(acked_config_lsn >= tag) = 0; 1 + 0 > 2/2 is false). */
static struct ack_args g_a5_0;
static void test_wait_majority_ack_ack_op_discriminates(void)
@ -233,8 +243,9 @@ static void test_wait_majority_ack_ack_op_discriminates(void)
TEST_ASSERT_EQUAL_UINT64_MESSAGE(1, m_committed(),
"walAck confirmation must advance committed_lsn per-LSN");
/* upgradeConfigAck with no pending ACKs → timeout */
int rc_cfg = repl_wait_majority_ack(&g_mp.repl, 0, 50,
/* upgradeConfigAck for push tag 0xC0FFEE with no covering config ACKs
* → timeout (entry_lsn carries the push tag — per-push arithmetic) */
int rc_cfg = repl_wait_majority_ack(&g_mp.repl, 0xC0FFEE, 50,
(uint8_t)OPCode_upgradeConfigAck);
TEST_ASSERT_EQUAL_INT(-ETIMEDOUT, rc_cfg);