Merge pull request #157 from levineuwirth/crdt-undo-repro
test(buffer): capture the CRDT undo no-op-with-op case
This commit is contained in:
commit
ccf29e3eb5
|
|
@ -2790,6 +2790,88 @@ mod tests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Deterministic reduction of a `rope_matches_crdt_projection_
|
||||||
|
/// after_arbitrary_edits` failure found by raising the case
|
||||||
|
/// count (`PROPTEST_CASES=2000`) on `main` @ `e745068`.
|
||||||
|
///
|
||||||
|
/// **What happens.** Replacing a byte range with *identical*
|
||||||
|
/// bytes is a textual no-op but a real CRDT operation (a delete
|
||||||
|
/// plus an insert). Undoing it therefore advances the CRDT
|
||||||
|
/// version while leaving the materialized text unchanged, so
|
||||||
|
/// `undo_crdt_mode` derives an EMPTY replacement edit — and
|
||||||
|
/// still attaches the `crdt_op` that `crdt.undo()` produced.
|
||||||
|
/// That trips the proptest's `crdt_op` shape invariant, "a
|
||||||
|
/// no-op edit must have `crdt_op = None`".
|
||||||
|
///
|
||||||
|
/// **What was verified about the consequences**, so the next
|
||||||
|
/// reader does not have to redo it:
|
||||||
|
///
|
||||||
|
/// * content stays correct — rope and CRDT projection agree
|
||||||
|
/// before and after (asserted below);
|
||||||
|
/// * replicas stay converged — both `crdt_op` consumers
|
||||||
|
/// (`EditorCore::queue_daemon_origin_crdt_op` and the remote-op
|
||||||
|
/// path) read `edit.crdt_op` unconditionally and do **not**
|
||||||
|
/// short-circuit on an empty range, so the op is broadcast;
|
||||||
|
/// * the cursor does not jump — `EditorCore::undo` only clamps
|
||||||
|
/// to buffer length and never seeks `edit.range.start`.
|
||||||
|
///
|
||||||
|
/// **The open question** is therefore whether the *invariant* is
|
||||||
|
/// simply mis-scoped rather than the behavior being wrong. It
|
||||||
|
/// was written for the FORWARD `apply_edit` short-circuit, which
|
||||||
|
/// returns before ever producing an op; CRDT-mode undo/redo
|
||||||
|
/// never reach that path. One artifact is genuinely arbitrary
|
||||||
|
/// either way: `derive_replacement_edit` reports the empty range
|
||||||
|
/// at the buffer END rather than at the edit site.
|
||||||
|
///
|
||||||
|
/// Ignored, not deleted: it documents a real, reproducible
|
||||||
|
/// asymmetry that nothing else on `main` records, and un-ignoring
|
||||||
|
/// it is the first step of whichever resolution wins.
|
||||||
|
#[test]
|
||||||
|
#[ignore = "known pre-existing main behavior; see the doc comment \
|
||||||
|
for the verified consequences and the open question"]
|
||||||
|
fn crdt_undo_of_an_identity_replace_reports_a_no_op_edit_carrying_an_op() {
|
||||||
|
let mut buffer =
|
||||||
|
Buffer::new_with_crdt(BufferId::next(), "*identity-undo*", 1).expect("crdt");
|
||||||
|
buffer
|
||||||
|
.apply_edit(EditOp::Insert {
|
||||||
|
pos: 0,
|
||||||
|
bytes: b"hello",
|
||||||
|
})
|
||||||
|
.expect("seed insert");
|
||||||
|
|
||||||
|
// Replace one byte with the SAME byte.
|
||||||
|
let forward = buffer
|
||||||
|
.apply_edit(EditOp::Replace {
|
||||||
|
range: Range::new(1, 2),
|
||||||
|
bytes: b"e",
|
||||||
|
})
|
||||||
|
.expect("identity replace");
|
||||||
|
assert_eq!(forward.range, Range::new(1, 2));
|
||||||
|
assert_eq!(forward.inserted_len, 1);
|
||||||
|
|
||||||
|
let undone = buffer.undo().expect("undo");
|
||||||
|
assert!(
|
||||||
|
undone.range.is_empty() && undone.inserted_len == 0,
|
||||||
|
"the undo produced no textual change: {:?}/{}",
|
||||||
|
undone.range,
|
||||||
|
undone.inserted_len
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
undone.crdt_op.is_some(),
|
||||||
|
"…yet it carries a version-advancing CRDT op — the invariant \
|
||||||
|
the proptest trips on"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Content is unharmed in both projections.
|
||||||
|
let mut bytes = vec![0u8; buffer.len() as usize];
|
||||||
|
buffer.snapshot_rope().slice(0, buffer.len(), &mut bytes);
|
||||||
|
assert_eq!(String::from_utf8(bytes).expect("utf8"), "hello");
|
||||||
|
assert_eq!(
|
||||||
|
buffer.crdt_state().expect("crdt").materialize_string(),
|
||||||
|
"hello"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
proptest! {
|
proptest! {
|
||||||
// Smaller proptest case count than the default (64) to keep
|
// Smaller proptest case count than the default (64) to keep
|
||||||
// CI overhead modest; the per-op invariant check is the
|
// CI overhead modest; the per-op invariant check is the
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue