postjevsql.git / tests / score.rs
score.rsannotatedscore.rssource115 lines · 5.0 KB · raw
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}