101 lines
4.9 KiB
TypeScript
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/);
|
|
});
|