Files
figcraft/crates/collab/src/frac_tests.rs
T
BFlatandClaude Opus 5.5 4924512f4e M9: collaboration core: ids, change sets, server ordering, convergence (#103)
* M9: real-time collaboration protocol (design)

docs/collab-protocol.md: WebSocket sessions with a server-ordered,
per-property last-writer-wins document sync, client-scoped ids,
fractional positions for children, ephemeral presence (cursors,
selections, follow), per-user undo, hosting from the app first and a
self-hosted server next, and the implementation plan. ROADMAP gains M9.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* M9: collaboration core: ids, change sets, server ordering, convergence

Step 1 of docs/collab-protocol.md, no networking:

- Client-scoped node ids: Document::set_client_id makes alloc_id hand out
  client << 32 | counter; single-user files allocate as before; the id
  counter is no longer part of document equality.
- crates/collab (layer 2): the synced object/property model (every saved
  Node field, plus document-level styles, comments, variables, images,
  dev snapshots, library data), Change batches, an O(changed) diff, strict
  and lenient apply, fractional sibling positions, snapshots.
- Server (ordering, validation, rejects with fixes) and Client (optimistic
  view, replay under remote batches, author-side repair of derived data)
  as pure state machines; binary (version byte + postcard, JSON values) and
  JSON message forms, fuzzed decoders.
- Engine: normalize as the recompute hook, collab.diff.
- Tests: round trips over random real commands, an N-client convergence
  fuzz test over delayed in-order links, junk batches.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* M9: fmt after merge

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 01:03:03 +07:00

105 lines
4.2 KiB
Rust

//! Fractional position keys.
use crate::frac::{FIRST_KEY, KeyError, between, n_between, validate};
use crate::sim::Rng;
#[test]
fn first_keys_count_up_and_down() {
assert_eq!(between(None, None).unwrap(), FIRST_KEY);
assert_eq!(between(Some("a0"), None).unwrap(), "a1");
assert_eq!(between(Some("az"), None).unwrap(), "b00");
assert_eq!(between(None, Some("a0")).unwrap(), "Zz");
assert_eq!(between(None, Some("a1V")).unwrap(), "a1");
assert_eq!(between(Some("a0"), Some("a1")).unwrap(), "a0V");
assert_eq!(between(Some("a0"), Some("a2")).unwrap(), "a1");
assert_eq!(between(Some("a0V"), Some("a1")).unwrap(), "a0l");
assert_eq!(between(Some("a0"), Some("a0V")).unwrap(), "a0G");
assert_eq!(between(Some("Zz"), Some("a0")).unwrap(), "ZzV");
}
#[test]
fn bad_keys_and_bounds_are_refused() {
for bad in ["", "a", "a00", "a0 ", "b1", "!0", "a0é", "A00000000000000000000000000"] {
assert!(validate(bad).is_err(), "{bad:?}");
assert!(between(Some(bad), None).is_err());
}
assert!(validate(&format!("a0{}", "1".repeat(200))).is_err(), "too long");
for good in ["a0", "a0V", "Zz", "b00", "zzzzzzzzzzzzzzzzzzzzzzzzzzz", "A000000000000000000000000001"] {
assert_eq!(validate(good), Ok(()), "{good:?}");
}
assert_eq!(between(Some("a1"), Some("a1")), Err(KeyError::Order));
assert_eq!(between(Some("a2"), Some("a1")), Err(KeyError::Order));
// Past the largest integer, the fraction grows instead of failing.
let top = "zzzzzzzzzzzzzzzzzzzzzzzzzzz";
let after = between(Some(top), None).unwrap();
assert!(after.as_str() > top);
}
#[test]
fn many_keys_are_ordered_and_short() {
let keys = n_between(None, None, 1000).unwrap();
assert!(keys.windows(2).all(|w| w[0] < w[1]));
assert!(keys.iter().all(|k| k.len() <= 3), "counting up keeps keys short");
let inside = n_between(Some("a0"), Some("a1"), 1000).unwrap();
assert_eq!(inside.len(), 1000);
assert!(inside.windows(2).all(|w| w[0] < w[1]));
assert!(inside.first().unwrap().as_str() > "a0" && inside.last().unwrap().as_str() < "a1");
assert!(inside.iter().all(|k| k.len() <= 8), "bisection keeps keys short");
let below = n_between(None, Some("a0"), 50).unwrap();
assert!(below.windows(2).all(|w| w[0] < w[1]) && below.last().unwrap().as_str() < "a0");
assert!(n_between(Some("a0"), Some("a1"), 0).unwrap().is_empty());
}
#[test]
fn random_inserts_stay_ordered_valid_and_deterministic() {
for seed in 0..20 {
let mut rng = Rng::new(seed);
let mut keys: Vec<String> = vec![between(None, None).unwrap()];
for _ in 0..300 {
let i = rng.below(keys.len() + 1);
let lo = i.checked_sub(1).map(|j| keys[j].clone());
let hi = keys.get(i).cloned();
let k = between(lo.as_deref(), hi.as_deref()).unwrap();
assert_eq!(validate(&k), Ok(()));
assert_eq!(between(lo.as_deref(), hi.as_deref()).unwrap(), k, "deterministic");
assert!(lo.as_deref().is_none_or(|l| l < k.as_str()) && hi.as_deref().is_none_or(|h| k.as_str() < h));
keys.insert(i, k);
}
assert!(keys.windows(2).all(|w| w[0] < w[1]));
}
}
#[test]
fn appending_and_prepending_forever_never_fails() {
let mut k = FIRST_KEY.to_string();
for _ in 0..5000 {
k = between(Some(&k), None).unwrap();
}
assert!(k.len() <= 4);
let mut k = FIRST_KEY.to_string();
for _ in 0..5000 {
k = between(None, Some(&k)).unwrap();
}
assert!(k.len() <= 4);
// Squeezing next to one key: the fraction grows by about one digit per six inserts.
let mut hi = "a1".to_string();
for _ in 0..60 {
hi = between(Some("a0"), Some(&hi)).unwrap();
}
assert!(hi.len() <= 16, "{hi}");
}
#[test]
fn junk_keys_never_panic() {
let mut rng = Rng::new(7);
for _ in 0..5000 {
let a: String = (0..rng.below(12)).map(|_| char::from(rng.below(128) as u8)).collect();
let b: String = (0..rng.below(12)).map(|_| char::from(32 + rng.below(95) as u8)).collect();
let _ = validate(&a);
let _ = between(Some(&a), Some(&b));
let _ = between(None, Some(&b));
let _ = between(Some(&a), None);
let _ = n_between(Some(&a), Some(&b), 5);
}
}