Testing Strategy & Module Decomposition Plan¶
This guide describes how CodeXomics is tested today, the direction the test suite should evolve, and a concrete plan for breaking up the largest modules.
Current state¶
- 771+ unit tests run under Vitest (
npm test). - A large fraction of the legacy suite asserts against source text —
fs.readFileSync(module)followed byexpect(source).toContain('...'). These "source-grep" tests verify that code looks a certain way, not that it behaves correctly. They break on harmless refactors (see theconfig-ipc-hardeningregression) and contribute almost nothing to executed coverage (~4%). - There were no end-to-end tests exercising the actual Electron app.
Target testing pyramid¶
- Behavioral unit tests — import the module and assert on its return values / side effects. New code must ship with these. Examples added recently:
test/unit/secret-store.test.js(encrypt/decrypt round-trips, migration, fail-soft, recursion).test/unit/content-security-policy.test.js(CSP invariants).- Integration tests — exercise a handler or service with its real collaborators (mock only the OS/Electron boundary).
- E2E smoke tests — launch the real app with Playwright and assert the UI boots (
test/e2e/smoke.spec.js,npm run test:e2e).
Testing main-process modules¶
require('electron') returns a path string outside an Electron runtime, so main modules that touch Electron APIs are awkward to import in Vitest. Two patterns:
- Test seam — resolve the Electron dependency through a small injectable accessor with a
__setXForTesting()export (seesrc/main/secret-store.js). - Mock module —
vi.mock('electron', () => ({ ... }))with avi.hoistedstate object; reserve for modules that capture the API lazily.
Prefer extracting pure logic into a dependency-free helper that can be tested directly, leaving a thin Electron-bound shell.
Coverage gate¶
vitest.config.mjs enforces a baseline coverage floor (currently 4%). This only guards against regression. Ratchet the thresholds upward as source-grep tests are replaced with behavioral tests — aim for 30%+ on actively developed modules.
Migrating a source-grep test¶
- Identify the behavior the
toContainassertions were proxying for. - Import the module (use a seam/mock for Electron).
- Assert on inputs → outputs / state changes.
- Delete the string assertions.
Viewport filtering baseline¶
Run node scripts/benchmark-genome-viewport.cjs after installing the repository dependencies. On Linux, launch it with xvfb-run -a. The script starts the Electron genome view with a temporary profile and emits JSON to standard output.
The fixture measures the first query and 40 subsequent queries for each of 10,000, 100,000 and 1,000,000 synthetic features. It exercises both linear viewports and viewports spanning a circular origin. The JSON includes p50, p95, the number of returned features, the runtime, and Electron's current and process-lifetime peak working sets where the platform exposes them.
Compare runs on the same machine and runtime. These timings cover feature filtering and are separate from file import, data IPC, layout and painting. The first-query measurement starts after fixture generation and renderer startup.
Module decomposition plan¶
Several modules have grown well past a maintainable size and concentrate change risk. They should be split along the service boundaries that already exist (renderer/modules/chat/services/, renderer/modules/tracks/, renderer/modules/Agents/).
| Module | Lines | Suggested split |
|---|---|---|
renderer/modules/ChatManager.js | ~20.7k | message orchestration vs. tool-call execution vs. rendering vs. sequence utils |
renderer/modules/TrackRenderer.js | ~16k | per-track-type renderers (genes, reads, variants, GC, WIG) behind a registry |
renderer/renderer-modular.js | ~11k | bootstrap/wiring only; move feature logic into modules |
renderer/modules/ActionManager.js | ~7.7k | group actions by domain (navigation, edit, file, analysis) |
main/ipc-handlers.js | ~4.1k | split per domain (config, files, blast, tools) like project-ipc.js already is |
main/menu-builder.js | ~92 KB | one builder module per window/menu |
Progress¶
- ChatManager →
ConversationTranscriptService(done). Everything about the transcript the model sees — replaying an executed tool round in the provider's native protocol, the structured execution ledger the policy layer reads, and the context budget — moved torenderer/modules/chat/services/ConversationTranscriptService.js. ChatManager keeps thin delegating methods, so call sites and the policy layer are unchanged. Behavioural tests landed before the move (tool-protocol-round-trip,conversation-token-budget), andtest/helpers/agent-loop-harness.jsdrives the realsendToLLM()loop against a scripted provider so further extractions have a net under them.
Note the file did not shrink much: the extraction removed ~215 lines while the same change set added more. The value is the seam and the tests, not the line count — and the next extraction is now cheaper.
Approach (per module)¶
- Pick one cohesive responsibility (e.g. ChatManager's sequence-search helpers).
- Extract it into a new module under the matching
services/directory with a focused public API. - Add behavioral tests for the extracted unit before wiring it back in.
- Replace the original code with a delegating call.
- Keep each extraction a small, independently reviewable PR — never a big-bang rewrite. Verify with
npm testandnpm run test:e2eafter each step.
Guardrails¶
- Do not change behavior during an extraction; refactor and feature work are separate commits.
- Because the renderer cannot be fully unit-tested, run the Playwright smoke test after structural changes.