fix(rectification): prevent collect focus dead-end after choice answers

This commit is contained in:
Jesse_Chen
2026-08-30 19:47:42 +08:00
parent 85b14b13ae
commit 40684eec6d
4 changed files with 76 additions and 17 deletions
@@ -39,7 +39,9 @@ import {
USER_ID,
candidateSnapshotFixture,
computeFixture,
conversationSummaryFixture,
dossierFixture,
activeFocusFixture,
fakeAccounting,
receiptHandlers,
} from "./rectification-v9-test-support.ts";
@@ -918,14 +920,24 @@ test("read-case follows method plan and keeps D9/D10 type tables when SQL missin
assert.match(JSON.stringify(projection.method_followup_plan.next_followup), /自然语言/);
});
test("accepted batch evidence triggers server rescore without offering adoption", async () => {
test("accepted batch evidence resolves a spoken collect focus before the next question", async () => {
const restore = stubEngine(ENGINE_SCORE);
let collectFocusResolved = false;
try {
const accounting = fakeAccounting({
...receiptHandlers,
get_agentic_rectification_case_dossier: () => dossierFixture({
evidence: [educationEvidence],
latestResult: null,
conversationSummary: conversationSummaryFixture({
activeFocus: collectFocusResolved
? null
: activeFocusFixture({
intent: "collect_method_evidence",
targetDomain: "education",
expectedAnswerSchema: { collect: true, prompt: "有没有记得住时间的升学经历?" },
}),
}),
}),
get_agentic_rectification_case_compute: () => computeFixture(),
record_agentic_rectification_evidence_batch: () => ({
@@ -943,6 +955,15 @@ test("accepted batch evidence triggers server rescore without offering adoption"
rejected_count: 0,
focus_id: null,
}),
resolve_agentic_rectification_conversation_focus: (_fn, args) => {
collectFocusResolved = true;
return {
focus_id: args.p_focus_id,
status: args.p_status,
evidence_id: args.p_evidence_id,
idempotent: false,
};
},
persist_agentic_rectification_candidate_v2: () => ({
...candidateSnapshotFixture(),
cached: false,
@@ -959,6 +980,7 @@ test("accepted batch evidence triggers server rescore without offering adoption"
execute(input: unknown): Promise<{
accepted_count: number;
rescore: { status: string; executed_methods: string[]; error_code: string | null };
open_question: { prompt?: string } | null;
}>;
}).execute({
caseId: CASE_ID,
@@ -975,6 +997,15 @@ test("accepted batch evidence triggers server rescore without offering adoption"
assert.equal(result.accepted_count, 1);
assert.equal(result.rescore.status, "completed");
assert.ok(result.rescore.executed_methods.includes("d1-rashi"));
assert.equal(accounting.calls[0]?.fn, "get_agentic_rectification_case_dossier");
const resolvedFocus = accounting.calls.find((call) => call.fn === "resolve_agentic_rectification_conversation_focus");
assert.equal(resolvedFocus?.args.p_focus_id, FOCUS_ID);
assert.equal(resolvedFocus?.args.p_status, "resolved");
assert.equal(resolvedFocus?.args.p_evidence_id, EDUCATION_ID);
const recordIndex = accounting.calls.findIndex((call) => call.fn === "record_agentic_rectification_evidence_batch");
const resolveIndex = accounting.calls.findIndex((call) => call.fn === "resolve_agentic_rectification_conversation_focus");
assert.ok(recordIndex >= 0 && resolveIndex > recordIndex);
assert.doesNotMatch(result.open_question?.prompt ?? "", /升学经历/);
const persistCall = accounting.calls.find((call) => call.fn === "persist_agentic_rectification_candidate_v2");
assert.ok(persistCall);
const receipt = persistCall.args.p_decision_receipt as { window_scan?: { d9_candidates_differ?: boolean; d9_sign_names?: unknown } };
@@ -2306,7 +2337,7 @@ test("coverage-complete tie with encoded D24/D10 collects a dated move instead o
}), "discriminate_candidates");
});
test("coverage-complete tie with no remaining split offers a provisional range", () => {
test("provisional range still exposes method coverage followup", () => {
const packet = buildCandidateContrastPacket({
candidateSetVersion: "05:00-05:04",
candidateTimes: DUMP_SCORES.map((item) => item.time),
@@ -2329,7 +2360,9 @@ test("coverage-complete tie with no remaining split offers a provisional range",
contrastPacket: packet,
sessionOutcome: "provisional_range",
});
assert.equal(plan.next_followup, null);
assert.equal(plan.next_followup?.intent, "collect_method_evidence");
assert.equal(plan.next_followup?.domain, "horary");
assert.equal(plan.deferred_followup, null);
assert.equal(conversationalSessionOutcome({
selectionAllowed: true,
proposeAllowed: true,