# Fakes for integrations

A test never reaches a real provider, the outside service an integration
talks to. Each provider is described to your app by a package
([Integrations](external-services.md)). Every provider call a test reaches
answers through its package's fake, with the operation's generated
default, unless the test's `fakes:` scripts it.

## Scripting answers: `fakes:`

A script lists the responses an operation gives, in order. Each is a value
of the operation's output type, or one of the provider conditions the
integration maps, such as `rate_limited`
([Calling](calling.md)). The response still goes through
the integration's real code: input checks, retries, time limits and
failure mapping. So a test proves how your code handles each outcome.

```jadpo
test "a rate-limited reminder is retried" {
    fakes: {
        ReminderMail.send_overdue_reminder: [rate_limited, ReminderReceipt { accepted_at: "2026-01-15T12:00:00Z" }]
    }
    ...
    var sent: List<ReminderMessage> = ReminderMail.send_overdue_reminder.requests
    assert list.count(sent) == 2
}
```

Here the first send is rate-limited and the retry is accepted, so the code
sent two requests.

## What the code sent: `.requests`

`Integration.operation.requests` lists what the code sent, as typed
values with secrets left out.

## What can't be faked

A fake can't run code of its own, and nothing else in the application can
be faked. A package's action runs its real code in a test, and `fakes:`
scripts the operations it calls, its own package's included.

## The number of answers

If the code calls an operation more times than the fake has responses, or
fewer, the test fails and says so (`test.fake_exhausted` or
`test.fake_unused`). A wrong count is a mistake in the test or the code,
not a provider outcome.

## An example

Here a courier's booking is faked three ways: with a scripted
`AcmeCouriers.Booking`, with the operation's `slot_taken`, which the
action maps to its own failure, and unscripted, with the generated
default:

```jadpo
use app { couriers }
use shipping/failures { SlotGone }
use shared/providers/acme_couriers

test "the courier's reference is kept" {
    fakes: {
        couriers.bookings.create: [
            AcmeCouriers.Booking {
                id: "b-17"
            }
        ]
    }
    var booking: AcmeCouriers.Booking = attempt book("N1 9GU")
    assert booking.id == "b-17"
}

test "a taken slot is the app's own failure" {
    fakes: {
        couriers.bookings.create: [slot_taken]
    }
    assert book("N1 9GU") fails SlotGone
}

test "an unscripted call answers with the generated default" {
    var booking: AcmeCouriers.Booking = attempt book("N1 9GU")
    assert booking.id == "example"
}
```

## Values a fake answers with

A test scripts a value of the operation's output type, as Jadpo code
sees it. The fake then encodes it as the provider would send it, with the
package's `wire_name:` spellings and `output_at:` envelope, and reads it
back through the operation's real decoding, as a real answer is. So a
scripted answer exercises the package's encoders and decoders too.

So is a generated default. It is a value of the output type that keeps
the type's rules:

- text of its least length, `example` where that fits;
- a number at its least;
- an enum's first plain variant;
- empty lists and maps;
- `none` for what is optional;
- the test's clock for an instant;
- a fresh UUID.

A type whose rules no such value keeps, such as one with a pattern,
faults where the answer is read, naming the field and the rule. The test
then scripts that operation instead.

## Provider calls and capabilities in tests

Every provider call in a test is faked, as in `jadpo dev`. Nothing can be
let through to a provider's sandbox from a test.

### Provider operations

A provider operation answers with its package fake's valid defaults
unless the test scripts it:
`fakes: { stripe.payment_intents.create: [card_declined, ...] }`. A
fake's `lost` answers as a call that may have acted, and its `in_use` as
`unavailable`. Each keeps keys as the next section says.

A scripted answer is a value of the clean output type or one of the
operation's provider failures. The test never writes the wire shape: the
fake encodes the value and decodes it, as "Values a fake answers with"
says. `jadpo calls script` sets the same answers by hand in
`jadpo dev` ([Debugging and review](debugging-and-review.md)).

### Several changing calls

An action can make more than one outside call that changes something,
such as creating a customer and then charging them. Jadpo keeps a record
of its steps, and each call carries an idempotency key, so a rerun after a
crash picks up where it stopped
([Several changing calls in one action](several-changing-calls-in-one-action.md)).

In a test, fakes honour those keys. A repeated step under the same key
gets the stored answer. A script can also answer `lost`, meaning the
request went and no answer came back, or `in_use`, meaning another runner
holds the key.

`jadpo test` reruns every test that reaches such an action, once with a
crash at each step boundary: before the send, after the send, and after
the answer is recorded. This is on by default, and the test must pass
every time. The report names the boundary of any run that fails.

A keyed call's answer is kept under its key:

- The same key and request get the first answer again, without using up a
  scripted one.
- A different request under the same key gets the package's `reused:`
  answer.
- A failure is kept as the provider keeps it, unless its kind is
  `unavailable` or `rate_limited`.
- `lost` acted, so what it did, the generated default, answers the next
  call under its key.
- `in_use`, `unavailable` and `misconfigured` did nothing, and keep
  nothing.

A test that passes, and reached steps under a record, runs again once for
each boundary of each step it reached. Each run uses its own copy of the
database and a fresh fake. The crash stops the run with nothing it wrote
kept. The delivery is given back and runs again at once from its record,
and the crash doesn't count as an attempt. A route's request is sent
again under its key.

A delivery stops as `diverged` when a rerun doesn't match the steps its
record holds. One that stops as `diverged` between a test's steps ends the
test as [`test.subscriber_failed`](../errors/test.subscriber_failed.md). A
crash's failure begins with a `crash` line, also in `--json`:

```text
FAIL  a chosen plan starts one subscription
 --> domains/billing/actions/choose.test.jadpo:3

  crash: before step 1 of `subscribe`, `pay.customers.create`, went out; then it ran again
  delivery: `PlanChosen` to subscriber `start_subscription` stopped as diverged on attempt 1 of 10
```

If a crash leaves the fake's effects different from the run without one,
for example a call that acted under a new key, that is a fault in Jadpo,
`test.fault`.

A crash sends its step again. The `ai` fake, or a call with no key,
answers that with the next scripted answer, as a model or provider asked
again would answer afresh. So `test.fake_exhausted` and `test.fake_unused`
are judged on the run without a crash only.

A request or delivery that a crash sends again keeps none of the `ai`
answers the crashed run was given, as a restarted runner keeps none. So a
model asked before the steps is asked again, and a different answer
diverges there, as it would in production. Why it works this way is in
the [Jobs and events design notes](https://github.com/jadpo-dev/jadpo/blob/main/docs/language/notes/12-jobs-and-events.md).

### One fake per capability

A capability is a service Jadpo provides itself, such as email, which a
package can fill ([Email](email.md)). `email.sent`
lists the mail the code sent. There is no `sms.sent`, since SMS is a
package whose calls are faked like any other. A package that fills a
capability isn't faked separately: the capability's fake stands in for
every connection that fills it.

### Documents

`documents.pdf` returns a placeholder PDF, and `documents.html` the real
rendered HTML, which a test can assert on
([Documents](documents.md)).

### Bot checks

Bot checks pass through a local driver. A test scripts a failure with
`fakes: { bot_check: [bot_check_failed] }`, and the request is refused
with `403 bot_check_failed` before the action runs.

### Search

Search uses PostgreSQL's full-text search, whatever engine production
names ([Search](search.md)).

### OAuth grants

An OAuth grant lets the app call another service on a user's behalf
([Acting for each user: OAuth grants](acting-for-each-user-oauth-grants.md)).
A test declares its grants at the top, beside `fakes:`, as
`grants: { calendar: [alice] }`: the connection's name, and the fixtures
whose records own a grant, as `jadpo dev grants add` makes them in
development. A call on behalf of an owner with no grant fails
`grant_missing`, and a script can answer `grant_revoked`.

### Files

In a test, an in-memory store takes the place of every `files`
connection ([Files](files.md)). It can be told to
fail a read (`unavailable`) or a write (`storage_full`). A real file sits
in a domain's `fixtures/` folder, such as `fixtures/files/customers.csv`,
and a test turns it into a stored `File` with
`fixture_file("customers.csv", store: imports)`.

### Batches

A batch run works through many items, such as the rows of a file, a few
at a time ([Batches: work over many items](batches-work-over-many-items.md)).
In a test, batches run to the end within the test, after the action that
started them commits, like events. `jadpo test` runs them in batches of
two items and simulates a restart between every batch, so every import or
export test also tests resuming. A test asserts on what the app recorded
and on the summary `finished:` received, and can read `batches.status`.

### Not built yet

These parts of fakes aren't built yet:

- `.requests`, apart from the `ai` fake's `ai.<prompt>.requests`.
- The capabilities' fakes, apart from `email.sent` and the `ai`
  capability's: its fake, `fakes: { ai.<prompt>: }`,
  `ai.<prompt>.requests`, `recorded("<name>")`, the fake's embeddings and
  `fakes: { ai.embed: }` ([Testing AI calls](ai-tests.md)).
- `fixture_file`, and telling the file store to fail from a test. The
  in-memory store itself is built.
- Running batches to the end: a test sees a run as it starts, and a
  file's run reads two rows a batch on a test's host.

What is built is every call a test reaches answering through its
package's fake, and a test's `fakes:` scripting an operation's answers in
order, each a value of its output type, one of its failures by its code,
or a condition. Why fakes work this way is in the
[Integrations design notes](https://github.com/jadpo-dev/jadpo/blob/main/docs/language/notes/11-integrations.md).
