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 = ''; const title = ""; const longVisiblePrefix = "一般知识不依赖个人星盘。".repeat(100); async function* reply() { yield `${longVisiblePrefix}正文说你将在2027年`; yield "3月15日一定会升职。\n\n"; } 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, / { 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 = ""; const suggestions = ''; async function* reply() { yield "一般知识可以说明概念。你的金星落"; yield "第七宫。\n'; } 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("general mode observes exact-timing without rewriting or blocking", () => { // 原值:general_no_birth_time 下 exact-timing 既不记 observe 也不拦 // 新值:日期原文通过,回执有 observe // 原因:BUG-952,最没有依据给日期的模式也要留痕。 const text = "2026年8月适合观察方向。"; const report = applyPass4Policy(text, "general_no_birth_time"); assert.equal(report.text, text); assert.equal(report.retry, false); assert.equal(report.steps.some((step) => step.action === "observe" && step.kind === "exact-timing"), true); }); 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/); });