---
title: "Record fixtures"
description: "Verify a provider's reads against the live forge."
canonical_url: "https://forges.link/contributing/record-fixtures"
---
# Record fixtures

> Verify a provider's reads against the live forge.

A provider can only declare a capability `true` when a test exercises it. For reads, the most reliable test is one that replays responses recorded from the live forge, so that the provider's parsing is checked against what the forge really sends. This page explains how to record those responses.

To record a forge, pass its name to `pnpm record-fixtures`, together with a token:

```sh
GITHUB_TOKEN=ghp_... pnpm record-fixtures github
```

The command runs the shared steps in `test/recording/steps.ts` against the forge. It writes the responses to `test/fixtures/<forge>/recorded/<host>/`, with a `manifest.json` that names the repository and threads it read. Each step gets its own directory under `steps/`, which holds:

- the responses the step received, as numbered fixtures named after the request, with ids and SHAs replaced by `N`, `SHA` or `ID`;
- a `step.json` with the step's name, when it was recorded, and the steps whose findings it read, in `needs`;
- the step's normalised output, in `golden.json`.

Responses received outside any step, such as the instance version, go to `setup/`. When it replays a step, the test serves only the step's own responses, and then those in `setup/`.

The recorder also reads a `.env` file at the root of the repository. A variable that is already set in your environment takes precedence over `.env`, so if you export a token globally, unset it to record with the one in `.env`:

```sh
env -u GITHUB_TOKEN pnpm record-fixtures github
```

A step that throws keeps its earlier recording, if it has one and none of the steps it reads from were recorded since. Otherwise it's removed, along with its error responses. A step that handles an error response and still returns, such as a fallback, keeps that response in the recording.

A step whose responses are the same as before, apart from rate limits, ETags, counters such as stars, activity timestamps and the expiry of signed download URLs, keeps its files and its `recordedAt`. Recording a forge again only changes the steps whose responses changed.

## Record some steps

To record only some read steps, name them with `--only`, by name or by directory name:

```sh
pnpm record-fixtures github --only 'ci log' --only checks
```

The recorder runs the steps up to the last one connected to those you name through `needs`, because later steps read what earlier ones found. It writes the steps you name. It also writes the connected steps whose responses changed, so that every step still replays against the others. The repository, threads and credentials must be the same as in `manifest.json`; otherwise, record every step. The setup repositories, such as `FIXTURE_<FORGE>_SCRATCH_REPO`, come from `manifest.json` when you don't set them.

## Record anonymously

Some providers make different requests without credentials, and some forges refuse reads without credentials that they allow with a token. To record what an anonymous provider sees, pass `--anonymous`:

```sh
pnpm record-fixtures gitlab --anonymous
```

The recorder ignores any token in the environment and writes to `test/fixtures/<forge>/recorded/<host>-anonymous/`, next to the authenticated recording. A run with no token set records anonymously too. The manifest sets `anonymous: true`, and the replay creates the provider without credentials.

The replay counts towards the [anonymous check](#check-the-capabilities). When a step fails with `AuthenticationRequiredError`, the forge refuses that read without credentials. Declare it `false` for an anonymous provider, as in `verb(!anonymous, run)`.

## Record writes

Writes are recorded against a scratch repository, separately from reads. Pass `--writes` to open an issue and two pull requests there, then comment, react, label, assign, review, close, reopen and merge them, create, change and delete a webhook, and create and edit releases:

```sh
pnpm record-fixtures github --writes
```

The recording goes to `test/fixtures/<forge>/recorded/<host>-writes/`. It keeps the body of every request as well, and the replay checks that the provider sends the same bodies.

Each write step reads what the steps before it wrote in the same run, so a write recording is replaced as a whole. When a run doesn't record some steps that the recording has, such as when the forge rate-limits it, the recorder renames the run's identifiers to the committed ones and keeps the committed recording of those steps, as described in [Record some write steps](#record-some-write-steps). It still exits with an error, so that a step that has started to fail isn't hidden by its earlier recording. If vitest isn't installed, or the recording doesn't replay, the recorder keeps the earlier recording as a whole. To replace it anyway, without those steps, pass `--allow-fewer`.

The recorder sends writes at least a second apart. When the forge rate-limits a request and names a wait of up to five minutes, the recorder waits and sends the request again, and the limited response isn't recorded. It waits a minute, once, for a limit that names no wait. When a limit persists or asks for a longer wait, the recorder stops before the next step.

### Keep write recordings stable

Every write recording opens new issues, pull requests and comments, so each run returns new numbers, ids and SHAs. To keep only the changes that matter, rename those identifiers to the ones the committed recording has in their place:

```sh
pnpm record-fixtures github --writes
node scripts/stabilise-recordings.ts
```

The script compares each changed write recording with the one committed at `HEAD`. Where the two hold different identifiers at the same place in the same request or response, it renames the new one to the old one, provided that the pairing is one to one and that the old identifier doesn't stand for something else in the new recording. It doesn't rename timestamps, counts or short numbers in text. A step whose responses are then the same as before, apart from timestamps, keeps its committed files.

The script keeps the renamed recording only if the recording replays both as recorded and once renamed, and if the golden output of every rewritten step is the recorded output with the same identifiers renamed. Otherwise, it leaves the recording as recorded. Responses it can't rename, such as compressed blobs, leave their recording as recorded too. The scheduled workflow runs it on every write recording.

### Record some write steps

To add write steps to the committed recording, or to record some of them again, name them with `--only`, by name or by directory name:

```sh
pnpm record-fixtures github --writes --only 'issue update to closed' --only 'pull update to closed'
```

The recorder runs every write step up to the last one you name, so that each step finds the forge as a full run would leave it. It then renames the identifiers that the run created to the committed ones, as `scripts/stabilise-recordings.ts` does, and writes the steps you name. It also writes the steps they read from, through `needs`, whose responses changed. Every other step, `setup/` and `manifest.json` stay as committed. A step that fails doesn't stop the others from being written, but the recorder then exits with an error. Install the dependencies first, as the recorder replays the recording with vitest.

The recorder writes nothing and exits with an error when:

- the run doesn't replay, either as recorded or once renamed, or renaming changes the golden output of a step;
- an identifier in a step it would write can't be renamed, or the manifest differs from the committed one once renamed, such as when the scratch repository changed;
- the committed recording doesn't replay with the new steps in it, with every golden output matching.

Steps also depend on each other through the forge's state, which `needs` doesn't record: a label set by an earlier step appears in a later response, and a closed thread changes what a later step receives. When an earlier step fails, a step you name can receive different responses from those of a full run. It still replays, because each step replays from its own responses, but record every step if the difference matters.

A write recording needs:

- a bot account that owns the scratch repository, with its token in the forge's token variable with `_BOT` added, such as `GITHUB_BOT_TOKEN` or `CODEBERG_BOT_TOKEN`;
- the scratch repository in `FIXTURE_<FORGE>_SCRATCH_REPO`, with a name that includes `forges-fixtures`, an open milestone and, for the rerun step, a CI workflow that has run on the default branch;
- your own token in the usual variable, such as `GITHUB_TOKEN`, for the steps a second account takes: reviewing, approving, and being added as a collaborator. Accept the collaborator invitation once, so that approving and merging works;
- to record `writes.transfer`, a second repository in `FIXTURE_<FORGE>_TRANSFER_REPO`, also with a name that includes `forges-fixtures`;
- where the reviewer is already a member of the scratch repository, such as through a GitLab group, a repository they are not a member of in `FIXTURE_<FORGE>_COLLABORATOR_REPO`, for `repos.addCollaborator`. The recorder removes them again afterwards;
- on Azure DevOps, which cannot say who signed in, your identity id in `FIXTURE_AZURE_DEVOPS_REVIEWER`, so that the recorder can request your review;
- on GitHub, to record writes to a discussion, the number of a discussion in the scratch repository in `FIXTURE_GITHUB_SCRATCH_DISCUSSION`;
- on GitHub, [immutable releases](https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/immutable-releases) turned on in the scratch repository, so that the recorder can check that GitHub refuses to retag a release.

Before the steps run, the recorder closes threads that an earlier run left open, and creates three branches with one commit each through the forge's own API. Afterwards it deletes the branches, the label and the webhook, and every release and tag whose name starts with `forges-fixture-`. On Codeberg, which refuses issues and pull requests whose titles are too similar to ones the account opened in the last hour, the title of each thread ends with a random suffix. The recorder can write to GitHub, GitLab, Bitbucket, Forgejo, Gitea, Gitee, Azure DevOps and Tangled.

On Tangled, the recorder pushes the branches to the knot over HTTPS. It signs in as the bot with `TANGLED_BOT_IDENTIFIER` and `TANGLED_BOT_PASSWORD`, and `TANGLED_BOT_PDS` when the identifier is a handle on a PDS other than `bsky.social`. A reviewer's records would go into its own account, so the reviewer is a second bot, in `TANGLED_REVIEWER_IDENTIFIER`, `TANGLED_REVIEWER_PASSWORD` and `TANGLED_REVIEWER_PDS`, rather than your account. Before and after the steps, it deletes the issues and pull requests that earlier runs opened, the bot's comments, states, labels and reactions on those and on the pull request in `FIXTURE_TANGLED_SCRATCH_PULL`, an AT-URI, and every label whose name starts with `forges-fixture-`.

The recorder refuses, before sending it, any write outside the scratch and transfer repositories, apart from the bot account's own notifications. On GitHub, a GraphQL mutation may only name ids that a response from those repositories returned. On Tangled, a record may only name the scratch repository, the bot's own records or the scratch pull request.

## Seed the scratch repository

Tangled reads are recorded against the scratch repository, so that its threads have labels, assignees and reactions. To put them there, pass `--seed` after recording writes, which change the labels and assignees of the scratch pull request:

```sh
pnpm record-fixtures tangled --writes
pnpm record-fixtures tangled --seed
pnpm record-fixtures tangled
pnpm record-fixtures tangled --anonymous
```

The seed uses the same bot accounts and records nothing. It keeps an issue titled `forges seed: issue` and the pull request in `FIXTURE_TANGLED_SCRATCH_PULL` with two comments, a `forges-seed` label, the bot as the assignee, and reactions on the thread and on a comment. The reviewer comments on the issue and reacts to it. The seed writes only what is missing, so you can run it again. It also writes the bot's profile, and stars the scratch repository as the bot.

## Record webhook deliveries

To record the deliveries that the forge sends, pass `--webhooks`. It uses the same bot account and scratch repository as `--writes`:

```sh
pnpm record-fixtures github --webhooks
```

The recorder creates a hook with a dummy secret, opens, comments on and closes an issue, and pushes a branch. It then reads the deliveries back and writes each one to `test/fixtures/<forge>/recorded/<host>-webhooks/` as `<name>.delivery.json`, with its headers and body, in place of the directory's earlier deliveries. The replay passes each delivery to `webhooks.ingest()`, which verifies its signature first.

GitHub and GitLab keep the headers and payload of each delivery, so the recorder reads them from the forge's API, and the hook points at `https://example.com/hook`. For other forges, the hook points at a [webhook.site](https://webhook.site) relay, which the recorder reads deliveries back from. Each body is redacted before it is kept, and is then signed again with the dummy secret.

## Replay and update snapshots

`test/recorded.test.ts` replays the recorded steps, each at the time it was recorded, and compares the normalised output of each step with the snapshot in its `golden.json`. When the output changes on purpose, update the snapshots by running:

```sh
pnpm vitest run test/recorded.test.ts -u
```

## Record on a schedule

The `fixtures` workflow records every forge each Monday: reads at 04:23 UTC and writes at 06:23 UTC. It opens or updates a pull request per kind, on the `fixtures/reads` or `fixtures/writes` branch, with a commit per forge whose recording changed, and runs CI on it. A forge whose responses didn't change gets no commit. To record now, run the workflow from the **Actions** tab, and choose the kind, the forges and the steps to record.

To record the fixtures of a pull request, name its branch in `ref`. The workflow records with the code on that branch, and opens a pull request into the branch from `fixtures/<kind>-<branch>`. Only branches of this repository can be recorded.

To run the same recordings locally, use `scripts/record-forge.ts`:

```sh
node scripts/record-forge.ts github --kind reads
```

Each forge records in its own GitHub environment, `fixtures-<forge>` for reads and `fixtures-<forge>-writes` for writes, which holds only that forge's variables and deploys from `main` only. A recording of another branch runs that branch's code with these variables, so record only branches whose code you have read. A write recording waits for you to approve it. The recording job installs no dependencies. It masks the credentials in the log, and searches the recording for them before uploading it. The job that opens the pull request has no forge credentials, and refuses an upload that holds anything other than fixture JSON.

As the recording job can't replay a recording, a write run that names steps, or that lost a step the committed recording has, saves the run in `partial.json` instead, with `--defer-splice`. The job that opens the pull request then splices it into the committed recording with `scripts/splice-partial.ts`.

To create the environments and store the variables of your `.env` in them, run the following command once, with `gh` signed in as a repository admin. Run it again to update the variables:

```sh
node scripts/fixtures-environments.ts --dry-run
node scripts/fixtures-environments.ts
```

To update only some forges, name each with `--forge`:

```sh
node scripts/fixtures-environments.ts --forge gitlab --forge tangled
```

## Record other targets

Most forges come with a default repository, issue and pull request to record. To record other ones, set environment variables when you run the command. For Azure DevOps and Cursor Origin there is no default, so you always have to name the repository. The variables also let you set the base URL when you want to record a self-hosted instance. The following sections list the variables of each forge.

Some reads need a repository that is set up for them. For these, every forge except Tangled accepts two more variables, named after the forge's prefix, such as `FIXTURE_GITHUB_`:

- `FIXTURE_<FORGE>_SCRATCH_REPO` names a repository that the credential administers. The recorder lists its webhooks and their deliveries. It also reads collaborators and permissions there when the recorded repository refuses them, and downloads a release asset from it when the recorded repository has no small one.
- `FIXTURE_<FORGE>_ALERTS_REPO` names a repository with security alerts to read. Without it, the recorder reads the alerts of the recorded repository.

For Azure DevOps, write both as `project/repo`.

The `user events` step reads the activity feed of `danielroe` on GitHub, GitLab, Codeberg and Gitea, of `danielroe.dev` on Tangled, and of the pull request's author on Gitee. To read another account on one of these forges, set `FIXTURE_<FORGE>_FEED_USER`, such as `FIXTURE_GITHUB_FEED_USER`.

### GitHub

```sh
GITHUB_TOKEN=ghp_... pnpm record-fixtures github
```

To record different targets, set `FIXTURE_GITHUB_REPO`, `FIXTURE_GITHUB_PULL`, `FIXTURE_GITHUB_ISSUE`, `FIXTURE_GITHUB_DISCUSSION_REPO` or `FIXTURE_GITHUB_DISCUSSION`, and to record a GitHub Enterprise Server instance, set `GITHUB_BASE_URL`.

Use a personal access token (classic), because GitHub doesn't let a fine-grained token read notifications. To record every step that the committed `github.com/` recording has, the token needs these scopes:

- `repo`, for notifications, collaborators and permissions, webhooks and their deliveries, and Dependabot, code scanning and secret scanning alerts;
- `read:org`, for collaborators, and for the teams and team members of the scratch repository's organization.

GitHub also only lists the collaborators of a repository to an account with push access to it. Without that, the recorder reads the collaborators of `FIXTURE_GITHUB_SCRATCH_REPO` instead, so the committed recording needs an account that can push to the recorded repository and administers the scratch repository. A token without one of the scopes fails the steps that need it.

To record as a GitHub App installation, pass `--app` and set the app's credentials. The recording goes to `github.com-app/`, and also reads the app's installations:

```sh
GITHUB_APP_ID=123 GITHUB_APP_PRIVATE_KEY_PATH=app.pem \
GITHUB_APP_INSTALLATION_ID=456 pnpm record-fixtures github --app
```

In place of `GITHUB_APP_PRIVATE_KEY_PATH`, you can set `GITHUB_APP_PRIVATE_KEY` to the PEM itself.

### GitLab

```sh
GITLAB_TOKEN=glpat-... FIXTURE_GITLAB_PROJECT=group/project \
FIXTURE_GITLAB_MR=1 pnpm record-fixtures gitlab
```

To add an issue, set `FIXTURE_GITLAB_ISSUE`, and to record a self-managed instance, set `GITLAB_BASE_URL`.

### Bitbucket

```sh
BITBUCKET_TOKEN=... FIXTURE_BITBUCKET_REPO=workspace/repo \
FIXTURE_BITBUCKET_PULL=1 pnpm record-fixtures bitbucket
```

To use Basic credentials, set `BITBUCKET_USERNAME` to your Atlassian email and `BITBUCKET_APP_PASSWORD` to an API token, in place of `BITBUCKET_TOKEN`. The variable keeps its old name, but Bitbucket no longer accepts app passwords.

### Forgejo

```sh
CODEBERG_TOKEN=... FIXTURE_FORGEJO_REPO=owner/repo \
FIXTURE_FORGEJO_PULL=1 pnpm record-fixtures forgejo
```

To add an issue, set `FIXTURE_FORGEJO_ISSUE`, and to record another instance, set `CODEBERG_BASE_URL`. The manifest then keeps the version of the instance, so the replay gates Actions the same way. With `CODEBERG_BASE_URL=https://code.forgejo.org`, the recorder signs in with `CODE_FORGEJO_TOKEN` when it is set, and records PR 1765 of `forgejo/runner` by default.

### Gitea

```sh
GITEA_TOKEN=... FIXTURE_GITEA_REPO=owner/repo FIXTURE_GITEA_PULL=1 \
pnpm record-fixtures gitea
```

To add an issue, set `FIXTURE_GITEA_ISSUE`, and to record a self-hosted instance, set `GITEA_BASE_URL`.

To set up accounts and tokens on a local instance that runs in the official Docker image, run the CLI as the `git` user, because it refuses to run as root. The `forgejo` CLI in the Forgejo image behaves the same way:

```sh
docker exec --user git <container> gitea admin user generate-access-token --username <user> --scopes all
```

### Gitee

```sh
GITEE_TOKEN=... FIXTURE_GITEE_REPO=owner/repo FIXTURE_GITEE_PULL=1 \
pnpm record-fixtures gitee
```

To add an issue, set `FIXTURE_GITEE_ISSUE`.

### Azure DevOps

```sh
AZURE_DEVOPS_TOKEN=... \
FIXTURE_AZURE_DEVOPS_ORGANIZATION=acme \
FIXTURE_AZURE_DEVOPS_PROJECT=Widgets \
FIXTURE_AZURE_DEVOPS_REPO=widgets FIXTURE_AZURE_DEVOPS_PULL=1 \
pnpm record-fixtures azure-devops
```

To add a work item, set `FIXTURE_AZURE_DEVOPS_ISSUE`.

An anonymous recording ignores these variables, because a new organization cannot have public projects. It reads PR 5 of `dnceng-public/public/dotnet-public-wiki` instead. To change that, set the same variables with `ANONYMOUS_` after `FIXTURE_AZURE_DEVOPS_`, such as `FIXTURE_AZURE_DEVOPS_ANONYMOUS_PULL`.

### Cursor Origin

```sh
CURSOR_AUTH_TOKEN=... FIXTURE_CURSOR_ORIGIN_REPO=owner/repo \
FIXTURE_CURSOR_ORIGIN_PULL=1 pnpm record-fixtures cursor-origin
```

### Pushin.eu

```sh
PUSHIN_TOKEN=... FIXTURE_PUSHIN_REPO=pjullrich/pushin \
pnpm record-fixtures pushin
```

To choose the threads to record, set `FIXTURE_PUSHIN_ISSUE` and `FIXTURE_PUSHIN_PULL`. The pull request is read from `danielroe/roe.dev-test` unless you set `FIXTURE_PUSHIN_PULL_REPO`. To record another deployment, set `PUSHIN_BASE_URL`. The recorder redacts `pun_pat_` tokens, so the fixtures don't contain your token.

### Cloudflare Artifacts

```sh
CLOUDFLARE_ARTIFACTS_TOKEN=... CLOUDFLARE_ARTIFACTS_ACCOUNT_ID=... \
pnpm record-fixtures cloudflare-artifacts
```

The token needs **Artifacts** > **Read**. Your account id never reaches the fixtures. The recorder names the example account from Cloudflare's docs, `023e105f4ecef8ad9ca31a8372d0c353`, and swaps in the real one on the way to the API.

The recordings read three imports of one small repository: `forges-fixtures/forges` at depth 2, so its oldest commit has no parent, `forges-fixtures/forges-fork`, a fork of it, and `forges-fixtures-more/forges-history` with every commit. Its commits add files, change only the mode of one, rename one and edit one. To record another repository, set `FIXTURE_CLOUDFLARE_ARTIFACTS_REPO`.

To set up an account the same way, build the repository with fixed authors and dates, so its commits get the shas the recordings name, and push it to a public repository:

```sh
git init -q -b main forges-artifacts-fixture && cd forges-artifacts-fixture
export GIT_AUTHOR_NAME='forges fixtures' GIT_AUTHOR_EMAIL='fixtures@forges.link'
export GIT_COMMITTER_NAME="$GIT_AUTHOR_NAME" GIT_COMMITTER_EMAIL="$GIT_AUTHOR_EMAIL"
commit() { GIT_AUTHOR_DATE="$1" GIT_COMMITTER_DATE="$1" git -c commit.gpgsign=false commit -qm "$2"; }
mkdir -p src scripts
printf '# forges-artifacts-fixture\n' >| README.md
printf 'export const answer = 42\n' >| src/index.ts
printf '#!/bin/sh\necho build\n' >| scripts/build.sh
git add . && commit 2026-10-01T10:00:00Z 'chore: initial commit'
chmod +x scripts/build.sh && git add scripts/build.sh && commit 2026-10-01T10:01:00Z 'chore: make the build script executable'
git mv src/index.ts src/main.ts && commit 2026-10-01T10:02:00Z 'refactor: rename the entry'
printf '# forges-artifacts-fixture\n\nA repository for the Cloudflare Artifacts\n recordings of forges.\n' >| README.md
git add README.md && commit 2026-10-01T10:03:00Z 'docs: describe the fixture'
git rev-parse HEAD # 95d9018dd1ac8cbec290cdcc3f104445160d1135
```

Then import it with a token that has **Artifacts** > **Edit**. Each import is a copy, so the public repository can go once they finish:

```sh
url=https://github.com/<you>/forges-artifacts-fixture
api="https://api.cloudflare.com/client/v4/accounts/$CLOUDFLARE_ARTIFACTS_ACCOUNT_ID/artifacts/namespaces"
h=(-H "Authorization: Bearer $EDIT_TOKEN" -H 'Content-Type: application/json')
curl -sS "${h[@]}" -X POST "$api" -d '{"namespace":"forges-fixtures"}'
curl -sS "${h[@]}" -X POST "$api/forges-fixtures/repos/forges/import" -d "{\"url\":\"$url\",\"branch\":\"main\",\"depth\":2,\"read_only\":true}"
curl -sS "${h[@]}" -X POST "$api/forges-fixtures/repos/forges/fork" -d '{"name":"forges-fork","read_only":true,"default_branch_only":true}'
curl -sS "${h[@]}" -X POST "$api" -d '{"namespace":"forges-fixtures-more"}'
curl -sS "${h[@]}" -X POST "$api/forges-fixtures-more/repos/forges-history/import" -d "{\"url\":\"$url\",\"branch\":\"main\",\"read_only\":true}"
```

The `own repos` step records the first namespace of the account, so keep nothing else in the account.

### Tangled

Recording Tangled needs no credentials, so without them the recording goes to `tangled.org-anonymous/`:

```sh
pnpm record-fixtures tangled
```

To record notifications too, sign in with an app password and name the notification service:

```sh
TANGLED_IDENTIFIER=alice.example TANGLED_PASSWORD=... \
TANGLED_NOTIFICATIONS_URL=https://... pnpm record-fixtures tangled
```

The recorder finds the account's PDS from a DID identifier. For a handle on a PDS other than `bsky.social`, set `TANGLED_PDS` too.

To record other threads, set `FIXTURE_TANGLED_ISSUE` and `FIXTURE_TANGLED_PULL` to AT-URIs, and to record through a Slingshot cache, set `TANGLED_RECORDS_URL`.

## Check the capabilities

We run `pnpm test:capabilities` on every commit to `forges`. During the unit tests, `test/setup/verbs.ts` records each verb that a test calls, along with where the forge data it used came from: a recording, a fixture hand-authored from the forge's documentation (marked `handAuthored: true`), or nowhere, for a call that sends no request. A response that a test overrides or builds inline counts for none of these. The check then fails in two cases:

- a provider declares `true`, `'experimental'` or `'emulated'`, and no call verifies it;
- a provider declares `'unverified'`, and a call verifies it.

The same check runs for anonymous providers, against the calls made without credentials, which the anonymous recordings cover. Provider pages and the [capability matrix](https://forges.link/reference/capability-matrix) describe the same declarations, and a unit test fails when they differ.

The check reads the calls from the last `pnpm test:unit` run, in `test/.verbs/`. A run of only some test files, a run filtered with `-t` or `FORGES_RECORDING`, or a run with failing tests leaves calls out, so the check refuses it. Run `pnpm test:unit` in full before `pnpm test:capabilities`.

A fixture written from documentation is a stand-in until the operation is recorded. To list the capabilities that only such fixtures verify, run:

```sh
node scripts/check-capabilities.ts --documented
```

To list the capabilities no recording exercises at all, with and without credentials, run:

```sh
pnpm recorded-coverage
```

The script replays the recordings with the verb tracker on and prints a table per forge. Pass `--out <file>` to write it to a file.


## Sitemap

See the full [sitemap](https://forges.link/sitemap.md) for all pages.
