Testing
Certichain is tested in layers. Each layer proves something the others cannot: the contract suites prove the rules the chain enforces, the unit suites prove the browser and server logic in isolation, the interop suites prove that independent software reads the formats, the end-to-end journeys prove that real pages, a real local chain and a real build work together, and the manual checks cover what only a person with a real wallet and real services can confirm.
| Page | What it covers |
|---|---|
| Contract tests | Hardhat suites for the v0.3 registries, the timelock, the access gate and v0.2 |
| Invariants and gas | Random action sequences checked against a model, the gas snapshot and contract sizes |
| Zero-knowledge tests | Pinned proof system, real-proof tests, key and circuit file checks |
| Unit and rendered tests | Vitest suites for src/, packages/sdk, scripts and the E2E helpers |
| End-to-end journeys | The Playwright harness and every journey |
| Interop and SDK | The independent verifier library tests and the SDK's own tests |
| Continuous integration | Every job in the GitHub workflow, and the current billing hold |
| Manual checks | Step-by-step scripts per role for a local chain and for Sepolia |
The layers
Lower layers are many, fast and narrow; higher layers are few, slow and wide. A behaviour is tested at the lowest layer that can prove it, and a journey exists for each flow a person depends on.
| Layer | Runner | Proves | Does not prove |
|---|---|---|---|
| Contracts | Hardhat (Mocha, Chai, ethers v6) | Access checks, state transitions, events, reverts, pause coverage, storage bounds, upgrade and governance rules | That the app calls them correctly |
| Invariants and gas | Hardhat with fast-check | Properties hold across 500 random sequences; gas stays within 10% of the snapshot | Every possible sequence (the search is random, not coverage guided) |
| ZK | Hardhat and scripts | Real proofs pass and tampered ones fail against the pinned verifier; pinned files and keys match their recorded hashes | That the original trusted setup was honest |
| Unit and rendered | Vitest | Services, hashing, encryption, formats, parsers, server routes, page components with fake services | Behaviour against a real chain or browser wallet |
| Interop | node:test with Digital Bazaar libraries | A library the project did not write verifies Certichain VCs, Open Badges, status lists and API answers | Anything about the chain |
| SDK | Vitest | The client refuses answers that fail signature, nonce, window or deployment checks | Network behaviour of a real deployment |
| E2E | Playwright | Whole flows across pages, wallet prompts, the server routes (upload mocked), a real local chain | Pinata, MetaMask, Sepolia |
| Manual | People | Real wallet extensions, real storage service, real public network | Repeatability |
The test-first rule
Every fix and feature is written test first: the test is run and fails for the expected reason, then the smallest change makes it pass, then the code is tidied with the tests green. A security fix is not done without a regression test that fails before the fix and passes after it. The engineering handbook (docs/conventions/testing.md, docs/AGENTS.md) states the rule; reviews also seed deliberate bugs into the code to confirm the tests catch them, and record the result in the session logs.
Other rules the suites follow: fix the implementation, not the test, unless the test is wrong; no .only and no skipped tests (Playwright sets forbidOnly in CI); and no test files under docs/, because the root Vitest run would pick them up.
Coverage gates
| Scope | Gate | Enforced by |
|---|---|---|
Contracts (blockchain/contracts) | 95% lines, 90% branches | npm run coverage in blockchain/, which runs hardhat coverage and then scripts/check-coverage.ts |
src/services/** and src/lib/** | 80% lines, statements, functions and branches | thresholds in vitest.config.ts, checked by npm run test:coverage |
The Vitest coverage excludes test files, __fixtures__ folders and the test fakes in src/services/testing/. Pages and components are tested but are not part of the coverage gate.
Where the suites live
| Folder | Suite | Size (counted 2026-10-24) |
|---|---|---|
blockchain/test/ | Contract, invariant, gas and ZK tests | 69 *.test.ts files |
src/, packages/sdk/test/, scripts/, e2e/support/ | Vitest | 334 files, 3,369 tests (the last full run recorded in the session log of 2026-10-24) |
interop/test/ | Interop (node:test) | 6 files, 44 tests |
e2e/journeys/ | Playwright | 22 files, 38 journeys |
Counts were taken with find and grep over the repository; re-count with the commands in Unit and rendered tests and End-to-end journeys before quoting them.
Run everything
From the repository root, after npm ci and npm --prefix blockchain ci:
npm run lint
npm run typecheck
npm run zk:public # puts the hash-checked circuit files the browser prover needs in public/zk
npm run test:coverage # Vitest with the 80% gate
npm run build
cd blockchain
npm run compile
npm run zk:artifacts # fetches the pinned circuit files, hash-checked
npm run check:zk-keys
npm run sizes
npm run gas:check # fails above 10% of gas-snapshot.json
npm run invariants # 500 random sequences
npm run coverage # 95% lines, 90% branches
cd ..
npm --prefix interop ci
npm run interop:generate
git diff --exit-code -- interop/fixtures
npm run interop
npm --prefix packages/sdk ci
npm run sdk:build
npm run sdk:test
npx playwright install chromium
npm run test:e2e # needs ports 8545 and 3100 free
The same sequence is what the CI jobs run; see Continuous integration. As of 2026-10-24 those jobs are not starting on GitHub because of a billing hold on the account, so the checks are run locally.
Source files
vitest.config.ts,playwright.config.ts,e2e/global-setup.tsblockchain/hardhat.config.ts,blockchain/scripts/check-coverage.tsinterop/package.json,packages/sdk/package.json.github/workflows/ci.ymlpackage.jsonandblockchain/package.json(scripts)docs/conventions/testing.md(handbook conventions)