Skip to content

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 .env and any keystore — the preflight makes this fatal in live mode
  • npm run lint — clean
  • .env reviewed — DRY_RUN=true, NODE_ENV=development
  • POSITIONS_JSON reviewed — each entry has the right chain, dex, address/tokenId, and for V3 a poolAddress
  • 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

npm run dryrun 2>&1 | tee dryrun-$(date +%Y%m%d-%H%M%S).log

Run for ≥24 hours. During this window:

  1. First 5 min — verify all chains connect, all positions register, no schema warnings.
  2. First 1 h — verify price updates flow on every position (LOG_LEVEL=debug for first hour helps).
  3. Sensitivity probe — temporarily lower TRIGGER_PRICE_DROP_PCT to ~0.5 to force a simulated trigger; confirm:
  4. Executor logs intended tx (router address, amounts, slippage, gas)
  5. Telegram alert fires
  6. firing guard prevents repeat trigger for same position
  7. Reset trigger to production value after probe
  8. Manual command test — send /status and /remove <pair> in Telegram; confirm simulated tx logged.
  9. 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+C for 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.