feat: scope local reviews to comparisons

This commit is contained in:
2026-10-25 01:00:58 +02:00
parent 815fcccbe8
commit 448c3bf477
15 changed files with 760 additions and 72 deletions
+22 -2
View File
@@ -154,6 +154,7 @@ fn cli_resolves_only_the_requested_comment_and_preserves_original_feedback() {
let storage = root.join("reviews");
let mut store = Store::open(&storage).unwrap();
let mut review = Review::new(root.join("repo"));
review.comparison = Some(["frozen-base".into(), "working:frozen-tip".into()]);
review.summary = "Keep summary".into();
review.verdict = Some(Verdict::ChangesRequested);
review.comments = [u64::MAX, 2, 3]
@@ -176,6 +177,11 @@ fn cli_resolves_only_the_requested_comment_and_preserves_original_feedback() {
let mut other = Review::new(root.join("other"));
other.comments = review.comments.clone();
store.save(&mut other).unwrap();
let mut source = Review::new(review.root.clone());
store.save(&mut source).unwrap();
let mut reversed = Review::new(review.root.clone());
reversed.comparison = Some(["working:frozen-tip".into(), "frozen-base".into()]);
store.save(&mut reversed).unwrap();
let id = review.storage_id.unwrap();
let id_text = id.to_string();
let cli = |repo: &str, args: &[&str]| {
@@ -214,6 +220,7 @@ fn cli_resolves_only_the_requested_comment_and_preserves_original_feedback() {
let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert_eq!(json["id"], id);
assert_eq!(json["revision"], review.storage_revision + 1);
assert_eq!(json["comparison"], serde_json::json!(review.comparison));
assert_eq!(
json["counts"],
serde_json::json!({"total": 3, "resolved": 2, "unresolved": 1})
@@ -227,6 +234,7 @@ fn cli_resolves_only_the_requested_comment_and_preserves_original_feedback() {
assert_eq!(loaded.summary, review.summary);
assert_eq!(loaded.verdict, review.verdict);
assert_eq!(loaded.reviewed_files, review.reviewed_files);
assert_eq!(loaded.comparison, review.comparison);
assert!(store.save(&mut review).is_err()); // Stale editors cannot undo resolution.
assert_eq!(store.load(&review.root, id).unwrap().comments, expected);
@@ -264,8 +272,20 @@ fn cli_resolves_only_the_requested_comment_and_preserves_original_feedback() {
let output = cli("repo", &["list", "--json"]);
assert!(output.status.success(), "{:?}", output);
let list: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert_eq!(list["reviews"].as_array().unwrap().len(), 1);
assert_eq!(list["reviews"][0]["counts"], json["counts"]);
assert_eq!(list["reviews"].as_array().unwrap().len(), 3);
assert_eq!(
list["reviews"][0]["comparison"],
serde_json::json!(reversed.comparison)
);
assert!(list["reviews"][1]["comparison"].is_null());
assert_eq!(list["reviews"][2]["comparison"], json["comparison"]);
assert_eq!(list["reviews"][2]["counts"], json["counts"]);
let output = cli("repo", &["list"]);
assert!(output.status.success(), "{:?}", output);
let text = String::from_utf8(output.stdout).unwrap();
assert!(text.contains("Scope: source"));
assert!(text.contains("Scope: comparison frozen-base → working:frozen-tip"));
assert!(text.contains("Scope: comparison working:frozen-tip → frozen-base"));
let output = cli("repo", &["resolve", &id_text, "2"]);
assert!(output.status.success(), "{:?}", output);
let text = String::from_utf8(output.stdout).unwrap();
+39 -5
View File
@@ -2370,6 +2370,7 @@ fn live_state(before: Vec<String>, after: Vec<String>, eof: bool) -> DiffState {
loaded: true,
}],
});
state.review_comparison = Some(["base".into(), "working:snapshot".into()]);
state.files = Some(FilePicker::from_paths(
PathBuf::new(),
[(0, PathBuf::from("file"))],
@@ -2764,7 +2765,33 @@ fn restored_review_marks_use_file_identities_and_rename_paths_not_catalog_order(
}
#[test]
fn unchanged_file_review_survives_other_files_changing() {
fn review_marks_cannot_cross_comparison_scopes_even_with_matching_file_identities() {
let mut review = crate::review::ReviewState::default();
let mut state = DiffState::new();
let mut first = comparison();
first.files[0].review_revisions = Some(["file-before".into(), "file-after".into()]);
state.request_kind = Some(RequestKind::Comparison);
state.apply_result(1, Ok(Outcome::Comparison(first)));
state.toggle_reviewed(&mut review).unwrap();
let initial_scope = review.document.comparison.clone();
let mut second = comparison();
second.files[0].review_revisions = Some(["file-before".into(), "file-after".into()]);
second.revisions[1] = "another-head".into();
state.request_kind = Some(RequestKind::Comparison);
state.apply_result(2, Ok(Outcome::Comparison(second)));
state.restore_reviewed_files(&review);
assert!(state.reviewed_files.is_empty());
assert_eq!(
state.toggle_reviewed(&mut review),
Err("Select the review for this comparison first")
);
assert!(state.reviewed_files.is_empty());
assert_eq!(review.document.comparison, initial_scope);
assert_eq!(review.document.reviewed_files.len(), 1);
}
#[test]
fn working_refresh_retains_scope_and_unchanged_file_progress() {
let mut review = crate::review::ReviewState::default();
use crate::repository::Repository;
use std::sync::{Arc, atomic::AtomicBool};
@@ -2825,20 +2852,27 @@ fn unchanged_file_review_survives_other_files_changing() {
.compare(2, &before, &after, ComparisonMode::Snapshots, &[])
.unwrap();
assert_ne!(changes.revisions, original_revisions);
state.request_kind = Some(RequestKind::Comparison);
state.apply_result(2, Ok(Outcome::Comparison(changes)));
state.after = Some(after);
state.request_kind = None;
state.pending = None;
state.loading = false;
state.queue_checkout_refresh(true);
state.take_request();
assert!(state.apply_result(2, Ok(Outcome::CheckoutRefresh(Some(changes)))));
assert_eq!(state.review_comparison, Some(original_revisions));
review.document = store.load(&root, id).unwrap();
state.restore_reviewed_files(&review);
assert_eq!(state.reviewed_files, BTreeSet::from([0]));
state.load_file(0);
let detail = repository.load_file(2, 0).unwrap();
state.apply_result(
assert!(state.apply_result(
3,
Ok(Outcome::File {
comparison_id: 2,
file_index: 0,
file: detail,
}),
);
));
state.restore_reviewed_files(&review);
assert_eq!(state.reviewed_files, BTreeSet::from([0]));
std::fs::remove_dir_all(root).unwrap();
+323 -2
View File
@@ -3017,6 +3017,15 @@ fn diff_preserves_parked_buffers_visual_carets_and_insert_history() {
}
fn working_diff(editor: &mut Editor, root: &Path, paths: &[&str]) -> Box<[(PathBuf, String)]> {
working_diff_with_revisions(editor, root, paths, ["base", "working-snapshot"])
}
fn working_diff_with_revisions(
editor: &mut Editor,
root: &Path,
paths: &[&str],
revisions: [&str; 2],
) -> Box<[(PathBuf, String)]> {
use crate::diff::{Catalog, Category, ChangedFile, Comparison, FileStatus, RepoKind, Revision};
editor.execute("diff");
let (id, _) = editor.take_diff_request().unwrap();
@@ -3070,7 +3079,7 @@ fn working_diff(editor: &mut Editor, root: &Path, paths: &[&str]) -> Box<[(PathB
Ok(Outcome::Comparison(Comparison {
blame: Default::default(),
root: root.canonicalize().unwrap(),
revisions: ["base".into(), "working-snapshot".into()],
revisions: revisions.map(str::to_owned),
before: "base".into(),
after: "Working".into(),
editable_paths: paths.iter().map(|path| PathBuf::from(*path)).collect(),
@@ -3081,6 +3090,16 @@ fn working_diff(editor: &mut Editor, root: &Path, paths: &[&str]) -> Box<[(PathB
}
fn historical_diff(editor: &mut Editor, root: &Path, before: Vec<String>, after: Vec<String>) {
historical_diff_with_revisions(editor, root, before, after, ["base", "head"]);
}
fn historical_diff_with_revisions(
editor: &mut Editor,
root: &Path,
before: Vec<String>,
after: Vec<String>,
revisions: [&str; 2],
) {
use crate::diff::{Catalog, Category, ChangedFile, Comparison, FileStatus, RepoKind, Revision};
editor.execute("diff");
let (id, _) = editor.take_diff_request().unwrap();
@@ -3108,7 +3127,7 @@ fn historical_diff(editor: &mut Editor, root: &Path, before: Vec<String>, after:
Ok(Outcome::Comparison(Comparison {
blame: Default::default(),
root: root.to_owned(),
revisions: ["base".into(), "head".into()],
revisions: revisions.map(str::to_owned),
before: "base".into(),
after: "head".into(),
editable_paths: Default::default(),
@@ -3154,6 +3173,305 @@ fn source_review_editor(directory: &Directory, text: &str) -> (Editor, PathBuf,
(editor, root, path)
}
#[test]
fn comparison_reviews_use_ordered_resolved_identities_and_resume_only_the_same_pair() {
use crate::review::{Store, Verdict};
let directory = Directory::new();
let (mut editor, root, _) = source_review_editor(&directory, "source\n");
historical_diff(&mut editor, &root, vec!["old".into()], vec!["new".into()]);
editor.execute("comment");
type_text(&mut editor, "iPair feedback");
editor.handle(Input::Escape);
editor.execute("w");
editor.handle(Input::Escape);
editor.execute("diff.review.toggle");
editor.execute("review.verdict looks_good");
let original = editor.local_review.document.clone();
editor.handle(Input::Escape);
for pair in [["other", "head"], ["base", "other"], ["head", "base"]] {
historical_diff_with_revisions(
&mut editor,
&root,
vec!["old".into()],
vec!["new".into()],
pair,
);
assert_eq!(
editor.local_review.document.comparison,
Some(pair.map(str::to_owned))
);
assert!(editor.local_review.document.comments.is_empty());
assert!(editor.local_review.document.verdict.is_none());
assert!(editor.local_review.document.reviewed_files.is_empty());
assert!(editor.workspace_diff().unwrap().reviewed_files.is_empty());
editor.execute("review.verdict changes_requested");
assert_ne!(editor.local_review.document.storage_id, original.storage_id);
editor.handle(Input::Escape);
}
historical_diff(&mut editor, &root, vec!["old".into()], vec!["new".into()]);
assert_eq!(editor.local_review.document.storage_id, original.storage_id);
assert_eq!(
editor.local_review.document.comments[0].body,
"Pair feedback"
);
assert_eq!(
editor.local_review.document.verdict,
Some(Verdict::LooksGood)
);
assert_eq!(
editor.workspace_diff().unwrap().reviewed_files,
BTreeSet::from([0])
);
assert_eq!(
Store::open(&root.join("reviews"))
.unwrap()
.list(&root)
.unwrap()
.len(),
4
);
}
#[test]
fn source_and_comparison_reviews_remain_separate_in_shared_panes_and_tabs() {
let directory = Directory::new();
let (mut editor, root, _) = source_review_editor(&directory, "source\n");
let source = editor.pane_id;
editor.execute("comment");
type_text(&mut editor, "iSource feedback");
editor.handle(Input::Escape);
editor.execute("w");
let source_id = editor.local_review.document.storage_id;
editor.handle(Input::Escape);
editor.split_pane(SplitAxis::Vertical);
let comparison_pane = editor.pane_id;
historical_diff(&mut editor, &root, vec!["old".into()], vec!["new".into()]);
editor.execute("comment");
type_text(&mut editor, "iComparison feedback");
editor.handle(Input::Escape);
editor.execute("w");
let comparison_id = editor.local_review.document.storage_id;
assert_ne!(comparison_id, source_id);
editor.handle(Input::Escape);
editor.select_pane(source);
editor.execute("review.edit 1");
assert_eq!(editor.local_review.document.storage_id, source_id);
assert!(editor.local_review.document.comparison.is_none());
assert_eq!(editor.context().text, "Source feedback");
editor.handle(Input::Escape);
editor.select_pane(comparison_pane);
editor.execute("review.edit 1");
assert_eq!(editor.local_review.document.storage_id, comparison_id);
assert_eq!(editor.context().text, "Comparison feedback");
editor.handle(Input::Escape);
editor.select_pane(source);
editor.execute("tab.new");
editor.execute("review.edit 1");
assert_eq!(editor.local_review.document.storage_id, source_id);
assert_eq!(editor.context().text, "Source feedback");
editor.handle(Input::Escape);
historical_diff(&mut editor, &root, vec!["old".into()], vec!["new".into()]);
assert_eq!(editor.local_review.document.storage_id, comparison_id);
editor.execute("tab.previous");
editor.select_pane(comparison_pane);
editor.execute("review.edit 1");
assert_eq!(editor.context().text, "Comparison feedback");
}
#[test]
fn review_selectors_and_explicit_opens_reject_other_comparison_and_source_scopes() {
use crate::review::{Review, Store};
let directory = Directory::new();
let (mut editor, root, _) = source_review_editor(&directory, "source\n");
let mut store = Store::open(&root.join("reviews")).unwrap();
let mut source = Review::new(root.clone());
source.summary = "source-only".into();
store.save(&mut source).unwrap();
let mut other = Review::new(root.clone());
other.comparison = Some(["different".into(), "head".into()]);
other.summary = "other-pair-only".into();
store.save(&mut other).unwrap();
let other_json = root.join("other.json");
other.save(&other_json).unwrap();
let source_json = root.join("source.json");
source.save(&source_json).unwrap();
historical_diff(&mut editor, &root, vec!["old".into()], vec!["new".into()]);
editor.execute("review.verdict looks_good");
let current = editor.local_review.document.clone();
for command in [
format!("review.open {}", source.storage_id.unwrap()),
format!("review.open {}", other.storage_id.unwrap()),
format!("review.open {}", source_json.display()),
format!("review.open {}", other_json.display()),
] {
editor.execute(&command);
assert!(
editor
.message()
.contains("different comparison or source scope")
);
assert_eq!(editor.local_review.document.storage_id, current.storage_id);
assert_eq!(editor.local_review.document.comparison, current.comparison);
assert_eq!(editor.local_review.document.verdict, current.verdict);
}
editor.execute("review.new");
let second_id = editor.local_review.document.storage_id;
editor.execute("review.open");
assert!(!editor.context().text.contains("source-only"));
assert!(!editor.context().text.contains("other-pair-only"));
editor.handle(Input::Enter);
assert_eq!(editor.local_review.document.storage_id, second_id);
editor.execute(&format!("review.open {}", current.storage_id.unwrap()));
assert_eq!(editor.local_review.document.storage_id, current.storage_id);
}
#[test]
fn dirty_review_drafts_guard_scope_replacement_new_reviews_and_mode_changes() {
let directory = Directory::new();
let (mut editor, root, _) = source_review_editor(&directory, "source\n");
let source = editor.pane_id;
editor.split_pane(SplitAxis::Vertical);
let comparison_pane = editor.pane_id;
historical_diff(&mut editor, &root, vec!["old".into()], vec!["new".into()]);
editor.execute("comment");
type_text(&mut editor, "iUnsaved feedback");
editor.handle(Input::Escape);
let draft = editor.buffer_id();
let panel = editor.review_pane.unwrap();
let scope = editor.local_review.document.comparison.clone();
editor.execute("review.new");
assert!(editor.message().contains("Save the current local review"));
assert_eq!(editor.context().text, "Unsaved feedback");
editor.select_pane(source);
editor.execute("review.overview");
assert!(editor.message().contains("changing review scopes"));
assert_eq!(editor.local_review.document.comparison, scope);
editor.select_pane(comparison_pane);
let comparison = editor.workspace_diff().unwrap().comparison_id;
editor.act(Action::DiffModeToggle);
assert_eq!(editor.workspace_diff().unwrap().comparison_id, comparison);
assert!(editor.workspace_diff().unwrap().comparison.is_some());
editor.execute("diff");
assert_eq!(editor.workspace_diff().unwrap().comparison_id, comparison);
editor.select_pane(panel);
assert_eq!(editor.buffer_id(), draft);
assert_eq!(editor.context().text, "Unsaved feedback");
editor.execute("w");
editor.handle(Input::Escape);
editor.select_pane(source);
editor.execute("review.overview");
assert!(editor.local_review.document.comparison.is_none());
assert!(editor.local_review.document.comments.is_empty());
}
#[test]
fn working_refresh_keeps_review_scope_and_drafts_but_explicit_reopen_uses_new_identities() {
let directory = Directory::new();
let (mut editor, root, _) = source_review_editor(&directory, "source\n");
fs::write(root.join("file.txt"), "original\n").unwrap();
working_diff(&mut editor, &root, &["file.txt"]);
editor.handle(Input::Enter);
editor.execute("comment");
type_text(&mut editor, "iWorking feedback");
editor.handle(Input::Escape);
editor.execute("w");
editor.handle(Input::Escape);
let original_id = editor.local_review.document.storage_id;
let scope = editor.local_review.document.comparison.clone();
editor.execute("review.summary");
type_text(&mut editor, "iUnsaved working summary");
editor.handle(Input::Escape);
let panel = editor.review_pane.unwrap();
let owner = editor.diff_owner.unwrap();
editor.queue_checkout_refreshes(true);
let (_, request, _) = editor.take_next_diff_request().unwrap();
let mut replacement = editor.with_pane(owner, |editor| {
editor.diff.as_mut().unwrap().comparison.take().unwrap()
});
replacement.revisions[1] = "working:changed".into();
replacement.files[0].after = vec!["refreshed".into()];
assert!(editor.apply_diff_result(request, Ok(Outcome::CheckoutRefresh(Some(replacement)))));
assert_eq!(editor.local_review.document.comparison, scope);
assert_eq!(editor.local_review.document.storage_id, original_id);
assert_eq!(
editor.local_review.document.comments[0].body,
"Working feedback"
);
assert!(editor.current_review_dirty());
editor.select_pane(panel);
assert_eq!(editor.context().text, "Unsaved working summary");
editor.execute("w");
editor.handle(Input::Escape);
working_diff_with_revisions(
&mut editor,
&root,
&["file.txt"],
["base", "working:changed"],
);
assert!(editor.local_review.document.comments.is_empty());
assert!(editor.local_review.document.summary.is_empty());
editor.execute("review.verdict looks_good");
assert_ne!(editor.local_review.document.storage_id, original_id);
editor.handle(Input::Escape);
working_diff(&mut editor, &root, &["file.txt"]);
assert_eq!(editor.local_review.document.storage_id, original_id);
assert_eq!(
editor.local_review.document.summary,
"Unsaved working summary"
);
}
#[test]
fn moved_bookmark_selects_new_review_and_dirty_draft_blocks_endpoint_replacement() {
use crate::diff::Comparison;
let directory = Directory::new();
let (mut editor, root, _) = source_review_editor(&directory, "source\n");
historical_diff(&mut editor, &root, vec!["old".into()], vec!["new".into()]);
editor.execute("review.verdict looks_good");
let original_id = editor.local_review.document.storage_id;
editor.handle(Input::Escape);
editor.execute("review.summary");
type_text(&mut editor, "iBookmark draft");
editor.handle(Input::Escape);
let panel = editor.review_pane.unwrap();
let moved = || Comparison {
revisions: ["base".into(), "moved-head".into()],
root: root.clone(),
before: "base".into(),
after: "head".into(),
blame: Default::default(),
editable_paths: Default::default(),
files: Vec::new(),
};
editor.queue_checkout_refreshes(false);
let (_, request, _) = editor.take_next_diff_request().unwrap();
assert!(editor.apply_diff_result(request, Ok(Outcome::BookmarkRefresh(moved()))));
assert_eq!(editor.local_review.document.storage_id, original_id);
assert_eq!(
editor
.workspace_diff()
.unwrap()
.comparison
.as_ref()
.unwrap()
.revisions[1],
"head"
);
editor.select_pane(panel);
assert_eq!(editor.context().text, "Bookmark draft");
editor.execute("w");
editor.handle(Input::Escape);
editor.queue_checkout_refreshes(false);
let (_, request, _) = editor.take_next_diff_request().unwrap();
assert!(editor.apply_diff_result(request, Ok(Outcome::BookmarkRefresh(moved()))));
assert_eq!(
editor.local_review.document.comparison,
Some(["base".into(), "moved-head".into()])
);
assert!(editor.local_review.document.verdict.is_none());
assert!(editor.local_review.document.summary.is_empty());
}
#[test]
fn source_review_drafts_guard_source_pane_close_and_saved_panels_retarget() {
let directory = Directory::new();
@@ -3443,6 +3761,8 @@ fn review_navigation_maps_working_ranges_and_preserves_stale_snapshots() {
.unwrap()
.revisions[1] = "working:old".into();
editor.local_review.document = crate::review::Review::new(root);
editor.local_review.document.comparison =
editor.workspace_diff().unwrap().review_comparison.clone();
let anchor = Anchor {
revision: "working:old".into(),
path: "file.txt".into(),
@@ -7595,6 +7915,7 @@ mod system_clipboard {
let root = fixture.0.0.canonicalize().unwrap();
let storage = root.join("reviews");
let mut review = Review::new(root.clone());
review.comparison = Some(["base".into(), "head".into()]);
review.summary = "Review summary".into();
review.verdict = Some(Verdict::ChangesRequested);
review.comments = vec![
+145 -5
View File
@@ -151,7 +151,7 @@ fn database_restarts_preserve_identity_and_exact_stale_anchors() {
let exported = fs::read(&path).unwrap();
let json: serde_json::Value = serde_json::from_slice(&exported).unwrap();
assert_eq!(json["comments"][0]["resolved"], true);
assert_eq!(json["version"], 4);
assert_eq!(json["version"], 5);
assert_eq!(json["reviewed_files"][0]["after"], "live 👩‍🔬\r\n");
assert!(json["reviewed_files"][1]["after"].is_null());
assert_eq!(json["reviewed_files"][2]["after"], "");
@@ -189,6 +189,139 @@ fn database_restarts_preserve_identity_and_exact_stale_anchors() {
assert_eq!(loaded.verdict, verdict);
}
#[test]
fn comparison_scopes_roundtrip_in_order_and_cannot_move_saved_reviews() {
let directory = directory();
let root = directory.0.canonicalize().unwrap();
let storage = root.join("storage");
let mut store = Store::open(&storage).unwrap();
let mut source = Review::new(root.clone());
store.save(&mut source).unwrap();
let endpoints = ["frozen-base".to_owned(), "working:frozen-tip".to_owned()];
let mut comparison = Review::new(root.clone());
comparison.comparison = Some(endpoints.clone());
store.save(&mut comparison).unwrap();
let id = comparison.storage_id.unwrap();
assert!(source.summary_text().contains("Scope: source"));
assert!(
comparison
.summary_text()
.contains("Scope: comparison frozen-base → working:frozen-tip")
);
let mut reversed = Review::new(root.clone());
reversed.comparison = Some([endpoints[1].clone(), endpoints[0].clone()]);
store.save(&mut reversed).unwrap();
drop(store);
let mut store = Store::open(&storage).unwrap();
let reviews = store.list(&root).unwrap();
assert_eq!(reviews.len(), 3);
assert_eq!(reviews[0].comparison, reversed.comparison);
assert_eq!(reviews[1].comparison, Some(endpoints.clone()));
assert_eq!(reviews[2].comparison, None);
let mut loaded = store.load(&root, id).unwrap();
let path = root.join("comparison.json");
loaded.save(&path).unwrap();
let json: serde_json::Value = serde_json::from_slice(&fs::read(&path).unwrap()).unwrap();
assert_eq!(json["comparison"], serde_json::json!(endpoints));
let imported = Review::load(&path, &root).unwrap();
assert_eq!(imported.comparison, loaded.comparison);
loaded.comparison = None;
assert!(store.save(&mut loaded).unwrap_err().contains("scope"));
assert_eq!(store.load(&root, id).unwrap().comparison, Some(endpoints));
assert_eq!(loaded.storage_revision, 1);
source.comparison = reversed.comparison;
assert!(store.save(&mut source).is_err());
assert_eq!(
store
.load(&root, source.storage_id.unwrap())
.unwrap()
.comparison,
None
);
}
#[test]
fn invalid_comparison_scopes_and_old_json_are_rejected_without_replacing_data() {
let directory = directory();
let root = directory.0.canonicalize().unwrap();
let mut review = Review::new(root.clone());
let path = root.join("review.json");
review.save(&path).unwrap();
let original = fs::read(&path).unwrap();
let mut store = Store::open(&root.join("storage")).unwrap();
for endpoint in ["", "bad\nrevision", "bad\0revision"] {
for side in [0, 1] {
let mut endpoints = ["base".to_owned(), "tip".to_owned()];
endpoints[side] = endpoint.into();
review.comparison = Some(endpoints);
assert!(review.save(&path).unwrap_err().contains("immutable"));
assert!(store.save(&mut review).unwrap_err().contains("immutable"));
assert_eq!(fs::read(&path).unwrap(), original);
assert_eq!((review.storage_id, review.storage_revision), (None, 0));
}
}
assert!(store.list(&root).unwrap().is_empty());
let mut old: serde_json::Value = serde_json::from_slice(&original).unwrap();
old["version"] = 4.into();
old.as_object_mut().unwrap().remove("comparison");
let bytes = serde_json::to_vec(&old).unwrap();
fs::write(&path, &bytes).unwrap();
assert!(
Review::load(&path, &root)
.unwrap_err()
.contains("Unsupported review version 4")
);
assert_eq!(fs::read(&path).unwrap(), bytes);
let fresh = Review::new(root.clone());
assert!(fresh.save(&path).is_err());
assert_eq!(fs::read(&path).unwrap(), bytes);
}
#[test]
fn comparison_rows_enforce_complete_pairs_and_the_review_size_limit() {
let directory = directory();
let root = directory.0.canonicalize().unwrap();
let mut store = Store::open(&root.join("storage")).unwrap();
let mut review = Review::new(root.clone());
store.save(&mut review).unwrap();
let id = review.storage_id.unwrap();
for update in [
"before_revision = 'base'",
"after_revision = 'tip'",
"before_revision = '', after_revision = 'tip'",
"before_revision = 'base', after_revision = ''",
] {
assert!(
store
.connection
.execute(&format!("UPDATE reviews SET {update} WHERE id = ?1"), [id],)
.is_err()
);
}
store
.connection
.execute(
"UPDATE reviews SET before_revision = 'base', after_revision = char(10) WHERE id = ?1",
[id],
)
.unwrap();
assert!(store.load(&root, id).unwrap_err().contains("immutable"));
store
.connection
.execute(
"UPDATE reviews SET after_revision = hex(zeroblob(?1)) WHERE id = ?2",
params![(LIMIT / 2) as i64, id],
)
.unwrap();
assert!(store.load(&root, id).unwrap_err().contains("64 MiB"));
assert!(store.list(&root).unwrap_err().contains("64 MiB"));
review.comparison = Some(["base".into(), "x".repeat(LIMIT)]);
assert!(store.save(&mut review).unwrap_err().contains("64 MiB"));
let path = root.join("too-large.json");
assert!(review.save(&path).unwrap_err().contains("64 MiB"));
assert!(!path.exists());
}
#[test]
fn database_repository_identity_is_canonical_and_independent() {
let first = directory();
@@ -268,9 +401,16 @@ fn database_conflicts_and_locked_writes_keep_drafts_and_identity() {
#[test]
fn incompatible_database_schemas_are_preserved() {
let previous = format!(
"{REVIEWS_SCHEMA}; {COMMENTS_SCHEMA};
"CREATE TABLE reviews (
id INTEGER PRIMARY KEY CHECK (id > 0),
root TEXT NOT NULL,
revision INTEGER NOT NULL CHECK (revision > 0),
summary TEXT NOT NULL,
verdict TEXT CHECK (verdict IN ('looks_good', 'changes_requested'))
) STRICT;
{COMMENTS_SCHEMA}; {REVIEWED_FILES_SCHEMA};
INSERT INTO reviews VALUES (1, '/old/repo', 1, 'Preserve me', 'looks_good');
PRAGMA user_version = 2"
PRAGMA user_version = 3"
);
for schema in [
previous.as_str(),
@@ -799,12 +939,12 @@ fn exact_grapheme_ranges_roundtrip_and_reject_invalid_coordinates() {
.as_object_mut()
.unwrap()
.remove("resolved");
for version in [2, 3, 4] {
for version in [2, 3, 4, 5] {
missing_resolution["version"] = version.into();
let bytes = serde_json::to_vec(&missing_resolution).unwrap();
fs::write(&path, &bytes).unwrap();
let error = Review::load(&path, &root).unwrap_err();
if version < 4 {
if version < 5 {
assert!(error.contains("Unsupported review version"));
} else {
assert!(error.contains("missing field"));
-4
View File
@@ -1272,10 +1272,6 @@ fn resolver_renders_aligned_native_draft_alternative_and_separate_history() {
assert!(!heading.contains(resolver.files[current_file].path.to_string_lossy().as_ref()));
assert!(!heading.split_whitespace().any(|word| word == "M"));
}
assert!(
line_at(&terminal, alternative, alternative.bottom() - 1)
.contains(&resolver.files[current_file].sources[alternative_index].label)
);
assert!(
line_at(&terminal, draft, draft.bottom() - 1)
.chars()