#![cfg(feature = "jev")]
use hikmah_kernel::decision_port::{ask, AdmittedAnswer, DecisionRequest, Question, QuestionKind};
use hikmah_kernel::jev::{JevEngine, Transport};
use serde_json::{json, Value};
use std::collections::BTreeMap;
use std::sync::{Arc, Mutex};
use std::time::Duration;
#[derive(Clone, Default)]
struct Recorder {
calls: Arc<Mutex<Vec<Value>>>,
replies: Arc<Mutex<Vec<(u16, Value)>>>,
}
impl Transport for Recorder {
fn post_json(
&self,
url: &str,
api_key: &str,
body: &Value,
_timeout: Duration,
) -> Result<(u16, Value), String> {
assert!(url.ends_with("/v1/systemone"));
assert_eq!(api_key, "test-key");
self.calls.lock().unwrap().push(body.clone());
let mut replies = self.replies.lock().unwrap();
Ok(replies.remove(0))
}
}
fn request() -> DecisionRequest {
DecisionRequest::new(
"Canary deploy: rolls out to 5% of traffic, automatic rollback on error-rate alarms.",
vec![
Question {
id: "safety".into(),
instructions: "How operationally safe is this deployment plan?".into(),
kind: QuestionKind::Score {
levels: ["very unsafe", "unsafe", "neutral", "safe", "very safe"]
.iter()
.map(|s| s.to_string())
.collect(),
},
family: None,
},
Question::noul("bad", "Is this plan irreversible?"),
],
)
.unwrap()
}
fn engine(replies: Vec<(u16, Value)>) -> (JevEngine, Recorder) {
let recorder = Recorder {
calls: Arc::default(),
replies: Arc::new(Mutex::new(replies)),
};
let engine = JevEngine::new("test-key")
.with_transport(Box::new(recorder.clone()))
.with_timeout(Duration::from_secs(5));
(engine, recorder)
}
fn captured() -> Value {
json!({"model":"jev-1.13.0","answers":{"safety":{"type":"score","score":3.7,"confidence":0.75,
"legend":{"0":"very unsafe","1":"unsafe","2":"neutral","3":"safe","4":"very safe"},
"probabilities":{"0":0.0,"1":0.0,"2":0.0,"3":0.29,"4":0.71}},
"bad":{"type":"noul","noul":0.05}},"usage":{"input_tokens":353,"output_tokens":35}})
}
#[test]
fn maps_questions_to_the_system_one_payload() {
let (engine, _) = engine(vec![]);
let payload = engine.payload(&request());
assert_eq!(payload["model"], "jev-latest");
assert_eq!(payload["questions"]["safety"]["type"], "score");
assert_eq!(payload["questions"]["safety"]["criteria"][4], "very safe");
assert_eq!(payload["questions"]["bad"]["type"], "noul");
}
#[test]
fn captured_response_is_admitted() {
let (engine, recorder) = engine(vec![(200, captured())]);
let decision = ask(&engine, &request()).unwrap();
assert!(decision.rejected.is_none(), "{:?}", decision.rejected);
assert_eq!(decision.engine.version, "jev-1.13.0");
assert_eq!(decision.answer("bad").unwrap().p_true(), Some(0.05));
match decision.answer("safety").unwrap() {
AdmittedAnswer::Score { normalized, .. } => assert!((normalized - 0.925).abs() < 1e-9),
other => panic!("{other:?}"),
}
assert_eq!(recorder.calls.lock().unwrap().len(), 1);
}
#[test]
fn overload_is_retried_then_succeeds() {
let (engine, recorder) = engine(vec![(529, json!({})), (200, captured())]);
let decision = ask(&engine, &request()).unwrap();
assert!(decision.rejected.is_none());
assert_eq!(recorder.calls.lock().unwrap().len(), 2);
}
#[test]
fn auth_failure_becomes_abstention_without_leaking_the_key() {
let (engine, _) = engine(vec![(
401,
json!({"detail":{"error_type":"authentication_error","message":"bad key"}}),
)]);
let decision = ask(&engine, &request()).unwrap();
let reason = decision.rejected.unwrap();
assert!(reason.contains("401") && reason.contains("authentication_error"));
assert!(!reason.contains("test-key"));
assert!(!format!("{engine:?}").contains("test-key"));
}
#[test]
fn null_noul_is_malformed_and_rejects_the_response() {
let mut reply = captured();
reply["answers"]["bad"] = json!({"type":"noul","noul":null});
let (engine, _) = engine(vec![(200, reply)]);
let decision = ask(&engine, &request()).unwrap();
assert!(decision.rejected.unwrap().contains("malformed"));
}
#[test]
fn unknown_choice_from_the_engine_is_rejected() {
let request = DecisionRequest::new(
"x",
vec![Question {
id: "tier".into(),
instructions: "pick".into(),
kind: QuestionKind::Choice {
options: BTreeMap::from([("a".into(), "A".into()), ("b".into(), "B".into())]),
},
family: None,
}],
)
.unwrap();
let (engine, _) = engine(vec![(
200,
json!({"model":"jev-1.13.0","answers":{"tier":{"type":"choice","choice":"c","confidence":1.0}}}),
)]);
assert!(ask(&engine, &request).unwrap().rejected.is_some());
}
#[test]
#[ignore]
fn live_jev_round_trip() {
if std::env::var("HIKMAH_LIVE_JEV").is_err() {
return;
}
let engine = JevEngine::from_env().expect("TYPESAFE_API_KEY");
let decision = ask(&engine, &request()).unwrap();
assert!(decision.rejected.is_none(), "{:?}", decision.rejected);
println!("{}", serde_json::to_string_pretty(&decision).unwrap());
}
#[test]
fn huge_timeouts_are_clamped_instead_of_panicking() {
let engine = JevEngine::new("test-key").with_timeout(Duration::MAX);
assert_eq!(engine.timeout(), Duration::from_secs(60));
}