Single-commit clean baseline after security scrub of niche-tells, project codenames, internal jargon, and contributor-email leaks. Contents: - 100 Rust crates (_primitives/_rust/) - 37 agent manifests (_manifests/) + generated specs (_generated/) - 67 user-invocable skills (skills/) - 33 hooks (hooks/) - Composition blocks (_blocks/) - Documentation (docs/, README.md) - TS adapter packages (_ts_packages/) - Assembler (_assembler/) - Roles (_roles/) - Templates (_templates/) - Forgejo CI (.forgejo/) Author: Denis Parfionovich <info@greendragon.info> License: see LICENSE.
278 lines
7.3 KiB
Rust
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}));
|
|
}
|