Files
Jyotisha/frontend/tests/report-public-projection.test.ts
T

331 lines
12 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/);
});
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",
};
}