Files
Jyotisha/frontend/tests/report-blocked-repairs.test.ts
T
Jesse_ChenandCursor 7faf8555a5
Independent Staging Quality Gate / validate (push) Successful in 9m15s
Independent Staging Quality Gate / publish (push) Successful in 1m50s
fix(report): unblock personal-full themes with real varga, transit, and karaka evidence
The extractor only read an obsolete varga object shape, never issued Transit receipts, and the consultation response omitted Chara Karakas, so career/marriage/timing/wealth all blocked. Supply the live engine shape, fail-closed receipts, and jaimini.chara_karakas without relaxing the evidence plan.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-02 18:03:23 +08:00

209 lines
8.6 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 {
return clone(GOLDEN.themes[theme] ?? GOLDEN.themes.career);
}
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}`);
}
});