Files
Jyotisha/frontend/tests/consultation-domain-registry.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

123 lines
7.9 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 { readFileSync } from "node:fs";
import test from "node:test";
import {
consultationDomainIds,
consultationDomainRegistry,
normalizeConsultationDomain,
validateConsultationDomainPlan,
} from "../src/lib/consultation-domain-registry.ts";
import { consultationInputSchema } from "../src/mastra/consultation-workflow.ts";
import { chatSessionWriteSchema } from "../src/lib/chat-session-write-contract.ts";
import {
defaultGuidedJyotishTopics,
generalGuidedJyotishTopics,
} from "../src/lib/guided-jyotish-topics.ts";
// 原值:十个领域,无 parents / children。
// 新值:family 后面加 parents、children(数据卡子领域,引擎侧按 family 计算)。
// 原因:TASK-consult-evidence-card-20260927 D2。会话主题(chat_sessions.theme CHECK)仍是原十个,见 sessionThemes。
const expected = ["career", "marriage", "wealth", "health", "education", "migration", "family", "parents", "children", "annual", "timing", "general"];
const sessionThemes = ["career", "marriage", "wealth", "health", "education", "migration", "family", "annual", "timing", "general"];
test("domain registry is the complete allowlist used by UI and workflow input", () => {
assert.deepEqual([...consultationDomainIds], expected);
assert.deepEqual(consultationDomainRegistry.map((domain) => domain.id), expected);
assert.deepEqual(defaultGuidedJyotishTopics.map((topic) => topic.id), expected);
for (const theme of expected) {
assert.equal(consultationInputSchema.safeParse({ year: 2000, month: 1, day: 1, hour: 0, minute: 0, lat: 0, lon: 0, tz: 0, city: "Test", question: "test", theme }).success, true);
}
});
test("aliases normalize to stable persisted domain ids", () => {
assert.equal(normalizeConsultationDomain("relationship"), "marriage");
assert.equal(normalizeConsultationDomain("finance"), "wealth");
assert.equal(normalizeConsultationDomain("health-pressure"), "health");
assert.equal(normalizeConsultationDomain("home"), "migration");
assert.equal(normalizeConsultationDomain("unknown"), null);
});
test("server domain plans are allowlisted, ordered and deduplicated", () => {
assert.deepEqual(validateConsultationDomainPlan(["career", "finance", "career", "home"]), ["career", "wealth", "migration"]);
assert.throws(() => validateConsultationDomainPlan([]), /invalid_consultation_domain_plan/);
assert.throws(() => validateConsultationDomainPlan(["career", "prashna"]), /unsupported_consultation_domain/);
});
test("default guided topics derive every canonical domain with evidence and claim boundaries", () => {
// Moved from starter-questions.test.ts when homepage theme cards were removed.
const guidedTopicsSource = readFileSync(new URL("../src/lib/guided-jyotish-topics.ts", import.meta.url), "utf8");
// 原值:assert.equal(consultationDomainIds.length, 10);新值:12;原因:D2 新增 parents / children。
assert.equal(consultationDomainIds.length, 12);
assert.deepEqual(consultationDomainRegistry.map((domain) => domain.id), [...consultationDomainIds]);
assert.match(guidedTopicsSource, /consultationDomainRegistry\.map/);
assert.deepEqual(defaultGuidedJyotishTopics.map((topic) => topic.id), [...consultationDomainIds]);
assert.equal(defaultGuidedJyotishTopics.length, consultationDomainRegistry.length);
for (const [index, topic] of defaultGuidedJyotishTopics.entries()) {
const domain = consultationDomainRegistry[index];
assert.equal(topic.id, domain.id);
assert.equal(topic.label, domain.label);
assert.equal(topic.prompt, domain.prompt);
assert.equal(topic.strictWorkflowRoute, domain.strictWorkflowRoute);
assert.deepEqual(topic.evidencePreview, [...domain.evidencePreview]);
assert.equal(topic.confidenceCap, domain.confidenceCap);
assert.equal(topic.claimBoundary, domain.claimBoundary);
}
const domainById = new Map(consultationDomainRegistry.map((domain) => [domain.id, domain]));
assert.match(domainById.get("career")?.requiredLayers.join(" ") ?? "", /D10/);
assert.match(domainById.get("marriage")?.requiredLayers.join(" ") ?? "", /D9/);
assert.match(domainById.get("wealth")?.requiredLayers.join(" ") ?? "", /Ashtakavarga/);
assert.match(domainById.get("timing")?.requiredLayers.join(" ") ?? "", /negative holdout gate/);
});
test("general guided topics keep user-centered prompts and the same claim boundaries", () => {
// Moved from starter-questions.test.ts. Homepage no longer renders these cards;
// the registry remains the contract for personal-report and no-minute copy.
assert.deepEqual(generalGuidedJyotishTopics.map((topic) => topic.id), [...consultationDomainIds]);
assert.deepEqual(generalGuidedJyotishTopics.map((topic) => topic.prompt), [
"请帮我梳理目前的事业方向和下一步重点。",
"请帮我看看我在关系中容易重复什么模式。",
"请帮我分析目前的财富重点、风险和更稳妥的选择。",
// 原值「请从非医疗诊断的角度,帮我看看近期的身心压力和调整重点。」→ 新值如下;
// 原因:TASK-consult-affliction-reading-20261001 D4 健康改口径,先讲体质底子。
"请从非医疗诊断的角度,帮我看看我的身体底子怎样,哪些方面要多留意。",
"请帮我看看我更适合怎样学习,以及如何安排下一步。",
"请帮我分析现阶段是否适合迁居、置业或考虑海外发展。",
"请帮我看看家庭关系中最需要处理的问题和责任边界。",
// 新增两行(D2):父母、子女的无出生分钟版问法。
"请帮我看看我和父母之间最需要处理的问题。",
"请帮我看看我和孩子相处时最需要留意的地方。",
"请帮我看看未来一年的主要趋势、机会和需要留意的阶段。",
"请帮我看看目前适合推进什么,哪些事情需要再等等。",
"请结合我的情况,帮我找出现在最值得优先处理的三个问题。",
]);
for (const topic of generalGuidedJyotishTopics) {
const personalTopic = defaultGuidedJyotishTopics.find((candidate) => candidate.id === topic.id);
assert.ok(personalTopic);
assert.equal(topic.label, personalTopic.label);
assert.equal(topic.strictWorkflowRoute, personalTopic.strictWorkflowRoute);
assert.deepEqual(topic.evidencePreview, personalTopic.evidencePreview);
assert.equal(topic.confidenceCap, personalTopic.confidenceCap);
assert.equal(topic.claimBoundary, personalTopic.claimBoundary);
assert.notEqual(topic.prompt, personalTopic.prompt);
}
assert.ok(generalGuidedJyotishTopics.every((topic) => /我/.test(topic.prompt)));
assert.ok(generalGuidedJyotishTopics.every((topic) => !/印度占星|一般如何|通常|哪些因素|证据层/.test(topic.prompt)));
});
test("session persistence accepts every canonical domain and rejects product routes", () => {
// 原值:遍历 expected(全部领域)都能落库。
// 新值:遍历 sessionThemes(chat_sessions.theme CHECK 的十个)能落库;parents / children 不能落库。
// 原因:D2 的两个子领域只作用于数据卡,不改数据库;写入口先挡住,免得撞 CHECK 变成 500。
for (const theme of ["parents", "children"]) {
const parsed = chatSessionWriteSchema.safeParse({ title: "Test", theme, model_id: "test", messages: [], session_type: "consultation", rectification_case_id: null, updated_at: "2026-08-14T00:00:00.000Z" });
assert.equal(parsed.success, false, theme);
}
for (const theme of sessionThemes) {
const parsed = chatSessionWriteSchema.safeParse({ title: "Test", theme, model_id: "test", messages: [], session_type: "consultation", rectification_case_id: null, updated_at: "2026-08-14T00:00:00.000Z" });
assert.equal(parsed.success, true, theme);
}
assert.equal(chatSessionWriteSchema.safeParse({ title: "Test", theme: "muhurta", model_id: "test", messages: [], session_type: "consultation", rectification_case_id: null, updated_at: "2026-08-14T00:00:00.000Z" }).success, false);
});