# `runBackendConformance` _(no submodules)_ ## Purpose `runBackendConformance` is the shared, runner-neutral conformance harness every concrete `@suaveplan/i18n-backend-*` package runs against its `Backend` implementation. It exists so the universal contract — name + capabilities surface, read-of-absent → typed `I18nError`, optional `readMulti` merge, optional `save` round-trip, optional `lastModified` monotonicity — gets one canonical test body that every backend re-uses verbatim. New backends inherit the guarantees automatically; no copy-pasted test code drifting between implementations. The harness lives under the `@suaveplan/i18n-shared/testing` subpath so backend test files can import it without pulling the full i18n core or any other backend. It deliberately uses `@suaveplan/testing/runner` so it executes identically under Vitest and Bun's test runner. ## Features - One `describe(...)` per backend, one `it(...)` per universal contract clause. - Capability-gated assertions: `readMulti` / `save` / `lastModified` tests early-return for backends that do not declare the corresponding capability flag, keeping the suite green for read-only backends like HTTP. - Optional `seed` adapter on the factory return so backends with `save` can pre-populate without exposing internals; backends without `save` (HTTP-style) seed via their constructor options instead. - Runner-neutral: imports `describe` / `it` / `expect` from `@suaveplan/testing/runner`, so the same harness runs under Vitest or Bun's test runner without modification. ## Usage: Basic Example ```ts import { runBackendConformance } from "@suaveplan/i18n-shared/testing"; import { MemoryBackend } from "@suaveplan/i18n-backend-memory"; runBackendConformance("MemoryBackend", async () => { const backend = new MemoryBackend(); return { backend, async seed(locale, namespace, record) { await backend.save(locale, namespace, record as never); }, }; }); ``` ## Usage: Advanced Example A read-only backend (no `save`, no `readMulti`) supplies its own seeding via constructor options and omits `seed` from the factory return. The harness inspects `backend.capabilities` and the structural presence of optional methods to decide which assertions to run; unsupported assertions exit early. ```ts import { runBackendConformance } from "@suaveplan/i18n-shared/testing"; import { httpBackend } from "@suaveplan/i18n-backend-http"; runBackendConformance("HttpBackend (seeded fetch)", async () => { const backend = httpBackend({ loadPath: "https://i18n.example/{{locale}}/{{ns}}.json", fetch: async () => new Response(JSON.stringify({ login: { title: "Sign in" } })), }); return { backend }; }); ``` ## API Reference ### `runBackendConformance(label, makeFactory)` - `label: string` — Human-readable backend label inserted into the outer `describe(...)` title. - `makeFactory: () => Promise` — Async factory invoked once per `it(...)`; produces a fresh backend instance and optional `seed` callback. Always producing a fresh backend per call prevents cross-test state leakage. - Returns: `void`. The harness registers `describe` / `it` blocks with the active runner; assertions execute when the runner runs the file. ### `ConformanceContext` ```ts interface ConformanceContext { readonly backend: Backend; // from @suaveplan/types readonly seed?: ( locale: string, namespace: string, record: Record ) => Promise; } ``` ### `ConformanceFactory` `type ConformanceFactory = () => Promise;` ## Implementation Notes - The harness gates each capability check on **both** the structural presence of the method (`backend.save`) **and** the corresponding capability flag (`backend.capabilities.canSave`). This catches backends whose flags drift out of sync with their methods at authoring time and keeps the harness honest for partial implementations. - The harness imports the `Backend` interface from `@suaveplan/types`, never from `@suaveplan/i18n` core. Backends consume the same type from the same location, so the harness adds zero coupling to the slim core. - `lastModified` monotonicity uses a 2-ms `setTimeout` between the two `save` calls to defend against backends whose injected clock has 1-ms resolution. Backends with deterministic counters always satisfy `t2 >= t1`. - This harness is the Wave 5 sequencing keystone: the five backend packages (`memory`, `http`, `storage`, `fs-multifile`, `chained`) all call it. Lifting it here lets every backend land independently without copy-pasting test fixtures.