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
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>
76 lines
4.0 KiB
TypeScript
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/);
|
|
});
|