Appearance
Testing Strategy
How VelaPay tests its 15 repos. The strategy is layered — each tier catches a specific class of failure, runs at a specific cost, and runs at a specific cadence. Tests at the wrong tier (a unit test that wants a real network, or an E2E test that should be a unit test) waste time and miss bugs.
Test Ladder
| Tier | What it asserts | Tooling | When it runs |
|---|---|---|---|
| 1. Rust unit (in-process VM) | Single-instruction correctness; account-validation paths; PDA seed derivation; numeric edge cases | cargo test + litesvm crate | Every commit (CI), local cargo test |
| 2. TS integration (in-process VM) | Multi-instruction flows; SDK ↔ program contracts; cross-instruction invariants | bun test + litesvm (npm) + anchor-litesvm | Every commit, local bun test |
| 3. App unit/component | Dashboard / portal / checkout / admin internal logic, hooks, components | bun test (built-in) | Every commit |
| 4. App E2E (browser) | Critical user flows in a real browser; auth, plan creation, checkout, portal cancel, plan switch | Playwright 1.59.x (bunx playwright test) | Pre-merge for PRs touching the surface; nightly full sweep |
| 5. Webhook simulation | Verifier signature handling; dispatch retry; DLQ | bun test over vela-webhook/test-fixtures/ | Every commit |
| 6. Mail smoke | Every @velapay/mail template renders + dispatches via real CF Email | vela-mail-smoke worker | Pre-deploy of any release that touches vela-mail |
| 7. Synthetic monitor | Full subscribe → pull → cancel funnel against deployed devnet staging | vela-synthetic Cron Worker | Every 15 minutes, continuously |
A bug should be caught at the lowest tier where it can be reproduced. If it's caught at tier 7 (synthetic), it should have a regression test added at tier 1, 2, or 4.
Tier 1 — Rust Unit (LiteSVM)
Where: vela-protocol/programs/*/tests/*.rs and vela-protocol/tests/.
LiteSVM is an in-process Solana VM — orders of magnitude faster than solana-test-validator. Each test boots a fresh VM, deploys the program from the workspace artifact, and exercises one or a few instructions against it.
rust
#[test]
fn execute_pull_blocks_when_amount_exceeds_plan() {
let mut svm = LiteSVM::new();
// ... seed plan, mandate, accounts
let ix = build_execute_pull(/* amount = plan.amount + 1 */);
let err = svm.send_transaction(tx).expect_err("must fail");
assert_matches!(err, TransactionError::InstructionError(_, _));
}Use Tier 1 for: numeric edge cases (u64::MAX, off-by-one, accrued-rate overflow), account-validation guards, instruction discriminator dispatch, transfer-hook validation paths.
Tier 2 — TS Integration (anchor-litesvm)
Where: vela-protocol/tests/integration/*.test.ts, vela-sdk/tests/*.test.ts.
anchor-litesvm 0.2.1 provides a LitesvmProvider that drops into Anchor's TS client wherever you'd otherwise pass an AnchorProvider. The result: real Anchor IDL deserialization, real instruction byte construction, real account-state inspection — without booting a validator.
ts
import { LitesvmProvider } from "anchor-litesvm";
import { LiteSVM } from "litesvm";
import { Program } from "@coral-xyz/anchor";
const svm = new LiteSVM();
const provider = new LitesvmProvider(svm, wallet);
const program = new Program<VelaProtocol>(IDL, provider);
const tx = await program.methods.executePull(...).rpc();Use Tier 2 for: SDK API contracts (does subscribe() produce the right instruction bytes?), multi-instruction sequences (subscribe → pull → cancel), accruedNow() arithmetic, error translation (translate.ts).
Tier 3 — App Unit / Component
Where: each surface's tests/ directory using bun:test.
Pure logic — no network, no real DOM beyond what the surface needs. Things like billing-amount formatters, date math, route guards, custom hooks, schema validators.
ts
import { test, expect } from "bun:test";
import { formatAmount } from "@vela/sdk/token";
test("formatAmount honors TokenConfig decimals", () => {
expect(formatAmount(1_000_000n, { decimals: 6 })).toBe("1.000000");
});Use Tier 3 for: hooks, formatters, route resolution, Zod schema fixtures, brand-token presence checks.
Tier 4 — App E2E (Playwright)
Where: vela-dashboard/tests/e2e/*.e2e.ts, similar in vela-portal, vela-checkout, vela-admin.
The dashboard ships per-phase Playwright configs (playwright.phase31.config.ts, playwright.phase50.config.ts, …) so a feature owner can run only their feature's E2Es during PR work and the full sweep happens nightly.
ts
import { test, expect } from "@playwright/test";
test("merchant can create a plan and see it appear", async ({ page }) => {
await loginAsTestMerchant(page);
await page.goto("/plans/new");
await page.fill('[name=name]', "Pro");
await page.fill('[name=amount]', "9.99");
await page.click('text="Create plan"');
await expect(page.locator("text=Pro")).toBeVisible();
});Use Tier 4 for: auth flows (magic-link, SIWS, Google SSO), plan creation, checkout flow, portal cancel + switch-plan, webhook endpoint CRUD.
Don't put protocol invariants here — Tier 1/2 is faster and more deterministic for those.
Tier 5 — Webhook Simulation
Where: vela-webhook/tests/*.test.ts over test-fixtures/.
Pre-signed payloads exercise the verifier and dispatch logic without any network or real on-chain state. Catches signature regressions, schema-evolution mistakes, retry/backoff bugs.
Tier 6 — Mail Smoke
Where: vela-mail-smoke Worker.
A throwaway Worker dispatches every @velapay/mail template via real Cloudflare Email Service to a single test inbox. Deploy → curl each template → visually confirm → tear down. Run before any release that touches vela-mail.
Tier 7 — Synthetic Monitor
Where: vela-synthetic Cron Worker.
Every 15 minutes, against deployed devnet staging:
- Subscribe a synthetic subscriber to a synthetic plan.
- Wait for the next pull window.
- Execute a pull.
- Verify the resulting webhook event lands in D1.
- Cancel.
Failures page on-call with the typed error class name (PullTooEarlyError, MandateExpiredError, …) so triage is fast.
What to Run When
| Situation | Command |
|---|---|
Touched a Rust file in vela-protocol | cargo test (and bun test if it touches IDL surface) |
Touched a TS file in vela-sdk | bun test in vela-sdk |
| Touched dashboard logic | bun test && bun run test:e2e:phaseNN for the feature area |
Touched vela-webhook schemas | bun test in vela-webhook + dashboard worker tests |
Touched vela-mail templates | bun test in vela-mail then deploy vela-mail-smoke and curl each template |
| Pre-release across all repos | Per-repo test suite + nightly full Playwright sweep + pre-deploy mail smoke |
Coverage Bars
There is no enforced numeric coverage threshold. The coverage bar is behavioral: every protocol invariant has a Tier 1 or 2 test, every typed error class has a triggering test, every E2E user flow that a merchant or subscriber would care about has a Tier 4 test.
Anti-patterns
- Mocking
vela-protocolin dashboard tests — this is what Tier 1/2 is for. - Testing UI in
vela-protocol— usevela-demofor visual sanity. - Letting Playwright failures sit "flaky" — flake is real signal; chase the root cause.
- Skipping mail smoke before a
vela-mailrelease — Mailpit doesn't catch CF Email rejection. - Adding tests that require a deployed devnet — push them to Tier 7 (synthetic) instead.
Related
- Per-repo test commands: each repo's
package.json scriptsblock. - Verification before a release: Verification Matrix →.
- Synthetic worker details: vela-synthetic →.