test(raft-ha-core-safety): payload-aware persist-guard RED + pins (task 2.1)

Case (a) empty-then-full = genuine RED (payload-blind guard at persist.c:822
no-ops the corrective write, Expected 64 Was 0). Cases (b) full-then-empty
and (c) identical-rewrite = baseline-passing pins that constrain the 2.2
fix asymmetry (empty must never overwrite stored full; identical re-stream
stays idempotent). 7 tests/1 RED/no ASAN.
This commit is contained in:
Rongsong Shen 2026-06-27 10:20:02 +00:00
parent 07f08e58f5
commit 3b5eda1429
2 changed files with 119 additions and 2 deletions

View file

@ -276,6 +276,118 @@ static void test_smoke_red_payload_blind_guard_drops_corrective_write(void)
printf("test_smoke_red_payload_blind_guard_drops_corrective_write: done\n");
}
/* ═══════════════════════════════════════════════════════════════════════════
* TASK 2.1 — payload-aware persistence guard cases (a)/(b)/(c)
*
* These three cases pin the post-fix contract task 2.2 must satisfy when it
* makes the LSN-only idempotency guard (persist.c:822) (term, lsn, payload-crc)
* -aware. They read as one coherent group:
*
* (a) empty-then-full at the SAME (term, lsn) → must end FULL.
* This is the RED case, owned by the smoke-RED above
* (test_smoke_red_payload_blind_guard_drops_corrective_write): the
* payload-blind guard no-ops the corrective write and keeps the empty
* body — FAILS on the baseline, goes GREEN with 2.2.
*
* (b) full-then-empty at the SAME (term, lsn) → must NOT degrade to empty.
* PIN: the baseline already satisfies this — once the full body advances
* current_lsn, the same LSN-only idempotency guard no-ops the later empty
* write, so the stored entry stays full. This case is therefore NOT a
* baseline RED; it is a guard that 2.2's new payload-crc-aware branch does
* not over-correct an empty (PLEN=0) write into a destructive
* overwrite/truncate of a known-non-empty committed entry.
*
* (c) identical (term, payload-crc) re-write → safe idempotent no-op.
* PIN: the baseline already satisfies this (the LSN-only guard no-ops it);
* it guards 2.2 against treating an identical re-stream as a "conflict"
* and over-truncating/duplicating a correct committed entry.
* ═══════════════════════════════════════════════════════════════════════════ */
/* Case (b): a stray empty (PLEN=0) write at an existing committed (term, lsn)
* must NEVER degrade the stored full entry to empty. Baseline-passing PIN
* (the LSN-only idempotency guard already no-ops the empty write). */
static void test_2_1_full_then_empty_must_not_degrade(void)
{
hcs_member_t m;
TEST_ASSERT_EQUAL_INT(0, hcs_member_up(&m, "fte"));
uint8_t full[HCS_FULL_LEN];
hcs_fill_body(full);
/* (1) the CORRECT full body lands first at committed LSN 1. */
TEST_ASSERT_EQUAL_INT_MESSAGE(0,
hcs_wal_put(&m, /*lsn=*/1, /*term=*/3, WAL_JOB_SUBMIT, full, HCS_FULL_LEN),
"the full replicated write must persist (needs a real fs for the inline "
"fdatasync; -EIO here means overlayfs — bug-924)");
/* (2) a stray empty (PLEN=0) body arrives for the SAME (term, lsn). */
int rc_empty = hcs_wal_put(&m, /*lsn=*/1, /*term=*/3, WAL_JOB_SUBMIT,
/*payload=*/NULL, /*len=*/0);
TEST_ASSERT_TRUE_MESSAGE(rc_empty == 0,
"the stray empty write returns 0 (baseline: an idempotent no-op; "
"fixed: a rejected/repaired conflict) — either way rc==0");
/* (3) OBSERVE: on disk the entry must STILL hold the FULL payload — an
* empty body must never become the durable form of a known-non-empty
* committed entry (the inverse of case (a)). */
hcs_probe_t pr;
TEST_ASSERT_EQUAL_INT(0, hcs_wal_probe(&m, 1, &pr));
TEST_ASSERT_TRUE_MESSAGE(pr.present,
"committed LSN 1 must remain present after the stray empty write");
TEST_ASSERT_EQUAL_UINT32_MESSAGE(HCS_FULL_LEN, pr.len,
"PIN: an empty (PLEN=0) write must NOT degrade the stored full entry to "
"empty (guards 2.2 against over-correcting into a destructive overwrite)");
TEST_ASSERT_NOT_EQUAL_MESSAGE(0u, pr.pcrc,
"PIN: the committed entry must keep its real payload CRC, not drop to 0");
hcs_member_down(&m);
printf("test_2_1_full_then_empty_must_not_degrade: PASS (pin)\n");
}
/* Case (c): re-streaming the IDENTICAL (term, payload-crc) entry at the same
* committed LSN is a safe idempotent no-op — no degrade, no duplicate, no
* CRC-chain break. Baseline-passing PIN. */
static void test_2_1_identical_rewrite_is_idempotent_noop(void)
{
hcs_member_t m;
TEST_ASSERT_EQUAL_INT(0, hcs_member_up(&m, "idem"));
uint8_t full[HCS_FULL_LEN];
hcs_fill_body(full);
/* (1) the full body lands at committed LSN 1. */
TEST_ASSERT_EQUAL_INT_MESSAGE(0,
hcs_wal_put(&m, /*lsn=*/1, /*term=*/3, WAL_JOB_SUBMIT, full, HCS_FULL_LEN),
"the initial full write must persist (real fs required — bug-924)");
hcs_probe_t before;
TEST_ASSERT_EQUAL_INT(0, hcs_wal_probe(&m, 1, &before));
TEST_ASSERT_TRUE(before.present);
TEST_ASSERT_EQUAL_UINT32(HCS_FULL_LEN, before.len);
TEST_ASSERT_NOT_EQUAL(0u, before.pcrc);
/* (2) the IDENTICAL (term, payload-crc) entry is re-streamed at the same
* (term, lsn) — must be accepted as a no-op, never a conflict. */
TEST_ASSERT_EQUAL_INT_MESSAGE(0,
hcs_wal_put(&m, /*lsn=*/1, /*term=*/3, WAL_JOB_SUBMIT, full, HCS_FULL_LEN),
"an identical (term, payload-crc) re-write must be a safe idempotent no-op");
/* (3) OBSERVE: the stored entry is unchanged — same length and same
* (term, payload-crc) fingerprint as before the re-write. */
hcs_probe_t after;
TEST_ASSERT_EQUAL_INT(0, hcs_wal_probe(&m, 1, &after));
TEST_ASSERT_TRUE(after.present);
TEST_ASSERT_EQUAL_UINT32_MESSAGE(HCS_FULL_LEN, after.len,
"PIN: an identical re-write must leave the full payload length intact");
TEST_ASSERT_EQUAL_UINT64_MESSAGE(before.term, after.term,
"PIN: an identical re-write must leave the stored term unchanged");
TEST_ASSERT_EQUAL_UINT32_MESSAGE(before.pcrc, after.pcrc,
"PIN: an identical re-write must leave the stored payload CRC unchanged "
"(no degrade, no over-truncate)");
hcs_member_down(&m);
printf("test_2_1_identical_rewrite_is_idempotent_noop: PASS (pin)\n");
}
/* ═══════════════════════════════════════════════════════════════════════════
* SCAFFOLDING (PASS) — prove each shared seam is usable by later phases.
* ═══════════════════════════════════════════════════════════════════════════ */
@ -400,8 +512,13 @@ static void test_seam_election_injection(void)
void test_ha_core_safety(void)
{
UNITY_BEGIN();
/* The ONE smoke-RED: payload-blind guard drops the corrective write. */
/* Task 2.1 payload-aware guard set:
* (a) empty-then-full — RED (the smoke-RED below): FAILs on the baseline.
* (b) full-then-empty — PIN: must not degrade (baseline-passing).
* (c) identical re-write — PIN: idempotent no-op (baseline-passing). */
RUN_TEST(test_smoke_red_payload_blind_guard_drops_corrective_write);
RUN_TEST(test_2_1_full_then_empty_must_not_degrade);
RUN_TEST(test_2_1_identical_rewrite_is_idempotent_noop);
/* Scaffolding: each shared seam is usable by 2.3 / 4.1 / 5.1. */
RUN_TEST(test_seam_wal_construct_and_probe);
RUN_TEST(test_seam_divergence_is_observable);