mock.rsannotatedmock.rssource81 lines · 3.4 KB · raw

The transport and the ledger-gated client against jev-mock's TLS endpoint, which trusts its own throwaway CA.

4use jev_client::ClientError;
5use jev_http::{Endpoint, Failure, Guards, Jev, Ledger};
6use jev_mock::{MockJev, Reply};
7use jev_protocol::{ApiErrorKind, Json, Key, ModelId, Noul, NoulAnswer, Questions};
8use serde_json::json;
10const MODEL: &str = "jev-1.13.0";
11const LOOSE: Guards = Guards { max_questions_per_minute: 30, max_dollars_per_hour: 1.0 };
12
13fn answer(p: f64) -> Reply {
14    Reply::json(
15        200,
16        json!({"model": MODEL, "answers": {"q": {"type": "noul", "noul": p}}, "usage": {"input_tokens": 1000, "output_tokens": 1}}),
17    )
18}
19
20fn client(mock: &MockJev, guards: Guards) -> Jev {
21    let dir = std::env::temp_dir()
22        .join(format!("jev-http-mock-{}", std::process::id()))
23        .join(jev_http::ledger::now().to_string());
24    let ledger = Ledger::at(dir).expect("a sandbox ledger");
25    let endpoint = Endpoint::parse(&mock.endpoint()).expect("the mock's URL").trusting(mock.ca_file());
26    Jev::new("test-key", "test", ledger, ModelId::pinned(MODEL).unwrap(), endpoint).unwrap().with_guards(guards)
27}
28
29fn one_noul() -> (Questions, Key<NoulAnswer>) {
30    let mut questions = Questions::new();
31    let key = questions.noul("q", Noul::new(Json::text("Is it raining?"))).unwrap();
32    (questions, key)
33}
34
35#[tokio::test]
36async fn answers_over_one_kept_connection_and_books_the_cost() {
37    let mock = MockJev::start(|_| answer(0.9)).await;
38    let jev = client(&mock, LOOSE);
39    let (questions, key) = one_noul();
40    for _ in 0..3 {
41        let answered = jev.ask(&Json::text("Austin, noon"), &questions, "test").await.unwrap();
42        assert!((answered.response.get(key).noul - 0.9).abs() < 1e-9);
43        assert_eq!(answered.attempts, 1);
44    }
45    assert_eq!(mock.requests().len(), 3);
46    assert_eq!(mock.connections(), 1, "the connection is kept warm between asks");
47    let status = jev.status().unwrap();
48    assert_eq!(status.unsettled_holds, 0);
49}
50
51#[tokio::test]
52async fn the_ledger_refuses_before_anything_is_sent() {
53    let mock = MockJev::start(|_| answer(0.5)).await;
54    let jev = client(&mock, Guards { max_questions_per_minute: 1, max_dollars_per_hour: 1.0 });
55    let (questions, _) = one_noul();
56    jev.ask(&Json::text("first"), &questions, "test").await.unwrap();
57    let refused = jev.ask(&Json::text("second"), &questions, "test").await.unwrap_err();
58    assert!(matches!(refused, Failure::Throttled { .. }), "{refused}");
59    assert_eq!(mock.requests().len(), 1);
60}
61
62#[tokio::test]
63async fn out_of_credit_stops_every_later_ask() {
64    let mock = MockJev::start(|_| Reply::json(402, json!({"detail": "no credit remaining"}))).await;
65    let jev = client(&mock, LOOSE);
66    let (questions, _) = one_noul();
67    match jev.ask(&Json::text("x"), &questions, "test").await.unwrap_err() {
68        Failure::Client(ClientError::Api { error, attempts }) => {
69            assert_eq!((error.kind, attempts), (ApiErrorKind::OutOfCredit, 1));
70        }
71        other => panic!("{other}"),
72    }
73    assert!(matches!(jev.ask(&Json::text("y"), &questions, "test").await.unwrap_err(), Failure::Budget(_)));
74    assert_eq!(mock.requests().len(), 1);
75    assert_eq!(jev.status().unwrap().unsettled_holds, 0, "a request refused outright was released");
76}
77
78#[test]
79fn a_plain_http_endpoint_is_refused() {
80    assert!(Endpoint::parse("http://api.typesafe.ai/v1/systemone").is_err());
81}