# How to chain probes (`Probe.depends_on`)

> **You want to:** thread a value extracted from one probe's
> response into a subsequent probe — e.g., "POST a resource and
> capture its id, then GET the resource at that id" or "obtain a
> bearer token and pass it as `Authorization` to every other
> probe".
> **You have:** a Suite where one probe's response shape depends
> on another probe having run first.
> **You'll end up with:** a Suite that runs in dependency order
> automatically; values flow via JSONPath extraction into
> ``{var}`` template placeholders.

This recipe closes
[PAPER-39](https://codeberg.org/magogi-admin/mgf-common/src/branch/main/FEEDBACK.md)
(filed by inthewords during its DRF starter-Suite adoption — the
token-obtain endpoint had to be hit outside apiprobe to thread
its result into subsequent probes).

## Steps

### 1. Use `Probe.depends_on(probe_id, extract={...})` to declare the dependency

```python
from mgf.apiprobe import Probe, Suite

create_room = (
    Probe.post("/rooms/")
    .id("create-room")
    .body({"name": "general"})
    .expect_status(201)
)

verify_room = (
    Probe.get("/rooms/{slug}/messages/")
    .id("verify-room")
    .depends_on("create-room", extract={"slug": "$.slug"})
    .expect_status(200)
)

suite = Suite(probes=[create_room, verify_room], checks=[])
```

The Suite topologically orders probes by their `.depends_on()`
declarations and runs them serially when ANY probe has
dependencies. Suites with no dependencies still use the
fast parallel-gather path — `depends_on` is opt-in.

### 2. Auth idiom — bearer token threaded into `Authorization`

The probe chain subsumes the OAuth2 / DRF token-obtain flow that
used to require a pre-Suite httpx call. The trick: extract the
token, then use `{var}` substitution in a header:

```python
auth = (
    Probe.post("/auth/token/")
    .id("auth-bootstrap")
    .body({"username": "alice", "password": "..."})
    .expect_status(200)
)

profile = (
    Probe.get("/me/")
    .id("profile")
    .depends_on("auth-bootstrap", extract={"tok": "$.token"})
    .header("Authorization", "Token {tok}")
    .expect_status(200)
)
```

`scheme="Token"` doesn't need a special `ChainedAuth` class —
template substitution + `Probe.header` give the same shape with
no new API surface. Django REST Framework's `Token` scheme,
GitHub's lowercase `token`, AWS-style schemes — all express
naturally.

### 3. Multiple dependencies + nested JSONPath

A probe can depend on more than one upstream, and JSONPath
expressions can reach into nested objects:

```python
seed = Probe.post("/seed/").id("seed").body({"name": "x"})

verify = (
    Probe.get("/lookup/")
    .id("verify")
    .depends_on("seed", extract={"slug": "$.data.room.slug"})
    .depends_on("auth-bootstrap", extract={"tok": "$.token"})
    .header("Authorization", "Token {tok}")
    .query("filter", "{slug}")
)
```

Substitutions happen in: path, header values, query-param values.
JSON body templating lands in a follow-up; for v1 of this feature,
build the request body explicitly per probe.

## Verify

```bash
python -m my_app.apiprobe_suite
```

Expected: probes execute in dependency order; the human report
prints findings keyed by probe id; no `suite.deps.*` findings if
the chain is healthy.

If you see `suite.deps.extraction_failed`, the JSONPath didn't
match. If you see `suite.deps.template_unresolved`, a `{var}` in
your path / header references a variable no upstream extracted.
If you see `suite.deps.skipped_due_to_upstream`, an upstream
failed — fix the upstream first.

## Common pitfalls

- **Cycles raise at Suite construction.** Two probes that
  `.depends_on` each other can't be ordered. The Suite raises
  `SuiteLogicError("cycle detected")` listing the cycle
  members. Break the cycle by removing one declaration.

- **Dangling references raise at Suite construction.** If you
  `.depends_on("typo-id", ...)` and no probe in the Suite has
  `probe_id="typo-id"`, the Suite raises `SuiteLogicError` with
  the list of available ids.

- **Template variables must be present at substitution time.** A
  `{slug}` in a path needs a matching `extract={"slug": "..."}`
  in some upstream's `.depends_on`. If you forget the extract,
  you'll see `suite.deps.template_unresolved` at run time.

- **JSONPath against non-JSON bodies fails fast.** If the
  upstream probe's response body isn't JSON, the suite emits
  `suite.deps.extraction_failed` with `cause: body-not-json`.
  Either fix the response (consumers usually want the upstream
  to return JSON anyway) or extract from the body manually
  pre-Suite.

- **Probes with dependencies run serially.** The Suite's
  parallel-gather path is disabled as soon as any probe has a
  `.depends_on()` declaration — the dependency order requires
  serial execution within waves. For suites where some probes
  are independent of others, this still costs time. Future work
  (PAPER-39 follow-up) may add parallel-wave execution; for now,
  splitting independent probes into a separate Suite is the
  workaround.

- **Templates use Python `str.format` semantics.** Literal braces
  in path / header values require doubling: `{{` for a literal
  `{`. Most consumers won't hit this, but be aware if your URL
  template legitimately contains braces.

## Cross-references

- [`mgf-common/FEEDBACK.md` PAPER-39](https://codeberg.org/magogi-admin/mgf-common/src/branch/main/FEEDBACK.md)
  — the consumer feedback this recipe closes.
- [`mgf-apiprobe/PUBLIC_API.md`](../../PUBLIC_API.md) — the
  `Probe.depends_on` signature + `ProbeDependency` dataclass.
- [`how-to-tune-findings-for-local-dev.md`](how-to-tune-findings-for-local-dev.md)
  — `severity_overrides` for noisy environments.
