1# Chapter 18¾: jev-client/src/client — a stage play for one client
2
3This folder holds one file, [tests.rs](tests.rs): the tests for
4`Client::ask`. Each test writes a script of what the "server" will do, hands
5it to a fake transport, and runs the real retry loop against it on a clock
6that only moves when told to.
7
8## The fake world
9
10Three fakes share one `Fake` struct, which keeps the script, a log of
11events, every request sent, a count of distrusted connections, and the
12clock:
13
14- **`FakeTransport`** plays the script one `Step` per send: `Reply { status,
15  headers, body }`, `Fail(TransportErrorKind)`, or `Stall`, which never
16  answers.
17- **`FakeRuntime`** keeps virtual time. `sleep` adds its duration to the
18  clock and returns at once. `timeout` polls the future once: if it is ready
19  it returns the output, and if not (a `Stall`) it adds the whole timeout to
20  the clock and returns `None`. `jitter` is always 0, so every backoff is
21  exact.
22- **`FakeObserver`** writes each `Event` as a line of text, so a test can
23  compare the whole story at once.
24
25`run(script)` builds a client over the three, asks one `Noul`, and polls the
26future exactly once. With fakes that never leave it waiting, one poll runs
27the whole ask, retries and sleeps included. If it does not finish, the test
28fails with "fakes never leave the client pending".
29
30> **Aside.** This is why the client takes its clock as a port. A real
31> 30-second budget test would take 30 seconds; here
32> `the_budget_bounds_everything` checks the clock reads exactly 30 s and
33> finishes instantly.
34
35## What the tests pin down
36
37| Test | Shows |
38| --- | --- |
39| `answers_first_time` | One attempt, no time passes, the authorization header is marked sensitive. |
40| `honours_the_servers_delay` | A `429` with `retry-after-ms: 300` waits exactly 300 ms, and the retry carries `x-typesafe-retry-count: 1`. |
41| `backs_off_then_gives_up_with_the_last_request_id` | Three `503`s: 0.5 s then 1 s of backoff, three attempts, the last request id kept. |
42| `other_4xx_fail_at_once` | A `403` is not retried. |
43| `context_overflow_is_not_resent` | `max_tokens_exceeded` comes back as `ContextOverflow` at once. |
44| `never_sent_is_redialled_not_retried` | One `NotSent` is resent with no backoff and no retry header. |
45| `repeated_never_sent_becomes_a_retry` | After two redials, a third `NotSent` is a retry. |
46| `unretryable_transport_failures_fail_at_once` | `Tls`, `TooLarge` and `Config` are not retried. |
47| `a_stalled_attempt_times_out_distrusts_the_connection_and_retries` | A stall costs 10 s, the connection is distrusted, the retry answers. |
48| `the_budget_bounds_everything` | Three stalls end in `TimedOut` at exactly 30 s. |
49| `transport_errors_keep_an_earlier_request_id` | An id from an earlier `503` survives later transport failures. |
50| `a_wrong_answer_carries_its_request_id` | A 200 from another model is a `Response` error with its id. |
51| `a_success_reports_its_request_id_and_usage` | The events of a clean answer. |
52| `a_retry_reports_its_cause_delay_and_request_id` | The events of two retries, with causes and delays. |
53| `a_redial_is_reported_and_is_not_a_retry` | The events of a redial. |
54
55## Try it
56
57```
58cargo test -p jev-client
59```
60
61## For the people who maintain it
62
63### In this folder
64
65| File | What |
66| --- | --- |
67| [tests.rs](tests.rs) | The fakes, `run`, and the fifteen tests above. |
68| [CLAUDE.md](CLAUDE.md) | Invariants for agents. |
69
70← Previous: [Chapter 18½: inside jev-client/src](../) · Up: [jev-client/src](../) · Next: [Chapter 19: jev-worker](../../../jev-worker/) →