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

# Testing

> 40 unit tests (zero network, sub-second) plus 9 end-to-end tests against real, freshly-deployed Stellar testnet environments.

## Two suites, deliberately separated

<CardGroup cols={2}>
  <Card title="Unit -- bun test test/unit" icon="bolt">
    40 tests, 0 network calls, \~200ms total. Runs on every change.
  </Card>

  <Card title="E2e -- bun test test/e2e" icon="globe">
    9 tests, real testnet transactions, \~2 minutes. Run on demand.
  </Card>
</CardGroup>

<Warning>
  Bare `bun test` (no path) discovers both suites -- that's `test:all`'s intentional job, not an accident. The default `bun run test` script is correctly scoped to `test/unit` only.
</Warning>

## What the unit suite actually guards against

Every hand-rolled Soroban XDR encoder in `src/scval-encoders.ts` was wrong on at least one real attempt during development -- each bug produced a **silent on-chain wasm trap**, not a TypeScript error, because the SDK happily accepts a structurally-valid-but-semantically-wrong `ScVal` tree. The unit tests decode the actual produced structure and assert its exact shape:

```typescript test/unit/scval-encoders.test.ts theme={null}
test("encodes a data-less enum variant as [Symbol(name)], not a bare Symbol", () => {
  // Regression test for the exact bug hit during development: SignerStorage
  // ::Persistent (no associated data) still encodes as a 1-element Vec --
  // never a bare ScVal::Symbol, which the host silently rejects.
  const v = asVec(unitVariant("Persistent"));
  expect(v.length).toBe(1);
  expect(v[0].sym().toString()).toBe("Persistent");
});
```

Other unit tests cover the host's canonical Map key ordering (`Symbol("Ed25519")` sorts before `Symbol("Policy")`, lexicographically -- get this wrong and the signatures map is silently malformed), the MCP tool schema generator's type mapping, and the address auto-fill heuristic.

## What the e2e suite proves live

<Tabs>
  <Tab title="policy-gate.e2e.test.ts">
    Deploys one fresh environment, then drives the real allow/reject sequence directly through `invokeAsWallet`:

    * An allowed, uncapped deposit succeeds
    * An allowed withdrawal within the spend cap succeeds
    * A withdrawal exceeding the cumulative cap is rejected on-chain (`Auth, InvalidAction`)
    * A call to a method off the allow-list is rejected on-chain
    * The wallet owner (unrestricted) can still call the same disallowed method -- proving the restriction is agent-signer-specific, not contract-wide
  </Tab>

  <Tab title="mcp-protocol.e2e.test.ts">
    Deploys its own independent fresh environment, then spawns the **compiled** `dist/cli.js` binary as a real subprocess and drives it through a genuine `@modelcontextprotocol/sdk` client:

    * Tool listing matches exactly what the on-chain allow-list permits
    * `wallet_info` reports the correct wallet and policy addresses
    * A deposit call with no address argument succeeds, address auto-filled
    * A withdrawal beyond the cap surfaces as `isError: true`, not a crash
  </Tab>
</Tabs>

<Note>
  Every e2e test generates and funds its own fresh testnet keys -- none of them read `.env`, and none of them depend on the state of any previous run. Two runs of the same test file never interfere with each other.
</Note>

## Running everything

```bash theme={null}
cargo test --workspace   # 17 Rust contract tests
bun run typecheck        # src/ + scripts/ + test/
bun run test             # 40 unit tests
bun run build            # compiled dist/cli.js, required before e2e's MCP test
bun run test:e2e         # 9 end-to-end tests against live testnet
```
