正文不再整段 hold:闭合句立刻过 Pass 4,无 reject 即发(950)。 无出生分钟改为按句丢弃,全丢才用兜底句(951);该模式日期记 observe(952)。 校正流 token 级 thinking 死链按 P2 删除,测试翻转成否定合同(953)。
304 lines
12 KiB
TypeScript
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。/,
|
|
);
|
|
});
|