TASK-rectification-in-chat-step1-20261006, direct execution.
- The homepage 生时校正 pill and the text trigger (isRectificationHandoffQuestion)
are gone. The consult agent decides (RECTIFICATION_OFFER_RULE, one
definition) and calls offer-birth-time-rectification; the server decides
whether a card may show (self, product on, no case in progress,
feature_pricing price). The offer is stored with the answer and travels on
run.completed; the card hangs under that answer.
- Opening from the card binds the offer to the case in the consultation's own
messages (no table change); GET/POST /api/rectification/cases/[caseId]/source
find the source and go back: one divider (time changed or not), earlier
answers captioned 按校正前的时间, and a 按新时间重新看 button that sends only
when clicked. Auto-return after the post-adopt check or a terminal case, only
for work done in this view.
- Model history skips the divider and notes answers written with the old time.
Full suite 4987 / fail 24, identical to 6ec8efd5 (4973 / 24), no test name
lost; tsc 0; lint 0 errors; build keeps / Static, first-load gzip +0.20%.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8
170 lines
11 KiB
TypeScript
170 lines
11 KiB
TypeScript
import assert from "node:assert/strict";
|
||
import { readFileSync } from "node:fs";
|
||
import test from "node:test";
|
||
import React from "react";
|
||
|
||
import { BirthSkyEntry } from "../src/components/birth-sky/birth-sky-entry";
|
||
import { ChartSymbolHelp, TOUCH_BUBBLE_MS, placeBubble } from "../src/components/chart-page/chart-symbol-help";
|
||
import { VedicChartSvg } from "../src/components/personal-report/vedic-chart-svg";
|
||
import { assembleChartView } from "../src/lib/chart-view-load";
|
||
import type { ChartViewOk } from "../src/lib/chart-view-contract";
|
||
import { goldenSky } from "./birth-sky-test-support";
|
||
import { createClientLifecycleHarness } from "./react-client-lifecycle-test-support";
|
||
|
||
// Product feedback 2026-09-29 evening (BUG-1100..1103).
|
||
Object.assign(globalThis, { React });
|
||
const read = (path: string) => readFileSync(new URL(path, import.meta.url), "utf8");
|
||
const golden = JSON.parse(read("./fixtures/chart-view-golden.json"));
|
||
|
||
async function d1() {
|
||
const result = await assembleChartView({
|
||
userId: "synthetic", accountId: "synthetic", profileFingerprint: "fixture-v1",
|
||
profile: { name: "Synthetic", date: "1990-06-15", time: "12:00", placeLabel: "Synthetic", latitude: 39.9042, longitude: 116.4074, timezoneOffset: 8, timezoneId: "Asia/Shanghai", ayanamsa: "raman", birthTimeStatus: "confirmed" },
|
||
asOf: "2026-09-15", layers: ["varga"],
|
||
postEngine: async (path) => ({ status: "ok", payload: path === "/api/chart" ? golden.chart : golden.varga_full }),
|
||
});
|
||
return (result.body as ChartViewOk).vedic.vargas.find((varga) => varga.id === "D1")!.chart;
|
||
}
|
||
|
||
test("the bubble sits over the mark, flips below near the top and never leaves the stage (BUG-1101)", () => {
|
||
const stage = { left: 100, top: 50, width: 440 };
|
||
const middle = placeBubble({ left: 300, top: 250, width: 20, height: 20 }, stage);
|
||
assert.deepEqual(middle, { x: 210, y: 200, below: false });
|
||
const top = placeBubble({ left: 300, top: 60, width: 20, height: 20 }, stage);
|
||
assert.equal(top.below, true);
|
||
assert.equal(top.y, 30, "below the mark: its bottom edge");
|
||
assert.equal(placeBubble({ left: 102, top: 250, width: 10, height: 10 }, stage).x, 118, "clamped from the left edge");
|
||
assert.equal(placeBubble({ left: 530, top: 250, width: 10, height: 10 }, stage).x, 322, "clamped from the right edge");
|
||
});
|
||
|
||
test("hover shows a bubble at the mark; a tap keeps it past pointer-out and closes after 3s; no hint line (BUG-1101)", async () => {
|
||
const chart = await d1();
|
||
const h = createClientLifecycleHarness();
|
||
try {
|
||
await h.render(<ChartSymbolHelp resetKey="D1"><VedicChartSvg chart={chart} ariaLabel="测试盘" interactive /></ChartSymbolHelp>);
|
||
const stage = h.elements().find((node) => node.getAttribute("class") === "chart-page-symbol-help")!;
|
||
const bubble = () => h.elements().find((node) => node.getAttribute("role") === "tooltip");
|
||
const asc = h.elements().find((node) => node.getAttribute("data-chart-help") === "As · 上升")!;
|
||
assert.equal(bubble()?.text, undefined);
|
||
assert.ok(!h.container.text.includes("停在或轻点"), "the hint line is gone");
|
||
await h.event(stage, "onPointerOver", { target: asc, pointerType: "mouse" });
|
||
assert.equal(bubble()?.text, "As · 上升");
|
||
await h.event(stage, "onPointerOut", { target: asc, relatedTarget: null, pointerType: "mouse" });
|
||
assert.equal(bubble()?.text, undefined);
|
||
// Touch: pointer-over is ignored, the tap (pointer-up) opens, pointer-out and leave do not close.
|
||
await h.event(stage, "onPointerOver", { target: asc, pointerType: "touch" });
|
||
assert.equal(bubble()?.text, undefined);
|
||
await h.event(stage, "onPointerUp", { target: asc, pointerType: "touch" });
|
||
assert.equal(bubble()?.text, "As · 上升");
|
||
await h.event(stage, "onPointerOut", { target: asc, relatedTarget: null, pointerType: "touch" });
|
||
await h.event(stage, "onPointerLeave", { pointerType: "touch" });
|
||
assert.equal(bubble()?.text, "As · 上升");
|
||
await new Promise((resolve) => setTimeout(resolve, TOUCH_BUBBLE_MS + 50));
|
||
await h.idle();
|
||
assert.equal(bubble()?.text, undefined, "closes by itself");
|
||
assert.deepEqual(h.errors, []);
|
||
} finally { await h.close(); }
|
||
});
|
||
|
||
test("the chart card hugs the drawing on desktop (BUG-1100)", () => {
|
||
const css = read("../src/app/globals.css");
|
||
assert.match(css, /\.chart-page-vedic-stage \{ width: 100%; max-width: calc\(440px \+ 2 \* var\(--space-3\) \+ 2px\); \}/);
|
||
assert.doesNotMatch(read("../src/components/chart-page/chart-symbol-help.tsx"), /停在或轻点盘内标记可查看说明/);
|
||
});
|
||
|
||
test("the sky button shows while the cover paints and opens the moment it lands (BUG-1102)", async () => {
|
||
const h = createClientLifecycleHarness();
|
||
try {
|
||
await h.render(<BirthSkyEntry cover={null} />);
|
||
const button = h.elements().find((node) => node.tagName === "BUTTON")!;
|
||
assert.match(button.text, /那一刻的天空/);
|
||
await h.event(button);
|
||
// Remembered, nothing opened yet and nothing spinning.
|
||
assert.ok(!h.container.text.includes("正在加载"));
|
||
const cover = { key: "self:fp", sky: goldenSky("day"), blob: new Blob([], { type: "image/png" }), url: "blob:birth-sky", serif: false };
|
||
await h.render(<BirthSkyEntry cover={cover} />);
|
||
await h.idle();
|
||
assert.deepEqual(h.errors, []);
|
||
} finally { await h.close(); }
|
||
const view = read("../src/components/chart-page/chart-page-view.tsx");
|
||
assert.match(view, /\(birthSky \|\| birthSkyPending\) \? <BirthSkyEntry cover=\{birthSky \?\? null\} \/>/);
|
||
const hook = read("../src/hooks/use-birth-sky-cover.ts");
|
||
assert.match(hook, /pending: key !== null && current === null && settledKey !== key/);
|
||
});
|
||
|
||
test("report centre card: spaced from the header, title says what, button says do (BUG-1103)", () => {
|
||
const centre = read("../src/components/personal-report/personal-report-center.tsx");
|
||
assert.match(centre, /<h2 id="report-center-create-title">完整本命报告<\/h2>/);
|
||
assert.doesNotMatch(centre, /<h2[^>]*>生成个人报告/);
|
||
assert.match(read("../src/app/globals.css"), /\.report-center-create \{\n width: min\(900px, 100%\);\n(?: \/\*[\s\S]*?\*\/\n)? margin: var\(--space-10\) auto var\(--space-8\);/);
|
||
});
|
||
|
||
// Phone check 2026-09-30 (BUG-1106..1107).
|
||
test("opening a report shows one waiting screen, the same from the route and from the reader (BUG-1106)", () => {
|
||
const route = read("../src/app/(app)/reports/[reportId]/loading.tsx");
|
||
assert.match(route, /return <ReportOpening \/>;/);
|
||
assert.doesNotMatch(route, /InlineSpinner|<main/);
|
||
const page = read("../src/components/personal-report/personal-report-page.tsx");
|
||
assert.match(page, /if \(state\.phase === "loading"\) return <ReportOpening \/>;/);
|
||
const opening = read("../src/components/personal-report/report-opening.tsx");
|
||
assert.match(opening, /<SecondaryHeader title="个人报告" \/>/);
|
||
});
|
||
|
||
test("on phones the reader bar is one row and the export sheet leaves room for the list (BUG-1107)", () => {
|
||
const css = read("../src/app/globals.css");
|
||
const phone = css.slice(css.indexOf(" .personal-report-back-label { display: none; }") - 800, css.indexOf(" .personal-report-back-label { display: none; }") + 400);
|
||
assert.match(phone, /\.personal-report-action-end \{ flex-wrap: nowrap;/);
|
||
assert.match(read("../src/components/personal-report/report-actions.tsx"), /aria-label="返回报告中心"[\s\S]*<span className="personal-report-back-label">返回报告中心<\/span>/);
|
||
// The export sheet was redesigned the same night (two steps, no presets); its own tests are in report-export-drawer.test.tsx.
|
||
const drawer = read("../src/components/personal-report/report-export-drawer.tsx");
|
||
assert.match(drawer, /aria-label="关闭导出" title="关闭"><X aria-hidden="true" \/>/);
|
||
assert.doesNotMatch(drawer, /report-export-presets|常用|全部分盘|仅核对表/);
|
||
});
|
||
|
||
test("switching editions keeps the reader on the same chapter (BUG-1108)", () => {
|
||
const page = read("../src/components/personal-report/personal-report-page.tsx");
|
||
assert.match(page, /const chapter = currentChapterIndex\(\);\n setLanguage\(next\);/);
|
||
assert.match(page, /setDrawnThrough\(chapter\);/);
|
||
assert.match(page, /drawnThrough=\{drawnThrough\}/);
|
||
const view = read("../src/components/personal-report/personal-report-markdown-view.tsx");
|
||
assert.match(view, /drawn=\{drawnThrough !== null && chapter <= drawnThrough\}/);
|
||
assert.match(view, /useState\(section\.eager \|\| drawn\)/);
|
||
});
|
||
|
||
// Product feedback 2026-09-30 01:00.
|
||
test("the home no longer says an unfinished rectification is waiting in history", async () => {
|
||
// 原值: rectificationEntryHint(true) === null 新值: 首页校正提示函数随按钮一起删除 原因: BUG-1247(TASK-rectification-in-chat-step1-20261006 P3/C1):首页生时校正按钮与文字口令删除,校正只从对话里的提议卡进入
|
||
const starterHome = await import("../src/components/starter-home");
|
||
assert.equal("rectificationEntryHint" in starterHome, false);
|
||
assert.doesNotMatch(read("../src/components/starter-home.tsx"), /上次那次校正还没完成/);
|
||
});
|
||
|
||
test("chart-type cells show the whole meaning line (two lines, CJK-safe line height)", () => {
|
||
const css = read("../src/app/globals.css");
|
||
const small = css.slice(css.indexOf(".chart-page-type-cell small {"), css.indexOf("}", css.indexOf(".chart-page-type-cell small {")));
|
||
assert.match(small, /line-height: 1\.4/);
|
||
assert.match(small, /-webkit-line-clamp: 2/);
|
||
assert.doesNotMatch(small, /white-space: nowrap/);
|
||
});
|
||
|
||
test("chart, ephemeris and report waits show the orbit, the sentence only for screen readers", () => {
|
||
const orbit = read("../src/components/orbit-waiting.tsx");
|
||
assert.match(orbit, /<span className="sr-only">\{label\}<\/span>/);
|
||
assert.match(orbit, /app-loading-orbit/);
|
||
assert.match(read("../src/components/secondary-page-shell.tsx"), /<OrbitWaiting className="secondary-page-waiting" label=\{waiting\} \/>/);
|
||
// 原值: /<OrbitWaiting className="chart-page-skeleton-copy" label=\{copy\} \/>/ (orbit logo over the empty chart)
|
||
// 新值: no orbit over the chart; the sentence is an sr-only span inside the role=status stage
|
||
// 原因: 产品 2026-09-30「星盘的加载动画太丑了」,选「宫位依次点亮」——空盘本身就是等待动画
|
||
const chartWait = read("../src/components/chart-page/chart-page-skeleton.tsx");
|
||
assert.doesNotMatch(chartWait, /OrbitWaiting/);
|
||
assert.match(chartWait, /<span className="sr-only">\{copy\}<\/span>/);
|
||
assert.doesNotMatch(read("../src/components/chart-page/chart-page-skeleton.tsx"), /<p className="chart-page-note/);
|
||
});
|
||
|
||
test("the sky button carries a drawn constellation, not the sparkle", () => {
|
||
const entry = read("../src/components/birth-sky/birth-sky-entry.tsx");
|
||
assert.doesNotMatch(entry, /Sparkles/);
|
||
assert.match(entry, /<ConstellationIcon \/>/);
|
||
assert.equal((entry.match(/<circle /g) ?? []).length, 4);
|
||
});
|