@atjam/tests#
Integration test harness for atjam. Drives real PDS writes against four test accounts (test-1 … test-4), then asserts via the same Constellation backlinks the web app uses for reads. Catches lexicon-shape regressions and (once invitation validation lands) bilateral correctness.
This is not unit tests — it talks to the PDS and to Constellation. Network-dependent, slower than unit tests by design. The point is to verify the system as it actually works, not as it's modeled.
Setup#
You need four ATProto accounts the harness can sign in to. By default the harness points at https://bsky.social; works against any ATProto-compliant PDS that exposes the standard server APIs.
Option 1 — Automated (bsky.social or self-hosted PDS)#
cd tests
GMAIL_ADDRESS=you@gmail.com pnpm create-accounts > .env
What happens:
- Calls
com.atproto.server.createAccountfour times, creatingtest-1.bsky.socialthroughtest-4.bsky.social(or whateverHANDLE_DOMAINyou point it at). - Plus-addresses your gmail per signup (
you+test-1@gmail.com, …) so verification emails all land in one inbox. - Mints an app password per account via
com.atproto.server.createAppPassword. - Writes a complete
.envblock to stdout containing handles, app passwords, and the rightPDS_URL.
You still need to click the verification email for each account in your inbox. Bsky.social may also push you to SMS verify on first use — handle that in the bsky.app UI if it happens.
Custom count: pnpm create-accounts 6 for six accounts (but only the first four are read by the harness today). Custom PDS: PDS_URL=https://your-pds pnpm create-accounts > .env.
Option 2 — Manual#
Create four accounts however you like (bsky.app web flow, goat, Tranquil UI), generate app passwords in each account's settings, then:
cd tests
cp .env.example .env
# edit .env, fill in TEST1_HANDLE / TEST1_APP_PASSWORD, etc.
.env is gitignored. Don't commit it.
Running#
pnpm --filter @atjam/tests test # run scenarios once, exit code reflects pass/fail
pnpm --filter @atjam/tests test:watch # vitest watch mode while developing
pnpm --filter @atjam/tests probe:lag # measure firehose → Constellation latency
Each scenario:
- Logs in to all four test accounts
- Resets each account's
at.atjam.*records (deletes whatever's there) - Runs the scenario's writes
- Asserts the resulting state via Constellation backlinks
- Cleans up again at the end
Reset means scenarios are hermetic — rerunning gives the same result, no accumulation.
Account naming#
The four accounts are referred to as test1, test2, test3, test4 internally (TS identifiers can't contain hyphens). The actual bsky handles use hyphens: test-1.bsky.social. Each scenario file aliases them locally to semantic roles for readability:
const organizer = agents.test1;
const participant1 = agents.test2;
const participant2 = agents.test3;
// test4 is reserved for chain/forgery scenarios
What's covered today#
| Scenario | Status | What it asserts |
|---|---|---|
open-round.test.ts |
runs | Organizer creates jam + open round; two participants sign up; one submits. Constellation returns the expected backlinks. |
hosted-validation.test.ts |
skipped | Invitation validation positive/negative cases. Waiting for the validation logic to exist in a reader. |
network-chain.test.ts |
skipped | Chain validation (organizer → A → B), forged-chain rejection, networkGate=contributed enforcement. Same — waiting for validator. |
When the invitation validator lands (likely in @atjam/lexicons or as a module in web/app/lib/), un-skip the stub files and replace it.todo(...) with real assertions. The setup pattern from open-round.test.ts is the template.
Probes#
probe:lag measures how long it takes a record written to the PDS to become visible via Constellation. Useful for:
- Sanity-checking the PDS → relay → indexer pipeline is alive
- Setting realistic
waitForBacklinkstimeouts in scenarios - Spotting if Constellation has gotten slower over time
Probes are plain TS scripts (no vitest), invoked via pnpm probe:*. Easy place to add one-off experiments without ceremony.
Adding a scenario#
- Drop a new file under
src/scenarios/*.test.ts. beforeAll→loginAll()thenresetAll(agents).- Write whatever sequence of records the scenario needs using helpers from
writes.ts. - Assert via
waitForBacklinks(handles firehose lag) or by directly fetching withlistMyRecords. afterAll→resetAll(agents)again so the next run starts clean.
Use types from @atjam/lexicons for record bodies — the compiler catches lexicon-shape mistakes before they reach the PDS.
Why this isn't unit tests#
- The behavior under test is bilateral — records on multiple PDSes that converge via the firehose. Unit tests can fake half of it; integration can't.
- Constellation is a real dependency — testing the read path with a fake indexer doesn't tell you whether production reads work.
- The lexicon is the contract. Real PDS writes validate it end-to-end; in-process mocks of
createRecorddon't.
The cost is speed and brittleness (network flakes, Constellation lag) — accepted in exchange for the realism.
Caveats with bsky.social#
The default PDS_URL is bsky.social, which works but has constraints:
- Records go to the global firehose. Anything you write during testing is public. Test data has throwaway content; just don't put anything sensitive in scenario inputs.
- Use disposable accounts.
reset.tsdeletes allat.atjam.*records on each account at the start of every run. Point the harness at burner handles, not your personal one. - Rate limits apply. Per-account write limits are tighter than a self-hosted PDS. ~15 writes per run is fine; running back-to-back-to-back dozens of times might trip them.
- Phone verification may gate things. Bsky started pushing SMS verify in 2024. The harness can sign in to a verified account fine; an unverified one may have limited capabilities.
For automated CI eventually, point at a self-hosted PDS via PDS_URL — no global firehose pollution, no rate limits, no SMS gates.