Files
suaveplan/packages/localization/i18n/i18n-backend-fs-multifile/src/backend/backend.test.ts
2026-05-27 23:47:53 +01:00

940 lines
35 KiB
TypeScript

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 <root>/<locale>/<namespace>.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 <root>/<locale>.<namespace>.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);
});
});