feat: scope local reviews to comparisons
This commit is contained in:
+39
-5
@@ -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
@@ -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
@@ -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"));
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user