use face_core::{Axis, ClusterId, Envelope, InputFormat, SimilarAlgorithm, Strategy};
const ENVELOPE_MINIMAL: &str = include_str!("fixtures/envelope_minimal.json");
const ENVELOPE_TWO_AXIS: &str = include_str!("fixtures/envelope_two_axis.json");
const ENVELOPE_WITH_PAGE: &str = include_str!("fixtures/envelope_with_page.json");
fn assert_value_round_trip(label: &str, json: &str) -> Envelope {
let first: Envelope =
serde_json::from_str(json).unwrap_or_else(|e| panic!("[{label}] first parse: {e}"));
let reser =
serde_json::to_string(&first).unwrap_or_else(|e| panic!("[{label}] re-serialize: {e}"));
let second: Envelope =
serde_json::from_str(&reser).unwrap_or_else(|e| panic!("[{label}] re-parse: {e}"));
assert_eq!(
first, second,
"[{label}] value round-trip mismatch via {reser}",
);
first
}
mod golden_minimal {
use super::*;
#[test]
fn deserializes() {
let env = assert_value_round_trip("minimal", ENVELOPE_MINIMAL);
assert_eq!(env.result.input_total, 412);
assert_eq!(env.result.skipped, 0);
assert_eq!(env.result.axes.len(), 1);
assert_eq!(env.clusters.len(), 1);
}
#[test]
fn detection_format_is_jsonl() {
let env: Envelope = serde_json::from_str(ENVELOPE_MINIMAL).expect("parse");
assert_eq!(env.result.detection.format, InputFormat::Jsonl);
}
#[test]
fn input_total_is_412() {
let env: Envelope = serde_json::from_str(ENVELOPE_MINIMAL).expect("parse");
assert_eq!(env.result.input_total, 412);
}
#[test]
fn cluster_id_round_trips_through_string() {
let env: Envelope = serde_json::from_str(ENVELOPE_MINIMAL).expect("parse");
let cid = &env.clusters[0].id;
let s = cid.to_string();
let re_parsed = ClusterId::parse_canonical(&s).expect("parse round-trip");
assert_eq!(&re_parsed, cid);
}
}
mod golden_two_axis {
use super::*;
#[test]
fn structure_matches_design() {
let env = assert_value_round_trip("two-axis", ENVELOPE_TWO_AXIS);
assert_eq!(env.result.axes.len(), 2);
let outer = &env.clusters[0];
assert_eq!(outer.axis, "file");
assert_eq!(outer.value, serde_json::Value::String("src/cli.rs".into()));
assert_eq!(outer.clusters.len(), 3);
let excellent = outer
.clusters
.iter()
.find(|c| c.value == serde_json::Value::String("excellent".into()))
.expect("excellent sub-cluster present");
assert_eq!(excellent.axis, "score");
assert_eq!(excellent.score_min, Some(0.85));
assert_eq!(excellent.score_max, Some(1.0));
}
#[test]
fn second_axis_carries_strategy_count() {
let env: Envelope = serde_json::from_str(ENVELOPE_TWO_AXIS).expect("parse");
match env.result.axes[1].strategy {
Strategy::Bands { count } => assert_eq!(count, 5),
ref other => panic!("expected Strategy::Bands, got {other:?}"),
}
}
}
mod golden_with_page {
use super::*;
#[test]
fn page_cluster_id_and_items() {
let env = assert_value_round_trip("with-page", ENVELOPE_WITH_PAGE);
let page_id = env.page.cluster_id.as_ref().expect("page.cluster_id set");
assert_eq!(page_id.depth(), 1);
assert_eq!(page_id.segments()[0].axis, "file");
assert_eq!(page_id.segments()[0].value, "src/cli.rs");
assert_eq!(env.page.page, 2);
assert_eq!(env.page.per_page, 20);
assert_eq!(env.page.total_items, 38);
assert_eq!(env.page.items.len(), 2);
let first = &env.page.items[0];
assert!(first.is_object(), "first page item should be a JSON object");
let line = first
.get("data")
.and_then(|d| d.get("line_number"))
.and_then(|n| n.as_u64())
.expect("line_number present");
assert_eq!(line, 42);
}
}
mod strategy_wire {
use super::*;
#[test]
fn exact() {
let json = serde_json::to_value(&Strategy::Exact).expect("serialize");
let expected: serde_json::Value = serde_json::json!({"strategy": "exact"});
assert_eq!(json, expected);
}
#[test]
fn bands_with_count() {
let json = serde_json::to_value(&Strategy::Bands { count: 5 }).expect("serialize");
let expected: serde_json::Value = serde_json::json!({"strategy": "bands", "count": 5});
assert_eq!(json, expected);
}
#[test]
fn prefix_no_depth_skips_field() {
let json = serde_json::to_value(&Strategy::Prefix { depth: None }).expect("serialize");
let expected: serde_json::Value = serde_json::json!({"strategy": "prefix"});
assert_eq!(json, expected);
}
#[test]
fn prefix_with_depth() {
let json = serde_json::to_value(&Strategy::Prefix { depth: Some(2) }).expect("serialize");
let expected: serde_json::Value = serde_json::json!({"strategy": "prefix", "depth": 2});
assert_eq!(json, expected);
}
#[test]
fn top_with_n() {
let json = serde_json::to_value(&Strategy::Top { n: 10 }).expect("serialize");
let expected: serde_json::Value = serde_json::json!({"strategy": "top", "n": 10});
assert_eq!(json, expected);
}
#[test]
fn quantiles_with_count() {
let json = serde_json::to_value(&Strategy::Quantiles { count: 4 }).expect("serialize");
let expected: serde_json::Value = serde_json::json!({"strategy": "quantiles", "count": 4});
assert_eq!(json, expected);
}
#[test]
fn natural_with_count() {
let json = serde_json::to_value(&Strategy::Natural { count: 3 }).expect("serialize");
let expected: serde_json::Value = serde_json::json!({"strategy": "natural", "count": 3});
assert_eq!(json, expected);
}
#[test]
fn similar_with_algorithm() {
let json = serde_json::to_value(&Strategy::Similar {
algorithm: SimilarAlgorithm::Token,
})
.expect("serialize");
let expected: serde_json::Value =
serde_json::json!({"strategy": "similar", "algorithm": "token"});
assert_eq!(json, expected);
}
#[test]
fn round_trip_each_variant() {
for s in [
Strategy::Exact,
Strategy::Prefix { depth: None },
Strategy::Prefix { depth: Some(2) },
Strategy::Top { n: 10 },
Strategy::Bands { count: 5 },
Strategy::Quantiles { count: 4 },
Strategy::Natural { count: 3 },
Strategy::Similar {
algorithm: SimilarAlgorithm::Token,
},
] {
let json = serde_json::to_string(&s).expect("serialize");
let back: Strategy = serde_json::from_str(&json).expect("deserialize");
assert_eq!(s, back, "round-trip via {json}");
}
}
#[test]
fn is_buffered_classification() {
assert!(Strategy::Quantiles { count: 4 }.is_buffered());
assert!(Strategy::Natural { count: 3 }.is_buffered());
assert!(
Strategy::Similar {
algorithm: SimilarAlgorithm::Token
}
.is_buffered()
);
assert!(!Strategy::Exact.is_buffered());
assert!(!Strategy::Prefix { depth: None }.is_buffered());
assert!(!Strategy::Top { n: 10 }.is_buffered());
assert!(!Strategy::Bands { count: 5 }.is_buffered());
}
}
mod axis_wire {
use super::*;
fn make_axis(field: &str, strategy_json: serde_json::Value, auto: bool) -> Axis {
let mut obj = serde_json::Map::new();
obj.insert("field".into(), serde_json::Value::String(field.into()));
if let serde_json::Value::Object(s) = strategy_json {
for (k, v) in s {
obj.insert(k, v);
}
}
obj.insert("auto".into(), serde_json::Value::Bool(auto));
serde_json::from_value(serde_json::Value::Object(obj)).expect("deserialize Axis")
}
#[test]
fn round_trip_flat_with_exact() {
let axis = make_axis(
"data.path.text",
serde_json::json!({"strategy": "exact"}),
true,
);
let v = serde_json::to_value(&axis).expect("serialize");
let obj = v.as_object().expect("object");
assert_eq!(
obj.get("field").and_then(|x| x.as_str()),
Some("data.path.text")
);
assert_eq!(obj.get("strategy").and_then(|x| x.as_str()), Some("exact"));
assert_eq!(obj.get("auto").and_then(|x| x.as_bool()), Some(true));
let back: Axis = serde_json::from_value(v).expect("deserialize");
assert_eq!(back, axis);
}
#[test]
fn round_trip_with_band_count() {
let axis = make_axis(
"score",
serde_json::json!({"strategy": "bands", "count": 5}),
true,
);
let v = serde_json::to_value(&axis).expect("serialize");
let obj = v.as_object().expect("object");
assert_eq!(obj.get("strategy").and_then(|x| x.as_str()), Some("bands"));
assert_eq!(obj.get("count").and_then(|x| x.as_u64()), Some(5));
let back: Axis = serde_json::from_value(v).expect("deserialize");
assert_eq!(back, axis);
}
#[test]
fn axis_carries_strategy_payload() {
let axis = make_axis(
"score",
serde_json::json!({"strategy": "bands", "count": 5}),
true,
);
match axis.strategy {
Strategy::Bands { count } => assert_eq!(count, 5),
ref other => panic!("expected Strategy::Bands, got {other:?}"),
}
assert_eq!(axis.field, "score");
assert!(axis.auto);
}
}
mod input_format_wire {
use super::*;
fn ser(f: InputFormat) -> String {
let s = serde_json::to_string(&f).expect("serialize");
s.trim_matches('"').to_string()
}
#[test]
fn variants_serialize_as_expected() {
assert_eq!(ser(InputFormat::Json), "json");
assert_eq!(ser(InputFormat::Jsonl), "jsonl");
assert_eq!(ser(InputFormat::Csv), "csv");
assert_eq!(ser(InputFormat::Tsv), "tsv");
assert_eq!(ser(InputFormat::FaceEnvelope), "face-envelope");
}
#[test]
fn deserializes_each_variant() {
for (s, expected) in [
(r#""json""#, InputFormat::Json),
(r#""jsonl""#, InputFormat::Jsonl),
(r#""csv""#, InputFormat::Csv),
(r#""tsv""#, InputFormat::Tsv),
(r#""face-envelope""#, InputFormat::FaceEnvelope),
] {
let back: InputFormat = serde_json::from_str(s).expect("deserialize");
assert_eq!(back, expected, "via {s}");
}
}
}
mod meta_block {
use super::*;
#[test]
fn meta_round_trip_preserves_query_and_elapsed_ms() {
let json = serde_json::json!({
"result": {
"input_total": 0,
"skipped": 0,
"axes": [],
"detection": {
"format": "json",
"items_path": ".",
"score_path": null,
"preset": null,
"fallback_reason": null
}
},
"meta": {
"query": "TODO",
"elapsed_ms": 42
},
"clusters": [],
"page": {
"cluster_id": null,
"page": 1,
"per_page": 20,
"total_items": 0,
"items": []
}
});
let env: Envelope = serde_json::from_value(json).expect("deserialize");
assert_eq!(env.meta.get("query").and_then(|q| q.as_str()), Some("TODO"),);
assert_eq!(
env.meta.get("elapsed_ms").and_then(|n| n.as_u64()),
Some(42),
);
let back = serde_json::to_value(&env).expect("serialize");
assert_eq!(
back.get("meta")
.and_then(|m| m.get("query"))
.and_then(|q| q.as_str()),
Some("TODO"),
);
assert_eq!(
back.get("meta")
.and_then(|m| m.get("elapsed_ms"))
.and_then(|n| n.as_u64()),
Some(42),
);
}
}
mod cache_block {
use super::*;
#[test]
fn default_cache_is_omitted_from_json() {
let env = Envelope::default();
let json = serde_json::to_string(&env).expect("serialize default envelope");
assert!(
!json.contains("\"cache\""),
"empty cache should not add noise to minimal envelopes: {json}",
);
}
#[test]
fn cache_items_round_trip() {
let mut env = Envelope::default();
env.cache.items = vec![serde_json::json!({"kind": "bug"})];
let json = serde_json::to_string(&env).expect("serialize cached envelope");
let back: Envelope = serde_json::from_str(&json).expect("deserialize cached envelope");
assert_eq!(back.cache.items.len(), 1);
assert_eq!(back.cache.items[0]["kind"].as_str(), Some("bug"));
}
}
mod zero_state {
use super::*;
#[test]
fn default_envelope_round_trips() {
let env = Envelope::default();
let json = serde_json::to_string(&env).expect("serialize default");
let back: Envelope = serde_json::from_str(&json).expect("deserialize default");
assert_eq!(back, env, "Envelope::default round-trip via {json}");
}
}