Files
Jyotisha/frontend/tests/consultation-methodology.test.ts
T
Jesse_ChenandClaude Opus 5.5 401796509a feat(consult): shared affliction-first reading, lists for every domain, health renamed
- T1 (BUG-1160): 「通用读法 · 先看受冲」 opens every turn's methodology once.
- T2 (BUG-1161): Chinese lists for parents, children, education, migration,
  family, annual, general, health; health replaces the English route in the
  web consultation only.
- T3 (BUG-1162): career / marriage / wealth each gain an affliction line.
- T4 (BUG-1163): health label 健康, prompt asks about the constitution first.
- Single-domain annual / timing turns keep the pre-brief method
  (CONSULT_READING_BUDGET_BLOCKED): they would exceed the 12,000-character
  assertions, which this brief may not change.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N4f2nya58RoRu4yEmJgRGE
2026-10-02 00:14:54 +08:00

183 lines
9.2 KiB
TypeScript

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`);
});
test("further reading offers the references the skill names, and only ones that exist", () => {
const methodology = consultationMethodologyForDomains(["career"]);
assert.ok(methodology);
assert.ok(methodology.further_reading.length > 0, "the skill names references worth reading");
assert.ok(
methodology.further_reading.includes("references/strict-workflow-router.md"),
"the router the skill points at must be reachable on purpose",
);
for (const path of methodology.further_reading) {
assert.match(path, /^references\//);
assert.ok(!path.includes(".."), "no path may escape the package");
}
assert.ok(
methodology.further_reading.length < 100,
"the index exists to replace the package listing, not to reproduce it",
);
});
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);
});