/*
Unit tests for formula eval.duration covering RFC-1156: duration pipe formatter.
- Not a contract source of truth — the module under test owns the authoritative behavior; fixtures are minimal scaffolding only.
- RFC-1156: initial tests — AC-2..AC-7 coverage for duration pipe formatter.
- RFC-1311: carried legacy entry — Duration-case extension: tests for case=dative/accusative/genitive and short-style no-op.
*/
import { describe, it, expect } from "vitest";
import { resolveFormula } from "./formula-eval.ts";
import { parseIsoDuration, formatDuration, formatBusinessDays } from "./duration.ts";
import type { ContentRefIndex } from "../content/content-reference.ts";
const index: ContentRefIndex = {
version: 1,
generatedAt: "2026-01-01",
collections: ["business-profile"],
entries: {
"business-profile": {
"policies/cancellation": {
de: {
ordinaryNotice: { period: "P30D" },
paymentDefault: { gracePeriod: "P14D" },
},
uk: {
ordinaryNotice: { period: "P30D" },
},
},
"policies/exit-package": {
de: { deliveryTarget: { duration: "PT72H" } },
},
"policies/renewal": {
de: { renewalPeriod: "P1Y6M", reminder: "P2W" },
},
"policies/price-changes": {
de: { priceFixPeriod: "P12M" },
},
"policies/liability": {
de: { cap: { period: "P2Y" } },
},
"documents/terms": {
de: { terms: { preLitigationPeriod: "P60D", review: "P3D", appeal: "P1D" } },
},
"policies/support-response": {
de: { terms: { responseWithin: "P5D" } },
},
"policies/bad": {
de: { note: "thirty days" },
},
},
},
};
const d = (expr: string, lang = "de", defaultLang = "de") =>
resolveFormula(index, expr, lang, defaultLang);
describe("RFC-1156: duration pipe formatter", () => {
it("AC-2: =(x | duration) renders P30D as '30 Tage' in de", () => {
const r = d("business-profile.policies/cancellation.ordinaryNotice.period | duration");
expect(r.resolved).toBe(true);
expect(r.value).toBe("30 Tage");
});
it("AC-3: uk plural boundaries — P60D → '60 днів', P3D → '3 дні', P1D → '1 день'", () => {
expect(
d("business-profile.documents/terms.terms.preLitigationPeriod | duration", "uk").value,
).toBe("60 днів");
expect(d("business-profile.documents/terms.terms.review | duration", "uk").value).toBe("3 дні");
expect(d("business-profile.documents/terms.terms.appeal | duration", "uk").value).toBe(
"1 день",
);
});
it("renders months, years, hours, weeks per output table", () => {
expect(d("business-profile.policies/price-changes.priceFixPeriod | duration").value).toBe(
"12 Monate",
);
expect(d("business-profile.policies/liability.cap.period | duration").value).toBe("2 Jahre");
expect(
d("business-profile.policies/exit-package.deliveryTarget.duration | duration").value,
).toBe("72 Stunden");
expect(d("business-profile.policies/renewal.reminder | duration").value).toBe("2 Wochen");
expect(d("business-profile.policies/price-changes.priceFixPeriod | duration", "uk").value).toBe(
"12 місяців",
);
});
it("renders compound durations descending without list commas (P1Y6M → '1 Jahr 6 Monate')", () => {
expect(d("business-profile.policies/renewal.renewalPeriod | duration").value).toBe(
"1 Jahr 6 Monate",
);
});
it("AC-4: non-ISO value fails with REF-xx error", () => {
const r = d("business-profile.policies/bad.note | duration");
expect(r.resolved).toBe(false);
expect(r.error).toMatch(/^REF-\d+/);
expect(r.error).toContain("thirty days");
});
it("AC-5: no-pipe expression returns the raw ISO value unchanged", () => {
const r = d("business-profile.policies/cancellation.ordinaryNotice.period");
expect(r.resolved).toBe(true);
expect(r.value).toBe("P30D");
});
it("AC-7: locale= param overrides context.lang", () => {
const r = d(
"business-profile.policies/cancellation.ordinaryNotice.period | duration locale=uk",
);
expect(r.resolved).toBe(true);
expect(r.value).toBe("30 днів");
});
it("style=short renders abbreviated units (de '14 T.', uk '14 дн.')", () => {
expect(
d("business-profile.policies/cancellation.paymentDefault.gracePeriod | duration style=short")
.value,
).toBe("14 T.");
expect(
d(
"business-profile.policies/cancellation.paymentDefault.gracePeriod | duration style=short",
"uk",
).value,
).toBe("14 дн.");
});
it("dayType=business renders Werktage / робочі дні", () => {
expect(
d(
"business-profile.policies/support-response.terms.responseWithin | duration dayType=business",
).value,
).toBe("5 Werktage");
expect(
d(
"business-profile.policies/support-response.terms.responseWithin | duration dayType=business",
"uk",
).value,
).toBe("5 робочих днів");
});
it("dayType=business on non-pure-days duration fails", () => {
const r = d("business-profile.policies/renewal.renewalPeriod | duration dayType=business");
expect(r.resolved).toBe(false);
expect(r.error).toMatch(/^REF-\d+/);
});
it("unknown param fails with REF-xx naming the param", () => {
const r = d("business-profile.policies/cancellation.ordinaryNotice.period | duration bogus=1");
expect(r.resolved).toBe(false);
expect(r.error).toMatch(/^REF-\d+/);
expect(r.error).toContain("bogus");
});
it("invalid style value fails", () => {
const r = d(
"business-profile.policies/cancellation.ordinaryNotice.period | duration style=narrow",
);
expect(r.resolved).toBe(false);
expect(r.error).toContain("narrow");
});
it("P0D rejected upstream — formatter errors on zero duration", () => {
expect(parseIsoDuration("P0D")).toBeNull();
expect(parseIsoDuration("P")).toBeNull();
expect(parseIsoDuration("P1Y6M")).toEqual({
years: 1,
months: 6,
weeks: 0,
days: 0,
hours: 0,
minutes: 0,
seconds: 0,
});
expect(parseIsoDuration("bogus")).toBeNull();
expect(parseIsoDuration(42)).toBeNull();
});
it("formatBusinessDays covers plural categories (de one/other, uk one/few/many)", () => {
expect(formatBusinessDays(1, "de")).toBe("1 Werktag");
expect(formatBusinessDays(5, "de")).toBe("5 Werktage");
expect(formatBusinessDays(1, "uk")).toBe("1 робочий день");
expect(formatBusinessDays(3, "uk")).toBe("3 робочі дні");
expect(formatBusinessDays(11, "uk")).toBe("11 робочих днів");
});
it("formatDuration falls back to PluralRules table if Intl.DurationFormat is absent", () => {
// Direct helper call — backend-independent contract surface.
expect(
formatDuration(
{ years: 0, months: 1, weeks: 0, days: 0, hours: 0, minutes: 0, seconds: 0 },
"de",
"long",
),
).toBe("1 Monat");
expect(
formatDuration(
{ years: 0, months: 1, weeks: 0, days: 0, hours: 0, minutes: 0, seconds: 0 },
"uk",
"long",
),
).toBe("1 місяць");
});
it("case=dative renders de plural -n (Tagen, Monaten, Jahren, Werktagen)", () => {
expect(
d("business-profile.policies/cancellation.ordinaryNotice.period | duration case=dative")
.value,
).toBe("30 Tagen");
expect(
d("business-profile.policies/price-changes.priceFixPeriod | duration case=dative").value,
).toBe("12 Monaten");
expect(d("business-profile.policies/liability.cap.period | duration case=dative").value).toBe(
"2 Jahren",
);
expect(
d(
"business-profile.policies/support-response.terms.responseWithin | duration dayType=business case=dative",
).value,
).toBe("5 Werktagen");
// Singular dative equals nominative for these nouns.
expect(d("business-profile.documents/terms.terms.appeal | duration case=dative").value).toBe(
"1 Tag",
);
});
it("case=accusative renders uk inanimate plurals as nominative (за 24 години)", () => {
expect(
d(
"business-profile.policies/exit-package.deliveryTarget.duration | duration case=accusative",
"uk",
).value,
).toBe("72 години");
expect(
d(
"business-profile.documents/terms.terms.preLitigationPeriod | duration case=accusative",
"uk",
).value,
).toBe("60 днів");
expect(
d("business-profile.documents/terms.terms.appeal | duration case=accusative", "uk").value,
).toBe("1 день");
});
it("case=genitive renders uk genitive forms (протягом 3 днів)", () => {
expect(
d("business-profile.documents/terms.terms.review | duration case=genitive", "uk").value,
).toBe("3 днів");
expect(
d("business-profile.documents/terms.terms.appeal | duration case=genitive", "uk").value,
).toBe("1 дня");
expect(
d(
"business-profile.policies/exit-package.deliveryTarget.duration | duration case=genitive",
"uk",
).value,
).toBe("72 годин");
expect(
d(
"business-profile.policies/support-response.terms.responseWithin | duration dayType=business case=genitive",
"uk",
).value,
).toBe("5 робочих днів");
});
it("case= is a no-op in short style (abbreviations do not inflect)", () => {
expect(
d(
"business-profile.policies/cancellation.paymentDefault.gracePeriod | duration style=short case=dative",
).value,
).toBe("14 T.");
});
it("invalid case value fails with REF-xx naming the value", () => {
const r = d(
"business-profile.policies/cancellation.ordinaryNotice.period | duration case=locative",
);
expect(r.resolved).toBe(false);
expect(r.error).toMatch(/^REF-\d+/);
expect(r.error).toContain("locative");
});
});