KeiSeiKit-1.0/_primitives/_rust/kei-diff/tests/round_trip.rs
Parfii-bot d521e7d89a feat(wave13): kei-diff + kei-scheduler + kei-watch primitives
3 new primitives, registered in workspace members (39 crates total):

- kei-diff (9 files, 27 tests): Structural JSON diff RFC 6902 subset
  (add/remove/replace). Pure computation. Index-based array diff
  (not LCS) matches drift-detection semantics. Round-trip property
  verified on 15+ fixtures. Zero sibling deps — pure utility.

- kei-scheduler (12 files, 16 tests): Durable task scheduler (cron /
  at / interval) primitive. Engine-native (SCHEDULER_SCHEMA on
  kei-entity-store). Name-unique via custom migration. compute_next
  pure fn + CLI tick for external executor.

- kei-watch (12 files, 30 tests): Filesystem watcher thin wrapper
  around notify 8.x. Sync API (no tokio). 50ms debounce. Cross-platform
  rename handling (macOS Modify(Name(Both)) vs Linux From/To pair).

All crate-local [workspace] tables removed. Registered in
_primitives/_rust/Cargo.toml. cargo check --workspace clean.

Constructor Pattern: all source files <=200 LOC, all functions <=30 LOC.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 15:51:42 +08:00

278 lines
7.3 KiB
Rust

//! Integration tests for kei-diff.
//!
//! Core property: `apply(old, diff(old, new)) == new` for every fixture.
//! Plus edge cases on pointer escaping, array edits, apply errors, and
//! the RFC 6902 wire format.
use kei_diff::{apply, diff, ApplyError, Op, Patch};
use serde_json::{json, Value};
fn rt(old: Value, new: Value) {
let patch = diff(&old, &new);
let applied = apply(&old, &patch).expect("apply failed");
assert_eq!(
applied, new,
"round-trip failed\n old = {old}\n new = {new}\n patch = {}",
serde_json::to_string(&patch).unwrap()
);
}
#[test]
fn equal_values_produce_empty_patch() {
let patch = diff(&json!({"a": 1}), &json!({"a": 1}));
assert!(patch.is_empty());
rt(json!({"a": 1}), json!({"a": 1}));
}
#[test]
fn scalar_replace() {
rt(json!(1), json!(2));
rt(json!("a"), json!("b"));
rt(json!(true), json!(false));
}
#[test]
fn type_change_emits_replace() {
let old = json!("hello");
let new = json!(42);
let patch = diff(&old, &new);
assert_eq!(patch.len(), 1);
assert!(matches!(patch.0[0], Op::Replace { .. }));
rt(old, new);
}
#[test]
fn object_add_key() {
let old = json!({"a": 1});
let new = json!({"a": 1, "b": 2});
let patch = diff(&old, &new);
assert_eq!(patch.len(), 1);
assert!(matches!(&patch.0[0], Op::Add { path, .. } if path == "/b"));
rt(old, new);
}
#[test]
fn object_remove_key() {
let old = json!({"a": 1, "b": 2});
let new = json!({"a": 1});
let patch = diff(&old, &new);
assert_eq!(patch.len(), 1);
assert!(matches!(&patch.0[0], Op::Remove { path } if path == "/b"));
rt(old, new);
}
#[test]
fn object_replace_value() {
let old = json!({"a": 1});
let new = json!({"a": 2});
let patch = diff(&old, &new);
assert_eq!(patch.len(), 1);
assert!(matches!(&patch.0[0], Op::Replace { path, .. } if path == "/a"));
rt(old, new);
}
#[test]
fn nested_object_replace() {
let old = json!({"a": {"b": {"c": 1}}});
let new = json!({"a": {"b": {"c": 2}}});
let patch = diff(&old, &new);
assert_eq!(patch.len(), 1);
assert!(matches!(&patch.0[0], Op::Replace { path, .. } if path == "/a/b/c"));
rt(old, new);
}
#[test]
fn array_append() {
let old = json!([1, 2]);
let new = json!([1, 2, 3, 4]);
let patch = diff(&old, &new);
assert_eq!(patch.len(), 2);
assert!(matches!(&patch.0[0], Op::Add { path, .. } if path == "/2"));
assert!(matches!(&patch.0[1], Op::Add { path, .. } if path == "/3"));
rt(old, new);
}
#[test]
fn array_truncate() {
let old = json!([1, 2, 3, 4]);
let new = json!([1, 2]);
let patch = diff(&old, &new);
// Expect removals highest-first (/3 then /2) so indices stay valid.
assert_eq!(patch.len(), 2);
assert!(matches!(&patch.0[0], Op::Remove { path } if path == "/3"));
assert!(matches!(&patch.0[1], Op::Remove { path } if path == "/2"));
rt(old, new);
}
#[test]
fn array_element_replace() {
let old = json!([1, 2, 3]);
let new = json!([1, 99, 3]);
let patch = diff(&old, &new);
assert_eq!(patch.len(), 1);
assert!(matches!(&patch.0[0], Op::Replace { path, .. } if path == "/1"));
rt(old, new);
}
#[test]
fn nested_array_inside_object() {
let old = json!({"xs": [1, 2, 3], "y": "z"});
let new = json!({"xs": [1, 7, 3, 4], "y": "z"});
rt(old, new);
}
#[test]
fn deeply_nested_mixed() {
let old = json!({
"meta": {"ts": 100, "tags": ["a", "b"]},
"items": [{"id": 1}, {"id": 2}],
});
let new = json!({
"meta": {"ts": 200, "tags": ["a", "b", "c"]},
"items": [{"id": 1}, {"id": 3}, {"id": 4}],
"extra": true,
});
rt(old, new);
}
#[test]
fn pointer_escapes_slash_and_tilde() {
let old = json!({"a/b": 1, "c~d": 2});
let new = json!({"a/b": 9, "c~d": 2});
let patch = diff(&old, &new);
assert_eq!(patch.len(), 1);
let expected_path = "/a~1b";
match &patch.0[0] {
Op::Replace { path, .. } => assert_eq!(path, expected_path),
other => panic!("expected Replace, got {other:?}"),
}
rt(old, new);
let old2 = json!({"c~d": 1});
let new2 = json!({"c~d": 2});
let p2 = diff(&old2, &new2);
match &p2.0[0] {
Op::Replace { path, .. } => assert_eq!(path, "/c~0d"),
other => panic!("expected Replace, got {other:?}"),
}
rt(old2, new2);
}
#[test]
fn apply_missing_path_errors() {
let doc = json!({"a": 1});
let patch = Patch(vec![Op::Replace {
path: "/nope".into(),
value: json!(9),
}]);
let err = apply(&doc, &patch).unwrap_err();
assert!(matches!(err, ApplyError::MissingTarget(_)));
}
#[test]
fn apply_remove_missing_errors() {
let doc = json!({"a": 1});
let patch = Patch(vec![Op::Remove { path: "/ghost".into() }]);
assert!(matches!(
apply(&doc, &patch).unwrap_err(),
ApplyError::MissingTarget(_)
));
}
#[test]
fn apply_replace_root() {
let doc = json!({"a": 1});
let patch = Patch(vec![Op::Replace {
path: "".into(),
value: json!([1, 2, 3]),
}]);
let out = apply(&doc, &patch).unwrap();
assert_eq!(out, json!([1, 2, 3]));
}
#[test]
fn empty_patch_is_identity() {
let doc = json!({"a": [1, 2, {"b": true}]});
let out = apply(&doc, &Patch::new()).unwrap();
assert_eq!(out, doc);
}
#[test]
fn apply_add_on_root_errors() {
let doc = json!({"a": 1});
let patch = Patch(vec![Op::Add {
path: "".into(),
value: json!(2),
}]);
assert!(matches!(
apply(&doc, &patch).unwrap_err(),
ApplyError::CannotAddToRoot
));
}
#[test]
fn apply_remove_root_errors() {
let doc = json!([1, 2]);
let patch = Patch(vec![Op::Remove { path: "".into() }]);
assert!(matches!(
apply(&doc, &patch).unwrap_err(),
ApplyError::CannotRemoveRoot
));
}
#[test]
fn wire_format_matches_rfc_6902() {
let patch = Patch(vec![
Op::Add {
path: "/x".into(),
value: json!(1),
},
Op::Remove { path: "/y".into() },
Op::Replace {
path: "/z".into(),
value: json!("hi"),
},
]);
let txt = serde_json::to_string(&patch).unwrap();
let parsed: Value = serde_json::from_str(&txt).unwrap();
assert_eq!(
parsed,
json!([
{"op": "add", "path": "/x", "value": 1},
{"op": "remove", "path": "/y"},
{"op": "replace", "path": "/z", "value": "hi"},
])
);
}
#[test]
fn patch_roundtrip_through_serde() {
let p1 = Patch(vec![
Op::Add {
path: "/a".into(),
value: json!({"nested": [1, 2]}),
},
Op::Remove { path: "/b/0".into() },
]);
let txt = serde_json::to_string(&p1).unwrap();
let p2: Patch = serde_json::from_str(&txt).unwrap();
assert_eq!(p1, p2);
}
#[test]
fn array_of_objects_element_replace() {
let old = json!([{"id": 1, "v": "a"}, {"id": 2, "v": "b"}]);
let new = json!([{"id": 1, "v": "a"}, {"id": 2, "v": "z"}]);
let patch = diff(&old, &new);
// Should be a single deep replace at /1/v
assert_eq!(patch.len(), 1);
assert!(matches!(&patch.0[0], Op::Replace { path, .. } if path == "/1/v"));
rt(old, new);
}
#[test]
fn null_to_value_is_replace() {
rt(json!(null), json!({"x": 1}));
rt(json!({"x": null}), json!({"x": 1}));
rt(json!({"x": 1}), json!({"x": null}));
}