331 lines
12 KiB
TypeScript
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",
|
|
};
|
|
}
|