# Testing instrumented code

Assert your own calls carry the labels you expect, in your own test suite, with no proxy and no network.

Source: https://tokentriage.com/docs/sdks/testing

---
import Callout from '../../../components/Callout.astro';
import NextSteps from '../../../components/NextSteps.astro';
import Details from '../../../components/Details.astro';
import CodeTabs from '../../../components/CodeTabs.astro';

Instrumentation rots quietly. Somebody refactors a call out of its `step` block, the labels stop
being set, and nothing fails — you find out a month later when a dashboard cannot answer a
question it used to answer.

The fix is a test that asserts your calls carry the labels you expect. The SDK ships a small kit
for exactly that, and it needs no proxy, no network and no fixtures of your own.

## What you get

Two pieces per language, and they mirror each other.

| Python | TypeScript | What it does |
|---|---|---|
| `RecordingTransport(...)` | `recordingFetch(...)` | captures each request's final headers, returns a canned response |
| `assert_tt_headers(...)` | `assertTtHeaders(...)` | asserts the expected `x-tt-*` headers, with a readable diff |

Both are optional modules. Python's needs `httpx` installed, since it builds on that transport
interface; TypeScript's is built in.

"Final headers" is the important part: what is captured is the map **after** the adapter has
injected, which is what actually would have gone on the wire.

## A test that catches the refactor

<CodeTabs>
  <Fragment slot="python">
  ```python
  import tokentriage as tt
  from tokentriage.testing import RecordingTransport, assert_tt_headers

  def test_my_calls_are_attributed():
      tt.init(base_url="http://tt.local")
      transport = RecordingTransport(response_headers={"x-tt-request-id": "tt_test"})
      client = tt.httpx_client(transport=transport, base_url="http://tt.local")

      with tt.trace(agent="my-agent", user="u-1"):
          with tt.step(step_class="user_turn"):
              resp = client.post("http://tt.local/v1/chat/completions", json={})
              rcpt = tt.receipt(resp)

      assert_tt_headers(
          transport.last_request_headers,
          {"x-tt-agent": "my-agent", "x-tt-user": "u-1", "x-tt-step-class": "user_turn"},
      )
      assert rcpt.request_id == "tt_test"
  ```
  </Fragment>
  <Fragment slot="typescript">
  ```ts
  import * as tt from "tokentriage";
  const { recordingFetch, assertTtHeaders } = tt.testing;

  test("my calls are attributed", async () => {
    tt.init({ baseUrl: "http://tt.local" });
    const rec = recordingFetch({ responseHeaders: { "x-tt-request-id": "tt_test" } });
    const f = tt.wrapFetch(rec, { baseUrl: "http://tt.local" });

    await tt.trace({ agent: "my-agent", user: "u-1" }, async () => {
      await tt.step({ stepClass: "user_turn" }, async () => {
        const r = await f("http://tt.local/v1/chat/completions", { method: "POST", body: "{}" });
        expect(tt.receipt(r).requestId).toBe("tt_test");
      });
    });

    assertTtHeaders(rec.requests[0].headers, {
      "x-tt-agent": "my-agent",
      "x-tt-user": "u-1",
    });
  });
  ```
  </Fragment>
</CodeTabs>

`http://tt.local` does not resolve and nothing tries to reach it. The captured headers are what
the adapter built:

```text
x-tt-trace-id  = bbbf47dfd02974f1483f8759f5598148
x-tt-step-id   = e0aad7fe4d96b2f7
x-tt-agent     = my-agent
x-tt-step-class= user_turn
x-tt-user      = u-1
```

Move the call outside the `step` block and the assertion on `x-tt-step-class` fails, which is the
whole point.

## When it fails

The diff names the header, both values, and what was present, so you do not have to print the map
yourself:

```text
assert_tt_headers mismatch:
  'x-tt-agent': expected 'wrong-agent', got 'my-agent'
  x-tt-* headers present: x-tt-agent, x-tt-step-class, x-tt-step-id, x-tt-trace-id, x-tt-user
```

TypeScript prints the same shape and throws an error named `AssertionError`, matching what Python
raises, so a shared CI log reads the same either way.

Matching is case-insensitive. Both take an `absent` argument for the opposite assertion — that a
header is **not** there, which is how you test that off-origin requests stay clean.

## Canned responses

The recorder returns a TokenTriage-shaped response you control, so you can test your handling of
states that are awkward to produce live:

<CodeTabs>
  <Fragment slot="python">
  ```python
  RecordingTransport(
      response_headers={"x-tt-request-id": "tt_x", "x-tt-cache": "hit",
                        "x-tt-cache-kind": "semantic", "x-tt-cache-verified": "none"},
      status=200,
  )
  ```
  </Fragment>
  <Fragment slot="typescript">
  ```ts
  recordingFetch({
    responseHeaders: { "x-tt-request-id": "tt_x", "x-tt-cache": "hit",
                       "x-tt-cache-kind": "semantic", "x-tt-cache-verified": "none" },
    status: 200,
  });
  ```
  </Fragment>
</CodeTabs>

A 429 with refusal headers, an unverified semantic cache hit, a response with no request id at
all — each is a header dict away, and each exercises a branch of your code that is otherwise hard
to reach.

<Callout type="note">
For a receipt on its own you do not need the kit at all. `receipt({...})` builds one from a plain
header mapping, so a unit test of your receipt handling is one call with no transport involved.
</Callout>

## What it will not fabricate

The kit builds wire shapes. It does not build money.

There is no way to construct a `PRICED` cost in either language, in a test or anywhere else,
because `Cost` has no public constructor and the only producers are the receipt parser and the
ledger row mapper over server JSON. If you want to test your handling of a priced row, feed your
code a row from a real fetch, or test the branch through `render` output rather than by minting a
cost object.

That is a deliberate limit rather than a missing feature. A test fixture that can mint a dollar is
a path by which a dollar can be minted.

<Details summary="Testing the disabled path">

Set `TOKENTRIAGE_DISABLED` in the test environment and assert `headers()` is empty and your
request still succeeds. That is worth one test, because the off state is the configuration your
service will run in the first time somebody sets the variable in a deployment and forgets.

Receipt parsing keeps working while disabled, so a test that asserts a receipt still parses under
the kill switch is checking the behaviour you actually want: instrumentation off, reading still
safe.

</Details>

<NextSteps items={[
  { title: 'Adapters', href: '/docs/sdks/adapters', description: 'The injection the recorder is capturing the output of.' },
  { title: 'Reading the receipt', href: '/docs/sdks/receipts', description: 'What receipt() builds, including from a plain header map in a test.' },
  { title: 'Diagnostics', href: '/docs/sdks/diagnostics', description: 'The codes to expect in test output, and which are normal there.' },
  { title: 'From your call to the wire', href: '/docs/sdks/emission', description: 'The value rules your assertions are checking the result of.' }
]} />
