import type { MessageBlock, ThreadMessage } from "@rakazo/contracts"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { i18n } from "./i18n.js"; import { intakeNoteText, localizedChoice, withIntakeNoteText } from "./onboarding-copy.js"; // The macros compile at build time; here they keep the English source. vi.mock("@lingui/core/macro", () => { const text = (parts: TemplateStringsArray, ...values: unknown[]) => parts.reduce( (out, part, index) => out + part + (index < values.length ? values[index] : ""), "", ); return { t: text, msg: (parts: TemplateStringsArray, ...values: unknown[]) => { const message = text(parts, ...values); return { id: message, message }; }, }; }); type Choice = Extract; type Note = Extract; // Intl may separate the time from AM/PM with a narrow no-break space. const plain = (text: string) => text.replace(/\s/g, " "); beforeEach(() => { i18n.load("en", {}); i18n.activate("en"); }); describe("localizedChoice", () => { it("says a known card from its step and option ids, whatever the server wrote", () => { const card: Choice = { kind: "choice", step: "rhythm", question: "server question", options: [ { id: "morning", letter: "A", label: "server morning" }, { id: "fortnightly", letter: "B", label: "Every other week" }, { id: "none", letter: "", label: "server none" }, ], }; expect(localizedChoice(card)).toEqual({ ...card, question: "When should I check in with you?", options: [ { id: "morning", letter: "A", label: "Every morning" }, // An option the web does not know keeps the server text. { id: "fortnightly", letter: "B", label: "Every other week" }, { id: "none", letter: "", label: "Not now" }, ], }); }); it("words the goal placeholder and keeps the server's quiet-hour ranges", () => { const goal = localizedChoice({ kind: "choice", step: "goal", question: "?", textInput: { placeholder: "server placeholder" }, options: [{ id: "skip", letter: "", label: "server skip" }], }); expect(goal.textInput).toEqual({ placeholder: "Your goal" }); expect(goal.options[0]?.label).toBe("Skip"); const quiet = localizedChoice({ kind: "choice", step: "quiet_hours", question: "?", options: [{ id: "22-07", letter: "A", label: "22:00 – 7:00" }], }); expect(quiet.options[0]?.label).toBe("22:00 – 7:00"); }); it("recognizes another bot's focus card and leaves other cards alone", () => { const focus: Choice = { kind: "choice", question: "server question", options: [ { id: "day", letter: "A", label: "x" }, { id: "crew", letter: "D", label: "y" }, ], }; expect(localizedChoice(focus).question).toBe("What do you want me on first?"); expect(localizedChoice(focus).options.map((option) => option.label)).toEqual([ "Day-to-day work", "Specialist crew", ]); const other: Choice = { kind: "choice", question: "Pick one", options: [{ id: "a", letter: "A", label: "Apples" }], }; expect(localizedChoice(other)).toBe(other); }); }); describe("intakeNoteText", () => { const now = new Date(2026, 9, 7, 10, 0); const note = (nextCheckInAt: Date | null): Note => ({ kind: "intake_note", note: "done", nextCheckInAt: nextCheckInAt?.toISOString() ?? null, text: "server text", }); it("names the first check-in relative to today, in the user's clock", () => { expect(plain(intakeNoteText(note(new Date(2026, 9, 7, 18, 0)), now))).toBe( "All set. I’ll check in today at 6:00 PM.", ); expect(plain(intakeNoteText(note(new Date(2026, 9, 8, 8, 0)), now))).toBe( "All set. I’ll check in tomorrow at 8:00 AM.", ); expect(plain(intakeNoteText(note(new Date(2026, 9, 11, 18, 0)), now))).toBe( "All set. I’ll check in on Sunday at 6:00 PM.", ); }); it("has a line for no check-in and falls back to the server text when unreadable", () => { expect(intakeNoteText(note(null), now)).toBe("All set. I’m here whenever you need me."); expect(intakeNoteText({ ...note(null), nextCheckInAt: "soon" }, now)).toBe("server text"); }); }); describe("withIntakeNoteText", () => { const message = (blocks: MessageBlock[]) => ({ id: "m", role: "bot", blocks, createdAt: "2026-10-07T08:00:00.000Z" }) as ThreadMessage; it("turns notes into text and leaves other messages untouched", () => { const plainMessage = message([{ kind: "text", text: "hi" }]); expect(withIntakeNoteText(plainMessage)).toBe(plainMessage); const withNote = message([ { kind: "intake_note", note: "done", nextCheckInAt: null, text: "server text" }, ]); expect(withIntakeNoteText(withNote).blocks).toEqual([ { kind: "text", text: "All set. I’m here whenever you need me." }, ]); }); });