fix(versions): review findings on PR #193
- A version no longer hides behind an original that duplicates resolve demoted (visible_version_clause, hidden_version_rows). - Recording a link drops the version's faces, CLIP data and clip failures (covers link-at-ingest and link_versions --apply). - The display file must live in the original's library; otherwise the original shows, and PUT /image/versions/display answers 400. - link_new_files looks up hashes in chunks of 500 and logs errors. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6135034d09
commit
9f50843479
6 files changed
+402
-47
No files matched your search
@@ -837,3 +837,104 @@ async fn face_stats_denominator_leaves_versions_out() {
|
||||
// denominator because they are never scanned.
|
||||
assert_eq!(stats.total_photos, 2);
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn linking_at_ingest_drops_derived_data_of_an_already_ingested_version() {
|
||||
let fx = fx();
|
||||
let original = fx.put("scan.jpg", b"original bytes");
|
||||
let xmp = producer_xmp(&original, "restored", &[]);
|
||||
let vh = fx.put_png("renamed.png", Some(&xmp), 1);
|
||||
fx.exec(&format!(
|
||||
"UPDATE image_exif SET clip_embedding = x'00', clip_model_version = 'm' \
|
||||
WHERE content_hash = '{vh}'"
|
||||
));
|
||||
fx.exec(&format!(
|
||||
"INSERT INTO face_detections (library_id, content_hash, rel_path, bbox_x, embedding, \
|
||||
source, status, model_version, created_at) \
|
||||
VALUES (1, '{vh}', 'renamed.png', 0.1, x'00', 'auto', 'detected', 'm', 0)"
|
||||
));
|
||||
fx.exec(&format!(
|
||||
"INSERT INTO clip_failures (library_id, rel_path, content_hash, error, failed_at) \
|
||||
VALUES (1, 'renamed.png', '{vh}', 'boom', 0)"
|
||||
));
|
||||
|
||||
let files = vec![(fx.root.join("renamed.png"), "renamed.png".to_string())];
|
||||
assert_eq!(
|
||||
link_new_files(&fx.library, &files, &fx.exif_arc(), &fx.state.version_dao),
|
||||
1
|
||||
);
|
||||
assert_eq!(
|
||||
fx.count(&format!(
|
||||
"SELECT COUNT(*) AS n FROM face_detections WHERE content_hash = '{vh}'"
|
||||
)),
|
||||
0
|
||||
);
|
||||
assert_eq!(
|
||||
fx.count(&format!(
|
||||
"SELECT COUNT(*) AS n FROM clip_failures WHERE content_hash = '{vh}'"
|
||||
)),
|
||||
0
|
||||
);
|
||||
assert_eq!(
|
||||
fx.count(&format!(
|
||||
"SELECT COUNT(*) AS n FROM image_exif WHERE content_hash = '{vh}' \
|
||||
AND (clip_embedding IS NOT NULL OR clip_model_version IS NOT NULL)"
|
||||
)),
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn link_new_files_links_across_more_than_one_lookup_chunk() {
|
||||
let fx = fx();
|
||||
let original = fx.put("scan.jpg", b"original bytes");
|
||||
let xmp = producer_xmp(&original, "restored", &[]);
|
||||
let vh = fx.put_png("zz/renamed.png", Some(&xmp), 1);
|
||||
// Enough unrelated PNG paths to force several lookup chunks; the real
|
||||
// one sits in the last.
|
||||
let mut files: Vec<(PathBuf, String)> = (0..1200)
|
||||
.map(|i| (fx.root.join(format!("n/{i}.png")), format!("n/{i}.png")))
|
||||
.collect();
|
||||
files.push((fx.root.join("zz/renamed.png"), "zz/renamed.png".to_string()));
|
||||
assert_eq!(
|
||||
link_new_files(&fx.library, &files, &fx.exif_arc(), &fx.state.version_dao),
|
||||
1
|
||||
);
|
||||
assert!(
|
||||
fx.state
|
||||
.version_dao
|
||||
.lock()
|
||||
.unwrap()
|
||||
.get_link(&vh)
|
||||
.unwrap()
|
||||
.is_some()
|
||||
);
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn display_choice_needs_a_file_in_the_originals_library() {
|
||||
let fx = fx();
|
||||
let original = fx.put("scan.jpg", b"original bytes");
|
||||
let xmp = producer_xmp(&original, "restored", &[]);
|
||||
let vh = fx.put_png("aiedits/scan.jpg.restored.png", Some(&xmp), 1);
|
||||
fx.link(&vh, &original, "aiedits/scan.jpg.restored.png");
|
||||
// The version's bytes live only in a second library.
|
||||
fx.exec("INSERT INTO libraries (id, name, root_path, created_at) VALUES (2, 'two', '/tmp/two-nope', 0)");
|
||||
fx.exec(
|
||||
"UPDATE image_exif SET library_id = 2 WHERE rel_path = 'aiedits/scan.jpg.restored.png'",
|
||||
);
|
||||
let app = photos_app!(fx);
|
||||
let req = awtest::TestRequest::put()
|
||||
.uri("/image/versions/display")
|
||||
.insert_header(("Authorization", format!("Bearer {}", Fx::token())))
|
||||
.set_json(serde_json::json!({"path": "scan.jpg", "display_hash": vh}))
|
||||
.to_request();
|
||||
assert_eq!(awtest::call_service(&app, req).await.status(), 400);
|
||||
// Clearing is still fine.
|
||||
let req = awtest::TestRequest::put()
|
||||
.uri("/image/versions/display")
|
||||
.insert_header(("Authorization", format!("Bearer {}", Fx::token())))
|
||||
.set_json(serde_json::json!({"path": "scan.jpg", "display_hash": null}))
|
||||
.to_request();
|
||||
assert_eq!(awtest::call_service(&app, req).await.status(), 200);
|
||||
}
|
||||
Reference in new issue
Block a user