1//! `jev_score(row, q, levels)` and `jev_score_norm` (contract *SQL 2//! surface*): the probability-weighted level of a Score, and that level 3//! scaled to [0, 1]. A Score takes 2 to 10 levels, sent in the caller's 4//! order, and the two functions share one judgment and one cache row. 5 6use serde_json::{Value, json}; 7use tokio_postgres::error::SqlState; 8use support::jev_instance; 9use support::mock_jev::{MockJev, Reply}; 10 11/// Answers every question in a request: a Noul 0.5, and a Score at level 12/// `id`·0.5 of however many levels it was sent. 13async fn graded() -> MockJev { 14 MockJev::start(|req| { 15 let body: Value = serde_json::from_slice(&req.body).unwrap(); 16 let id = body["state"]["id"].as_f64().unwrap_or(0.0); 17 let mut answers = serde_json::Map::new(); 18 for (qid, q) in body["questions"].as_object().unwrap() { 19 let answer = match q["type"].as_str() { 20 Some("score") => { 21 let n = q["criteria"].as_array().unwrap().len(); 22 let level = (id * 0.5).min((n - 1) as f64); 23 let legend: serde_json::Map<_, _> = 24 (0..n).map(|i| (i.to_string(), q["criteria"][i].clone())).collect(); 25 let mut probabilities: serde_json::Map<_, _> = (0..n).map(|i| (i.to_string(), json!(0.0))).collect(); 26 probabilities.insert("0".into(), json!(1.0)); 27 json!({ "type": "score", "score": level, "confidence": 0.8, "legend": legend, "probabilities": probabilities }) 28 } 29 _ => json!({ "type": "noul", "noul": 0.5 }), 30 }; 31 answers.insert(qid.clone(), answer); 32 } 33 Reply::json(200, json!({ "model": "jev-1.13.0", "answers": answers, "usage": { "input_tokens": 296, "output_tokens": 20 } })) 34 }) 35 .await 36} 37 38const LEVELS: &str = "ARRAY['calm', 'annoyed', 'angry', 'furious', 'livid']"; 39 40#[tokio::test(flavor = "multi_thread")] 41async fn level_and_normalized_share_one_judgment() { 42 let mock = graded().await; 43 let (_pg, client) = jev_instance(&mock).await; 44 client.batch_execute("INSERT INTO tickets VALUES (2, 'b'), (4, 'c')").await.unwrap(); 45 46 let sql = format!( 47 "SELECT id, jev_score(t, 'How angry?', {LEVELS}), jev_score_norm(t, 'How angry?', {LEVELS}) 48 FROM tickets t ORDER BY id" 49 ); 50 let rows = client.query(&sql, &[]).await.unwrap(); 51 let got: Vec<(i32, f64, f64)> = rows.iter().map(|r| (r.get(0), r.get(1), r.get(2))).collect(); 52 assert_eq!(got, [(1, 0.5, 0.125), (2, 1.0, 0.25), (4, 2.0, 0.5)]); 53 54 // One question per row: the two calls shared it. 55 let requests = mock.requests(); 56 assert_eq!(requests.len(), 3); 57 for r in &requests { 58 let body: Value = serde_json::from_slice(&r.body).unwrap(); 59 assert_eq!(body["questions"].as_object().unwrap().len(), 1, "{body}"); 60 } 61 62 // The levels went in the caller's order, lowest first. 63 let sent = String::from_utf8(requests[0].body.to_vec()).unwrap(); 64 assert!( 65 sent.contains(r#""criteria":["calm","annoyed","angry","furious","livid"]"#), 66 "levels out of order in {sent}" 67 ); 68 69 // One cache row per row, and a rerun of either function reads it. 70 let cached: i64 = client.query_one("SELECT count(*) FROM jev_cache", &[]).await.unwrap().get(0); 71 assert_eq!(cached, 3); 72 let norm: f64 = client 73 .query_one(&format!("SELECT jev_score_norm(t, 'How angry?', {LEVELS}) FROM tickets t WHERE id = 4"), &[]) 74 .await 75 .unwrap() 76 .get(0); 77 assert_eq!(norm, 0.5); 78 let level: f64 = client 79 .query_one(&format!("SELECT jev_score(t, 'How angry?', {LEVELS}) FROM tickets t WHERE id = 2"), &[]) 80 .await 81 .unwrap() 82 .get(0); 83 assert_eq!(level, 1.0); 84 assert_eq!(mock.requests().len(), 3, "served from the cache"); 85 86 // Reordering the levels is another question. 87 client 88 .query("SELECT jev_score(t, 'How angry?', ARRAY['livid', 'furious', 'angry', 'annoyed', 'calm']) FROM tickets t WHERE id = 1", &[]) 89 .await 90 .unwrap(); 91 assert_eq!(mock.requests().len(), 4); 92} 93 94#[tokio::test(flavor = "multi_thread")] 95async fn two_to_ten_levels() { 96 let mock = graded().await; 97 let (_pg, client) = jev_instance(&mock).await; 98 99 for levels in ["ARRAY['only']", "ARRAY['a','b','c','d','e','f','g','h','i','j','k']", "ARRAY['a', NULL]"] { 100 for f in ["jev_score", "jev_score_norm"] { 101 let err = client 102 .query(&format!("SELECT {f}(t, 'How angry?', {levels}) FROM tickets t"), &[]) 103 .await 104 .expect_err("refused"); 105 assert_eq!(err.as_db_error().expect("an ERROR").code(), &SqlState::INVALID_PARAMETER_VALUE, "{f} {levels}"); 106 } 107 } 108 assert!(mock.requests().is_empty(), "refused before sending"); 109 110 // Both ends of the range are sent. 111 for levels in ["ARRAY['no','yes']", "ARRAY['a','b','c','d','e','f','g','h','i','j']"] { 112 client.query(&format!("SELECT jev_score(t, 'How angry?', {levels}) FROM tickets t"), &[]).await.unwrap(); 113 } 114 assert_eq!(mock.requests().len(), 2); 115}