fix: preserve review comments across unrelated edits

This commit is contained in:
Valdemar Lorenzen
2026-10-20 14:58:56 +02:00
parent 2727425518
commit 31203889bb
10 changed files with 422 additions and 147 deletions
+131 -28
View File
@@ -67,13 +67,18 @@ fn database_restarts_preserve_identity_and_exact_stale_anchors() {
assert_eq!(loaded.comments, review.comments);
assert_eq!(loaded.reviewed_files, review.reviewed_files);
assert_eq!(loaded.verdict, review.verdict);
assert!(!loaded.comments[0].anchor.matches_lines(
"working:new-snapshot",
Path::new("日本語.rs"),
Side::Before,
&["changed".into()],
true,
));
assert!(
loaded.comments[0]
.anchor
.mapped_range(
"working:new-snapshot",
Path::new("日本語.rs"),
Side::Before,
&["changed".into()],
true,
)
.is_none()
);
let path = root.join("export.json");
loaded.save(&path).unwrap();
let exported = fs::read(&path).unwrap();
@@ -387,6 +392,92 @@ fn default_directory_uses_absolute_xdg_then_absolute_home() {
assert!(resolve(None, Some("")).is_err());
}
#[test]
fn anchor_survives_unrelated_edit() {
let anchor = Anchor {
revision: "working:old".into(),
path: "source.rs".into(),
side: Side::After,
start: crate::plugin::TextPosition { row: 1, column: 0 },
end: crate::plugin::TextPosition { row: 1, column: 6 },
content: "before\ntarget\nafter\n".into(),
};
let map = |text: &str| {
anchor.mapped_range(
"working:new",
Path::new("source.rs"),
Side::After,
&text
.trim_end_matches('\n')
.split('\n')
.map(str::to_owned)
.collect::<Vec<_>>(),
text.ends_with('\n'),
)
};
assert_eq!(
map("changed\ntarget\nafter\n"),
Some(anchor.start..anchor.end)
);
assert_eq!(
map("inserted\nbefore\ntarget\nafter\n"),
Some(
crate::plugin::TextPosition { row: 2, column: 0 }..crate::plugin::TextPosition {
row: 2,
column: 6
}
)
);
assert!(map("before\ntargEt\nafter\n").is_none());
assert!(map("before\nafter\n").is_none());
assert!(map("before\ntarget\nafter\nbefore\ntarget\nafter\n").is_none());
assert_eq!(map("target\nbefore\ntarget\nafter\n").unwrap().start.row, 2);
assert_eq!(anchor.content, "before\ntarget\nafter\n");
let partial = Anchor {
content: "header\r\nprefix 👩‍💻e\u{301} suffix\r\nfooter\r\n".into(),
start: crate::plugin::TextPosition { row: 1, column: 7 },
end: crate::plugin::TextPosition { row: 1, column: 9 },
..anchor.clone()
};
let moved = partial
.mapped_range(
"working:new",
Path::new("source.rs"),
Side::After,
&[
"inserted\r".into(),
"header\r".into(),
"longer prefix 👩‍💻e\u{301} suffix\r".into(),
"footer\r".into(),
],
true,
)
.unwrap();
assert_eq!(
moved.start,
crate::plugin::TextPosition { row: 2, column: 14 }
);
assert_eq!(
moved.end,
crate::plugin::TextPosition { row: 2, column: 16 }
);
assert!(
partial
.mapped_range(
"other-commit",
Path::new("source.rs"),
Side::After,
&[
"header\r".into(),
"prefix 👩‍💻e\u{301} suffix\r".into(),
"footer\r".into()
],
true
)
.is_none()
);
}
#[test]
fn persistence_validates_anchors_and_preserves_files_on_failure() {
let directory = directory();
@@ -413,27 +504,39 @@ fn persistence_validates_anchors_and_preserves_files_on_failure() {
assert_eq!(loaded.comments, review.comments);
assert_eq!(loaded.verdict, review.verdict);
let anchor = &loaded.comments[0].anchor;
assert!(!anchor.matches_lines(
"working:other",
Path::new("source.rs"),
Side::After,
&["first".into(), "last".into()],
true
));
assert!(anchor.matches_lines(
"working:immutable",
Path::new("source.rs"),
Side::After,
&["first".into(), "last".into()],
true
));
assert!(!anchor.matches_lines(
"working:immutable",
Path::new("source.rs"),
Side::After,
&["first".into(), "last".into()],
false
));
assert!(
anchor
.mapped_range(
"working:other",
Path::new("source.rs"),
Side::After,
&["first".into(), "last".into()],
true
)
.is_some()
);
assert!(
anchor
.mapped_range(
"working:immutable",
Path::new("source.rs"),
Side::After,
&["first".into(), "last".into()],
true
)
.is_some()
);
assert!(
anchor
.mapped_range(
"working:immutable",
Path::new("source.rs"),
Side::After,
&["first".into(), "last".into()],
false
)
.is_none()
);
loaded.summary = "Updated".into();
loaded.save(&path).unwrap();
assert_eq!(Review::load(&path, &root).unwrap().summary, "Updated");