use face_core::FaceError;
use face_core::detect::score::{ScoreDetection, detect_score, validate_score_path};
use serde_json::json;
#[track_caller]
fn assert_path(det: &ScoreDetection, expected_unprefixed: &str) {
let path = det
.path
.as_deref()
.unwrap_or_else(|| panic!("expected Some path, got None; det = {det:?}"));
let with_dot = format!(".{expected_unprefixed}");
assert!(
path == expected_unprefixed || path == with_dot,
"expected `{expected_unprefixed}` or `{with_dot}`, got `{path}`",
);
}
mod named_candidates {
use super::*;
#[test]
fn picks_score_at_top_level() {
let items = vec![json!({"score": 0.8, "name": "x"})];
let det = detect_score(&items).expect("score field detects");
assert_path(&det, "score");
assert!(
det.fallback_reason.is_none(),
"named hit should not carry a fallback_reason; got {:?}",
det.fallback_reason,
);
}
#[test]
fn picks_rank_when_score_absent() {
let items = vec![json!({"rank": 5, "x": 1})];
let det = detect_score(&items).expect("rank field detects");
assert_path(&det, "rank");
}
#[test]
fn priority_score_over_relevance() {
let items = vec![json!({"relevance": 0.5, "score": 0.8})];
let det = detect_score(&items).expect("score beats relevance by priority");
assert_path(&det, "score");
}
#[test]
fn priority_relevance_over_confidence() {
let items = vec![json!({"confidence": 0.5, "relevance": 0.7})];
let det = detect_score(&items).expect("relevance beats confidence by priority");
assert_path(&det, "relevance");
}
#[test]
fn bm25_picked_when_above_appear() {
let items = vec![json!({"bm25": 12.5})];
let det = detect_score(&items).expect("bm25 field detects when alone");
assert_path(&det, "bm25");
}
#[test]
fn distance_picked() {
let items = vec![json!({"distance": 4.2})];
let det = detect_score(&items).expect("distance field detects when alone");
assert_path(&det, "distance");
}
}
mod top_level_beats_nested {
use super::*;
#[test]
fn top_level_score_wins_over_nested_score() {
let items = vec![json!({"score": 0.5, "data": {"score": 0.9}})];
let det = detect_score(&items).expect("top-level score beats nested score");
assert_path(&det, "score");
}
#[test]
fn nested_score_picked_when_no_top_level() {
let items = vec![json!({"data": {"score": 0.7}})];
let det = detect_score(&items).expect("nested score detects when no top-level");
assert_path(&det, "data.score");
}
}
mod single_numeric_fallback {
use super::*;
#[test]
fn single_unnamed_numeric_field_picked() {
let items = vec![json!({"foo": 42})];
let det = detect_score(&items).expect("single unnamed numeric is the score");
assert_path(&det, "foo");
}
#[test]
fn single_nested_numeric_field_picked() {
let items = vec![json!({"a": {"b": 7.5}})];
let det = detect_score(&items).expect("single nested numeric is the score");
assert_path(&det, "a.b");
}
#[test]
fn multiple_unnamed_no_match() {
let items = vec![json!({"foo": 1, "bar": 2})];
let det = detect_score(&items).expect("multiple unnamed numerics is `Ok` but `None`");
assert!(
det.path.is_none(),
"multiple unnamed numerics → no score path; got {:?}",
det.path,
);
let reason = det
.fallback_reason
.as_deref()
.expect("a fallback_reason should explain why no score was picked");
let lower = reason.to_lowercase();
assert!(
lower.contains("multiple") || lower.contains("numeric"),
"fallback_reason should mention multiple/numeric; got `{reason}`",
);
}
}
mod no_score {
use super::*;
#[test]
fn all_string_fields() {
let items = vec![json!({"name": "x", "kind": "y"})];
let det = detect_score(&items).expect("no numeric is `Ok` but `None`");
assert!(
det.path.is_none(),
"all-string items → no score path; got {:?}",
det.path,
);
}
#[test]
fn empty_items_list() {
let items: Vec<serde_json::Value> = Vec::new();
let det = detect_score(&items).expect("empty items list returns `Ok`, not `Err`");
assert!(
det.path.is_none(),
"empty items → no score path; got {:?}",
det.path,
);
assert!(
det.fallback_reason.is_some(),
"empty items should carry some fallback_reason; got None",
);
}
}
mod ignored_shapes {
use super::*;
#[test]
fn arrays_inside_items_are_not_walked() {
let items = vec![json!({"data": {"counts": [1, 2, 3]}, "score": 0.5})];
let det = detect_score(&items).expect("named top-level score wins over array contents");
assert_path(&det, "score");
}
#[test]
fn bool_is_not_numeric() {
let items = vec![json!({"flag": true})];
let det = detect_score(&items).expect("bool-only items resolve to `Ok` with no path");
assert!(
det.path.is_none(),
"bool field is not numeric → no score path; got {:?}",
det.path,
);
}
}
mod validate_score_path {
use super::*;
#[track_caller]
fn assert_unknown_score_path(err: &FaceError, expected_path_unprefixed: &str) {
match err {
FaceError::UnknownScorePath { path } => {
let with_dot = format!(".{expected_path_unprefixed}");
assert!(
path == expected_path_unprefixed || path == &with_dot,
"expected path `{expected_path_unprefixed}` or `{with_dot}` in error, got `{path}`",
);
}
other => panic!("expected FaceError::UnknownScorePath, got {other:?} ({other})",),
}
}
#[test]
fn valid_path_resolves_numeric() {
let items = vec![json!({"score": 0.5})];
validate_score_path(&items, ".score").expect("valid numeric path resolves");
}
#[test]
fn unknown_path_errors() {
let items = vec![json!({"score": 0.5})];
let err = validate_score_path(&items, ".nope").expect_err("unknown path must error");
assert_unknown_score_path(&err, "nope");
}
#[test]
fn non_numeric_value_at_path_errors() {
let items = vec![json!({"name": "hello"})];
let err =
validate_score_path(&items, ".name").expect_err("non-numeric value at path must error");
assert_unknown_score_path(&err, "name");
}
#[test]
fn numeric_in_first_16_is_enough() {
let mut items: Vec<serde_json::Value> =
(0..15).map(|_| json!({"score": "not-a-number"})).collect();
items.push(json!({"score": 0.5}));
validate_score_path(&items, ".score").expect("a numeric within the sample window suffices");
}
}