Files
Jyotisha/frontend/tests/personal-report-raw-appendix.test.ts
T
2026-09-23 10:42:17 +08:00

113 lines
7.3 KiB
TypeScript

import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import test from "node:test";
import { releaseRawReportAppendix, MISSING_DASHA_APPLICABILITY } from "../src/lib/personal-report-raw-appendix.ts";
import { projectOrdinaryReportMarkdown } from "../src/lib/report-public-projection.ts";
import { cleanReaderAppendixMarkdown } from "../src/lib/reader-appendix-language.ts";
// Vocabulary/security cases, not fabricated engine contract fixtures.
test("raw appendix retains status rows and raw columns without changing ordinary projection", () => {
const source = "# 数据\n\n| sign_cn | degree_in_sign | nakshatra_lord |\n| --- | --- | --- |\n| Aries | 12.5 | parameter_sensitive |\n";
const before = projectOrdinaryReportMarkdown(source);
const result = releaseRawReportAppendix(source);
assert.equal(result.documentKind, "raw_appendix");
assert.equal(result.markdown, source.replace("parameter_sensitive", "参数敏感") + MISSING_DASHA_APPLICABILITY);
assert.equal(result.markdown.split("\n").filter((line) => line.startsWith("|")).length, 3);
assert.equal(projectOrdinaryReportMarkdown(source), before);
assert.doesNotMatch(before, /parameter_sensitive|参数敏感|12\.5/);
});
test("raw appendix cleans old cache vocabulary but leaves English prose intact", () => {
for (const [value, label] of Object.entries({
parameter_sensitive: "参数敏感", pyjhora_behavior_only: "仅单一外部参照,未做多引擎核对",
not_multiengine_parity: "仅单一外部参照,未做多引擎核对", unresolved_external_tuple_boundary: "外部边界未对齐",
partial_verified: "部分核验", raw_appendix_only: "仅原始附录可见", missing_in_local: "本地暂无",
})) assert.equal(cleanReaderAppendixMarkdown(value), label);
for (const value of ["PL9 第 83-88 页", "PL9 第 40 / 48 页", "PL9 page 99-100", "PL9.pdf"]) {
assert.equal(cleanReaderAppendixMarkdown(value), "外部参照资料");
}
assert.equal(cleanReaderAppendixMarkdown("PyJHora JHora cmd_full_reading"), "外部参照引擎 外部参照引擎 本地计算");
const prose = "The available result was computed and executed, not blocked.";
assert.equal(cleanReaderAppendixMarkdown(prose), prose);
assert.equal(cleanReaderAppendixMarkdown("| blocked | `executed` | available | computed |"), "| 暂不可用 | `已执行` | 可用 | 已计算 |");
});
test("raw appendix keeps established HTML URL secret and chart-fence safety boundaries", () => {
const fence = '```jyotish-chart\n{"version":1,"id":"D1","title":"本命","layout":"north","ascendant":{"sign":"Aries","degree":1},"planets":[]}\n```\n';
const raw = '<script>alert(1)</script>\n<iframe src="https://evil.example"></iframe>\n[bad](javascript:alert(1))\nhttp://127.0.0.1:5200/internal\nsk-testsecretvalue\n' + fence;
const result = releaseRawReportAppendix(raw).markdown;
assert.doesNotMatch(result, /script|iframe|javascript:|127\.0\.0\.1|sk-testsecretvalue/);
assert.ok(result.includes(fence));
assert.equal(releaseRawReportAppendix(result).markdown, result);
});
function routeCase(mode: string) {
const script = `
import { mock } from "node:test";
const mode = ${JSON.stringify(mode)};
const id = "123e4567-e89b-12d3-a456-426614174000";
const filters = [];
let reads = 0;
mock.module("@/lib/personal-report-entitlement", { namedExports: {
checkSameOrigin: () => ({ ok: mode !== "cross-origin" }), resolveAllowedReportOrigins: () => [],
}});
mock.module("@/lib/personal-report-service", { namedExports: {
createSupabasePersonalReportService: () => ({ getOwnedById: async (userId, reportId) => {
if (userId !== "owner" || reportId !== id) throw new Error("identity mismatch");
return mode === "foreign" ? null : { status: mode === "generating" ? "generating" : "ready", requestId: "request" };
}}),
}});
mock.module("@/lib/supabase/config", { namedExports: { isSupabaseConfigurationError: () => false }});
mock.module("@/lib/supabase/server", { namedExports: {
createServerSupabaseClient: async () => ({
auth: { getUser: async () => ({ data: { user: mode === "anonymous" ? null : { id: "owner" } }, error: null }) },
from: (table) => table === "personal_report_sections" ? {
select() { return this; }, eq() { return this; }, async maybeSingle() { return { data: null, error: null }; }
} : ({
select() { return this; }, eq(key, value) { filters.push([key,value]); return this; },
async maybeSingle() { reads++; return { error: null, data: mode === "missing" ? null : {
report_id: mode === "wrong-report" ? "other" : id,
user_id: mode === "wrong-owner" ? "other" : "owner",
request_id: mode === "wrong-request" ? "other" : "request",
status: mode === "pending" ? "pending" : "ready", markdown: "| degree_in_sign | parameter_sensitive |",
}}; },
}),
}),
}});
globalThis.fetch = () => { throw new Error("must not recalculate"); };
const { POST } = await import("./src/app/api/reports/[reportId]/raw-appendix/route.ts");
const response = await POST(new Request("https://staging.jyotisha.chat/api/reports/" + id + "/raw-appendix", { method: "POST" }),
{ params: Promise.resolve({ reportId: mode === "invalid-id" ? "invalid" : id }) });
console.log(JSON.stringify({ status: response.status, body: await response.json(), cache: response.headers.get("cache-control"), filters, reads }));
`;
const result = spawnSync(process.execPath, ["--experimental-test-module-mocks", "--import", "tsx", "--input-type=module", "--eval", script], {
cwd: fileURLToPath(new URL("../", import.meta.url)), encoding: "utf8",
});
assert.equal(result.status, 0, result.stderr || result.stdout);
return JSON.parse(result.stdout.trim());
}
for (const [mode, status] of Object.entries({ anonymous: 401, "cross-origin": 403, foreign: 404, "invalid-id": 404, generating: 409, missing: 410, pending: 410, "wrong-report": 410, "wrong-owner": 410, "wrong-request": 410, ready: 200 })) {
test(`raw appendix route guards ${mode}`, () => {
const result = routeCase(mode);
assert.equal(result.status, status);
assert.equal(result.cache, "private, no-store");
if (status < 410 && status !== 200) assert.equal(result.reads, 0);
if (status === 200) {
assert.deepEqual(result.filters, [["report_id", "123e4567-e89b-12d3-a456-426614174000"], ["user_id", "owner"], ["request_id", "request"]]);
assert.deepEqual(result.body, { documentKind: "raw_appendix", format: "markdown", markdown: "| degree_in_sign | 参数敏感 |" + MISSING_DASHA_APPLICABILITY });
} else assert.equal(result.body.markdown, undefined);
});
}
test("raw appendix UI is independent of ordinary export and accepts no birth payload", () => {
const ui = readFileSync(new URL("../src/components/personal-report/raw-appendix-download.tsx", import.meta.url), "utf8");
assert.match(ui, /下载原始附录/);
assert.match(ui, /\/raw-appendix/);
assert.doesNotMatch(ui, /正在加载|Spinner|body:|birth|projectOrdinaryReportMarkdown/);
const ordinary = readFileSync(new URL("../src/lib/personal-report-longform-download.ts", import.meta.url), "utf8");
assert.doesNotMatch(ordinary, /raw-appendix/);
});