fix(rectification): render adoption carrier on deterministic choice path
Independent Staging Quality Gate / validate (push) Has been cancelled
Independent Staging Quality Gate / publish (push) Has been cancelled

This commit is contained in:
Jesse_Chen
2026-08-31 02:15:12 +08:00
parent 79bfa73da3
commit ff5998f184
12 changed files with 298 additions and 17 deletions
@@ -423,6 +423,74 @@ function familyCollectDossier() {
});
}
function adoptionInference() {
return buildInferenceState({
range_start: "04:45",
range_end: "05:15",
candidates: [
{ id: "05:00", time: "05:00", relative_support: 16 },
{ id: "05:10", time: "05:10", relative_support: 14 },
],
events: [
{ id: "e1", domain: "career", year: 2020, precision: "month" },
{ id: "e2", domain: "career", year: 2024, precision: "day" },
{ id: "e3", domain: "relationship", year: 2024, precision: "month" },
{ id: "e4", domain: "relationship", year: 2024, precision: "day" },
],
probes: [RELOCATION_2015_PROBE],
});
}
function adoptionDossier() {
const inference = adoptionInference();
return dossierFixture({
evidenceCount: 6,
evidence: [
...fourEventRows(),
{
id: "occupation-note",
source_turn_id: TURN_ID,
subject: "self",
event_kind: "occupation_note",
domain: "occupation",
occurred_from: null,
occurred_to: null,
date_precision: "unknown",
summary: "长期从事技术工作",
status: "confirmed",
supersedes_evidence_id: null,
created_at: "2026-08-28T07:38:00.000Z",
},
],
latestResult: candidateSnapshotFixture({
selectionAllowed: true,
representativeTime: "05:00",
decisionReceipt: { inference_state: inference },
}),
conversationSummary: conversationSummaryFixture({
declinedSkippedTopics: [{ target_domain: "family", status: "skipped" }],
activeFocus: activeFocusFixture({
intent: "distinguish_candidates",
targetDomain: "relocation",
targetKind: "home_change",
expectedAnswerSchema: {
choice: {
prompt: RELOCATION_2015_PROBE.question,
options: STYLE_OPTIONS.map((option, index) => ({
key: (["A", "B", "C", "D"] as const)[index]!,
label: option.label,
answer_class: option.answer_class,
})),
},
probe_id: RELOCATION_2015_PROBE.id,
semantic_key: RELOCATION_2015_PROBE.semantic_key,
candidate_split_hash: RELOCATION_2015_PROBE.candidate_split_hash,
},
}),
}),
});
}
function persistChoiceAccounting(
dossier: ReturnType<typeof twoProbeDossier>,
extra: Parameters<typeof fakeAccounting>[0] = {},
@@ -515,6 +583,30 @@ test("synthetic choice taps are not kept as chat user lines", () => {
assert.equal(isStructuredChoiceUserText("没有,那年很顺利"), false);
});
test("last structured choice emits the adoption range and persists the same narration", async () => {
const accounting = persistChoiceAccounting(adoptionDossier());
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: "A",
expectedRevision: adoptionInference().revision,
});
assert.equal(applied.nextAction.type, "offer_provisional_range");
assert.equal(applied.nextAction.can_adopt, true);
assert.match(applied.narration, /当前可信区间是/);
assert.match(applied.narration, /代表分钟/);
assert.match(applied.narration, /代表分钟只是代表性候选,不是已确认的唯一出生分钟。/);
assert.match(applied.narration, /可以从下面的时间里选一个采用/);
const turn = accounting.calls.find((call) => call.fn === "append_agentic_rectification_turn");
assert.match(String(turn?.args.p_assistant_message ?? ""), /可以从下面的时间里选一个采用/);
});
test("clicking A applies the choice without invoking a language model", async () => {
const accounting = choiceAccounting();
const applied = await applyRectificationChoice(accounting.client, {