feat(report): render longform Markdown as the report and close gaps2 holes
New reports skip the writer, persist pl9 Markdown as the body, and settle zero-token usage on the catalog model. Planned longform sections now emit blocked rows instead of vanishing. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1132,6 +1132,38 @@ test("core read: returns a legitimate ready document after canonical re-validati
|
||||
assert.ok(response.body.reportDocument);
|
||||
});
|
||||
|
||||
test("core read: markdown loader returns longform and hides the five-chapter body when it is missing", async () => {
|
||||
const persistence = new MemoryPersistence();
|
||||
persistence.rows.set(REPORT_ID, seedRecord());
|
||||
const withMarkdown = await resolveReportRead({
|
||||
requestUrl: "https://jyotisha.chat/api/reports/x",
|
||||
origin: null,
|
||||
allowedOrigins: [],
|
||||
userId: UUID_A,
|
||||
reportId: REPORT_ID,
|
||||
persistence,
|
||||
loadLongformMarkdown: async () => "# 长报告\n\n### 摘要\n正文",
|
||||
validateReadyDocument: acceptAnyDocument,
|
||||
});
|
||||
assert.equal(withMarkdown.status, 200);
|
||||
assert.equal(withMarkdown.body.longformMarkdown, "# 长报告\n\n### 摘要\n正文");
|
||||
assert.ok(withMarkdown.body.reportDocument);
|
||||
|
||||
const missing = await resolveReportRead({
|
||||
requestUrl: "https://jyotisha.chat/api/reports/x",
|
||||
origin: null,
|
||||
allowedOrigins: [],
|
||||
userId: UUID_A,
|
||||
reportId: REPORT_ID,
|
||||
persistence,
|
||||
loadLongformMarkdown: async () => null,
|
||||
validateReadyDocument: acceptAnyDocument,
|
||||
});
|
||||
assert.equal(missing.status, 200);
|
||||
assert.equal(missing.body.longformMarkdown, null);
|
||||
assert.equal("reportDocument" in missing.body, false);
|
||||
});
|
||||
|
||||
test("core delete: owner-only, 200 ok for the owner and 404 otherwise", async () => {
|
||||
const persistence = new MemoryPersistence();
|
||||
await createReadyRow(persistence);
|
||||
@@ -1232,6 +1264,7 @@ test("GET/DELETE use the authenticated client (least privilege) and the core han
|
||||
assert.match(itemRoute, /createSupabasePersonalReportService\(supabase\)/);
|
||||
assert.match(itemRoute, /resolveReportRead/);
|
||||
assert.match(itemRoute, /resolveReportDelete/);
|
||||
assert.match(itemRoute, /loadLongformMarkdown/);
|
||||
assert.doesNotMatch(itemRoute, /createAdminSupabaseClient/);
|
||||
});
|
||||
|
||||
@@ -1277,7 +1310,12 @@ test("POST enqueues durable work without Next.js after and GET lists metadata wi
|
||||
createRoute.indexOf("const REPORT_LIST_COLUMNS"),
|
||||
createRoute.indexOf("function sanitizedErrorCode"),
|
||||
);
|
||||
assert.doesNotMatch(listColumns, /report_document|calculation_hash|evidence_hash/);
|
||||
assert.doesNotMatch(listColumns, /calculation_hash|evidence_hash/);
|
||||
assert.match(listColumns, /card_summary:report_document->executiveSummary->>summary/);
|
||||
assert.doesNotMatch(
|
||||
listColumns.replace("card_summary:report_document->executiveSummary->>summary", ""),
|
||||
/report_document/,
|
||||
);
|
||||
assert.doesNotMatch(createRoute, /STALE_GENERATION_MS|staleBefore/);
|
||||
assert.match(createRoute, /reportListTimestamp\(row\.created_at\)/);
|
||||
assert.match(createRoute, /reportListTimestamp\(row\.completed_at\)/);
|
||||
|
||||
@@ -285,11 +285,14 @@ test("legacy consultation Markdown export is untouched and still works", () => {
|
||||
});
|
||||
|
||||
|
||||
test("ready reports expose the full-data appendix Markdown export only in the ready branch", () => {
|
||||
assert.match(reportCenterSource, /全量数据附录/);
|
||||
test("ready reports expose Markdown export only in the ready branch", () => {
|
||||
assert.match(reportCenterSource, /PERSONAL_REPORT_EXPORT_LABEL/);
|
||||
assert.match(reportCenterSource, /downloadPersonalReportLongformAppendix/);
|
||||
assert.match(reportCenterSource, /cardSummary/);
|
||||
assert.match(longformDownloadSource, /professional-reference/);
|
||||
assert.match(longformDownloadSource, /downloadMarkdownReport\("个人全量数据附录", markdown\)/);
|
||||
assert.match(longformDownloadSource, /personalReportMarkdownFilename/);
|
||||
assert.doesNotMatch(reportCenterSource, /全量数据附录/);
|
||||
assert.doesNotMatch(reportCenterSource, /章节/);
|
||||
const readyActionStart = reportCenterSource.indexOf(
|
||||
'{report.status === "ready" ? (',
|
||||
);
|
||||
@@ -299,6 +302,6 @@ test("ready reports expose the full-data appendix Markdown export only in the re
|
||||
);
|
||||
assert.ok(readyActionStart >= 0 && generatingActionStart > readyActionStart);
|
||||
const readyBranch = reportCenterSource.slice(readyActionStart, generatingActionStart);
|
||||
assert.match(readyBranch, /全量数据附录/);
|
||||
assert.doesNotMatch(reportCenterSource.slice(generatingActionStart), /全量数据附录/);
|
||||
assert.match(readyBranch, /PERSONAL_REPORT_EXPORT_LABEL/);
|
||||
assert.doesNotMatch(reportCenterSource.slice(generatingActionStart), /PERSONAL_REPORT_EXPORT_LABEL/);
|
||||
});
|
||||
|
||||
@@ -89,12 +89,10 @@ test("client export never touches a server PDF pipeline", () => {
|
||||
});
|
||||
|
||||
|
||||
test("native print uses the same validated structured DOM for stored v1 and current v2", () => {
|
||||
assert.match(pageSource, /safeParseReportDocument\(json\.reportDocument\)/);
|
||||
assert.match(pageSource, /<PersonalReportDocumentView document=\{document\} \/>/);
|
||||
assert.match(documentViewSource, /isReportDocumentV2\(document\)/);
|
||||
assert.match(documentViewSource, /<VedicChartSvg chart=\{structuredChart\(chart\)\}/);
|
||||
test("native print remains a secondary action on the Markdown report page", () => {
|
||||
assert.match(pageSource, /longformMarkdown/);
|
||||
assert.match(pageSource, /PersonalReportMarkdownView/);
|
||||
assert.doesNotMatch(pageSource, /PersonalReportDocumentView/);
|
||||
assert.doesNotMatch(documentViewSource, /dangerouslySetInnerHTML|srcDoc|<iframe/i);
|
||||
assert.doesNotMatch(documentViewSource, /modelHtml|modelCss|modelSvg/i);
|
||||
assert.match(exportSource, /window\.print/);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import test from "node:test";
|
||||
import React from "react";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
|
||||
import { PersonalReportMarkdownView } from "../src/components/personal-report/personal-report-markdown-view.tsx";
|
||||
import { buildLongformCoverDocument } from "../src/lib/personal-report-longform-cover.ts";
|
||||
import {
|
||||
PERSONAL_REPORT_EXPORT_LABEL,
|
||||
PERSONAL_REPORT_GENERATING_COPY,
|
||||
PERSONAL_REPORT_LEGACY_PLACEHOLDER,
|
||||
} from "../src/lib/personal-report-longform-copy.ts";
|
||||
import {
|
||||
buildLongformOutline,
|
||||
extractLongformSummary,
|
||||
personalReportMarkdownFilename,
|
||||
} from "../src/lib/personal-report-longform-outline.ts";
|
||||
import { PERSONAL_REPORT_WRITER_ENABLED } from "../src/lib/personal-report-writer-flag.ts";
|
||||
|
||||
Object.assign(globalThis, { React });
|
||||
|
||||
const SAMPLE_MARKDOWN = [
|
||||
"# 个人长报告",
|
||||
"",
|
||||
"## 成品阅读导航",
|
||||
"",
|
||||
"导航正文 blocked",
|
||||
"",
|
||||
"## 摘要",
|
||||
"",
|
||||
"事业方向保持观察,不承诺日期。parameter_sensitive",
|
||||
"",
|
||||
"## 力量章",
|
||||
"",
|
||||
"### 子节",
|
||||
"",
|
||||
"后续章节",
|
||||
"",
|
||||
"## 质量验收矩阵",
|
||||
"",
|
||||
"| 项 | 状态 |",
|
||||
"| --- | --- |",
|
||||
"| MEVG | blocked |",
|
||||
].join("\n");
|
||||
|
||||
const UNSAFE_MARKDOWN = [
|
||||
SAMPLE_MARKDOWN,
|
||||
"",
|
||||
"## 摘要",
|
||||
"",
|
||||
"事业方向保持观察。",
|
||||
"",
|
||||
"<script>alert(1)</script>",
|
||||
"<img src=\"https://evil.example/x.png\">",
|
||||
"[bad](javascript:alert(1))",
|
||||
"",
|
||||
"| 月 | KP |",
|
||||
"| --- | --- |",
|
||||
"| 2026-01 | blocked |",
|
||||
].join("\n");
|
||||
|
||||
test("report centre cards read the stored Markdown excerpt, not a writer summary field name", () => {
|
||||
const listRoute = readFileSync(new URL("../src/app/api/reports/route.ts", import.meta.url), "utf8");
|
||||
assert.match(listRoute, /card_summary:report_document->executiveSummary->>summary/);
|
||||
const coverSource = readFileSync(
|
||||
new URL("../src/lib/personal-report-longform-cover.ts", import.meta.url),
|
||||
"utf8",
|
||||
);
|
||||
assert.match(coverSource, /extractLongformSummary/);
|
||||
});
|
||||
|
||||
test("writer pipeline stays in the tree but is feature-off", () => {
|
||||
assert.equal(PERSONAL_REPORT_WRITER_ENABLED, false);
|
||||
assert.equal(PERSONAL_REPORT_EXPORT_LABEL, "导出报告(.md)");
|
||||
assert.equal(PERSONAL_REPORT_LEGACY_PLACEHOLDER, "旧版本报告,请重新生成");
|
||||
assert.match(PERSONAL_REPORT_GENERATING_COPY, /10–30 秒/);
|
||||
});
|
||||
|
||||
test("outline lifts navigation and summary to the first screen", () => {
|
||||
const outline = buildLongformOutline(SAMPLE_MARKDOWN);
|
||||
assert.ok(outline.headings.some((heading) => heading.title === "成品阅读导航"));
|
||||
assert.ok(outline.headings.some((heading) => heading.title === "摘要"));
|
||||
assert.match(outline.leadMarkdown, /成品阅读导航/);
|
||||
assert.match(outline.leadMarkdown, /事业方向保持观察/);
|
||||
assert.equal(outline.sections.some((section) => section.eager && section.title === "成品阅读导航"), true);
|
||||
assert.equal(outline.sections.some((section) => section.eager && section.title === "摘要"), true);
|
||||
});
|
||||
|
||||
test("card excerpt comes from the Markdown 摘要 section", () => {
|
||||
const excerpt = extractLongformSummary(SAMPLE_MARKDOWN, 80);
|
||||
assert.match(excerpt, /事业方向保持观察/);
|
||||
assert.doesNotMatch(excerpt, /executiveSummary/);
|
||||
assert.equal(personalReportMarkdownFilename("2026-09-06T08:00:00.000Z"), "个人报告-2026-09-06");
|
||||
});
|
||||
|
||||
test("cover document parses and stores the Markdown excerpt", () => {
|
||||
const document = buildLongformCoverDocument({
|
||||
report: {
|
||||
id: "123e4567-e89b-12d3-a456-426614174000",
|
||||
reportType: "personal_full",
|
||||
presentationMode: "default",
|
||||
depth: "standard",
|
||||
requestedThemes: ["career", "wealth"],
|
||||
skillName: "jyotish-vedic-astrology",
|
||||
skillVersion: "6.9.14",
|
||||
skillSourceCommit: null,
|
||||
skillSnapshotSha256: "ab".repeat(32),
|
||||
},
|
||||
subject: {
|
||||
displayName: "测试",
|
||||
birthTimeStatus: "accepted",
|
||||
birthPlaceLabel: "测试地点",
|
||||
},
|
||||
markdown: SAMPLE_MARKDOWN,
|
||||
generatedAt: "2026-09-06T00:00:00.000Z",
|
||||
});
|
||||
assert.equal(document.schemaVersion, "report_document.v2");
|
||||
assert.match(document.executiveSummary.summary, /事业方向保持观察/);
|
||||
assert.equal(document.blockedConflictDisclosure.length, 2);
|
||||
assert.equal(document.thematicNarrative.length, 0);
|
||||
});
|
||||
|
||||
test("markdown view escapes HTML, skips images, and wraps wide tables", () => {
|
||||
const started = Date.now();
|
||||
const markup = renderToStaticMarkup(React.createElement(PersonalReportMarkdownView, { markdown: UNSAFE_MARKDOWN }));
|
||||
const elapsed = Date.now() - started;
|
||||
assert.ok(elapsed < 1500, `first paint ${elapsed}ms`);
|
||||
assert.match(markup, /personal-report-toc/);
|
||||
assert.match(markup, /成品阅读导航/);
|
||||
assert.match(markup, /parameter_sensitive/);
|
||||
assert.match(markup, /blocked/);
|
||||
assert.match(markup, /markdown-table personal-report-table-wrap/);
|
||||
assert.doesNotMatch(markup, /<script/i);
|
||||
assert.doesNotMatch(markup, /<img/i);
|
||||
assert.doesNotMatch(markup, /javascript:/i);
|
||||
assert.doesNotMatch(markup, /evil\.example/);
|
||||
|
||||
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, /remarkGfm/);
|
||||
assert.doesNotMatch(viewSource, /rehype-raw/);
|
||||
assert.doesNotMatch(viewSource, /dangerouslySetInnerHTML/);
|
||||
});
|
||||
@@ -320,17 +320,17 @@ test("GET envelope classification: 401/404/ready/generating/failed/invalid (real
|
||||
|
||||
const ready = classifyReportEnvelope(200, {
|
||||
report: view("ready"),
|
||||
reportDocument: canonicalFixture,
|
||||
longformMarkdown: "# 个人长报告\n\n### 解读摘要\n正文",
|
||||
});
|
||||
assert.equal(ready.phase, "ready");
|
||||
assert.ok(ready.phase === "ready" && ready.document.reportId === canonicalFixture.reportId);
|
||||
assert.equal(ready.phase, "markdown-ready");
|
||||
assert.ok(ready.phase === "markdown-ready" && ready.markdown.includes("解读摘要"));
|
||||
|
||||
const readyV2 = classifyReportEnvelope(200, {
|
||||
report: view("ready"),
|
||||
longformMarkdown: "## 成品阅读导航\n导航",
|
||||
reportDocument: canonicalV2Fixture,
|
||||
});
|
||||
assert.equal(readyV2.phase, "ready");
|
||||
assert.ok(readyV2.phase === "ready" && readyV2.document.schemaVersion === "report_document.v2");
|
||||
assert.equal(readyV2.phase, "markdown-ready");
|
||||
|
||||
assert.deepEqual(classifyReportEnvelope(200, { report: view("generating") }), { phase: "generating" });
|
||||
assert.deepEqual(classifyReportEnvelope(200, { report: view("failed", "model_unavailable") }), {
|
||||
@@ -357,13 +357,13 @@ test("GET envelope classification: 401/404/ready/generating/failed/invalid (real
|
||||
assert.deepEqual(classifyReportEnvelope(500, {}), { phase: "failed", failureCode: null });
|
||||
|
||||
const missingDocument = classifyReportEnvelope(200, { report: view("ready") });
|
||||
assert.equal(missingDocument.phase, "invalid");
|
||||
assert.equal(missingDocument.phase, "legacy-unavailable");
|
||||
|
||||
const badDocument = classifyReportEnvelope(200, {
|
||||
report: view("ready"),
|
||||
reportDocument: { schemaVersion: "wrong" },
|
||||
});
|
||||
assert.equal(badDocument.phase, "invalid");
|
||||
assert.equal(badDocument.phase, "legacy-unavailable");
|
||||
|
||||
const unknownStatus = classifyReportEnvelope(200, { report: view("mystery") });
|
||||
assert.equal(unknownStatus.phase, "invalid");
|
||||
@@ -409,11 +409,11 @@ test("ready reports expose the browser print/PDF action with capability and hydr
|
||||
);
|
||||
assert.match(actionsSource, /href="\/reports"/);
|
||||
assert.match(actionsSource, /返回报告中心/);
|
||||
assert.match(actionsSource, /打印 \/ 保存为 PDF/);
|
||||
assert.match(actionsSource, /全量数据附录/);
|
||||
assert.match(actionsSource, /PERSONAL_REPORT_EXPORT_LABEL/);
|
||||
assert.match(actionsSource, /PERSONAL_REPORT_PRINT_LABEL/);
|
||||
assert.match(actionsSource, /downloadPersonalReportLongformAppendix/);
|
||||
assert.match(actionsSource, /printPersonalReport/);
|
||||
assert.match(actionsSource, /Printer/);
|
||||
assert.match(actionsSource, /useSyncExternalStore\(subscribePrintCapability, isPrintSupported, \(\) => false\)/);
|
||||
assert.match(actionsSource, /disabled=\{printBusy \|\| appendixBusy \|\| !printSupported\}/);
|
||||
assert.match(actionsSource, /disabled=\{printBusy \|\| exportBusy \|\| !printSupported\}/);
|
||||
});
|
||||
|
||||
@@ -654,6 +654,83 @@ test("retryable durable generation failure does not release before retry exhaust
|
||||
assert.deepEqual(harness.billingReleaseReasons, []);
|
||||
});
|
||||
|
||||
test("zero-token settlement is forwarded after a ready cover document", async () => {
|
||||
const billing: ReportBillingPort = {
|
||||
async reserve() { throw new Error("not used"); },
|
||||
async complete(input) {
|
||||
assert.equal(input.usage.inputTokens, 0);
|
||||
assert.equal(input.usage.outputTokens, 0);
|
||||
assert.equal(input.usage.actualModelId, "model-a");
|
||||
return true;
|
||||
},
|
||||
async release() { throw new Error("release must not run"); },
|
||||
};
|
||||
const harness = createHarness({
|
||||
billing,
|
||||
generate: async () => ({
|
||||
status: "ready",
|
||||
document: READY_DOCUMENT,
|
||||
evidenceHash: "c".repeat(64),
|
||||
usage: { inputTokens: 0, outputTokens: 0, actualModelId: "model-a", modelConfigVersion: 7 },
|
||||
}),
|
||||
});
|
||||
const result = await harness.worker.tick();
|
||||
assert.equal(result.outcome, "ready");
|
||||
assert.equal(harness.billingCompleteCount, 1);
|
||||
});
|
||||
|
||||
test("zero-token longform settlement completes reserved usage and does not release", async () => {
|
||||
const billing: ReportBillingPort = {
|
||||
async reserve() { throw new Error("not used"); },
|
||||
async complete(input) {
|
||||
assert.equal(input.usage.actualModelId, "catalog-default");
|
||||
assert.equal(input.usage.inputTokens, 0);
|
||||
assert.equal(input.usage.outputTokens, 0);
|
||||
return true;
|
||||
},
|
||||
async release() { throw new Error("release must not run"); },
|
||||
};
|
||||
const harness = createHarness({
|
||||
billing,
|
||||
generate: async () => ({
|
||||
status: "ready",
|
||||
document: READY_DOCUMENT,
|
||||
evidenceHash: "c".repeat(64),
|
||||
usage: { inputTokens: 0, outputTokens: 0, actualModelId: "catalog-default", modelConfigVersion: 1 },
|
||||
}),
|
||||
});
|
||||
|
||||
const result = await harness.worker.tick();
|
||||
|
||||
assert.equal(result.outcome, "ready");
|
||||
assert.equal(harness.billingCompleteCount, 1);
|
||||
assert.deepEqual(harness.billingReleaseReasons, []);
|
||||
});
|
||||
|
||||
test("billing refuses the unknown model id before the report is marked ready", async () => {
|
||||
const billing: ReportBillingPort = {
|
||||
async reserve() { throw new Error("not used"); },
|
||||
async complete() { throw new Error("complete must not run"); },
|
||||
async release() { return true; },
|
||||
};
|
||||
const harness = createHarness({
|
||||
billing,
|
||||
job: jobRecord({ maxAttempts: 2 }),
|
||||
generate: async () => ({
|
||||
status: "ready",
|
||||
document: READY_DOCUMENT,
|
||||
evidenceHash: "c".repeat(64),
|
||||
usage: { inputTokens: 0, outputTokens: 0, actualModelId: "unknown" },
|
||||
}),
|
||||
});
|
||||
|
||||
const result = await harness.worker.tick();
|
||||
|
||||
assert.equal(result.outcome, "retry_scheduled");
|
||||
assert.equal(harness.report?.status, "generating");
|
||||
assert.equal(harness.billingCompleteCount, 0);
|
||||
});
|
||||
|
||||
test("instrumentation retains the Skill guard and starts a singleton Node worker loop", () => {
|
||||
const instrumentation = readFileSync(new URL("../src/instrumentation.ts", import.meta.url), "utf8");
|
||||
const productionAdapter = readFileSync(new URL("../src/lib/personal-report-worker.ts", import.meta.url), "utf8");
|
||||
@@ -672,6 +749,10 @@ test("instrumentation retains the Skill guard and starts a singleton Node worker
|
||||
assert.match(productionAdapter, /jyotishaPersonalReportWorker/);
|
||||
assert.match(productionAdapter, /if \(state\.jyotishaPersonalReportWorker\) return/);
|
||||
assert.match(productionAdapter, /loadReportCandidateRange/);
|
||||
assert.match(productionAdapter, /generatePersonalReportLongform/);
|
||||
assert.match(productionAdapter, /PERSONAL_REPORT_WRITER_ENABLED/);
|
||||
assert.match(productionAdapter, /inputTokens: 0/);
|
||||
assert.doesNotMatch(productionAdapter, /actualModelId: "unknown"/);
|
||||
assert.doesNotMatch(productionAdapter, /from\("agentic_rectification_cases"\)/);
|
||||
assert.doesNotMatch(productionAdapter, /from\("birth_time_rectification_cases"\)/);
|
||||
assert.doesNotMatch(productionAdapter, /after\s*\(/);
|
||||
|
||||
@@ -11,14 +11,14 @@ const routeSource = readFileSync(
|
||||
|
||||
const frontendRoot = fileURLToPath(new URL("../", import.meta.url));
|
||||
|
||||
function executeReadyReportExport() {
|
||||
function executeReadyReportExport(cached: boolean) {
|
||||
const script = String.raw`
|
||||
import { mock } from "node:test";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { Agent } from "@mastra/core/agent";
|
||||
|
||||
const moduleUrl = (path) => pathToFileURL(process.cwd() + "/" + path).href;
|
||||
// Node 22 ignores the unsupported exports mock option; keep the route aliases explicit.
|
||||
const cached = ${cached ? "true" : "false"};
|
||||
mock.module("@/lib/personal-report-entitlement", { namedExports: {
|
||||
checkSameOrigin: () => ({ ok: true }),
|
||||
resolveAllowedReportOrigins: () => [],
|
||||
@@ -28,36 +28,27 @@ function executeReadyReportExport() {
|
||||
getOwnedById: async () => ({ status: "ready", requestId: "123e4567-e89b-12d3-a456-426614174111" }),
|
||||
}),
|
||||
}});
|
||||
mock.module("@/lib/supabase/admin", { namedExports: {
|
||||
createAdminSupabaseClient: () => { throw new Error("admin unused in this unit test"); },
|
||||
}});
|
||||
mock.module("@/lib/report-candidate-range", { namedExports: {
|
||||
loadReportCandidateRange: async () => null,
|
||||
}});
|
||||
mock.module("@/lib/personal-report-longform-appendix", { namedExports: {
|
||||
LONGFORM_APPENDIX_TABLE: "personal_report_longform_appendices",
|
||||
parseLongformAppendixRow: () => null,
|
||||
parseLongformAppendixRow: () => cached
|
||||
? { status: "ready", markdown: "# Professional reference" }
|
||||
: null,
|
||||
nextLongformAppendixState: () => ({ status: "ready", attemptCount: 0, markdown: "# Professional reference", contentSha256: "ab", lastErrorCode: null }),
|
||||
}});
|
||||
mock.module("@/lib/personal-report-longform-copy", { namedExports: {
|
||||
PERSONAL_REPORT_LEGACY_PLACEHOLDER: "旧版本报告,请重新生成",
|
||||
}});
|
||||
mock.module("@/lib/supabase/config", { namedExports: {
|
||||
isSupabaseConfigurationError: () => false,
|
||||
}});
|
||||
const profileQuery = {
|
||||
select() { return this; },
|
||||
eq() { return this; },
|
||||
async maybeSingle() { return { data: {
|
||||
birth_date: "1990-01-02",
|
||||
reported_birth_time: "03:04",
|
||||
latitude: 39.9,
|
||||
longitude: 116.4,
|
||||
timezone_offset: 8,
|
||||
ayanamsa: "lahiri",
|
||||
}, error: null }; },
|
||||
};
|
||||
mock.module("@/lib/supabase/server", { namedExports: {
|
||||
createServerSupabaseClient: async () => ({
|
||||
auth: { getUser: async () => ({ data: { user: { id: "user-1" } }, error: null }) },
|
||||
from: () => profileQuery,
|
||||
from: () => ({
|
||||
select() { return this; },
|
||||
eq() { return this; },
|
||||
async maybeSingle() { return { data: cached ? { markdown: "# Professional reference" } : null, error: null }; },
|
||||
}),
|
||||
}),
|
||||
}});
|
||||
|
||||
@@ -72,15 +63,8 @@ function executeReadyReportExport() {
|
||||
};
|
||||
const upstream = [];
|
||||
globalThis.fetch = async (input, init) => {
|
||||
upstream.push({
|
||||
url: String(input),
|
||||
method: init?.method,
|
||||
body: JSON.parse(String(init?.body)),
|
||||
});
|
||||
return new Response(JSON.stringify({ format: "markdown", markdown: "# Professional reference" }), {
|
||||
status: 200,
|
||||
headers: { "content-type": "application/json" },
|
||||
});
|
||||
upstream.push({ url: String(input), method: init?.method });
|
||||
throw new Error("cache-only export must not call the engine");
|
||||
};
|
||||
|
||||
const { POST } = await import(moduleUrl("src/app/api/reports/[reportId]/professional-reference/route.ts"));
|
||||
@@ -110,10 +94,10 @@ function executeReadyReportExport() {
|
||||
assert.equal(result.status, 0, result.stderr || result.stdout);
|
||||
return JSON.parse(result.stdout.trim()) as {
|
||||
status: number;
|
||||
body: { format: string; markdown: string };
|
||||
body: Record<string, unknown>;
|
||||
modelCalls: number;
|
||||
telemetryEvents: number;
|
||||
upstream: Array<{ url: string; method: string; body: Record<string, unknown> }>;
|
||||
upstream: Array<{ url: string; method: string }>;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -128,54 +112,35 @@ test("professional reference route authenticates, checks origin, owner and ready
|
||||
assert.match(routeSource, /status: 403/);
|
||||
assert.match(routeSource, /status: 404/);
|
||||
assert.match(routeSource, /status: 409/);
|
||||
assert.match(routeSource, /status: 410/);
|
||||
});
|
||||
|
||||
test("birth data stays server-owned and the route calls only the public Python export", () => {
|
||||
assert.match(routeSource, /\.from\("profiles"\)/);
|
||||
assert.match(routeSource, /select\(ACCOUNT_BIRTH_SELECT\)/);
|
||||
assert.match(routeSource, /globalBirthProfileFromAccountRow/);
|
||||
assert.match(routeSource, /\/api\/professional_report_reference/);
|
||||
assert.match(routeSource, /format: "markdown"/);
|
||||
assert.match(routeSource, /packs: \["full"\]/);
|
||||
assert.match(routeSource, /target_year/);
|
||||
assert.match(routeSource, /birth_time_accuracy/);
|
||||
assert.match(routeSource, /loadReportCandidateRange/);
|
||||
test("ready export is cache-only and never calls the writer or Python engine", () => {
|
||||
assert.match(routeSource, /LONGFORM_APPENDIX_TABLE/);
|
||||
assert.match(routeSource, /from "@\/lib\/personal-report-longform-appendix"/);
|
||||
assert.match(routeSource, /PERSONAL_REPORT_LEGACY_PLACEHOLDER/);
|
||||
assert.doesNotMatch(routeSource, /request\.json\(/);
|
||||
assert.doesNotMatch(routeSource, /mastra|writer|billing|personal_report_sections/i);
|
||||
assert.doesNotMatch(routeSource, /from\("personal_reports"\)/);
|
||||
assert.doesNotMatch(routeSource, /\/api\/professional_report_reference/);
|
||||
assert.doesNotMatch(routeSource, /\.from\("profiles"\)/);
|
||||
assert.doesNotMatch(routeSource, /loadReportCandidateRange/);
|
||||
});
|
||||
|
||||
test("ready report export calls the Python endpoint without model calls or writer telemetry", () => {
|
||||
const result = executeReadyReportExport();
|
||||
test("cached appendix returns markdown without model or engine calls", () => {
|
||||
const result = executeReadyReportExport(true);
|
||||
assert.equal(result.status, 200);
|
||||
assert.deepEqual(result.body, { format: "markdown", markdown: "# Professional reference" });
|
||||
assert.equal(result.modelCalls, 0);
|
||||
assert.equal(result.telemetryEvents, 0);
|
||||
assert.equal(result.upstream.length, 1);
|
||||
assert.equal(result.upstream[0].url, "http://127.0.0.1:5200/api/professional_report_reference");
|
||||
assert.equal(result.upstream[0].method, "POST");
|
||||
assert.equal(result.upstream[0].body.year, 1990);
|
||||
assert.equal(result.upstream[0].body.month, 1);
|
||||
assert.equal(result.upstream[0].body.day, 2);
|
||||
assert.equal(result.upstream[0].body.hour, 3);
|
||||
assert.equal(result.upstream[0].body.minute, 4);
|
||||
assert.equal(result.upstream[0].body.lat, 39.9);
|
||||
assert.equal(result.upstream[0].body.lon, 116.4);
|
||||
assert.equal(result.upstream[0].body.tz, 8);
|
||||
assert.equal(result.upstream[0].body.ayanamsa, "lahiri");
|
||||
assert.equal(result.upstream[0].body.format, "markdown");
|
||||
assert.deepEqual(result.upstream[0].body.packs, ["full"]);
|
||||
assert.match(String(result.upstream[0].body.today), /^\d{4}-\d{2}-\d{2}$/);
|
||||
assert.equal(result.upstream[0].body.target_year, Number(String(result.upstream[0].body.today).slice(0, 4)));
|
||||
assert.equal(result.upstream[0].body.age, Number(result.upstream[0].body.target_year) - 1990);
|
||||
assert.equal(result.upstream[0].body.birth_time_accuracy, "confirmed");
|
||||
assert.equal(result.upstream[0].body.candidate_range, undefined);
|
||||
assert.equal(result.upstream.length, 0);
|
||||
});
|
||||
|
||||
test("busy upstream responses preserve 429 and Retry-After", () => {
|
||||
assert.match(routeSource, /upstream\.status/);
|
||||
assert.match(routeSource, /upstream\.headers\.get\("retry-after"\)/);
|
||||
assert.match(routeSource, /"Retry-After": retryAfter/);
|
||||
test("missing appendix is a retired report, not an on-demand generation", () => {
|
||||
const result = executeReadyReportExport(false);
|
||||
assert.equal(result.status, 410);
|
||||
assert.equal(result.body.error, "旧版本报告,请重新生成");
|
||||
assert.equal(result.body.code, "legacy_report");
|
||||
assert.equal(result.modelCalls, 0);
|
||||
assert.equal(result.upstream.length, 0);
|
||||
});
|
||||
|
||||
@@ -101,7 +101,8 @@ test("elapsed wait is rendered in Simplified Chinese minutes and seconds", () =>
|
||||
// Original assertion required the fixed generating copy. Task 4 intentionally
|
||||
// replaces it with phase-aware progress text while retaining the spinner.
|
||||
assert.match(pageSource, /<InlineSpinner className="text-primary" size=\{32\} \/>/);
|
||||
assert.match(pageSource, /generating \? \(progressLabel \?\? "报告正在生成中,请稍候…"\)/);
|
||||
assert.match(pageSource, /PERSONAL_REPORT_GENERATING_COPY/);
|
||||
assert.doesNotMatch(pageSource, /章节/);
|
||||
assert.match(pageSource, /已等待 \{formatWaitedDuration\(waitedMs\)\}/);
|
||||
});
|
||||
|
||||
|
||||
@@ -83,7 +83,7 @@ test("daily and synastry routes share a literal account-birth select string", ()
|
||||
const synastry = readFileSync(new URL("../src/app/api/synastry/route.ts", import.meta.url), "utf8");
|
||||
assert.equal(
|
||||
ACCOUNT_BIRTH_SELECT,
|
||||
"name,birth_date,reported_birth_time,active_birth_time,birth_time,birth_time_status,country_code,province_code,city_code,district_code,latitude,longitude,timezone_offset,timezone_id,ayanamsa",
|
||||
"name,birth_date,reported_birth_time,active_birth_time,birth_time,birth_time_status,birth_time_source,country_code,province_code,city_code,district_code,latitude,longitude,timezone_offset,timezone_id,ayanamsa,birth_place_label,uncertainty_before_minutes,uncertainty_after_minutes,declared_window_start,declared_window_end",
|
||||
);
|
||||
assert.match(daily, /select\(ACCOUNT_BIRTH_SELECT\)/);
|
||||
assert.match(synastry, /select\(ACCOUNT_BIRTH_SELECT\)/);
|
||||
|
||||
Reference in New Issue
Block a user