130 lines
4.1 KiB
TypeScript
130 lines
4.1 KiB
TypeScript
/**
|
|
* Spec-conformance pins for SegmenterFormatter — Zod rejection paths,
|
|
* Intl.RangeError mapping, OTel emission, and cache reuse semantics.
|
|
*/
|
|
|
|
import { I18N_CODES, I18nError } from "@suaveplan/i18n-shared";
|
|
import { describe, expect, expectTypeOf, it } from "@suaveplan/testing/runner";
|
|
import {
|
|
clearSegmenterCache,
|
|
SegmenterFormatter,
|
|
segmentText,
|
|
} from "./formatter/formatter.js";
|
|
import type {
|
|
SegmenterFormatterOptions,
|
|
SegmenterGranularity,
|
|
} from "./formatter.types.js";
|
|
import {
|
|
getSegmenterMetricsState,
|
|
resetSegmenterMetricsState,
|
|
} from "./observability/observability.js";
|
|
|
|
describe("SegmenterFormatter spec-conformance", () => {
|
|
it("rejects non-object options shape (no path → field='options')", {
|
|
timeout: 5000,
|
|
}, () => {
|
|
try {
|
|
// @ts-expect-error wrong type for safeParse
|
|
segmentText("a", "en-GB", "not-an-object");
|
|
expect.fail("expected throw");
|
|
} catch (err) {
|
|
expect(err).toBeInstanceOf(I18nError);
|
|
expect((err as I18nError).code).toBe(
|
|
I18N_CODES.INVALID_FORMATTER_OPTIONS
|
|
);
|
|
expect((err as I18nError).context).toMatchObject({
|
|
field: "options",
|
|
});
|
|
}
|
|
});
|
|
|
|
it("rejects unknown granularity with INVALID_FORMATTER_OPTIONS", {
|
|
timeout: 5000,
|
|
}, () => {
|
|
try {
|
|
new SegmenterFormatter({ granularity: "magic" as never });
|
|
expect.fail("expected throw");
|
|
} catch (err) {
|
|
expect(err).toBeInstanceOf(I18nError);
|
|
expect((err as I18nError).code).toBe(
|
|
I18N_CODES.INVALID_FORMATTER_OPTIONS
|
|
);
|
|
expect((err as I18nError).context).toMatchObject({
|
|
field: "granularity",
|
|
});
|
|
}
|
|
});
|
|
|
|
it("maps Intl RangeError to UNSUPPORTED_LOCALE", {
|
|
timeout: 5000,
|
|
}, () => {
|
|
clearSegmenterCache();
|
|
try {
|
|
segmentText("a", "en US");
|
|
expect.fail("expected throw");
|
|
} catch (err) {
|
|
expect(err).toBeInstanceOf(I18nError);
|
|
expect((err as I18nError).code).toBe(I18N_CODES.UNSUPPORTED_LOCALE);
|
|
}
|
|
});
|
|
|
|
it("reuses cached Intl.Segmenter on identical key", {
|
|
timeout: 5000,
|
|
}, () => {
|
|
clearSegmenterCache();
|
|
resetSegmenterMetricsState();
|
|
segmentText("a", "en-GB");
|
|
segmentText("a", "en-GB");
|
|
const state = getSegmenterMetricsState();
|
|
expect(state.cacheHitSamples).toHaveLength(1);
|
|
expect(state.cacheHitSamples[0]?.attributes.formatter).toBe(
|
|
"segmenter"
|
|
);
|
|
});
|
|
|
|
it("does not reuse cache across distinct granularities", {
|
|
timeout: 5000,
|
|
}, () => {
|
|
clearSegmenterCache();
|
|
resetSegmenterMetricsState();
|
|
segmentText("a b", "en-GB", { granularity: "grapheme" });
|
|
segmentText("a b", "en-GB", { granularity: "word" });
|
|
const state = getSegmenterMetricsState();
|
|
expect(state.cacheHitSamples).toHaveLength(0);
|
|
});
|
|
|
|
it("records a duration_ms sample per call with locale + granularity", {
|
|
timeout: 5000,
|
|
}, () => {
|
|
clearSegmenterCache();
|
|
resetSegmenterMetricsState();
|
|
segmentText("hello", "en-GB", { granularity: "word" });
|
|
const state = getSegmenterMetricsState();
|
|
expect(state.durationSamples).toHaveLength(1);
|
|
expect(state.durationSamples[0]?.attributes).toMatchObject({
|
|
locale: "en-GB",
|
|
granularity: "word",
|
|
});
|
|
});
|
|
|
|
it("defaults granularity to 'grapheme' when omitted", {
|
|
timeout: 5000,
|
|
}, () => {
|
|
clearSegmenterCache();
|
|
resetSegmenterMetricsState();
|
|
segmentText("ab", "en-GB");
|
|
const state = getSegmenterMetricsState();
|
|
expect(state.durationSamples[0]?.attributes.granularity).toBe(
|
|
"grapheme"
|
|
);
|
|
});
|
|
|
|
it("granularity field of options matches SegmenterGranularity", {
|
|
timeout: 5000,
|
|
}, () => {
|
|
expectTypeOf<SegmenterFormatterOptions["granularity"]>().toEqualTypeOf<
|
|
SegmenterGranularity | undefined
|
|
>();
|
|
});
|
|
});
|