Testing & Reproducibility
The core on-chain behaviours of OhShii — creating a Liquidity Generation Event (LGE), buying into one, creating and voting on governance proposals, upgrading a canister by vote, locking and unlocking tokens, and the identity-verification gate — are covered by automated end-to-end suites that anyone in the DAO can run from a clone of the repository. The tests exercise the real production code paths inside a faithful local replica of the network, so a green run is meaningful evidence that a change behaves the way it will on mainnet.
Coverage is broad on the canister side and still being extended; the exact figures, what is excluded and why, are in Coverage at a glance below rather than asserted here, because a number written into prose goes stale the day after it is written.
This page explains what the suites verify, how to reproduce each one, how to drive the same flows interactively, and — importantly — the guarantees that keep the test-only tooling from ever reaching a mainnet build.
Coverage at a glance
Section titled “Coverage at a glance”Derive these numbers, never trust them. They are correct at the time of
writing and the commands beside them are the point — an earlier version of this
programme carried a hand-written total that was wrong by 24, because the needle
counted #[test] where it appeared inside comments. Anchor to the start of
the line:
ls test-env/harness/tests/*.rs | wc -lgrep -rh '^[[:space:]]*#\[test\]' test-env/harness/tests/*.rs | wc -lgrep -rh '^[[:space:]]*#\[ignore' test-env/harness/tests/*.rs | wc -l⚠ Do not quote a count from this page — derive it. Run
bash scripts/test_census.sh, which produces every figure below with a positive and a negative control beside it, and reconciles each split against its own population. A written count rots silently, and a rotted count is indistinguishable from a correct one: the figures this paragraph used to carry (80 suite files · 236 test cases) were 7 and 10 short by the time anyone re-measured.
Snapshot 2026-08-31, for orientation only: 97 suite files · 263 test
functions in the PocketIC harness, of which 12 are #[ignore]d and 251
actually execute; plus in-crate unit tests reached by cargo test --workspace
in each repo (38 launcher files and 10 locker files carry a
#[cfg(test)] module).
⚠ This snapshot is dated for a reason: it goes stale within days. Re-run
bash scripts/test_census.shrather than quoting this line — the command is the answer, the number beside it is only orientation.⚠⚠ The two harnesses are DISJOINT, and “the tests pass” is usually false as intended.
test-env/harnessis a standalone cargo workspace — its empty[workspace]table exists precisely to stop cargo walking up to the production one, so that joining them cannot mutate the productionCargo.lockand break the reproducible Docker build.cargo test --workspacefrom either repo root does not run the PocketIC suites, and never has. Always name which harness a green result came from.
ⓘ The per-surface split below is a hand-classified snapshot and is older than the totals above — its rows sum to less than the current 87/246. Nothing derives it, so treat it as a shape, never as a count.
| Surface | Suite files | Cases |
|---|---|---|
| Locker — locks, vesting & unlocks | 21 | 75 |
| Governance — OHSHII (ONS) | 15 | 47 |
| LGE, purchase & refunds | 13 | 25 |
| Cycles, timers, upgrades & lifecycle | 8 | 24 |
| Cross-canister, guardian & commit points | 5 | 19 |
| Shield (in-canister firewall) | 5 | 18 |
| Governance — per-campaign (SONS) | 5 | 13 |
| ICPSwap & liquidity | 4 | 5 |
| Storage & disclosure | 2 | 5 |
| Identity & verification | 2 | 5 |
| Total | 80 | 236 |
What is still being implemented
Section titled “What is still being implemented”Coverage is tracked as an explicit backlog of identified behaviours rather than as a percentage of lines. Of 83 tracked coverage items, 47 are complete, 10 are partial (one half proven, the other blocked or deferred) and 25 are not yet implemented. Work is ongoing, and two of the gaps are structural rather than a matter of time:
- The browser frontend has no automated runner. There is no unit or end-to-end test framework wired into the React app today, so frontend behaviour is verified by hand against a live local world. Frontend logic that matters for correctness is instead pinned indirectly, by Rust tests that read the JSX as text and assert its shape.
- A few behaviours are only provable on mainnet — real threshold-ECDSA signatures, real external services, and the irreversible one-shot steps that cannot be rehearsed against a local replica.
Two further honest caveats, because a reader deserves them before reading a green run as a guarantee:
cargo test --workspaceruns none of the suites on this page. The harness is a deliberately separate workspace, so “the tests pass” in the production workspace means the unit tests passed and says nothing about the end-to-end suites. Name both, or name neither.- The suites run on demand, not on a schedule. There is no continuous- integration runner executing them on every change today; each suite is run by a person typing the command, which is why this page lists them explicitly.
Suites that do not run by default
Section titled “Suites that do not run by default”Twelve cases carry #[ignore], and none of them is a disabled or failing test —
each needs something the default run does not build or does not have:
| Suite | Why it is opt-in |
|---|---|
icpswap_stack (2), icpswap_liquidity_properties, mainnet_snapshot (pool half) | need the vendored ICPSwap V3 stack built first (./test-env/build-icpswap.sh) |
timer_freeze (2) | slow: builds WASM and spins a world per case |
verification_gating (threshold-ECDSA probe) | exercises real signature machinery, run explicitly |
sons_vesting_batch_cost | a measurement fixture, not an assertion — run to read its numbers |
pm1_injection_probe | a probe: prints a table and asserts only that the sweep ran |
lge_finalize_liquidity_probe | a probe: reports the raw finalization amounts for an independent derivation |
icpswap_claim_ordering_probe | needs the vendored ICPSwap stack; an A/B measurement rather than a pin |
get_my_vote_live | needs a live world started with start.sh |
Run them with -- --ignored, for example:
cd test-env/harness./../build-icpswap.shcargo test --test icpswap_stack -- --ignored --nocaptureThe local world the tests run against
Section titled “The local world the tests run against”The tests boot a PocketIC world (a deterministic, in-process replica of the Internet Computer) that installs the launcher and locker canisters at their real mainnet ids, alongside real system canisters (the ICP ledger, the Cycles Minting Canister, Internet Identity) and the real ICPSwap V3 stack. See Local Test Environment for standing the world up interactively; this page is about the automated suites and the simulator that run on top of it.
The harness lives in test-env/harness/ and is a standalone Rust workspace,
deliberately separate from the production workspace so the reproducible build is
never affected by test dependencies.
Running the suites
Section titled “Running the suites”The one-command form builds the canisters and runs the whole suite:
npm run testenv:testTo run an individual suite (from test-env/harness/), and rebuild the canister
WASMs first when you have changed canister code:
cd test-env/harness
# Rebuild WASMs before running (needed after any canister change):OHSHII_TESTENV_BUILD=1 cargo test --test lge_participation -- --nocapture
# Reuse the already-built WASMs (faster, no rebuild):cargo test --test ons_proposal_lifecycle -- --nocapturecargo test --test sons_proposal_lifecycle -- --nocapturecargo test --test verification_gating -- --nocaptureEach suite boots its own isolated world and takes roughly one to two minutes.
What runs on GitHub, and what runs only on a developer machine
Section titled “What runs on GitHub, and what runs only on a developer machine”Two workflows run in the repository’s CI. Both are readable in
.github/workflows/, and everything they do can be reproduced locally with the
commands above.
| workflow | when | what it runs |
|---|---|---|
ci.yml | every push and pull request | the static gates, the in-crate unit tests, and the harness suites that need no booted world |
suites.yml | weekly, Sunday 03:00 UTC, and on demand | a selected set of PocketIC end-to-end suites |
Both check out the sibling repository as well, because several suites read locker
source directly — some at runtime, and at least one at COMPILE time through an
include_str!. Without it the harness does not merely fail to run: as a whole it
does not build.
⚠ A green CI is NOT a green suite set, and the gap is large. The end-to-end harness holds far more suites than CI executes — the exact figures are in Coverage at a glance, deliberately not repeated here. CI runs a selected subset; the rest are run locally, by hand, before a deploy. Reading a green checkmark on a commit as “the suites pass” is the single easiest mistake to make on this page.
Why weekly rather than nightly. A matrix run bills the sum of its jobs, not its elapsed time, and the end-to-end job is minutes of world boot per suite. Run nightly it consumed more than the account’s entire monthly Actions allowance on its own — which would have exhausted the budget and taken the every-push checks down with it, at a twentieth of the cost. The weekly cadence keeps a periodic signal; the every-push checks are the ones that catch a regression on the commit that introduced it.
ⓘ If you change that cadence, measure by summing job durations, not by reading the elapsed time the Actions UI shows. A matrix run’s displayed time is a fraction of what it bills, which is why the cost was invisible until someone added the jobs up. The arithmetic is recorded at the schedule itself.
What CI cannot tell you, stated because a checkmark is read as more than it is:
| it proves | it does NOT prove |
|---|---|
| the tree compiles and the static gates pass | that the deployed wasm carries the change — see the chain-clean warning further down |
| the in-crate unit tests pass | anything about multi-canister behaviour; those live in the PocketIC suites |
| the CI-selected suites pass | that the suites it does not run would pass |
| the two repositories agree on their vendored artefacts | that either one is correct — only that they are identical |
What each suite verifies
Section titled “What each suite verifies”LGE participation — lge_participation
Section titled “LGE participation — lge_participation”Drives the real purchase flow (an ICRC-2 approval followed by the backend’s
purchase_tokens_icrc2) across the Voter Benefit Protocol tiers, and checks
that:
- verified buyers in each tier receive tokens and a contribution is recorded;
- the OHSHII guest fee for unverified buyers is charged at most once per campaign — including the real-world edge where, at the start of the bonding curve, the Guest tier’s token cap costs less than the minimum purchase, so a Guest is temporarily unable to participate until the price rises;
- the per-caller payment rate limit engages on a burst and recovers afterwards.
ONS proposal lifecycle — ons_proposal_lifecycle
Section titled “ONS proposal lifecycle — ons_proposal_lifecycle”Exercises the OHSHII (ONS) governance canister end to end:
- a Motion reaches quorum and is finalized by the on-chain timer, while a second Motion without enough voters expires;
- the chunked WASM upload path — a multi-megabyte module is uploaded across many chunks and the canister-reassembled hash is verified against the local hash;
- a canister-upgrade proposal is created, driven to immediate approval, executed, and the target canister’s installed module hash is checked against the proposed WASM.
SONS proposal lifecycle — sons_proposal_lifecycle
Section titled “SONS proposal lifecycle — sons_proposal_lifecycle”The same, for a per-campaign (SONS) governance canister created for an imported DAO or an LGE: a configuration-change proposal that is voted and executed, a child-canister upgrade, and the verification gate — a SONS configured to require verification rejects an unverified proposer, then accepts them after they verify, exercising the cross-canister call from SONS to ONS that checks personhood.
Verification gating — verification_gating
Section titled “Verification gating — verification_gating”Confirms the World ID / DecideID gate behaves correctly: when either method is enabled an unverified voter is rejected and a verified one is accepted (the gate is the logical OR of the two methods), and that a freshly installed governance canister converges on the verification modes currently active on mainnet after an upgrade, while never overriding a configuration an operator or the DAO has deliberately set.
Two further suites cover related surfaces: lge_creation_slot (LGE and
imported-DAO creation, payment-memo and slot-allocation hardening) and the
asset-batch cleanup suites for both governance canisters.
Simulating flows against a live world
Section titled “Simulating flows against a live world”For interactive testing, a live world can be driven with the same real flows. Multi-participant scenarios (many buyers, many voters) cannot come from a single browser wallet, so they are issued through a small command-line tool that talks to the running world directly:
npm run testenv # start the live worldnpm run testenv:sim -- plan dao-0000000012 --to-percent 60npm run testenv:sim -- quote dao-0000000012 --amount-icp 1 --tier humannpm run testenv:sim -- participants dao-0000000012 --n 5 --tier guestnpm run testenv:sim -- participants dao-0000000012 --n 30 --mode burst --freshnpm run testenv:sim -- proposal ons motion "adopt the thing"npm run testenv:sim -- wasm listnpm run testenv:sim -- vote ons 1 --immediatenpm run testenv:sim -- execute ons 1npm run testenv:sim -- status ons 1./test-env/sim.sh help # full command listThe simulator is diagnostic, not just a runner:
planstarts from the goal instead of the knobs: state the completion level the campaign should reach and it resolves that into a concrete cohort — how many participants, which participation tier, and how much ICP each — from the live bonding curve, then previews it. Nothing is bought until the plan is confirmed. It also reports when the participant budget, not the target, is the binding constraint.quoteis a curve-accurate pre-flight. It reads the campaign’s live bonding-curve position and reports how many tokens a chosen ICP amount buys now, the tier’s purchase cap, the remaining headroom, and the maximum ICP that fits the cap — so you choose a valid amount instead of guessing. Purchase caps are denominated in tokens, so at the start of the curve a small tier’s cap can be reached with very little ICP; the quote makes that explicit. Tiers are earned, not bought: the capital tiers also require voting discipline, so a simulated participant is given both the voting power and the ballots that keep it an active voter — otherwise the platform correctly demotes it and enforces the lower cap (the quote reports the demotion when it happens).participantsreports each buyer on its own line, classified as success, tier-cap rejection, rate-limit, high-load (proof-of-work) rejection, or below-minimum, with a summary of counts and total ICP spent. A sequential run paces the buyers; a burst run submits every purchase at once, which is how you exercise the platform’s in-canister rate-limiting and high-load defences under real concurrency.--freshgives each run brand-new participants so earlier runs don’t skew the per-participant cap accounting.wasm listshows the upgrade payloads available for an upgrade-by-vote test. Every payload is read and scanned server-side before it can be attached to a proposal; a payload carrying a development-only method is refused up front (the on-chain governance check would reject it too).
The same commands are also reachable from the launcher UI when it runs against a local world: a small floating bubble in the bottom-left corner opens a development-only panel — participation on a campaign page, proposals and voting on the governance page, environment controls on the wallet page — and closes it again, so the interface otherwise looks exactly like production. None of this tooling is present in a production build.
The no-backdoor guarantee (why the test tooling is safe)
Section titled “The no-backdoor guarantee (why the test tooling is safe)”To make local testing practical, the test build compiles in development-only
methods — most importantly one that lets a caller grant itself admin rights, and
methods that mark a principal as verified without the real proof-of-personhood
flow. These are gated behind a Cargo feature (dev-admin) and are compiled
out of every production build. This matters for a DAO, so the codebase
guards it in four independent layers rather than trusting a single check:
- Build isolation. The development build is written to a separate output directory, never to the path a mainnet deployment installs from.
- Compile-time opt-in. Every affected canister contains a block that fails to compile unless an explicit environment opt-in is set, so the feature flag alone cannot produce a backdoored module.
- Artefact scan.
scripts/check_no_dev_admin.pyreads a compiled WASM’s export section and fails if any development method is present. It runs in the Docker build, the deploy scripts, and the voter-side proposal verification. - On-chain gate. Both governance canisters scan any WASM proposed for upgrade and reject it if it exports a development method — the one layer a proposer cannot bypass, enforced under consensus.
Note that this applies to every OhShii canister that carries the feature,
including sons_governance (the per-campaign governance canister). A test that
proposes a canister upgrade therefore has to use a clean WASM as the payload —
the on-chain gate rejects anything else — which is exactly the discipline the
upgrade suites follow.
Verifying a released canister yourself
Section titled “Verifying a released canister yourself”Anyone can check that a canister running on mainnet was built from a specific, reviewed source, and that its module carries no development methods:
# 1. On-chain module hash equals the local reproducible build:scripts/verify-hash.sh ic <canister-name>
# 2. That same local build carries no development-only methods:python3 scripts/check_no_dev_admin.py <path-to-local-build.wasm>A matching hash proves provenance (the running code was built from the source you reviewed); the scan proves the content carries no test methods. Neither can, on its own, detect a hand-edited authorization check hidden in the source — only a reproducible build bound to a reviewed commit gives that assurance. The Voter Verification Guide and the Docker Deploy Guide cover the reproducible-build chain in full.
The gates, by name — and what each one CANNOT see
Section titled “The gates, by name — and what each one CANNOT see”npm run check:did and its siblings are referred to throughout this page; this section names them,
because a gate you cannot name is one you cannot run before pushing.
| command | what it proves | what it does NOT prove |
|---|---|---|
npm run check:did | declaration/vendored .did sync across both repos, plus the Rust-mirror drift gate | nothing about the DEPLOYED interface — it reads the tree |
npm run check:icc | bounded inter-canister calls under its scan roots | anything outside those roots; it prints its own SCOPE GAP, read it |
npm run check:levers | the FE mirrors of ONS/SONS admin-method lists and criticality categories match Rust, both directions | that any individual lever is correct — a wrong target id or param order passes |
node scripts/check_admin_method_doors.mjs | every allowlisted method has a UI door | whether the door opens. The string guard appears zero times in it |
python3 scripts/check_rust_mirror_drift.py | type-mirror // from <did>:<line> anchors resolve to the right construct | function-level anchors. It resolves above a struct/enum, not above a pub fn |
bash scripts/test_census.sh | the suite/backlog/decision counts, each with a control | coverage. A suite that EXISTS is not a flow that is COVERED |
⚠ Source clean is not chain clean. Every gate above reads the working tree. A fix can be landed, reviewed, and green in all of them while the deployed wasm still serves the old surface — measured twice in August 2026. Before calling a change done on a published Candid surface, ask the chain:
dfx canister info <id> --network ic --identity anonymousfor the module hash (the identity flag is NOT optional — without it dfx fails to load an identity even for a read), andicp canister call <id> <method> '()' -n ic --query --identity anonymous -o candidfor the surface itself.
⚠ Provenance anchors: .did files are line-number TARGETS
Section titled “⚠ Provenance anchors: .did files are line-number TARGETS”Rust mirrors of another canister’s wire types carry a comment recording where the type came from:
/// from ../dev-ohshii-locker/src/backend/token_locker_backend.did:351 (`LockStatus`)test-env/harness/src/flows.rs alone carries 65 of these, and check_rust_mirror_drift.py
resolves them to prove a mirror still matches its source.
The consequence, and it is not obvious: editing a .did is not free even when you only touch a
comment. Inserting or deleting lines shifts every anchor below the edit, and they fail silently
— an anchor pointing at a blank line produces no diagnostic, and one pointing at a different
type produces a confident, wrong comparison.
Measured 2026-08-25. A three-line comment was replaced with a nine-line block in
token_locker_backend.did. The +6 shift moved type LockStatus from :351 to :357 and broke
thirty anchors at once. The gate caught it only because the harness had been added to its scan
roots one commit earlier.
So:
- Edit a
.didLINE-COUNT-NEUTRALLY where you can — the fix above was rewritten to exactly three lines for three, andgit diff --numstatreading3 3is the check. - When a net change is unavoidable, re-derive every anchor below the edit before committing.
git show --numstat <commit>names the insertion point in one command. - Never revert a
.didby re-editing it. Restore the pristine copy — a re-edit can land a different line count than the original.