# atproto-devtool

Last verified: 2026-04-15

Single-crate Rust 2024 binary providing developer tooling for the atproto
ecosystem. Today the only shipped subcommand is `test labeler`, which runs a
conformance suite against an atproto labeler.

## Tech stack

- Rust 2024, single crate (binary + library). Library target exists so
  integration tests under `tests/*.rs` can reach pipeline internals via
  `atproto_devtool::commands::test::labeler::...`.
- Async runtime: tokio (rt + macros + time + net only — not full).
- HTTP: reqwest with rustls. WebSockets: tokio-tungstenite with
  rustls-tls-native-roots.
- CLI: clap derive. Diagnostics: miette (fancy) + thiserror.
- Crypto: k256 + p256 (ECDSA), sha2, multibase.
- atproto types: atrium-api (not atrium-xrpc-client — we use reqwest directly
  through our own seams so every network hop is mockable).
- Testing: insta snapshots, assert_cmd for CLI end-to-end.

## Commands

- `cargo build` / `cargo run -- test labeler <target>`
- `cargo test` — unit + integration + snapshot tests.
- `cargo insta review` — review snapshot diffs after test runs.
- `cargo fmt` / `cargo clippy -- -D warnings` — both must be clean.
- `cargo read <crate>` / `cargo read --api <crate>` / `cargo read --docs <crate>`
  — preferred over browsing docs.rs.

## Project structure

- `src/main.rs` — thin tokio bootstrap over `cli::run`.
- `src/lib.rs` — re-exports everything under the same tree so integration
  tests in `tests/` can reach pipeline internals.
- `src/cli.rs` — clap root, tracing subscriber wiring (`--verbose` toggles
  `DEBUG`), `NO_COLOR`/`--no-color` handling.
- `src/commands/test/labeler/` — the `test labeler` subcommand. See
  `src/commands/test/labeler/CLAUDE.md` for pipeline architecture.
- `src/common/identity.rs` — DID/handle/multikey/PLC primitives shared across
  stages. See `src/common/CLAUDE.md`.
- `src/common/diagnostics.rs` — small helpers for building `NamedSource` and
  spans against JSON payloads.
- `tests/` — one integration binary per stage (`labeler_identity.rs`,
  `labeler_http.rs`, `labeler_subscription.rs`, `labeler_endtoend.rs`,
  `labeler_cli.rs`) plus shared fakes in `tests/common/mod.rs`.
- `tests/fixtures/` — JSON + binary CBOR fixtures per stage. The
  `tests/snapshots/` tree holds insta snapshots.
- `docs/implementation-plans/2026-04-13-test-labeler/` — the phased
  implementation plan that produced this codebase. Still useful for grepping
  rationale on individual decisions.

## Conventions

- No `mod.rs`. Use the `foo.rs` + `foo/` sibling-file layout.
- `#[expect(...)]` over `#[allow(...)]` for every lint suppression.
- Imports at module top only (the one exception is `cfg()`-gated items).
- `uninlined_format_args = "deny"` is on in `Cargo.toml`. Always use
  `format!("{x}")`, never `format!("{}", x)`.
- All code comments end with a period. Sentence case for headings.
- Every `thiserror::Error` that is rendered to users must also derive
  `miette::Diagnostic` with a stable `code = "..."` string. Diagnostic codes
  appear in snapshot tests — changing one is a breaking change.
- Never use `#[serde(flatten)]` or `#[serde(untagged)]`; write explicit structs
  and/or custom visitors.
- Newtypes everywhere: `Did`, `Curve`, `AnyVerifyingKey`, etc. Don't let raw
  strings leak across stage boundaries.

## Testing pattern

Every network-touching stage goes through an injected trait object
(`HttpClient`, `DnsResolver`, `RawHttpTee`, `WebSocketClient`). Production
wires in `Real*` implementations; tests wire in fakes from
`tests/common/mod.rs`. Do not add network calls that bypass these seams —
they make stages untestable and break snapshot determinism.

Integration tests pin per-check output via insta snapshots. When a check's
rendered text, diagnostic code, or order changes, `cargo insta review` is the
expected workflow. Treat every `.snap` file under `tests/snapshots/` as part
of the public contract for the CLI.

## Boundaries

- Safe to edit: `src/`, `tests/`, `docs/`.
- The `docs/implementation-plans/` tree is historical — update it only when
  the plan genuinely needs revising, not as part of regular code changes.
- `Cargo.lock` is committed; update it through `cargo` only.
