import assert from "node:assert/strict"; import test from "node:test"; import { readFileSync } from "node:fs"; import { createOnboardingFallbackGreeting, createStartGreeting, createStartGreetingParts, isCurrentOnboardingRequest, onboardingProfileFingerprint, onboardingRequestIdentity, } from "../src/lib/onboarding-client.ts"; const completeProfile = { name: "林遥", date: "1990-06-15", time: "12:30", reportedTime: "12:30", birthTimeSource: "hospital_record", birthTimePeriod: "", birthTimeClue: "出生证明", birthTimeStatus: "confirmed", uncertaintyBeforeMinutes: 0, uncertaintyAfterMinutes: 0, rectificationCaseId: "", countryCode: "CN", provinceCode: "110000", cityCode: "110000-city", districtCode: "110101", } as const; test("rejects stale A presentation and derives terminal and fallback greetings from profile B", () => { // Given: one account starts onboarding for a complete persisted profile A. const firstIdentity = onboardingRequestIdentity("account-1", onboardingProfileFingerprint(completeProfile)); // When: the name changes before that request completes and profile B is presented. const changedProfile = { ...completeProfile, name: "周宁" }; const currentIdentity = onboardingRequestIdentity("account-1", onboardingProfileFingerprint(changedProfile)); const terminalGreeting = createStartGreeting(changedProfile.name, new Date("2026-07-19T08:00:00+08:00"), 0); const fallbackGreeting = createOnboardingFallbackGreeting(changedProfile.name); // Then: stale work is rejected and neither presentation path can retain A's name. assert.notEqual(currentIdentity, firstIdentity); assert.equal(isCurrentOnboardingRequest(currentIdentity, firstIdentity), false); for (const greeting of [terminalGreeting, fallbackGreeting]) { assert.match(greeting, /周宁/); assert.doesNotMatch(greeting, /林遥/); } assert.equal(fallbackGreeting, "周宁,从你此刻最关心的问题开始吧。"); }); test("the starter hero splits one time-aware greeting instead of drawing from a separate pool", () => { // Given: the same daypart variant, asked for as parts and as one line. const noon = new Date("2026-07-19T12:00:00+08:00"); const parts = createStartGreetingParts("周宁", noon, 0); const whole = createStartGreeting("周宁", noon, 0); // Then: the salutation carries the name and the question carries no name, and they recompose exactly. assert.equal(parts.salutation, "中午好,周宁。"); assert.equal(parts.question, "现在最想理清哪件事?"); assert.equal(`${parts.salutation}${parts.question}`, whole); assert.doesNotMatch(parts.question, /周宁/); // And: the extremes stay inside the variant list rather than falling off the end. assert.equal(createStartGreetingParts("周宁", noon, 1).question, "事业、关系或选择,想先聊哪个?"); assert.equal(createStartGreetingParts("周宁", noon, 0.999999).question, "事业、关系或选择,想先聊哪个?"); // And: every daypart is reachable, so the heading is not one fixed line in disguise. const questions = new Set([0, 8, 12, 16, 20, 23].flatMap((hour) => [0, 1 / 3, 2 / 3].map((selection) => createStartGreetingParts("周宁", new Date(2026, 6, 19, hour), selection).question))); assert.ok(questions.size >= 12); }); test("the starter hero stays at a salutation and a question", () => { const source = readFileSync(new URL("../src/app/page.tsx", import.meta.url), "utf8"); // Given: the hero heading is the question half of the time-aware greeting. assert.match(source, /

\{starterGreeting\.salutation\}<\/p>/); assert.match(source, /

\{starterGreeting\.question\}<\/h1>/); // Then: no third line repeats the welcome the two lines above already carry. assert.doesNotMatch(source, /starter-hero-note/); const styles = readFileSync(new URL("../src/app/globals.css", import.meta.url), "utf8"); assert.doesNotMatch(styles, /starter-hero-note/); // And: the separate static heading pool is gone for good. assert.doesNotMatch(source, /starterPrompt|createStarterPrompt|greetingForHour/); assert.doesNotMatch(source, /greeting: createStartGreeting\(/); });