ed9497e76a
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>
172 lines
8.0 KiB
TypeScript
172 lines
8.0 KiB
TypeScript
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:00–05:07,代表分钟 05:00。代表分钟只是代表性候选,不是已确认的唯一出生分钟。",
|
||
financeCollect: "有没有记得住时间的收入变化、大笔支出或欠债?",
|
||
deliveryAdopt: "当前可信区间是 05:00–05: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:00–05: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:00–05: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:47–04: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/);
|
||
});
|