Files
Jyotisha/frontend/tests/report-blocked-repairs.test.ts
T
2026-09-03 23:57:27 +08:00

249 lines
11 KiB
TypeScript

import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import test from "node:test";
import { buildReportEvidenceBundleV2 } from "../src/lib/personal-report-generation.ts";
import { REPORT_THEME_IDS } from "../src/lib/report-theme-evidence-plan.ts";
type JsonRecord = Record<string, unknown>;
const GOLDEN = JSON.parse(readFileSync(
new URL("./fixtures/consultation-workflow-report-blocked-repairs-golden.json", import.meta.url),
"utf8",
)) as {
themes: Record<string, JsonRecord>;
};
const SKILL = {
name: "jyotish-vedic-astrology",
version: "6.9.14",
sha256: "a".repeat(64),
sourceCommit: "b".repeat(40),
} as const;
const SUBJECT = {
displayName: "冒烟用户",
birthTimeStatus: "accepted" as const,
birthPlaceLabel: "冒烟市",
};
const FOUR_THEMES = ["career", "marriage", "timing", "wealth"] as const;
function clone<T>(value: T): T {
return JSON.parse(JSON.stringify(value)) as T;
}
function goldenWorkflow(theme: string): JsonRecord {
const workflow = clone(GOLDEN.themes[theme] ?? GOLDEN.themes.career);
if (theme === "health" || theme === "health_pressure") {
const modules = chartModules(workflow);
const source = clone((modules.varga_full as JsonRecord).D24_Chaturvimsamsa);
(modules.varga_full as JsonRecord).D6_Shashthamsa = clone(source);
(modules.varga_full as JsonRecord).D8_Ashtamsa = clone(source);
(modules.varga_full as JsonRecord).D30_Trimsamsa = clone(source);
const sections = (workflow.machine_evidence_packet as JsonRecord).sections as JsonRecord;
sections.D8 = { status: "used", source_path: "modules.varga_full.D8" };
(workflow.chart as JsonRecord).ai_prompt_pack = { evidence_snapshot: {
health_narrative: {
headline: "非医疗边界:D6/D8/D30 显示压力负荷与恢复节奏需要分层观察。",
strengths: ["当前 Dasha 激活 6/8/12 宫主的部分恢复支持。"],
risks: ["相关 Shadbala 较弱时需要降低负荷。"],
boundaries: ["非医疗边界:本段不构成诊断或治疗建议。"],
},
} };
}
return workflow;
}
function buildBundle(themes: readonly string[], workflows?: readonly JsonRecord[]) {
return buildReportEvidenceBundleV2({
workflows: themes.map((theme, index) => ({
theme,
workflow: workflows?.[index] ?? goldenWorkflow(theme),
})),
subject: SUBJECT,
requestedThemes: [...themes],
reportType: "personal_full",
presentationMode: "default",
skillSnapshot: SKILL,
});
}
function chartModules(workflow: JsonRecord): JsonRecord {
const chart = workflow.chart as JsonRecord;
return chart.modules as JsonRecord;
}
function executedTechniques(bundle: ReturnType<typeof buildBundle>): string[] {
return bundle.executionLedger
.filter((receipt) => receipt.executed && receipt.status !== "blocked")
.map((receipt) => receipt.technique)
.sort();
}
test("golden fixture locks the live engine varga and transits shape", () => {
const vargaFull = chartModules(GOLDEN.themes.career).varga_full as JsonRecord;
assert.ok(vargaFull.D2_Hora);
assert.ok(vargaFull.D9_Navamsa);
assert.ok(vargaFull.D10_Dasamsa);
assert.ok(vargaFull.D11_Rudramsa);
assert.ok(vargaFull.D24_Chaturvimsamsa);
for (const key of ["D2_Hora", "D9_Navamsa", "D10_Dasamsa", "D11_Rudramsa", "D24_Chaturvimsamsa"] as const) {
const varga = vargaFull[key] as JsonRecord;
const ascendant = varga.ascendant as JsonRecord;
const planets = varga.planets as JsonRecord;
assert.equal(typeof ascendant.sign, "string", `${key} ascendant.sign`);
assert.equal(typeof ascendant.sign_index, "number", `${key} ascendant.sign_index`);
assert.equal(typeof ascendant.degree, "number", `${key} ascendant.degree`);
assert.equal(varga.Ascendant, undefined, `${key} must not use the old Ascendant key as the live shape`);
assert.ok(planets.Sun && typeof (planets.Sun as JsonRecord).sign_index === "number", `${key} planets.Sun`);
assert.ok(Array.isArray(varga.house_chart));
assert.equal((varga.house_chart as unknown[]).length, 12);
}
const transits = chartModules(GOLDEN.themes.career).transits as JsonRecord;
assert.equal(typeof transits.status, "string");
const karakas = (chartModules(GOLDEN.themes.career).jaimini as JsonRecord).chara_karakas as JsonRecord;
assert.equal(typeof (karakas.AmK as JsonRecord).planet, "string");
assert.equal(typeof (karakas.DK as JsonRecord).planet, "string");
});
test("deriveVargaHousesFromEngine reads the live planets-dict shape and the old top-level shape", () => {
const goldenBundle = buildBundle(["wealth"], [goldenWorkflow("wealth")]);
const required = ["D1", "D2", "D9", "D10", "D11", "D24"];
for (const id of required) {
assert.ok(goldenBundle.charts.some((chart) => chart.id === id), `golden missing ${id}`);
}
const d2 = goldenBundle.charts.find((chart) => chart.id === "D2")!;
assert.equal(d2.houses.length, 12);
assert.equal(d2.houses[0].sign, "Leo");
assert.ok(d2.houses[0].occupants.includes("Sun"));
const oldShape = goldenWorkflow("wealth");
const modules = chartModules(oldShape);
// Keep the old 643f6f7a-era shape as a compatibility path: Ascendant/sign_idx
// plus top-level planet keys. Live engine responses no longer look like this.
modules.varga_full = {
D2_Hora: {
_meta: { fixture: "legacy-shape" },
Ascendant: { sign: "Cancer", sign_idx: 3, degree_in_sign: 8.25 },
Sun: { sign: "Leo", sign_idx: 4 },
Moon: { sign: "Virgo", sign_idx: 5 },
},
D11_Rudramsa: {
Ascendant: { sign: "Scorpio", sign_idx: 7 },
Sun: { sign: "Sagittarius", sign_idx: 8 },
},
};
const oldBundle = buildBundle(["wealth"], [oldShape]);
const oldD2 = oldBundle.charts.find((chart) => chart.id === "D2");
const oldD11 = oldBundle.charts.find((chart) => chart.id === "D11");
assert.ok(oldD2);
assert.ok(oldD11);
assert.equal(oldD2.houses[0].sign, "Cancer");
assert.ok(oldD2.houses[1].occupants.includes("Sun"));
assert.equal(oldD11.houses[0].sign, "Scorpio");
});
test("Transit receipt upserts only when modules.transits.status is executed", () => {
const executed = buildBundle(["timing"], [goldenWorkflow("timing")]);
assert.ok(executed.claimCards.some((card) => card.theme === "timing"));
assert.equal(executed.blockedSections.some((section) => section.theme === "timing"), false);
const transit = executed.executionLedger.find((receipt) => receipt.technique === "Transit");
assert.ok(transit);
assert.equal(transit.executed, true);
assert.equal(transit.status, "partial");
const missing = goldenWorkflow("timing");
delete chartModules(missing).transits;
const missingBundle = buildBundle(["timing"], [missing]);
assert.equal(missingBundle.claimCards.some((card) => card.theme === "timing"), false);
assert.ok(missingBundle.blockedSections.some((section) => (
section.theme === "timing" && section.reason.includes("Transit")
)));
assert.equal(
missingBundle.executionLedger.some((receipt) => receipt.technique === "Transit" && receipt.executed),
false,
);
const blocked = goldenWorkflow("timing");
(chartModules(blocked).transits as JsonRecord).status = "blocked";
const blockedBundle = buildBundle(["timing"], [blocked]);
assert.equal(blockedBundle.claimCards.some((card) => card.theme === "timing"), false);
assert.equal(
blockedBundle.executionLedger.some((receipt) => receipt.technique === "Transit" && receipt.executed),
false,
);
});
test("AmK and DK receipts require chara_karakas planet rows", () => {
const full = buildBundle(["career", "marriage"]);
assert.ok(full.claimCards.some((card) => card.theme === "career"));
assert.ok(full.claimCards.some((card) => card.theme === "marriage"));
assert.ok(executedTechniques(full).includes("AmK"));
assert.ok(executedTechniques(full).includes("DK"));
const noKarakas = goldenWorkflow("career");
delete (chartModules(noKarakas).jaimini as JsonRecord).chara_karakas;
const careerOnly = buildBundle(["career"], [noKarakas]);
const blocked = careerOnly.blockedSections.find((section) => section.theme === "career");
assert.ok(blocked);
assert.ok(blocked.reason.includes("AmK"));
assert.equal(blocked.reason.includes("Transit"), false);
});
test("golden four-theme pipeline emits claim cards and no blocked sections", () => {
const bundle = buildBundle(FOUR_THEMES);
assert.deepEqual(bundle.claimCards.map((card) => card.theme).sort(), [...FOUR_THEMES].sort());
assert.deepEqual(bundle.blockedSections.map((section) => section.theme), []);
const chartIds = bundle.charts.map((chart) => chart.id).sort();
for (const id of ["D1", "D2", "D9", "D10", "D11", "D24"]) {
assert.ok(chartIds.includes(id), `charts missing ${id}: ${chartIds.join(",")}`);
}
const executed = executedTechniques(bundle);
for (const technique of ["AmK", "DK", "Transit", "D2", "D11"]) {
assert.ok(executed.includes(technique), `ledger missing executed ${technique}`);
}
assert.equal(
bundle.claimCards.some((card) => card.theme === "wealth")
&& !bundle.blockedSections.some((section) => section.reason.includes("结构化分盘")),
true,
);
});
test("nine requested themes close from the same golden chart layers", () => {
const bundle = buildBundle(REPORT_THEME_IDS);
const claimed = new Set(bundle.claimCards.map((card) => card.theme));
const blocked = Object.fromEntries(
bundle.blockedSections.map((section) => [section.theme, section.reason]),
);
const rows = REPORT_THEME_IDS.map((theme) => ({
theme,
status: claimed.has(theme) ? "claim" : "blocked",
reason: blocked[theme] ?? null,
}));
for (const row of rows) {
assert.equal(row.status, "claim", `${row.theme} still blocked: ${row.reason}`);
}
});
test("health requires D1 D6 D8 D30 and Vimshottari while Transit stays optional", () => {
const full = goldenWorkflow("health");
delete chartModules(full).transits;
const bundle = buildBundle(["health"], [full]);
const card = bundle.claimCards.find((item) => item.theme === "health_pressure");
assert.ok(card);
assert.match(card.conclusion, /D6\/D8\/D30/);
assert.match(card.conclusion, /非医疗/);
assert.equal(bundle.blockedSections.some((item) => item.theme === "health_pressure"), false);
assert.equal(card.assertionLevel, "parameter_sensitive");
for (const missing of ["D6_Shashthamsa", "D8_Ashtamsa", "D30_Trimsamsa"] as const) {
const workflow = goldenWorkflow("health");
delete (chartModules(workflow).varga_full as JsonRecord)[missing];
const technique = missing.split("_")[0];
delete ((workflow.machine_evidence_packet as JsonRecord).sections as JsonRecord)[technique];
const blocked = buildBundle(["health"], [workflow]);
assert.ok(blocked.blockedSections.some((item) => item.theme === "health_pressure"));
}
});