Autotests for agents
June 18, 2026 · View on GitHub
Guide for Cursor / Codex / CI bots: verify MailAgent on prod without human involvement (no real SMTP, no DATABASE_URL).
Operator connects secrets once → OPERATOR.md.
Quick start
npm ci
MAILAGENT_API_URL=https://api.webmailagent.com \
MAILAGENT_API_KEY=ma_… \
npm run test:prod:gate # CI after deploy (smoke only)
# Before merge or release:
npm run test:prod # full contracts + Playwright
CI after deploy runs test:prod:gate (smoke only — saves KV quota). Full suite: manual or tag v* publish workflow.
Test layers
| Layer | Command | Where | API key |
|---|---|---|---|
| Prod gate (CI) | npm run test:prod:gate | deploy / PR | MAILAGENT_API_KEY |
| Prod gate (full) | npm run test:prod | pre-merge, tag v* | MAILAGENT_API_KEY |
| Smoke agent | npm run smoke:agent | MCP, OAuth, DCR, Streamable HTTP | yes |
| Smoke QA | npm run smoke:qa | inbox lifecycle on prod | yes |
| Contract (all) | npm run test:contract:all | 13 scripts via simulate | yes |
| Playwright simulate | npm run test:pw:simulate | CI gate, no DATABASE_URL | yes |
| Typecheck | npm run check | PR, no prod | no |
| Codex scaffold | npm run verify:codex | PR | no |
| Unit (local) | npm run test:allowlist, test:extract, … | dev, not prod | no |
Contract tests do not send real mail: messages are injected via POST /v1/inboxes/:id/simulate.
Environment variables
| Variable | Required | Value |
|---|---|---|
MAILAGENT_API_KEY | yes | team key or legacy API_KEY |
MAILAGENT_API_URL | no | default https://api.webmailagent.com |
API_KEY | fallback | alias for MAILAGENT_API_KEY |
SMOKE_EXPECT_ATTACHMENTS | smoke:agent | "1" in CI |
Local key can live in .env — loaded by scripts/load-env.mjs.
Prod gate
CI (test:prod:gate) — scripts/test-prod-gate.mjs: smoke:agent → smoke:qa.
Full (test:prod) — scripts/test-prod.mjs:
Order:
smoke:agent— discovery, OAuth metadata, DCR, MCP session, tool callsmoke:qa— create → simulate → wait → extract → deletetest:contract:all— all contract-qa scriptstest:pw:simulate— Playwright simulate gate
Any failing step exits non-zero.
Contract scripts (one at a time)
Run a narrow script after changes in a specific area:
| Script | npm script | Covers |
|---|---|---|
contract-qa.mjs | test:contract:qa | create → simulate OTP → wait → extract |
contract-qa-agent.mjs | test:contract:qa:agent | GET /v1/agent, /v1/me, /mcp/auth |
contract-qa-agent-access.mjs | test:contract:qa:agent-access | short-lived scoped key broker |
contract-qa-callback.mjs | test:contract:qa:callback | webhook callback after simulate; set CONTRACT_CALLBACK_URL for strict non-2xx failure |
contract-qa-attachments.mjs | test:contract:qa:attachments | attachments + raw MIME |
contract-qa-threads.mjs | test:contract:qa:threads | threads / reply grouping |
contract-qa-domains.mjs | test:contract:qa:domains | custom domains (Resend quota → skip) |
contract-qa-search.mjs | test:contract:qa:search | message search |
contract-qa-extract.mjs | test:contract:qa:extract | structured extract |
contract-qa-console.mjs | test:contract:qa:console | console summary API |
contract-qa-audit.mjs | test:contract:qa:audit | audit log (async poll) |
contract-qa-console-inbox.mjs | test:contract:qa:console-inbox | console inbox UI API |
contract-qa-team-keys.mjs | test:contract:qa:team-keys | team keys CRUD |
contract-qa-session.mjs | test:contract:qa:session | run session GET/PATCH |
contract-qa-oidc.mjs | test:contract:qa:oidc | OIDC authorize redirect (skip if disabled) |
Example — agent hub only after edits to src/routes/agent.ts:
MAILAGENT_API_KEY=ma_… npm run test:contract:qa:agent
CI (automatic runs)
| Workflow | Trigger | Tests |
|---|---|---|
| deploy-worker.yml | push main (Worker paths) | deploy → test:prod:gate |
| qa-smoke.yml | PR / qa/** | check + verify:codex + test:prod:gate |
| publish-packages.yml | tag v* | test:prod → npm publish |
Without MAILAGENT_API_KEY in GitHub Secrets, deploy fails — by design.
Agent workflow after a code change
- Discovery —
GET /v1/agent(tools, docs, auth). - Types —
npm run check(if you changedsrc/). - Narrow contract — script from the table above.
- Full gate —
npm run test:prodbefore merge / after deploy. - Diagnostics —
npm run doctor:qa(plan, outbound, oidc hints).
When a test fails
- Read stderr of the last contract script (name in
--- contract-qa-….mjs ---). - Re-run one script locally with the same key.
- Inbox flow:
mailagent_diagnose_inboxorPOST …/simulatevia curl. - Audit: event is async — contract already polls; if flaky, increase delay in the script.
- Domains: Resend quota — script cleanup + skip; not an API regression.
npm run doctor:qa
curl -s -H "Authorization: Bearer $MAILAGENT_API_KEY" \
https://api.webmailagent.com/v1/agent | jq .
Adding a new contract test
- Copy template
scripts/contract-qa.mjs. - Use
scripts/lib/contract-api.mjs(contractSimulate,contractApi). - Register in
scripts/test-contract-all.mjs. - Add npm script
test:contract:qa:<name>inpackage.json. - Update this table and AGENTS.md.
Do not use DATABASE_URL or simulate-inbound.mjs in CI — HTTP simulate only.
E2E (Playwright / Vitest)
Product E2E with @mailagent/qa: QA.md and examples/playwright/.
Contract tests cover the API contract; Playwright covers external app UI.
Links
- AGENTS.md — MCP, verify flow
- CI.md — secrets, workflows
- OPERATOR.md — human: secrets only
- examples/github-actions/contract-qa.yml — template for your repo
- Public docs: autotests.html