system2 0.1.0

An (unofficial) idiomatic Rust SDK for TypeSafe's System One model, Jev.
Documentation
//! Tests of tuples, arrays, `Vec`, and `Option` of question sets.

mod support;

use serde_json::json;
use support::{
    INSTRUCTIONS, STATE, answer_set_response, client_for, mock_api, no_request_sent, noul,
    noul_answer, noul_response, only_request, request_body, request_text,
};
use system2::{AskError, Choice, InvalidJson, Noul, Score};

const REQUEST_ID: &str = "req_01a13d1fbca4728f8f7c1cdd312bd3a5";

#[tokio::test]
async fn ask_sends_a_tuple_in_one_request() {
    let server = mock_api(answer_set_response(&[
        &noul_answer(0.92),
        &json!({
            "type": "choice",
            "choice": "go",
            "confidence": 0.7,
            "probabilities": { "rust": 0.3, "go": 0.7 },
        }),
        &json!({
            "type": "score",
            "score": 0.2,
            "confidence": 0.8,
            "probabilities": { "0": 0.8, "1": 0.2 },
        }),
    ]))
    .await;
    let question_set = (
        noul(INSTRUCTIONS),
        Choice::new("Which language?", ["rust", "go"]).unwrap(),
        Score::new("How big?", ["A line.", "A module."]).unwrap(),
    );

    let (spam, language, size) = client_for(&server).ask(question_set, STATE).await.unwrap();

    assert_eq!(spam.probability(), 0.92);
    assert_eq!(*language.most_likely(), "go");
    assert_eq!(*size.most_likely(), "A line.");
    assert_eq!(
        request_text(&only_request(&server).await),
        concat!(
            r#"{"model":"jev-latest","state":"Buy now!","questions":{"#,
            r#""q0":{"type":"noul","instructions":"Is this message spam?"},"#,
            r#""q1":{"type":"choice","instructions":"Which language?","criteria":{"rust":null,"go":null}},"#,
            r#""q2":{"type":"score","instructions":"How big?","criteria":["A line.","A module."]}}}"#,
        )
    );
}

#[tokio::test]
async fn ask_sends_nested_question_sets_in_one_request() {
    let server = mock_api(answer_set_response(&[
        &noul_answer(0.1),
        &noul_answer(0.2),
        &noul_answer(0.3),
        &noul_answer(0.4),
    ]))
    .await;
    let question_set = (
        [noul("Is it spam?"), noul("Is it urgent?")],
        vec![noul("Is it a reply?")],
        (Some(noul("Is it rude?")), None::<Noul>),
    );

    let ([spam, urgent], replies, (rude, none)) =
        client_for(&server).ask(question_set, STATE).await.unwrap();

    assert_eq!(spam.probability(), 0.1);
    assert_eq!(urgent.probability(), 0.2);
    assert_eq!(replies[0].probability(), 0.3);
    assert_eq!(rude.unwrap().probability(), 0.4);
    assert!(none.is_none());
    let body = request_body(&only_request(&server).await);
    let instructions: Vec<_> = (0..4)
        .map(|index| body["questions"][format!("q{index}")]["instructions"].clone())
        .collect();
    assert_eq!(
        instructions,
        [
            "Is it spam?",
            "Is it urgent?",
            "Is it a reply?",
            "Is it rude?"
        ]
    );
    assert_eq!(body["questions"].as_object().unwrap().len(), 4);
}

#[tokio::test]
async fn ask_sends_a_vec_in_one_request() {
    let server = mock_api(answer_set_response(&[&noul_answer(0.1), &noul_answer(0.9)])).await;
    let rules = ["Is it spam?", "Is it urgent?"];

    let question_set = rules
        .iter()
        .map(Noul::new)
        .collect::<Result<Vec<_>, _>>()
        .unwrap();
    let answer_set = client_for(&server).ask(question_set, STATE).await.unwrap();

    assert_eq!(answer_set[0].probability(), 0.1);
    assert_eq!(answer_set[1].probability(), 0.9);
    let body = request_body(&only_request(&server).await);
    assert_eq!(body["questions"]["q1"]["instructions"], "Is it urgent?");
}

#[tokio::test]
async fn ask_sends_no_request_for_a_question_set_with_no_questions() {
    let server = mock_api(noul_response(0.92)).await;
    let client = client_for(&server);

    let empty = client.ask(Vec::<Noul>::new(), STATE).await.unwrap();
    let none = client.ask(None::<Noul>, STATE).await.unwrap();

    assert!(empty.is_empty());
    assert!(none.is_none());
    assert!(no_request_sent(&server).await);
}

#[tokio::test]
async fn ask_refuses_a_bad_state_for_a_question_set_with_no_questions() {
    let server = mock_api(noul_response(0.92)).await;

    let error = client_for(&server)
        .ask(Vec::<Noul>::new(), "")
        .await
        .unwrap_err();

    assert!(matches!(error, AskError::State(InvalidJson::Empty)));
}

#[tokio::test]
async fn ask_refuses_answers_that_do_not_fit_the_questions() {
    let cases = [
        (
            vec![noul_answer(0.1)],
            "must answer each question: `q1` has no answer",
        ),
        (
            vec![noul_answer(0.1), noul_answer(0.2), noul_answer(0.3)],
            "must answer only the questions of the request, not `q2`",
        ),
        (
            vec![
                noul_answer(0.1),
                json!({
                    "type": "choice",
                    "choice": "go",
                    "confidence": 0.7,
                    "probabilities": { "rust": 0.3, "go": 0.7 },
                }),
            ],
            "must answer each question: unknown variant `choice`, expected `noul`",
        ),
    ];
    for (answers, message) in cases {
        let answers: Vec<_> = answers.iter().collect();
        let response =
            answer_set_response(&answers).insert_header("x-typesafe-request-id", REQUEST_ID);
        let server = mock_api(response).await;

        let error = client_for(&server)
            .ask((noul("Is it spam?"), noul("Is it urgent?")), STATE)
            .await
            .unwrap_err();

        assert!(matches!(error, AskError::InvalidResponse { .. }));
        assert_eq!(
            error.to_string(),
            format!("the response {message} (request ID `{REQUEST_ID}`)")
        );
    }
}