Files
Jyotisha/frontend/tests/consultation-methodology.test.ts
T
jesse-ux 3874a328a5 feat(consult): copy the full-data packet into a per-domain judgment table
Dialogue cards now take numbers from the same packet as the full-data report. Each of the twelve domains gets one table the card builder assembles. Lookups allow three calls a turn. further_reading is a per-domain technique index. BUG-1293, BUG-1294, BUG-1295.
2026-10-09 10:00:45 +08:00

183 lines
9.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import assert from "node:assert/strict";
import test from "node:test";
import {
consultationMethodologyForDomains,
markdownSection,
METHODOLOGY_SECTION_MAX_CHARS,
METHODOLOGY_TOTAL_MAX_CHARS,
} from "../src/lib/consultation-methodology.ts";
import {
CONDENSED_CHECKLIST_SOURCE,
CONDENSED_SHARED_READING_TITLE,
CONSULT_BRIEF_CHECKLIST_SOURCE,
} from "../src/lib/consultation-condensed-checklist.ts";
import { consultationDomainIds } from "../src/lib/consultation-domain-registry.ts";
import { resolveLiveJyotishSkill } from "../src/lib/skill-package-registry.ts";
test("the route's own strict checklist reaches the answer, not just the package listing", () => {
const methodology = consultationMethodologyForDomains(["career"]);
assert.ok(methodology, "career must resolve a methodology");
const career = methodology.sections.find((section) => section.title.includes("career-timing-strict"));
assert.ok(career, "the career strict route must be delivered as its own section");
// Former: source was references/strict-workflow-router.md and the text the full
// route ("Opportunity contact" …), plus event_judgment_career.md as a second section.
// New: the condensed consultation checklist, condensed from both, as one section.
// Reason: TASK-consult-evidence-card-v2-20260927 T4 (方案 B, consultation only).
assert.equal(career.source, CONDENSED_CHECKLIST_SOURCE);
assert.match(career.source, /strict-workflow-router\.md/);
// The checklist is only useful if the specific instructions arrive with it.
assert.match(career.text, /D10/);
assert.match(career.text, /接触/);
assert.ok(
methodology.sections.some((section) => section.title.includes("Shared mandatory baseline")),
"every route is read against the shared baseline",
);
// Former: Full-spectrum invocation and Event judgment skeleton were tool-result sections.
// New: those two live in the cached system block; sections keep the domain checklists.
// Reason: they were resent with every tool result outside any cache prefix.
assert.equal(
methodology.sections.filter((section) => (
section.title.includes("Full-spectrum") || section.title.includes("Event judgment")
)).length,
0,
);
// 原值:只排除 Shared mandatory baseline;新值:同时排除每轮一次的「通用读法 · 先看受冲」;
// 原因:TASK-consult-affliction-reading-20261001 T1 把通用读法放在 sections 最前面,它不是领域清单。
const domainSections = methodology.sections.filter((section) => (
!section.title.includes("Shared mandatory baseline") && section.title !== CONDENSED_SHARED_READING_TITLE
));
// Former: 2 (strict route + event judgment). New: 1 (condensed list). Reason: as above.
assert.equal(domainSections.length, 1);
assert.deepEqual(methodology.domains_without_strict_checklist, []);
});
test("a route the skill declares no checklist for gets this product's own list, never another route's", () => {
const methodology = consultationMethodologyForDomains(["education"]);
assert.ok(methodology);
// 原值:["education"](路由没声明清单就报缺、不替代);新值:[],学业发本产品自己的中文清单;
// 原因:TASK-consult-affliction-reading-20261001 决策 D6 对普通对话推翻「报缺不替代」。
assert.deepEqual(methodology.domains_without_strict_checklist, []);
const education = methodology.sections.find((section) => section.title.includes("consult-education"));
assert.ok(education);
assert.equal(education.source, CONSULT_BRIEF_CHECKLIST_SOURCE);
assert.ok(
!methodology.sections.some((section) => section.title.includes("timing-strict") || section.title.includes("verification-strict")),
"no strict section may be substituted for a route that has none",
);
assert.ok(
methodology.sections.some((section) => section.title.includes("Shared mandatory baseline")),
"the baseline still applies",
);
// Former: full-spectrum still arrived as a methodology section for domains with no checklist.
// New: full-spectrum is bound in the system block; this plan only reports the missing checklist.
assert.equal(
methodology.sections.filter((section) => section.title.includes("Full-spectrum")).length,
0,
);
});
test("health uses the consultation's Chinese health list in place of health-timing-strict", () => {
const methodology = consultationMethodologyForDomains(["health"]);
assert.ok(methodology);
assert.deepEqual(methodology.domains_without_strict_checklist, []);
// 原值:标题含 health-timing-strict(路由英文清单);新值:consult-health 中文清单,来源是任务书;
// 原因:TASK-consult-affliction-reading-20261001 决策 D4/D5,普通对话健康改口径(报告面不变)。
assert.equal(methodology.sections.some((section) => section.title.includes("health-timing-strict")), false);
const health = methodology.sections.find((section) => section.title.includes("consult-health"));
assert.ok(health);
assert.equal(health.source, CONSULT_BRIEF_CHECKLIST_SOURCE);
assert.match(health.text, /体质底子/);
assert.match(health.text, /D6/);
assert.match(health.text, /D30/);
});
test("wealth uses finance-timing-strict as the live checklist, with the wealth alias in the heading", () => {
const methodology = consultationMethodologyForDomains(["wealth"]);
assert.ok(methodology);
assert.deepEqual(methodology.domains_without_strict_checklist, []);
const wealth = methodology.sections.find((section) => section.title.includes("finance-timing-strict"));
assert.ok(wealth, "wealth must deliver finance-timing-strict, not the retired primary name");
// Former: the full router section, whose heading names the legacy alias (wealth-timing-strict).
// New: the condensed consultation checklist condensed from that section, titled by the live name.
// Reason: TASK-consult-evidence-card-v2-20260927 T4 (方案 B, consultation only).
assert.equal(wealth.source, CONDENSED_CHECKLIST_SOURCE);
assert.match(wealth.text, /D2/);
assert.ok(
!methodology.sections.some((section) => (
section.title.includes("wealth-timing-strict") && !section.title.includes("finance-timing-strict")
)),
"the old wealth route name is an alias, not a second checklist",
);
});
test("a multi-domain plan carries every executed route's checklist once", () => {
const methodology = consultationMethodologyForDomains(["career", "timing", "career"]);
assert.ok(methodology);
const titles = methodology.sections.map((section) => section.title);
assert.equal(titles.filter((title) => title.includes("career-timing-strict")).length, 1);
assert.equal(titles.filter((title) => title.includes("event-timing-strict")).length, 1);
});
test("no plan can spend the answer's context on method", () => {
for (const domain of consultationDomainIds) {
const methodology = consultationMethodologyForDomains([domain]);
assert.ok(methodology, `${domain} must resolve`);
const total = methodology.sections.reduce((sum, section) => sum + section.text.length, 0);
assert.ok(
total <= METHODOLOGY_TOTAL_MAX_CHARS,
`${domain} delivered ${total} chars, over the ${METHODOLOGY_TOTAL_MAX_CHARS} budget`,
);
for (const section of methodology.sections) {
assert.ok(section.text.length <= METHODOLOGY_SECTION_MAX_CHARS);
}
}
});
test("the widest legal plan still fits the budget", () => {
const methodology = consultationMethodologyForDomains(["career", "marriage", "wealth", "timing"]);
assert.ok(methodology);
const total = methodology.sections.reduce((sum, section) => sum + section.text.length, 0);
assert.ok(total <= METHODOLOGY_TOTAL_MAX_CHARS, `delivered ${total} chars`);
});
// 原值: further_reading 是技能点名的 references/ 路径,含 strict-workflow-router.md,长度 < 100
// 新值: 每领域 `domain: references/...`,1 到 8 条,路径在技能包里,不含 oracle / governance
// 原因: 产品 2026-10-08 把资料目录改成按领域的技法文件(BUG-1295)
test("further reading is a per-domain technique index of files that exist", () => {
for (const domain of consultationDomainIds) {
const methodology = consultationMethodologyForDomains([domain]);
assert.ok(methodology, domain);
const rows = methodology.further_reading;
assert.ok(rows.length > 0 && rows.length <= 8, `${domain} ${rows.length}`);
for (const row of rows) {
assert.match(row, new RegExp(`^${domain}: references\\/`));
assert.equal(row.includes("/oracle/"), false);
assert.equal(row.toLowerCase().includes("governance"), false);
assert.equal(row.includes(".."), false);
}
}
});
test("the delivered method is quoted from the live skill tree", () => {
const methodology = consultationMethodologyForDomains(["career"]);
assert.ok(methodology);
assert.equal(methodology.skill, "jyotish-vedic-astrology");
assert.equal(methodology.version, resolveLiveJyotishSkill().version);
assert.equal(methodology.version.includes("/"), false);
});
test("a section is found by heading text so the router may be renumbered", () => {
const source = [
"# Doc",
"## 3. `career-timing-strict`",
"body one",
"## 4. next",
"body two",
].join("\n");
const section = markdownSection(source, "career-timing-strict");
assert.ok(section);
assert.match(section, /body one/);
assert.ok(!section.includes("body two"), "a section stops at the next heading");
assert.equal(markdownSection(source, "absent-route"), null);
});