216 lines
9.5 KiB
TypeScript
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/);
|
|
});
|