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:
Jesse_Chen
2026-09-06 21:46:16 +08:00
co-authored by Cursor
parent e428e9d056
commit cfcd369d4f
36 changed files with 2290 additions and 402 deletions
+39 -1
View File
@@ -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\)/);
+8 -5
View File
@@ -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, /1030 秒/);
});
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/);
});
+10 -10
View File
@@ -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\)/);