Test Strategy
This document captures the current state of automated testing across the Laika CMS monorepo and the plan to close the gaps. It is intentionally prescriptive: it names which packages to start with, why, and in what order.
Current state
Vitest 4.x is wired into the workspace catalog and Turbo has a test task. Tests now exist for several modules under packages/laikacms/src/shared/crypto/, packages/laikacms/src/shared/file-sanitizer/, packages/laikacms/src/impl/contentbase-settings-default/, and packages/laikacms/src/impl/documents-contentbase/. The @laikacms/aws DynamoDB settings tests (added in LCMS-221) moved out with that package. Coverage across the rest of the codebase is still incomplete — see "Where to invest first" below for the remaining priorities.
Note: this document predates two reshapes — the package consolidation that merged the original 39 packages into
laikacms,@laikacms/aws,@laikacms/decap, and@laikacms/github, and the June 2026 restructure that moved the adapters (including@laikacms/aws) and the example apps out of this repo. References to module paths (e.g.packages/shared/crypto) have been updated where they are wrong; the high-level test-rollout guidance is still applicable to the three remaining packages.
| Metric | Value (snapshot, pre-consolidation) |
|---|---|
Source files (.ts/.tsx, excluding .d.ts) | ~241 |
Test files (*.test.*, *.spec.*, __tests__/) | 0 |
Packages with a test script | 5 |
| Packages with actual tests | 0 |
| Coverage tooling configured | None |
(These numbers reflect the pre-consolidation state. Today the repo has 4 publishable packages plus one private dev-tools package; tests have been added in several locations — re-run pnpm coverage for the current figures.)
Where to invest first
The priorities below are ordered by risk reduction per hour of work.
1. Security-critical primitives
These modules are where bugs become CVEs.
packages/laikacms/src/shared/crypto(constant-time.ts,hash.ts,password.ts,random.ts,timing.ts). Use known-answer test vectors from RFCs/NIST, property-test that constant-time comparisons do not short-circuit, fuzz password hashing parameters, and assert non-determinism plus entropy onrandom.packages/decap/src/decap-oauth2(27 source files includingpasskey/,totp/, OAuth2 flow). Cover PKCE state/nonce handling, TOTP RFC 6238 vectors, replay-attack rejection, passkey challenge verification, and redirect-URI allowlisting.packages/laikacms/src/shared/file-sanitizer(16 files: jpeg/gif/webp/png sanitizers). Feed real EXIF-laden samples and assert metadata is stripped. Add malformed-input/fuzz tests so a crafted file cannot crash the parser.
2. Domain contracts
Domain packages define the interfaces every implementation depends on. Drift here breaks everything downstream.
packages/laikacms/src/domain/storage— repository, provider, format, serializer roundtrips, cache invalidation rules.packages/laikacms/src/domain/documents— revision lifecycle (revision.ts,revision-create.ts,revision-summary.ts) and repository CRUD invariants.packages/laikacms/src/domain/assets— asset URL generation and create/update flows.
The pattern: each domain package should export a shared conformance test suite that any implementation can re-run against itself. See section 3.
3. Storage adapter conformance
storage-fs and storage-r2 both implement the same abstract StorageRepository. They should share one parametrized suite (read/write/list/delete/concurrent-write semantics) executed once with the FS adapter and once against R2 via miniflare or a mocked binding. Same approach for assets-r2 and the documents-drizzle/storage-drizzle adapters.
4. Serializers
storage-serializers-{json,markdown,yaml,raw} are 1-file packages. Roundtrip tests (serialize ∘ deserialize === id) plus a few golden-file fixtures cost roughly 30 minutes per package and lock in behavior permanently.
5. JSON:API surface
packages/laikacms/src/shared/json-api, the four *-api subpaths, and the three *-jsonapi-proxy impls all share the JSON:API wire format. A single fixture-driven contract test, run against both proxies and servers, is enough to catch most regressions.
6. Don't add a test script until there are real tests
Several packages used to declare "test": "vitest run" with no test files, so CI passed vacuously. The fix is not "add a smoke test" — structural smoke tests (asserting a class is a function, that methods exist on the prototype, etc.) test TypeScript rather than behavior, never catch real regressions, and just get in the way of refactors. The fix is to remove the test script until there are tests worth running.
Repo-wide gaps
- Coverage reporting.
@vitest/coverage-v8is wired up at the workspace level. Runpnpm coveragefrom the repo root to produce per-package text-summary, lcov, and html reports under each package'scoverage/directory. Per-package threshold enforcement is not yet on — that's the next coverage step once the test suites are dense enough. - No enforcement that packages opt into tests. Only 5 of 39 packages declare a
testscript.scripts/validate-packages.tsis the natural place to require one. - No shared test utilities package. Worth adding
packages/laikacms/src/shared/testingfor fixtures, in-memory mocks, and the domain conformance suites mentioned above. - No integration test layer. Once unit tests exist, add a small e2e harness that boots a
*-apiserver against an in-memory impl and exercises real HTTP.
Rollout order
Wire up✓@vitest/coverage-v8and a shared baselinevitest.config.tstemplate.Cover✓shared/cryptoandshared/file-sanitizer.- Add domain conformance suites in
domain/storage,domain/documents,domain/assets. Run those suites againststorage-fs,storage-r2,assets-r2. (FS and R2 covered as datasource integration tests, not yet via shared conformance suite.)Roundtrip tests for all four serializers.✓- OAuth2 flow tests in
decap-oauth2(TOTP done; PKCE / passkey / handler flow still pending). - JSON:API contract tests across
*-apiand*-jsonapi-proxy.