Face Recognition / People Integration #61
76
src/faces.rs
76
src/faces.rs
@@ -3153,4 +3153,80 @@ mod tests {
|
|||||||
assert!(img.height() <= 100);
|
assert!(img.height() <= 100);
|
||||||
assert!(img.width() > 0 && img.height() > 0);
|
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<i32>) -> 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());
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user