Initial Commit

This commit is contained in:
2026-05-27 23:47:53 +01:00
parent fbc70f96e6
commit 9b28db724f
18103 changed files with 1600554 additions and 0 deletions

View File

@@ -0,0 +1,129 @@
# `runBackendConformance`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## 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<ConformanceContext>` — 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<string, unknown>
) => Promise<void>;
}
```
### `ConformanceFactory`
`type ConformanceFactory = () => Promise<ConformanceContext>;`
## 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.

View File

@@ -0,0 +1,221 @@
import { describe, expect, it } from "@suaveplan/testing/runner";
import type { Backend, TranslationRecord } from "@suaveplan/types";
import { I18N_CODES } from "../errors/error-codes.js";
import { I18nError } from "../errors/i18n-error.js";
import { runBackendConformance } from "./conformance.js";
import type { ConformanceContext } from "./conformance.types.js";
/**
* Inline full-featured mock backend exercising every capability path
* the harness inspects. Reused across the harness self-tests below.
*/
class MockFullBackend implements Backend {
public readonly name = "mock-full";
public readonly capabilities = {
canRead: true as const,
canReadMulti: true,
canSave: true,
canTrackModification: true,
canWatch: false,
};
readonly #data = new Map<string, Map<string, TranslationRecord>>();
readonly #modified = new Map<string, number>();
#clock = 1000;
public async read(
locale: string,
namespace: string
): Promise<TranslationRecord> {
const ns = this.#data.get(locale)?.get(namespace);
if (ns === undefined) {
throw new I18nError(
I18N_CODES.BACKEND_LOCALE_NOT_AVAILABLE,
`mock: no record for ${locale}/${namespace}`,
{ context: { locale, namespace, backend: this.name } }
);
}
return ns;
}
public async readMulti(
locale: string,
namespaces: readonly string[]
): Promise<Record<string, TranslationRecord>> {
const out: Record<string, TranslationRecord> = {};
const byLocale = this.#data.get(locale);
if (!byLocale) return out;
for (const ns of namespaces) {
const r = byLocale.get(ns);
if (r !== undefined) out[ns] = r;
}
return out;
}
public async save(
locale: string,
namespace: string,
record: TranslationRecord
): Promise<void> {
let byNs = this.#data.get(locale);
if (!byNs) {
byNs = new Map();
this.#data.set(locale, byNs);
}
byNs.set(namespace, record);
this.#modified.set(`${locale} ${namespace}`, this.#clock++);
}
public async lastModified(
locale: string,
namespace: string
): Promise<number | undefined> {
return this.#modified.get(`${locale} ${namespace}`);
}
}
/**
* Read-only mock — `canRead: true`, every other flag `false`. Used to
* exercise the harness's capability-gating branches (early returns
* for save / readMulti / lastModified).
*/
class MockReadOnlyBackend implements Backend {
public readonly name = "mock-readonly";
public readonly capabilities = {
canRead: true as const,
canReadMulti: false,
canSave: false,
canTrackModification: false,
canWatch: false,
};
public async read(
locale: string,
namespace: string
): Promise<TranslationRecord> {
throw new I18nError(
I18N_CODES.BACKEND_LOCALE_NOT_AVAILABLE,
`mock-readonly: no record for ${locale}/${namespace}`
);
}
}
/**
* Capability-mismatch mock — declares methods are present but flags
* them off. Exercises the second disjunct of every capability gate
* (`backend.<method>` truthy AND `capabilities.canX === false` →
* early return).
*/
class MockCapabilityMismatchBackend implements Backend {
public readonly name = "mock-mismatch";
public readonly capabilities = {
canRead: true as const,
canReadMulti: false,
canSave: false,
canTrackModification: false,
canWatch: false,
};
public async read(
locale: string,
namespace: string
): Promise<TranslationRecord> {
throw new I18nError(
I18N_CODES.BACKEND_LOCALE_NOT_AVAILABLE,
`mock-mismatch: no record for ${locale}/${namespace}`
);
}
public async readMulti(
_locale: string,
_namespaces: readonly string[]
): Promise<Record<string, TranslationRecord>> {
return {};
}
public async save(
_locale: string,
_namespace: string,
_record: TranslationRecord
): Promise<void> {
// no-op
}
public async lastModified(
_locale: string,
_namespace: string
): Promise<number | undefined> {
return undefined;
}
}
// ── Run the harness against the full-featured mock — exercises every
// "supported" branch. Registered at module load so vitest picks it up.
runBackendConformance(
"MockFullBackend",
async (): Promise<ConformanceContext> => {
const backend = new MockFullBackend();
return {
backend,
async seed(locale, namespace, record) {
await backend.save(locale, namespace, record as never);
},
};
}
);
// ── Run the harness against the read-only mock — exercises every
// "not supported" early-return branch (no seed, no save, no
// readMulti, no lastModified).
runBackendConformance(
"MockReadOnlyBackend",
async (): Promise<ConformanceContext> => ({
backend: new MockReadOnlyBackend(),
})
);
// ── Mismatch mock with seed: methods exist but capability flags off
// — exercises the second disjunct of each capability gate.
runBackendConformance(
"MockCapabilityMismatchBackend",
async (): Promise<ConformanceContext> => ({
backend: new MockCapabilityMismatchBackend(),
})
);
// ── Full backend whose capability flags are all flipped off — this
// exercises the second disjunct of every capability gate (the
// `!ctx.backend.capabilities.canX` short-circuit) when the seed
// callback IS present.
class MockFlagsOffBackend extends MockFullBackend {
public override readonly capabilities = {
canRead: true as const,
canReadMulti: false,
canSave: false,
canTrackModification: false,
canWatch: false,
};
}
runBackendConformance(
"MockFlagsOffBackend",
async (): Promise<ConformanceContext> => {
const backend = new MockFlagsOffBackend();
return {
backend,
async seed(locale, namespace, record) {
await backend.save(locale, namespace, record as never);
},
};
}
);
describe("runBackendConformance", () => {
it("is a callable function", { timeout: 5000 }, () => {
expect(typeof runBackendConformance).toBe("function");
expect(runBackendConformance.length).toBe(2);
});
});

View File

@@ -0,0 +1,132 @@
import { describe, expect, it } from "@suaveplan/testing/runner";
import { I18nError } from "../errors/i18n-error.js";
import type { ConformanceFactory } from "./conformance.types.js";
/**
* Run the universal `Backend` interface contract against a factory.
*
* Every concrete backend in the `@suaveplan/i18n-backend-*` family
* MUST call this from its own test file with a factory that produces
* a fresh backend per call. The harness covers the universal contract
* (name + capabilities surface, read of an absent record, read of a
* seeded record, optional readMulti merge, optional save round-trip,
* optional lastModified monotonicity) — per-backend specifics live
* in their own test files.
*
* Capability gating: the harness inspects `backend.capabilities` and
* the structural presence of `readMulti` / `save` / `lastModified`
* to decide which assertions apply. Backends that do not support a
* capability silently skip the corresponding assertion (it still
* runs, but exits early), keeping the suite green for read-only
* backends like HTTP.
*
* @param label - Human-readable backend label inserted into the
* `describe(...)` block title. Conventionally the class name.
* @param makeFactory - Async factory producing a fresh
* {@link ConformanceContext} per invocation.
*/
export function runBackendConformance(
label: string,
makeFactory: ConformanceFactory
): void {
describe(`Backend conformance: ${label}`, () => {
it("exposes a non-empty name", { timeout: 5000 }, async () => {
const { backend } = await makeFactory();
expect(typeof backend.name).toBe("string");
expect(backend.name.length).toBeGreaterThan(0);
});
it("exposes capabilities with canRead === true", {
timeout: 5000,
}, async () => {
const { backend } = await makeFactory();
expect(backend.capabilities.canRead).toBe(true);
});
it("exposes capabilities with the full 5-flag matrix", {
timeout: 5000,
}, async () => {
const { backend } = await makeFactory();
const c = backend.capabilities;
expect(typeof c.canRead).toBe("boolean");
expect(typeof c.canReadMulti).toBe("boolean");
expect(typeof c.canSave).toBe("boolean");
expect(typeof c.canTrackModification).toBe("boolean");
expect(typeof c.canWatch).toBe("boolean");
});
it("read(absent) rejects with BACKEND_LOCALE_NOT_AVAILABLE", {
timeout: 5000,
}, async () => {
const { backend } = await makeFactory();
await expect(backend.read("xx", "missing")).rejects.toBeInstanceOf(
I18nError
);
await expect(backend.read("xx", "missing")).rejects.toMatchObject({
code: "I18N.BACKEND_LOCALE_NOT_AVAILABLE",
});
});
it("read(seeded) resolves to the seeded record", {
timeout: 5000,
}, async () => {
const ctx = await makeFactory();
if (!ctx.seed) return;
await ctx.seed("en", "auth", { login: { title: "Sign in" } });
const record = await ctx.backend.read("en", "auth");
expect(record).toEqual({ login: { title: "Sign in" } });
});
it("readMulti merges when supported", { timeout: 5000 }, async () => {
const ctx = await makeFactory();
if (!ctx.backend.readMulti || !ctx.seed) return;
if (!ctx.backend.capabilities.canReadMulti) return;
await ctx.seed("en", "auth", { ok: "Ok" });
await ctx.seed("en", "common", { cancel: "Cancel" });
const result = await ctx.backend.readMulti("en", [
"auth",
"common",
]);
expect(result).toEqual({
auth: { ok: "Ok" },
common: { cancel: "Cancel" },
});
});
it("save then read round-trips when supported", {
timeout: 5000,
}, async () => {
const { backend } = await makeFactory();
if (!backend.save || !backend.capabilities.canSave) return;
await backend.save("en", "auth", { login: { title: "Sign in" } });
const record = await backend.read("en", "auth");
expect(record).toEqual({ login: { title: "Sign in" } });
});
it("lastModified increases monotonically when supported", {
timeout: 5000,
}, async () => {
const { backend } = await makeFactory();
if (!backend.save || !backend.lastModified) return;
if (!backend.capabilities.canTrackModification) return;
await backend.save("en", "auth", { a: "1" });
const t1 = await backend.lastModified("en", "auth");
expect(t1).toBeDefined();
await new Promise((r) => setTimeout(r, 2));
await backend.save("en", "auth", { a: "2" });
const t2 = await backend.lastModified("en", "auth");
expect(t2).toBeDefined();
expect(t2 as number).toBeGreaterThanOrEqual(t1 as number);
});
it("lastModified(absent) returns undefined when supported", {
timeout: 5000,
}, async () => {
const { backend } = await makeFactory();
if (!backend.lastModified) return;
if (!backend.capabilities.canTrackModification) return;
const t = await backend.lastModified("xx", "missing");
expect(t).toBeUndefined();
});
});
}

View File

@@ -0,0 +1,28 @@
import type { Backend } from "@suaveplan/types";
/**
* Context returned by a {@link ConformanceFactory}. The harness calls
* the factory once per assertion to produce a fresh, empty backend
* along with an optional `seed` adapter that pre-populates the backend
* with a record at a given `(locale, namespace)` slot.
*
* Backends that lack `save` (HTTP-style read-only sources) supply
* their own seeding through their factory's options instead and omit
* the `seed` field — the harness skips read-after-seed assertions
* that depend on it.
*/
export interface ConformanceContext {
readonly backend: Backend;
readonly seed?: (
locale: string,
namespace: string,
record: Record<string, unknown>
) => Promise<void>;
}
/**
* Factory signature for {@link runBackendConformance}. The harness
* invokes this once per `it(...)` so the backend instance under test
* is always fresh — no cross-test state leakage.
*/
export type ConformanceFactory = () => Promise<ConformanceContext>;

View File

@@ -0,0 +1,7 @@
// Explicit named re-exports — no `export *` per CLAUDE.md §10.
export { runBackendConformance } from "./conformance.js";
export type {
ConformanceContext,
ConformanceFactory,
} from "./conformance.types.js";