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,872 @@
import { MemoryBackend, memoryBackend } from "@suaveplan/i18n-backend-memory";
import { I18N_CODES, I18nError } from "@suaveplan/i18n-shared";
import { runBackendConformance } from "@suaveplan/i18n-shared/testing";
import {
beforeEach,
describe,
expect,
expectTypeOf,
it,
} from "@suaveplan/testing/runner";
import type {
Backend,
BackendCapabilities,
BackendInitContext,
TranslationRecord,
} from "@suaveplan/types";
import {
getChainedBackendMetricsState,
resetChainedBackendMetricsCache,
resetChainedBackendMetricsState,
} from "../observability/observability.js";
import { ChainedBackend, ChainedBackendOptionsSchema } from "./backend.js";
import type { ChainedBackendOptions, MergeStrategy } from "./backend.types.js";
// ─────────────────────────────────────────────────────────────────────
// Stub backends used across the suite
// ─────────────────────────────────────────────────────────────────────
interface ReadOnlyStubOptions {
readonly name: string;
readonly response?: TranslationRecord;
readonly error?: I18nError;
readonly throwGeneric?: Error;
readonly capabilities?: Partial<BackendCapabilities>;
readonly recordSignal?: { last: AbortSignal | undefined };
readonly initOrder?: string[];
readonly disposeOrder?: string[];
readonly throwOnSave?: Error;
readonly recordSaves?: Array<{
locale: string;
namespace: string;
record: TranslationRecord;
}>;
}
function makeStub(opts: ReadOnlyStubOptions): Backend {
const baseCaps: BackendCapabilities = {
canRead: true,
canReadMulti: false,
canSave: false,
canTrackModification: false,
canWatch: false,
...opts.capabilities,
} as BackendCapabilities;
const backend: Backend & {
save?: (
l: string,
n: string,
r: TranslationRecord,
s?: AbortSignal
) => Promise<void>;
} = {
name: opts.name,
capabilities: baseCaps,
async read(_locale, _namespace, signal?: AbortSignal) {
if (opts.recordSignal) opts.recordSignal.last = signal;
if (opts.throwGeneric) throw opts.throwGeneric;
if (opts.error) throw opts.error;
if (opts.response) return opts.response;
throw new I18nError(
I18N_CODES.BACKEND_LOCALE_NOT_AVAILABLE,
`${opts.name}: locale not available`
);
},
async init(_context: BackendInitContext) {
opts.initOrder?.push(opts.name);
},
async dispose() {
opts.disposeOrder?.push(opts.name);
},
};
if (baseCaps.canSave || opts.throwOnSave || opts.recordSaves) {
backend.save = async (locale, namespace, record, _signal) => {
if (opts.throwOnSave) throw opts.throwOnSave;
opts.recordSaves?.push({ locale, namespace, record });
};
}
return backend;
}
// ─────────────────────────────────────────────────────────────────────
// Universal Backend conformance suite
// ─────────────────────────────────────────────────────────────────────
runBackendConformance("ChainedBackend", async () => {
const cache = new MemoryBackend();
const primary = new MemoryBackend();
const chain = new ChainedBackend({ backends: [cache, primary] });
return {
backend: chain,
async seed(locale, namespace, record) {
await primary.save(locale, namespace, record as never);
},
};
});
// ─────────────────────────────────────────────────────────────────────
// ChainedBackend specifics
// ─────────────────────────────────────────────────────────────────────
describe("ChainedBackend constructor", () => {
it("default name is chained", { timeout: 5000 }, () => {
const chain = new ChainedBackend({ backends: [memoryBackend()] });
expect(chain.name).toBe("chained");
});
it("accepts a custom name", { timeout: 5000 }, () => {
const chain = new ChainedBackend({
backends: [memoryBackend()],
name: "custom-chain",
});
expect(chain.name).toBe("custom-chain");
});
it("rejects empty backends with INVALID_OPTIONS", { timeout: 5000 }, () => {
expect(() => new ChainedBackend({ backends: [] })).toThrow(I18nError);
try {
new ChainedBackend({ backends: [] });
} catch (err) {
expect((err as I18nError).code).toBe(I18N_CODES.INVALID_OPTIONS);
}
});
it("rejects non-string name with INVALID_OPTIONS", {
timeout: 5000,
}, () => {
try {
new ChainedBackend({
backends: [memoryBackend()],
name: "" as string,
});
expect.fail("expected throw");
} catch (err) {
expect(err).toBeInstanceOf(I18nError);
expect((err as I18nError).code).toBe(I18N_CODES.INVALID_OPTIONS);
}
});
it("rejects unknown mergeStrategy with INVALID_OPTIONS", {
timeout: 5000,
}, () => {
try {
new ChainedBackend({
backends: [memoryBackend()],
mergeStrategy: "unknown" as MergeStrategy,
});
expect.fail("expected throw");
} catch (err) {
expect(err).toBeInstanceOf(I18nError);
expect((err as I18nError).code).toBe(I18N_CODES.INVALID_OPTIONS);
}
});
it("MergeStrategy is a discriminated union", { timeout: 5000 }, () => {
expectTypeOf<MergeStrategy>().toEqualTypeOf<
"first-wins" | "last-wins"
>();
});
});
describe("ChainedBackend capabilities (union over children)", () => {
it("capabilities are derived from child union", { timeout: 5000 }, () => {
const a = makeStub({ name: "a", capabilities: { canSave: true } });
const b = makeStub({ name: "b" });
const chain = new ChainedBackend({ backends: [a, b] });
expect(chain.capabilities.canSave).toBe(true);
});
it("canTrackModification union over children", { timeout: 5000 }, () => {
const a = makeStub({ name: "a" });
const b = makeStub({
name: "b",
capabilities: { canTrackModification: true },
});
const chain = new ChainedBackend({ backends: [a, b] });
expect(chain.capabilities.canTrackModification).toBe(true);
});
it("canWatch union over children", { timeout: 5000 }, () => {
const a = makeStub({ name: "a", capabilities: { canWatch: true } });
const b = makeStub({ name: "b" });
const chain = new ChainedBackend({ backends: [a, b] });
expect(chain.capabilities.canWatch).toBe(true);
});
it("canRead is always true literal", { timeout: 5000 }, () => {
const chain = new ChainedBackend({
backends: [makeStub({ name: "a" })],
});
expect(chain.capabilities.canRead).toBe(true);
});
});
describe("ChainedBackend.read fallthrough semantics", () => {
beforeEach(() => {
resetChainedBackendMetricsState();
resetChainedBackendMetricsCache();
});
it("fallthrough on BACKEND_LOCALE_NOT_AVAILABLE", {
timeout: 5000,
}, async () => {
const empty = makeStub({ name: "empty" });
const seeded = makeStub({
name: "seeded",
response: { x: "y" },
capabilities: { canSave: true },
});
const chain = new ChainedBackend({ backends: [empty, seeded] });
const record = await chain.read("en", "common");
expect(record).toEqual({ x: "y" });
});
it("BACKEND_READ_FAILED propagates immediately", {
timeout: 5000,
}, async () => {
const failing = makeStub({
name: "failing",
error: new I18nError(I18N_CODES.BACKEND_READ_FAILED, "boom"),
});
let nextCalled = false;
const next: Backend = {
name: "next",
capabilities: {
canRead: true,
canReadMulti: false,
canSave: false,
canTrackModification: false,
canWatch: false,
},
async read() {
nextCalled = true;
return { x: "y" };
},
};
const chain = new ChainedBackend({ backends: [failing, next] });
await expect(chain.read("en", "common")).rejects.toMatchObject({
code: I18N_CODES.BACKEND_READ_FAILED,
});
expect(nextCalled).toBe(false);
});
it("all children miss throws BACKEND_LOCALE_NOT_AVAILABLE", {
timeout: 5000,
}, async () => {
const a = makeStub({ name: "a" });
const b = makeStub({ name: "b" });
const chain = new ChainedBackend({ backends: [a, b] });
await expect(chain.read("xx", "common")).rejects.toMatchObject({
code: I18N_CODES.BACKEND_LOCALE_NOT_AVAILABLE,
});
});
it("rethrows generic Error from child", { timeout: 5000 }, async () => {
const failing = makeStub({
name: "failing",
throwGeneric: new Error("generic boom"),
});
const chain = new ChainedBackend({ backends: [failing] });
await expect(chain.read("en", "common")).rejects.toThrow(
"generic boom"
);
});
it("fallthrough increments fallthrough_total", {
timeout: 5000,
}, async () => {
const a = makeStub({ name: "a" });
const b = makeStub({ name: "b", response: { x: "y" } });
const chain = new ChainedBackend({ backends: [a, b] });
await chain.read("en", "common");
const state = getChainedBackendMetricsState();
expect(state.fallthroughTotal.length).toBe(1);
expect(state.fallthroughTotal[0]?.attributes).toEqual({
from_backend: "a",
to_backend: "b",
});
});
it("single-backend chain reports primary outcome on hit", {
timeout: 5000,
}, async () => {
const only = makeStub({ name: "only", response: { x: "y" } });
const chain = new ChainedBackend({ backends: [only] });
await chain.read("en", "common");
const state = getChainedBackendMetricsState();
const outcomes = state.readTotal.map((s) => s.attributes["outcome"]);
expect(outcomes).toContain("primary");
});
it("multi-backend chain reports cache_hit when index 0 serves", {
timeout: 5000,
}, async () => {
const cache = makeStub({
name: "cache",
response: { x: "y" },
capabilities: { canSave: true },
});
const primary = makeStub({ name: "primary" });
const chain = new ChainedBackend({ backends: [cache, primary] });
await chain.read("en", "common");
const state = getChainedBackendMetricsState();
const outcomes = state.readTotal.map((s) => s.attributes["outcome"]);
expect(outcomes).toContain("cache_hit");
});
it("multi-backend chain reports cache_miss when later backend serves", {
timeout: 5000,
}, async () => {
const cache = makeStub({ name: "cache" });
const primary = makeStub({ name: "primary", response: { x: "y" } });
const chain = new ChainedBackend({ backends: [cache, primary] });
await chain.read("en", "common");
const state = getChainedBackendMetricsState();
const outcomes = state.readTotal.map((s) => s.attributes["outcome"]);
expect(outcomes).toContain("cache_miss");
});
it("miss outcome on total fallthrough exhaustion", {
timeout: 5000,
}, async () => {
const a = makeStub({ name: "a" });
const b = makeStub({ name: "b" });
const chain = new ChainedBackend({ backends: [a, b] });
await expect(chain.read("xx", "common")).rejects.toBeInstanceOf(
I18nError
);
const state = getChainedBackendMetricsState();
const outcomes = state.readTotal.map((s) => s.attributes["outcome"]);
expect(outcomes).toContain("miss");
});
it("error outcome on propagation", { timeout: 5000 }, async () => {
const failing = makeStub({
name: "failing",
error: new I18nError(I18N_CODES.BACKEND_READ_FAILED, "boom"),
});
const chain = new ChainedBackend({ backends: [failing] });
await expect(chain.read("en", "common")).rejects.toBeInstanceOf(
I18nError
);
const state = getChainedBackendMetricsState();
const errCodes = state.readErrorsTotal.map((s) => s.attributes["code"]);
expect(errCodes).toContain(I18N_CODES.BACKEND_READ_FAILED);
});
});
describe("ChainedBackend write-through cache semantics", () => {
it("write-through populates cache layer", { timeout: 5000 }, async () => {
const cache = new MemoryBackend({ name: "cache" });
const primary = new MemoryBackend({ name: "primary" });
await primary.save("en", "common", { x: "y" });
const chain = new ChainedBackend({ backends: [cache, primary] });
await chain.read("en", "common");
// Cache should now have the record.
expect(await cache.read("en", "common")).toEqual({ x: "y" });
});
it("write-through failure does not block read", {
timeout: 5000,
}, async () => {
const recordedSaves: Array<{
locale: string;
namespace: string;
record: TranslationRecord;
}> = [];
const broken = makeStub({
name: "broken",
capabilities: { canSave: true },
throwOnSave: new Error("disk full"),
recordSaves: recordedSaves,
});
const seeded = makeStub({
name: "seeded",
response: { x: "y" },
});
const chain = new ChainedBackend({ backends: [broken, seeded] });
const record = await chain.read("en", "common");
expect(record).toEqual({ x: "y" });
});
it("write-through skips backends without save", {
timeout: 5000,
}, async () => {
const readOnlyCache = makeStub({ name: "ro" });
const seeded = makeStub({
name: "seeded",
response: { x: "y" },
});
const chain = new ChainedBackend({
backends: [readOnlyCache, seeded],
});
const record = await chain.read("en", "common");
expect(record).toEqual({ x: "y" });
});
});
describe("ChainedBackend.readMulti merge strategies", () => {
it("first-wins prefers earlier backend", { timeout: 5000 }, async () => {
const a = memoryBackend({ seed: { en: { auth: { x: "A" } } } });
const b = memoryBackend({ seed: { en: { auth: { x: "B" } } } });
const chain = new ChainedBackend({ backends: [a, b] });
const merged = await chain.readMulti("en", ["auth"]);
expect(merged["auth"]).toEqual({ x: "A" });
});
it("last-wins prefers later backend", { timeout: 5000 }, async () => {
const a = memoryBackend({ seed: { en: { auth: { x: "A" } } } });
const b = memoryBackend({ seed: { en: { auth: { x: "B" } } } });
const chain = new ChainedBackend({
backends: [a, b],
mergeStrategy: "last-wins",
});
const merged = await chain.readMulti("en", ["auth"]);
expect(merged["auth"]).toEqual({ x: "B" });
});
it("gaps are filled regardless of strategy", {
timeout: 5000,
}, async () => {
const a = memoryBackend({ seed: { en: { auth: { x: "A" } } } });
const b = memoryBackend({ seed: { en: { common: { y: "B" } } } });
const chain = new ChainedBackend({ backends: [a, b] });
const merged = await chain.readMulti("en", ["auth", "common"]);
expect(merged).toEqual({ auth: { x: "A" }, common: { y: "B" } });
});
it("readMulti falls back to per-namespace read when child has no readMulti", {
timeout: 5000,
}, async () => {
const stub = makeStub({
name: "stub",
response: { x: "y" },
});
const chain = new ChainedBackend({ backends: [stub] });
const merged = await chain.readMulti("en", ["a", "b"]);
expect(merged).toEqual({ a: { x: "y" }, b: { x: "y" } });
});
it("readMulti per-namespace fallthrough on BACKEND_LOCALE_NOT_AVAILABLE", {
timeout: 5000,
}, async () => {
const empty = makeStub({ name: "empty" });
const chain = new ChainedBackend({ backends: [empty] });
const merged = await chain.readMulti("xx", ["a", "b"]);
expect(merged).toEqual({});
});
it("readMulti per-namespace propagates non-LOCALE_NOT_AVAILABLE errors", {
timeout: 5000,
}, async () => {
// When a child has no readMulti, the chained backend falls
// back to per-namespace `read()` calls. BACKEND_READ_FAILED
// from any per-namespace read in that path propagates
// upward (not swallowed) — the swallow only applies to the
// child.readMulti() path.
const failing = makeStub({
name: "failing",
error: new I18nError(I18N_CODES.BACKEND_READ_FAILED, "boom"),
});
const chain = new ChainedBackend({ backends: [failing] });
await expect(chain.readMulti("en", ["a"])).rejects.toMatchObject({
code: I18N_CODES.BACKEND_READ_FAILED,
});
});
it("readMulti uses child readMulti when present", {
timeout: 5000,
}, async () => {
const seed = memoryBackend({
seed: { en: { auth: { x: "A" }, common: { y: "B" } } },
});
const chain = new ChainedBackend({ backends: [seed] });
const merged = await chain.readMulti("en", ["auth", "common"]);
expect(merged).toEqual({ auth: { x: "A" }, common: { y: "B" } });
});
it("readMulti swallows child readMulti errors per backend", {
timeout: 5000,
}, async () => {
const failing: Backend = {
name: "failing",
capabilities: {
canRead: true,
canReadMulti: true,
canSave: false,
canTrackModification: false,
canWatch: false,
},
async read() {
throw new I18nError(
I18N_CODES.BACKEND_LOCALE_NOT_AVAILABLE,
"miss"
);
},
async readMulti() {
throw new I18nError(I18N_CODES.BACKEND_READ_FAILED, "boom");
},
};
const seed = memoryBackend({
seed: { en: { auth: { x: "A" } } },
});
const chain = new ChainedBackend({ backends: [failing, seed] });
const merged = await chain.readMulti("en", ["auth"]);
expect(merged).toEqual({ auth: { x: "A" } });
});
});
describe("ChainedBackend.save fan-out", () => {
it("save writes to every save-capable child", {
timeout: 5000,
}, async () => {
const a = memoryBackend({ name: "a" });
const b = memoryBackend({ name: "b" });
const chain = new ChainedBackend({ backends: [a, b] });
await chain.save("en", "common", { x: "y" });
expect(await a.read("en", "common")).toEqual({ x: "y" });
expect(await b.read("en", "common")).toEqual({ x: "y" });
});
it("save skips backends without save", { timeout: 5000 }, async () => {
const ro = makeStub({ name: "ro" });
const writable = memoryBackend({ name: "writable" });
const chain = new ChainedBackend({ backends: [ro, writable] });
await chain.save("en", "common", { x: "y" });
expect(await writable.read("en", "common")).toEqual({ x: "y" });
});
it("save failure throws BACKEND_WRITE_FAILED", {
timeout: 5000,
}, async () => {
const broken = makeStub({
name: "broken",
capabilities: { canSave: true },
throwOnSave: new Error("disk full"),
});
const chain = new ChainedBackend({ backends: [broken] });
try {
await chain.save("en", "common", { x: "y" });
expect.fail("expected throw");
} catch (err) {
expect(err).toBeInstanceOf(I18nError);
expect((err as I18nError).code).toBe(
I18N_CODES.BACKEND_WRITE_FAILED
);
expect(((err as I18nError).cause as Error).message).toBe(
"disk full"
);
}
});
it("save preserves I18nError thrown by child", {
timeout: 5000,
}, async () => {
const broken = makeStub({
name: "broken",
capabilities: { canSave: true },
throwOnSave: new I18nError(
I18N_CODES.BACKEND_WRITE_FAILED,
"child write failed"
),
});
const chain = new ChainedBackend({ backends: [broken] });
await expect(
chain.save("en", "common", { x: "y" })
).rejects.toMatchObject({
code: I18N_CODES.BACKEND_WRITE_FAILED,
message: "child write failed",
});
});
});
describe("ChainedBackend.lastModified", () => {
it("lastModified returns first defined value", {
timeout: 5000,
}, async () => {
const a: Backend = {
name: "a",
capabilities: {
canRead: true,
canReadMulti: false,
canSave: false,
canTrackModification: true,
canWatch: false,
},
async read() {
return {};
},
async lastModified() {
return undefined;
},
};
const b: Backend = {
name: "b",
capabilities: {
canRead: true,
canReadMulti: false,
canSave: false,
canTrackModification: true,
canWatch: false,
},
async read() {
return {};
},
async lastModified() {
return 4242;
},
};
const chain = new ChainedBackend({ backends: [a, b] });
expect(await chain.lastModified("en", "common")).toBe(4242);
});
it("lastModified returns undefined when no child reports", {
timeout: 5000,
}, async () => {
const stub = makeStub({ name: "stub" });
const chain = new ChainedBackend({ backends: [stub] });
expect(await chain.lastModified("en", "common")).toBeUndefined();
});
});
describe("ChainedBackend lifecycle (init/dispose)", () => {
it("init forwards in registration order", { timeout: 5000 }, async () => {
const initOrder: string[] = [];
const a = makeStub({ name: "a", initOrder });
const b = makeStub({ name: "b", initOrder });
const c = makeStub({ name: "c", initOrder });
const chain = new ChainedBackend({ backends: [a, b, c] });
await chain.init({
catalogName: "test",
emitLoaded: () => {},
});
expect(initOrder).toEqual(["a", "b", "c"]);
});
it("dispose runs in reverse order", { timeout: 5000 }, async () => {
const disposeOrder: string[] = [];
const a = makeStub({ name: "a", disposeOrder });
const b = makeStub({ name: "b", disposeOrder });
const c = makeStub({ name: "c", disposeOrder });
const chain = new ChainedBackend({ backends: [a, b, c] });
await chain.dispose();
expect(disposeOrder).toEqual(["c", "b", "a"]);
});
it("init skips children without init hook", { timeout: 5000 }, async () => {
const noHook: Backend = {
name: "no-hook",
capabilities: {
canRead: true,
canReadMulti: false,
canSave: false,
canTrackModification: false,
canWatch: false,
},
async read() {
return {};
},
};
const chain = new ChainedBackend({ backends: [noHook] });
await chain.init({
catalogName: "test",
emitLoaded: () => {},
});
// No throw — pass.
});
it("dispose skips children without dispose hook", {
timeout: 5000,
}, async () => {
const noHook: Backend = {
name: "no-hook",
capabilities: {
canRead: true,
canReadMulti: false,
canSave: false,
canTrackModification: false,
canWatch: false,
},
async read() {
return {};
},
};
const chain = new ChainedBackend({ backends: [noHook] });
await chain.dispose();
// No throw — pass.
});
});
describe("ChainedBackend AbortSignal threading", () => {
it("pre-aborted signal aborts read with BACKEND_TIMEOUT", {
timeout: 5000,
}, async () => {
let childCalled = false;
const stub: Backend = {
name: "stub",
capabilities: {
canRead: true,
canReadMulti: false,
canSave: false,
canTrackModification: false,
canWatch: false,
},
async read() {
childCalled = true;
return {};
},
};
const chain = new ChainedBackend({ backends: [stub] });
const ctrl = new AbortController();
ctrl.abort();
await expect(
chain.read("en", "common", ctrl.signal)
).rejects.toMatchObject({ code: I18N_CODES.BACKEND_TIMEOUT });
expect(childCalled).toBe(false);
});
it("pre-aborted signal aborts readMulti with BACKEND_TIMEOUT", {
timeout: 5000,
}, async () => {
const chain = new ChainedBackend({
backends: [memoryBackend()],
});
const ctrl = new AbortController();
ctrl.abort();
await expect(
chain.readMulti("en", ["a"], ctrl.signal)
).rejects.toMatchObject({ code: I18N_CODES.BACKEND_TIMEOUT });
});
it("pre-aborted signal aborts save with BACKEND_TIMEOUT", {
timeout: 5000,
}, async () => {
const chain = new ChainedBackend({
backends: [memoryBackend()],
});
const ctrl = new AbortController();
ctrl.abort();
await expect(
chain.save("en", "a", { x: "y" }, ctrl.signal)
).rejects.toMatchObject({ code: I18N_CODES.BACKEND_TIMEOUT });
});
it("signal threaded into child reads", { timeout: 5000 }, async () => {
const recorder: { last: AbortSignal | undefined } = {
last: undefined,
};
const stub = makeStub({
name: "stub",
response: { x: "y" },
recordSignal: recorder,
});
const chain = new ChainedBackend({ backends: [stub] });
const ctrl = new AbortController();
await chain.read("en", "common", ctrl.signal);
expect(recorder.last).toBe(ctrl.signal);
});
it("signal threaded into child saves", { timeout: 5000 }, async () => {
const seenSignals: Array<AbortSignal | undefined> = [];
const stub: Backend = {
name: "stub",
capabilities: {
canRead: true,
canReadMulti: false,
canSave: true,
canTrackModification: false,
canWatch: false,
},
async read() {
return {};
},
async save(
_l: string,
_n: string,
_r: TranslationRecord,
signal?: AbortSignal
) {
seenSignals.push(signal);
},
};
const chain = new ChainedBackend({ backends: [stub] });
const ctrl = new AbortController();
await chain.save("en", "a", { x: "y" }, ctrl.signal);
expect(seenSignals[0]).toBe(ctrl.signal);
});
it("signal threaded into child readMulti", { timeout: 5000 }, async () => {
const seen: Array<AbortSignal | undefined> = [];
const stub: Backend = {
name: "stub",
capabilities: {
canRead: true,
canReadMulti: true,
canSave: false,
canTrackModification: false,
canWatch: false,
},
async read() {
return {};
},
async readMulti(
_l: string,
_ns: readonly string[],
signal?: AbortSignal
) {
seen.push(signal);
return {};
},
};
const chain = new ChainedBackend({ backends: [stub] });
const ctrl = new AbortController();
await chain.readMulti("en", ["a"], ctrl.signal);
expect(seen[0]).toBe(ctrl.signal);
});
});
describe("ChainedBackend CHAINED_BACKEND_ALL_FAILED defence-in-depth", () => {
it("runtime empty chain throws CHAINED_BACKEND_ALL_FAILED on read", {
timeout: 5000,
}, async () => {
// The constructor's Zod schema rejects empty `backends`
// arrays, so the only way to land in the runtime defensive
// branch is to bypass the schema. Use vi.spyOn to make
// safeParse accept an empty array for one constructor call.
const spyTarget = ChainedBackendOptionsSchema as unknown as {
safeParse: (input: unknown) => {
success: boolean;
data?: unknown;
error?: unknown;
};
};
const original = spyTarget.safeParse.bind(spyTarget);
spyTarget.safeParse = ((_input: unknown) => ({
success: true,
data: { backends: [] },
})) as typeof spyTarget.safeParse;
try {
const chain = new ChainedBackend({
backends: [] as unknown as readonly Backend[],
});
await expect(chain.read("en", "common")).rejects.toMatchObject({
code: I18N_CODES.CHAINED_BACKEND_ALL_FAILED,
});
} finally {
spyTarget.safeParse =
original as unknown as typeof spyTarget.safeParse;
}
});
it("constructor schema rejects backends: []", { timeout: 5000 }, () => {
try {
new ChainedBackend({
backends: [] as unknown as readonly Backend[],
} as ChainedBackendOptions);
expect.fail("expected throw");
} catch (err) {
expect(err).toBeInstanceOf(I18nError);
expect((err as I18nError).code).toBe(I18N_CODES.INVALID_OPTIONS);
}
});
});