fix(rectification): require three answers before uncertainty stop
Independent Staging Quality Gate / validate (push) Successful in 9m19s
Independent Staging Quality Gate / publish (push) Successful in 1m52s

One "一时说不好" no longer ends the interview. Plateau streaks use the
same sample floor. Delivery copy states why the range stopped.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jesse_Chen
2026-09-03 12:08:37 +08:00
co-authored by Cursor
parent e8c98c37cf
commit 0c0df42679
11 changed files with 335 additions and 51 deletions
@@ -805,6 +805,48 @@ test("deferFollowup scores the choice without persisting the next interview or t
assert.equal(applied.narrationPersisted, false);
});
test("one unsure discriminator answer does not adopt and persists the next question", async () => {
const accounting = persistChoiceAccounting(twoProbeDossier(), {
set_agentic_rectification_conversation_focus: (_fn, args) => ({
focus: {
id: NEXT_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-28T07:37:50.000Z",
resolved_at: null,
},
idempotent: false,
}),
});
const applied = await applyRectificationChoice(accounting.client, {
userId: USER_ID,
caseId: CASE_ID,
sessionId: SESSION_ID,
actionId: ACTION_ID,
action: CHOICE_ACTION,
focusId: FOCUS_ID,
questionId: QUESTION_ID,
probeId: RELOCATION_2015_PROBE.id,
optionId: "D",
expectedRevision: twoProbeInference().revision,
});
assert.equal(applied.answerClass, "unsure");
assert.equal(applied.narration, "已记录。这题先不计分,换一件事问。");
assert.notEqual(applied.nextAction.session_outcome, "adopt_representative");
assert.equal(applied.nextAction.type, "ask_candidate_discriminator");
assert.equal(applied.nextInterviewPersisted, true);
const persist = accounting.calls.find((call) => call.fn === "apply_agentic_rectification_choice_action");
assert.ok(persist?.args.p_inference);
const setFocus = accounting.calls.find((call) => call.fn === "set_agentic_rectification_conversation_focus");
assert.ok(setFocus);
});
test("answering the last discriminator persists a year-locked family collect focus, not a yearless D24 card", async () => {
const accounting = persistChoiceAccounting(familyCollectDossier(), {
set_agentic_rectification_conversation_focus: (_fn, args) => ({
@@ -1116,11 +1158,29 @@ test("structured choice narration never persists a fake loading state", () => {
composeChoiceNarration({ optionId: "A", scoring: true, appliedInference: true }),
composeChoiceNarration({ optionId: "A", scoring: true, appliedInference: false }),
composeChoiceNarration({ optionId: "A", scoring: false, appliedInference: false }),
composeChoiceNarration({
optionId: "D",
scoring: true,
appliedInference: true,
answerClass: "unsure",
}),
]) {
assert.doesNotMatch(narration, /正在准备下一步/);
}
});
test("an unsure choice keeps applied inference and does not claim scores changed", () => {
const narration = composeChoiceNarration({
optionId: "D",
scoring: true,
appliedInference: true,
answerClass: "unsure",
});
// 旧:appliedInference 仍说「更新了候选比较」→ 新:unsure 不计分旁白 → 保留 inference 行仍持久化
assert.equal(narration, "已记录。这题先不计分,换一件事问。");
assert.doesNotMatch(narration, /更新了候选比较/);
});
test("the public agent route treats structured choice as a non-model command", () => {
const route = readFileSync(new URL("../src/app/api/rectification/agent/route.ts", import.meta.url), "utf8");
const start = route.indexOf("if (isStructuredChoice)");