use face_core::{Axis, AxisPlan, NullDiagnostics, Record, VecDiagnostics, build_tree};
use serde_json::{Value, json};
fn axis(field: &str, strategy: Value) -> Axis {
let mut object = serde_json::Map::new();
object.insert("field".into(), Value::String(field.into()));
object.insert("auto".into(), Value::Bool(false));
if let Value::Object(map) = strategy {
for (k, v) in map {
object.insert(k, v);
}
} else {
panic!("strategy must be a JSON object");
}
serde_json::from_value(Value::Object(object)).expect("axis must deserialize")
}
mod from_items_no_score {
use super::*;
#[test]
fn none_score_path_yields_none_scores() {
let items = vec![
json!({"score": 0.9}),
json!({"score": 0.5}),
json!({"name": "no-score-field"}),
];
let records = Record::from_items(items, None);
assert_eq!(records.len(), 3);
for (i, r) in records.iter().enumerate() {
assert!(
r.score.is_none(),
"record {i} should have score: None when score_path is None, got {:?}",
r.score,
);
}
}
#[test]
fn preserves_raw_value() {
let original = json!({"a": 1, "b": [2, 3], "c": {"d": "x"}});
let items = vec![original.clone()];
let records = Record::from_items(items, None);
assert_eq!(records.len(), 1);
assert_eq!(
records[0].raw, original,
"raw must equal the original Value (deep equality)",
);
}
}
mod from_items_with_score {
use super::*;
#[test]
fn numeric_score_resolved() {
let items = vec![json!({"score": 0.9}), json!({"score": 0.42})];
let records = Record::from_items(items, Some(".score"));
assert_eq!(records[0].score, Some(0.9));
assert_eq!(records[1].score, Some(0.42));
}
#[test]
fn missing_score_path_per_record_silent() {
let items = vec![
json!({"score": 0.9}),
json!({"name": "middle, no score"}),
json!({"score": 0.5}),
];
let records = Record::from_items(items, Some(".score"));
assert_eq!(records[0].score, Some(0.9));
assert!(
records[1].score.is_none(),
"missing path on a single record must yield score: None, not error",
);
assert_eq!(records[2].score, Some(0.5));
}
#[test]
fn non_numeric_score_yields_none() {
let items = vec![json!({"score": "high"})];
let records = Record::from_items(items, Some(".score"));
assert!(
records[0].score.is_none(),
"string at score path must yield score: None",
);
}
#[test]
fn nested_score_path() {
let items = vec![
json!({"data": {"score": 0.7}}),
json!({"data": {"score": 0.1}}),
];
let records = Record::from_items(items, Some(".data.score"));
assert_eq!(records[0].score, Some(0.7));
assert_eq!(records[1].score, Some(0.1));
}
#[test]
fn null_score_yields_none() {
let items = vec![json!({"score": null})];
let records = Record::from_items(items, Some(".score"));
assert!(
records[0].score.is_none(),
"null at score path must yield score: None",
);
}
}
mod constructors {
use super::*;
#[test]
fn record_new_has_no_score() {
let r = Record::new(json!({"x": 1}));
assert!(r.score.is_none());
assert_eq!(r.raw, json!({"x": 1}));
}
}
mod diagnostics_smoke {
use super::*;
fn dataset_with_one_dropped() -> Vec<Record> {
let items = vec![json!({"kind": "bug"}), json!({"kind": [1, 2]})];
Record::from_items(items, None)
}
fn exact_axis() -> Axis {
axis("kind", json!({"strategy": "exact"}))
}
#[test]
fn null_diagnostics_swallows_skips() {
let records = dataset_with_one_dropped();
let plan = AxisPlan::leaf(exact_axis());
let mut diag = NullDiagnostics;
let clusters = build_tree(&plan, records, &mut diag).expect("build_tree must succeed");
assert_eq!(clusters.len(), 1);
assert_eq!(clusters[0].total, 1);
}
#[test]
fn vec_diagnostics_collects_skips() {
let records = dataset_with_one_dropped();
let plan = AxisPlan::leaf(exact_axis());
let mut diag = VecDiagnostics::default();
let _ = build_tree(&plan, records, &mut diag).expect("build_tree must succeed");
assert_eq!(
diag.skips.len(),
1,
"exactly one record was uncoercible and must produce one SkipReport, got {}",
diag.skips.len(),
);
}
}