Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions tests/Tests.FSharp/Properties/Math.Invariants.Tests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,76 @@ let ``LWW-register merge tie-breaks left on equal (timestamp, replica)`` () =
(LwwRegister<int>.Merge b a).Value |> should equal 2


// ─── Delta-CRDT anti-entropy laws (Almeida et al. 2018) ────────────
// Delta-state CRDTs ship per-operation deltas instead of full state.
// The anti-entropy contract: applying deltas in ANY order, with
// duplicates allowed, must converge to the same state. The laws
// inherit from GCounter/PNCounter semilattice merges, but pinning
// them at the delta-apply layer guards against future regressions
// in DeltaCrdt's apply implementation (e.g. accidentally using
// addition instead of max-merge under the hood).

[<Property>]
let ``GCounterDelta apply is order-independent``
(NonNegativeInt d1) (NonNegativeInt d2) =
let initial = GCounter.Empty
let priorDvv = Dvv.Empty
let delta1 = GCounterDelta.increment "r1" (int64 d1) priorDvv
let delta2 = GCounterDelta.increment "r2" (int64 d2) priorDvv
let lhs = GCounterDelta.apply (GCounterDelta.apply initial delta1) delta2
let rhs = GCounterDelta.apply (GCounterDelta.apply initial delta2) delta1
Comment on lines +221 to +229
lhs.Value = rhs.Value
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Verify delta convergence on full state, not only Value

The anti-entropy comment states replicas must converge to the same state, but these properties only compare .Value. A broken apply implementation can preserve totals while mutating per-replica/component structure, and that latent divergence affects later merges while still passing this assertion. The properties should assert structural equivalence of counter state (e.g., Counts for GCounter, both P and N internals for PNCounter).

Useful? React with 👍 / 👎.


[<Property>]
let ``GCounterDelta apply is idempotent on Value``
(NonNegativeInt d) =
let initial = GCounter.Empty
let delta = GCounterDelta.increment "r1" (int64 d) Dvv.Empty
let once = GCounterDelta.apply initial delta
let twice = GCounterDelta.apply once delta
once.Value = twice.Value

[<Property>]
let ``PNCounterDelta apply is order-independent``
(d1: int) (d2: int) =
let initial = PNCounter.Empty
let priorDvv = Dvv.Empty
let delta1 = PNCounterDelta.increment "r1" (int64 d1) priorDvv
let delta2 = PNCounterDelta.increment "r2" (int64 d2) priorDvv
Comment thread
AceHack marked this conversation as resolved.
let lhs = PNCounterDelta.apply (PNCounterDelta.apply initial delta1) delta2
let rhs = PNCounterDelta.apply (PNCounterDelta.apply initial delta2) delta1
lhs.Value = rhs.Value

[<Property>]
let ``PNCounterDelta apply is idempotent on Value`` (d: int) =
// Duplicate-tolerance: applying the SAME delta twice must yield
// the same Value as applying it once. Catches a future regression
// that uses additive merge instead of GCounter max-merge — under
// additive merge, double-apply would double the count.
let initial = PNCounter.Empty
let delta = PNCounterDelta.increment "r1" (int64 d) Dvv.Empty
let once = PNCounterDelta.apply initial delta
let twice = PNCounterDelta.apply once delta
once.Value = twice.Value

[<Property>]
let ``PNCounterDelta same-replica deltas converge order-independently``
Comment on lines +252 to +265
(d1: int) (d2: int) =
// Same-replica test exercises the WITHIN-component merge path
Comment on lines +266 to +267
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Constrain same-replica test to same PN component

This property is intended to exercise the within-component max-merge path, but d1/d2 are unconstrained ints, so opposite-sign cases send one delta to P and the other to N. In those runs, the test falls back to a disjoint-component scenario and no longer validates the bug class described in the comment, which can let same-component merge regressions slip through. Constrain both deltas to the same sign (or split into explicit positive/negative cases).

Useful? React with 👍 / 👎.

// (both deltas land in the same PN P-side or N-side GCounter
// entry under "r1"). The disjoint-replica order-independence
// property above can stay green even under a broken additive
// merge because deltas land in different replicas; this property
// forces the merge to actually max-fold the same component.
let initial = PNCounter.Empty
let priorDvv = Dvv.Empty
let delta1 = PNCounterDelta.increment "r1" (int64 d1) priorDvv
let delta2 = PNCounterDelta.increment "r1" (int64 d2) priorDvv
let lhs = PNCounterDelta.apply (PNCounterDelta.apply initial delta1) delta2
Comment on lines +267 to +277
let rhs = PNCounterDelta.apply (PNCounterDelta.apply initial delta2) delta1
lhs.Value = rhs.Value


Comment thread
AceHack marked this conversation as resolved.
// ─── HyperMinHash — Jaccard monotonicity ───────────────────────────
// Reference: Yu & Weber arXiv:1710.08436.

Expand Down
Loading