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
+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"));