diff --git a/src/faces.rs b/src/faces.rs index 66ddfca..0c6c007 100644 --- a/src/faces.rs +++ b/src/faces.rs @@ -3153,4 +3153,80 @@ mod tests { assert!(img.height() <= 100); assert!(img.width() > 0 && img.height() > 0); } + + // ── hydrate_face_with_person — PATCH/POST /image/faces response shape ── + + fn seed_library_and_face(dao: &mut SqliteFaceDao, person_id: Option) -> FaceDetectionRow { + diesel::sql_query( + "INSERT OR IGNORE INTO libraries (id, name, root_path, created_at) \ + VALUES (1, 'main', '/tmp', 0)", + ) + .execute(dao.connection.lock().unwrap().deref_mut()) + .expect("seed libraries"); + dao.store_detection( + &ctx(), + InsertFaceDetectionInput { + library_id: 1, + content_hash: "h-hydrate".into(), + rel_path: "p.jpg".into(), + bbox: Some((0.1, 0.2, 0.3, 0.4)), + embedding: Some(vec![0u8; 2048]), + confidence: Some(0.9), + source: "manual".into(), + person_id, + status: "detected".into(), + model_version: "buffalo_l".into(), + }, + ) + .unwrap() + } + + #[test] + fn hydrate_face_carries_person_name_when_assigned() { + // Regression guard for the bug where PATCH /image/faces/{id} + // returned a bare FaceDetectionRow (no person_name), causing + // the carousel overlay's optimistic replace to drop the VFD + // label off the bbox after every save. The handler hydrates + // via this helper; if anyone refactors the helper to skip the + // persons join, this test fails. + let mut dao = fresh_dao(); + let p = dao + .create_person( + &ctx(), + &CreatePersonReq { + name: "Alice".into(), + notes: None, + entity_id: None, + is_ignored: false, + }, + false, + ) + .unwrap(); + let row = seed_library_and_face(&mut dao, Some(p.id)); + let joined = + hydrate_face_with_person(&mut dao, &ctx(), row).expect("hydrate assigned"); + assert_eq!(joined.person_id, Some(p.id)); + assert_eq!(joined.person_name.as_deref(), Some("Alice")); + // Bbox + confidence + source must round-trip — these are what + // the optimistic-replace also keys on. + assert!((joined.bbox_x - 0.1).abs() < 1e-6); + assert!((joined.bbox_y - 0.2).abs() < 1e-6); + assert!((joined.bbox_w - 0.3).abs() < 1e-6); + assert!((joined.bbox_h - 0.4).abs() < 1e-6); + assert_eq!(joined.source, "manual"); + } + + #[test] + fn hydrate_face_leaves_person_name_null_when_unassigned() { + // Mirror branch: an unassigned face must hydrate cleanly with + // person_name = None, not a stale value left over from a + // previously-assigned row's serialization. + let mut dao = fresh_dao(); + let row = seed_library_and_face(&mut dao, None); + let joined = + hydrate_face_with_person(&mut dao, &ctx(), row).expect("hydrate unassigned"); + assert!(joined.person_id.is_none()); + assert!(joined.person_name.is_none()); + } + }