Unify reader cleanup rules, lock writer table guards, and register the exact fictional timestamp collision. Preserve existing ordinary-report safety contracts and source-data gaps. Validation: report Node 165/165, final safety 29/29, Python 101/101, Chrome 28/28; both PDFs retain all 130 rows. Full Node 3704 tests with the same 91 baseline failures. Privacy test: 62 passed, 1 failed due to 17 protected-file READ_ERRORs; not a green gate. Build, DB, manual checklist and controlled-login gaps remain documented. User explicitly authorized staging push with these gaps disclosed. Co-Authored-By: Claude Code <noreply@anthropic.com>
85 lines
4.5 KiB
TypeScript
85 lines
4.5 KiB
TypeScript
import assert from "node:assert/strict";
|
||
import { spawnSync } from "node:child_process";
|
||
import { readFileSync } from "node:fs";
|
||
import test from "node:test";
|
||
import { fileURLToPath } from "node:url";
|
||
import { cleanReaderAppendixMarkdown } from "../src/lib/reader-appendix-language.ts";
|
||
|
||
const golden = JSON.parse(readFileSync(new URL("./fixtures/report-density-fictional-reader.json", import.meta.url), "utf8"));
|
||
// Vocabulary-only cases, not invented calculation fixtures.
|
||
const pageCases = [
|
||
"PL9 第 42 页", "PL9第42页", "PL9.pdf 第 42 页", "PL9.pdf第42页后文",
|
||
"参见PL9 第 42 页。", "PL9 第 43–44 页", "PL9 第 40 / 48 页",
|
||
"PL9 pages 99-100", "PL9 p-42", "PL9第42页中的原始字段",
|
||
];
|
||
const chartFence = ' ```jyotish-chart\r\n{"title":"PL9 第 42 页 parameter_sensitive","id":"D1"}\r\n ```\r\n';
|
||
const copyCases = [
|
||
...pageCases,
|
||
"中文parameter_sensitive中文 PyJHora中文 cmd_full_reading后文",
|
||
"some_parameter_sensitive_field PL99 XPL9 PL9_suffix",
|
||
"The blocked / executed / available / computed result is not a claim.",
|
||
"| blocked | `executed` | available | computed | sign_cn | 12.50 |\n",
|
||
`parameter_sensitive\r\n${chartFence}| parameter_sensitive |\r\n`,
|
||
];
|
||
|
||
function pythonClean(inputs: string[]): string[] {
|
||
const script = [
|
||
"import json, sys",
|
||
"from scripts.reader_appendix_language import clean_reader_appendix_markdown",
|
||
"sys.stdout.write(json.dumps([clean_reader_appendix_markdown(value) for value in json.load(sys.stdin)], ensure_ascii=False))",
|
||
].join("\n");
|
||
const result = spawnSync(process.env.PYTHON ?? (process.platform === "win32" ? "python" : "python3"), ["-c", script], {
|
||
cwd: fileURLToPath(new URL("../../", import.meta.url)),
|
||
encoding: "utf8", input: JSON.stringify(inputs), maxBuffer: 16 * 1024 * 1024,
|
||
env: { ...process.env, PYTHONIOENCODING: "utf-8", PYTHONDONTWRITEBYTECODE: "1" },
|
||
});
|
||
assert.equal(result.status, 0, `${result.error ?? ""}\n${result.stderr}`);
|
||
return JSON.parse(result.stdout);
|
||
}
|
||
|
||
test("appendix Python and TS share byte-identical golden and Chinese-boundary output", () => {
|
||
assert.equal(golden.fixtureProvenance.fictional, true);
|
||
const inputs = [golden.markdown, golden.reader_dasha_applicability, ...copyCases] as string[];
|
||
const python = pythonClean(inputs);
|
||
for (const [index, input] of inputs.entries()) {
|
||
const ts = cleanReaderAppendixMarkdown(input);
|
||
assert.deepEqual(Buffer.from(python[index], "utf8"), Buffer.from(ts, "utf8"), `case ${index}`);
|
||
assert.equal(cleanReaderAppendixMarkdown(ts), ts, `idempotent case ${index}`);
|
||
}
|
||
assert.doesNotMatch(python[0], /第\s*\d+(?:\s*[–—/-]\s*\d+)*\s*页/);
|
||
assert.doesNotMatch(cleanReaderAppendixMarkdown(golden.markdown), /第\s*\d+(?:\s*[–—/-]\s*\d+)*\s*页/);
|
||
});
|
||
|
||
test("appendix page references disappear without swallowing adjacent Chinese prose", () => {
|
||
for (const source of pageCases) {
|
||
const output = cleanReaderAppendixMarkdown(source);
|
||
assert.doesNotMatch(output, /PL9|第\s*\d+|pages?\s*\d+|p-42/);
|
||
}
|
||
assert.equal(cleanReaderAppendixMarkdown("参见PL9第42页中的原始字段"), "参见外部参照资料中的原始字段");
|
||
assert.equal(cleanReaderAppendixMarkdown("PL9.pdf第42页后文"), "外部参照资料后文");
|
||
assert.equal(cleanReaderAppendixMarkdown("some_parameter_sensitive_field PL99 XPL9"), "some_parameter_sensitive_field PL99 外部参照资料");
|
||
});
|
||
|
||
test("appendix golden preserves line and table shape, field names, numeric cells and chart bytes", () => {
|
||
const raw = golden.markdown as string;
|
||
const clean = cleanReaderAppendixMarkdown(raw);
|
||
const sourceLines = raw.split("\n");
|
||
const cleanLines = clean.split("\n");
|
||
assert.equal(cleanLines.length, sourceLines.length);
|
||
for (const [index, line] of sourceLines.entries()) {
|
||
if (!line.startsWith("|")) continue;
|
||
const cells = line.split("|");
|
||
const outputCells = cleanLines[index].split("|");
|
||
assert.equal(outputCells.length, cells.length, `table columns at line ${index}`);
|
||
cells.forEach((cell, column) => {
|
||
if (/^\s*-?\d+(?:\.\d+)?\s*$/.test(cell)) assert.equal(outputCells[column], cell);
|
||
});
|
||
}
|
||
for (const field of ["sign_cn", "degree_in_sign", "nakshatra_lord"]) {
|
||
assert.equal(clean.split(field).length, raw.split(field).length);
|
||
}
|
||
const fences = (text: string) => [...text.matchAll(/^[ \t]*```jyotish-chart[^\n]*\n[\s\S]*?^[ \t]*```[^\n]*(?:\n|$)/gm)].map(match => Buffer.from(match[0]));
|
||
assert.deepEqual(fences(clean), fences(raw));
|
||
assert.deepEqual(fences(cleanReaderAppendixMarkdown(copyCases.at(-1)!)), [Buffer.from(chartFence)]);
|
||
});
|