New reports skip the writer, persist pl9 Markdown as the body, and settle zero-token usage on the catalog model. Planned longform sections now emit blocked rows instead of vanishing. Co-authored-by: Cursor <cursoragent@cursor.com>
148 lines
5.3 KiB
TypeScript
148 lines
5.3 KiB
TypeScript
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 {
|
||
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:report_document->executiveSummary->>summary/);
|
||
const coverSource = readFileSync(
|
||
new URL("../src/lib/personal-report-longform-cover.ts", import.meta.url),
|
||
"utf8",
|
||
);
|
||
assert.match(coverSource, /extractLongformSummary/);
|
||
});
|
||
|
||
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, /10–30 秒/);
|
||
});
|
||
|
||
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.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/);
|
||
});
|