Files
Jyotisha/frontend/tests/report-public-projection.test.ts
T
jesse-ux 2a1f5f0fc9 fix(report): use the upstream reader edition for new longform bodies
New reports request reader_main from upstream origin/main 23b9609e.
KP and transit no longer copy an empty vars() dict, solar returns keep
birth_asc_sign_idx, and ordinary projection deletes internal lines whole.
2026-09-25 02:19:32 +08:00

395 lines
15 KiB
TypeScript

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 { consultationReportMarkdown } from "../src/lib/consultation-report-export.ts";
import { ordinaryReportDownloadMarkdown } from "../src/lib/personal-report-longform-download.ts";
import { resolveReportRead } from "../src/lib/personal-report-route-core.ts";
import type { PersonalReportRecord } from "../src/lib/personal-report-service-core.ts";
import { stripReportChartBlocks } from "../src/lib/report-chart-block.ts";
import {
INTERNAL_REPORT_FIELDS,
ORDINARY_LIMITATION_COPY,
ORDINARY_PUBLIC_FIELDS,
REPORT_DOCUMENT_KINDS,
ordinaryOutputLeaks,
projectChatExportMarkdown,
projectOrdinaryReportDocument,
projectOrdinaryReportMarkdown,
projectOrdinarySnippet,
releaseProfessionalReference,
} from "../src/lib/report-public-projection.ts";
const CLEAN_MARKDOWN = "# 长报告\n\n### 摘要\n正文";
const CHART_FENCE = [
"事业先看阶段。",
"",
"```jyotish-chart",
JSON.stringify({
version: 1,
id: "D1",
title: "本命",
layout: "north",
ascendant: { sign: "Aries", degree: 1 },
planets: [],
}),
"```",
"",
"<svg viewBox=\"0 0 420 480\"></svg>",
].join("\n");
const CACHED_MARKDOWN = [
"# 旧缓存报告",
"",
"事业先看阶段。",
"",
"## Claim boundary",
"",
"technique_truth: partial",
"workflow_route: career",
"workflow_status: degraded",
"precise_timing: blocked",
"missing_layers: MEVG",
"score: 0.82",
"weight: 1.4",
"job_id: job-1",
"attempt_count: 3",
"provider: openai",
"model_debug: true",
"tool_call_id: call_1",
"",
"## 质量验收矩阵",
"",
"| 项 | 状态 |",
"| --- | --- |",
"| MEVG | blocked |",
"",
"## 摘要",
"",
"不承诺具体日期。",
].join("\n");
const UNSAFE_MARKDOWN = [
"事业方向保持观察。正文里夹了 <script>alert(2)</script> 还能读。",
"",
"<script>alert(1)</script>",
"<iframe src=\"https://evil.example/frame\"></iframe>",
"<object data=\"https://evil.example/o\"></object>",
"<embed src=\"https://evil.example/e\">",
"<img src=\"https://evil.example/x.png\" alt=\"图\">",
"[bad](javascript:alert(1))",
"[file](file:///etc/passwd)",
"见 http://127.0.0.1:5200/api/secret 与 http://localhost:3000/hidden",
"sk-testsecretvalue",
"provider_payload: {\"model\":\"hidden\"}",
"Bearer abcdefghijklmnop",
].join("\n");
test("ordinary fields are an allowlist and internal fields stay classified", () => {
assert.deepEqual(ORDINARY_PUBLIC_FIELDS, [
"title",
"prose",
"conclusion",
"action",
"limitation",
"chart_fence",
"engine_svg",
]);
assert.ok(INTERNAL_REPORT_FIELDS.includes("technique_truth"));
assert.ok(INTERNAL_REPORT_FIELDS.includes("workflow_route"));
assert.ok(INTERNAL_REPORT_FIELDS.includes("secret"));
assert.ok(INTERNAL_REPORT_FIELDS.includes("provider"));
assert.deepEqual(REPORT_DOCUMENT_KINDS, [
"chat_export",
"personal_report_detail",
"ordinary_markdown_download",
"professional_reference",
]);
});
test("clean markdown is unchanged and absent fields are not invented", () => {
assert.equal(projectOrdinaryReportMarkdown(CLEAN_MARKDOWN), CLEAN_MARKDOWN);
assert.equal(projectOrdinarySnippet("事业方向保持观察"), "事业方向保持观察");
const report = projectChatExportMarkdown({
documentKind: "chat_export",
title: "空",
prose: "只有正文。",
});
assert.match(report, /只有正文/);
assert.doesNotMatch(report, /需要知道的限制/);
assert.doesNotMatch(report, /technique_truth|unknown|Claim boundary/);
assert.equal(ordinaryOutputLeaks(report).length, 0);
});
test("chat export rewrites limitation signals and hides internal keys", () => {
const report = consultationReportMarkdown({
title: "事业咨询",
messages: [
{ role: "user", text: "未来一年事业如何?" },
{
role: "assistant",
text: "先看阶段,不承诺具体日期。",
techniqueTruth: "partial",
workflowReceipt: {
route: "career",
status: "ready",
preciseTiming: "blocked",
missingLayers: ["MEVG"],
},
},
],
});
assert.match(report, /先看阶段/);
assert.match(report, new RegExp(ORDINARY_LIMITATION_COPY.preciseTiming));
assert.match(report, new RegExp(ORDINARY_LIMITATION_COPY.missingEvidence));
assert.match(report, new RegExp(ORDINARY_LIMITATION_COPY.techniqueOpen));
assert.equal(ordinaryOutputLeaks(report).length, 0);
assert.doesNotMatch(report, /career/);
});
test("old cached markdown cannot bypass the ordinary projection", () => {
const projected = projectOrdinaryReportMarkdown(CACHED_MARKDOWN);
assert.match(projected, /事业先看阶段/);
assert.match(projected, /不承诺具体日期/);
assert.match(projected, new RegExp(ORDINARY_LIMITATION_COPY.preciseTiming));
assert.doesNotMatch(projected, /Claim boundary|质量验收矩阵/);
assert.equal(ordinaryOutputLeaks(projected).length, 0, ordinaryOutputLeaks(projected).join(", "));
assert.equal(projectOrdinaryReportMarkdown(projected), projected);
});
test("professional reference is an explicit kind and still fail-safes without a separate grant", () => {
const released = releaseProfessionalReference(CACHED_MARKDOWN);
assert.equal(released.documentKind, "professional_reference");
assert.equal(released.format, "markdown");
assert.equal(ordinaryOutputLeaks(released.markdown).length, 0);
assert.notEqual(released.documentKind, "ordinary_markdown_download");
const routeSource = readFileSync(
new URL("../src/app/api/reports/[reportId]/professional-reference/route.ts", import.meta.url),
"utf8",
);
assert.match(routeSource, /releaseProfessionalReference/);
assert.doesNotMatch(routeSource, /professionalGrant/);
const downloadSource = readFileSync(
new URL("../src/lib/personal-report-longform-download.ts", import.meta.url),
"utf8",
);
assert.doesNotMatch(downloadSource, /professional-reference/);
});
test("chart fences stay in ordinary reading output and download still strips them", () => {
const projected = projectOrdinaryReportMarkdown(CHART_FENCE);
assert.match(projected, /```jyotish-chart/);
assert.match(projected, /"id":"D1"/);
assert.match(projected, /<svg viewBox="0 0 420 480"><\/svg>/);
const downloaded = ordinaryReportDownloadMarkdown(CHART_FENCE);
assert.equal(downloaded, stripReportChartBlocks(projected));
assert.doesNotMatch(downloaded, /```jyotish-chart/);
assert.match(downloaded, /<svg viewBox="0 0 420 480"><\/svg>/);
const engineSvg = [
"图如下。",
"",
"<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 420 480\" style=\"font-family:Arial,sans-serif\">",
"<rect width=\"10\" height=\"10\" fill=\"#fff\"/>",
"<text>日</text>",
"</svg>",
].join("\n");
const keptSvg = projectOrdinaryReportMarkdown(engineSvg);
assert.match(keptSvg, /<svg[\s\S]*<rect[\s\S]*日[\s\S]*<\/svg>/);
const poisoned = projectOrdinaryReportMarkdown([
"```jyotish-chart",
"{\"technique_truth\":\"partial\"}",
"```",
].join("\n"));
assert.doesNotMatch(poisoned, /jyotish-chart|technique_truth/);
});
test("ordinary output drops HTML, dangerous URLs, secrets, and model fields", () => {
const projected = projectOrdinaryReportMarkdown(UNSAFE_MARKDOWN);
assert.match(projected, /事业方向保持观察/);
assert.match(projected, /还能读/);
assert.equal(ordinaryOutputLeaks(projected).length, 0, ordinaryOutputLeaks(projected).join(", "));
assert.doesNotMatch(projected, /evil\.example|alert\(1\)|alert\(2\)|sk-testsecretvalue|abcdefghijklmnop/);
const svgScript = projectOrdinaryReportMarkdown("<svg><script>alert(1)</script></svg>");
assert.doesNotMatch(svgScript, /<script|alert\(1\)|<svg/);
});
test("report detail and read API project stored markdown and structured documents", async () => {
const detail = classifyReportEnvelope(200, {
report: {
id: "11111111-1111-4111-8111-111111111111",
status: "ready",
createdAt: "2026-09-22T00:00:00.000Z",
},
longformMarkdown: CACHED_MARKDOWN,
});
assert.equal(detail.phase, "markdown-ready");
if (detail.phase === "markdown-ready") {
assert.match(detail.markdown, /事业先看阶段/);
assert.equal(ordinaryOutputLeaks(detail.markdown).length, 0);
}
const leakedDocument = {
schemaVersion: "report_document.v2",
executiveSummary: {
headline: "方向",
summary: "先看阶段。",
priorities: ["先观察"],
},
evidenceAppendix: {
techniqueAudit: [{ techniqueName: "MEVG", status: "blocked", score: 0.2 }],
},
provenance: {
skillSnapshotSha256: "ab".repeat(32),
calculationHash: "cd".repeat(32),
provider: "openai",
},
disclaimer: "不把未闭合的判断写成确定结论。",
};
const projectedDocument = projectOrdinaryReportDocument(leakedDocument);
assert.equal(JSON.stringify(projectedDocument).includes("evidenceAppendix"), false);
assert.equal(JSON.stringify(projectedDocument).includes("techniqueAudit"), false);
assert.equal(JSON.stringify(projectedDocument).includes("skillSnapshotSha256"), false);
assert.equal(ordinaryOutputLeaks(JSON.stringify(projectedDocument)).length, 0);
const response = await resolveReportRead({
requestUrl: "https://jyotisha.chat/api/reports/x",
origin: null,
allowedOrigins: [],
userId: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
reportId: "11111111-1111-4111-8111-111111111111",
persistence: {
async getOwnedById() {
return syntheticRecord();
},
},
loadLongformAppendix: async () => ({
status: "ready",
lastErrorCode: null,
markdown: CACHED_MARKDOWN,
}),
validateReadyDocument: () => ({ ok: true, document: leakedDocument }),
});
assert.equal(response.status, 200);
assert.equal(typeof response.body.longformMarkdown, "string");
assert.match(String(response.body.longformMarkdown), /事业先看阶段/);
assert.equal(ordinaryOutputLeaks(String(response.body.longformMarkdown)).length, 0);
assert.equal(JSON.stringify(response.body.reportDocument).includes("evidenceAppendix"), false);
const passthrough = { ok: true };
assert.equal(projectOrdinaryReportDocument(passthrough), passthrough);
});
test("markdown view and chart fence contracts are not weakened", () => {
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, /disallowedElements=\{\["script", "iframe", "object", "embed", "img"\]\}/);
assert.doesNotMatch(viewSource, /rehype-raw/);
assert.doesNotMatch(viewSource, /dangerouslySetInnerHTML/);
const fenceSource = readFileSync(
new URL("../src/lib/report-chart-block.ts", import.meta.url),
"utf8",
);
assert.match(fenceSource, /```jyotish-chart/);
const coreSource = readFileSync(
new URL("../src/lib/personal-report-route-core.ts", import.meta.url),
"utf8",
);
assert.match(coreSource, /projectOrdinaryReportMarkdown/);
assert.match(coreSource, /projectOrdinaryReportDocument/);
});
test("PL9 audit vocabulary deletes the whole line and the adjacent table caption", () => {
const source = [
"# 读者报告",
"",
"这一句保留。",
"未完成对照,所有行保持 parameter_sensitive。",
"",
"### KP 原始表",
"",
"本节只解释下面这张表。",
"",
"| 项 | 状态 |",
"| --- | --- |",
"| annual_tajika_pack | blocked |",
"",
"表后的正文还在。",
"KP producer failed: 'types.SimpleNamespace' object has no attribute 'year'",
"Traceback (most recent call last):",
"full-report://profile/hash",
"PL9 第 42 页 fixture",
"pl9_personal_long_report.v2",
"kp_lord_sub_unavailable",
].join("\n");
const projected = projectOrdinaryReportMarkdown(source);
assert.match(projected, /这一句保留/);
assert.match(projected, /表后的正文还在/);
assert.doesNotMatch(projected, /所有行保持/);
assert.doesNotMatch(projected, /parameter_sensitive/);
assert.doesNotMatch(projected, /KP 原始表/);
assert.doesNotMatch(projected, /本节只解释下面这张表/);
assert.doesNotMatch(projected, /annual_tajika_pack/);
assert.doesNotMatch(projected, /^- `$/m);
assert.doesNotMatch(projected, /object has no attribute|Traceback|full-report:\/\/|PL9 第 42 页|pl9_personal_long_report|_unavailable/);
assert.deepEqual(ordinaryOutputLeaks(projected), []);
});
test("real reader_main fixture has no ordinary output leaks", () => {
const fixture = JSON.parse(readFileSync(new URL("./fixtures/report-reader-main-fictional.json", import.meta.url), "utf8"));
assert.equal(fixture.fixtureProvenance.fictional, true);
assert.equal(fixture.fixtureProvenance.edition, "reader_main");
const markdown = fixture.markdown as string;
assert.equal(ordinaryOutputLeaks(markdown).length, 0, ordinaryOutputLeaks(markdown).join(", "));
for (const chapter of ["出生资料", "Ashtakavarga", "标准大运", "KP", "Year Lord", "Tajika", "Muntha"]) {
assert.match(markdown, new RegExp(chapter));
}
const projected = projectOrdinaryReportMarkdown(markdown);
assert.equal(ordinaryOutputLeaks(projected).length, 0);
});
test("twelve English words count as a paragraph leak, planet names do not", () => {
const planet = projectOrdinaryReportMarkdown("太阳与 Mars 同宫,再看 Venus。");
assert.match(planet, /Mars/);
assert.match(planet, /Venus/);
const english = [
"This reference export combines local calculations with available external engine",
"replays and source comparisons that stay explicitly labeled.",
].join(" ");
const projected = projectOrdinaryReportMarkdown(`开头。\n${english}\n结尾。`);
assert.match(projected, /开头/);
assert.match(projected, /结尾/);
assert.doesNotMatch(projected, /This reference export/);
assert.deepEqual(ordinaryOutputLeaks(projected), []);
});
function syntheticRecord(): PersonalReportRecord {
return {
id: "11111111-1111-4111-8111-111111111111",
userId: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
sessionId: null,
chartProfileId: null,
requestId: "22222222-2222-4222-8222-222222222222",
requestFingerprint: "f".repeat(64),
reportType: "personal_full",
status: "ready",
schemaVersion: "report_document.v2",
presentationMode: "default",
depth: "standard",
requestedThemes: ["career"],
reportDocument: null,
calculationHash: null,
evidenceHash: null,
skillName: null,
skillVersion: null,
skillSourceCommit: null,
skillSnapshotSha256: "a".repeat(64),
failureCode: null,
createdAt: "2026-09-22T00:00:00.000Z",
updatedAt: "2026-09-22T00:00:00.000Z",
completedAt: "2026-09-22T00:00:00.000Z",
};
}