Files
Jyotisha/frontend/tests/onboarding-presentation.test.ts
T
Jesse_Chen a12f57970b
Independent Staging Quality Gate / validate (pull_request) Successful in 9m16s
Independent Staging Quality Gate / publish (pull_request) Has been skipped
Independent Staging Quality Gate / validate (push) Successful in 9m44s
Independent Staging Quality Gate / publish (push) Successful in 8m41s
fix(home): request the daily card on the same condition that renders it
The daily starlanguage card sat on "正在结合你的星盘写今天的星语。" forever for
every account whose birth time was usable. Its effect bailed out on
birthTimeDisplayState(profile), which returns a value precisely when the
birth time is candidate, accepted or confirmed, so the request went out
only for accounts that had nothing to read. The guard predates the Agent
rewrite and was masked by the written-in client fallback that rewrite
deleted. It now gates on personalChartAvailable, the same fact the card
uses to render personal content, and retries once before admitting that
today has no card.

The route stops letting one engine call take the whole card down
silently: /api/chart fails into a named reason like the other four
layers, and the engine and agent budgets leave room for a cold chart and
an observed 30s generation inside the 60s ceiling.

The home also had three greeting implementations. The hero heading drew
from a static pool while the time-aware greeting lived elsewhere and the
Agent's own greeting was overwritten client-side into a field nothing
rendered. createStartGreeting now exposes its salutation and question
halves, the hero uses both, and the served greeting reaches the hero note.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-18 12:00:53 +08:00

76 lines
4.0 KiB
TypeScript

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 shows the Agent greeting rather than discarding it", () => {
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, /<p className="starter-greeting">\{starterGreeting\.salutation\}<\/p>/);
assert.match(source, /<h1 id="starter-heading">\{starterGreeting\.question\}<\/h1>/);
// Then: the served onboarding greeting reaches the hero note and is never overwritten locally.
assert.match(source, /starter-hero-note">\{onboarding\?\.greeting/);
assert.doesNotMatch(source, /greeting: createStartGreeting\(/);
// And: the separate static heading pool is gone for good.
assert.doesNotMatch(source, /starterPrompt|createStarterPrompt|greetingForHour/);
});