use jevapi::{Answer, Content, EvaluationRequest, EvaluationResponse, NoulCriteria, Question};
use serde_json::json;
#[test]
fn request_serializes_all_question_types_and_structured_content() {
let request = EvaluationRequest::new(
Content::try_from(json!({"message": "Please investigate", "attempts": 3})).unwrap(),
)
.model("jev-pinned-version")
.question(
"urgent",
Question::noul_with_criteria(
Content::try_from(json!({"question": "Urgent?", "context": ["support"]})).unwrap(),
NoulCriteria::default().yes("Time-sensitive").no("Routine"),
),
)
.question(
"team",
Question::choice(
"Route to a team",
[("support", Some("Help".into())), ("sales", None)],
),
)
.question(
"severity",
Question::score("Rate severity", ["Low", "High"]),
);
request.validate().unwrap();
let expected = json!({
"state": {"message": "Please investigate", "attempts": 3},
"model": "jev-pinned-version",
"questions": {
"urgent": {"type": "noul", "instructions": {"question": "Urgent?", "context": ["support"]}, "criteria": {"true": "Time-sensitive", "false": "Routine"}},
"team": {"type": "choice", "instructions": "Route to a team", "criteria": {"support": "Help", "sales": null}},
"severity": {"type": "score", "instructions": "Rate severity", "criteria": ["Low", "High"]}
}
});
assert_eq!(serde_json::to_value(&request).unwrap(), expected);
assert_eq!(
serde_json::from_value::<EvaluationRequest>(expected).unwrap(),
request
);
}
#[test]
fn optional_noul_criteria_are_omitted() {
assert_eq!(
serde_json::to_value(Question::noul("Ready?")).unwrap(),
json!({"type": "noul", "instructions": "Ready?"})
);
assert_eq!(
serde_json::to_value(NoulCriteria::default().yes("Ready")).unwrap(),
json!({"true": "Ready"})
);
}
#[test]
fn content_enforces_only_top_level_shape() {
for value in [json!(null), json!(false), json!(42), json!(0.5)] {
assert!(Content::try_from(value.clone()).is_err());
assert!(serde_json::from_value::<Content>(value).is_err());
}
for value in [
json!("text"),
json!({"nested": null}),
json!([true, 42, null, {"a": []}]),
] {
let content = Content::try_from(value.clone()).unwrap();
assert_eq!(serde_json::to_value(content).unwrap(), value);
}
}
#[test]
fn rubric_boundaries_and_request_validation() {
for count in [0, 1, 2, 10, 11] {
let question = Question::score("Rate", vec!["level"; count]);
assert_eq!(question.validate().is_ok(), (2..=10).contains(&count));
}
for count in [0, 1, 255, 256] {
let question = Question::choice("Select", (0..count).map(|i| (i.to_string(), None)));
assert_eq!(question.validate().is_ok(), (1..=255).contains(&count));
}
assert_eq!(
EvaluationRequest::new("state").validate().unwrap_err().path,
"questions"
);
let invalid =
EvaluationRequest::new("state").question("severity", Question::score("Rate", ["Only"]));
assert_eq!(
invalid.validate().unwrap_err().path,
"questions[\"severity\"].criteria"
);
assert_eq!(invalid.model(" ").validate().unwrap_err().path, "model");
}
#[test]
fn response_decodes_all_variants_and_ignores_additive_fields() {
let value = json!({
"model": "jev-version",
"answers": {
"urgent": {"type": "noul", "noul": 0.95},
"team": {"type": "choice", "choice": "support", "probabilities": {"support": 0.8, "sales": 0.2}, "confidence": 0.7},
"severity": {"type": "score", "score": 1.25, "legend": {"0": "Low", "1": "Medium", "2": "High"}, "probabilities": {"0": 0.0, "1": 0.75, "2": 0.25}, "confidence": 0.6}
},
"usage": {"input_tokens": 300, "output_tokens": 50},
"future_field": "ignored"
});
let response: EvaluationResponse = serde_json::from_value(value).unwrap();
assert!(matches!(response.answers["urgent"], Answer::Noul { noul } if noul == 0.95));
match &response.answers["team"] {
Answer::Choice {
choice,
probabilities,
confidence,
} => {
assert_eq!(choice, "support");
assert_eq!(probabilities["sales"], 0.2);
assert_eq!(*confidence, 0.7);
}
_ => panic!("wrong answer type"),
}
match &response.answers["severity"] {
Answer::Score {
score,
legend,
probabilities,
confidence,
} => {
assert_eq!(*score, 1.25);
assert_eq!(legend["2"], "High");
assert_eq!(probabilities["1"], 0.75);
assert_eq!(*confidence, 0.6);
}
_ => panic!("wrong answer type"),
}
assert_eq!(response.usage.input_tokens, 300);
assert_eq!(response.usage.output_tokens, 50);
assert!(
serde_json::from_value::<Answer>(json!({"type": "choice", "choice": "support"})).is_err()
);
assert!(serde_json::from_value::<Answer>(json!({"type": "unknown"})).is_err());
}