import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { I18N_CODES, I18nError } from "@suaveplan/i18n-shared"; import { runBackendConformance } from "@suaveplan/i18n-shared/testing"; import { afterEach, beforeEach, describe, expect, it, } from "@suaveplan/testing/runner"; import { getMultiFileFsBackendMetricsState, resetMultiFileFsBackendMetricsCache, resetMultiFileFsBackendMetricsState, } from "../observability/observability.js"; import { MultiFileFsBackend } from "./backend.js"; let testRoot: string; beforeEach(async () => { testRoot = await mkdtemp(join(tmpdir(), "i18n-mfs-")); resetMultiFileFsBackendMetricsCache(); resetMultiFileFsBackendMetricsState(); }); afterEach(async () => { if (testRoot) await rm(testRoot, { recursive: true, force: true }); }); // ───────────────────────────────────────────────────────────────────── // Universal Backend conformance suite (real tmpdir; nested layout) // ───────────────────────────────────────────────────────────────────── runBackendConformance("MultiFileFsBackend", async () => { const root = await mkdtemp(join(tmpdir(), "i18n-mfs-conf-")); const backend = new MultiFileFsBackend({ root }); return { backend, async seed(locale, namespace, record) { await backend.save(locale, namespace, record as never); }, }; }); // ───────────────────────────────────────────────────────────────────── // MultiFileFsBackend specifics — nested layout // ───────────────────────────────────────────────────────────────────── describe("MultiFileFsBackend nested layout", () => { it("loads //.json", { timeout: 5000, }, async () => { await mkdir(join(testRoot, "en"), { recursive: true }); await writeFile( join(testRoot, "en", "auth.json"), JSON.stringify({ login: { title: "Sign in" } }) ); const backend = new MultiFileFsBackend({ root: testRoot }); const record = await backend.read("en", "auth"); expect(record).toEqual({ login: { title: "Sign in" } }); }); it("nested layout reads expected path", { timeout: 5000 }, async () => { const seen: string[] = []; const fs = { readFile: async (p: string) => { seen.push(p); return new TextEncoder().encode( JSON.stringify({ hello: "Hi" }) ); }, writeFile: async () => {}, mkdir: async () => {}, stat: async () => ({ mtimeMs: 0 }), }; const backend = new MultiFileFsBackend({ root: "/tmp/i18n", layout: "nested", fs, }); const record = await backend.read("en", "common"); expect(record).toEqual({ hello: "Hi" }); expect(seen[0]).toBe("/tmp/i18n/en/common.json"); }); it("ENOENT maps to BACKEND_LOCALE_NOT_AVAILABLE", { timeout: 5000, }, async () => { const backend = new MultiFileFsBackend({ root: testRoot }); try { await backend.read("de", "auth"); expect.fail("expected throw"); } catch (err) { expect(err).toBeInstanceOf(I18nError); expect((err as I18nError).code).toBe( I18N_CODES.BACKEND_LOCALE_NOT_AVAILABLE ); } }); it("invalid JSON maps to BACKEND_READ_FAILED", { timeout: 5000, }, async () => { await mkdir(join(testRoot, "en"), { recursive: true }); await writeFile(join(testRoot, "en", "auth.json"), "not-json"); const backend = new MultiFileFsBackend({ root: testRoot }); try { await backend.read("en", "auth"); expect.fail("expected throw"); } catch (err) { expect(err).toBeInstanceOf(I18nError); expect((err as I18nError).code).toBe( I18N_CODES.BACKEND_READ_FAILED ); } }); it("array JSON rejects as BACKEND_READ_FAILED", { timeout: 5000, }, async () => { await mkdir(join(testRoot, "en"), { recursive: true }); await writeFile(join(testRoot, "en", "auth.json"), "[]"); const backend = new MultiFileFsBackend({ root: testRoot }); await expect(backend.read("en", "auth")).rejects.toBeInstanceOf( I18nError ); }); it("save creates the locale directory and round-trips", { timeout: 5000, }, async () => { const backend = new MultiFileFsBackend({ root: testRoot }); await backend.save("fr", "common", { ok: "Ok" }); const record = await backend.read("fr", "common"); expect(record).toEqual({ ok: "Ok" }); }); it("save creates parent directory", { timeout: 5000 }, async () => { const mkdirCalls: Array<{ path: string; recursive?: boolean }> = []; const fs = { readFile: async () => new Uint8Array(), writeFile: async () => {}, mkdir: async (p: string, opts?: { recursive?: boolean }) => { mkdirCalls.push({ path: p, recursive: opts?.recursive }); }, stat: async () => ({ mtimeMs: 0 }), }; const backend = new MultiFileFsBackend({ root: "/tmp/i18n", fs }); await backend.save("en", "common", { x: "y" }); expect(mkdirCalls).toEqual([{ path: "/tmp/i18n/en", recursive: true }]); }); it("writeFile failure throws BACKEND_WRITE_FAILED", { timeout: 5000, }, async () => { const fs = { readFile: async () => new Uint8Array(), writeFile: async () => { throw new Error("disk full"); }, mkdir: async () => {}, stat: async () => ({ mtimeMs: 0 }), }; const backend = new MultiFileFsBackend({ root: "/tmp/i18n", fs }); try { await backend.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 encoder", { timeout: 5000, }, async () => { const backend = new MultiFileFsBackend({ root: testRoot, extension: "yaml", }); try { await backend.save("en", "auth", { ok: "Ok" }); expect.fail("expected throw"); } catch (err) { expect(err).toBeInstanceOf(I18nError); expect((err as I18nError).code).toBe( I18N_CODES.BACKEND_WRITE_FAILED ); } }); it("lastModified returns mtime-ms after save", { timeout: 5000, }, async () => { const backend = new MultiFileFsBackend({ root: testRoot }); await backend.save("en", "auth", { ok: "Ok" }); const t = await backend.lastModified("en", "auth"); expect(typeof t).toBe("number"); expect(t).toBeGreaterThan(0); }); it("lastModified returns mtime", { timeout: 5000 }, async () => { const fs = { readFile: async () => new Uint8Array(), writeFile: async () => {}, mkdir: async () => {}, stat: async () => ({ mtimeMs: 4242 }), }; const backend = new MultiFileFsBackend({ root: "/tmp", fs }); expect(await backend.lastModified("en", "common")).toBe(4242); }); it("lastModified swallows stat failure", { timeout: 5000 }, async () => { const fs = { readFile: async () => new Uint8Array(), writeFile: async () => {}, mkdir: async () => {}, stat: async () => { throw new Error("stat failed"); }, }; const backend = new MultiFileFsBackend({ root: "/tmp", fs }); expect(await backend.lastModified("en", "auth")).toBeUndefined(); }); it("lastModified returns undefined for missing file", { timeout: 5000, }, async () => { const backend = new MultiFileFsBackend({ root: testRoot }); expect(await backend.lastModified("xx", "auth")).toBeUndefined(); }); }); // ───────────────────────────────────────────────────────────────────── // MultiFileFsBackend specifics — flat layout // ───────────────────────────────────────────────────────────────────── describe("MultiFileFsBackend flat layout", () => { it("loads /..json", { timeout: 5000, }, async () => { await writeFile( join(testRoot, "en.auth.json"), JSON.stringify({ ok: "Ok" }) ); const backend = new MultiFileFsBackend({ root: testRoot, layout: "flat", }); expect(await backend.read("en", "auth")).toEqual({ ok: "Ok" }); }); it("flat layout reads expected path", { timeout: 5000 }, async () => { const seen: string[] = []; const fs = { readFile: async (p: string) => { seen.push(p); return new TextEncoder().encode(JSON.stringify({ ok: "Ok" })); }, writeFile: async () => {}, mkdir: async () => {}, stat: async () => ({ mtimeMs: 0 }), }; const backend = new MultiFileFsBackend({ root: "/tmp/i18n", layout: "flat", fs, }); await backend.read("en", "common"); expect(seen[0]).toBe("/tmp/i18n/en.common.json"); }); }); // ───────────────────────────────────────────────────────────────────── // MultiFileFsBackend specifics — glob layout // ───────────────────────────────────────────────────────────────────── describe("MultiFileFsBackend glob layout", () => { it("substitutes {locale} and {namespace} in pattern", { timeout: 5000, }, async () => { await mkdir(join(testRoot, "fr"), { recursive: true }); await writeFile( join(testRoot, "fr", "auth.fr.json"), JSON.stringify({ ok: "Ok-fr" }) ); const backend = new MultiFileFsBackend({ root: testRoot, layout: "glob", globPattern: "{locale}/{namespace}.{locale}.json", }); expect(await backend.read("fr", "auth")).toEqual({ ok: "Ok-fr" }); }); it("glob layout requires globPattern", { timeout: 5000 }, () => { try { new MultiFileFsBackend({ root: testRoot, layout: "glob" }); expect.fail("expected throw"); } catch (err) { expect(err).toBeInstanceOf(I18nError); expect((err as I18nError).code).toBe(I18N_CODES.INVALID_OPTIONS); } }); it("glob layout substitutes placeholders", { timeout: 5000 }, async () => { const seen: string[] = []; const fs = { readFile: async (p: string) => { seen.push(p); return new TextEncoder().encode(JSON.stringify({ ok: "Ok" })); }, writeFile: async () => {}, mkdir: async () => {}, stat: async () => ({ mtimeMs: 0 }), }; const backend = new MultiFileFsBackend({ root: "/tmp", layout: "glob", globPattern: "i18n/{namespace}.{locale}.json", fs, }); await backend.read("en", "common"); expect(seen[0]).toBe("/tmp/i18n/common.en.json"); }); }); // ───────────────────────────────────────────────────────────────────── // Capabilities + extension support // ───────────────────────────────────────────────────────────────────── describe("MultiFileFsBackend capabilities", () => { it("capabilities matrix follows watch option", { timeout: 5000 }, () => { const a = new MultiFileFsBackend({ root: testRoot, watch: true }); expect(a.capabilities.canWatch).toBe(true); const b = new MultiFileFsBackend({ root: testRoot, watch: false }); expect(b.capabilities.canWatch).toBe(false); }); }); describe("MultiFileFsBackend extension support", () => { it("rejects unknown extension as BACKEND_READ_FAILED", { timeout: 5000, }, async () => { await mkdir(join(testRoot, "en"), { recursive: true }); await writeFile(join(testRoot, "en", "auth.toml"), "ok = 'Ok'"); const backend = new MultiFileFsBackend({ root: testRoot, extension: "toml", }); try { await backend.read("en", "auth"); expect.fail("expected throw"); } catch (err) { expect(err).toBeInstanceOf(I18nError); expect((err as I18nError).code).toBe( I18N_CODES.BACKEND_READ_FAILED ); } }); it("yaml extension parses via the yaml peer when present", { timeout: 5000, }, async () => { await mkdir(join(testRoot, "en"), { recursive: true }); await writeFile(join(testRoot, "en", "auth.yaml"), "ok: Ok\n"); const backend = new MultiFileFsBackend({ root: testRoot, extension: "yaml", }); try { const record = await backend.read("en", "auth"); expect(record).toEqual({ ok: "Ok" }); } catch (err) { // If yaml peer is not installed, the surface error must // identify the missing peer. expect(err).toBeInstanceOf(I18nError); expect((err as I18nError).code).toBe( I18N_CODES.BACKEND_READ_FAILED ); expect(String((err as I18nError).message)).toContain("yaml"); } }); it("yml extension routes through yaml peer", { timeout: 5000, }, async () => { await mkdir(join(testRoot, "en"), { recursive: true }); await writeFile(join(testRoot, "en", "auth.yml"), "ok: Ok\n"); const backend = new MultiFileFsBackend({ root: testRoot, extension: "yml", }); try { await backend.read("en", "auth"); } catch (err) { expect((err as I18nError).message).toContain("yaml"); } }); it("yaml peer returning non-record payload surfaces BACKEND_READ_FAILED", { timeout: 5000, }, async () => { await mkdir(join(testRoot, "en"), { recursive: true }); await writeFile(join(testRoot, "en", "auth.yaml"), '"just a string"\n'); const backend = new MultiFileFsBackend({ root: testRoot, extension: "yaml", }); try { await backend.read("en", "auth"); expect.fail("expected throw"); } catch (err) { expect(err).toBeInstanceOf(I18nError); expect((err as I18nError).code).toBe( I18N_CODES.BACKEND_READ_FAILED ); // Either "non-record" (peer present) or "yaml" (peer missing) // — both are valid spec outcomes. const msg = String((err as I18nError).message); expect(/non-record|yaml/.test(msg)).toBe(true); } }); it("json5 extension routes through the parseWithPeer dispatch", { timeout: 5000, }, async () => { await mkdir(join(testRoot, "en"), { recursive: true }); await writeFile(join(testRoot, "en", "auth.json5"), "{ ok: 'Ok', }\n"); const backend = new MultiFileFsBackend({ root: testRoot, extension: "json5", }); try { await backend.read("en", "auth"); } catch (err) { expect(err).toBeInstanceOf(I18nError); expect((err as I18nError).code).toBe( I18N_CODES.BACKEND_READ_FAILED ); expect(String((err as I18nError).message)).toContain("json5"); } }); it("save rejects non-JSON extension as BACKEND_WRITE_FAILED", { timeout: 5000, }, async () => { const backend = new MultiFileFsBackend({ root: testRoot, extension: "yaml", }); try { await backend.save("en", "auth", { ok: "Ok" }); expect.fail("expected throw"); } catch (err) { expect(err).toBeInstanceOf(I18nError); expect((err as I18nError).code).toBe( I18N_CODES.BACKEND_WRITE_FAILED ); } }); it("save accepts json5 extension (encodes as JSON)", { timeout: 5000, }, async () => { const writes: Array<{ p: string; data: Uint8Array | string }> = []; const fs = { readFile: async () => new Uint8Array(), writeFile: async (p: string, data: Uint8Array | string) => { writes.push({ p, data }); }, mkdir: async () => {}, stat: async () => ({ mtimeMs: 0 }), }; const backend = new MultiFileFsBackend({ root: "/tmp", extension: "json5", fs, }); await backend.save("en", "auth", { ok: "Ok" }); expect(writes.length).toBe(1); }); }); // ───────────────────────────────────────────────────────────────────── // Watch lifecycle // ───────────────────────────────────────────────────────────────────── describe("MultiFileFsBackend watch mode", () => { it("init starts watcher when watch=true", { timeout: 5000 }, async () => { const events: Array<{ locale: string; namespace: string; ctx: unknown; }> = []; const iter = (async function* () { yield { filename: "en/common.json" }; })(); const fs = { readFile: async () => new TextEncoder().encode(JSON.stringify({ ok: "Ok" })), writeFile: async () => {}, mkdir: async () => {}, stat: async () => ({ mtimeMs: 1 }), watch: () => iter, }; const backend = new MultiFileFsBackend({ root: "/virtual", watch: true, fs, }); await backend.init({ catalogName: "test", emitLoaded: (locale, namespace, ctx) => { events.push({ locale, namespace, ctx }); }, }); await new Promise((r) => setTimeout(r, 30)); await backend.dispose(); expect(events).toEqual([ { locale: "en", namespace: "common", ctx: { backend: "multi-file-fs" }, }, ]); }); it("emits loaded event when a watched file changes", { timeout: 5000, }, async () => { const events: Array<{ locale: string; namespace: string }> = []; const fakeIter = (async function* () { yield { filename: "en/auth.json" }; })(); const fs = { readFile: async () => new TextEncoder().encode(JSON.stringify({ ok: "Ok" })), writeFile: async () => {}, mkdir: async () => {}, stat: async () => ({ mtimeMs: 1 }), watch: () => fakeIter, }; const backend = new MultiFileFsBackend({ root: "/virtual", watch: true, fs, }); await backend.init({ catalogName: "test", emitLoaded: (locale, namespace) => { events.push({ locale, namespace }); }, }); await new Promise((r) => setTimeout(r, 30)); await backend.dispose(); expect(events).toEqual([{ locale: "en", namespace: "auth" }]); }); it("watch ignores null filenames and unmatchable patterns", { timeout: 5000, }, async () => { const events: unknown[] = []; const iter = (async function* () { yield { filename: null }; yield { filename: "noslash.json" }; })(); const fs = { readFile: async () => new Uint8Array(), writeFile: async () => {}, mkdir: async () => {}, stat: async () => ({ mtimeMs: 0 }), watch: () => iter, }; const backend = new MultiFileFsBackend({ root: "/virtual", watch: true, fs, }); await backend.init({ catalogName: "x", emitLoaded: () => events.push({}), }); await new Promise((r) => setTimeout(r, 30)); await backend.dispose(); expect(events).toEqual([]); }); it("watch parses flat-layout filenames", { timeout: 5000, }, async () => { const events: Array<{ locale: string; namespace: string }> = []; const iter = (async function* () { yield { filename: "en.auth.json" }; yield { filename: "no-extension" }; })(); const fs = { readFile: async () => new TextEncoder().encode(JSON.stringify({ ok: "Ok" })), writeFile: async () => {}, mkdir: async () => {}, stat: async () => ({ mtimeMs: 0 }), watch: () => iter, }; const backend = new MultiFileFsBackend({ root: "/virtual", layout: "flat", watch: true, fs, }); await backend.init({ catalogName: "test", emitLoaded: (locale, namespace) => { events.push({ locale, namespace }); }, }); await new Promise((r) => setTimeout(r, 30)); await backend.dispose(); expect(events).toEqual([{ locale: "en", namespace: "auth" }]); }); it("watch parser ignores unrelated filenames in nested layout", { timeout: 5000, }, async () => { const events: unknown[] = []; const iter = (async function* () { yield { filename: "unrelated.txt" }; })(); const fs = { readFile: async () => new Uint8Array(), writeFile: async () => {}, mkdir: async () => {}, stat: async () => ({ mtimeMs: 0 }), watch: () => iter, }; const backend = new MultiFileFsBackend({ root: "/virtual", watch: true, fs, }); await backend.init({ catalogName: "test", emitLoaded: (...args) => events.push(args), }); await new Promise((r) => setTimeout(r, 30)); await backend.dispose(); expect(events).toEqual([]); }); it("dispose is idempotent", { timeout: 5000 }, async () => { const backend = new MultiFileFsBackend({ root: testRoot }); await backend.dispose(); await backend.dispose(); expect(true).toBe(true); }); it("init with watch=false does not start a watcher", { timeout: 5000, }, async () => { const backend = new MultiFileFsBackend({ root: testRoot }); await backend.init({ catalogName: "x", emitLoaded: () => {} }); await backend.dispose(); expect(true).toBe(true); }); }); // ───────────────────────────────────────────────────────────────────── // FS error classification // ───────────────────────────────────────────────────────────────────── describe("MultiFileFsBackend filesystem error classification", () => { it("non-ENOENT errno (e.g. EACCES) maps to BACKEND_READ_FAILED", { timeout: 5000, }, async () => { const fs = { readFile: async () => { const err = new Error("permission denied") as Error & { code: string; }; err.code = "EACCES"; throw err; }, writeFile: async () => {}, mkdir: async () => {}, stat: async () => ({ mtimeMs: 0 }), }; const backend = new MultiFileFsBackend({ root: "/virtual", fs }); try { await backend.read("en", "auth"); expect.fail("expected throw"); } catch (err) { expect(err).toBeInstanceOf(I18nError); expect((err as I18nError).code).toBe( I18N_CODES.BACKEND_READ_FAILED ); } }); it("ENOTDIR maps to BACKEND_LOCALE_NOT_AVAILABLE", { timeout: 5000, }, async () => { const fs = { readFile: async () => { const err = new Error("not a directory") as Error & { code: string; }; err.code = "ENOTDIR"; throw err; }, writeFile: async () => {}, mkdir: async () => {}, stat: async () => ({ mtimeMs: 0 }), }; const backend = new MultiFileFsBackend({ root: "/virtual", fs }); try { await backend.read("en", "auth"); expect.fail("expected throw"); } catch (err) { expect(err).toBeInstanceOf(I18nError); expect((err as I18nError).code).toBe( I18N_CODES.BACKEND_LOCALE_NOT_AVAILABLE ); } }); }); // ───────────────────────────────────────────────────────────────────── // Constructor option validation // ───────────────────────────────────────────────────────────────────── describe("MultiFileFsBackend constructor", () => { it("rejects malformed options shape via Zod (e.g. layout: bogus)", { timeout: 5000, }, () => { try { const bad: unknown = "bogus"; new MultiFileFsBackend({ root: testRoot, layout: bad as "nested", }); expect.fail("expected throw"); } catch (err) { expect(err).toBeInstanceOf(I18nError); expect((err as I18nError).code).toBe(I18N_CODES.INVALID_OPTIONS); } }); it("rejects empty name with INVALID_OPTIONS", { timeout: 5000 }, () => { try { new MultiFileFsBackend({ root: testRoot, name: "" }); expect.fail("expected throw"); } catch (err) { expect(err).toBeInstanceOf(I18nError); expect((err as I18nError).code).toBe(I18N_CODES.INVALID_OPTIONS); } }); it("default name is multi-file-fs", { timeout: 5000 }, () => { const backend = new MultiFileFsBackend({ root: testRoot }); expect(backend.name).toBe("multi-file-fs"); }); it("accepts empty root (paths relative to cwd)", { timeout: 5000, }, async () => { let mkdirCalls = 0; let writePath: string | undefined; const fs = { readFile: async () => new Uint8Array(), writeFile: async (p: string) => { writePath = p; }, mkdir: async () => { mkdirCalls++; }, stat: async () => ({ mtimeMs: 0 }), }; const backend = new MultiFileFsBackend({ root: "", layout: "glob", globPattern: "{namespace}.{locale}.json", fs, }); await backend.save("en", "auth", { ok: "Ok" }); expect(writePath).toBe("auth.en.json"); expect(mkdirCalls).toBe(0); }); }); // ───────────────────────────────────────────────────────────────────── // AbortSignal cancellation // ───────────────────────────────────────────────────────────────────── describe("MultiFileFsBackend AbortSignal threading", () => { it("pre-aborted signal aborts read with BACKEND_TIMEOUT", { timeout: 5000, }, async () => { let touched = false; const fs = { readFile: async () => { touched = true; return new Uint8Array(); }, writeFile: async () => {}, mkdir: async () => {}, stat: async () => ({ mtimeMs: 0 }), }; const backend = new MultiFileFsBackend({ root: "/virtual", fs }); const ctrl = new AbortController(); ctrl.abort(); await expect( backend.read("en", "auth", ctrl.signal) ).rejects.toMatchObject({ code: I18N_CODES.BACKEND_TIMEOUT }); expect(touched).toBe(false); }); it("pre-aborted signal aborts save with BACKEND_TIMEOUT", { timeout: 5000, }, async () => { let touched = false; const fs = { readFile: async () => new Uint8Array(), writeFile: async () => { touched = true; }, mkdir: async () => {}, stat: async () => ({ mtimeMs: 0 }), }; const backend = new MultiFileFsBackend({ root: "/virtual", fs }); const ctrl = new AbortController(); ctrl.abort(); await expect( backend.save("en", "auth", { ok: "Ok" }, ctrl.signal) ).rejects.toMatchObject({ code: I18N_CODES.BACKEND_TIMEOUT }); expect(touched).toBe(false); }); }); // ───────────────────────────────────────────────────────────────────── // Observability — layout attribute on metrics // ───────────────────────────────────────────────────────────────────── describe("MultiFileFsBackend observability", () => { it("read emits layout attribute", { timeout: 5000 }, async () => { await writeFile( join(testRoot, "en.common.json"), JSON.stringify({ ok: "Ok" }) ); const backend = new MultiFileFsBackend({ root: testRoot, layout: "flat", }); await backend.read("en", "common"); const state = getMultiFileFsBackendMetricsState(); const flat = state.readTotal.find( (s) => s.attributes["layout"] === "flat" ); expect(flat).toBeDefined(); expect(flat?.attributes["outcome"]).toBe("hit"); }); it("write emits layout attribute on success", { timeout: 5000, }, async () => { const backend = new MultiFileFsBackend({ root: testRoot }); await backend.save("en", "auth", { ok: "Ok" }); const state = getMultiFileFsBackendMetricsState(); const okWrite = state.writeTotal.find( (s) => s.attributes["layout"] === "nested" && s.attributes["outcome"] === "ok" ); expect(okWrite).toBeDefined(); }); it("write emits layout attribute on error", { timeout: 5000, }, async () => { const fs = { readFile: async () => new Uint8Array(), writeFile: async () => { throw new Error("disk full"); }, mkdir: async () => {}, stat: async () => ({ mtimeMs: 0 }), }; const backend = new MultiFileFsBackend({ root: "/tmp/i", fs }); await expect( backend.save("en", "auth", { ok: "Ok" }) ).rejects.toBeInstanceOf(I18nError); const state = getMultiFileFsBackendMetricsState(); const errWrite = state.writeTotal.find( (s) => s.attributes["outcome"] === "error" ); expect(errWrite).toBeDefined(); expect(errWrite?.attributes["layout"]).toBe("nested"); }); it("read miss records BACKEND_LOCALE_NOT_AVAILABLE error code", { timeout: 5000, }, async () => { const backend = new MultiFileFsBackend({ root: testRoot }); await expect(backend.read("xx", "auth")).rejects.toBeInstanceOf( I18nError ); const state = getMultiFileFsBackendMetricsState(); const codes = state.readErrorsTotal.map((s) => s.attributes["code"]); expect(codes).toContain(I18N_CODES.BACKEND_LOCALE_NOT_AVAILABLE); }); it("read error records BACKEND_READ_FAILED code", { timeout: 5000, }, async () => { const fs = { readFile: async () => { const err = new Error("perm") as Error & { code: string }; err.code = "EACCES"; throw err; }, writeFile: async () => {}, mkdir: async () => {}, stat: async () => ({ mtimeMs: 0 }), }; const backend = new MultiFileFsBackend({ root: "/virtual", fs }); await expect(backend.read("en", "auth")).rejects.toBeInstanceOf( I18nError ); const state = getMultiFileFsBackendMetricsState(); const codes = state.readErrorsTotal.map((s) => s.attributes["code"]); expect(codes).toContain(I18N_CODES.BACKEND_READ_FAILED); }); });