Files
Jyotisha/frontend/tests/rectification-spoken-collect.test.ts
T
Jesse_Chen fef54b6e42
Independent Staging Quality Gate / validate (push) Successful in 11m54s
Independent Staging Quality Gate / publish (push) Successful in 8m14s
fix(rectification): downgrade yearless personality probes to tie-breakers (BUG-629)
Ask dated dasha probes first; D9/D10 and nakshatra wait until that pool is empty, score at half weight, and never eliminate. Skill 10.0.21.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-10 00:26:58 +08:00

538 lines
25 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 { parseRectificationChoiceCard } from "../src/lib/rectification-agentic/v9/choice-card.ts";
import {
isRenderableChoiceOpenQuestion,
openQuestionFromPersistedFocus,
persistServerOwnedFocus,
stableFollowupQuestionId,
} from "../src/lib/rectification-agentic/v9/server-focus.ts";
import { projectTurnDecision } from "../src/lib/rectification-agentic/v9/turn-decision.ts";
import {
declinedCollectDomains,
nextDatedCollectFollowup,
reverseVerifyRemainingForAdopt,
spokenFollowupForUser,
spokenCollectFallbackFollowup,
type MethodFollowup,
type MethodFollowupEvidence,
} from "../src/lib/rectification-agentic/v9/method-followup.ts";
import { RECTIFICATION_SKILL_VERSION } from "../src/lib/rectification-agentic/v9/case-status.ts";
import {
CASE_ID,
EVIDENCE_ID,
FOCUS_ID,
TURN_ID,
USER_ID,
activeFocusFixture,
candidateSnapshotFixture,
conversationSummaryFixture,
dossierFixture,
fakeAccounting,
} from "./rectification-v9-test-support.ts";
import { parseV9CaseDossier as parseDossier } from "../src/lib/rectification-agentic/v9/tool-service.ts";
import { USER_COLLECT_QUESTION } from "../src/lib/rectification-agentic/user-copy.ts";
import type { DiscriminatingEventProbe } from "../src/lib/rectification-agentic/v9/refinement-packet.ts";
import { isCollectDeclineUtterance, isCollectSkipUtterance } from "../src/lib/rectification-agentic/v9/turn-intent-classifier.ts";
const RELATIONSHIP_PROMPT = USER_COLLECT_QUESTION.relationship;
const COLLECT_GET_QUESTION = {
question_id: "active_focus:active_focus",
kind: "collect_spoken",
intent: "collect_method_evidence",
domain: "relationship",
prompt: RELATIONSHIP_PROMPT,
};
function collectFollowup(overrides: Partial<MethodFollowup> = {}): MethodFollowup {
return {
method_id: "active_focus",
intent: "collect_method_evidence",
ask_theme: "active_focus",
domain: "relationship",
kind_hint: null,
user_prompt_hint: "collect",
must_not_label: false,
choice_frame: null,
source: "active_focus",
...overrides,
};
}
function collectPersistResult(overrides: {
questionId?: string;
prompt?: string;
schema?: Record<string, unknown>;
domain?: string | null;
} = {}) {
const questionId = overrides.questionId ?? "collect:relationship:collect_method_evidence";
const prompt = overrides.prompt ?? RELATIONSHIP_PROMPT;
return {
status: "created" as const,
questionId,
prompt,
focus: {
id: FOCUS_ID,
caseId: CASE_ID,
questionId,
intent: "collect_method_evidence",
targetEvidenceId: null,
targetDomain: overrides.domain ?? "relationship",
targetKind: null,
expectedAnswerSchema: overrides.schema ?? {
prompt,
collect: true,
},
status: "active" as const,
askedAt: "2026-08-30T00:00:00.000Z",
resolvedAt: null,
},
};
}
test("skill version is 10.0.21 after the delivery UI simplify bump", () => {
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.21");
});
test("cases current_question remains the submit contract, not a visual slot", () => {
const chat = readFileSync(new URL("../src/components/rectification-agentic-chat.tsx", import.meta.url), "utf8");
assert.match(chat, /current_question\?: unknown/);
assert.match(chat, /setCurrentQuestion\(nextQuestion\)/);
assert.doesNotMatch(chat, /className="rectification-question-slot"/);
assert.match(chat, /currentQuestion\?\.kind === "collect_spoken"/);
assert.match(chat, /<RectificationChoiceCard/);
assert.match(chat, /onSelect=\{submitChoice\}/);
assert.match(chat, /onStop=\{submitStop\}/);
assert.doesNotMatch(chat, /finalizeRectificationSpokenAndThinking/);
assert.doesNotMatch(chat, /rectification-agentic\/v9\/spoken-answer/);
assert.doesNotMatch(chat, /message\.text\.(?:includes|match|search)\(/);
// 原值: 口述采集态必须渲染 `.rectification-collect-stop`
// 新值: 停止只留选择题卡内「先这样」与生成中「停止回答」
// 原因: BUG-595 决策 2,输入框上方不再挂先这样
const wrap = chat.slice(chat.indexOf("className=\"composer-wrap\""), chat.indexOf("className=\"composer-footer\""));
assert.doesNotMatch(wrap, /rectification-collect-stop/);
assert.doesNotMatch(wrap, /CHOICE_STOP_LABEL/);
// 旧:口述采集挂「没有」「记不清」芯片(BUG-605 / BUG-618
// 新:芯片全部去掉;打字「没有 / 记不清」仍走短路
// 原因:BUG-628,用户一致差评
assert.doesNotMatch(wrap, /rectification-collect-replies/);
assert.doesNotMatch(wrap, /rectification-collect-reply/);
assert.doesNotMatch(wrap, /send\("message", "没有"\)/);
assert.doesNotMatch(wrap, /rectification-step-state/);
const afterAnswer = chat.slice(
chat.indexOf("const afterAnswer = question"),
chat.indexOf("key={message.renderKey}"),
);
assert.doesNotMatch(afterAnswer, /CollectSpokenReplies/);
assert.doesNotMatch(afterAnswer, /shouldShowCollectSpokenReplies/);
assert.doesNotMatch(chat, /function CollectSpokenReplies/);
assert.doesNotMatch(chat, /rectification-collect-replies/);
assert.doesNotMatch(chat, /rectification-collect-reply/);
assert.doesNotMatch(chat, /onChoose\("没有"\)/);
assert.doesNotMatch(chat, /onChoose\("记不清"\)/);
assert.doesNotMatch(chat, />[\s\n]*没有[\s\n]*</);
assert.doesNotMatch(chat, />[\s\n]*记不清[\s\n]*</);
const styles = readFileSync(new URL("../src/app/globals.css", import.meta.url), "utf8");
assert.doesNotMatch(styles, /\.rectification-collect-replies/);
assert.doesNotMatch(styles, /\.rectification-collect-reply/);
assert.equal(parseRectificationChoiceCard(COLLECT_GET_QUESTION), null);
});
test("collect_spoken stem lives on turn.question inside the same assistant article", () => {
// old → new → kept: slot sibling → in-bubble question → no body-text scan; refresh rebuilds from asked_turn_id
const chat = readFileSync(new URL("../src/components/rectification-agentic-chat.tsx", import.meta.url), "utf8");
const row = readFileSync(new URL("../src/components/chat-message-row.tsx", import.meta.url), "utf8");
const styles = readFileSync(new URL("../src/app/globals.css", import.meta.url), "utf8");
const agentRun = readFileSync(new URL("../src/lib/rectification-agentic/v9/agent-run.ts", import.meta.url), "utf8");
assert.match(chat, /currentQuestion\?\.kind === "collect_spoken"/);
assert.match(chat, /const collectSpokenPrompt =/);
assert.match(chat, /parseTurnQuestion\(turn\.question\)/);
assert.match(chat, /function markQuestionAnswered/);
assert.match(chat, /afterAnswer=\{afterAnswer\}/);
assert.match(chat, /rectification-message-question/);
assert.match(row, /afterAnswer/);
assert.doesNotMatch(chat, /composeCollectSpokenAssistantText/);
assert.doesNotMatch(chat, /function attachCollectSpokenStem/);
assert.match(chat, /useLayoutEffect\(\(\) => \{\s*currentQuestionRef\.current = currentQuestion;/);
assert.doesNotMatch(chat, /\[busy, currentQuestion, regeneratingMessageKey\]/);
assert.doesNotMatch(chat, /showCollectSpokenPrompt/);
assert.doesNotMatch(chat, /rectification-question-slot/);
assert.doesNotMatch(chat, /showQuestionSlot/);
assert.match(chat, /collectSpokenPrompt\n\s+\? "请回答上面的问题…"/);
assert.match(
readFileSync(new URL("../src/components/chat-composer.tsx", import.meta.url), "utf8"),
/\[describedBy, showRemaining \? remainingId : undefined\]\.filter\(Boolean\)\.join\(" "\)/,
);
assert.match(chat, /<RectificationChoiceCard/);
assert.doesNotMatch(chat, /请在下方输入框回答/);
assert.doesNotMatch(chat, /questionHintId/);
assert.doesNotMatch(chat, /message\.text\.(?:includes|match|search)\(/);
assert.doesNotMatch(agentRun, /composeCollectSpokenAssistantText/);
assert.match(agentRun, /replace: true/);
assert.match(agentRun, /finalizeTurn\("completed", spokenAnswer/);
assert.doesNotMatch(agentRun, /answerText\.(?:includes|match|search)\(/);
assert.match(styles, /\.rectification-message-question \{/);
assert.match(styles, /\.rectification-choice-card\.is-embedded/);
assert.doesNotMatch(styles, /\.rectification-question-slot \{/);
});
test("missing current_question is explicit only for resumable cases", () => {
const chat = readFileSync(new URL("../src/components/rectification-agentic-chat.tsx", import.meta.url), "utf8");
// Was: `const showMissingQuestion = Boolean(... caseSnapshotLoaded ... resumableCase ...
// currentQuestion === null` plus the copy /当前没有可回答的问题,正在等待服务端更新/ and
// /题目加载失败,请刷新/. Those were bare waiting sentences; the same inputs now feed
// `rectificationQuestionGapState`, which yields a live row with retries and then a
// reload button, and is still gated on a resumable case (BUG-505).
assert.match(chat, /const questionGap = rectificationQuestionGapState\(\{[\s\S]*questionMissing: currentQuestion === null,[\s\S]*snapshotLoaded: caseSnapshotLoaded,[\s\S]*resumableCase,/);
assert.match(chat, /const resumableCase = caseStatus !== null && isResumableStatus\(caseStatus\)/);
assert.doesNotMatch(chat, /等待服务端更新|题目加载失败,请刷新/);
assert.match(chat, /RECTIFICATION_QUESTION_UNAVAILABLE_COPY/);
assert.match(chat, /readonly && \(/);
assert.doesNotMatch(chat, /questionGap[\s\S]*caseStatus.*TERMINAL/);
});
test("choice options render inside the same assistant message, not a sibling slot", () => {
const chat = readFileSync(new URL("../src/components/rectification-agentic-chat.tsx", import.meta.url), "utf8");
const render = chat.slice(
chat.indexOf("{messages.map((message) => {") ,
chat.indexOf("{savedTime &&"),
);
assert.match(chat, /showLiveChoiceCard/);
assert.match(render, /<RectificationChoiceCard/);
assert.match(render, /onSelect=\{submitChoice\}/);
assert.match(render, /onStop=\{submitStop\}/);
assert.match(render, /afterAnswer/);
assert.doesNotMatch(render, /rectification-question-slot/);
assert.doesNotMatch(render, /liveSpoken/);
assert.doesNotMatch(render, /showLiveSpokenCollect/);
});
test("openQuestionFromPersistedFocus returns collect_spoken without making collect choice-ready", () => {
const collect = openQuestionFromPersistedFocus(collectPersistResult());
assert.equal(collect?.kind, "collect_spoken");
assert.equal(collect?.prompt, RELATIONSHIP_PROMPT);
assert.equal(collect?.unrenderable, undefined);
assert.equal(isRenderableChoiceOpenQuestion(collect), false);
const choiceFocus = {
status: "created" as const,
questionId: "probe:education.2016",
prompt: "2016 年前后 · 升学结果或学习环境出现明显变化",
focus: {
id: FOCUS_ID,
caseId: CASE_ID,
questionId: "probe:education.2016",
intent: "distinguish_candidates",
targetEvidenceId: null,
targetDomain: "education",
targetKind: null,
expectedAnswerSchema: {
choice: {
prompt: "2016 年前后 · 升学结果或学习环境出现明显变化",
option_a: "明确发生且时间吻合",
option_b: "发生过但程度较弱",
option_c: "明确没有发生",
option_d: "这段记不清楚",
options: [
{ key: "A", label: "明确发生且时间吻合", answer_class: "yes" },
{ key: "B", label: "发生过但程度较弱", answer_class: "weak_yes" },
{ key: "C", label: "明确没有发生", answer_class: "no" },
{ key: "D", label: "这段记不清楚", answer_class: "unsure" },
],
},
},
status: "active" as const,
askedAt: "2026-08-30T00:00:00.000Z",
resolvedAt: null,
},
};
const choice = openQuestionFromPersistedFocus(choiceFocus);
assert.equal(choice?.kind, "choice");
assert.equal(choice?.prompt, choiceFocus.prompt);
assert.notEqual(choice?.unrenderable, true);
assert.equal(isRenderableChoiceOpenQuestion(choice), true);
const answerChoice = readFileSync(new URL("../src/lib/rectification-agentic/v9/answer-choice.ts", import.meta.url), "utf8");
const nextInterview = answerChoice.slice(
answerChoice.indexOf("export async function persistNextInterviewAfterChoice"),
answerChoice.indexOf("async function persistFocusAfterChoice"),
);
assert.match(nextInterview, /isRenderableChoiceOpenQuestion/);
assert.match(nextInterview, /hostNarration: open\.prompt/);
const route = readFileSync(new URL("../src/app/api/rectification/agent/route.ts", import.meta.url), "utf8");
assert.match(route, /isRenderableChoiceOpenQuestion/);
const fastPath = route.slice(
route.indexOf('if (action === "message")'),
route.indexOf("const requestTime"),
);
assert.match(fastPath, /isRenderableChoiceOpenQuestion\(open\)/);
});
test("collect focus keeps current_probe and inference.next_probe null; choice focus does not", () => {
const collectDossier = parseDossier(dossierFixture({
latestResult: candidateSnapshotFixture({
decisionReceipt: { inference_state: { next_probe: { id: "should-hide" } } },
}),
conversationSummary: conversationSummaryFixture({
activeFocus: activeFocusFixture({
intent: "collect_method_evidence",
targetDomain: "relationship",
expectedAnswerSchema: {
prompt: RELATIONSHIP_PROMPT,
collect: true,
},
}),
}),
}));
assert.ok(collectDossier);
const collectProjection = projectTurnDecision(collectDossier);
assert.equal((collectProjection.current_question as { kind?: string } | null)?.kind, "collect_spoken");
assert.equal(collectProjection.current_probe ?? null, null);
assert.equal((collectProjection.inference as { next_probe?: unknown } | null)?.next_probe ?? null, null);
const tools = readFileSync(new URL("../src/mastra/rectification-v9-tools.ts", import.meta.url), "utf8");
const projection = tools.slice(
tools.indexOf("function agentVisibleLatestProjection"),
tools.indexOf("question_contract: {"),
);
assert.match(projection, /kind:\s*extras\.openQuestion\.kind|kind: extras\.openQuestion\.kind/);
assert.match(projection, /kind === "choice"/);
assert.match(projection, /next_probe: null/);
assert.match(projection, /current_probe: null/);
});
test("active_focus collect followups get a domain-stable questionId, not active_focus:active_focus", async () => {
const followup = collectFollowup();
assert.equal(stableFollowupQuestionId(followup).includes("active_focus:active_focus"), false);
assert.match(stableFollowupQuestionId(followup), /^collect:relationship:collect_method_evidence$/);
const degraded = spokenCollectFallbackFollowup({
...followup,
method_id: "dasha_events",
ask_theme: "dated_event",
domain: "education",
semantic_key: "education:2016",
probe_year: 2016,
});
assert.equal(stableFollowupQuestionId(degraded).includes("education:2016"), false);
assert.match(stableFollowupQuestionId(degraded), /^collect:education:collect_method_evidence$/);
const accounting = fakeAccounting({
set_agentic_rectification_conversation_focus: (_fn, args) => ({
id: FOCUS_ID,
case_id: CASE_ID,
question_id: args.p_question_id,
intent: args.p_intent,
target_evidence_id: args.p_target_evidence_id,
target_domain: args.p_target_domain,
target_kind: args.p_target_kind,
expected_answer_schema: args.p_expected_answer_schema,
status: "active",
asked_at: "2026-08-30T00:00:00.000Z",
resolved_at: null,
idempotent: false,
}),
});
const stale = {
id: FOCUS_ID,
caseId: CASE_ID,
questionId: "active_focus:active_focus",
intent: "collect_method_evidence",
targetEvidenceId: null,
targetDomain: "relationship",
targetKind: null,
expectedAnswerSchema: {
prompt: RELATIONSHIP_PROMPT,
collect: true,
},
status: "active" as const,
askedAt: "2026-08-30T00:00:00.000Z",
resolvedAt: null,
};
const persisted = await persistServerOwnedFocus({
accounting: accounting.client,
userId: USER_ID,
caseId: CASE_ID,
activeFocus: stale,
decisionReceipt: null,
followup,
});
assert.equal(persisted.status, "created");
assert.notEqual(persisted.questionId, "active_focus:active_focus");
assert.match(persisted.questionId ?? "", /^collect:relationship:collect_method_evidence$/);
assert.equal(persisted.status, "created");
const open = openQuestionFromPersistedFocus(persisted);
assert.equal(open?.kind, "collect_spoken");
});
test("agent prompt lets the model write the stem only through set-focus spokenPrompt", () => {
const agent = readFileSync(new URL("../src/mastra/agentic-rectification.ts", import.meta.url), "utf8");
const prompt = agent.slice(
agent.indexOf("const agenticRectificationInstructions"),
agent.indexOf("export function getRectificationV9Agent"),
);
assert.match(prompt, /用 rectification-set-focus 的 spokenPrompt 写出服务端给你的下一问/);
assert.match(prompt, /collect_spoken[\s\S]*不输出输入提示/);
assert.match(prompt, /没有下一问/);
assert.match(prompt, /选择卡|choice 选项由服务端/);
assert.doesNotMatch(prompt, /题干与选项完全由结构化槽位和 UI 承担/);
assert.doesNotMatch(prompt, /collect_spoken 题干由服务器接在同一条正文末尾并写入聊天历史/);
assert.doesNotMatch(prompt, /必须由你用自己的话在正文里问出来/);
assert.doesNotMatch(prompt, /界面提示条/);
});
const COLLECT_QUESTION_ID = "collect:relationship:collect_method_evidence";
function collectFocus() {
return activeFocusFixture({
intent: "collect_method_evidence",
targetDomain: "relationship",
questionId: COLLECT_QUESTION_ID,
expectedAnswerSchema: {
prompt: RELATIONSHIP_PROMPT,
collect: true,
},
});
}
test("agent route keeps question ownership in the server Case projection", () => {
const route = readFileSync(new URL("../src/app/api/rectification/agent/route.ts", import.meta.url), "utf8");
const turnExit = readFileSync(new URL("../src/lib/rectification-agentic/v9/turn-exit.ts", import.meta.url), "utf8");
const afterRun = route.slice(route.indexOf("const result = await runV9AgentTurn"));
const beforeRun = route.slice(
route.indexOf("async start(controller)"),
route.indexOf("const result = await runV9AgentTurn"),
);
assert.match(beforeRun, /if \(action === "opening"\)/);
assert.match(beforeRun, /persistNextInterviewIfIdle/);
assert.match(afterRun, /await finalizeSuccessfulTurnExit/);
assert.match(turnExit, /persistNextInterviewIfIdle/);
assert.doesNotMatch(afterRun, /persistCollectSpokenAssistantIfNew|persistEmptyCollectSpokenAssistant/);
assert.doesNotMatch(afterRun, /answerText\.(?:includes|match|search)\(/);
// 原值: 成功分支是第一个 `} else {`,其后再 `send({ type: "done"`
// 新值: already_delivered 先 send done,成功分支在 else if (!result.ok) 之后
// 原因: BUG-596
const alreadyDelivered = afterRun.slice(
afterRun.indexOf("if (result.errorCode === \"already_delivered\")"),
afterRun.indexOf("} else if (!result.ok)"),
);
assert.match(alreadyDelivered, /send\(\{\s*type:\s*"done"/);
assert.doesNotMatch(alreadyDelivered, /await finalizeSuccessfulTurnExit/);
const successStart = afterRun.indexOf("} else {", afterRun.indexOf("} else if (!result.ok)"));
const successExit = afterRun.slice(
successStart,
afterRun.lastIndexOf("send({ type: \"done\""),
);
assert.match(successExit, /await finalizeSuccessfulTurnExit/);
assert.doesNotMatch(successExit, /send\(\{\s*type:\s*"error"/);
const agentRun = readFileSync(new URL("../src/lib/rectification-agentic/v9/agent-run.ts", import.meta.url), "utf8");
assert.doesNotMatch(agentRun, /collectSpokenPromptForNewFocus/);
assert.doesNotMatch(agentRun, /composeCollectSpokenAssistantText/);
assert.match(agentRun, /askedTurnId: turnId/);
assert.doesNotMatch(agentRun, /当前可询问范围/);
});
test("没有 and 记不清 short-circuit collect_spoken without a model run", () => {
assert.equal(isCollectDeclineUtterance("没有"), true);
assert.equal(isCollectDeclineUtterance("没有。"), true);
assert.equal(isCollectSkipUtterance("记不清"), true);
assert.equal(isCollectSkipUtterance("记不清!"), true);
assert.equal(isCollectDeclineUtterance("记不清"), false);
assert.equal(isCollectSkipUtterance("没有"), false);
const route = readFileSync(new URL("../src/app/api/rectification/agent/route.ts", import.meta.url), "utf8");
const collectStart = route.lastIndexOf("} else if (focus && isCollectFocusSchema(focus.expectedAnswerSchema))");
const collectBranch = route.slice(collectStart);
const collectFocus = collectBranch.slice(0, collectBranch.indexOf("classifyRectificationTurnIntent"));
assert.match(collectFocus, /isCollectDeclineUtterance/);
assert.match(collectFocus, /isCollectSkipUtterance/);
assert.match(collectFocus, /status: isCollectSkipUtterance\(userMessage\) \? "skipped" : "declined"/);
assert.match(collectFocus, /persistCollectDenialTurn/);
assert.doesNotMatch(collectFocus, /runV9AgentTurn/);
});
test("skipped collect domains are not re-asked this session; reverse-verify may still touch them", () => {
const skippedFamily = [{
target_domain: "family",
status: "skipped",
intent: "collect_method_evidence",
}];
const declined = declinedCollectDomains(skippedFamily);
assert.equal(declined.has("family"), true);
const next = nextDatedCollectFollowup([], declined);
assert.notEqual(next?.domain, "family");
const familyProbe: DiscriminatingEventProbe = {
year: 2018,
year_label: "2018 年前后",
domain: "family",
event_family: "家里的大事",
source: "dasha_boundary",
tracks: ["vimshottari", "narayana"],
tracks_agree: true,
unique_minute_claim: false,
user_meaning: "时间范围锁定 2018 年。",
role: "distinguish",
information_gain: 1.1,
semantic_key: "family.2018.dasha_boundary",
candidate_split_hash: "family.2018.dasha_boundary",
candidate_ids: ["05:00", "05:20"],
expected_outcomes: [
{ answer_class: "yes", supports: ["05:00"], conflicts: ["05:20"] },
{ answer_class: "no", supports: ["05:20"], conflicts: ["05:00"] },
],
choice_kind: "existence",
};
const remaining = reverseVerifyRemainingForAdopt({
eventProbes: [familyProbe],
evidence: [],
declinedTopics: skippedFamily,
});
assert.equal(remaining.some((probe) => probe.domain === "family"), true);
});
test("single confirmed domain opening goes to dated collect without a chase-more turn", () => {
const evidence: MethodFollowupEvidence[] = [
{ status: "confirmed", domain: "education", datePrecision: "month", occurredFrom: "2016-09-01", occurredTo: null },
];
const next = nextDatedCollectFollowup(evidence, new Set());
assert.ok(next);
assert.notEqual(next.domain, "education");
assert.equal(next.domain, "family");
const spoken = spokenFollowupForUser(next);
assert.ok(spoken);
assert.match(spoken, /家里/);
assert.match(spoken, /想到别的也可以一起说/);
assert.doesNotMatch(spoken, /还有吗|别的吗|还能想起别的吗/);
assert.doesNotMatch(spoken, /先说你最容易想起的一两件/);
const later = nextDatedCollectFollowup(evidence, new Set(), new Set(), { askedDatedCollect: true });
assert.equal(later?.domain, "family");
assert.doesNotMatch(spokenFollowupForUser(later) ?? "", /想到别的也可以一起说/);
const tools = readFileSync(new URL("../src/mastra/rectification-v9-tools.ts", import.meta.url), "utf8");
assert.match(tools, /recordV10EvidenceBatch/);
assert.match(tools, /result\.acceptedCount > 0[\s\S]*autoRescoreAfterEvidenceChange/);
const copy = readFileSync(new URL("../src/lib/rectification-agentic/user-copy.ts", import.meta.url), "utf8");
const agent = readFileSync(new URL("../src/mastra/agentic-rectification.ts", import.meta.url), "utf8");
const agentRun = readFileSync(new URL("../src/lib/rectification-agentic/v9/agent-run.ts", import.meta.url), "utf8");
assert.doesNotMatch(copy, /还有吗|还能想起别的吗/);
assert.doesNotMatch(agent, /还有吗|还能想起别的吗/);
assert.doesNotMatch(agentRun, /还有吗|还能想起别的吗/);
});
test("three confirmed domains from one message are not re-asked by dated collect", () => {
const evidence: MethodFollowupEvidence[] = [
{ status: "confirmed", domain: "education", datePrecision: "month", occurredFrom: "2016-09-01", occurredTo: null },
{ status: "confirmed", domain: "career", datePrecision: "month", occurredFrom: "2018-07-01", occurredTo: null },
{ status: "confirmed", domain: "relocation", datePrecision: "year", occurredFrom: "2023-01-01", occurredTo: null },
];
const next = nextDatedCollectFollowup(evidence, new Set());
assert.notEqual(next?.domain, "education");
assert.notEqual(next?.domain, "career");
assert.notEqual(next?.domain, "relocation");
const tools = readFileSync(new URL("../src/mastra/rectification-v9-tools.ts", import.meta.url), "utf8");
assert.match(tools, /recordV10EvidenceBatch/);
assert.match(tools, /result\.acceptedCount > 0[\s\S]*autoRescoreAfterEvidenceChange/);
});