Skip to content

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.

MetricValue (snapshot, pre-consolidation)
Source files (.ts/.tsx, excluding .d.ts)~241
Test files (*.test.*, *.spec.*, __tests__/)0
Packages with a test script5
Packages with actual tests0
Coverage tooling configuredNone

(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 on random.
  • packages/decap/src/decap-oauth2 (27 source files including passkey/, 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-v8 is wired up at the workspace level. Run pnpm coverage from the repo root to produce per-package text-summary, lcov, and html reports under each package's coverage/ 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 test script. scripts/validate-packages.ts is the natural place to require one.
  • No shared test utilities package. Worth adding packages/laikacms/src/shared/testing for 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 *-api server against an in-memory impl and exercises real HTTP.

Rollout order

  1. Wire up @vitest/coverage-v8 and a shared baseline vitest.config.ts template.
  2. Cover shared/crypto and shared/file-sanitizer.
  3. Add domain conformance suites in domain/storage, domain/documents, domain/assets.
  4. Run those suites against storage-fs, storage-r2, assets-r2. (FS and R2 covered as datasource integration tests, not yet via shared conformance suite.)
  5. Roundtrip tests for all four serializers.
  6. OAuth2 flow tests in decap-oauth2 (TOTP done; PKCE / passkey / handler flow still pending).
  7. JSON:API contract tests across *-api and *-jsonapi-proxy.

Released under the MIT License.