Late night no longer remarks on being awake (产品 2026-10-01); every daypart, weekday and returning line reworded in the same caring voice. Daily line: each house sentence names where the Moon is in plain words, then one concrete sentence; no stacked hedges. Sign-fallback moods rewritten the same way. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N4f2nya58RoRu4yEmJgRGE
192 lines
11 KiB
TypeScript
192 lines
11 KiB
TypeScript
import assert from "node:assert/strict";
|
||
import test from "node:test";
|
||
import { readFileSync } from "node:fs";
|
||
import {
|
||
createOnboardingFallbackGreeting,
|
||
createStartGreeting,
|
||
createStartGreetingParts,
|
||
} from "../src/lib/starter-greeting.ts";
|
||
import { homeSurface } from "./home-surface.ts";
|
||
|
||
// 2026-07-19 是周日,20 周一,22 周三(星期池为空),24 周五,25 周六。
|
||
const SUNDAY = (hour: number) => new Date(2026, 6, 19, hour);
|
||
const MONDAY = (hour: number) => new Date(2026, 6, 20, hour);
|
||
const WEDNESDAY = (hour: number) => new Date(2026, 6, 22, hour);
|
||
const FRIDAY = (hour: number) => new Date(2026, 6, 24, hour);
|
||
|
||
test("terminal and fallback greetings follow the current profile name", () => {
|
||
// 原值: 同时锁 onboardingRequestIdentity / isCurrentOnboardingRequest,避免建议问题串号。
|
||
// 新值: 只保留问候语随当前称呼变化。
|
||
// 原因: 建议问题请求已删除,身份去重不再有消费点。
|
||
//
|
||
// 原值 `createStartGreeting("周宁", …, 0)` + `"周宁,从你此刻最关心的问题开始吧。"`
|
||
// / 新值 `createStartGreeting("周宁", …, 0.25)` + `"欢迎,周宁"`
|
||
// / 原因:时段池现在第一条是不带称呼的「早上好」,要取带称呼的条目得换下标;
|
||
// 兜底问候按产品口径改成单行「欢迎,{名字}」,不再追问。断言强度没变:
|
||
// 两条都仍然必须带当前称呼、不得带旧称呼。
|
||
const terminalGreeting = createStartGreeting("周宁", SUNDAY(8), 0.25);
|
||
const fallbackGreeting = createOnboardingFallbackGreeting("周宁");
|
||
for (const greeting of [terminalGreeting, fallbackGreeting]) {
|
||
assert.match(greeting, /周宁/);
|
||
assert.doesNotMatch(greeting, /林遥/);
|
||
}
|
||
// 原值 "早上好,周宁" / 新值 "早安,周宁" / 原因:开场语改知心姐姐口吻(产品 2026-10-01)。
|
||
assert.equal(terminalGreeting, "早安,周宁");
|
||
assert.equal(fallbackGreeting, "欢迎,周宁");
|
||
});
|
||
|
||
test("the starter greeting is one line drawn from daypart, weekday and returning pools", () => {
|
||
// 原值:`parts.salutation` = "中午好,周宁。"、`parts.question` = "现在最想理清哪件事?",
|
||
// 并断言两半拼起来等于整行、每个时段至少 12 条不同问句。
|
||
// 新值:`parts.text` 就是整行问候;断言时段池按下标取到「中午好」/「中午好,周宁」。
|
||
// 原因:产品把开场语改成 claude.ai 式的单行问候,追问句移到输入框占位符,
|
||
// 「招呼语 + 问句」两半这个形状本身被取消,问句池不再存在。
|
||
const noon = WEDNESDAY(12);
|
||
// 原值 "中午好" / "中午好,周宁" / 新值 "中午好,记得吃饭" / "午安,周宁"
|
||
// / 原因:开场语改知心姐姐口吻(产品 2026-10-01);下标与池长不变。
|
||
assert.equal(createStartGreetingParts("周宁", noon, 0).text, "中午好,记得吃饭");
|
||
assert.equal(createStartGreetingParts("周宁", noon, 0.5).text, "午安,周宁");
|
||
assert.equal(createStartGreeting("周宁", noon, 0.5), createStartGreetingParts("周宁", noon, 0.5).text);
|
||
|
||
// 边界不越界:1 与 0.999999 都落在池内最后一条。
|
||
assert.equal(createStartGreetingParts("周宁", noon, 1).text, "午安,周宁");
|
||
assert.equal(createStartGreetingParts("周宁", noon, 0.999999).text, "午安,周宁");
|
||
});
|
||
|
||
test("late night has its own three lines and none of them remarks on being awake", () => {
|
||
// 原值 "夜猫子,晚上好" / "还没睡呀,周宁" / "晚安前再聊一会"
|
||
// / 新值 "这会儿很安静" / "夜里好,周宁" / "慢慢说,周宁"
|
||
// / 原因:产品 2026-10-01「夜猫子、还不睡,用户会觉得你管别人睡不睡」;断言加严:深夜池任何一条都不得提睡觉。
|
||
const lateNight = WEDNESDAY(2);
|
||
assert.equal(createStartGreetingParts("周宁", lateNight, 0).text, "这会儿很安静");
|
||
assert.equal(createStartGreetingParts("周宁", lateNight, 1 / 3).text, "夜里好,周宁");
|
||
assert.equal(createStartGreetingParts("周宁", lateNight, 0.7).text, "慢慢说,周宁");
|
||
// 23 点与 4 点同属深夜。
|
||
assert.equal(createStartGreetingParts("周宁", WEDNESDAY(23), 1 / 3).text, "夜里好,周宁");
|
||
assert.equal(createStartGreetingParts("周宁", WEDNESDAY(4), 1 / 3).text, "夜里好,周宁");
|
||
for (let step = 0; step < 100; step += 1) {
|
||
assert.doesNotMatch(createStartGreetingParts("周宁", lateNight, step / 100).text, /睡|夜猫子|晚安/);
|
||
}
|
||
});
|
||
|
||
test("a blank name leaves only the lines that carry no name", () => {
|
||
const lateNight = WEDNESDAY(2);
|
||
// 原值 "夜猫子,晚上好" / "晚安前再聊一会" / 新值 "这会儿很安静"(深夜池唯一不带称呼的一条)
|
||
// / 原因:深夜池换句(产品 2026-10-01)。
|
||
assert.equal(createStartGreetingParts("", lateNight, 0).text, "这会儿很安静");
|
||
assert.equal(createStartGreetingParts(" ", lateNight, 0.9).text, "这会儿很安静");
|
||
|
||
// 原值:断言名字为空时不出现「周一了」/ 新值:只断言带称呼的回访条目不可达
|
||
// / 原因:周一条目改成不带称呼的「周一了,别太累」,名字为空也该出现;带称呼条目不可达的断言保留。
|
||
for (let step = 0; step < 100; step += 1) {
|
||
for (const hasSessions of [false, true]) {
|
||
const text = createStartGreetingParts("", MONDAY(9), step / 100, hasSessions).text;
|
||
assert.doesNotMatch(text, /又见面了|你来啦|最近还好吗/);
|
||
assert.ok(text.length > 0);
|
||
assert.doesNotMatch(text, /\{名字\}/);
|
||
assert.doesNotMatch(text, /,$|^,/);
|
||
}
|
||
}
|
||
});
|
||
|
||
test("the weekday pool only opens on its own day", () => {
|
||
const friday = FRIDAY(9);
|
||
const fridayLines = new Set(Array.from({ length: 100 }, (_, step) =>
|
||
createStartGreetingParts("周宁", friday, step / 100).text));
|
||
// 原值 "周五快乐" / "周一了,周宁" / 新值 "周五了,松口气" / "周一了,别太累" / 原因:知心姐姐口吻(产品 2026-10-01)。
|
||
assert.ok(fridayLines.has("周五了,松口气"));
|
||
|
||
const wednesdayLines = new Set(Array.from({ length: 100 }, (_, step) =>
|
||
createStartGreetingParts("周宁", WEDNESDAY(9), step / 100).text));
|
||
assert.ok(!wednesdayLines.has("周五了,松口气"));
|
||
assert.equal(wednesdayLines.size, 3);
|
||
|
||
assert.ok(new Set(Array.from({ length: 100 }, (_, step) =>
|
||
createStartGreetingParts("周宁", MONDAY(9), step / 100).text)).has("周一了,别太累"));
|
||
assert.ok(new Set(Array.from({ length: 100 }, (_, step) =>
|
||
createStartGreetingParts("周宁", SUNDAY(9), step / 100).text)).has("周日,慢一点"));
|
||
});
|
||
|
||
test("the returning pool needs at least one session behind the account", () => {
|
||
const evening = WEDNESDAY(20);
|
||
// 原值 "嗨,周宁" / 新值 "你来啦,周宁" / 原因:知心姐姐口吻(产品 2026-10-01);池长不变。
|
||
const returning = ["又见面了,周宁", "你来啦,周宁", "欢迎回来", "最近还好吗,周宁?"];
|
||
|
||
const firstVisit = new Set(Array.from({ length: 100 }, (_, step) =>
|
||
createStartGreetingParts("周宁", evening, step / 100, false).text));
|
||
for (const line of returning) assert.ok(!firstVisit.has(line), `first visit must not reach ${line}`);
|
||
assert.equal(firstVisit.size, 3);
|
||
|
||
const returnVisit = new Set(Array.from({ length: 100 }, (_, step) =>
|
||
createStartGreetingParts("周宁", evening, step / 100, true).text));
|
||
for (const line of returning) assert.ok(returnVisit.has(line), `return visit must reach ${line}`);
|
||
assert.equal(returnVisit.size, 7);
|
||
});
|
||
|
||
test("the same inputs always produce the same line", () => {
|
||
for (const hour of [2, 8, 12, 16, 20]) {
|
||
for (const hasSessions of [false, true]) {
|
||
for (const step of [0, 17, 42, 99]) {
|
||
const args = [hour, step / 100, hasSessions] as const;
|
||
const first = createStartGreeting("周宁", WEDNESDAY(args[0]), args[1], args[2]);
|
||
const second = createStartGreeting("周宁", WEDNESDAY(args[0]), args[1], args[2]);
|
||
assert.equal(first, second);
|
||
}
|
||
}
|
||
}
|
||
});
|
||
|
||
test("no greeting line uses a metaphor or trails off into a sentence", () => {
|
||
for (let day = 19; day <= 25; day += 1) {
|
||
for (let hour = 0; hour < 24; hour += 1) {
|
||
for (const name of ["周宁", ""]) {
|
||
for (const hasSessions of [false, true]) {
|
||
for (let step = 0; step < 100; step += 1) {
|
||
const text = createStartGreetingParts(
|
||
name,
|
||
new Date(2026, 6, day, hour),
|
||
step / 100,
|
||
hasSessions,
|
||
).text;
|
||
assert.doesNotMatch(text, /[星月陪]/, `metaphor in ${text}`);
|
||
assert.doesNotMatch(text, /。$/, `sentence period in ${text}`);
|
||
assert.ok(text.length > 0);
|
||
assert.ok(text.length <= 12, `${text} is longer than one short line`);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
});
|
||
|
||
test("the starter hero is one heading and nothing else", () => {
|
||
const source = homeSurface;
|
||
|
||
// 原值 `className="starter-greeting"` / 新值 `className="starter-salutation"`
|
||
// / 原因:`.starter-greeting` 这个类名此前既指招呼语那一行、又被读成整块问候区,
|
||
// 拆成 `.starter-greeting-block`(容器)与 `.starter-salutation`(招呼语)后不再歧义。
|
||
//
|
||
// 原值:断言 `<p className="starter-salutation">` 与 `<h1>{starterGreeting.question}</h1>` 同时存在
|
||
// / 新值:断言只剩 `<h1 id="starter-heading">{starterGreeting.text}</h1>`,且 `starter-salutation` 在源码与样式里都消失
|
||
// / 原因:开场语改成单行问候,副标题那一行连同它的样式一起下线;断言从「两行」改成「一行且不得有第二行」,强度不降反升。
|
||
assert.match(source, /<h1 id="starter-heading">\{starterGreeting\.text\}<\/h1>/);
|
||
assert.doesNotMatch(source, /starter-salutation/);
|
||
assert.doesNotMatch(source, /starterGreeting\.(salutation|question)/);
|
||
|
||
// Then: no third line repeats the welcome the heading already carries.
|
||
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/);
|
||
assert.doesNotMatch(styles, /starter-salutation/);
|
||
|
||
// And: the separate static heading pool is gone for good.
|
||
assert.doesNotMatch(source, /starterPrompt|createStarterPrompt|greetingForHour/);
|
||
assert.doesNotMatch(source, /greeting: createStartGreeting\(/);
|
||
});
|
||
|
||
test("the greeting knows whether the account already has sessions", () => {
|
||
// hasSessions 必须来自已有的会话列表变量,不得新开一份状态。
|
||
assert.match(homeSurface, /const starterHasPriorSessions = sessions\.some\(/);
|
||
assert.match(homeSurface, /createStartGreetingParts\(profile\.name, new Date\(\), starterGreetingSelection, starterHasPriorSessions\)/);
|
||
});
|