# test labeler

Last verified: 2026-04-15

## Purpose

Implements `atproto-devtool test labeler <target>`, a conformance suite that
validates an atproto labeler across four stages — identity, HTTP,
subscription, and crypto — and produces a structured report plus an exit
code (0 if all spec-required checks pass, 1 otherwise). Each stage is built
around an injected I/O seam so integration tests can replay fixtures
instead of talking to real servers.

## Contracts

- **Public entry points**:
  - `LabelerCmd::run(no_color) -> Result<ExitCode, miette::Report>` (in
    `labeler.rs`) — constructs the shared reqwest client and calls
    `pipeline::run_pipeline`.
  - `pipeline::parse_target(raw, explicit_did) -> LabelerTarget` — the
    accepted target grammar is frozen: handle, `did:*`, or `https://` URL
    (HTTP is rejected with a helpful error; raw endpoints with no DID
    simply skip identity/crypto).
  - `pipeline::run_pipeline(target, LabelerOptions) -> LabelerReport` — the
    one orchestrator that every test hits.
- **Per-stage entry points**: `identity::run`, `http::run`,
  `subscription::run`, `crypto::run`. Each returns a `*StageOutput` with an
  `Option<*Facts>` (populated only when the stage succeeds enough to let
  downstream stages run) plus a `Vec<CheckResult>`.
- **Report shape**: `report::{LabelerReport, CheckResult, CheckStatus,
  Stage, SummaryCounts, ReportHeader, RenderConfig}`. Five-way
  `CheckStatus`: `Pass`, `SpecViolation`, `NetworkError`, `Advisory`,
  `Skipped`. Exit code semantics: `1` if any `SpecViolation` is
  recorded; else `2` if any `NetworkError` is recorded; else `0`.
  `SpecViolation` takes precedence over `NetworkError` so that a
  conformance bug is never masked by an unrelated reachability
  failure. `Advisory` and `Skipped` never influence the exit code.
- **Check IDs are stable strings** (e.g. `"identity::target_resolved"`,
  `"http::first_page_decodes"`, `"crypto::rollup"`). They appear verbatim
  in insta snapshots under `tests/snapshots/`; renaming one is a breaking
  change to the CLI output contract.
- **Diagnostic codes are stable strings** (e.g.
  `"labeler::identity::labeler_endpoint_parseable"`). Same deal — snapshots
  pin them.

## Dependencies

- **Uses**: `crate::common::identity` for every network hop and DID
  primitive. `atrium-api` for labeler record + queryLabels types (we go
  through `serde_json` + atrium types, never through `atrium-xrpc-client`).
  `reqwest` and `tokio-tungstenite` only via the `RealHttpTee` and
  `RealWebSocketClient` seams.
- **Used by**: `crate::cli` wires this into the clap command tree; nothing
  else depends on it.
- **Boundary**: Stage modules talk to each other only through `*Facts`
  structs passed by `pipeline::run_pipeline`. A stage must not import
  another stage's internals.

## Key decisions

- **Every I/O boundary is a trait**: `HttpClient` + `DnsResolver` from
  `common::identity`, plus stage-local `RawHttpTee` (HTTP stage) and
  `WebSocketClient` / `FrameStream` (subscription stage). All four are
  injectable through `LabelerOptions`. The CLI passes real clients; tests
  pass fakes from `tests/common/mod.rs`.
- **Shared reqwest client**: `LabelerCmd::run` builds one reqwest client
  with rustls + 10s timeout + user-agent and threads it through every
  stage. Do not construct fresh clients inside stages.
- **Two-connection subscription strategy**: the subscription stage tries
  to observe backfill via an idle-gap heuristic, then on `ExceededBudget`
  or `StreamClosedDuringBackfill` reconnects live-tail to distinguish a
  healthy long backfill from a stuck stream. Outcome is captured in the
  `BackfillOutcome` / `LiveTailOutcome` enums.
- **Crypto stage falls back to PLC history**: if the current signing key
  fails to verify a label, `did:plc` targets retry against historic keys
  from the PLC audit log (`plc_history_for_fragment`). `did:web` has no
  history, so a failure there is a hard `SpecViolation`. Verification that
  only succeeds against a historic key still passes the stage but emits an
  `Advisory`.
- **DRISL-CBOR canonicalization for label signing**: crypto stage
  implements the deterministic CBOR canonicalization in
  `canonicalize_label_for_signing` rather than pulling a library — label
  signing uses a specific sort order and tag encoding that no existing
  crate matches. See `crypto.rs` for the spec.
- **Every check always emits a result**: stages never short-circuit on the
  first failure. When a prerequisite fails, downstream checks in the same
  stage still emit `Skipped` rows with a reason. When a
  whole stage is blocked upstream, the pipeline emits one
  `<stage>::not_run` row per downstream stage with a reason string.
- **Identity facts gate downstream stages**: HTTP and crypto stages only
  run when identity populated `IdentityFacts`. Subscription can run from
  an explicit endpoint URL without identity (the
  `LabelerTarget::Endpoint { did: None }` path).
- **Crypto pulls labels from HTTP and/or subscription**: the crypto stage
  runs when identity succeeded and *either* the HTTP stage produced
  `HttpFacts` *or* the subscription stage collected at least one
  `sample_labels` entry. Labels from both sources are concatenated before
  verification so a JSON-decoded `queryLabels` page and a CBOR-decoded
  `subscribeLabels` frame are both exercised. Subscription samples are
  capped at `subscription::SAMPLE_LABEL_CAP` to bound memory on noisy
  streams.

## Invariants

- Every `CheckResult` with `status == SpecViolation` carries a `diagnostic`
  with a non-empty `#[source_code]` — the report renderer uses miette's
  `GraphicalReportHandler` and a missing source span degrades output.
- `LabelerReport::exit_code` returns `1` if any `SpecViolation` is
  recorded, `2` if not but at least one `NetworkError` is recorded,
  and `0` otherwise. Advisories and skipped checks never fail the run.
- Snapshot tests under `tests/snapshots/` are part of the contract. Any
  check ID, diagnostic code, or rendered line change must be accompanied
  by a reviewed `cargo insta review`.
- The pipeline never calls `reqwest::Client::new()` or constructs a
  tokio-tungstenite connection outside of `Real*` seam structs.

## Key files

- `labeler.rs` — clap args, `LabelerCmd::run`, CLI bootstrap.
- `pipeline.rs` — `LabelerTarget`, `LabelerOptions`, `parse_target`,
  `run_pipeline` orchestration.
- `report.rs` — `CheckStatus`, `CheckResult`, `LabelerReport`,
  `RenderConfig`, rendering via `miette::GraphicalReportHandler`.
- `identity.rs` — identity stage: DID resolution, labeler record fetch
  (through `atrium-api` types over the `HttpClient` seam), policy
  validation.
- `http.rs` — HTTP stage: `RawHttpTee` trait, `RealHttpTee` reqwest
  implementation, first-page / pagination / cursor checks against
  `com.atproto.label.queryLabels`.
- `subscription.rs` — WebSocket stage: `WebSocketClient` / `FrameStream`
  traits, CBOR frame decoder, two-connection backfill / live-tail logic.
- `crypto.rs` — label canonicalization, signature verification, PLC key
  history fallback.

## Gotchas

- `LabelerTarget::Endpoint { did: None }` runs HTTP and subscription but
  skips identity and crypto. Emitting those as "blocked" rather than
  "skipped — no DID supplied" is a regression.
- `RawHttpTee::query_labels(cursor)` must NOT duplicate the first-page
  request for reachability — the stage previously pinged before the real
  request, doubling traffic against real servers.
- `CheckResult::diagnostic` is `Option<Box<dyn Diagnostic + Send + Sync>>`
  — when you add a new failure case, wire the diagnostic through the
  whole way. Snapshots will expose "diagnostic: None" as a rendered
  blank block if you forget.
- The CLI pipes through `tracing` with `EnvFilter` — `--verbose` toggles
  `DEBUG`. Per-stage instrumentation is load-bearing for the
  `verbose_flag_accepted` CLI test.
- Fixture layout under `tests/fixtures/labeler/<stage>/<case>/` is
  referenced by test helper `gen_fixtures` anchored to
  `CARGO_MANIFEST_DIR`. Empty case directories need a `.gitkeep`.
