Files
Jyotisha/frontend/tests/consultation-birth-accuracy.test.ts
T
jesse-ux cd4775aec6 fix(consult): BUG-950~953 Pass 4 按句放行,无分钟按句丢弃
正文不再整段 hold:闭合句立刻过 Pass 4,无 reject 即发(950)。
无出生分钟改为按句丢弃,全丢才用兜底句(951);该模式日期记 observe(952)。
校正流 token 级 thinking 死链按 P2 删除,测试翻转成否定合同(953)。
2026-09-18 14:08:45 +08:00

304 lines
12 KiB
TypeScript

import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import test from "node:test";
import { prepareConsultationRoute } from "../src/lib/consultation-route-service.ts";
import {
shouldRunBirthChartWorkflow,
shouldRunDeclaredWindowWorkflow,
UNVERIFIED_BIRTH_TIME_NOTICE,
applyBirthTimeModeToWorkflowContext,
unverifiedBirthTimeNotice,
} from "../src/lib/consultation-birth-time-mode.ts";
import { applyPass4Policy, GENERAL_NO_BIRTH_TIME_REFUSAL } from "../src/lib/timing-output-guard.ts";
import {
consultationInputSchema,
runConsultationWorkflow,
toAgentConsultationContext,
} from "../src/mastra/consultation-workflow.ts";
const profile = Object.freeze({
name: "岳辰",
birth_date: "1997-08-08",
reported_birth_time: "05:30:00",
active_birth_time: "05:18:00",
birth_time_source: "approximate",
birth_time_status: "candidate",
country_code: "CN",
province_code: "130000",
city_code: "130400",
district_code: "130406",
latitude: 36.420487,
longitude: 114.209936,
timezone_offset: 8,
});
const RANGE_INTERACTION = /按时间范围|candidate windows|not_auto_rectified/;
function agentContext(rectification: Record<string, unknown>) {
return toAgentConsultationContext({
success: true,
question: "事业如何",
chart: { modules: {} },
routing: { primary_theme: "career" },
consumer_context: {
route: "career",
core_status: "ready",
available_layers: ["D1", "D9", "D10"],
missing_route_layers: [],
hard_blockers: [],
answer_policy: {
can_answer_direction: true,
can_answer_precise_timing: true,
},
},
rectification,
});
}
async function chartRoute(
mode: "verified_chart" | "unverified_birth_time",
overrides: Record<string, unknown>,
) {
return prepareConsultationRoute({
userId: "user-1",
mode,
loadProfile: async () => ({ ...profile, ...overrides }),
reserve: async () => "reserved",
});
}
test("verified_chart users send rectified accuracy and do not consume a not_auto_rectified boundary", async () => {
const prepared = await chartRoute("verified_chart", {
birth_time_status: "accepted",
birth_time_source: "approximate",
});
const toolInput = prepared.serverChart?.toolInput;
assert.equal(toolInput?.declared_accuracy, "rectified");
const parsed = consultationInputSchema.parse({
...toolInput,
question: "事业如何",
theme: "career",
entryMode: "direct_chart",
});
assert.equal(parsed.declared_accuracy, "rectified");
let requestBody = "";
const originalFetch = globalThis.fetch;
globalThis.fetch = async (_input, init) => {
requestBody = typeof init?.body === "string" ? init.body : "";
return new Response(JSON.stringify({
success: true,
chart: {},
routing: {},
consumer_context: {
route: "career",
core_status: "ready",
available_layers: [],
missing_route_layers: [],
hard_blockers: [],
answer_policy: { can_answer_direction: true, can_answer_precise_timing: true },
},
}), { status: 200 });
};
try {
await runConsultationWorkflow(parsed);
} finally {
globalThis.fetch = originalFetch;
}
assert.equal(JSON.parse(requestBody).declared_accuracy, "rectified");
const context = agentContext({
effective_accuracy: "rectified",
summary: { headline: "出生时间风险较低,可进入完整解盘" },
enabled_vargas: { D1: "enabled", D9: "enabled", D10: "enabled" },
lagna_boundary: { is_sensitive: false },
});
assert.notEqual(context.rectification.boundary, "not_auto_rectified");
assert.doesNotMatch(JSON.stringify(context), RANGE_INTERACTION);
});
test("hospital_record users send minute/hospital and never receive rectified", async () => {
const prepared = await chartRoute("unverified_birth_time", {
birth_time_source: "hospital_record",
birth_time_status: "reported",
active_birth_time: "05:18:00",
});
const toolInput = prepared.serverChart?.toolInput;
assert.equal(toolInput?.declared_accuracy, "minute");
assert.equal(toolInput?.time_source, "hospital");
assert.notEqual(toolInput?.declared_accuracy, "rectified");
const mapper = await import("../src/lib/consultation-route-service.ts");
assert.equal(typeof mapper.declaredBirthAccuracyFromProfile, "function");
assert.deepEqual(mapper.declaredBirthAccuracyFromProfile({
birthTimeStatus: "reported",
birthTimeSource: "hospital_record",
hasActiveBirthTime: true,
}), { declaredAccuracy: "minute", timeSource: "hospital" });
});
test("family_exact and approximate keep the documented downgrade without range-interaction copy", async () => {
const family = await chartRoute("unverified_birth_time", {
birth_time_source: "family_exact",
birth_time_status: "reported",
active_birth_time: null,
});
assert.equal(family.serverChart?.toolInput.declared_accuracy, "minute");
assert.equal(family.serverChart?.toolInput.time_source, "family_clear");
assert.notEqual(family.serverChart?.toolInput.declared_accuracy, "rectified");
const approximate = await chartRoute("unverified_birth_time", {
birth_time_source: "approximate",
birth_time_status: "reported",
active_birth_time: null,
});
assert.equal(approximate.serverChart?.toolInput.declared_accuracy, "15min");
assert.notEqual(approximate.serverChart?.toolInput.declared_accuracy, "rectified");
const familyContext = agentContext({
effective_accuracy: "5min",
summary: { headline: "按填报时间排盘,分钟敏感结论已标注精度" },
enabled_vargas: { D1: "enabled", D9: "enabled", D10: "enabled" },
lagna_boundary: { is_sensitive: false },
});
assert.notEqual(familyContext.rectification.boundary, "not_auto_rectified");
assert.doesNotMatch(JSON.stringify(familyContext), RANGE_INTERACTION);
const approximateContext = agentContext({
effective_accuracy: "15min",
summary: { headline: "按填报时间排盘,分钟敏感结论已标注精度" },
enabled_vargas: { D1: "enabled", D9: "enabled", D10: "enabled_with_warning" },
lagna_boundary: { is_sensitive: false },
});
assert.notEqual(approximateContext.rectification.boundary, "not_auto_rectified");
assert.doesNotMatch(JSON.stringify(approximateContext), RANGE_INTERACTION);
});
test("rectified accuracy is reserved for accepted or confirmed profiles with an active birth time", async () => {
const mapper = await import("../src/lib/consultation-route-service.ts");
const map = mapper.declaredBirthAccuracyFromProfile;
assert.equal(typeof map, "function");
assert.deepEqual(map({
birthTimeStatus: "accepted",
birthTimeSource: "approximate",
hasActiveBirthTime: true,
}), { declaredAccuracy: "rectified", timeSource: "rectified" });
assert.deepEqual(map({
birthTimeStatus: "confirmed",
birthTimeSource: "hospital_record",
hasActiveBirthTime: true,
}), { declaredAccuracy: "rectified", timeSource: "rectified" });
assert.notEqual(map({
birthTimeStatus: "accepted",
birthTimeSource: "hospital_record",
hasActiveBirthTime: false,
}).declaredAccuracy, "rectified");
assert.notEqual(map({
birthTimeStatus: "candidate",
birthTimeSource: "hospital_record",
hasActiveBirthTime: true,
}).declaredAccuracy, "rectified");
assert.notEqual(map({
birthTimeStatus: "reported",
birthTimeSource: "family_exact",
hasActiveBirthTime: true,
}).declaredAccuracy, "rectified");
});
test("declared_birth_window and general_no_birth_time never enter the natal accuracy mapper", async () => {
const windowed = await prepareConsultationRoute({
userId: "user-1",
mode: "declared_birth_window",
loadProfile: async () => ({
...profile,
reported_birth_time: null,
active_birth_time: null,
birth_time_source: "period_only",
birth_time_period: "evening",
birth_time_status: "reported",
}),
reserve: async () => "reserved",
});
assert.equal(windowed.consultationMode, "declared_birth_window");
assert.equal(windowed.serverChart, null);
assert.equal("declared_accuracy" in (windowed.declaredWindow?.toolInput ?? {}), false);
assert.equal("time_source" in (windowed.declaredWindow?.toolInput ?? {}), false);
assert.equal(shouldRunBirthChartWorkflow("declared_birth_window"), false);
assert.equal(shouldRunDeclaredWindowWorkflow("declared_birth_window"), true);
assert.equal(shouldRunBirthChartWorkflow("general_no_birth_time"), false);
assert.equal(shouldRunDeclaredWindowWorkflow("general_no_birth_time"), false);
});
test("unverified notices grade by source without changing the output guards", () => {
assert.equal(unverifiedBirthTimeNotice("hospital_record"), "使用你填报的出生时间(医院记录)排盘");
assert.equal(unverifiedBirthTimeNotice("family_exact"), "使用你填报的出生时间排盘;分钟敏感结论的置信度已降低。");
assert.equal(unverifiedBirthTimeNotice("approximate"), UNVERIFIED_BIRTH_TIME_NOTICE);
assert.equal(unverifiedBirthTimeNotice("unknown"), UNVERIFIED_BIRTH_TIME_NOTICE);
const guarded = applyBirthTimeModeToWorkflowContext({
success: true,
consumer_context: {
core_status: "ready",
answer_policy: { can_answer_direction: true, can_answer_precise_timing: true },
},
}, "unverified_birth_time", { birthTimeSource: "hospital_record" });
assert.equal(
(guarded.consumer_context as { birth_time_notice?: string }).birth_time_notice,
"使用你填报的出生时间(医院记录)排盘",
);
assert.equal(guarded.consumer_context.answer_policy.can_answer_precise_timing, true);
});
test("output guards and window/general instruction seams stay byte-stable", () => {
// 原值:恒等壳下线;Pass 4 按模式分流,窗口日期原样保留,无分钟二次仍命中才整段拒绝
// 新值:窗口日期仍原样;无分钟混合文本按句丢弃,知识句保留
// 原因:BUG-951,产品改按句丢弃,全丢才用兜底句。
const modeSource = readFileSync(new URL("../src/lib/consultation-birth-time-mode.ts", import.meta.url), "utf8");
const routeSource = readFileSync(new URL("../src/app/api/consult/route.ts", import.meta.url), "utf8");
assert.doesNotMatch(modeSource, /createBirthTimeModeOutputGuard|guardPreciseTimingOutput|guardGeneralNoBirthTimeOutput/);
assert.match(routeSource, /pass4Mode:/);
assert.doesNotMatch(routeSource, /createBirthTimeModeOutputGuard|guardPreciseTimingOutput|guardGeneralNoBirthTimeOutput/);
assert.equal(UNVERIFIED_BIRTH_TIME_NOTICE.includes("未校正填报时间"), true);
const generalFirst = applyPass4Policy(
"D9 在印度占星中通常用于观察婚姻与法则层面的成熟。\n你的上升是巨蟹座。",
"general_no_birth_time",
);
assert.equal(generalFirst.retry, true);
const general = applyPass4Policy(
"D9 在印度占星中通常用于观察婚姻与法则层面的成熟。\n你的上升是巨蟹座。",
"general_no_birth_time",
{ secondPass: true },
);
// 原值:二次整段换成 GENERAL_NO_BIRTH_TIME_REFUSAL
// 新值:一般知识句保留,个人盘句丢掉,全丢才用兜底句
// 原因:BUG-951 按句丢弃。
assert.match(general.text, /D9 在印度占星中通常用于观察婚姻与法则层面的成熟/);
assert.doesNotMatch(general.text, /你的上升是巨蟹座/);
assert.doesNotMatch(general.text, /。。/);
assert.doesNotMatch(general.text, new RegExp(GENERAL_NO_BIRTH_TIME_REFUSAL));
const windowed = applyPass4Policy(
"Rahu 大运为 2013年11月21日 至 2031年11月22日。",
"declared_birth_window",
);
assert.match(windowed.text, /2013年11月21日/);
assert.doesNotMatch(windowed.text, /具体时间已省略/);
assert.equal(windowed.steps.some((step) => step.action === "observe" && step.kind === "exact-timing"), true);
assert.match(routeSource, /function generalNoMinuteInstruction\(hasPublicDaily: boolean\) \{/);
assert.match(routeSource, /function declaredWindowInstruction\(\) \{/);
assert.match(
routeSource,
/当前没有具体出生分钟。不得计算或推断个人星盘,也不得补 00:00、时段中点或任何候选分钟。/,
);
assert.match(
routeSource,
/当前是声明出生窗口咨询,没有单一出生分钟。如需个人结构结论,必须调用 run-jyotish-window-consultation。/,
);
});