249 lines
11 KiB
TypeScript
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"));
|
|
}
|
|
});
|