Files
Jyotisha/frontend/tests/timing-output-guard.test.ts
T
jesse-ux e32ce6247e fix(consult): BUG-945~949 领域截断、思考分片、Pass 4 按模式分流
schema 上限与执行上限解耦;校正思考改分片门;日期观察不删字,保证句与无分钟个人盘退回重写。
2026-09-18 13:17:35 +08:00

216 lines
9.5 KiB
TypeScript

import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import test from "node:test";
import {
applyPass4Policy,
detectGeneralNoBirthTimeViolations,
detectPreciseTimingViolations,
GENERAL_NO_BIRTH_TIME_REFUSAL,
} from "../src/lib/timing-output-guard.ts";
import { streamTextResponse } from "../src/lib/stream-text-response.ts";
import { parseAgentReply } from "../src/lib/agent-reply.ts";
test("detects exact dates and months when precise timing is blocked", () => {
// 原值:就地替换成「[具体时间已省略]」
// 新值:只检测,不改写;Pass 4 在有盘模式下记 observe
// 原因:任务书 P3 / BUG-948,正则当门不当刀。
const text = "你会在2027年3月15日结婚,事业将在11月转折。";
const hits = detectPreciseTimingViolations(text);
assert.equal(hits.some((item) => item.kind === "exact-timing"), true);
const report = applyPass4Policy(text, "verified_chart");
assert.equal(report.text, text);
assert.equal(report.steps.some((step) => step.action === "observe" && step.kind === "exact-timing"), true);
assert.doesNotMatch(report.text, /具体时间已省略/);
});
test("detects guarantee conclusions when the evidence contract is incomplete", () => {
const text = "我保证你一定会升职。";
const hits = detectPreciseTimingViolations(text);
assert.equal(hits.some((item) => item.kind === "guarantee"), true);
const first = applyPass4Policy(text, "verified_chart");
assert.equal(first.retry, true);
assert.equal(first.steps.some((step) => step.action === "reject" && step.kind === "guarantee"), true);
const second = applyPass4Policy(text, "verified_chart", { secondPass: true });
assert.doesNotMatch(second.text, /一定会升职/);
assert.equal(
detectPreciseTimingViolations("You will definitely get promoted.").some((item) => item.kind === "guarantee"),
true,
);
});
test("streamed dates stay intact because the guard no longer rewrites", async () => {
async function* reply() {
yield "应期是2027年";
yield "3月15日,但我保证你一定会升职。";
}
const response = streamTextResponse(reply(), {
mode: "mastra",
requestId: "00000000-0000-4000-8000-000000000098",
});
const text = await response.text();
assert.match(text, /2027年3月15日/);
assert.equal(detectPreciseTimingViolations(text).length > 0, true);
});
test("guards only visible prose and preserves AYANAM blocks across arbitrary chunk boundaries", async () => {
const suggestions = '<!--AYANAM_SUGGESTIONS:["你一定会升职吗?","D9 是什么?","先完成生时校正"]-->';
const title = "<!--AYANAM_TITLE:一般占星咨询-->";
const longVisiblePrefix = "一般知识不依赖个人星盘。".repeat(100);
async function* reply() {
yield `${longVisiblePrefix}正文说你将在2027年`;
yield "3月15日一定会升职。\n<!";
yield "--AYANAM_SUGGESTIONS:[\"你一定会升职吗?\",\"D9 是什么?\",\"先完成生时校正\"]--";
yield ">\n<!--AYANAM_TIT";
yield "LE:一般占星咨询-->";
}
const response = streamTextResponse(reply(), {
mode: "mastra",
requestId: "00000000-0000-4000-8000-000000000099",
});
const text = await response.text();
const parsed = parseAgentReply(text);
assert.match(text, /2027年3月15日/);
assert.match(text, /^一般知识不依赖个人星盘/);
assert.equal(detectPreciseTimingViolations(parsed.text).length > 0, true);
assert.equal(text.includes(suggestions), true);
assert.equal(text.includes(title), true);
assert.doesNotMatch(text, /<!\[/);
// The guard must keep a hidden block intact across chunks, but parsing strips it:
// an unsolicited suggestion block must never reach the transcript as text or as data.
assert.doesNotMatch(parsed.text, /AYANAM_SUGGESTIONS|你一定会升职吗/);
assert.equal(parsed.title, "一般占星咨询");
});
test("general mode structurally rejects personalized chart placements in Chinese and English", () => {
const unsafeClaims = [
"你的七宫落入摩羯。",
"盘面显示你的事业宫很强。",
"你的金星落第七宫。",
"你的上升落在巨蟹座。",
"你的 D9 显示婚姻会晚一些。",
"Your Venus is in the 7th house.",
"Your ascendant falls in Cancer.",
"Your D9 chart shows a strong marriage house.",
];
for (const claim of unsafeClaims) {
const hits = detectGeneralNoBirthTimeViolations(claim);
assert.equal(hits.some((item) => item.kind === "personal-chart" || item.kind === "exact-timing"), true, claim);
const first = applyPass4Policy(claim, "general_no_birth_time");
assert.equal(first.retry, true, claim);
const second = applyPass4Policy(claim, "general_no_birth_time", { secondPass: true });
assert.equal(second.text, GENERAL_NO_BIRTH_TIME_REFUSAL, claim);
}
assert.equal(
applyPass4Policy("第七宫在占星概念中常与关系相关。", "general_no_birth_time").text,
"第七宫在占星概念中常与关系相关。",
);
assert.equal(
applyPass4Policy("Venus is generally associated with relating and values.", "general_no_birth_time").text,
"Venus is generally associated with relating and values.",
);
assert.equal(
applyPass4Policy("你问的第七宫,在占星概念中常与关系相关。", "general_no_birth_time").text,
"你问的第七宫,在占星概念中常与关系相关。",
);
});
test("general mode rejects personalized chart predicates beyond possessive placements", () => {
const unsafeClaims = [
"您的上升星座为巨蟹。",
"你是巨蟹上升。",
"您有一个巨蟹上升。",
"You have Cancer rising.",
"You are Cancer rising.",
"You have a Cancer ascendant.",
"你拥有巨蟹上升。",
"In your chart, Venus occupies the seventh house.",
"For you, Venus falls in the seventh house.",
];
for (const claim of unsafeClaims) {
assert.equal(detectGeneralNoBirthTimeViolations(claim).length > 0, true, claim);
const second = applyPass4Policy(claim, "general_no_birth_time", { secondPass: true });
assert.equal(second.text, GENERAL_NO_BIRTH_TIME_REFUSAL, claim);
}
});
test("general mode preserves educational chart questions and general knowledge", () => {
const safeStatements = [
"你的问题是第七宫的一般含义。",
"你问的是第七宫的一般含义。",
"你问的第七宫在占星概念中常与关系相关。",
"关于你的第七宫的一般含义。",
"你是一个正在研究第七宫的学生。",
"你可以先了解第七宫的一般概念。",
"你有第七宫的一般问题。",
"你有金星方面的问题,可以继续问。",
"你是第七宫问题的提问者。",
"第七宫在占星概念中常与关系相关。",
"Your question is the general meaning of the seventh house.",
"You asked about the seventh house.",
"You have a question about the seventh house.",
"The seventh house is associated with relationships.",
];
for (const statement of safeStatements) {
const report = applyPass4Policy(statement, "general_no_birth_time");
assert.equal(report.text, statement);
assert.equal(report.retry, false, statement);
}
});
test("hidden AYANAM comments cannot split a personalized claim around the guard", async () => {
const title = "<!--AYANAM_TITLE:一般占星咨询-->";
const suggestions = '<!--AYANAM_SUGGESTIONS:["了解第七宫的一般概念","先完成生时校正","改问一般知识"]-->';
async function* reply() {
yield "一般知识可以说明概念。你的<!";
yield "--AYANAM_TITLE:一般占星咨询--";
yield ">金星落";
yield "第七宫。\n<!--AYANAM_SUGGEST";
yield 'IONS:["了解第七宫的一般概念","先完成生时校正","改问一般知识"]-->';
}
const response = streamTextResponse(reply(), {
mode: "mastra",
requestId: "00000000-0000-4000-8000-000000000097",
});
const text = await response.text();
const parsed = parseAgentReply(text);
assert.match(text, /一般知识可以说明概念/);
assert.match(parsed.text, /金星落/);
assert.equal(detectGeneralNoBirthTimeViolations(parsed.text).length > 0, true);
assert.equal(text.includes(title), true);
assert.equal(text.includes(suggestions), true);
assert.equal(parsed.title, "一般占星咨询");
assert.doesNotMatch(parsed.text, /AYANAM_SUGGESTIONS|了解第七宫的一般概念/);
});
test("declared birth window keeps calculated dates as interval facts", () => {
const text = "Rahu 大运为 2013年11月21日 至 2031年11月22日。按范围看应期。";
const report = applyPass4Policy(text, "declared_birth_window");
assert.equal(report.text, text);
assert.equal(report.retry, false);
assert.equal(report.steps.some((step) => step.action === "observe" && step.kind === "exact-timing"), true);
assert.doesNotMatch(report.text, /具体时间已省略/);
});
test("identity output guards are gone from production sources", () => {
const guard = readFileSync(new URL("../src/lib/timing-output-guard.ts", import.meta.url), "utf8");
const mode = readFileSync(new URL("../src/lib/consultation-birth-time-mode.ts", import.meta.url), "utf8");
const route = readFileSync(new URL("../src/app/api/consult/route.ts", import.meta.url), "utf8");
for (const source of [guard, mode, route]) {
assert.doesNotMatch(source, /function guardPreciseTimingOutput/);
assert.doesNotMatch(source, /function guardGeneralNoBirthTimeOutput/);
assert.doesNotMatch(source, /createBirthTimeModeOutputGuard/);
}
assert.match(route, /pass4Mode/);
assert.match(guard, /export function applyPass4Policy/);
});