Files
Jyotisha/frontend/tests/personal-report-plan.test.ts
2026-08-15 05:10:37 +08:00

101 lines
4.9 KiB
TypeScript

import assert from "node:assert/strict";
import test from "node:test";
import {
buildPersonalReportSectionPlan,
validatePersonalReportSectionPlan,
} from "../src/lib/personal-report-plan.ts";
import {
finalizeReportEvidenceBundleV2,
type ReportEvidenceBundleV2,
} from "../src/lib/report-evidence-bundle-v2.ts";
function bundleFixture(): ReportEvidenceBundleV2 {
return finalizeReportEvidenceBundleV2({
schemaVersion: "report_evidence_bundle.v2",
subject: { displayName: "测试用户", birthTimeStatus: "accepted", birthPlaceLabel: "北京" },
requestedThemes: ["career", "wealth", "timing"],
reportType: "personal_full",
presentationMode: "default",
calculationProfile: {
calculationHash: "a".repeat(64), calculationHashDerived: false,
birthTimeStatus: "accepted", ayanamsa: "Lahiri", nodeMode: "mean", houseSystem: "whole_sign",
vimshottari: null, narayana: null,
},
skill: { name: "jyotish-personal-report", version: "1.0.0", sha256: "b".repeat(64), sourceCommit: null },
charts: [{
id: "D1", title: "本命盘 D1", ascendant: { sign: "Leo", degree: 20.5 },
houses: Array.from({ length: 12 }, (_, index) => ({ number: index + 1, sign: "Leo", signDerived: false, occupants: [] })),
planets: [],
}],
claimCards: [{
id: "ev-claim-career", theme: "career", section: "事业", conclusion: "事业结论",
supportingFacts: [{ id: "ev-fact-career", label: "D10", value: "已执行", evidenceRef: "ev-tech-d10", status: "verified" }],
counterFacts: [], executedTechniqueRefs: ["ev-tech-d10"], assertionLevel: "single_system_inference",
timingBoundary: null, verificationQuestions: [],
}],
blockedSections: [
{ id: "ev-blocked-wealth", theme: "wealth", section: "财富", reason: "缺少 D2", missingTechniqueRefs: ["ev-tech-d2"] },
{ id: "ev-blocked-timing", theme: "timing", section: "应期", reason: "时间证据不足", missingTechniqueRefs: ["ev-tech-vimshottari"] },
],
conflicts: [],
executionLedger: [
{ id: "ev-tech-d10", technique: "D10", status: "verified", executed: true, note: "已执行" },
{ id: "ev-tech-d2", technique: "D2", status: "blocked", executed: false, note: "未提供" },
{ id: "ev-tech-vimshottari", technique: "Vimshottari", status: "blocked", executed: false, note: "未提供" },
],
evidenceRefs: [
{ id: "ev-tech-d10", technique: "D10", status: "verified" },
{ id: "ev-tech-d2", technique: "D2", status: "blocked" },
{ id: "ev-tech-vimshottari", technique: "Vimshottari", status: "blocked" },
],
answerPolicy: {
canAnswerPreciseTiming: false,
birthTimePolicy: "accepted_directional_only",
deterministicClaimsForbiddenFor: ["timing"],
},
});
}
test("section plan covers every requested theme exactly once and preserves blocked themes", () => {
const bundle = bundleFixture();
const plan = buildPersonalReportSectionPlan(bundle, "standard");
const thematic = plan.sections.filter((section) => section.kind === "thematic");
assert.deepEqual(thematic.map((section) => section.theme), bundle.requestedThemes);
assert.equal(thematic.find((section) => section.theme === "career")?.disposition, "write");
assert.equal(thematic.find((section) => section.theme === "wealth")?.disposition, "blocked");
assert.equal(thematic.find((section) => section.theme === "timing")?.disposition, "blocked");
assert.equal(plan.sections.find((section) => section.kind === "current_phase")?.disposition, "blocked");
});
test("four report depths use bounded configurable ranges instead of one-character completeness", () => {
const bundle = bundleFixture();
const depths = ["concise", "standard", "deep", "research"] as const;
const minimums = depths.map((depth) => {
const plan = buildPersonalReportSectionPlan(bundle, depth);
return plan.sections.find((section) => section.kind === "thematic")!.targetCharacters.min;
});
assert.deepEqual(minimums, [120, 260, 480, 600]);
assert.ok(minimums.every((minimum) => minimum > 1));
});
test("plan validation rejects omitted themes, wrong blocked disposition and dangling refs", () => {
const bundle = bundleFixture();
const plan = buildPersonalReportSectionPlan(bundle, "standard");
assert.throws(() => validatePersonalReportSectionPlan({
...plan,
sections: plan.sections.filter((section) => section.theme !== "wealth"),
}, bundle), /report_plan_theme_coverage_invalid:wealth/);
assert.throws(() => validatePersonalReportSectionPlan({
...plan,
sections: plan.sections.map((section) => section.theme === "wealth"
? { ...section, disposition: "write" }
: section),
}, bundle), /report_plan_theme_disposition_invalid:wealth/);
assert.throws(() => validatePersonalReportSectionPlan({
...plan,
sections: plan.sections.map((section) => section.theme === "career"
? { ...section, evidenceRefs: ["ev-missing"] }
: section),
}, bundle), /report_plan_dangling_evidence_ref:ev-missing/);
});