> ## Documentation Index
> Fetch the complete documentation index at: https://nelo.udokaam.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Testing

> What is proven, what is written but unexecuted, and what the gap has already cost.

This page is kept honest rather than flattering. The distinction between *a test exists* and *a test has run* is the one that matters, and for most of this project's life it cut the wrong way. It no longer does — but the parts that are still only written say so.

## What has actually run

| Suite                                    | Tests | Needs            |
| ---------------------------------------- | ----: | ---------------- |
| `packages/onboard`                       |    60 | Node only        |
| `packages/pay`                           |    54 | Node only        |
| `programs/nelo_vault` — LiteSVM          |    40 | Solana toolchain |
| `services/settle`                        |    39 | Node only        |
| `packages/voucher`                       |    29 | `pnpm install`   |
| `programs/nelo_vault` — curve unit tests |    17 | Rust only        |
| `packages/reserve`                       |    16 | Node only        |
| `packages/ledger`                        |    13 | Node only        |

**268, all green, all on every push.** Everything but the LiteSVM suite runs with no phone and no validator — which is the whole reason the arithmetic was pulled out of the app and the program in the first place.

The 40 LiteSVM tests were the long-standing gap. They went unexecuted for weeks because no machine that had the repo had the Solana toolchain — `release.anza.xyz` is unreachable from the development environment. The first run scored **39 of 40**, and [the one failure was worth more than the 39 passes](#things-that-went-wrong-and-were-caught).

## What CI runs

`.github/workflows/ci.yml`, on every push and every pull request:

| Job             | What                                | Cold  | Warm  |
| --------------- | ----------------------------------- | ----- | ----- |
| **TypeScript**  | 211 tests, then four typechecks     | \~21s | \~21s |
| **Rust (host)** | `cargo fmt`, clippy, the curve's 17 | \~27s | \~27s |
| **Anchor**      | the 40 LiteSVM tests                | \~14m | \~9m  |

The Rust job is deliberately separate from the Anchor one: the curve carries no Anchor types precisely so it can be checked without a validator, and it answers in seconds instead of waiting on a toolchain install. The Anchor job pins from `Anchor.toml`'s own `solana_version` and `anchor_version`, and is week 4's graded deliverable — *clone → install → `anchor test` green* — run on a machine that starts with nothing.

<Note>
  **The typecheck job exists because of a pattern, not a principle.** Every defect that has reached `main` was a TypeScript error found by a person reading code. [See below](#what-that-gap-already-cost).

  Each check is negative-tested rather than trusted: removing the `ScrollView` import gives `TS2304`, renaming an export in `@nelo/pay` gives `TS2724`, and a malformed function makes `cargo fmt --check` exit 1. A job that cannot fail is not worth adding.
</Note>

## What still has never run

<Warning>
  **Neither app has rendered.** An EAS development build compiles the entire native side, so `@nelo/attest`'s Kotlin is no longer unproven at the compiler — but a development client **does not embed the JS bundle**; it loads it from Metro at runtime. So no screen in either app has been drawn, no component has mounted, and the JS has never been bundled at all.

  Both apps typecheck clean under `strict`. That is a real result and it is not the same result.
</Warning>

<Warning>
  **StrongBox has never run on hardware.** `@nelo/attest` compiles; whether a real secure element produces the attestation chain and the low-S signature the chain expects is untested. The devnet gate deliberately uses a software P-256 key, which isolates *"does the chain do what we think"* from *"does the handset do what we think"*. Only the first is answered.
</Warning>

<Warning>
  **The Privy calls have never executed.** The flow machine around them has 29 tests, but `useLoginWithSMS` and `useEmbeddedSolanaWallet` need an app ID and a device. The call sites were typechecked against the published `@privy-io/expo` declarations, which catches shape errors and nothing about behaviour.
</Warning>

## What that gap already cost

Not hypothetical. While adding the balance display:

```ts theme={null}
// apps/merchant/App.tsx
import { awaitPayment, type PaymentOutcome } from "@nelo/pay";
```

`packages/pay/src/index.ts` **never re-exported either of them.** `awaitPayment` would have been `undefined` at runtime — the till would have crashed the moment a merchant pressed **Charge**.

No test caught it, and at the time none could have: the app had never been bundled and there was no CI. It was found by reading imports while adding an unrelated feature.

Then it happened again. While wiring onboarding:

```tsx theme={null}
// apps/merchant/App.tsx — the day-book screen
<ScrollView contentContainerStyle={styles.bookBody}>
```

`ScrollView` was **never imported.** Tapping **Today** would have thrown. Found the same way — by eye, adding something else.

Then a third, when `tsc` was pointed at the apps for the first time: they could not typecheck the packages **at all**. The config error stopped the check before it reached a line of app code.

<Note>
  All three are TypeScript errors. All three were found by a person reading code. None needed an emulator, a handset or an Android toolchain to catch.

  That is why the typecheck job exists, and why it is four separate invocations rather than a hopeful one. Reading is not a method; it is what is left when there is nothing to run — and now there is.
</Note>

The gap that remains is narrower but real: a typecheck proves the types, not the render. Nothing in either app has drawn a screen.

## How the tests are written

<AccordionGroup>
  <Accordion title="Negative tests before positive ones" icon="ban">
    Everywhere in the vault. The **first test in `vault.rs`** is the one that must fail: a *valid* P-256 signature over *different* bytes has to be rejected.

    Get the precompile offsets wrong and every positive test below still passes while the program verifies nothing at all. A test that passes because nothing was checked is worse than no test.
  </Accordion>

  <Accordion title="Golden vectors across the language boundary" icon="file-code">
    `packages/voucher/vectors/voucher-v1.json` is generated by the Rust side — the chain is authoritative — and asserted by both. TypeScript also verifies signatures that Rust produced, so the check covers the crypto and not only the byte layout.
  </Accordion>

  <Accordion title="Properties, not just examples" icon="function">
    The curve is tested for sublinearity **at every doubling across six orders of magnitude**, not at one convenient point. `isqrt` is brute-forced against its floor property including at `u128::MAX`. The money splits are checked to add back exactly, at every amount in a list chosen to include the awkward ones.
  </Accordion>

  <Accordion title="Read the balance, not the field" icon="eye">
    The LiteSVM tests read the **SPL token account**, deliberately not the vault's `balance` field. The two must agree, and reading the field to check the field proves nothing.
  </Accordion>
</AccordionGroup>

## Things that went wrong, and were caught

Kept because they are more informative than the passes:

| What                                                                                                                              | Caught by                                |
| --------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------- |
| Two cap tests asserted a cap that was unreachable at the inputs chosen                                                            | The tests failing on first run           |
| A vacuous assertion — `sq <= u128::MAX`, always true                                                                              | `cargo clippy`                           |
| FX legs in the payout that cancelled to nothing, leaving the transaction unbalanced                                               | Working the double-entry through by hand |
| A test asserting custody empties after a full payout, when the platform fee stays behind                                          | The test failing                         |
| Two test objects built by spreading a class instance — the methods live on the prototype, so they never implemented the interface | `tsc --strict`                           |
| A misread Poisson table asserting a quantile of 3 where `P(X≤2)` already cleared the confidence                                   | The test failing                         |
| A test that refused the right thing for the wrong reason, proving nothing                                                         | `anchor test`, the first time it ran     |

Most were caught by a tool or a test rather than by reading. The one that was not — the FX legs — was a design error, and the ledger's own balance rule would have rejected it at runtime anyway.

### The one worth reading twice

The first-ever `anchor test` run scored 39 of 40. The failure was `only_the_risk_authority_can_publish_reputation`, and it was not the program.

The test helper passed the same keypair twice — once as fee payer, once as the instruction's signer. A message dedupes its account keys, so it required **one** signature while the transaction builder was handed **two**, and failed with `NotEnoughSigners` *before the program was entered*.

So the test got an error, `is_err()` was satisfied, and the property it names — *reputation you can set yourself is not reputation* — had **never been checked**. The authority constraint was there and correct, but correct-by-inspection is not proven.

<Note>
  **Failing was the good outcome.** Had the assertion been on `is_err()` alone, it would be green today and still empty. Its sibling test passes only because an impostor keypair happens to differ from the payer, so it never hit the trap at all.

  That is the argument for asserting on the *reason* rather than the fact of a refusal, and it is why every check added to CI was negative-tested before being trusted.
</Note>

## The devnet gate

The week-1 trust model has been run end to end on devnet **against the Trust Stake build**: vault funded, `RiskConfig` initialised, voucher redeemed with the device signature verified by the secp256r1 precompile **on a real validator**, double-spend refused, signature-over-other-bytes refused.

That distinction matters. The gate had passed before, but against a build that predated the Trust Stake. This run is the first time `RiskConfig` has existed on a real validator and the first time `redeem_voucher` has been exercised on chain with its new account struct — exactly the kind of change LiteSVM can accept and a validator reject.

```bash theme={null}
cargo test -p nelo_vault --test devnet -- --ignored --nocapture
```

`#[ignore]` so it never runs in the default suite. It creates its own 6-decimal mint, so a run is self-contained, and it costs devnet SOL.

<Note>
  It uses a **software P-256 key** in place of StrongBox. That is deliberate: it isolates *"does the chain do what we think"* from *"does the handset do what we think"*. The second question is still open.
</Note>
