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:
co-authored by
Claude Opus 5.5
parent
60b5542b7d
commit
447a19b561
@@ -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,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");
|
||||
});
|
||||
|
||||
@@ -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;/);
|
||||
});
|
||||
Reference in New Issue
Block a user