Testing
COLLAB is tested in four layers: the Foundry suite for the contracts, end-to-end suites that drive the real site in a browser against a local chain, the server kit's unit tests and sandbox install, and static checks of the web app and of every generated file. This page says what each layer covers, how to run it and how to add to it. It is for developers and reviewers; the tests are part of every release build (Building a release).
At a glance
| Layer | Command | Size in 2.2.0 |
|---|---|---|
| Contracts | forge test | 331 tests in 92 suites |
| Deeper invariants | FOUNDRY_PROFILE=ci forge test --match-path "test/invariants/*" | 25 tests in 7 suites |
| End to end | python3 tools/e2e/run.py [filter] | 161 scenarios in 17 suites |
| Visual sweep | python3 tools/e2e/sweep.py --base <web> --rpc <rpc> | the main pages and every DAO tab, light, dark and phone |
| Server kit, unit | python3 -m unittest discover -s server/tests | 32 tests |
| Server kit, install | sudo server/tests/sandbox_install.sh [--keep] | install, re-run, upgrade, rollback |
| Web app | python3 tools/lint_web.py | every script and page |
For release 2.2.0 every layer passed: the Foundry suite in the release build (331 of 331), the end-to-end suites on the extracted archive, the 32 unit tests and the sandbox install. Passing tests are not an audit: COLLAB 2.2 has had AI-assisted internal reviews and no external audit.
The Foundry suite
forge build --skip test --skip script # the contracts, then a few test files per build (memory)
forge test # 331 tests in 92 suites
forge test --match-path test/Profile.t.sol -vv
FOUNDRY_PROFILE=ci forge test --match-path "test/invariants/*"| Folder | Suites | Tests | What it holds |
|---|---|---|---|
test/*.t.sol | 29 | 167 | unit and integration tests on the full stack |
test/invariants/ | 7 | 25 | invariant campaigns |
test/audit/ | 56 | 139 | the reviews' proof-of-concept tests and fuzz checks |
test/Base.t.sol deploys the whole protocol with the real CIPNFT (twice), GL1F and GL1F Crypto bytecode from externals/bin/, so every proposal type runs end to end against the contracts DAOs use on GenesisL1. The main files:
| File | Tests | Covers |
|---|---|---|
Governance.t.sol | 36 | the proposal lifecycle, deposits and penalties, quorum, veto, threshold and abstain cases, delegation, snapshots |
Profile.t.sol | 30 | the 2.2 profile: PNG checks, 128 × 128, 512 KB, rewrites in place, chunk reads (fuzzed), the description's text rules and 256 KB, slugs (fuzzed against a reference), the directory, and ProfileGasTest |
Integrations.t.sol | 22 | each DAO's integrations: lists, roles, rule precedence (fuzzed), listing within one proposal, removal, misbehaving cross-reference getters |
Modules.t.sol | 16 | constitution versions, representatives and the permanent title, operator calls through module permissions, the treasury's asset kinds, ERC-1271 signatures, locked implementations, airdrops, TextStore's large blobs, the lens summaries |
GovernorHardening.t.sol | 14 | the outside review's governance items (OR-2 to OR-4) and the 2.1 review's: the minimum Yes, critical rules never laxer than standard ones, freeze votes side by side |
Utf8.t.sol | 12 | the text checks against a reference decoder written differently |
DeSciFi.t.sol, Launcher.t.sol, GL1F.t.sol, CipherVault.t.sol, CipherVaultMulti.t.sol, HoldingVotes.t.sol | 34 | pools and fees, the issuance example from the specification, a DAO earning GL1F inference fees, key custody, purchase, reveal and sale on CIPNFT, one key across several CIPNFT contracts, votes with a holding period |
Invariants.t.sol | 2 | two quick invariants: share units and votes conserved, pool fees always covered |
SizeLimits.t.sol | 1 | every COLLAB contract fits EIP-170's 24,576 bytes (local chains run anvil with a raised limit, so this test is what catches a contract GenesisL1 would refuse) |
Fuzz tests run 512 cases each, 4,096 with FOUNDRY_PROFILE=ci. Invariant campaigns run 64 runs of depth 48, 128 of depth 96 under ci; four suites (DistributorInvariants, GovernorInvariants, LauncherInvariants and TreasuryStanceInvariants) set their own in the file (/// forge-config: lines). fail_on_revert is off: the handlers predict from the rules whether each call must succeed, so a call that should have failed counts as a violation, and so does one that should have worked.
| Invariant suite | What always holds |
|---|---|
ShareTokenInvariants | voting units equal the balances of eligible accounts and add up to the total; each delegate holds exactly its delegators' units, now and at every past block; lock claims are counted exactly; only the DAO mints and burns |
DAOTokenInvariants | the supply never exceeds the cap; powers given up never return; a pause blocks transfers between holders; only the owner administers |
LauncherInvariants | allocations add up to the supply and reach their destinations; pools hold exactly what was asked; the launcher keeps nothing |
DistributorInvariants | each claim pays exactly what the snapshot entitles, once; the contract holds what open airdrops owe |
DeSciFiInvariants | swaps never lower the constant product of the effective reserves; deposits and withdrawals never dilute others; fees owed never exceed fees held; lock boxes release only to their owner after the date |
GovernorInvariants | no vote weighs more than its voter held or was lent at both readings; frozen accounts never vote, propose or delegate |
TreasuryStanceInvariants | in a DAO where only treasury spends are critical, no action the policy calls standard leaves the treasury with less, across two GL1F collections and two CIPNFT contracts |
The review suites
test/audit/ keeps the proof-of-concept tests of the internal reviews, each now asserting the fixed behaviour: AuditPoC.t.sol (release 1.0's review), with the reviewers' regression and fuzz checks in Controls, Informational, LaunchPoolFuzz, LockClaims and VoteFuzz; TokenReviewV2.t.sol and the three TokenVerify*.t.sol files (2.0's token review and its two rounds of verification); Review21.t.sol and Verify21.t.sol (2.1's review and its verification); and Hardening22.t.sol (H22-1 and H22-2, found while building 2.2). Security maps each finding to its test. The 2.2 contract review ran its proof-of-concept tests (RV22-1 to RV22-8) in the reviewer's own copy; its findings are documented in Security or handled in the interface, and they have no test in this suite.
End-to-end tests
tools/e2e/run.py drives headless Chromium through Playwright against the real contracts on a local chain, with an injected test wallet. It starts its own sandbox from the snapshot (see Local development), resets the chain before every scenario, and stops the sandbox at the end.
python3 tools/e2e/run.py # every scenario
python3 tools/e2e/run.py profile # scenarios whose "<module>.<function>" contains "profile"
python3 tools/e2e/run.py descifi --name mine --rpc-port 8601 --web-port 8701 --keep--name, --rpc-port and --web-port pick the sandbox (default e2e, 8690, 8790), and --keep leaves it running. Each scenario prints PASS or FAIL with its time; a failure saves a screenshot of every open browser (build/screenshots/FAIL-<scenario>-<n>.png) and prints the console errors, and the run exits 1. All scenarios share one Chromium process. On a machine shared with other people's runs, run browser suites one at a time, for example under flock with a lock file of your own.
| Suite | Scenarios | Covers |
|---|---|---|
admin | 5 | sign-in, deploy and publish, publishing a record and site settings, a host without the service, the page's checks matching the server's |
create | 7 | the wizard: limits matching the contracts, minimal and full creations, validation and drafts, a narrow screen, GL1F Crypto only, a malformed title |
deploy | 7 | the Deploy and Check tabs: a deployment, a reload while sending, without a site configuration, checking configurations and tampered records, the names rule, config.js byte for byte equal to the tool's |
descifi | 18 | swaps (native L1, launch pools, several hops), liquidity, pools, locks, fees, wrapping, requotes, unknown and look-alike tokens, without a wallet |
docs | 10 | the docs home and index, a page's parts and anchors, search, the questions page's structured data, figures, the live figures (drawings and this site's contracts), a dark phone, widths from 320 to 2560 px, the 404 page, the manifest, icons and crawler files |
governance_builders | 18 | the proposal builders: sends, rules, constitution, representatives, freezes, signatures, airdrops, NFTs, policy rules, integrations, raw calls |
governance_core | 13 | a raw transaction's lifecycle, deposits, delegation overrides, the workspace and proposal pages, the composer, critical escalation, the minimum Yes, freeze votes, refresh races, phones |
landing | 9 | the network molecule against the chain, the largest DAOs and votes, links, the directory's URL state and empty states |
market | 7 | /tokens and token pages against tokens.json, without server data, hostile names, right after a deployment, lopsided pools, vouching |
models | 7 | GL1F: paid inference after a pricing vote, donations, sends and deletes, listings, registration per collection, GL1F Crypto's rules |
portal | 10 | the shell, the phone drawer, search, /votes, site settings, the site's price, the undeployed mode, the cookie notice, the settings bar, capped reads |
profile | 8 | slug URLs, a logo by proposal, a heavy logo and creation on a node capped at 25M, the description limit, the permanent title, hostile and copycat titles |
review | 3 | spoofed token symbols, cleaned proposal titles |
sealed | 4 | key epochs, purchase, reveal, mint, sale, declassification, stewards and key rotation, a second CIPNFT contract |
seo | 13 | the prerender: the glyph port, the demo DAOs, robots.txt on another main host, hostile and malformed DAOs, Markdown text, the member network, server configurations and the sitemap against the pages, config.js origins, static hosts, a hidden DAO, the /pools and /locks heads, failing parts |
tokens | 7 | issuance, an airdrop claim, minting, liquidity and locks, critical value actions, every builder submitting cleanly |
views | 15 | treasury values and flows, sweeps, lock fees, airdrops, NFTs per integration, members, settings (rules, pending changes, the treasury stance, outside keys), representatives, constitution versions |
The sealed suite needs the reference CIPNFT dapp (CIPNFT_REF). The admin suite runs the real admin service behind tools/e2e/admin_site.py, a local HTTPS front that does what nginx does. The suites that need server data make it with the real tools (tools/seo/tokens.py, tools/prerender.py) and serve it with tools/serve_local.py --data or --prerender.
The harness
tools/e2e/harness.py holds Browser(base, rpc, account, width=1360, height=900, dark=False, cookie_notice=False, ignore_https_errors=False): a browser context with the test wallet for account (anvil's unlocked development accounts), the cookie notice marked as seen unless cookie_notice=True, and every console error and warning collected in errors. Its helpers:
| Helper | Does |
|---|---|
go(path, wait=1500) | loads a page by its clean path |
nav(path) | follows an in-app link without a page load, so what the tab holds in memory stays |
wait_text(text), click(name, role="button"), fill(label, value) | waits, clicks and types by visible text, role and label |
wait_toast(text="Confirmed") | waits for a transaction toast |
assert_no_errors() | fails on any console error or page error |
shot(name) | a full-page screenshot in build/screenshots/ |
page | the Playwright page, for everything else |
tools/e2e/sweep.py opens the home page, /daos, /create, /swap, /pools, /pools/add, /locks, /docs, /settings, /privacy, /admin and a 404, then every tab of one DAO (--dao, default 1), in light, dark and a 390 px phone layout. It saves screenshots to build/screenshots/sweep/ and exits non-zero when a page logs an error or scrolls sideways on a phone. /votes, /tokens and the pages under /docs/ are not in its list: their own suites (portal, market, docs) test them.
Writing a scenario
A suite is a module in tools/e2e/scenarios/ that exports SCENARIOS, a list of functions. Each function receives an Env and fails by raising, with an assert or any exception; the runner gives it a freshly reset chain and closes its browsers afterwards.
# MIT License. Copyright (c) 2026 Decentralized Science Labs LLC.
"""Example: a DAO opened by its treasury address lands on its slug URL."""
def address_url_moves_to_slug(env):
b = env.browser(0) # account #0, with the test wallet
b.go(env.dao_url(1)) # /dao/<treasury of DAO 1>
b.wait_text("Open Proteomics Collective")
assert b.page.url.endswith("/dao/open-proteomics-collective"), b.page.url
b.assert_no_errors()
SCENARIOS = [address_url_moves_to_slug]Env member | Gives |
|---|---|
browser(i, **kw) | a Browser for account i (None for no wallet), with the harness's options |
dao(n) | DAO n's addresses: treasury, governor, shares, profile, policy, vault, creator, createdAt, createdBlock |
dao_url(n, *parts, preset=) | the clean path of DAO n or one of its sections, with a composer preset |
send(i, to, sig, *args, value="0"), cast(*args) | a transaction from account i with cast send --unlocked; any cast command against the sandbox |
warp(seconds), mine(n), rpc(method, params) | the chain clock, blocks and raw JSON-RPC |
accounts, deploy, demo, web, rpc_url | the twelve accounts, deployments/local.json, deployments/demo.json, the sandbox's web and RPC URLs |
Check what you build by looking at it: take screenshots, in light and dark and at 390 px, and read them. Keep scenarios independent of each other's state, prefer visible text and roles to CSS selectors, and wait for what the user waits for (a toast, a text) rather than for fixed times.
Server tests
python3 -m unittest discover -s server/tests # 32 tests, a few seconds
sudo server/tests/sandbox_install.sh # the installer end to end, in a sandboxserver/tests/test_admin.py tests the admin service and its helpers without a server: the site settings and record checks of tools/write_web_config.py, config.js and its policy, keccak-256 vectors, sessions (durable sign-out, damaged session files), the sign-in limit, safe reads and writes that never follow a link, the nginx scan, the API's routes, proxy secret, cross-site checks, bodies and flow, the history's bound, and the prerender and policy jobs. The policy-step test needs root. server/tests/adminenv.py writes the throwaway settings these tests and tools/e2e/admin_site.py use.
server/tests/sandbox_install.sh runs as root and needs nginx, python3, openssl and curl. It builds a private nginx (its own configuration, PID file and high ports: 8088, 8443 and 8417 by default, or the next free ones, --http-port, --https-port, --admin-port) with another site on it, in a folder under /var/tmp that it removes unless --keep. It checks that the installer refuses while that site claims one of COLLAB's names, installs with --prefix, --etc, --self-signed and --no-systemd, runs nginx -t, and then runs server/tests/sandbox_checks.py against the site: redirects, headers, caching, the 204s, the prerender overlay's limits and planted links, config.js, the admin API (cross-site checks, schemas, publish and unpublish, the prerender job, sign-out and revocation), the policy step with analytics, deploy/check_site.py, a re-run that changes nothing, an upgrade to a newer release and the rollback, the rate limits, the Let's Encrypt path with stubbed certbot and DNS (stub_certbot.sh, stub_getent.sh, stub_check_certs.py), and the refusal of a symbolic link planted in shared/. It stops only what it started, by process id, and leaves the system users collab and collab-prerender on the machine.
Lint and generated files
python3 tools/lint_web.py checks the rules of The web app that a script can check, and must report 0 errors: raw-HTML sinks and dynamic code in scripts, remote resources, hand-written page links, inline scripts, styles and handlers, a missing or differing Content-Security-Policy, relative paths, canonical links, missing imports, scripts, stylesheets and icons, the licence line of every script, page and stylesheet in web/ and every Python file in tools/, stale shells, stale /pools and /locks copies, and the documentation as tools/build_docs.py --check reports it.
Generated files have checks of their own, each exiting 1 when its file is stale:
| Check | Verifies |
|---|---|
python3 tools/shell_html.py --check | the portal shell and preload blocks of every entry |
python3 tools/prerender.py --static-pages --check | web/pools/ and web/locks/ |
python3 tools/build_docs.py --check [--strict] | the docs sources, the generated docs pages and the docs pages web/sitemap.xml lists; --strict also fails placeholder pages |
python3 tools/docs_figures.py --check | every figure the docs pages show |
python3 tools/make_assets.py --check, python3 tools/make_demo_logos.py --check | the app icons and the demo DAOs' logos |
python3 tools/export_deploy_artifacts.py --check | the deploy bundle against the compiled contracts |
python3 tools/merge_css.py --check | that no CSS part is still imported |
python3 deploy/sync_csp.py --check | the nginx and Caddy configurations and web/_headers against the pages' policy |
python3 tools/version_assets.py web --check | in a release tree: every asset URL carries the release's ?v= |
The docs renderer has unit tests of its own, which the release build runs: python3 tools/tests/test_build_docs.py (escaping, links, headings, tables, callouts, code, lists, figures, the FAQ's structured data and the checks --check makes).
Related
- Local development: the chain, the sandboxes and the test wallets.
- Building a release: where each check runs in the build.
- Contracts guide: what the Foundry suite tests.
- Security: the reviews behind the audit suites.
- Installing COLLAB on a server: the installer the sandbox runs.