Testing PDT
How PDT’s own tests are run and what they must never touch. Before pushing, run the CI checks listed in SDLC.md.
Running them
- Rust:
cargo test(andcargo clippy --all-targets -- -D warnings,cargo fmt). CI runscargo nextest run: every test in its own process, all at once, so a test must not depend on another’s state or on running alone. The tests are built once into nextest archives (on Linux and Windows the unit tests and the integration tests apart, in two builds) and split across parallel jobs, each in a fresh checkout with the archives’target/(docs/SDLC.md “Build once, test in partitions”): a test may run in any job, and can rely only on the checkout, its own test binary and its package’s binaries (env!("CARGO_BIN_EXE_…")), not on anything else a build wrote. - Slow requests: a request still running after 2 s logs a warning (
crates/pdt-api/src/slow.rs), but the tests install no tracing subscriber and nextest hides a passing test’s output. WithPDT_SLOW_REQUESTS_LOGset to a file, each warning is also appended there as a line naming the test. CI’s test jobs set it, print the file in a “Slow requests” step (and the job summary) and upload it as theslow-requests-<os>-<partition>artifact. - Generated types:
cargo testwrites the TypeScript types (ts-rs) intoweb/src/api/gen. Commit them with the Rust change, or CI’s diff check fails. - Web:
pnpm --dir web test,typecheck,build.web/src/lint.test.tsenforces the design-system rules (web/ui/README.md). Helpers:stubApiandrenderWithQueryinweb/src/test-utils.tsx; a test that touches the file system needs// @vitest-environment node. - Site:
pnpm --filter site test(link rewriting, and the user docs’ check for internal references). - Desktop:
cargo test -p pdt-desktopneedspnpm --dir web buildfirst; plaincargo testskips that crate. - Core logic is test-first. Rules live in pdt-core as pure functions, so most of them are unit tests there; the
routes are tested through the router in
crates/pdt-api/tests/(helpers intests/common:call, andcall_as_uifor routes that need the UI token); the daemon end to end incrates/pdtd/tests/. - One integration test binary per crate: a crate’s
tests/*.rsfiles are modules of itstests/main.rs(theintegrationtarget; Cargo.toml setsautotests = false), not a binary each. A new file needsmod <name>;there; a test in eachmain.rs(test-support/all_listed.rs) fails when one is left out. Shared helpers (common,port_blocks) are declared once inmain.rsand imported withuse crate::common;. A test that changes the process environment gets its own[[test]]target instead (pdt-api’schecks_env). Run one file’s tests withcargo test -p pdt-api --test integration pm::. - Waiting: never sleep a fixed time and then assert.
test-support/wait.rs(common::waitin pdt-api’s tests) polls:wait_until(andwait_until_blocking) until a check holds, with one default timeout, andstill_afterfor “this does not happen within N ms”, which fails as soon as it does. A check returnsbool,OptionorResult(its error is shown when the wait times out).PDT_TEST_WAIT_SCALE=3triples every timeout on a slow machine. pdt-api’scommon::conversationreads and waits on a session’s events and state. - Agent settings: pdt-api’s tests build their state with
common::app_state, which puts the home folder and the agents’ config folders (~/.claude,~/.codex, Devin’s config) in the test’s temp folder.
Build output
Each worktree has its own target/, so its size multiplies by the number of worktrees. To keep it down:
- Less debug info: the dev profile (also
cargo test’s) keeps line tables only for PDT’s crates, so backtraces still show file and line, and none for dependencies (the workspaceCargo.toml). For a debugger that shows variables, build withCARGO_PROFILE_DEV_DEBUG=full. - One test binary per crate (above): each links all of PDT and its dependencies.
- A shared compile cache (optional): install sccache (
brew install sccache,cargo install sccache --locked) and setRUSTC_WRAPPER=sccachein your shell profile, orbuild.rustc-wrapper = "sccache"in~/.cargo/config.toml. The repository doesn’t set it, so builds without sccache keep working. Worktrees then share compiled dependencies instead of each building them again. sccache can’t cache crates built incrementally, which PDT’s own crates are in the dev profile: those still build per worktree.CARGO_INCREMENTAL=0caches them too, and dropstarget/debug/incremental, at the cost of slower rebuilds after an edit. Don’t share oneCARGO_TARGET_DIRbetween worktrees: agents building at once would wait on cargo’s lock. - Stale artifacts:
scripts/clean-targets.shremoves build output cargo hasn’t written for 7 days (--days N) from the main checkout’starget/and every.worktrees/*/target, or from the folders given;--dry-runlists it. Cargo rebuilds what it needs later (from sccache when set up). A worktree’scargo cleanremoves all of its own.
What tests never touch
| Never | Use instead |
|---|---|
| Port 7420, or a bare random port | Ports PDT picks for runs come from a block claimed with test-support/port_blocks.rs (one range per test crate, listed in that file) |
The real data folders (pdt, pdt-dev) | A temp folder, as PDT_DATA_DIR |
| The OS keychain | PDT_SECRET_STORE=memory |
| Tailscale | PDT_TAILSCALE_BIN pointing at fake-tailscale (crates/pdt-tailscale/src/bin) or a missing file |
| GitHub, GitLab | PDT_GITHUB_OFFLINE=1 (takes every forge offline), GithubApi::offline(), recorded answers (pdt_forge::transport::testing::Recorded), or FakeForge (crates/pdt-forge) |
| Real agents | fake-agent (crates/pdt-api/src/bin/fake-agent: Claude, Codex app-server and ACP); PDT_CLAUDE_PROGRAM, PDT_CODEX_PROGRAM, PDT_DEVIN_PROGRAM name the program |
| The machine’s free disk space | AppState::new reads none (no disk is ever low); set AppState::disk_space to a fake |
| A real pdtd under pdt-host | fake-pdtd (crates/pdt-host/src/bin) |
A new daemon test copies the harness in crates/pdtd/tests/common: it drops every inherited PDT_* variable, then
sets the dev profile, an explicit port and data folder, in-memory secrets, a missing Tailscale, offline GitHub and
missing agent programs.
Cross-platform
Tests pass on Linux, macOS and Windows. Build fixture paths from std::env::temp_dir() (/srv/code is not absolute
on Windows), and label the PR windows for any path, process or file-system change so the Windows checks run.
Snapshots
crates/pdtd/src/integrations/tests.rspins, insnapshot.jsonandpm_snapshot.md, every tool agents and the PM get (with their flags), the node kinds’ specs and the PM’s prompt. A new tool, node kind or prompt change shows up there: update the snapshot on purpose and read the diff.- A test in
crates/pdt-api/src/guides.rschecks that the guides pdtd serves link to each other by API path, never by repository path. - A test in
crates/pdt-storerefuses a migration named with a number instead of its UTC creation time, and duplicate versions.
Looking at the app
- A dev instance for the user is the repository’s pdt run (Run → pdt on the branch’s worktree).
- For an agent (screenshots, Playwright): build
target/debug/pdtdandweb/dist, thenscripts/scratch-daemon.sh start(Windows:scripts\scratch-daemon.ps1). It printsport=,dir=,url=; stop it withscripts/scratch-daemon.sh stop <port>, which also deletes its folder. Set thePDT_*_PROGRAMvariables to a stand-in, or new sessions start the real agents. - Seeds (
PDT_SEED, dev profile only, on a database with no projects;crates/pdtd/src/seed.rs):demofills in sample projects, tickets, sessions and runs;websiteis the made-up Acme Shop project the website’s screenshots are taken from (seed/website.rs,scripts/site-screenshots.mjs, site/README.md “Screenshots”). Nothing is started by either. - Never open the stable UI (7420) in a test browser: attaching a terminal resizes the user’s real PTY.