Files
Jyotisha/frontend/tests/agent-voice-copy-contract.test.ts
T
Jesse_Chen ed9497e76a fix(voice): speak first, then keep Level 2 skeleton and boundary semantics
Natal answers were opening on parameter tables, and rectification turns were one-sentence legal copy. Centralize user-facing strings, keep representative-minute and question-slot red lines, and stop duplicating the opening collect prompt as a second assistant message.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-02 02:27:50 +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.match(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("E-1 opening collect 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,
}), true);
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/);
});