# peach-browser — Test Coverage Analysis **Date:** 2026-06-11 **Base:** dev lane, HEAD `7100d7d25214` (v3.9.8) **Method:** Full read of `src/` (130 source modules, 43,826 LOC) cross-referenced against the test suite (109 test files). **Mode:** Read-only analysis. No source modified. **Test count:** 1,632 literal `it()`/`test()` declarations (1,062 unit/component in `src/`, 570 Playwright e2e in `tests/`). Many suites are parameterized via `it.each`/loops (e.g. `content.adversarial`, `vault.adversarial`, `codex.adversarial` each declare "≥200 cases"), so the **executed** assertion count recorded in `LOG.md` for the v3.9.x line is ~3,200. Headline ratio: ~1,632 declared tests against ~44k LOC ≈ **1 test per 27 LOC** — high by volume, but unevenly distributed (see §3, §5). --- ## Section 1 — Module Inventory ### Background (`src/background/`) — service worker, vault, sync, passkeys | Module | Lines | Purpose | |--------|-------|---------| | vault.ts | 2890 | Core vault: unlock/lock, AES-GCM crypto, entry CRUD, password history, soft delete/trash, duress vault, vault-history snapshots, sync merge (`applyMergedRemoteEntries`), changePassword, recovery escrow, `normalizeEntry` whitelist | | messages.ts | 1610 | Message router: `GET_ENTRY_FOR_FILL`/`_EDIT`, `AUTOFILL_QUERY`, `GET_RECOVERY_WORDS`, destructive-op gating, canary, `SAVE_NOTE`, sync alerts, license/pro gate | | passkey.ts | 1413 | WebAuthn interception — Chrome `webAuthenticationProxy` + Firefox content-proxy create/get ceremonies, approval flow, pending-request tracking | | passkey-crypto.ts | 370 | WebAuthn crypto: ES256/Ed25519 keypair gen, COSE/CBOR encoding, P1363→DER, authenticator data | | codex-handlers.ts | 310 | Codex backup restore: verify-code validation, blob decompress, metadata validation before unlock | | chirp-sync-vault-adapter.ts | 438 | Sync crypto boundary: per-entry AES-GCM key derivation, entry/tombstone apply, device-manifest sign/verify | | chirp-sync-coordinator.ts | 295 | Manifest-diff sync protocol (HELLO→MANIFEST→REQUEST→ENTRIES→TOMBSTONES→DONE) over Noise session | | chirp-sync-runner.ts | 181 | Production sync runner: vault adapter + Noise XX transport + IP cache + subnet-probe fallback | | sync-trigger.ts | 192 | Sync dispatcher: vault-change/foreground/manual/mDNS triggers, per-device runner, `trustSource:'direct'` filter | | clipboard.ts | 201 | Clipboard write + auto-clear via `chrome.alarms` + offscreen (HV-2) | | ml.ts | 200 | Logistic-regression field classifier (hard-coded weights) | | alarms.ts | 69 | Auto-lock via `chrome.alarms` + idle detection | | paired-devices.ts | 180 | Paired-device registry + last-known-IP cache | | chirp-identity.ts | 143 | Local device ID (UUID, storage.local) + device name | | health.ts | 170 | Advanced vault-health analysis (Pro): age/reuse/exposure scoring | | license.ts | 169 | Ed25519 license signature verification + Pro gate | | self-test.ts | 110 | Crypto KAT on unlock (PCH-01-020 pinned vector) | | sync-alerts.ts | 77 | Pending sync-alert queue (HV-1b) | | pending-vouched-devices.ts | 81 | MV-7 queue for vouched devices awaiting user confirmation | | commands.ts | 69 | Keyboard-shortcut → summon-inline routing | | entry.ts | 73 | MV3 lock-on-close port (LV-5 nonce'd port) | | offscreen.ts | 43 | Offscreen-document clipboard clear (HV-2) | | recovery-pending.ts | 33 | Transient recovery-confirm flag (suppresses lock-on-close) | ### Content / Autofill (`src/content/`) — the injection layer | Module | Lines | Purpose | |--------|-------|---------| | inject.ts | 4405 | Inline UI rendering: `showAutofillSelector`, `injectFieldIcon`, `showTOTPSelector`, `showCardSelector`, passkey toasts/gate/offer, remember-site toast, smart-generator drawer, anchor positioning | | index.ts | 1263 | Autofill orchestration: `handleFieldInteraction`, 3-layer dismissal model, fill-success lock, focusin/mutation observers, `initialScan` aggression levels, TOTP/card paths, message routing (`SUMMON_AUTOFILL`/`VAULT_UNLOCKED`/passkey), search palette, Firefox passkey bridge | | detect.ts | 357 | Field/form detection: login/signup/email-first/TOTP/card detection, deep active-input, visibility, CSS.escape (PCH-01-025) | | passkeys-firefox.ts | 325 | Firefox MAIN-world `navigator.credentials` interception proxy | | save.ts | 309 | Save-on-submit detection (offer to save new credentials) | | content-ml-detector.ts | 190 | Content-side ML field-prediction singleton | | fill.ts | 136 | Field filling: `fillFields` (cross-origin-safe), `setNativeValue` (React), card fill, Luhn/network | | policy.ts | 130 | Site password-policy extraction (min/max/forbidden chars) | | passkeys-firefox-relay.ts | 68 | MAIN↔content allowlisted postMessage relay | | identity-helper.ts | 58 | Saved-identity sort/filter for signup surface | | interaction.ts | 40 | `resolveAutofillInteractionTargets` (which fields/whether to open) | | passkey-toast/* | ~300 | Preact passkey approval toast (inject.tsx, PasskeyToast.tsx, css) | ### Crypto (`src/crypto/`) | Module | Lines | Purpose | |--------|-------|---------| | peachPayload.ts | 1080 | Codex/backup payload: Argon2id `deriveKey` + NFC, AES-GCM/ChaCha, zstd, BLAKE3 checksum, section encoding, decompression bounds | | argon2.ts | 52 | Argon2id KDF with NFC normalize + raw fallback | | zstd.ts | 51 | zstd compress/decompress with output-size cap (MV-4) | | totp.ts | 40 | TOTP generation + `parseOtpauthURI` (algorithm/digits/period) | | peachScript.ts | 64 | PUA (U+E000..E0FF) byte↔char codec | ### Shared (`src/shared/`) | Module | Lines | Purpose | |--------|-------|---------| | import.ts | 605 | Multi-format import (1Password/Bitwarden/ProtonPass/Credential-Exchange/CSV/Peach) | | types.ts | 543 | Cross-boundary type definitions (no runtime logic) | | backup.ts | 295 | Peach encrypted backup export/import (canonical NFC raw-retry pattern) | | codex-types.ts | 288 | Codex payload types + `computeParityShards`/`deriveShardCounts` | | recovery-word-list.ts | 275 | BIP39 2048-word English list | | chirp-sync-transport.ts | 292 | WebSocket client + Noise XX + subnet probe (rate-limited) | | chirp-pairing.ts | 291 | Audio pairing driver (chirp exchange → Noise NK) | | chirp-handshake.ts | 261 | Post-chirp NK + identity frame exchange + SAS derivation | | noise-xx.ts | 260 | Noise XX mutual-auth handshake (25519/ChaChaPoly/BLAKE2s) | | sync-identity.ts | 189 | Vault-tied X25519/Ed25519 derivation + vault ID | | ggwave.ts | 187 | WASM audio-codec loader + ChirpCodec wrapper | | shortcuts.ts | 185 | Keybinding schema/format/match/conflict | | recovery-words.ts | 181 | BIP39 recovery (12-word ↔ root key) + HKDF KEK (128-bit prefix) | | noise-js.ts | 422 | Noise NK handshake + post-handshake HMAC auth proof | | device-manifest.ts | 221 | Signed device-graph manifest + freshness window | | chirp-payload.ts | 139 | 64-byte audio chirp payload codec (CRC32) | | generate.ts | 75 | CSPRNG password generator (rejection sampling, ≤128 chars) | | favicon.ts | 95 | Favicon URL + fallback initials/colors | | domain.ts | 49 | eTLD+1 extraction (tldts) + homograph detection (LV-2) | | browser.ts | 34 | Feature-detected Chrome/Firefox identification | | passkey-utils.ts | 53 | WebAuthn rpId validation (SEV-1-A) | | sync-limits.ts / import-limits.ts | 68 | Resource bounds (MV-3/MV-4/HV-1 clamp) | | Others (ml, storage, config, bridge, native, passkey-flags, local-network) | ~250 | Thin wrappers / constants / types | ### Popup UI (`src/popup/`) | Module | Lines | Purpose | |--------|-------|---------| | Settings.tsx | 2146 | Settings: password change, recovery words, license, feedback, appearance, sync | | Entry.tsx | 1779 | Entry view/edit: copy, TOTP, inline edit, identity | | App.tsx | 1707 | Root: routing, vault state, message dispatch | | Lock.tsx | 1058 | Unlock/create vault, master-password strength, restore options | | Import.tsx | 912 | Import file parse + mode selection | | Add.tsx | 764 | Create new entry | | Vault.tsx | 733 | Vault list, search, sort | | codex/pdf-import.ts | 703 | Codex PDF → Aztec extraction (MV-5/MV-9/PCH-01-014) | | CodexRestoreView.tsx | 694 | Codex restore flow (PCH-01-015) | | Export.tsx / Health.tsx / ChirpSync.tsx | 1576 | Export, health dashboard, sync UI | | codex/generate.ts | 484 | Codex generation pipeline (compress→erasure→Aztec→PDF) | | codex/{restore,erasure,aztec,pdf,reed-solomon,helpers}.ts | ~1480 | Codex erasure/restore internals | | Recovery/Notes/Trash/Panic/Onboarding views | ~1400 | Recovery words, notes editor, trash, panic, onboarding | | components/{GeneratorDrawer,ChirpPairingFlow,Rune*,Note*}.tsx | ~1700 | Generator drawer, pairing flow, rune canvases, note components | | chirp-pair.ts / chirp-pair-capacitor.ts | 557 | Pairing UI driver (gateOnSAS) | --- ## Section 2 — Test Suite Inventory ### Unit / component tests (`src/`, Vitest) — 1,062 declared | Test File | Tests | Covers | |-----------|-------|--------| | background/vault.test.ts | 76 | Unlock/lock, entry CRUD, password change, duress, biometric unlock, MV-6 whitelist, LV-1 NFC, PCH-01-008/009 | | background/messages.test.ts | 51 | All message types, MV-1/MV-2, LV-3/LV-4, TOTP strip, pro gate | | background/messages.adversarial.test.ts | 55 | Origin spoofing, PCH-01-006/007, PCH-01-018 destructive-op gating | | background/vault.adversarial.test.ts | 44 | Password boundary cases (unicode/emoji/RTL/control/injection), batchAdd invalid | | background/recovery.adversarial.test.ts | 27 | Recovery word tamper at every position, garbage rejection, password edge cases | | background/recovery-words.test.ts | 26 | 12-word round-trip, validation, recovery cycles, survives changePassword | | background/chirp-sync-vault-adapter.test.ts | 23 | MV-3 bounds, MV-7 queue, MV-8d key-rotation, PCH-01-005 separator, entry round-trip | | background/passkey-chrome-ceremony.test.ts | 23 | Chrome create/get, isUvpaa, locked-vault wait, origin resolution | | background/vault-history.test.ts | 14 | Snapshots, PCH-01-022 bucket coarsening, dedup | | background/paired-devices.test.ts | 12 | Upsert/find/remove, IP cache, legacy compat | | background/passkey-firefox-proxy.test.ts | 12 | Firefox create/get, cross-origin rejection, enumeration protection | | background/clipboard.test.ts | 11 | HV-2 alarms+offscreen, PCH-01-021 cold-start hash, re-arm | | background/chirp-sync-runner.test.ts | 11 | Locked check, IP cache, subnet probe, vault-id mismatch | | background/notes.test.ts | 11 | Note CRUD/sanitization | | background/sync-trigger.test.ts | 11 | Per-device sync, concurrency guard, MV-7 filter | | background/chirp-sync-static.test.ts | 9 | Static sync helpers | | background/health.test.ts | 7 | Reuse/age scoring, hashes-never-stored | | background/codex-handlers.test.ts | 7 | Pro/locked gates, PCH-01-017 validation | | background/chirp-sync-coordinator.test.ts | 7 | HELLO, version short-circuit, clock skew, full sync | | background/passkey-crypto.test.ts | 7 | ES256 keypair, authenticator data, COSE | | background/alarms.test.ts | 6 | Auto-lock alarm, idle lock | | background/vault.sync-clamp.test.ts | 6 | HV-1 clamp + REMOTE_PASSWORD_CHANGE/DELETE alerts | | background/aes.test.ts | 5 | AES-GCM primitives | | background/chirp-identity.test.ts | 3 | PCH-01-004 deviceId local + migration | | background/{self-test,passkey-deny-on-close,entry.lock-port,session-escrow}.test.ts | 8 | KAT (PCH-01-020), deny-on-close, LV-5 port, PCH-01-008 escrow | | crypto/peachPayload.test.ts | 25 | Entry round-trips, AEAD, padding, PCH-01-001 NFC (5 cases), MV-4 | | crypto/peachScript.test.ts | 24 | 256-byte codec, PUA validation, fuzz corpus | | crypto/{argon2,totp,crypto,decompression-bomb}.test.ts | 22 | LV-1 NFC parity, TOTP params, MV-4 caps | | content/content.adversarial.test.ts | 47 | extractDomain boundaries, field-detection adversarial, fillFields safety, Luhn | | content/detect.test.ts | 15 | Login/signup/TOTP/card detection, PCH-01-025 CSS.escape | | content/policy.test.ts | 11 | Password-policy extraction | | content/passkey-toast/passkey-toast.test.ts | 10 | Passkey toast rendering | | content/fill.test.ts | 9 | fillFields, cross-origin refusal, card fill | | content/passkeys-firefox-relay.test.ts | 6 | Relay allowlist, cross-frame block | | content/{identity-helper,interaction}.test.ts | 8 | Identity sort/filter, interaction targets | | content/inject.keyguard.test.ts | 2 | **PCH-01-007 isTrusted gating — the ONLY test touching inject.ts** | | popup/codex/codex.adversarial.test.ts | 47 | parseRawScan/ChunkBuffer/reconstruct adversarial, PCH-01-010/011/012/013/026 | | popup/codex/codex.test.ts | 26 | Compression, erasure, verify code, handlers | | popup/codex/codex.suite.test.ts | 21 | Chunk/stripe planning, Aztec import, bundle hygiene | | popup/codex/{generate,restore,pdf-import,pdf-import.clamp,browser-bundle}.test.ts | 59 | Erasure pipeline, restore, PDF import, PCH-01-014 clamp, MV-5 | | popup/views/contracts.test.tsx | 21 | Lock/Export/Health/Entry/Import/Recovery/Vault/Settings contracts | | popup/views/NoteEditor.toHtml.test.ts | 21 | Note HTML sanitization (THREAT-006) | | popup/views/{vault-search,Export.csv,zxcvbn-strength,CodexRestoreView,CodexGenerateView,Health}.test | 13 | Search/sort, CSV injection, LV-8 zxcvbn, PCH-01-015 | | popup/components/GeneratorDrawer.test.ts | 24 | Generator policy constraints (≤128) | | popup/components/ChirpPairingFlow.test.tsx | 9 | Pairing phases | | popup/{chirp-pair,chirp-pair-capacitor}.test.ts | 14 | PCH-01-024 gateOnSAS | | shared/import.test.ts | 17 | Bitwarden/ProtonPass/1Password/CredExchange/Peach, MV-5, PCH-01-019/026 | | shared/browser.test.ts | 17 | Chrome/Firefox detection + derivatives | | shared/shortcuts.test.ts | 15 | Keybinding match/format/conflict | | shared/sync-identity.test.ts | 13 | X25519/vault-id derivation determinism | | shared/{noise-xx,noise-js,noise-blake2s-kat}.test.ts | 15 | Full XX/NK handshakes, PCH-01-002/003/003b KATs | | shared/{chirp-handshake,chirp-pairing,chirp-payload,chirp-sync-transport}.test.ts | 35 | SAS, pairing, payload codec, subnet cooldown | | shared/{device-manifest,generate,backup,ggwave,passkey-utils,domain.homograph,favicon}.test.ts | 53 | Manifest sign/verify, generator, backup, ggwave, rpId (SEV-1-A), homograph (LV-2) | ### End-to-end tests (`tests/`, Playwright, real Chromium + built extension) — 570 declared | Test File | Tests | Covers | |-----------|-------|--------| | vault-lifecycle.spec.ts | 55 | Create→unlock→lock→persist, SW restart | | keyboard-comprehensive.spec.ts | 44 | Global shortcuts, navigation | | settings-persistence.spec.ts | 42 | Settings survive reload (auto-lock, clipboard, theme) | | lock-screen.spec.ts | 37 | Master password, recovery words, lock/unlock | | entry-comprehensive.spec.ts | 35 | Field validation, notes, URLs, edit/delete | | visual-compliance.spec.ts | 36 | Animation/transition/visual-order compliance | | password-generator.spec.ts | 33 | Generator UI, entropy, integration | | autofill-deep.spec.ts | 32 | Icon injection on **synthetic** forms, matching, security | | vault-security.spec.ts | 32 | XSS/CSP/session (mix of behavioral + static grep) | | import-export-robust.spec.ts | 29 | CSV/JSON import/export, duplicates, unicode | | keyboard-edge.spec.ts | 29 | Focus traps, overlay behavior | | clipboard.spec.ts | 23 | Copy + timed clear | | search.spec.ts | 23 | Filtering, empty state, special chars | | a11y-totp-vault.spec.ts | 22 | ARIA labels, focus order | | totp-edge.spec.ts | 22 | Base32 parsing, invalid inputs | | security-visual.spec.ts | 21 | No exposed credentials, safe animations | | entry-flows-state.spec.ts | 18 | Add/edit/delete/copy/reveal/TOTP flows | | settings.spec.ts | 10 | Settings visibility, toggles | | {entry-crud,copy-totp}.spec.ts | 14 | CRUD, TOTP copy | | codex-pdf-restore.spec.ts | 4 | Codex PDF import (hard-coded local fixture path) | | autofill.spec.ts | 6 | Content-script injection smoke | | passkeys.spec.ts | 1 | One real Chrome create()+get() ceremony | | passkeys-firefox.spec.ts | 0 | **10 skipped** (gated on `PEACH_E2E_FIREFOX_PASSKEYS=1`) | | {visual-check,diagnostic}.spec.ts | 2 | Placeholders | --- ## Section 3 — Coverage Assessment by Module ### src/background/vault.ts **Rating:** Adequate **Tested:** unlock/lock, entry CRUD with storage-failure rollback, changePassword + session re-escrow (PCH-01-008), biometric unlock root-key rehydration (PCH-01-009), constant-time parallel-slot unlock, duress equal-length slots, monotonic timestamps, NFC normalize + legacy heal (LV-1), proto-pollution filter (LV-7), HV-1 clamp + alerts, snapshot bucketing (PCH-01-022), MV-6 import whitelist with passkey/canary shape + HTTPS canary check. **Not tested:** - `purgeExpiredTrash` retention cutoff — off-by-one would leak past-retention entries or erase early. No functional test. - `consolidatePasskeyEntries` merge heuristic — called after passkey registration; only exercised via the legacy-migration import path, never directly. Wrong winner = duplicate or wrong-credential entry. - `getIdentities` dedup logic for autofill identity suggestions — null-timestamp / domain-extraction-failure paths untested. - `checkBreach` HIBP k-anonymity (SHA-1 prefix/suffix) — network stubbed; encoding/parse never verified. - `restoreEntry`/`purgeEntry` (undelete / hard-delete) — no direct test; restore-after-restore monotonic case untested. - `consumeLastUnlockFailureReason` corruption signal (good tag / bad schema, M7) — never asserted; corruption vs wrong-password indistinguishable in test. - Concurrent writes (two `updateEntry` racing on the single storage key). **Risk:** Medium. The crown-jewel module's high-value mutating paths (changePassword, MV-6) are tested, but several destructive/lifecycle paths (trash purge, hard delete, passkey consolidation) and the corruption-vs-wrong-password discrimination are not. ### src/background/messages.ts **Rating:** Strong **Tested:** all 51 message types, MV-1 (origin re-validation + explicitSummon + mismatch→not_found + unparseable→origin_unverified), MV-2 (includeAll behind userGesture), LV-3 (canary rate-limit), LV-4 (recovery-words re-auth), PCH-01-006/007 (EDIT origin), PCH-01-018 (4 destructive ops × content/extension matrix), PCH-01-019 (TOTP code generated, seed stripped), pro gate. **Not tested:** `GET_ENTRY_FOR_EDIT` via `sender.url` (only `sender.tab` exercised); `AUTOFILL_QUERY includeAll` against a **locked** vault; PCH-01-016 batch-add canary/tag sanitization (BATCH_ADD tested but not the sanitize path); forged `rpId`+`origin` match attempt on `GET_PASSKEY_FOR_DOMAIN`; IPv6 webhook-URL variants ([::1], fc00::/7). **Risk:** Low–Medium. PCH-01-016 batch sanitization is the notable security gap (see §4). ### src/background/passkey.ts / passkey-crypto.ts **Rating:** Strong (Chrome) / Adequate (Ed25519, crypto edges) **Tested:** Chrome isUvpaa-when-locked, locked-vault wait, origin resolution (subdomain/exact/fallback), create/get ceremonies, cancel cleanup; Firefox cross-origin rejection (SEV-1-A), forged clientDataJSON.origin rejection, enumeration protection. **Not tested:** Ed25519 (-8) keypair round-trip (only -7); algorithm-preference order; `p1363ToDer` edge cases (r/s=0, high-bit boundaries); malformed `privateKeyJwk` import; rpId case-insensitivity match; `consolidatePasskeyEntries` ran-assertion after create. **Risk:** Medium — Ed25519 passkeys are a supported algorithm with zero round-trip coverage. ### src/background/clipboard.ts / offscreen.ts / alarms.ts **Rating:** Strong / Adequate / Adequate **Tested:** HV-2 alarm arm + offscreen route + Firefox fallback, PCH-01-021 cold-start hash skip, re-arm on second copy, failed-clear re-arm; auto-lock alarm + idle lock. **Not tested:** offscreen `createDocument` non-"already-exists" error; `alreadyWritten=true` path; both clipboard APIs failing; `clearAutoLock`; idle `MIN_IDLE_SECONDS` clamp boundary; offscreen message-type validation; `navigator.clipboard` reject → execCommand fallback. **Risk:** Low. ### src/background/{license,ml,sync-alerts,commands,self-test}.ts **Rating:** license **Weak**, ml **Missing**, sync-alerts **Weak**, commands **Weak**, self-test **Strong** **license.ts:** Ed25519 signature verification, base58 decode, prefix/checksum/length — **no direct tests** (only indirect pro-gate triggering). A broken verifier would not be caught. **ml.ts:** logistic-regression prediction — **no test file**; softmax stability, feature-vector completeness untested. **sync-alerts.ts:** queue filter/bound — no unit tests; tested only via vault. **commands.ts:** ping timeout, restricted-URL handling — no tests. **self-test.ts:** PCH-01-020 real-KAT divergence — Strong. **Risk:** Medium for license (a defeated signature check = free Pro / forged license with no test). ### Sync cluster (chirp-*, noise-*, sync-identity, device-manifest, paired-devices) **Rating:** Strong across the board. **Tested:** full Noise XX **and** NK end-to-end handshakes (not just KATs), BLAKE2s cross-repo KAT byte-identical to Android (PCH-01-002), NK handshake-hash binding (PCH-01-003/003b), deviceId local+migration (PCH-01-004), per-entry HKDF separator (PCH-01-005), MV-3 bounds, MV-7 queue + sync-trigger filter, MV-8d key-rotation alert, SAS gate (PCH-01-024), PCH-01-018 pre-msg3 abort (in isolation). **Not tested:** subnet-probe→Noise-XX **integration** (pin check tested in isolation, not against a live wrong-peer responder — the PCH-01-018 threat surface end-to-end); WebSocket `onerror`/`onclose` handling (PCH-01-026 #9); timestamp-clamp and manifest-freshness boundary values; `rotateSyncIdentity` function itself (MV-8b — only the key-rotation *alert* is tested); chirp playback failure / mic discontinuity. **Risk:** Low–Medium. The one notable security gap is the absence of an end-to-end test that a probed wrong-peer never receives the long-term static (PCH-01-018). ### src/crypto/peachPayload.ts **Rating:** Adequate **Tested:** entry/passkey/card/note round-trips, AEAD tamper, bucket padding, PCH-01-001 NFC (NFD-seal→NFC-open, NFC→NFC, codex-restore NFD, legacy raw fallback, CJK+combining), v3 email field. **Not tested:** malicious **declared-length > cap** rejection (MV-4 declared-length path is implicit, never asserted in payload context); malformed/truncated zstd frame; unknown-section skip; section exactly at 64 MiB boundary. **Risk:** Medium — the declared-length bomb guard in peachPayload is not directly exercised (only the zstd-output cap is, in `decompression-bomb.test.ts`). ### src/crypto/totp.ts **Rating:** Weak **Tested:** param threading (SHA256/8/60), legacy defaults, `parseOtpauthURI`. **Not tested:** **no RFC 6238 known-answer vectors** for SHA1/SHA256/SHA512; time-step boundary roll; clock-skew window; digits/period range validation; base32 secret validation. Production correctness rests entirely on the `otpauth` library with no independent vector check. **Risk:** High for a TOTP feature — a library regression or wrong byte-order would ship silently. A wrong 2FA code is a hard user-facing failure. ### src/crypto/{argon2,zstd,peachScript}.ts **Rating:** argon2 Adequate, zstd Strong (bounds) / Weak (format), peachScript Strong **Not tested:** Argon2id **KAT vectors** + parameter (m/t/p) verification — a silent param downgrade (e.g. memory cost) would not be caught; zstd malformed-frame handling; peachScript multi-MB payloads / Unicode-space normalization. **Risk:** Medium for argon2 — no test asserts the KDF actually runs at the intended cost. ### src/shared/recovery-words.ts + recovery-word-list.ts **Rating:** Adequate / **Unaudited** **Tested:** 12-word round-trip, validation (case/whitespace), tamper at every position, recovery cycles, survives changePassword, 100-word BIP39 sampling. **Not tested:** **BIP39 checksum rejection** with a known-good-words/bad-checksum phrase (current "wrong words" test uses `Array(12).fill('abandon')` which is valid words but its checksum status is unasserted); **128-bit prefix-KEK construction** (PCH-01-005 recovery escrow) — no test that bytes 16–31 are zero-padded / that two roots differing only past byte 16 yield the same words; **word-list integrity** — no test that the 2048-word list is unique and byte-identical to the official BIP39 English list. A single transcription error would silently break recovery for affected phrases with no test catching it. **Risk:** High — recovery is the disaster path; the word-list integrity and checksum-rejection gaps are exactly where a silent, unrecoverable data-loss bug hides. ### src/shared/import.ts + backup.ts **Rating:** Strong / Adequate **Tested:** Bitwarden/ProtonPass/1Password-8/CredentialExchange/Peach formats, format detection, LastPass grouping, MV-5 size cap, PCH-01-019 TOTP params, PCH-01-026 single-parse + trim fix; backup wrong-password/malformed/missing-field rejection. **Not tested:** **CSV formula injection** on the *import* side (Export side is covered by Export.csv.test.ts; import accepts `=cmd`/`@`/`+` verbatim); truncated CSV; **the LV-1 NFC raw-fallback in import.ts/backup.ts** — the `password !== password.normalize("NFC")` branch is never exercised with a genuinely non-NFC password, so the "old non-NFC backup still opens" guarantee is unverified at this layer; 1Password trashed-item skip; CredentialExchange invalid-PKCS8 path. **Risk:** Medium — the backup NFC raw-fallback is the canonical pattern PCH-01-001 was modeled on, yet its own decrypt path is untested (see §4). ### src/shared/generate.ts **Rating:** Strong **Tested:** exact length, forbidden-char exclusion (all pools), fallback pool, smart-policy min/max clamp, rejection-sampling no-modulo-bias. **Not tested:** all-chars-forbidden → empty pool (potential infinite loop / crash); 128-char boundary; `minLength > maxLength` policy; NaN policy values. **Risk:** Low–Medium — the empty-pool hang is a real (if narrow) availability bug. ### src/shared/domain.ts / passkey-utils.ts / browser.ts **Rating:** Strong. **Tested:** homograph detection (Cyrillic/punycode/bare/ASCII/malformed, LV-2); rpId exact/suffix/spoof/bare-eTLD/dotless/case (SEV-1-A); Chrome/Firefox detection + derivatives. **Not tested:** LV-2 UI *surfacing* of the warning (only the detector); extractDomain on IPs / multi-part TLDs; rpId with port / IPv6. **Risk:** Low. ### Content / autofill layer See **Section 5** — assessed behavior-by-behavior. ### Popup views (App, Settings, Entry, Lock, Import, Add, Vault, Export, Onboarding, Panic, Notes, Trash, ChirpSync) **Rating:** Weak (unit) — covered mainly by Playwright e2e. `contracts.test.tsx` (21 tests) is the only component-level coverage and verifies: Lock unlock/create + strength gate; Export surfaces; Settings change-password/recovery/license/feedback-XSS/Firefox-consent; Entry copy+canary/TOTP/inline-edit-blur/empty-password-reject; Vault search/sort; Import file-select/dup/replace/onboarding/encrypted-reparse; RecoveryWordEntry → RECOVER_WITH_WORDS. **Add, Onboarding, Panic, TrashView, ChirpSync, NotesEditor, RecoveryWordDisplay have zero component tests.** The 2,146-line Settings and 1,779-line Entry views have only the handful of contract assertions above. **Risk:** Medium — large stateful views rely entirely on e2e, which (see §5/§8) is happy-path-heavy. ### Codex (popup/codex/*) **Rating:** Strong. **Tested:** erasure encode/decode with loss, ChunkBuffer state machine, parseRawScan/parseChunkEnvelope adversarial, reconstruct fingerprint checks, full single-stripe round-trip, all of PCH-01-010/011/012/013/014/015/026 + MV-5. **Not tested:** **multi-stripe restore** (generate tests multi-stripe; restore only single-stripe); **MV-9 pdf.js `isEvalSupported:false`/disableFontFace/Range/Stream flags** (no test asserts they're set); reed-solomon try/finally free on trap; full UI generate→PDF→restore round-trip. **Risk:** Medium — multi-stripe restore (large vaults) and the MV-9 sandbox flags are real gaps. --- ## Section 4 — High-Risk Untested Paths ### Untested: BIP39 word-list integrity + checksum rejection **File:** src/shared/recovery-word-list.ts, recovery-words.ts:110-113 **Risk:** Recovery is the disaster-recovery path. No test verifies the 2048-word list is unique and byte-identical to the canonical BIP39 English list, and no test rejects a valid-words/invalid-checksum phrase. A single transcription error or a checksum-bypass would make some phrases silently unrecoverable — discovered only by a locked-out user. **Test:** (a) assert list length 2048, all unique, all 3–8 ASCII letters, SHA-256 of the joined list == known BIP39 digest; (b) take a valid 12-word phrase, swap the last word for another valid word, assert recovery fails on checksum. **Priority:** High ### Untested: 128-bit prefix-KEK recovery-escrow construction (PCH-01-005) **File:** src/shared/recovery-words.ts:118-154 **Risk:** Recovery words encode only the first 16 bytes of the 32-byte root key (bytes 16–31 zero-padded). If that invariant breaks, either recovery silently fails or the security claim (128-bit) is wrong. No test asserts the prefix/zero-pad behavior or the HKDF salt/info usage. **Test:** two root keys identical in bytes 0–15 but differing in 16–31 produce identical recovery words; HKDF output changes when salt changes. **Priority:** High ### Untested: TOTP against RFC 6238 vectors **File:** src/crypto/totp.ts **Risk:** No independent verification of generated codes for SHA1/SHA256/SHA512. A library regression ships a wrong 2FA code with no test failing — a hard, visible breakage for every 2FA login. **Test:** RFC 6238 Appendix B vectors (secret `12345678901234567890`, known timestamps) for all three algorithms; verify period-boundary roll. **Priority:** High ### Untested: peachPayload declared-length decompression bomb (MV-4) **File:** src/crypto/peachPayload.ts (pre-`decompressZstd` declared-length check) **Risk:** Only the zstd *output* cap is tested (`decompression-bomb.test.ts`). The declared-length-before-inflate guard in the payload parser — the first line of MV-4 defense on the Codex/import path — has no regression test and could be removed unnoticed. **Test:** payload claiming length > MAX_DECLARED_LENGTH throws before any decompression; sum-of-sections > MAX_DECOMPRESSED_BYTES rejected. **Priority:** High ### Untested: backup/import NFC raw-fallback decrypt (LV-1 / PCH-01-001 sibling) **File:** src/shared/backup.ts:96-111, src/shared/import.ts:96-111 **Risk:** This is the canonical normalize-with-raw-retry pattern PCH-01-001 cites. The `raw:true` fallback that opens pre-LV-1 non-NFC backups is never exercised with a genuinely non-NFC password. If the fallback is broken, users with non-ASCII passwords are permanently locked out of old backups. **Test:** seal a backup with raw (un-normalized) non-NFC password bytes → import → succeeds via raw fallback; NFC password still opens normally. **Priority:** High ### Untested: Ed25519 passkey round-trip **File:** src/background/passkey-crypto.ts:164-238 **Risk:** Ed25519 (-8) credentials are supported but only ES256 (-7) is tested. A broken PKCS8 import/export or COSE encoding for Ed25519 would let registration succeed and authentication silently fail. **Test:** generate -8 keypair → sign assertion → verify; export/re-import private key. **Priority:** Medium ### Untested: license signature verification **File:** src/background/license.ts:94-99 **Risk:** Ed25519 license verification has no direct test. A defeated/short-circuited check = forged Pro licenses, with nothing failing. **Test:** valid signature accepts; tampered payload / wrong signature / wrong prefix / bad length all reject. **Priority:** Medium ### Untested: PCH-01-018 subnet-probe identity leak (end-to-end) **File:** src/shared/chirp-sync-transport.ts:132-140 (verifyRemoteStatic) + noise-xx **Risk:** The pin check is tested in isolation, but no integration test exercises a subnet scan finding a wrong-pubkey host and confirms the local long-term static is **not** sent before msg3. This is exactly the threat PCH-01-018 fixed. **Test:** simulate `openChirpSyncSession` against a responder whose static ≠ pinned → assert abort before msg3, no static transmitted. **Priority:** Medium ### Untested: PCH-01-016 batch-add canary/tag sanitization (MV-6 on import/batch) **File:** src/background/messages.ts (BATCH_ADD_ENTRIES → sanitizeEntryDraft) **Risk:** MV-6 whitelist is tested on the restore path (vault.test.ts) but not on `BATCH_ADD_ENTRIES`/import, which is the actual attacker-reachable bulk surface. A non-HTTPS canary webhook or unbounded tags could slip in via import. **Test:** batch-add with a non-HTTPS canary webhook + oversized tag array → assert sanitized/rejected. **Priority:** Medium ### Untested: MV-9 pdf.js sandbox flags **File:** src/popup/codex/pdf-import.ts:259 **Risk:** No test asserts `getDocument` is called with `isEvalSupported:false`/`disableFontFace/Range/Stream`. A refactor could silently re-enable eval in the PDF parser (untrusted-PDF attack surface). **Test:** spy on `getDocument`, assert the hardened options object. **Priority:** Medium --- ## Section 5 — The Autofill Injection Layer **Headline:** The autofill *logic helpers* (`detect`, `fill`, `policy`, `interaction`, `identity-helper`) are well unit-tested (≈90 tests). The two largest and most behaviorally complex files — **`inject.ts` (4,405 lines) and `index.ts` (1,263 lines)** — are almost entirely untested at the unit level. `inject.ts` has exactly **2 tests** (`inject.keyguard.test.ts`, PCH-01-007 isTrusted). `index.ts` has **no dedicated test file at all**. The e2e `autofill-deep.spec.ts` (32 tests) exercises icon injection but against `page.setContent()` synthetic HTML, not real pages, and does not drive the orchestration state machine in `index.ts`. Behavior-by-behavior: | Behavior (source) | Coverage | Notes | |---|---|---| | Field detection — login/signup/email-first/TOTP (detect.ts) | **Strong (unit)** | detect.test.ts + content.adversarial.test.ts (hidden/disabled/honeypot/0x0/types) | | Card form detection + Luhn + network (detect/fill) | **Strong (unit)** | content.adversarial + detect.test.ts (Stripe iframe, autocomplete, name/id heuristics) | | Domain matching (extractDomain) | **Strong (unit)** | content.adversarial — `github.com.evil.com`→`evil.com`, www/sub/case/co.uk, ~30 cases | | `fillFields` cross-origin-safe + React setNativeValue | **Strong (unit)** | fill.test.ts — refuses cross-origin form action, dispatches input/change | | CSS.escape on field ids (PCH-01-025) | **Strong (unit)** | detect.test.ts — special chars + quotes don't throw | | isTrusted keydown gating (PCH-01-007) | **Strong (unit)** | inject.keyguard.test.ts — untrusted Arrow/Enter/printable no-op | | `resolveAutofillInteractionTargets` shouldOpen logic | **Adequate (unit)** | interaction.test.ts — 4 cases (passive gate, detected fields, explicit email/password click) | | Password-policy extraction (policy.ts) | **Strong (unit)** | policy.test.ts — maxlength/pattern/nearby-text/forbidden-chars/no-leak | | Firefox passkey relay allowlist | **Strong (unit)** | passkeys-firefox-relay.test.ts — allowed/blocked types, cross-frame block, null payload | | **`handleFieldInteraction` orchestration (index.ts)** | **None (unit)** | The central state machine — ML-predict timeout race, field-type resolution, locked-vault prompt, signup-mode branch — untested except indirectly by e2e | | **3-layer dismissal model (index.ts: lastDismissedField, DISMISS_LOCK_MS, fill-success lock)** | **None** | Complex, regression-prone (the comments cite v1.4.14/§4.7/§4.12 prior regressions). Zero direct tests | | **`wasAlreadyFilled` / `recordFillSuccess` form+page-grace logic** | **None** | The "don't re-pop after fill" guarantee has no unit test | | **`initialScan` aggression levels (0/1/2) + manualSummonDomains recall** | **None** | The aggressive password-field injection on recall domains is untested | | **MutationObserver re-attach + 200ms debounce** | **None** | The "do not lower" debounce regression guard has no test | | **SUMMON_AUTOFILL routing (shortcut / passkey-unlock / search-palette)** | **None** | Message-handler branches untested | | **VAULT_UNLOCKED re-trigger vs passkeySummonActive** | **None** | The "no double-trigger race" claim is asserted only by comment | | **showAutofillSelector / showTOTPSelector / showCardSelector rendering + keyboard nav (inject.ts)** | **None** | 4,405 lines of inline-UI rendering, the lazy MV-2 search load, the smart-generator drawer — untested beyond the 2 keyguard tests | | **showRememberSiteToast, passkey toasts/offer/available (inject.ts)** | **None** | Untested | | TOTP digit-box fill + email-first flow + signup confirm-password fill | **None (unit)** | Logic lives in index.ts/inject.ts; not unit-covered | | **MV-1/MV-2 enforcement at the content boundary** | **Partial** | Enforced & tested in `messages.ts` (background side). The content-side caller (explicitSummon flags, lazy `loadSearchableEntries` thunk) is untested | **E2E reality (autofill-deep.spec.ts):** injects icons onto synthetic forms and checks locators/matching/security guards. It does **not**: submit forms and verify post-submit suppression; test email-first/two-step (Google/Microsoft) flows; test the inline chip dismissal model; test cross-origin iframe fill (the iframe test is synthetic and unverified); test against any real login page. As a result the content-script orchestration is effectively **uncovered** — neither unit nor realistic-integration. **Conclusion:** This is the highest-complexity, lowest-coverage area in the codebase. The pure functions are safe; the stateful glue that decides *when* to open/suppress/fill — historically the source of multiple regressions — is the single largest test gap. --- ## Section 6 — PCH / Audit Fix Coverage Source reports (`peach-browser-security-audit.md`, `peach-browser-technical-report.md`) were draft-only and are no longer on disk; findings reconstructed from `LOG.md` and the two fix briefs in `~/Downloads`. ### First-round audit (Fable, v3.7.x) | Finding | Fix | Regression test? | Quality | |---|---|---|---| | HV-1 | clampSyncTimestamp + sync alerts | **Yes** | Strong — `vault.sync-clamp.test.ts` clamp + REMOTE_PASSWORD_CHANGE + REMOTE_DELETE | | HV-2 | clipboard via chrome.alarms + offscreen | **Yes** | Strong — `clipboard.test.ts` arm/route/fallback | | MV-1 | GET_ENTRY_FOR_FILL origin re-validation | **Yes** | Strong — `messages.test.ts` 4 cases (match/mismatch/summon/unparseable) | | MV-2 | includeAll behind userGesture | **Yes** | Strong — `messages.test.ts` strip-without-gesture + full-with-gesture | | MV-3 | sync frame/count/size bounds | **Yes** | Strong — `chirp-sync-vault-adapter.test.ts` all bounds | | MV-4 | decompression bomb caps | **Partial** | zstd output cap tested; **declared-length cap in peachPayload NOT tested** | | MV-5 | import file-size limits | **Yes** | Strong — import/backup/pdf-import tests | | MV-6 | import whitelist (sanitizeRestoredEntry) | **Partial** | Restore path tested; **batch-add/import path (PCH-01-016) NOT directly tested** | | MV-7 | vouched-device confirmation | **Yes** | Strong — adapter queue + sync-trigger filter | | MV-8 | rotateSyncIdentity + key-rotation notify | **Partial** | DEVICE_KEY_ROTATED tested; **`rotateSyncIdentity` itself NOT directly tested** | | MV-9 | pdf.js isEvalSupported:false | **No** | **No test asserts the hardened getDocument options** | | LV-1 | NFC normalize master password | **Yes** | Strong — `argon2.test.ts` parity + `vault.test.ts` heal | | LV-2 | homograph warning | **Partial** | `detectHomograph` tested; **UI surfacing untested** | | LV-3 | canary rate-limit | **Yes** | Strong — `messages.test.ts` 3 cases | | LV-4 | GET_RECOVERY_WORDS re-auth | **Yes** | Strong — `messages.test.ts` 3 cases | | LV-5 | dedup onConnect + nonce port | **Yes** | Adequate — `entry.lock-port.test.ts` 2 cases | | LV-6 | lockfile standardization | n/a | Build config — no test (acceptable) | | LV-7 | ggwave new Function + proto-pollution | **Partial** | Proto-pollution tested (`vault.test.ts`); **ggwave new Function removal verified only by build artifact, no test** | | LV-8 | zxcvbn → @zxcvbn-ts/core | **Yes** | Strong — `zxcvbn-strength.test.ts` | | LV-9 | exact-pin crypto deps | n/a | package.json — no test (acceptable) | ### Second-round technical report (Fable, v3.8.1 → PCH-01-001..026) | Finding | Regression test? | Quality | |---|---|---| | PCH-01-001 NFC export/restore + raw fallback | **Yes** | Strong — 5 cases in `peachPayload.test.ts`. (Gap: backup.ts/import.ts sibling raw-fallback untested — §4) | | PCH-01-002 Noise BLAKE2s | **Yes** | Strong — `noise-blake2s-kat.test.ts` cross-repo byte-identical | | PCH-01-003 / 003b NK handshakeHash binding | **Yes** | Strong — `noise-js.test.ts` binds + rejects-on-modify | | PCH-01-004 deviceId local + migration | **Yes** | Strong — `chirp-identity.test.ts` | | PCH-01-005 HKDF null separator | **Yes** | Strong — adapter round-trip + legacy-fails-closed | | PCH-01-006/007 GET_ENTRY_FOR_EDIT origin | **Yes** | Strong — `messages.adversarial.test.ts` 4 cases | | PCH-01-007 inject isTrusted keydown | **Yes** | Adequate — `inject.keyguard.test.ts` 2 cases | | PCH-01-008 re-escrow after changePassword | **Yes** | Adequate — `vault.session-escrow.test.ts` | | PCH-01-009 biometric rootKey rehydrate | **Yes** | Adequate — `vault.test.ts` | | PCH-01-010 deriveShardCounts cap | **Yes** | Strong — `codex.adversarial.test.ts` | | PCH-01-011 stripe-count cap | **Yes** | Strong — `codex.adversarial.test.ts` | | PCH-01-012 isInteger on i/t/s/S | **Yes** | Strong — `codex.adversarial` + `pdf-import.test.ts` | | PCH-01-013 fingerprint length | **Yes** | Strong — `codex.adversarial.test.ts` | | PCH-01-014 PDF page/dimension clamp | **Yes** | Strong — `pdf-import.clamp.test.ts` | | PCH-01-015 untrimmed passphrase | **Yes** | Strong — `CodexRestoreView.test.tsx` (untrimmed + all-whitespace) | | PCH-01-016 sanitizeCanary/Tags in batch | **No** | **No direct batch-sanitization test (MV-6 batch surface)** | | PCH-01-017 handleRestoreFromCodex validation | **Yes** | Strong — `codex-handlers.test.ts` (malformed code, NaN ts, oversize) | | PCH-01-018 destructive-op gating | **Yes** | Strong — `messages.adversarial.test.ts` 4 ops × matrix | | PCH-01-018 subnet-probe identity defer | **Partial** | Pin tested in isolation; **end-to-end probe→wrong-peer untested** | | PCH-01-019 TOTP otpauth params | **Partial** | Param threading tested; **no RFC vectors, -8/SHA512 path** | | PCH-01-020 self-test KAT | **Yes** | Strong — `self-test.test.ts` divergence | | PCH-01-021 clipboard cold-start hash | **Yes** | Adequate — `clipboard.test.ts` | | PCH-01-022/023 history entryCount bucket | **Yes** | Strong — `vault-history.test.ts` | | PCH-01-024 gateOnSAS | **Yes** | Adequate — `chirp-pair.test.ts` | | PCH-01-025 CSS.escape detect.ts | **Yes** | Strong — `detect.test.ts` | | PCH-01-026 (10 items) | **Partial** | ChunkBuffer never-throws ✓, out-of-range≠duplicate ✓, CSV single-parse ✓, SAVE_NOTE typed (implicit). **Untested: save.ts dead-observer removal, reed-solomon try/finally, generate.ts step renumber, pdf-import progress total, chirp-sync-transport WS-error handling** | **Fixes with NO or only weak regression test (revert-risk):** MV-9 (none), PCH-01-016 (none), LV-7 ggwave (build-only), MV-4 declared-length (none), MV-8 rotateSyncIdentity (none), LV-2 UI surfacing (none), PCH-01-018 probe-integration (isolation only), PCH-01-019 RFC vectors (none), several PCH-01-026 sub-items. --- ## Section 7 — Error Path Coverage | Error path | Tested? | How well | |---|---|---| | Import malformed/truncated data | **Partial** | backup.ts rejects malformed JSON; **truncated CSV / mid-parse corruption untested**; import.ts returns "unknown" on bad format but graceful-failure not asserted | | Import wrong backup password | **Yes** | `backup.test.ts` + `import.test.ts` reject wrong password (note: masks non-tag crypto errors as "INVALID_EXPORT_PASSWORD") | | Vault unlock wrong password | **Yes** | `vault.test.ts` + `lock-screen.spec.ts`; **but corruption (good tag/bad schema) vs wrong-password not distinguished in test** | | Network failure in feedback/canary | **Partial** | Canary rate-limit tested; actual fetch failure / HIBP network error not e2e tested (stubbed) | | Service-worker eviction mid-operation | **Weak** | `vault-lifecycle.spec.ts` covers SW-restart→lock-screen; **eviction during create/import/clipboard-timer/autofill untested** | | Sync peer disconnect mid-transfer | **Weak** | WebSocket onerror/onclose code present but **not regression-tested** (PCH-01-026 #9) | | Codex scan with damaged/partial code | **Strong** | `codex.adversarial.test.ts` — tampered/missing/parity-only/excess-loss, fingerprint mismatch | | Autofill on page with no password fields | **Partial** | index.ts has the path (search-palette / email-first); **not unit-tested**; e2e covers some no-form cases | | Entry with all optional fields empty | **Partial** | `peachPayload.test.ts` "handles missing optional fields" round-trips; backup.ts empty-password contradiction (defaults `""` then validator rejects) untested | **Pattern:** Error *inputs* (malformed crypto/codex/sync payloads) are well covered by the adversarial suites. Error *conditions* (eviction, disconnect, network failure, partial writes) are weakly covered because they require infrastructure (SW termination, two-device sync, network mocking) the suite doesn't have. --- ## Section 8 — Integration vs Unit Balance - **Unit:** ~1,062 tests, the bulk of the suite. Crypto, sync, message-router, codex, detect/fill — all strongly unit-tested. This is where the suite is strongest. - **E2E (Playwright):** ~570 tests against the real built extension in Chromium. Covers vault lifecycle, entry CRUD, settings persistence, keyboard, lock screen, clipboard, generator, search well. This is genuine integration for the **popup**. - **Behavioral/contract:** `contracts.test.tsx` (21) is the only component-contract layer; thin relative to the ~12k LOC of popup views. **Assessment:** For a password manager, the interesting bugs are in component *interactions*, and the suite has two systemic integration gaps: 1. **The content-script ↔ page boundary** is not integration-tested against real pages. `autofill-deep.spec.ts` uses synthetic `setContent` HTML and never drives `index.ts`'s state machine or a real form submit. The single highest-value integration test — *create vault → visit a real login page → autofill → submit → verify suppression* — does not exist. 2. **Cross-device sync** (chirp pairing, Noise handshake, manifest exchange) is exhaustively **unit**-tested but has **zero** end-to-end coverage (it needs two devices + mDNS). The Noise handshakes are tested initiator↔responder in-memory, which is the best achievable without hardware, but the full pairing→sync→merge flow across two real instances is unverified. The suite is **not** "mostly unit tests" in volume terms (e2e is a third of it), but the e2e coverage is concentrated on the popup and thin exactly where integration matters most (autofill on real pages, sync across devices). It skews happy-path: lots of "does X work" and few "does X fail gracefully / does X suppress correctly after Y." --- ## Section 9 — Prioritized Gap List ### Priority 1 — Must Have Before Audit **T-001: BIP39 word-list integrity check** File: src/shared/recovery-word-list.ts — assert length 2048, all unique, all 3–8 ASCII letters, SHA-256(joined) == canonical BIP39 English digest. Why P1: silent transcription error = unrecoverable vaults; no test guards it. **T-002: BIP39 checksum rejection** File: src/shared/recovery-words.ts:110-113 — valid 12 words with a deliberately wrong checksum (swap last word) must fail recovery. Why P1: PCH-01-005 escrow integrity; current "wrong words" test uses all-valid words with unasserted checksum. **T-003: 128-bit prefix-KEK invariant (PCH-01-005)** File: src/shared/recovery-words.ts:118-154 — two root keys equal in bytes 0–15, differing in 16–31, yield identical recovery words; bytes 16–31 zero-padded on reconstruct. Why P1: silent escrow regression / wrong security claim. **T-004: TOTP RFC 6238 known-answer vectors (PCH-01-019)** File: src/crypto/totp.ts — RFC 6238 App. B vectors for SHA1/SHA256/SHA512 at fixed timestamps; period-boundary roll. Why P1: a wrong 2FA code is a hard user failure with no current guard. **T-005: peachPayload declared-length bomb (MV-4)** File: src/crypto/peachPayload.ts — payload declaring length > MAX_DECLARED_LENGTH throws before decompress; section-sum > MAX_DECOMPRESSED_BYTES rejected. Why P1: first-line MV-4 defense on import/codex, no regression test. **T-006: backup/import NFC raw-fallback decrypt (LV-1/PCH-01-001 sibling)** File: src/shared/backup.ts:96-111, import.ts:96-111 — seal with raw non-NFC password → import succeeds via raw fallback; NFC password still opens. Why P1: canonical pattern, data-loss risk on disaster path, never exercised. **T-007: MV-9 pdf.js sandbox flags** File: src/popup/codex/pdf-import.ts:259 — spy on getDocument, assert isEvalSupported:false + disableFontFace/Range/Stream. Why P1: untrusted-PDF eval surface, zero test. **T-008: PCH-01-016 batch-add canary/tag sanitization** File: src/background/messages.ts BATCH_ADD_ENTRIES — non-HTTPS canary webhook + oversized tags via batch/import → sanitized or rejected. Why P1: MV-6's actually-attacker-reachable surface, untested. **T-009: license Ed25519 signature verification** File: src/background/license.ts:94-99 — valid accepts; tampered payload / wrong sig / wrong prefix / bad length reject. Why P1: defeated check = forged Pro, no test. **T-010: PCH-01-018 subnet-probe identity-leak integration** File: src/shared/chirp-sync-transport.ts:132-140 — probe finds wrong-pubkey host → abort before msg3, no local static transmitted. Why P1: the exact threat PCH-01-018 fixed, only isolation-tested. ### Priority 2 — High Value **T-011: index.ts dismissal model** — unit-test the 3 layers (lastDismissedField per-field lock; DISMISS_LOCK_MS global lock; fill-success `wasAlreadyFilled` form+page-grace). Drive focusin/Escape/click-outside sequences; assert no reopen after fill, no reopen on rapid cross-field focus. (Refactor index.ts to export the predicate functions.) Why P2: historically regression-prone (v1.4.14, §4.12), zero coverage. **T-012: index.ts handleFieldInteraction orchestration** — ML-predict 150ms timeout race, locked-vault prompt, signup-mode (no domain entries → identities + confirm-password), email-first (no pwField) path. Why P2: central autofill decision logic, untested. **T-013: index.ts initialScan aggression levels** — level 0 (no form → skip), level 1, level 2 final-pass injecting bare password fields; manualSummonDomains recall mapping. Why P2: aggressive injection on recall domains untested. **T-014: SUMMON_AUTOFILL / VAULT_UNLOCKED message routing (index.ts)** — shortcut vs passkey-unlock vs search-palette branch; VAULT_UNLOCKED re-trigger vs passkeySummonActive (no double-trigger). Why P2: message-handler branches untested. **T-015: Ed25519 (-8) passkey round-trip** — generate → sign → verify → export/re-import. Why P2: supported algorithm, only ES256 tested. **T-016: MV-8 rotateSyncIdentity** — rotate generates new root/syncMasterKey, clears paired devices, old key can't decrypt. Why P2: lost/stolen-device revocation, function untested. **T-017: Codex multi-stripe restore** — generate a >200-shard (2-stripe) codex, restore, assert vault matches. Why P2: large-vault restore path, only single-stripe tested. **T-018: vault corruption vs wrong-password discrimination** — unlock with good AES-GCM tag but bad schema → consumeLastUnlockFailureReason signals corruption (M7), distinct from wrong-password. Why P2: user-facing error correctness. **T-019: Argon2id KAT + parameter assertion** — pin a known (password, salt) → expected key with m/t/p; assert cost params can't silently downgrade. Why P2: KDF strength unverified. **T-020: WebSocket sync-disconnect handling (PCH-01-026 #9)** — onerror/onclose mid-handshake → session aborts cleanly, parked receive() rejects, no hang. Why P2: peer-disconnect resilience, no test. **T-021: CSV formula-injection on import** — import CSV with `=cmd`/`@`/`+`/`-` prefixed fields → values sanitized or flagged (mirror Export.csv coverage on the import side). Why P2: known injection class, import side uncovered. **T-022: GET_ENTRY_FOR_EDIT via sender.url + locked-vault includeAll** — EDIT origin check through `sender.url` (not just tab); AUTOFILL_QUERY includeAll on a locked vault returns vault_locked before processing. Why P2: untested branches of tested fixes. **T-023: vault.consolidatePasskeyEntries** — after passkey create, same-domain TOTP/login entries merge with correct winner (lastUsed ranking). Why P2: called on every registration, untested directly. **T-024: vault.purgeExpiredTrash retention cutoff** — entries older than retention purged, within-window kept, boundary exact. Why P2: data-deletion correctness. ### Priority 3 — Good To Have **T-025: generate.ts empty-pool guard** — all chars forbidden → no infinite loop / defined error; 128-char boundary; minLength>maxLength policy. **T-026: timestamp-clamp + manifest-freshness boundaries** — now+SKEW±1ms; issuedAt at exact freshness edge. **T-027: full Codex UI round-trip e2e** — generate → download PDF → re-upload → restore → vault matches (replace hard-coded fixture path with a generated one). **T-028: autofill real-page e2e** — host a fixture login page (with form submit), assert icon→fill→submit→post-submit suppression; email-first two-step; cross-origin iframe. **T-029: inject.ts selector keyboard nav** — Arrow/Enter selection, search box (MV-2 lazy load fires GET on gesture), Escape dismiss — beyond the isTrusted guard. **T-030: ml.ts predictBatch** — empty/single/large batches, softmax stability with extreme weights. **T-031: SW eviction mid-operation** — terminate SW during import/clipboard-timer, assert recovery. **T-032: backup.ts empty-password contradiction** — login entry with no password: define and assert behavior (currently defaults `""` then validator rejects). **T-033: Firefox passkey e2e** — un-skip `passkeys-firefox.spec.ts` in CI or document the manual gate. **T-034: LV-2 homograph UI surfacing** — non-ASCII URL saved → warning rendered in entry edit. **T-035: HIBP breach-check integration** — mock endpoint, verify k-anonymity (5-char SHA-1 prefix sent, suffix matched). **T-036: reed-solomon try/finally free (PCH-01-026 #8)** — WASM instance freed on reconstruct trap. **T-037: clipboard double-failure / alreadyWritten path** — both clipboard APIs fail; alreadyWritten=true skips write. **T-038: favicon canvas fallbacks** — titleInitials/titleBackground determinism, tainted-canvas error handling. **T-039: chirp pairing failure paths** — playback failure, mic discontinuity, replay of captured chirp (nonce defeats it). **T-040: sync alert queue bounds** — sync-alerts MAX_PENDING boundary, malformed-alert filtering. --- ## Section 10 — Summary **The numbers:** 109 test files, ~1,632 declared tests (~3,200 executed after parameterization) against ~44k LOC. By volume this is a well-tested codebase — roughly 1 declared test per 27 lines, with the security-critical primitives (crypto round-trips, Noise handshakes, codex erasure, message-router origin checks, recovery word cycles) genuinely well covered, and 24 of 26 PCH findings plus most audit findings carrying real regression tests. **The biggest coverage gap, in one sentence:** the 5,668-line content-script autofill orchestration (`index.ts` + `inject.ts`) — the most regression-prone code in the project — has essentially no unit coverage and only synthetic, state-machine-bypassing e2e coverage, so the logic deciding *when* to open, suppress, and fill is untested at exactly the layer where past regressions happened. **The one test that would buy the most confidence:** a real-page autofill e2e (T-028) — load an actual login form, autofill, submit, and assert the popup is suppressed afterward — because it exercises `index.ts`'s entire decision/dismissal/fill-success machine end-to-end in one go, the single largest untested surface. **Runner-up by risk-density:** the recovery-path trio (T-001/T-002/T-003) — BIP39 list integrity, checksum rejection, and the 128-bit prefix-KEK invariant — because that is where a silent, *unrecoverable* data-loss bug would hide with no current test to catch it. **Readiness for a fresh external review:** Good on cryptography, sync protocol, codex, and the background message boundary — a reviewer will find these defensible and well-pinned. They will, correctly, flag (1) the autofill content-script layer as under-tested relative to its complexity and trust position, (2) the recovery/BIP39 integrity gaps on the disaster-recovery path, (3) TOTP lacking RFC vectors, and (4) a handful of fixes (MV-9, PCH-01-016, MV-4 declared-length, license verification) that are implemented but have no regression test and could be reverted unnoticed. Closing Priority 1 (T-001–T-010) before review would neutralize the most likely "this critical path has no test" findings; Priority 2 would close the autofill-orchestration gap that a thorough reviewer will otherwise raise.