Files
Jyotisha/frontend/tests/personal-report-longform-md.test.ts
T
Jesse_ChenandCursor e87c58d6e4 fix(report): persist longform appendix on self-hosted upsert (BUG-576)
Engine calls already returned markdown; the worker failed because local postgres upsert required onConflict and the appendix write omitted it.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-07 14:19:21 +08:00

163 lines
6.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import test from "node:test";
import React from "react";
import { renderToStaticMarkup } from "react-dom/server";
import { PersonalReportMarkdownView } from "../src/components/personal-report/personal-report-markdown-view.tsx";
import { buildLongformCoverDocument } from "../src/lib/personal-report-longform-cover.ts";
import {
PERSONAL_REPORT_EXPORT_LABEL,
PERSONAL_REPORT_GENERATING_COPY,
PERSONAL_REPORT_LEGACY_PLACEHOLDER,
} from "../src/lib/personal-report-longform-copy.ts";
import {
PERSONAL_REPORT_CARD_SUMMARY_MAX_CHARS,
buildLongformOutline,
extractLongformSummary,
personalReportMarkdownFilename,
} from "../src/lib/personal-report-longform-outline.ts";
import { PERSONAL_REPORT_WRITER_ENABLED } from "../src/lib/personal-report-writer-flag.ts";
Object.assign(globalThis, { React });
const SAMPLE_MARKDOWN = [
"# 个人长报告",
"",
"## 成品阅读导航",
"",
"导航正文 blocked",
"",
"## 摘要",
"",
"事业方向保持观察,不承诺日期。parameter_sensitive",
"",
"## 力量章",
"",
"### 子节",
"",
"后续章节",
"",
"## 质量验收矩阵",
"",
"| 项 | 状态 |",
"| --- | --- |",
"| MEVG | blocked |",
].join("\n");
const UNSAFE_MARKDOWN = [
SAMPLE_MARKDOWN,
"",
"## 摘要",
"",
"事业方向保持观察。",
"",
"<script>alert(1)</script>",
"<img src=\"https://evil.example/x.png\">",
"[bad](javascript:alert(1))",
"",
"| 月 | KP |",
"| --- | --- |",
"| 2026-01 | blocked |",
].join("\n");
test("report centre cards read the stored Markdown excerpt, not a writer summary field name", () => {
const listRoute = readFileSync(new URL("../src/app/api/reports/route.ts", import.meta.url), "utf8");
assert.match(listRoute, /"card_summary"/);
assert.doesNotMatch(listRoute, /report_document->|->>summary/);
const coverSource = readFileSync(
new URL("../src/lib/personal-report-longform-cover.ts", import.meta.url),
"utf8",
);
assert.match(coverSource, /extractLongformSummary/);
assert.match(coverSource, /PERSONAL_REPORT_CARD_SUMMARY_MAX_CHARS/);
});
test("writer pipeline stays in the tree but is feature-off", () => {
assert.equal(PERSONAL_REPORT_WRITER_ENABLED, false);
assert.equal(PERSONAL_REPORT_EXPORT_LABEL, "导出报告(.md");
assert.equal(PERSONAL_REPORT_LEGACY_PLACEHOLDER, "旧版本报告,请重新生成");
assert.match(PERSONAL_REPORT_GENERATING_COPY, /1030 秒/);
});
test("longform generate persists through PK upsert and logs engine timing without bodies", () => {
const generateSource = readFileSync(
new URL("../src/lib/personal-report-longform-generate.ts", import.meta.url),
"utf8",
);
assert.match(generateSource, /onConflict:\s*"report_id"/);
assert.match(generateSource, /\[personal-report\] engine http_status=/);
assert.match(generateSource, /duration_ms=/);
assert.doesNotMatch(generateSource, /console\.(?:info|error|log)\([^\n]*markdown/);
});
test("outline lifts navigation and summary to the first screen", () => {
const outline = buildLongformOutline(SAMPLE_MARKDOWN);
assert.ok(outline.headings.some((heading) => heading.title === "成品阅读导航"));
assert.ok(outline.headings.some((heading) => heading.title === "摘要"));
assert.match(outline.leadMarkdown, /成品阅读导航/);
assert.match(outline.leadMarkdown, /事业方向保持观察/);
assert.equal(outline.sections.some((section) => section.eager && section.title === "成品阅读导航"), true);
assert.equal(outline.sections.some((section) => section.eager && section.title === "摘要"), true);
});
test("card excerpt comes from the Markdown 摘要 section", () => {
const excerpt = extractLongformSummary(SAMPLE_MARKDOWN, 80);
assert.equal(PERSONAL_REPORT_CARD_SUMMARY_MAX_CHARS, 500);
assert.match(excerpt, /事业方向保持观察/);
assert.doesNotMatch(excerpt, /executiveSummary/);
assert.equal(personalReportMarkdownFilename("2026-09-06T08:00:00.000Z"), "个人报告-2026-09-06");
});
test("cover document parses and stores the Markdown excerpt", () => {
const document = buildLongformCoverDocument({
report: {
id: "123e4567-e89b-12d3-a456-426614174000",
reportType: "personal_full",
presentationMode: "default",
depth: "standard",
requestedThemes: ["career", "wealth"],
skillName: "jyotish-vedic-astrology",
skillVersion: "6.9.14",
skillSourceCommit: null,
skillSnapshotSha256: "ab".repeat(32),
},
subject: {
displayName: "测试",
birthTimeStatus: "accepted",
birthPlaceLabel: "测试地点",
},
markdown: SAMPLE_MARKDOWN,
generatedAt: "2026-09-06T00:00:00.000Z",
});
assert.equal(document.schemaVersion, "report_document.v2");
assert.match(document.executiveSummary.summary, /事业方向保持观察/);
assert.equal(document.blockedConflictDisclosure.length, 2);
assert.equal(document.thematicNarrative.length, 0);
});
test("markdown view escapes HTML, skips images, and wraps wide tables", () => {
const started = Date.now();
const markup = renderToStaticMarkup(React.createElement(PersonalReportMarkdownView, { markdown: UNSAFE_MARKDOWN }));
const elapsed = Date.now() - started;
assert.ok(elapsed < 1500, `first paint ${elapsed}ms`);
assert.match(markup, /personal-report-toc/);
assert.match(markup, /成品阅读导航/);
assert.match(markup, /parameter_sensitive/);
assert.match(markup, /blocked/);
assert.match(markup, /markdown-table personal-report-table-wrap/);
assert.doesNotMatch(markup, /<script/i);
assert.doesNotMatch(markup, /<img/i);
assert.doesNotMatch(markup, /javascript:/i);
assert.doesNotMatch(markup, /evil\.example/);
const viewSource = readFileSync(
new URL("../src/components/personal-report/personal-report-markdown-view.tsx", import.meta.url),
"utf8",
);
assert.match(viewSource, /skipHtml/);
assert.match(viewSource, /remarkGfm/);
assert.doesNotMatch(viewSource, /rehype-raw/);
assert.doesNotMatch(viewSource, /dangerouslySetInnerHTML/);
});