feat(report): drop the report sky cover; docs, checklist and progress for M1–M5 (M5+M3)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N4f2nya58RoRu4yEmJgRGE
This commit is contained in:
Jesse_Chen
2026-09-29 08:41:27 +08:00
co-authored by Claude Opus 5.5
parent 60b5542b7d
commit 447a19b561
16 changed files with 236 additions and 357 deletions
+11 -55
View File
@@ -1453,7 +1453,6 @@ test("failed report reads aggregate existing section error codes without importi
// ---------------------------------------------------------------------------
const BIRTH_FP = "0123456789abcdef0123456789abcdef";
const OTHER_FP = "fedcba9876543210fedcba9876543210";
test("core create: inline generation writes subject.birthFingerprint from the stamped profile", async () => {
const persistence = new MemoryPersistence();
@@ -1471,24 +1470,18 @@ test("core create: inline generation writes subject.birthFingerprint from the st
assert.equal("birthFingerprint" in (unstamped.rows.get(REPORT_ID)?.reportDocument?.subject ?? {}), false);
});
async function readWithCover(input: Readonly<{
storedFingerprint: string | null;
chartProfileId?: string | null;
current: (userId: string, subjectId: string) => Promise<string | null>;
}>) {
// 原值:readWithCover + 四条 core read 测试(指纹一致 → subject id;不一致 / 缺字段 / 非本人 → null;响应不含指纹)
// 新值:一条测试——即使报告存了指纹,详情响应也没有 `coverSubject`,且不含指纹与出生字段
// 原因:TASK-mobile-chart-and-confirmed-edit-20260929 M5,产品 2026-09-29 决定去掉报告星图封面;F1 写入与隐私断言保留
test("core read: the detail response has no coverSubject, and never the fingerprint or birth fields", async () => {
const persistence = new MemoryPersistence();
const subject = {
displayName: "测试用户",
birthTimeStatus: "confirmed",
birthPlaceLabel: "北京",
...(input.storedFingerprint ? { birthFingerprint: input.storedFingerprint } : {}),
};
persistence.rows.set(REPORT_ID, {
...seedRecord(),
chartProfileId: input.chartProfileId ?? null,
reportDocument: { schemaVersion: "report_document.v2", subject } as unknown as PersonalReportRecord["reportDocument"],
reportDocument: {
schemaVersion: "report_document.v2",
subject: { displayName: "测试用户", birthTimeStatus: "confirmed", birthPlaceLabel: "北京", birthFingerprint: BIRTH_FP },
} as unknown as PersonalReportRecord["reportDocument"],
});
const asked: string[] = [];
const response = await resolveReportRead({
requestUrl: "https://jyotisha.chat/api/reports/x",
origin: null,
@@ -1498,47 +1491,10 @@ async function readWithCover(input: Readonly<{
persistence,
validateReadyDocument: acceptAnyDocument,
loadLongformAppendix: async () => ({ status: "ready", lastErrorCode: null, markdown: "# 报告\n\n正文。" }),
loadCurrentBirthFingerprint: async (userId, subjectId) => {
asked.push(`${userId}:${subjectId}`);
return input.current(userId, subjectId);
},
});
assert.equal(response.status, 200);
return { response, asked, report: response.body.report as Record<string, unknown> };
}
test("core read: coverSubject is the subject id when the stored fingerprint matches the current one", async () => {
const self = await readWithCover({ storedFingerprint: BIRTH_FP, current: async () => BIRTH_FP });
assert.equal(self.report.coverSubject, "self");
assert.deepEqual(self.asked, [`${UUID_A}:self`]);
const person = await readWithCover({ storedFingerprint: BIRTH_FP, chartProfileId: SESSION_ID, current: async () => BIRTH_FP });
assert.equal(person.report.coverSubject, SESSION_ID);
assert.deepEqual(person.asked, [`${UUID_A}:${SESSION_ID}`]);
// Serialized response: no fingerprint, no birth fields.
const body = JSON.stringify(self.response.body);
assert.equal("coverSubject" in (response.body.report as Record<string, unknown>), false);
const body = JSON.stringify(response.body);
assert.doesNotMatch(body, new RegExp(BIRTH_FP));
assert.doesNotMatch(body, /birthFingerprint|birth_date|latitude|longitude|timezone|birthPlaceLabel|北京/);
});
test("core read: coverSubject is null when the birth data changed since the report", async () => {
const { report, response } = await readWithCover({ storedFingerprint: BIRTH_FP, current: async () => OTHER_FP });
assert.equal(report.coverSubject, null);
assert.doesNotMatch(JSON.stringify(response.body), new RegExp(`${BIRTH_FP}|${OTHER_FP}`));
});
test("core read: coverSubject is null for a legacy report without the field, and nothing is looked up", async () => {
const { report, asked } = await readWithCover({ storedFingerprint: null, current: async () => BIRTH_FP });
assert.equal(report.coverSubject, null);
assert.deepEqual(asked, []);
});
test("core read: coverSubject is null for a subject that is not the reader's (foreign, deleted, unreadable)", async () => {
const foreign = await readWithCover({
storedFingerprint: BIRTH_FP,
chartProfileId: SESSION_ID,
current: async () => { throw new Error("subject_not_found"); },
});
assert.equal(foreign.report.coverSubject, null);
const missing = await readWithCover({ storedFingerprint: BIRTH_FP, current: async () => null });
assert.equal(missing.report.coverSubject, null);
assert.doesNotMatch(body, /coverSubject|birthFingerprint|birth_date|latitude|longitude|timezone|birthPlaceLabel|北京/);
});
+10 -16
View File
@@ -10,8 +10,6 @@ import { loadReportWorkerProfile } from "../src/lib/personal-report-worker-subje
import {
REPORT_BIRTH_FINGERPRINT_KEY,
readReportBirthFingerprint,
reportCoverSubject,
reportDocumentBirthFingerprint,
stampReportBirthFingerprint,
subjectBirthFingerprint,
withReportBirthFingerprint,
@@ -109,10 +107,11 @@ test("longform cover document carries the fingerprint and still parses; without
const fingerprint = subjectBirthFingerprint(storedSelf);
const bound = buildLongformCoverDocument({ ...input, birthFingerprint: fingerprint });
assert.equal(bound.subject.birthFingerprint, fingerprint);
assert.equal(reportDocumentBirthFingerprint(bound), fingerprint);
// 原值:另用 reportDocumentBirthFingerprint(bound / legacy) 读回 fingerprint / null
// 新值:只看 subject 上的字段(上下两行已断言)
// 原因:M5 去掉报告封面后没有读回方,只给比对用的 reportDocumentBirthFingerprint 已删除
const legacy = buildLongformCoverDocument(input);
assert.equal("birthFingerprint" in legacy.subject, false);
assert.equal(reportDocumentBirthFingerprint(legacy), null);
});
test("documents without the field (old reports) still parse; a malformed fingerprint does not", () => {
@@ -138,16 +137,11 @@ test("documents without the field (old reports) still parse; a malformed fingerp
assert.equal(withReportBirthFingerprint(document, undefined), document, "no fingerprint → untouched");
});
test("coverSubject: only a matching fingerprint yields the subject id, self when chart_profile_id is null", () => {
const fp = "a".repeat(32);
assert.equal(reportCoverSubject({ chartProfileId: null, storedFingerprint: fp, currentFingerprint: fp }), "self");
assert.equal(reportCoverSubject({ chartProfileId: "11111111-1111-4111-8111-111111111111", storedFingerprint: fp, currentFingerprint: fp }), "11111111-1111-4111-8111-111111111111");
assert.equal(reportCoverSubject({ chartProfileId: null, storedFingerprint: fp, currentFingerprint: "b".repeat(32) }), null);
assert.equal(reportCoverSubject({ chartProfileId: null, storedFingerprint: null, currentFingerprint: fp }), null);
assert.equal(reportCoverSubject({ chartProfileId: null, storedFingerprint: fp, currentFingerprint: null }), null);
});
test("GET /api/reports/:id compares on the server through the chart page's subject loader", () => {
assert.match(itemRouteSource, /loadCurrentBirthFingerprint: async \(ownerId: string, subjectId: string\) => \(\s*subjectBirthFingerprint\(\(await loadSubjectBirth\(supabase, ownerId, subjectId\)\)\.row\)/);
assert.match(routeCoreSource, /reportDocumentBirthFingerprint\(row\.reportDocument\)/);
// 原值:两条测试——`reportCoverSubject` 只在指纹一致时给出 subject id;详情路由经 `loadSubjectBirth` 在服务端比对
// 新值:一条测试——详情路由与路由核心不再查人物资料、不再比对、不下发 `coverSubject`;比对函数已删
// 原因:TASK-mobile-chart-and-confirmed-edit-20260929 M5,产品 2026-09-29 决定去掉报告星图封面(推翻 followup F2 / F3);F1 写入保留
test("GET /api/reports/:id no longer looks up the subject for a cover (M5)", () => {
assert.doesNotMatch(itemRouteSource, /loadCurrentBirthFingerprint|loadSubjectBirth|subjectBirthFingerprint/);
assert.doesNotMatch(routeCoreSource, /coverSubject|readyReportCoverSubject|loadCurrentBirthFingerprint/);
assert.match(routeCoreSource, /birthFingerprint: readReportBirthFingerprint\(profile\),/, "F1 still writes the fingerprint");
});
-81
View File
@@ -1,81 +0,0 @@
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import test from "node:test";
import { classifyReportEnvelope } from "../src/components/personal-report/personal-report-page.tsx";
import type { BirthSkyPaletteName } from "../src/lib/birth-sky/draw.ts";
import { loadReportSkyCover } from "../src/lib/birth-sky/report-cover.ts";
import { goldenSky } from "./birth-sky-test-support.ts";
// Report cover (TASK-birth-sky-followup-20260928 F3).
const page = readFileSync(new URL("../src/components/personal-report/personal-report-page.tsx", import.meta.url), "utf8");
const component = readFileSync(new URL("../src/components/birth-sky/report-sky-cover.tsx", import.meta.url), "utf8");
const css = readFileSync(new URL("../src/components/birth-sky/report-sky-cover.css", import.meta.url), "utf8");
function readyEnvelope(coverSubject?: unknown) {
return {
report: {
id: "11111111-1111-4111-8111-111111111111",
status: "ready",
createdAt: "2026-09-28T00:00:00.000Z",
...(coverSubject === undefined ? {} : { coverSubject }),
},
longformMarkdown: "# 报告\n\n正文。",
};
}
test("matching fingerprint → the ready state carries coverSubject; mismatch or legacy → no cover", () => {
const bound = classifyReportEnvelope(200, readyEnvelope("self"));
assert.equal(bound.phase, "markdown-ready");
assert.equal(bound.phase === "markdown-ready" && bound.coverSubject, "self");
for (const value of [null, undefined, "", 42]) {
const state = classifyReportEnvelope(200, readyEnvelope(value));
assert.equal(state.phase, "markdown-ready");
assert.equal("coverSubject" in state, false, `coverSubject=${String(value)}`);
}
});
test("the cover sits above the body, lazy, and only when coverSubject is set", () => {
assert.match(page, /\{state\.coverSubject \? <ReportSkyCover subjectId=\{state\.coverSubject\} \/> : null\}\s*<PersonalReportMarkdownView/);
assert.match(page, /const ReportSkyCover = dynamic\(\s*\(\) => import\("@\/components\/birth-sky\/report-sky-cover"\)/);
// No buttons, no placeholder, no waiting state.
assert.doesNotMatch(component, /<Button|<button|Spinner|skeleton|正在/i);
assert.match(component, /if \(!images\) return null;/, "nothing renders before both pictures are painted");
});
test("loader paints night for the screen and paper for print with the same sky", async () => {
const sky = goldenSky("day");
const painted: BirthSkyPaletteName[] = [];
const asked: string[] = [];
const pngs = await loadReportSkyCover("self", new AbortController().signal, {
fetchSky: async (subjectId) => { asked.push(subjectId); return sky; },
render: async (_sky, palette) => { painted.push(palette); return new Blob([palette]); },
});
assert.deepEqual(asked, ["self"]);
assert.deepEqual(painted.sort(), ["night", "paper"]);
assert.equal(await pngs?.screen.text(), "night");
assert.equal(await pngs?.print.text(), "paper");
assert.match(pngs?.description ?? "", /^星星都在,只是还看不见。|太阳/);
});
test("fetch failure, an empty sky or an abort → no cover, and nothing throws into the report", async () => {
const signal = new AbortController().signal;
const render = async () => new Blob(["x"]);
assert.equal(await loadReportSkyCover("self", signal, { fetchSky: async () => { throw new Error("offline"); }, render }), null);
assert.equal(await loadReportSkyCover("self", signal, { fetchSky: async () => null, render }), null);
assert.equal(await loadReportSkyCover("self", signal, { fetchSky: async () => goldenSky("day"), render: async () => { throw new Error("canvas"); } }), null);
const aborted = new AbortController();
aborted.abort();
assert.equal(await loadReportSkyCover("self", aborted.signal, { fetchSky: async () => goldenSky("day"), render }), null);
});
test("print: paper picture alone on the first A4 page; the screen picture is hidden", () => {
assert.match(component, /className="report-sky-cover-print" src=\{images\.print\}/);
assert.match(css, /\.report-sky-cover-print \{ display: none !important; \}/);
const print = css.slice(css.indexOf("@media print"));
assert.match(print, /\.report-sky-cover \{[^}]*break-after: page;/);
assert.match(print, /\.report-sky-cover-screen \{ display: none !important; \}/);
assert.match(print, /\.report-sky-cover-print \{[^}]*display: block !important;/);
assert.match(css, /aspect-ratio: 3 \/ 4;/);
});