Peersky Browser has 291 tests across 9 suites. They cover protocol handlers, P2P networking and file sync, backup and identity transfer, extension lifecycle, security policies, LLM streaming, the auto-updater, and performance regressions.
npm test # Every suite + combined tally (~8 min)
npm run test:ci # Everything except integration (what CI runs)
npm run test:p2p # Protocol handler units (~15s)
npm run test:p2p:e2e # E2E sync (~2-3 min)
npm run test:backup # Backup / restore / identity transfer (~2s)
npm run test:extensions # Extension lifecycle (~1s)
npm run test:security # Security policies (~1s)
npm run test:llm # LLM streaming + dispatcher contract (~1s)
npm run test:updater # Auto-updater (~1s)
npm run test:perf # Performance regressions (~5s)
npm run test:integration # App restart (~5+ min)
npm run coverage # Coverage reportnpm test and npm run test:ci go through test/run-tests.mjs, which runs
each suite as its own mocha process and prints one table at the end:
SUITE PASS FAIL SKIP RESULT
-----------------------------------------------
p2p 58 0 0 ok
backup 42 0 0 ok
...
-----------------------------------------------
TOTAL 186 0 0
It does not stop at the first failure, so one run shows the whole picture. A
suite that dies before mocha prints its epilogue is reported as CRASH rather
than counted as zero, and the run exits non-zero naming what failed.
Pass suite names to run a subset: node test/run-tests.mjs backup llm.
Run single test:
npx mocha test/p2p/ipfs-handler.test.js --timeout 20000
npx mocha test/p2p/ipfs-handler.test.js --grep "CID norm" --timeout 20000test/run-tests.mjs— Suite runner and combined tally (not a spec)test/setup.js— PolyfillPromise.withResolversfor Node.js <22test/p2p/*.test.js— Unit & E2E tests with mocked/real nodestest/backup/— Archive, encryption, restore, identity transfertest/extensions/— Extension lifecycle, browser-action broadcast, CRX parsingtest/security/— Write policy & manifest validationtest/llm/— Streaming and dispatcher contracttest/updater/— Auto-updater statestest/perf/— Performance regressions (seetest/perf/README.md)test/integration/— Real Electron app restarttest/fixtures/— Shared fixtures (no specs)
- esmock — Isolates handlers with mocked dependencies (no filesystem, network, or libp2p calls)
- Promise wrapper — Converts Electron callback style to async/await
- PEERSKY_TEST_USERDATA env var — Isolates test data per run
- CID: v0→v1 normalization
- PeerId:
Qm...(base58) → peerIdFromString,bafz...(base32 CID) → peerIdFromCID - ENS:
ipfs-nscodec (serve CID),ipns-nscodec (route via IPNS), fallback (strip prefix) - Upload naming: Single file → filename, directory → folder name, multiple → parent dir
- MIME detection: By extension + HTML sniffing (first 512 bytes)
- Upload cache: Metadata tracking with timestamp/URL/name
- p2pmd rooms: an encrypted holesail seed survives a restart, an undecryptable one falls back to the room key rather than failing, and a failed request answers with its reason instead of dropping the connection
- Protocol initialization (IPFS & Hyper)
- File upload & DHT discovery (local + delegated routing)
- Directory serving & index.html auto-serve
- File content round-trip verification
- libdatachannel error suppression (non-fatal WebRTC teardown errors)
- Install/update/uninstall lifecycle
- Service worker reload & state persistence
- GET always allowed, POST/PUT/PATCH require
p2pWritepermission - Dangerous permissions blocked (nativeMessaging, debugger, desktopCapture)
- Path traversal protection (
../rejected) - Extension detection via
chrome-extension://in referrer
- Archive streaming, SHA-256 manifests, zip-slip path validation
- Passphrase-encrypted wrapper (scrypt + AES-256-GCM)
- Transactional restore: staging, swap, rollback on failure
- Receiver-sealed identity transfer and verification-code derivation
- Streaming response parsing, including malformed chunks
- undici dispatcher contract: the Agent must not reach global fetch
- Manual check, pending update, and already-latest states
- Squirrel error handling and install restart
These pin the work the browser does on its hot paths — renderer round-trips, disk stats, node boots, state writes — rather than wall-clock times, which vary too much across machines to assert on. Each one guards a shape that was slow once, so re-introducing it fails the build.
- p2p backends (Helia, hyper-sdk, the WebTorrent worker) do not start before the first window can paint, and a burst of requests starts exactly one of each
- Extensions load into the session together, and a tab that attaches while the extension host is still booting is queued rather than lost
- A drag-sized burst of window/tab events writes the session once, not once per event, and the last request in a burst is never dropped
- Internal assets resolve once and revalidate (304) instead of being re-read,
and
browser://themeno longer stamps a fresh ETag per request - Opening an extension popup costs no renderer round-trip and no directory scan
- Real Electron app restart with extension persistence
- Service worker survives restart
DHT test passes if any of these:
- Local DHT finds provider (<10s, happens if test ran before)
- Delegated routing finds provider (2+ min, needs internet)
- 0 providers after 2 min — test skips (expected)
If failing: curl https://delegated-ipfs.dev/health (check delegated routing)
cross-env PEERSKY_TEST_USERDATA=.my-test npm run test:p2p:e2e
ls -la .test-e2e-data/ # View test data after runDEBUG_P2P=1 npm run test:p2p:e2eThe app's own console transport is set to info. Hot-path lines (per session
save, per badge update) are logged at debug, so raise the level to see them:
PEERSKY_LOG_LEVEL=debug npm startrm -rf .test-e2e-data-* # Delete stale test data
npx mocha test/extensions/*.test.js --reporter specnpm run coverage # Generate NYC coverage report
# Opens coverage/index.html with line/branch/function/statement coverage
# Config: .nycrc.json or package.json nyc field
# Includes: src/protocols/*.js, test/p2p/*.js
# Excludes: node_modules, test fixtures, preload.js- Choose a suite: mocked handler →
test/p2p/*-handler.test.js, real nodes →test/p2p/p2p-e2e.test.js, backup →test/backup/, extensions →test/extensions/, security →test/security/, integration →test/integration/ - Write the test using the
callHandler(handler, request)wrapper andexpect() - Run it:
npx mocha test/p2p/ipfs-handler.test.js --grep "name" --timeout 20000 - Update this guide if it demonstrates new behaviour
Adding a file to an existing directory needs no wiring — the suite's glob picks it up and the count rolls into the total.
A new directory also runs with no wiring. test/run-tests.mjs runs any spec
no test:* script claims, grouped per directory, with default flags
(--timeout 20000 --exit). It appears in the table marked *:
settings * 2 0 0 ok
* auto-discovered (settings) — add a test:<name> script to control its flags.
Add a test:<name> script when the defaults are not right — a longer timeout,
env vars, or a pre-step like rimraf. Once the script exists it takes over, and
the * disappears.
Two rules the runner applies so this stays predictable:
- Explicit scripts win. Only specs nothing else claims are auto-discovered,
so
test/p2p/p2p-e2e.test.jskeeps running throughtest:p2p:e2ewith its env and clean data dir rather than being picked up bare. - Subsumed suites are skipped. A script whose specs are a subset of another's
(
test:p2p:btinsidetest:p2p) is not run twice. It still works on its own.
Auto-discovery only applies to a full run. Naming suites
(node test/run-tests.mjs backup llm) runs exactly those.