1pub use crate::oplog::FoldTier;
38use crate::oplog::{canonical_json, Hlc, OpRecord};
39use car_state::crdt::{LwwMap, LwwRegister};
40use serde::{Deserialize, Serialize};
41use serde_json::Value;
42use sha2::{Digest, Sha256};
43use std::collections::BTreeMap;
44
45#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
47pub struct FoldedRecord {
48 pub op_id: String,
49 pub hlc: Hlc,
50 pub payload: Value,
51}
52
53#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
86pub struct IntentAgent {
87 pub fencing_epoch: u64,
90 pub runs: BTreeMap<String, FoldedRecord>,
93 #[serde(default)]
99 pub committed_runs: BTreeMap<String, FoldedRecord>,
100}
101
102#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
107pub struct SyncState {
108 pub logs: BTreeMap<String, BTreeMap<String, FoldedRecord>>,
111 pub registries: BTreeMap<String, BTreeMap<String, FoldedRecord>>,
113 #[serde(default)]
118 pub intents: BTreeMap<String, IntentAgent>,
119}
120
121impl SyncState {
122 pub fn log_entries(&self, surface_tag: &str) -> Vec<&FoldedRecord> {
126 let mut entries: Vec<&FoldedRecord> = self
127 .logs
128 .get(surface_tag)
129 .map(|m| m.values().collect())
130 .unwrap_or_default();
131 entries.sort_by(|a, b| (&a.hlc, &a.op_id).cmp(&(&b.hlc, &b.op_id)));
132 entries
133 }
134
135 pub fn replay<T, F>(&self, surface_tag: &str, init: T, apply: F) -> T
141 where
142 F: FnMut(T, &FoldedRecord) -> T,
143 {
144 self.log_entries(surface_tag).into_iter().fold(init, apply)
145 }
146
147 pub fn intent(&self, agent_id: &str, run_id: &str) -> Option<&FoldedRecord> {
157 self.intents.get(agent_id)?.runs.get(&format!("id:{run_id}"))
158 }
159
160 pub fn committed_run(&self, agent_id: &str, run_id: &str) -> Option<&FoldedRecord> {
166 self.intents
167 .get(agent_id)?
168 .committed_runs
169 .get(&format!("id:{run_id}"))
170 }
171
172 pub fn committed_run_ids(&self, agent_id: &str) -> Vec<&str> {
176 self.intents
177 .get(agent_id)
178 .map(|a| {
179 a.committed_runs
180 .keys()
181 .filter_map(|k| k.strip_prefix("id:"))
182 .collect()
183 })
184 .unwrap_or_default()
185 }
186
187 pub fn fencing_epoch(&self, agent_id: &str) -> Option<u64> {
190 self.intents.get(agent_id).map(|a| a.fencing_epoch)
191 }
192}
193
194fn intent_is_terminal(rec: &FoldedRecord) -> bool {
197 crate::lease::intent_status_rank(&rec.payload) == 1
198}
199
200fn intent_priority(rec: &FoldedRecord) -> (u8, u64, &Hlc, &str) {
205 (
206 crate::lease::intent_status_rank(&rec.payload),
207 crate::lease::intent_epoch(&rec.payload),
208 &rec.hlc,
209 rec.op_id.as_str(),
210 )
211}
212
213pub fn fold(ops: &[OpRecord]) -> SyncState {
228 fold_onto(&SyncState::default(), ops)
229}
230
231pub fn fold_onto(base: &SyncState, ops: &[OpRecord]) -> SyncState {
250 let canonical_record = |op: &OpRecord| -> String {
255 canonical_json(&serde_json::to_value(op).expect("OpRecord serializes"))
256 };
257 let mut unique: BTreeMap<&str, &OpRecord> = BTreeMap::new();
258 for op in ops {
259 unique
260 .entry(&op.op_id)
261 .and_modify(|existing| {
262 if *existing != op && canonical_record(op) < canonical_record(existing) {
263 *existing = op;
264 }
265 })
266 .or_insert(op);
267 }
268
269 let mut state = base.clone();
270
271 {
279 let mut agent_max: BTreeMap<String, u64> = BTreeMap::new();
280 for op in unique.values() {
281 if op.surface.fold_tier() == FoldTier::Leased {
282 let slot = agent_max
283 .entry(crate::lease::intent_agent(&op.payload).to_string())
284 .or_insert(0);
285 *slot = (*slot).max(crate::lease::intent_epoch(&op.payload));
286 }
287 }
288 for (agent, max_epoch) in agent_max {
289 let entry = state.intents.entry(agent).or_default();
290 if max_epoch > entry.fencing_epoch {
291 entry.fencing_epoch = max_epoch;
292 entry.runs.retain(|_, r| intent_is_terminal(r)); }
294 }
295 }
296
297 for op in unique.values() {
298 let record = FoldedRecord {
299 op_id: op.op_id.clone(),
300 hlc: op.hlc.clone(),
301 payload: op.payload.clone(),
302 };
303 match op.surface.fold_tier() {
304 FoldTier::GrowOnly => {
305 let slot = state
306 .logs
307 .entry(op.surface.tag())
308 .or_default()
309 .entry(op.fold_key());
310 slot.and_modify(|existing| {
311 if (&record.hlc, &record.op_id) < (&existing.hlc, &existing.op_id) {
315 *existing = record.clone();
316 }
317 })
318 .or_insert(record);
319 }
320 FoldTier::Registry => {
321 let slot = state
322 .registries
323 .entry(op.surface.tag())
324 .or_default()
325 .entry(op.fold_key());
326 slot.and_modify(|existing| {
327 if (&record.hlc, &record.op_id) > (&existing.hlc, &existing.op_id) {
329 *existing = record.clone();
330 }
331 })
332 .or_insert(record);
333 }
334 FoldTier::Leased => {
335 let agent = crate::lease::intent_agent(&op.payload).to_string();
337 let epoch = crate::lease::intent_epoch(&op.payload);
338 let key = op.fold_key();
339 let is_terminal = crate::lease::intent_status_rank(&op.payload) == 1;
340 let is_committed = crate::lease::intent_is_committed(&op.payload);
341 let entry = state.intents.entry(agent).or_default();
342
343 if is_committed {
348 let better = entry
349 .committed_runs
350 .get(&key)
351 .is_none_or(|existing| intent_priority(&record) > intent_priority(existing));
352 if better {
353 entry.committed_runs.insert(key.clone(), record.clone());
354 }
355 }
356
357 let eligible = is_terminal || epoch == entry.fencing_epoch;
364 if eligible {
365 let better = entry
366 .runs
367 .get(&key)
368 .is_none_or(|existing| intent_priority(&record) > intent_priority(existing));
369 if better {
370 entry.runs.insert(key, record);
371 }
372 }
373 }
374 }
375 }
376 state
377}
378
379pub fn state_hash(state: &SyncState) -> String {
384 let value = serde_json::to_value(state).expect("SyncState serializes");
385 let mut hasher = Sha256::new();
386 hasher.update(canonical_json(&value).as_bytes());
387 let digest = hasher.finalize();
388 let hex: String = digest.iter().take(16).map(|b| format!("{b:02x}")).collect();
389 format!("state-{hex}")
390}
391
392pub fn hlc_version(hlc: &Hlc) -> u64 {
399 debug_assert!(hlc.counter < (1 << 20), "HLC counter exceeds encoding range");
400 debug_assert!(
401 hlc.wall_ms < (1 << 44),
402 "HLC wall_ms exceeds encoding range (the << 20 would drop high bits in release)"
403 );
404 (hlc.wall_ms << 20) | (u64::from(hlc.counter) & 0xF_FFFF)
405}
406
407pub fn registry_as_lww(state: &SyncState, surface_tag: &str) -> LwwMap {
412 state
413 .registries
414 .get(surface_tag)
415 .map(|records| {
416 records
417 .iter()
418 .map(|(key, rec)| {
419 (
420 key.clone(),
421 LwwRegister::new(
422 rec.payload.clone(),
423 hlc_version(&rec.hlc),
424 rec.hlc.device_id.clone(),
425 ),
426 )
427 })
428 .collect()
429 })
430 .unwrap_or_default()
431}
432
433#[cfg(test)]
434mod tests {
435 use super::*;
436 use crate::lease::{Intent, IntentStatus};
437 use crate::oplog::{DeviceLog, Scope, Surface};
438 use serde_json::json;
439
440 fn intent_op(
442 dev: &mut DeviceLog,
443 agent: &str,
444 run: &str,
445 epoch: u64,
446 status: IntentStatus,
447 ) -> OpRecord {
448 dev.append(
449 Scope::Personal,
450 Surface::Intent,
451 Intent::new(agent, run, epoch, status).payload(),
452 )
453 }
454
455 #[test]
456 fn grow_only_unions_by_stable_key() {
457 let mut a = DeviceLog::new("a");
458 let mut b = DeviceLog::new("b");
459 let ops = vec![
460 a.append(Scope::Personal, Surface::Knowledge, json!({"id": "f1", "v": 1})),
461 b.append(Scope::Personal, Surface::Knowledge, json!({"id": "f2", "v": 2})),
462 ];
463 let state = fold(&ops);
464 let knowledge = &state.logs[&Surface::Knowledge.tag()];
465 assert_eq!(knowledge.len(), 2);
466 assert_eq!(knowledge["id:f1"].payload["v"], json!(1));
467 assert_eq!(knowledge["id:f2"].payload["v"], json!(2));
468 }
469
470 #[test]
471 fn grow_only_key_collision_resolves_to_earliest_deterministically() {
472 let mut a = DeviceLog::new("a");
475 let mut b = DeviceLog::new("b");
476 let oa = a.append(Scope::Personal, Surface::Knowledge, json!({"id": "f", "v": "a"}));
477 b.observe(&oa.hlc); let ob = b.append(Scope::Personal, Surface::Knowledge, json!({"id": "f", "v": "b"}));
479 let fwd = fold(&[oa.clone(), ob.clone()]);
480 let rev = fold(&[ob, oa]);
481 assert_eq!(fwd, rev);
482 assert_eq!(fwd.logs[&Surface::Knowledge.tag()]["id:f"].payload["v"], json!("a"));
483 }
484
485 #[test]
486 fn registry_is_lww_per_record_not_per_file() {
487 let mut a = DeviceLog::new("a");
488 let mut b = DeviceLog::new("b");
489 let oa = a.append(Scope::Personal, Surface::Declagent, json!({"id": "x", "owner": "a"}));
491 let ob = b.append(Scope::Personal, Surface::Declagent, json!({"id": "y", "owner": "b"}));
492 b.observe(&oa.hlc);
494 let ob2 = b.append(Scope::Personal, Surface::Declagent, json!({"id": "x", "owner": "b"}));
495 let state = fold(&[oa, ob, ob2]);
496 let reg = &state.registries[&Surface::Declagent.tag()];
497 assert_eq!(reg.len(), 2, "both records survive");
498 assert_eq!(reg["id:x"].payload["owner"], json!("b"), "later HLC wins the shared record");
499 assert_eq!(reg["id:y"].payload["owner"], json!("b"));
500 }
501
502 #[test]
503 fn registry_concurrent_tie_breaks_on_device_deterministically() {
504 let mut a = DeviceLog::new("a");
507 let mut b = DeviceLog::new("b");
508 let oa = a.append(Scope::Personal, Surface::Declagent, json!({"id": "x", "owner": "a"}));
509 let ob = b.append(Scope::Personal, Surface::Declagent, json!({"id": "x", "owner": "b"}));
510 assert_eq!(oa.hlc.wall_ms, ob.hlc.wall_ms);
511 let fwd = fold(&[oa.clone(), ob.clone()]);
512 let rev = fold(&[ob, oa]);
513 assert_eq!(fwd, rev);
514 assert_eq!(
516 fwd.registries[&Surface::Declagent.tag()]["id:x"].payload["owner"],
517 json!("b")
518 );
519 }
520
521 #[test]
522 fn state_hash_detects_divergence_and_agrees_on_convergence() {
523 let mut a = DeviceLog::new("a");
524 let o1 = a.append(Scope::Personal, Surface::Knowledge, json!({"id": "f1"}));
525 let o2 = a.append(Scope::Personal, Surface::Knowledge, json!({"id": "f2"}));
526 let h_full = state_hash(&fold(&[o1.clone(), o2.clone()]));
527 let h_full_again = state_hash(&fold(&[o2.clone(), o1.clone()]));
528 assert_eq!(h_full, h_full_again, "same op-set → same hash");
529 let h_partial = state_hash(&fold(&[o1]));
530 assert_ne!(h_full, h_partial, "different op-set → different hash");
531 assert!(h_full.starts_with("state-"));
532 }
533
534 #[test]
535 fn hlc_version_preserves_order() {
536 let stamps = [
537 Hlc { wall_ms: 1, counter: 0, device_id: "a".into() },
538 Hlc { wall_ms: 1, counter: 1, device_id: "a".into() },
539 Hlc { wall_ms: 2, counter: 0, device_id: "a".into() },
540 ];
541 for w in stamps.windows(2) {
542 assert!(hlc_version(&w[0]) < hlc_version(&w[1]));
543 }
544 }
545
546 #[test]
547 fn registry_as_lww_matches_crdt_merge_including_export_shape() {
548 let mut a = DeviceLog::new("dev-a");
551 let mut b = DeviceLog::new("dev-b");
552 let oa1 = a.append(Scope::Personal, Surface::Registry { kind: "agents".into() }, json!({"id": "r1", "v": "a"}));
553 let oa2 = a.append(Scope::Personal, Surface::Registry { kind: "agents".into() }, json!({"id": "r2", "v": "a"}));
554 b.observe(&oa1.hlc);
555 b.observe(&oa2.hlc);
556 let ob1 = b.append(Scope::Personal, Surface::Registry { kind: "agents".into() }, json!({"id": "r1", "v": "b"}));
557
558 let tag = Surface::Registry { kind: "agents".into() }.tag();
559 let union = registry_as_lww(&fold(&[oa1.clone(), oa2.clone(), ob1.clone()]), &tag);
560 let export_a = registry_as_lww(&fold(&[oa1, oa2]), &tag);
561 let export_b = registry_as_lww(&fold(&[ob1]), &tag);
562
563 assert_eq!(car_state::crdt::merge_maps(&export_a, &export_b), union);
564 assert_eq!(car_state::crdt::merge_many(&[export_b, export_a]), union);
565 let plain = car_state::crdt::materialize(&union);
566 assert_eq!(plain["id:r1"]["v"], json!("b"));
567 assert_eq!(plain["id:r2"]["v"], json!("a"));
568 }
569
570 #[test]
571 fn log_entries_are_hlc_ordered() {
572 let mut a = DeviceLog::new("a");
573 let mut b = DeviceLog::new("b");
574 let o1 = a.append(Scope::Personal, Surface::Conversation, json!({"t": "first"}));
575 b.observe(&o1.hlc);
576 let o2 = b.append(Scope::Personal, Surface::Conversation, json!({"t": "second"}));
577 a.observe(&o2.hlc); let o3 = a.append(Scope::Personal, Surface::Conversation, json!({"t": "third"}));
579 let state = fold(&[o3, o1, o2]);
581 let texts: Vec<&Value> = state
582 .log_entries(&Surface::Conversation.tag())
583 .iter()
584 .map(|r| &r.payload["t"])
585 .collect();
586 assert_eq!(texts, vec![&json!("first"), &json!("second"), &json!("third")]);
587 }
588
589 #[test]
590 fn routing_observations_fold_as_a_multiset() {
591 let mut dev = DeviceLog::new("dev-a");
596 let ops = vec![
597 dev.append(Scope::Personal, Surface::Routing, json!({"sample": 1.0})),
598 dev.append(Scope::Personal, Surface::Routing, json!({"sample": 1.0})),
599 ];
600 let state = fold(&ops);
601 assert_eq!(
602 state.log_entries(&Surface::Routing.tag()).len(),
603 2,
604 "two byte-identical observations are two events"
605 );
606 let ema = |s: f64, rec: &FoldedRecord| 0.7 * s + 0.3 * rec.payload["sample"].as_f64().unwrap();
607 let value = state.replay(&Surface::Routing.tag(), 0.5_f64, ema);
608 assert!((value - 0.755).abs() < 1e-12, "EMA over both events: got {value}");
609 }
610
611 #[test]
612 fn logical_entity_surfaces_dedup_identical_content() {
613 let mut a = DeviceLog::new("a");
618 let mut b = DeviceLog::new("b");
619 let fact = json!({"kind": "note", "body": "the sky is blue"});
620 let oa = a.append(Scope::Personal, Surface::Knowledge, fact.clone());
621 let ob = b.append(Scope::Personal, Surface::Knowledge, fact);
622 let state = fold(&[oa, ob]);
623 assert_eq!(state.log_entries(&Surface::Knowledge.tag()).len(), 1);
624 }
625
626 #[test]
627 fn forged_colliding_op_id_dedups_order_independently() {
628 let mut dev = DeviceLog::new("d1");
632 let genuine = dev.append(Scope::Personal, Surface::Knowledge, json!({"id": "f", "v": 1}));
633 let mut forged = genuine.clone();
634 forged.payload = json!({"id": "f", "v": 2}); assert!(crate::oplog::verify_log(&[forged.clone()]).is_err());
636
637 let ab = fold(&[genuine.clone(), forged.clone()]);
638 let ba = fold(&[forged, genuine]);
639 assert_eq!(ab, ba, "colliding-id dedup must not depend on arrival order");
640 assert_eq!(state_hash(&ab), state_hash(&ba));
641 }
642
643 #[test]
644 fn fold_onto_prefix_fold_equals_full_fold() {
645 let mut a = DeviceLog::new("a");
650 let mut b = DeviceLog::new("b");
651 let prefix = vec![
652 a.append(Scope::Personal, Surface::Knowledge, json!({"id": "f", "v": "old"})),
653 a.append(Scope::Personal, Surface::Declagent, json!({"id": "x", "owner": "a"})),
654 a.append(Scope::Personal, Surface::Routing, json!({"sample": 1.0})),
655 ];
656 for op in &prefix {
657 b.observe(&op.hlc);
658 }
659 let suffix = vec![
660 b.append(Scope::Personal, Surface::Knowledge, json!({"id": "f", "v": "new"})),
662 b.append(Scope::Personal, Surface::Declagent, json!({"id": "x", "owner": "b"})),
664 b.append(Scope::Personal, Surface::Routing, json!({"sample": 0.0})),
666 ];
667 let mut full = prefix.clone();
668 full.extend(suffix.iter().cloned());
669
670 let via_base = fold_onto(&fold(&prefix), &suffix);
671 assert_eq!(via_base, fold(&full));
672 assert_eq!(state_hash(&via_base), state_hash(&fold(&full)));
673 assert_eq!(via_base.logs[&Surface::Knowledge.tag()]["id:f"].payload["v"], json!("old"));
674 assert_eq!(
675 via_base.registries[&Surface::Declagent.tag()]["id:x"].payload["owner"],
676 json!("b")
677 );
678 assert_eq!(via_base.log_entries(&Surface::Routing.tag()).len(), 2);
679
680 assert_eq!(fold_onto(&via_base, &prefix), via_base);
683 }
684
685 #[test]
686 fn empty_fold_is_empty_and_stable() {
687 let state = fold(&[]);
688 assert_eq!(state, SyncState::default());
689 assert_eq!(state_hash(&state), state_hash(&fold(&[])));
690 assert!(state.log_entries("conversation").is_empty());
691 assert!(registry_as_lww(&state, "declagent").is_empty());
692 assert!(state.intent("milo", "R").is_none());
693 assert!(state.fencing_epoch("milo").is_none());
694 }
695
696 #[test]
701 fn intent_fold_fences_stale_epoch_order_independently() {
702 let mut a = DeviceLog::new("dev-a");
707 let mut b = DeviceLog::new("dev-b");
708 let ops = vec![
709 intent_op(&mut a, "milo", "R", 1, IntentStatus::Pending),
710 intent_op(&mut a, "milo", "R", 1, IntentStatus::Committed),
711 intent_op(&mut b, "milo", "R", 2, IntentStatus::Pending),
712 intent_op(&mut b, "milo", "R", 2, IntentStatus::Committed),
713 ];
714 let b_commit = ops[3].clone();
715
716 let baseline = fold(&ops);
717 assert_eq!(baseline.fencing_epoch("milo"), Some(2));
718 assert_eq!(baseline.intents["milo"].runs.len(), 1, "single record — no double-commit");
719 let winner = baseline.intent("milo", "R").expect("R survives");
720 let decoded = Intent::from_payload(&winner.payload).unwrap();
721 assert_eq!((decoded.epoch, decoded.status), (2, IntentStatus::Committed));
722 assert_eq!(winner.op_id, b_commit.op_id, "the current holder's commit wins");
723
724 for order in [
726 vec![ops[3].clone(), ops[2].clone(), ops[1].clone(), ops[0].clone()],
727 vec![ops[2].clone(), ops[0].clone(), ops[3].clone(), ops[1].clone()],
728 vec![ops[1].clone(), ops[3].clone(), ops[0].clone(), ops[2].clone()],
729 ] {
730 assert_eq!(fold(&order), baseline);
731 assert_eq!(state_hash(&fold(&order)), state_hash(&baseline));
732 }
733 }
734
735 #[test]
736 fn intent_per_agent_pending_fencing_with_committed_immunity() {
737 let mut a = DeviceLog::new("dev-a");
744 let mut b = DeviceLog::new("dev-b");
745 let ops = vec![
746 intent_op(&mut a, "milo", "S", 1, IntentStatus::Pending), intent_op(&mut a, "milo", "K", 1, IntentStatus::Committed), intent_op(&mut b, "milo", "T", 2, IntentStatus::Committed), ];
750 let state = fold(&ops);
751 assert_eq!(state.fencing_epoch("milo"), Some(2));
752 assert!(state.intent("milo", "S").is_none(), "unshared zombie pending is fenced");
754 assert!(state.committed_run("milo", "S").is_none(), "S never committed");
755 assert!(
757 state.committed_run("milo", "K").is_some(),
758 "committed run survives an unrelated epoch bump (idempotency oracle)"
759 );
760 assert!(state.intent("milo", "K").is_some(), "committed is terminal-immune in runs too");
761 assert!(state.committed_run("milo", "T").is_some());
762 let mut c = DeviceLog::new("dev-c");
764 let mixed = {
765 let mut v = ops.clone();
766 v.push(intent_op(&mut c, "other", "U", 1, IntentStatus::Committed));
767 v
768 };
769 assert!(
770 fold(&mixed).committed_run("other", "U").is_some(),
771 "fencing does not cross agents"
772 );
773 }
774
775 #[test]
776 fn intent_fold_onto_equals_full_fold_across_an_epoch_bump() {
777 let mut a = DeviceLog::new("dev-a");
784 let mut b = DeviceLog::new("dev-b");
785 let prefix = vec![intent_op(&mut a, "milo", "R", 1, IntentStatus::Committed)];
786 for op in &prefix {
787 b.observe(&op.hlc);
788 }
789 let tail = vec![intent_op(&mut b, "milo", "S", 2, IntentStatus::Committed)];
790 let full = {
791 let mut v = prefix.clone();
792 v.extend(tail.iter().cloned());
793 v
794 };
795
796 let base = fold(&prefix); assert_eq!(base.fencing_epoch("milo"), Some(1));
798 assert!(base.committed_run("milo", "R").is_some());
799
800 let via_base = fold_onto(&base, &tail);
801 assert_eq!(via_base, fold(&full), "fold_onto == fold across the epoch bump");
802 assert_eq!(state_hash(&via_base), state_hash(&fold(&full)));
803 assert_eq!(via_base.fencing_epoch("milo"), Some(2));
804 assert!(
806 via_base.committed_run("milo", "R").is_some(),
807 "committed R survives the epoch bump in the idempotency oracle"
808 );
809 assert!(via_base.intent("milo", "R").is_some(), "committed R is terminal-immune in runs");
810 assert!(via_base.committed_run("milo", "S").is_some());
811 assert_eq!(fold_onto(&via_base, &tail), via_base);
813 }
814
815 #[test]
816 fn intent_fencing_beats_a_later_hlc() {
817 let mut cloud = DeviceLog::new("cloud");
820 let mut zombie = DeviceLog::new("laptop");
821 let c = intent_op(&mut cloud, "milo", "R", 2, IntentStatus::Committed);
822 zombie.observe(&c.hlc); let z = intent_op(&mut zombie, "milo", "R", 1, IntentStatus::Committed);
824 assert!(z.hlc > c.hlc, "the zombie op is later in HLC");
825
826 let state = fold(&[c.clone(), z]);
827 let winner = state.intent("milo", "R").unwrap();
828 assert_eq!(winner.op_id, c.op_id, "higher epoch wins despite lower HLC");
829 assert_eq!(state.fencing_epoch("milo"), Some(2));
830 }
831
832 #[test]
833 fn idempotent_run_under_failover_uses_the_same_deterministic_run_id() {
834 let run_id = car_proto::deterministic_run_id("milo", "3am digest", "2026-07-02T03:00");
838 assert_eq!(
839 run_id,
840 car_proto::deterministic_run_id("milo", "3am digest", "2026-07-02T03:00"),
841 "same occurrence → same run_id"
842 );
843 let mut zombie = DeviceLog::new("laptop");
844 let mut cloud = DeviceLog::new("cloud");
845 let z = intent_op(&mut zombie, "milo", &run_id, 1, IntentStatus::Committed);
846 let c = intent_op(&mut cloud, "milo", &run_id, 2, IntentStatus::Committed);
847
848 let state = fold(&[z, c.clone()]);
849 assert_eq!(state.intents["milo"].runs.len(), 1, "exactly one execution record");
850 assert_eq!(
851 state.intent("milo", &run_id).unwrap().op_id,
852 c.op_id,
853 "the epoch-2 holder's run wins; the zombie is a no-op"
854 );
855 }
856
857 #[test]
858 fn intent_fold_is_order_independent_over_every_permutation() {
859 fn permutations<T: Clone>(items: &[T]) -> Vec<Vec<T>> {
863 fn heap<T: Clone>(k: usize, arr: &mut Vec<T>, out: &mut Vec<Vec<T>>) {
864 if k == 1 {
865 out.push(arr.clone());
866 return;
867 }
868 for i in 0..k {
869 heap(k - 1, arr, out);
870 if k.is_multiple_of(2) {
871 arr.swap(i, k - 1);
872 } else {
873 arr.swap(0, k - 1);
874 }
875 }
876 }
877 let mut arr = items.to_vec();
878 let mut out = Vec::new();
879 heap(arr.len(), &mut arr, &mut out);
880 out
881 }
882
883 let mut a = DeviceLog::new("a");
884 let mut b = DeviceLog::new("b");
885 let ops = vec![
886 intent_op(&mut a, "milo", "R", 1, IntentStatus::Committed), intent_op(&mut a, "milo", "S", 1, IntentStatus::Pending), intent_op(&mut a, "milo", "T", 1, IntentStatus::Committed), intent_op(&mut b, "milo", "R", 2, IntentStatus::Pending), intent_op(&mut b, "milo", "S", 2, IntentStatus::Committed), ];
892 let baseline = fold(&ops);
893 assert_eq!(baseline.fencing_epoch("milo"), Some(2));
895 let mut committed = baseline.committed_run_ids("milo");
896 committed.sort();
897 assert_eq!(committed, vec!["R", "S", "T"], "the oracle keeps every committed run");
898 assert!(baseline.intent("milo", "S").map(|r| r.op_id.clone()).is_some_and(|_| {
899 Intent::from_payload(&baseline.intent("milo", "S").unwrap().payload).unwrap().status
900 == IntentStatus::Committed
901 }));
902 assert_eq!(
903 Intent::from_payload(&baseline.intent("milo", "R").unwrap().payload).unwrap().status,
904 IntentStatus::Committed,
905 "R is not reverted to pending"
906 );
907
908 for perm in permutations(&ops) {
909 assert_eq!(fold(&perm), baseline, "leased fold must be order-independent");
910 assert_eq!(state_hash(&fold(&perm)), state_hash(&baseline));
911 }
912 }
913
914 #[test]
915 fn c1_committed_run_survives_an_unrelated_higher_epoch_run() {
916 let mut a = DeviceLog::new("dev-a");
922 let mut b = DeviceLog::new("dev-b");
923 let r_commit = intent_op(&mut a, "milo", "R", 1, IntentStatus::Committed);
924 b.observe(&r_commit.hlc);
925 let t_pending = intent_op(&mut b, "milo", "T", 2, IntentStatus::Pending);
926
927 let state = fold(&[r_commit.clone(), t_pending]);
928 assert_eq!(state.fencing_epoch("milo"), Some(2), "the unrelated run bumped the fence");
929 assert_eq!(
931 state.committed_run("milo", "R").unwrap().op_id,
932 r_commit.op_id,
933 "committed_run(R) survives the unrelated epoch bump (C1 fixed)"
934 );
935 assert_eq!(state.committed_run_ids("milo"), vec!["R"]);
936 }
937
938 #[test]
939 fn c3_committed_then_pending_across_a_bump_stays_committed() {
940 let mut orig = DeviceLog::new("orig");
946 let mut failover = DeviceLog::new("failover");
947 let committed = intent_op(&mut orig, "milo", "R", 1, IntentStatus::Committed);
948 failover.observe(&committed.hlc);
949 let late_pending = intent_op(&mut failover, "milo", "R", 2, IntentStatus::Pending);
950 assert!(late_pending.hlc > committed.hlc, "the pending is even later in HLC");
951
952 for order in [
953 vec![committed.clone(), late_pending.clone()],
954 vec![late_pending.clone(), committed.clone()],
955 ] {
956 let state = fold(&order);
957 assert_eq!(
959 state.committed_run("milo", "R").unwrap().op_id,
960 committed.op_id,
961 "committed stays committed across the bump (oracle)"
962 );
963 let decoded = Intent::from_payload(&state.intent("milo", "R").unwrap().payload).unwrap();
965 assert_eq!(decoded.status, IntentStatus::Committed, "runs view is not reverted to pending");
966 }
967 }
968}