Property-based testing does not reach the merge path: the convergence harness is outstanding #92
Labels
No labels
area:docs
area:identity
area:ops
area:plugin
area:server
channel:community
channel:direct
channel:owned
channel:press
channel:social
e2ee-constrained
gate:at-ga
gate:pre-ga
marketing
parity
relay:absent
relay:planned
relay:requested
relay:supported
risk:additive
risk:contract
risk:none
usability
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
Nectenda/nectenda#92
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Why
The library, the harness conventions and the CI path all now exist. What does
not exist is the one target
5a4d1b7named and deferred, in its own words:That fourth target is the merge path, and it is the one that matters most here.
The three that landed cover pure functions with clean oracles — a path
normaliser, a document id, an AEAD envelope. Convergence is different in kind: it
needs generated operation sequences across simulated replicas, and the oracle
is an invariant rather than a return value.
Every one of our remaining test cases over that path is a hand-written example:
we assert the cases we thought of.
The bug classes this repo actually suffers from are the ones nobody thought of —
docs/sync-limitations.mdis largely a list of them, andCLAUDE.mdrecordsthat several survived rounds of testing because the file on disk looked correct
right up until it was overwritten.
Concurrent CRDT merge under adversarial interleaving is the canonical case for
generated tests rather than enumerated ones. It is also where our central
principle bites hardest: a merge that loses a paragraph is the failure mode the
whole codebase is subordinate to.
What Relay does, as far as it can be read
Caveat, stated first: their test bodies are encrypted and I have not read a
single line.
.gitattributesinNo-Instructions/Relayis one line —__tests__/** filter=git-crypt diff=git-crypt— and all 193 files under it aregit-crypt encrypted. Nothing else in that repo is. Their
CONTRIBUTING.mdsaysso plainly:
Their CI agrees, erroring with "git-crypt unlock did not expose the private unit
tests" and unlocking from a
RELAY_GIT_CRYPT_KEY_B64secret.So everything below is inferred from filenames and directory structure only.
Treat it as a sketch of an approach, not as knowledge of their implementation.
The shape that is visible
Their merge engine is a hierarchical state machine, and the fuzzing is
model-based over that machine rather than generic input fuzzing. 58 of their 193
test files sit in
__tests__/merge-hsm/, with a siblinghsm/interpreter.test.tsand parallel
canvas-hsm/andfolder-hsm/suites.The support harness —
__tests__/merge-hsm/testing/, excluded from the run bytestPathIgnorePatterns— has 12 modules whose names describe the method:random.tssyntheticDoc.tscreateTestHSM.tscreateCrossVaultTest.tsMockYjsProvider.tsfakeEditorHarness.tsFakeVaultinvault-adapter.tsis the same ideaassertions.tsreplay-fixtures.tsfixtures.ts,events.ts,integration.ts,index.tsTest names in the same directory that corroborate it:
born-attached-fuzz,invariants,merge-path-adversarial,crdt-integrity,machine-edit-convergence,machine-edit-rewind,machine-definition,recording,E2ERecorded,network-resilience,merge-large-doc-perf.The four ideas worth taking
an explicit state machine, not random bytes.
machine-definitionplusmachine-edit-convergenceandmachine-edit-rewindreads as: define themachine, generate edit sequences, assert convergence and that rewind returns
where it started.
invariantstest and a sharedassertions.tssay the property being checked is "these things are alwaystrue", not "output equals expected". For us the invariants are already
written down in prose in
CLAUDE.md— no silent loss, conflicts keep bothsides, an empty document never blanks a file that has held content.
replay-fixtures.tsis the part that makes fuzzing pay. A random failurethat cannot be reproduced is a flake; captured as a fixture it becomes a
permanent regression test. Any fuzzing we add needs a seed printed on failure
and a one-command path from that seed to a committed fixture, or it will be
deleted within a month for being noisy.
package.jsondeclares nofast-check,jsverifyor similar, andrandom.tssuggests they hand-rolled it. Worthweighing against
fast-check, which gives shrinking for free — shrinking isthe expensive half to build and the half that makes a failure legible.
Where they do not fuzz
relay-serverhas none. 46 readable.rsfiles, 245 test functions, andno
proptest,quickcheck,arbitraryorcargo-fuzzanywhere.fuzz, seed or iteration-countreferences in any of their 9 workflows. Their nightly
e2e-burninisrepetition of scripted e2e plans with flake rates accumulated in an S3 ledger —
a different technique, and separately worth copying.
So their fuzzing appears to be unit-level, developer-run, over the merge state
machine. Not a nightly fuzzing campaign.
What we would do
Unknown until scoped; the likely shape:
applyMinimalDiffand the CRDT merge path with generatededit sequences across two or more replicas.
CLAUDE.md, asserted as code rather than prose.fixture, mirroring
replay-fixtures.ts.fast-checkis already the answer — settled by5a4d1b7, and itsshrinker is what named the
chunkSizebytes. The open question is narrower:whether it can drive
MockProvider-style replicas deterministically, orwhether the sequence generator has to be hand-rolled over it.
CLAUDE.md): invert a real invariant and confirm thegenerator finds it. A fuzzer that has never failed proves nothing — and this
is exactly the rule that has already caught tests here that could not fail.
packages/shared/src/crypto.tsis already partly covered byblob-cipher.property.test.ts; merge convergence is the gap.Ties into the v1 conformance work —
5a4d1b7moveddocs/v1/conformance.mdanddocs/v1/protocol/crypto.mdalongside the tests, so a convergence harnessshould state which conformance clauses it exercises rather than standing alone.
Not in scope
Fuzzing the server. It stores opaque ciphertext and never parses a document
(
doc-store.ts: "Nothing in this file interpretspayload"), so there islittle for a fuzzer to find. Malformed-frame handling at the WebSocket boundary
could be argued separately.
Relay repositories read 22 September 2026 at plugin
main. Filenames anddirectory structure only — the contents are encrypted and were not read.
No fuzz or property-based testing over the merge pathto Property-based testing does not reach the merge path: the convergence harness is outstandingMoved to the Vikunja board as NEC-68: https://projectron.nerchure.com/tasks/68