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