Testing
The gate
Section titled “The gate”pnpm verify # typecheck + lint + tests + buildNothing ships without it. The test suite is hermetic: no network, no API keys, no database. Every adapter has a fake, so the whole thing runs offline in well under a minute.
pnpm test # oncepnpm test:watch # while workingLayout
Section titled “Layout”| Kind | What it covers | Style |
|---|---|---|
| Domain unit tests | Every decision rule, exhaustively | Plain values in, assertions out, no mocks |
| Loop tests | The processing loop through injected fakes | Assemble deps, run, assert outcome and stored state |
| Adapter tests | One per adapter, with an injected transport and a fixture | No network |
| API tests | Routes through in-process injection, including auth and ownership | No listening socket |
| Integration test | One test against a real database | Excluded from the gate; self-skips |
MONGO_TEST_URI="mongodb://localhost:27017" pnpm test:integrationWhat a change must cover
Section titled “What a change must cover”| Change | Tests it needs |
|---|---|
| A decision rule | Direct unit tests and a loop test proving the effect end to end |
| A collector | Happy path, malformed item, transport failure, trust resolution |
| A notifier | Formatting per tier, destination override, throwing on failure |
| A route | Success, validation failure, unauthenticated, wrong owner |
| Anything persisted | A round-trip test through the real repository adapter |
Writing a loop test
Section titled “Writing a loop test”Assemble the dependencies from fakes, run, and assert both the returned outcome and what was persisted:
const repository = new InMemoryRepository([watch])const notifier = new InMemoryNotifier('telegram')
const result = await runWatch(watch, { collectors: { rss: new FakeCollector({ [FEED]: [item] }) }, evaluator: new FakeEvaluator(verdict), notifiers: { telegram: notifier }, repository,})
expect(result.outcome).toBe('notified')expect(notifier.sent).toHaveLength(1)expect((await repository.loadState(watch.id)).notifiedFingerprints).toHaveLength(1)Asserting only the return value is half a test: most regressions in this system are state that was or was not written.
Determinism
Section titled “Determinism”- Inject the clock. Domain functions take time as an argument; adapters accept a
nowoption. - Never depend on wall-clock ordering, and never sleep.
- Fixtures live beside the tests; a test that needs a page fetches it from a fixture, never from the internet.
Test names
Section titled “Test names”Describe the behaviour and, where relevant, the reason:
it('caps an unrated source at the weak tier')it('redelivers an undelivered alert on the NEXT run through the Mongo repository')A failing test should tell you what broke without opening the file.
Accuracy is measured separately
Section titled “Accuracy is measured separately”Unit tests prove the machinery. Whether the decisions are good is measured by the golden-set harness — see The evaluation harness.