Files
Jyotisha/frontend/tests/agent-voice-copy-contract.test.ts
T
Jesse_Chen 1a3e14e726
Independent Staging Quality Gate / validate (push) Successful in 13m40s
Independent Staging Quality Gate / publish (push) Successful in 9m59s
fix(rectification): show question stems and unblock adopt after occupation
Choice legends and spoken collect prompts were hidden after BUG-461, and occupation still blocked canAdopt once dated coverage was done. Restore visible stems and stop polling extra collects when adopt is allowed.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-02 11:09:56 +08:00

172 lines
8.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import test from "node:test";
import {
GENERIC_COLLECT_QUESTION,
RECTIFICATION_USER_COPY,
REPRESENTATIVE_MINUTE_DISCLAIMER,
accidentCaseHardcodedTurns,
containsBoundarySemantics,
engineMeaningToDisplayCopy,
listUserVisibleCopy,
nonConvergingRangeNarration,
} from "../src/lib/rectification-agentic/user-copy.ts";
import { shouldPersistFocusPromptTurn } from "../src/lib/rectification-agentic/v9/turn-exit.ts";
const MACHINE_VOICE_LEXICON = [
"按同一规则计入",
"请刷新后重新作答",
"当前状态为",
"请点选下面的选项作答",
"当前区分题",
"接下来请点选下面这一问",
"可以从下面的时间里选一个采用",
"我没能确定这句话是否在回答当前问题",
"请先说一件你记得大概时间的人生经历,比如升学、入职、搬家、结婚或生病",
"有没有记得住时间的收入变化、大笔支出或欠债",
] as const;
/** Task 0 frozen “before” snapshot: accident case f83d9b42, origin/staging @ 8617eb56. */
export const ACCIDENT_CASE_BEFORE_COPY = {
openingCollect: "请先说一件你记得大概时间的人生经历,比如升学、入职、搬家、结婚或生病;只记得年份也可以。",
discriminatorRedirect: "请点选下面的选项作答。点选和一句话回答会按同一规则计入当前区分题。",
unclearFocusReply: "我没能确定这句话是否在回答当前问题。请点选下面的选项,或换一种说法。",
staleQuestion: "当前问题已更新,请刷新后重新作答。",
intermediateRange: "当前可信区间是 05:0005:07,代表分钟 05:00。代表分钟只是代表性候选,不是已确认的唯一出生分钟。",
financeCollect: "有没有记得住时间的收入变化、大笔支出或欠债?",
deliveryAdopt: "当前可信区间是 05:0005:07,代表分钟 05:00。代表分钟只是代表性候选,不是已确认的唯一出生分钟。 可以从下面的时间里选一个采用。",
marriageConsultOpen: [
"## 统一参数与原始结构",
"## 关系",
"## 技法审计表",
],
} as const;
const ACCIDENT_SHAPE = {
openingRange: ["04:45", "05:15"] as const,
credibleRange: ["05:00", "05:07"] as const,
representativeTime: "05:00",
};
test("user-visible copy rejects the machine-voice lexicon", () => {
assert.ok(MACHINE_VOICE_LEXICON.length >= 8);
const visible = listUserVisibleCopy().join("\n");
for (const phrase of MACHINE_VOICE_LEXICON) {
assert.equal(visible.includes(phrase), false, phrase);
}
});
test("delivery and adopt turns keep representative-minute boundary semantics", () => {
const after = accidentCaseHardcodedTurns(ACCIDENT_SHAPE);
assert.equal(containsBoundarySemantics(REPRESENTATIVE_MINUTE_DISCLAIMER), true);
assert.equal(containsBoundarySemantics(after.deliveryAdopt), true);
assert.match(after.deliveryAdopt, /05:0005:07/);
assert.match(after.deliveryAdopt, /05:00/);
assert.match(after.deliveryAdopt, /30 分钟/);
assert.match(after.deliveryAdopt, /7 分钟/);
assert.equal(containsBoundarySemantics(after.deliveryAdopt), true);
assert.doesNotMatch(after.intermediateRange, /不是已确认的唯一出生分钟/);
assert.match(after.intermediateRange, /05:0005:07|7 分钟/);
});
test("accident-case after copy is rebuilt from the shared module", () => {
const after = accidentCaseHardcodedTurns(ACCIDENT_SHAPE);
assert.equal(after.openingCollect, GENERIC_COLLECT_QUESTION);
assert.equal(after.discriminatorRedirect, RECTIFICATION_USER_COPY.choicePrompt);
assert.equal(after.unclearFocusReply, RECTIFICATION_USER_COPY.unclearFocusReply);
assert.equal(after.staleQuestion, RECTIFICATION_USER_COPY.questionUpdated);
assert.equal(after.financeCollect, "钱的方面,还记得哪年收入明显变过、有过大笔支出,或欠过债吗?");
assert.notEqual(after.openingCollect, ACCIDENT_CASE_BEFORE_COPY.openingCollect);
assert.notEqual(after.financeCollect, ACCIDENT_CASE_BEFORE_COPY.financeCollect);
assert.ok(containsBoundarySemantics(after.deliveryAdopt));
});
test("default range narration stays semantic even without an opening window", () => {
const text = nonConvergingRangeNarration({
credibleRange: ["04:47", "04:53"],
representativeTime: "04:51",
});
assert.match(text, /04:4704:53/);
assert.match(text, /04:51/);
assert.equal(containsBoundarySemantics(text), true);
});
test("question-slot ownership stays in the rectification agent prompt", () => {
const agent = readFileSync(new URL("../src/mastra/agentic-rectification.ts", import.meta.url), "utf8");
const route = readFileSync(new URL("../src/app/api/rectification/agent/route.ts", import.meta.url), "utf8");
const exit = readFileSync(new URL("../src/lib/rectification-agentic/v9/turn-exit.ts", import.meta.url), "utf8");
assert.match(agent, /正文不得提问、复述、改写或拼接题干/);
assert.match(agent, /每轮正文 2-4 句/);
assert.doesNotMatch(agent, /每轮正文只输出一句确认或承接/);
assert.doesNotMatch(route, /接下来请点选下面这一问/);
assert.doesNotMatch(exit, /接下来请点选下面这一问/);
assert.match(exit, /shouldPersistFocusPromptTurn/);
assert.doesNotMatch(exit, /action === ["']opening["']/);
assert.match(route, /assistantBodyPresent: Boolean\(result\.answerText\?\.trim\(\)\)/);
assert.doesNotMatch(exit, /assistantMessage\.(?:includes|match|search)/);
assert.doesNotMatch(exit, /question\.prompt\.(?:includes|match|search)/);
const skipFn = exit.slice(
exit.indexOf("export function shouldPersistFocusPromptTurn"),
exit.indexOf("export async function finalizeSuccessfulTurnExit"),
);
assert.doesNotMatch(skipFn, /\.test\(/);
assert.doesNotMatch(skipFn, /\.match\(/);
assert.doesNotMatch(skipFn, /\.search\(/);
assert.doesNotMatch(skipFn, /includes\(/);
});
test("collect prompt skip is structural, not a body-text regex", () => {
assert.equal(shouldPersistFocusPromptTurn({
action: "opening",
questionKind: "collect_spoken",
questionIntent: "collect_method_evidence",
assistantBodyPresent: true,
}), false);
assert.equal(shouldPersistFocusPromptTurn({
action: "opening",
questionKind: "collect_spoken",
questionIntent: "collect_method_evidence",
assistantBodyPresent: false,
}), true);
assert.equal(shouldPersistFocusPromptTurn({
action: "opening",
questionKind: "choice",
questionIntent: "distinguish_candidates",
assistantBodyPresent: true,
}), true);
assert.equal(shouldPersistFocusPromptTurn({
action: "message",
questionKind: "collect_spoken",
questionIntent: "collect_method_evidence",
assistantBodyPresent: true,
}), false);
assert.equal(shouldPersistFocusPromptTurn({
action: "answer_choice",
questionKind: "choice",
questionIntent: "distinguish_candidates",
assistantBodyPresent: true,
}), true);
});
test("E-2 display copy keeps engine meaning out of instruction tone", () => {
const dasha = engineMeaningToDisplayCopy(
"主限更偏向 05:00,分盘大运更偏向 04:54。冲突时不能按更高把握收口。",
);
assert.match(dasha, /05:00/);
assert.match(dasha, /04:54/);
assert.doesNotMatch(dasha, /不得/);
assert.doesNotMatch(dasha, /不能按更高把握收口/);
const natal = engineMeaningToDisplayCopy(
"本命宫位已按 05:07 重算(上升 金牛座)。下面是本轮实际执行的技法,不能当作唯一分钟确认。",
);
assert.match(natal, /05:07/);
assert.doesNotMatch(natal, /不能当作唯一分钟确认/);
const instruct = engineMeaningToDisplayCopy("可以采用代表性候选作当前排盘;不得把它描述为已确认的唯一出生分钟。");
assert.doesNotMatch(instruct, /不得/);
const board = readFileSync(new URL("../src/components/rectification-board.tsx", import.meta.url), "utf8");
const gate = readFileSync(new URL("../src/components/technique-audit-disclosure.tsx", import.meta.url), "utf8");
assert.match(board, /engineMeaningToDisplayCopy/);
assert.match(gate, /engineMeaningToDisplayCopy/);
});