use logprobe::diagnostics;
use logprobe::filters;
use logprobe::math;
use logprobe::metrics;
use logprobe::parse;
use logprobe::types::{LogprobSequence, Severity, TokenEntropy, TokenLogprob, TopKEntry};
const OPENAI_FIXTURE: &str = include_str!("fixtures/openai_sample.json");
const VLLM_FIXTURE: &str = include_str!("fixtures/vllm_sample.json");
const JSONL_FIXTURE: &str = include_str!("fixtures/stream.jsonl");
#[test]
fn parse_openai_format() {
let seq = parse::parse_string(OPENAI_FIXTURE, None, false).unwrap();
assert_eq!(seq.format_detected, "openai");
assert_eq!(seq.tokens.len(), 2);
assert_eq!(seq.tokens[0].token, "Hello");
assert_eq!(seq.model.as_deref(), Some("gpt-4"));
assert!(seq.tokens[0].bytes.is_some());
assert!(seq.tokens[0].top_logprobs.is_some());
}
#[test]
fn parse_vllm_format() {
let seq = parse::parse_string(VLLM_FIXTURE, None, false).unwrap();
assert_eq!(seq.format_detected, "vllm");
assert_eq!(seq.tokens.len(), 4);
assert_eq!(seq.tokens[0].token, "The");
assert_eq!(seq.model.as_deref(), Some("meta-llama/Llama-2-7b"));
}
#[test]
fn parse_jsonl_format() {
let seq = parse::parse_string(JSONL_FIXTURE, None, false).unwrap();
assert_eq!(seq.format_detected, "jsonl");
assert_eq!(seq.tokens.len(), 7);
assert_eq!(seq.tokens[0].token, "Once");
}
#[test]
fn summary_computes_correctly() {
let seq = parse::parse_string(OPENAI_FIXTURE, None, false).unwrap();
let summary = metrics::compute_summary(&seq);
assert_eq!(summary.token_count, 2);
assert!((summary.mean_logprob - (-0.65)).abs() < 1e-10);
assert!(summary.perplexity > 1.0);
}
#[test]
fn entropy_with_top_logprobs() {
let seq = parse::parse_string(OPENAI_FIXTURE, None, false).unwrap();
let entropies = metrics::compute_entropy(&seq);
assert_eq!(entropies.len(), 2);
assert!(entropies[0].entropy_partial > 0.0);
assert!(entropies[0].missing_mass > 0.0);
}
#[test]
fn bpb_works_with_bytes() {
let seq = parse::parse_string(OPENAI_FIXTURE, None, false).unwrap();
let result = metrics::compute_bpb(&seq);
match result {
metrics::BpbResult::Value { bpb } => assert!(bpb > 0.0),
metrics::BpbResult::Unavailable { reason } => panic!("expected BPB value, got: {reason}"),
}
}
#[test]
fn bpb_refuses_without_bytes() {
let seq = parse::parse_string(JSONL_FIXTURE, None, false).unwrap();
let result = metrics::compute_bpb(&seq);
match result {
metrics::BpbResult::Unavailable { .. } => {} metrics::BpbResult::Value { .. } => panic!("should have refused without bytes"),
}
}
#[test]
fn validate_clean_input() {
let seq = parse::parse_string(OPENAI_FIXTURE, None, false).unwrap();
let findings = diagnostics::validate(&seq);
let errors: Vec<_> = findings
.iter()
.filter(|f| f.severity == Severity::Error)
.collect();
assert!(errors.is_empty(), "unexpected errors: {errors:?}");
}
#[test]
fn validate_catches_positive_logprob() {
let bad_json = r#"{"token":"bad","logprob":0.5}
{"token":"ok","logprob":-0.3}"#;
let seq = parse::parse_string(bad_json, None, false).unwrap();
let findings = diagnostics::validate(&seq);
let has_positive_error = findings.iter().any(|f| f.check == "nonpositive_logprob");
assert!(has_positive_error, "should catch positive logprob");
}
#[test]
fn diagnose_reports_missing_mass() {
let seq = parse::parse_string(OPENAI_FIXTURE, None, false).unwrap();
let report = diagnostics::diagnose_report(&seq);
assert!(
report.total_positions > 0,
"should have positions with top_logprobs"
);
assert!(
report.mean_missing_mass > 0.0,
"should have non-zero missing mass"
);
}
#[test]
fn confidence_filter_works() {
let seq = parse::parse_string(VLLM_FIXTURE, None, false).unwrap();
let low = filters::find_low_confidence(&seq, -1.0, 2);
assert!(
low.len() >= 2,
"expected at least 2 low-confidence tokens, got {}",
low.len()
);
assert!(low.iter().any(|t| t.token == " 42"));
}
#[test]
fn missing_mass_math() {
let lps = [-0.5_f64, -2.0, -3.5];
let mm = math::missing_mass(&lps);
let expected_mass: f64 = lps.iter().map(|lp| lp.exp()).sum();
let expected_missing = 1.0 - expected_mass;
assert!((mm - expected_missing).abs() < 1e-10);
}
#[test]
fn perplexity_math() {
let lps = [-0.5, -0.8];
let ppl = math::perplexity(&lps);
let expected = (-math::mean_logprob(&lps)).exp();
assert!((ppl - expected).abs() < 1e-10);
}
#[test]
fn truncated_entropy_bias_is_bounded() {
let full_probs: [f64; 5] = [0.4, 0.3, 0.15, 0.10, 0.05];
let observed_probs: [f64; 3] = [0.4, 0.3, 0.15];
let true_entropy: f64 = full_probs.iter().map(|&p| -p * p.log2()).sum();
let observed_lps: Vec<f64> = observed_probs.iter().map(|p| p.ln()).collect();
let h_partial = math::entropy_bits_partial(&observed_lps);
let h_normalized = math::entropy_bits_normalized(&observed_lps);
assert!(
h_partial < true_entropy,
"partial entropy ({h_partial:.4}) should be < true entropy ({true_entropy:.4})"
);
assert!(
h_normalized > h_partial,
"normalized ({h_normalized:.4}) should be > partial ({h_partial:.4})"
);
}
#[test]
fn unnormalized_logits_detected() {
let seq = make_seq_with_topk(vec![
vec![("A", 2.0), ("B", 1.0), ("C", 0.5)],
vec![("X", 3.0), ("Y", 1.5), ("Z", 0.0)],
]);
let report = diagnostics::diagnose_report(&seq);
assert_eq!(
report.normalization_status,
Severity::Error,
"should detect unnormalized scores"
);
assert!(
report.mean_log_mass > 2.0,
"mean log mass should be >> 0 for raw logits, got {}",
report.mean_log_mass
);
}
#[test]
fn all_zero_logprobs_flagged() {
let seq = make_simple_seq(vec![0.0, 0.0, 0.0]);
let report = diagnostics::diagnose_report(&seq);
let has_zero = report
.suspicious_patterns
.iter()
.any(|f| f.check == "all_zero_logprobs");
assert!(has_zero, "should flag all-zero logprobs");
}
#[test]
fn constant_logprobs_flagged() {
let seq = make_simple_seq(vec![-1.5, -1.5, -1.5]);
let report = diagnostics::diagnose_report(&seq);
let has_constant = report
.suspicious_patterns
.iter()
.any(|f| f.check == "constant_logprobs");
assert!(has_constant, "should flag constant logprobs");
}
#[test]
fn empty_input_errors() {
let result = parse::parse_string("", None, false);
assert!(result.is_err(), "empty input should return error");
let err_msg = result.unwrap_err().to_string();
assert!(
err_msg.contains("empty"),
"error should mention empty: {err_msg}"
);
}
#[test]
fn malformed_json_errors() {
let result = parse::parse_string("{not valid json at all!!!", None, false);
assert!(result.is_err(), "malformed JSON should return error");
}
#[test]
fn missing_mass_high_flags_unreliable() {
let seq = make_seq_with_topk(vec![vec![("only", -1.0)]]);
let entropies = metrics::compute_entropy(&seq);
assert_eq!(entropies.len(), 1);
assert!(
entropies[0].missing_mass > 0.5,
"missing mass should be >50%: {}",
entropies[0].missing_mass
);
assert!(entropies[0].unreliable, "should be flagged as unreliable");
}
#[test]
fn entropy_spike_detection() {
let entropies = vec![
make_token_entropy(0, "a", 0.1, 0.1),
make_token_entropy(1, "b", 0.1, 0.1),
make_token_entropy(2, "SPIKE", 10.0, 10.0),
make_token_entropy(3, "c", 0.1, 0.1),
make_token_entropy(4, "d", 0.1, 0.1),
];
let spikes = filters::detect_entropy_spikes(&entropies, 1.5);
assert!(
spikes.contains(&2),
"should detect spike at position 2, got: {spikes:?}"
);
assert!(
!spikes.contains(&0) && !spikes.contains(&1),
"low-entropy positions should not be spikes"
);
}
#[test]
fn bpb_strict_refuses_token_bytes_fallback() {
let seq = make_simple_seq(vec![-0.5, -1.0]);
let result = metrics::compute_bpb(&seq);
match result {
metrics::BpbResult::Unavailable { reason } => {
assert!(
reason.contains("byte") || reason.contains("BPE"),
"error should explain byte requirement: {reason}"
);
}
metrics::BpbResult::Value { .. } => {
panic!("should refuse BPB without byte data")
}
}
}
#[test]
fn validate_catches_unsorted_top_logprobs() {
let seq = LogprobSequence {
tokens: vec![TokenLogprob {
token: "test".into(),
logprob: -0.5,
bytes: None,
top_logprobs: Some(vec![
TopKEntry {
token: "worst".into(),
logprob: -3.0,
},
TopKEntry {
token: "mid".into(),
logprob: -1.5,
},
TopKEntry {
token: "best".into(),
logprob: -0.5,
},
]),
}],
model: None,
format_detected: "test".into(),
total_logprob: -0.5,
};
let findings = diagnostics::validate(&seq);
let has_sorted = findings.iter().any(|f| f.check == "sorted_top_logprobs");
assert!(has_sorted, "should catch unsorted top_logprobs");
}
#[test]
fn validate_catches_duplicate_top_tokens() {
let seq = LogprobSequence {
tokens: vec![TokenLogprob {
token: "test".into(),
logprob: -0.5,
bytes: None,
top_logprobs: Some(vec![
TopKEntry {
token: "hello".into(),
logprob: -0.5,
},
TopKEntry {
token: "world".into(),
logprob: -1.0,
},
TopKEntry {
token: "hello".into(),
logprob: -2.0,
},
]),
}],
model: None,
format_detected: "test".into(),
total_logprob: -0.5,
};
let findings = diagnostics::validate(&seq);
let has_dup = findings.iter().any(|f| f.check == "duplicate_top_token");
assert!(has_dup, "should catch duplicate tokens in top_logprobs");
}
#[test]
fn vllm_top_logprobs_sorted_after_parse() {
let seq = parse::parse_string(VLLM_FIXTURE, None, false).unwrap();
for (i, tok) in seq.tokens.iter().enumerate() {
if let Some(ref top_k) = tok.top_logprobs {
for w in top_k.windows(2) {
assert!(
w[0].logprob >= w[1].logprob,
"vLLM top_logprobs not sorted at position {i}: {} >= {} failed",
w[0].logprob,
w[1].logprob
);
}
}
}
}
#[test]
fn format_override_works() {
let input = r#"[{"token":"Hi","logprob":-0.5},{"token":"!","logprob":-1.0}]"#;
let seq = parse::parse_string(
input,
Some(logprobe::types::InputFormat::JsonlStream),
false,
)
.unwrap();
assert_eq!(seq.format_detected, "jsonl");
assert_eq!(seq.tokens.len(), 2);
}
#[test]
fn diagnose_json_roundtrips() {
let seq = parse::parse_string(OPENAI_FIXTURE, None, false).unwrap();
let report = diagnostics::diagnose_report(&seq);
let json = serde_json::to_string(&report).expect("should serialize");
let _: logprobe::types::DiagnoseReport =
serde_json::from_str(&json).expect("should deserialize back");
}
#[test]
fn real_gpt4o_mini_creative() {
let input = include_str!("../demo/gpt4o_mini_creative.json");
let seq = parse::parse_string(input, None, false).unwrap();
assert_eq!(seq.format_detected, "openai");
assert_eq!(seq.tokens.len(), 150);
assert_eq!(seq.model.as_deref(), Some("gpt-4o-mini-2024-07-18"));
let report = diagnostics::diagnose_report(&seq);
assert_eq!(report.normalization_status, Severity::Ok);
assert!(
report.mean_missing_mass > 0.0,
"creative writing should have some missing mass"
);
let errors: Vec<_> = report
.findings
.iter()
.filter(|f| f.severity == Severity::Error)
.collect();
assert!(
errors.is_empty(),
"should have no validation errors: {errors:?}"
);
}
#[test]
fn gemini_format_parses() {
let input = include_str!("../demo/gemini_sample.json");
let seq = parse::parse_string(input, None, false).unwrap();
assert_eq!(seq.format_detected, "gemini");
assert_eq!(seq.tokens.len(), 12);
assert_eq!(seq.model.as_deref(), Some("gemini-2.0-flash"));
assert!(seq.tokens[0].top_logprobs.is_some());
let report = diagnostics::diagnose_report(&seq);
assert_eq!(report.normalization_status, Severity::Ok);
let errors: Vec<_> = report
.findings
.iter()
.filter(|f| f.severity == Severity::Error)
.collect();
assert!(
errors.is_empty(),
"gemini should have no errors: {errors:?}"
);
}
#[test]
fn ollama_format_parses() {
let input = include_str!("../demo/ollama_sample.json");
let seq = parse::parse_string(input, None, false).unwrap();
assert_eq!(seq.format_detected, "ollama");
assert_eq!(seq.tokens.len(), 7);
assert_eq!(seq.model.as_deref(), Some("llama3.2:3b"));
assert!(seq.tokens[0].bytes.is_some());
assert!(seq.tokens[0].top_logprobs.is_some());
let report = diagnostics::diagnose_report(&seq);
assert_eq!(report.normalization_status, Severity::Ok);
let errors: Vec<_> = report
.findings
.iter()
.filter(|f| f.severity == Severity::Error)
.collect();
assert!(
errors.is_empty(),
"ollama should have no errors: {errors:?}"
);
}
#[test]
fn gemini_bpb_refuses() {
let input = include_str!("../demo/gemini_sample.json");
let seq = parse::parse_string(input, None, false).unwrap();
assert!(
seq.tokens.iter().all(|t| t.bytes.is_none()),
"Gemini tokens must not carry fabricated byte arrays"
);
match metrics::compute_bpb(&seq) {
metrics::BpbResult::Unavailable { .. } => {}
metrics::BpbResult::Value { bpb } => {
panic!("Gemini BPB should be unavailable, got {bpb}")
}
}
}
#[test]
fn compare_orders_files_and_deltas() {
let seq_a = parse::parse_string(OPENAI_FIXTURE, None, false).unwrap();
let seq_b = parse::parse_string(VLLM_FIXTURE, None, false).unwrap();
let report = metrics::compute_compare(&seq_a, &seq_b, "A", "B");
assert_eq!(report.file_a.label, "A");
assert_eq!(report.file_b.label, "B");
assert_eq!(report.file_a.token_count, seq_a.tokens.len());
assert_eq!(report.file_b.token_count, seq_b.tokens.len());
let expected_delta_ppl = report.file_b.perplexity - report.file_a.perplexity;
assert!((report.delta_perplexity - expected_delta_ppl).abs() < 1e-12);
}
fn make_simple_seq(logprobs: Vec<f64>) -> LogprobSequence {
let total: f64 = logprobs.iter().sum();
let tokens = logprobs
.into_iter()
.enumerate()
.map(|(i, lp)| TokenLogprob {
token: format!("t{i}"),
logprob: lp,
bytes: None,
top_logprobs: None,
})
.collect();
LogprobSequence {
tokens,
model: None,
format_detected: "test".into(),
total_logprob: total,
}
}
fn make_seq_with_topk(positions: Vec<Vec<(&str, f64)>>) -> LogprobSequence {
let mut total = 0.0;
let tokens: Vec<TokenLogprob> = positions
.into_iter()
.map(|entries| {
let chosen_logprob = entries[0].1;
total += chosen_logprob;
TokenLogprob {
token: entries[0].0.to_string(),
logprob: chosen_logprob,
bytes: None,
top_logprobs: Some(
entries
.into_iter()
.map(|(t, lp)| TopKEntry {
token: t.to_string(),
logprob: lp,
})
.collect(),
),
}
})
.collect();
LogprobSequence {
tokens,
model: None,
format_detected: "test".into(),
total_logprob: total,
}
}
fn make_token_entropy(
position: usize,
token: &str,
entropy_partial: f64,
entropy_normalized: f64,
) -> TokenEntropy {
TokenEntropy {
position,
token: token.to_string(),
entropy_partial,
entropy_normalized,
missing_mass: 0.0,
unreliable: false,
}
}