Testing¶
How to run automated tests, what's covered, and the non-optional manual validation procedure before going live.
Last updated: 2026-08-23 Current status: 323 tests passing. EVM-only
npm audit: 0 vulnerabilities (proxy agents now in scope).npm run security:scan: all invariants hold.
1. Running tests¶
| Command | Mode | Use case |
|---|---|---|
npm test |
Watch mode (vitest) | Local dev while changing code |
npm run test:run |
Single run | CI, pre-commit verification |
npx vitest run tests/<file>.test.js |
Single file | Targeted debugging |
npm run test:cov |
With coverage report | Periodic; before releases |
npm run verify |
lint + security:scan + all tests + audit | The single gate before any push or go-live decision |
Focused suites, so a failure names the area rather than "something broke":
| Command | Covers |
|---|---|
npm run test:security |
Identity/secret redaction, dashboard exposure + availability, preflight, proxy, keystore, config gating |
npm run test:safety |
Anything that can move money: signer split, triggers, approvals, gas cap, slippage, rate limit, executors |
npm run test:resilience |
Staying connected and correct: WS reconnect, monitors, position resolver, broadcast split, HTTP egress |
None of these need an RPC endpoint, a key, or network access — they are pure unit/integration tests against mocks and a real loopback HTTP server.
Vitest config: 📁 vitest.config.mjs.
2. What's covered¶
| Suite | File | Covers |
|---|---|---|
| Config | tests/config.test.js |
zod parsing, NODE_ENV gating, position schema |
| Rate limit | tests/rateLimit.test.js |
Token-bucket refill, minute/hour caps |
| Trigger engine | tests/triggerEngine.test.js |
Predicate eval, firing guard, completed terminal, manualFire |
| Price monitor | tests/priceMonitor.test.js |
V2 reserves, V3 sqrtPriceX96, rolling volume buffer |
| Event monitor | tests/eventMonitor.test.js |
Custom log filter, emergency wallet detection |
| Executor dispatch | tests/executorDispatch.test.js |
DEX routing, pRetry, rate-limit acquisition |
| V2 executor | tests/executorV2.test.js |
Honeypot pre-flight, removeLiquidity, log decode, revoke |
| V3 executor | tests/executorV3.test.js |
NFT ownership, staticCall slippage, multicall |
| Raydium executor | tests/executorRaydium.test.js |
AMM v4 / CPMM / CLMM dispatch, slippage |
| Approval mgr | tests/approvalManager.test.js |
Allowance check, USDT-style reset, revoke, gas cap honoured before broadcast, waits bounded by TX_DEADLINE_SEC |
| Keystore | tests/keystore.test.js |
Priority chain (Ledger > keystore > raw), prod gating |
| Gas | tests/gas.test.js |
EIP-1559 fee build, cap enforcement |
| Slippage | tests/slippage.test.js |
bigint math, edge cases (0 bps, 10_000 bps) |
| PnL | tests/pnl.test.js |
USD value calc from reserves |
| Audit regressions | tests/poc.findings.test.js |
Regression tests for the 2026-07-07 audit fixes: live gas-cap guard (requiredFeePerGas), cumulative price-drop from trailing peak, timer-based max-age sweep, per-window volume-spike baseline, awaited Solana signer. |
| Redaction | tests/redact.test.js |
maskUrl (RPC/webhook key stripping), maskAddress (identity), scrubSecrets (embedded keys/URLs) |
| Security preflight | tests/preflight.test.js |
File-perm checks (600 vs 644), prod-critical vs dev-warning, dashboard/proxy/identity warnings |
| Dashboard security | tests/dashboardSecurity.test.js |
CSP + frame/sniff/referrer headers, GET-only, token gate, no full wallet address in /api/state; malformed-URL/cookie crash resistance, handler-throw containment, position masking in the API |
| HTTP egress | tests/httpClient.test.js |
Proxy parsing (HTTPS_PROXY), fail-closed on missing SOCKS agent |
| Identity leaks | tests/identityLeaks.test.js |
End-to-end: the exact object shapes the real call sites log, pushed through a real pino instance wired with the production serializers, asserting no full address survives. Exists because every shape in it leaked in a live dry run while the isolated serializer tests were green. |
| Signer factory | tests/signerFactory.test.js |
Dry run gets a VoidSigner (cannot broadcast), DRY_RUN_ADDRESS keyless path, keyless failure is permanent not transient, live path keeps NonceManager + private relay, address masked into the store |
| WS reconnect | tests/wsReconnect.test.js |
Rebuild + ws-reconnected emission, retry when the rebuild itself throws, single reconnect on close+error, dead provider destroyed |
| Trigger engine | tests/triggerEngine.test.js |
(above) plus TRIGGER_RETRY_COOLDOWN_SEC backoff after transient failure, per-position isolation |
| Position resolver | tests/positionResolver.test.js |
Boot-time enrichment: V3 pool derivation via factory, V2/V3 token0/token1, chain-wins-on-conflict, empty-position + blacklist warnings, per-PM factory promise cache (incl. no cache poisoning), never-throw/never-drop on RPC failure |
3. Known gaps (queued for future work)¶
In rough priority order:
| # | Area | Why it matters |
|---|---|---|
| 1 | Keystore loader full integration (Ledger mock) | High-value path; current tests cover priority chain but not full Ledger transport |
| 2 | V3 price decoder utility | sqrtPriceX96 → human price; isolated test would catch decimal-handling regressions |
| 3 | WS reconnect (makeWsProvider) |
Currently untested; mock socket disconnects |
| 4 | Alerter fan-out | Telegram + Discord retry on transient HTTP failures |
| 5 | Full suite execution check | Periodic green-light gate before release |
4. Adding a test¶
The repo uses vitest with ESM. Mock pattern (see tests/keystore.test.js for a complete example):
import { describe, it, expect, vi, beforeEach } from 'vitest';
const mockConfig = { /* fields */ };
vi.mock('../src/config.js', () => ({ default: mockConfig }));
vi.mock('../src/logger.js', () => ({
default: { info: () => {}, warn: () => {}, debug: () => {}, error: () => {} },
}));
const { moduleUnderTest } = await import('../src/path/to/module.js');
describe('feature', () => {
beforeEach(() => { /* reset mockConfig */ });
it('does the thing', async () => {
expect(await moduleUnderTest()).toBe(/* … */);
});
});
Notes:
- ESM mocks must be declared before the dynamic await import().
- Always reset mockConfig in beforeEach — mutations leak between tests otherwise.
- For tests that spin up an encrypted keystore, use encryptKeystoreJsonSync with scrypt: { N: 2 } to keep tests fast (default N=131072 is ~3s per call).
5. Manual validation — REQUIRED before going live¶
Unit tests do not verify market behavior, RPC reliability, or trigger sensitivity. Every config change that affects executors, triggers, or chains must pass this checklist before flipping DRY_RUN=false.
5.1 Pre-flight checklist¶
-
npm run test:run— all tests pass -
npm run security:scan— security/privacy invariants hold -
npm audit --omit=dev --omit=optional— 0 vulnerabilities -
chmod 600 .envand any keystore — the preflight makes this fatal in live mode -
npm run lint— clean -
.envreviewed —DRY_RUN=true,NODE_ENV=development -
POSITIONS_JSONreviewed — each entry has the rightchain,dex,address/tokenId, and for V3 apoolAddress - WebSocket RPC URLs are valid for every chain referenced
- Telegram bot token + chat ID set and tested (you should get a startup ping)
5.2 Dry-run procedure¶
Run for ≥24 hours. During this window:
- First 5 min — verify all chains connect, all positions register, no schema warnings.
- First 1 h — verify price updates flow on every position (
LOG_LEVEL=debugfor first hour helps). - Sensitivity probe — temporarily lower
TRIGGER_PRICE_DROP_PCTto ~0.5 to force a simulated trigger; confirm: - Executor logs intended tx (router address, amounts, slippage, gas)
- Telegram alert fires
firingguard prevents repeat trigger for same position- Reset trigger to production value after probe
- Manual command test — send
/statusand/remove <pair>in Telegram; confirm simulated tx logged. - 24h soak — leave running unattended. Look for: WS reconnects (should be rare and recover cleanly), unhandled errors, memory growth.
5.3 Going live¶
After ≥24h clean dry run:
# In .env:
DRY_RUN=false
NODE_ENV=production
# Confirm signer is keystore or Ledger (a raw key is refused whenever the bot can sign)
Restart:
npm start | npx pino-pretty
# OR
docker compose down && docker compose up -d && docker compose logs -f
⚠️ First live run: keep one finger on
Ctrl+Cfor the first hour. Verify on a block explorer that the first real removal (if any fires) has correct amounts and slippage. Past mistakes cost real money.
6. Continuous integration (not yet wired)¶
No CI is configured. When you wire one (GitHub Actions recommended):
# .github/workflows/test.yml
name: test
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '20', cache: 'npm' }
- run: npm ci
- run: npm run lint
- run: npm run test:run
- run: npm run audit:evm-only
Add a coverage badge once CI is up.