fix(rectification): persist the next interview after a choice tap
Closing a discriminator used to leave GET without a card after refresh. Write the next dated question in the same request, skip childhood career and move probes, and do not continue a read-only turn when that question is already persisted. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -3,10 +3,12 @@ import { readFileSync } from "node:fs";
|
||||
import test from "node:test";
|
||||
|
||||
import { applyRectificationChoice } from "../src/lib/rectification-agentic/v9/answer-choice.ts";
|
||||
import { choiceCardFromCaseDossier } from "../src/lib/rectification-agentic/v9/interview-state.ts";
|
||||
import {
|
||||
CHOICE_ACTION,
|
||||
STOP_ACTION,
|
||||
composeChoiceNarration,
|
||||
isStructuredChoiceUserText,
|
||||
quoteIsFromAssistantQuestion,
|
||||
shouldContinueAfterStructuredChoice,
|
||||
userVisibleChoiceLine,
|
||||
@@ -22,12 +24,14 @@ import { buildInferenceState } from "../src/lib/rectification-agentic/core/build
|
||||
import { parseV9CaseDossier, RectificationToolServiceError } from "../src/lib/rectification-agentic/v9/tool-service.ts";
|
||||
import {
|
||||
CASE_ID,
|
||||
EVIDENCE_ID,
|
||||
FOCUS_ID,
|
||||
SESSION_ID,
|
||||
TURN_ID,
|
||||
USER_ID,
|
||||
activeFocusFixture,
|
||||
candidateSnapshotFixture,
|
||||
computeFixture,
|
||||
conversationSummaryFixture,
|
||||
dossierFixture,
|
||||
fakeAccounting,
|
||||
@@ -36,6 +40,116 @@ import {
|
||||
|
||||
const ACTION_ID = "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa";
|
||||
const QUESTION_ID = "question-1";
|
||||
const NEXT_FOCUS_ID = "acacacac-acac-4cac-8cac-acacacacacac";
|
||||
const EDUCATION_ID = "44444444-4444-4444-8444-444444444445";
|
||||
const RELATIONSHIP_ID = "44444444-4444-4444-8444-444444444446";
|
||||
const CAREER_EXIT_ID = "44444444-4444-4444-8444-444444444447";
|
||||
const RELATIONSHIP_END_ID = "44444444-4444-4444-8444-444444444448";
|
||||
|
||||
const STYLE_OPTIONS = [
|
||||
{ label: "明确发生且时间吻合", answer_class: "yes" as const },
|
||||
{ label: "发生过但程度较弱", answer_class: "weak_yes" as const },
|
||||
{ label: "明确没有发生", answer_class: "no" as const },
|
||||
{ label: "这段记不清楚", answer_class: "unsure" as const },
|
||||
];
|
||||
|
||||
const RELOCATION_2015_PROBE = {
|
||||
id: "p-reloc-2015",
|
||||
semantic_key: "relocation.2015.05.dasha_boundary",
|
||||
candidate_split_hash: "reloc-2015-split",
|
||||
domain: "relocation",
|
||||
year: 2015,
|
||||
month: 5,
|
||||
question: "2015 年 5 月前后有没有搬家或长期住到外地?",
|
||||
candidate_ids: ["05:00", "05:10"],
|
||||
expected_outcomes: [
|
||||
{ answer_class: "yes", supports: ["05:00"], conflicts: ["05:10"] },
|
||||
{ answer_class: "no", supports: ["05:10"], conflicts: ["05:00"] },
|
||||
{ answer_class: "unsure", supports: [], conflicts: [] },
|
||||
],
|
||||
information_gain: 0.87,
|
||||
source: "dasha_boundary",
|
||||
};
|
||||
|
||||
const EDUCATION_2014_PROBE = {
|
||||
id: "p-edu-2014",
|
||||
semantic_key: "education.2014",
|
||||
candidate_split_hash: "edu-2014-split",
|
||||
domain: "education",
|
||||
year: 2014,
|
||||
question: "2014 年前后有没有升学、转学或换学习环境?",
|
||||
candidate_ids: ["05:00", "05:10"],
|
||||
expected_outcomes: [
|
||||
{ answer_class: "yes", supports: ["05:00"], conflicts: ["05:10"] },
|
||||
{ answer_class: "no", supports: ["05:10"], conflicts: ["05:00"] },
|
||||
{ answer_class: "unsure", supports: [], conflicts: [] },
|
||||
],
|
||||
information_gain: 0.64,
|
||||
source: "dasha_activation",
|
||||
};
|
||||
|
||||
const FAMILY_2021_COLLECT = {
|
||||
year: 2021,
|
||||
year_label: "2021 年前后",
|
||||
domain: "family",
|
||||
event_family: "家人结婚、添丁或住院",
|
||||
source: "age_band",
|
||||
tracks: ["vimshottari", "narayana"],
|
||||
tracks_agree: false,
|
||||
unique_minute_claim: false,
|
||||
user_meaning: "时间范围锁定 2021 年前后;领域锁定 family;语义目标是家人结婚、添丁或住院。",
|
||||
role: "collect",
|
||||
phase: "evidence_collection",
|
||||
information_gain: 0,
|
||||
semantic_key: "family.2021",
|
||||
candidate_split_hash: "family:2021",
|
||||
candidate_ids: [],
|
||||
expected_outcomes: [],
|
||||
style_options: STYLE_OPTIONS,
|
||||
choice_kind: "existence",
|
||||
};
|
||||
|
||||
const YEARLESS_D24_PROBE = {
|
||||
id: "contrast:varga.d24.05:00/05:10",
|
||||
semantic_key: "varga.d24.05:00/05:10",
|
||||
candidate_split_hash: "varga.d24.05:00/05:10",
|
||||
domain: "education",
|
||||
year: 0,
|
||||
question: "引擎给出的区分机会绑定 D24。",
|
||||
candidate_ids: ["05:00", "05:10"],
|
||||
expected_outcomes: [
|
||||
{ answer_class: "yes", supports: ["05:00"], conflicts: ["05:10"] },
|
||||
{ answer_class: "no", supports: ["05:10"], conflicts: ["05:00"] },
|
||||
],
|
||||
information_gain: 2.5,
|
||||
source: "varga_contrast",
|
||||
};
|
||||
|
||||
function eventProbeFromConflict(probe: typeof RELOCATION_2015_PROBE | typeof EDUCATION_2014_PROBE) {
|
||||
return {
|
||||
year: probe.year,
|
||||
year_label: `${probe.year} 年前后`,
|
||||
...("month" in probe && probe.month ? { month: probe.month, year_label: `${probe.year} 年 ${probe.month} 月前后` } : {}),
|
||||
domain: probe.domain,
|
||||
event_family: probe.domain === "education"
|
||||
? "升学、转学或换学习环境"
|
||||
: "搬家或长期住到外地",
|
||||
source: probe.source,
|
||||
tracks: ["vimshottari", "narayana"],
|
||||
tracks_agree: false,
|
||||
unique_minute_claim: false,
|
||||
user_meaning: probe.question,
|
||||
role: "distinguish",
|
||||
phase: "candidate_discriminator",
|
||||
information_gain: probe.information_gain,
|
||||
semantic_key: probe.semantic_key,
|
||||
candidate_split_hash: probe.candidate_split_hash,
|
||||
candidate_ids: probe.candidate_ids,
|
||||
expected_outcomes: probe.expected_outcomes,
|
||||
style_options: STYLE_OPTIONS,
|
||||
choice_kind: "existence",
|
||||
};
|
||||
}
|
||||
|
||||
function inferenceState() {
|
||||
return buildInferenceState({
|
||||
@@ -100,6 +214,219 @@ function choiceDossier() {
|
||||
});
|
||||
}
|
||||
|
||||
function twoProbeInference() {
|
||||
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: "career", year: 2026, precision: "day" },
|
||||
{ id: "e5", domain: "relationship", year: 2024, precision: "day" },
|
||||
],
|
||||
probes: [RELOCATION_2015_PROBE, EDUCATION_2014_PROBE],
|
||||
});
|
||||
}
|
||||
|
||||
function fourEventRows() {
|
||||
return [
|
||||
{
|
||||
id: EVIDENCE_ID,
|
||||
source_turn_id: TURN_ID,
|
||||
subject: "self",
|
||||
event_kind: "career_entry",
|
||||
domain: "career",
|
||||
occurred_from: "2020-04-01",
|
||||
occurred_to: "2020-04-01",
|
||||
date_precision: "month",
|
||||
summary: "2020 年 4 月开始实习",
|
||||
status: "confirmed",
|
||||
supersedes_evidence_id: null,
|
||||
created_at: "2026-08-28T07:35:45.000Z",
|
||||
},
|
||||
{
|
||||
id: EDUCATION_ID,
|
||||
source_turn_id: TURN_ID,
|
||||
subject: "self",
|
||||
event_kind: "career_entry",
|
||||
domain: "career",
|
||||
occurred_from: "2024-04-07",
|
||||
occurred_to: null,
|
||||
date_precision: "day",
|
||||
summary: "2024 年 4 月 7 日正式入职",
|
||||
status: "confirmed",
|
||||
supersedes_evidence_id: null,
|
||||
created_at: "2026-08-28T07:36:35.000Z",
|
||||
},
|
||||
{
|
||||
id: RELATIONSHIP_ID,
|
||||
source_turn_id: TURN_ID,
|
||||
subject: "self",
|
||||
event_kind: "relationship_start",
|
||||
domain: "relationship",
|
||||
occurred_from: "2024-05-01",
|
||||
occurred_to: "2024-05-01",
|
||||
date_precision: "month",
|
||||
summary: "2024 年 5 月认识一位女生",
|
||||
status: "confirmed",
|
||||
supersedes_evidence_id: null,
|
||||
created_at: "2026-08-28T07:37:02.000Z",
|
||||
},
|
||||
{
|
||||
id: CAREER_EXIT_ID,
|
||||
source_turn_id: TURN_ID,
|
||||
subject: "self",
|
||||
event_kind: "career_exit",
|
||||
domain: "career",
|
||||
occurred_from: "2026-08-11",
|
||||
occurred_to: null,
|
||||
date_precision: "day",
|
||||
summary: "2026 年 8 月 11 日离职",
|
||||
status: "confirmed",
|
||||
supersedes_evidence_id: null,
|
||||
created_at: "2026-08-28T07:36:35.000Z",
|
||||
},
|
||||
{
|
||||
id: RELATIONSHIP_END_ID,
|
||||
source_turn_id: TURN_ID,
|
||||
subject: "self",
|
||||
event_kind: "relationship_end",
|
||||
domain: "relationship",
|
||||
occurred_from: "2024-08-08",
|
||||
occurred_to: "2024-08-08",
|
||||
date_precision: "day",
|
||||
summary: "2024 年 8 月 8 日与对方分手",
|
||||
status: "confirmed",
|
||||
supersedes_evidence_id: null,
|
||||
created_at: "2026-08-28T07:37:02.000Z",
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function twoProbeDossier() {
|
||||
const inference = twoProbeInference();
|
||||
const snapshot = candidateSnapshotFixture({
|
||||
decisionReceipt: {
|
||||
inference_state: inference,
|
||||
discriminating_event_probes: [
|
||||
eventProbeFromConflict(RELOCATION_2015_PROBE),
|
||||
eventProbeFromConflict(EDUCATION_2014_PROBE),
|
||||
],
|
||||
},
|
||||
});
|
||||
return dossierFixture({
|
||||
evidenceCount: 5,
|
||||
evidence: fourEventRows(),
|
||||
latestResult: snapshot,
|
||||
conversationSummary: conversationSummaryFixture({
|
||||
activeFocus: activeFocusFixture({
|
||||
intent: "distinguish_candidates",
|
||||
targetDomain: "relocation",
|
||||
targetKind: "home_change",
|
||||
expectedAnswerSchema: {
|
||||
choice: {
|
||||
prompt: "2015 年 5 月前后,有没有搬家或长期住到外地?",
|
||||
option_a: "明确发生且时间吻合",
|
||||
option_b: "发生过但程度较弱",
|
||||
option_c: "明确没有发生",
|
||||
option_d: "这段记不清楚",
|
||||
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,
|
||||
},
|
||||
}),
|
||||
}),
|
||||
turns: [{
|
||||
id: TURN_ID,
|
||||
role: "assistant",
|
||||
text: "接下来有一个问题需要您点选一下,麻烦看一下下方选项。",
|
||||
status: "completed",
|
||||
created_at: "2026-08-28T07:36:54.000Z",
|
||||
completed_at: "2026-08-28T07:37:34.000Z",
|
||||
}],
|
||||
});
|
||||
}
|
||||
|
||||
function familyCollectInference() {
|
||||
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: "career", year: 2026, precision: "day" },
|
||||
{ id: "e5", domain: "relationship", year: 2024, precision: "day" },
|
||||
],
|
||||
probes: [RELOCATION_2015_PROBE, YEARLESS_D24_PROBE],
|
||||
});
|
||||
}
|
||||
|
||||
function familyCollectDossier() {
|
||||
const inference = familyCollectInference();
|
||||
const snapshot = candidateSnapshotFixture({
|
||||
decisionReceipt: {
|
||||
inference_state: inference,
|
||||
discriminating_event_probes: [eventProbeFromConflict(RELOCATION_2015_PROBE)],
|
||||
evidence_collection_probes: [FAMILY_2021_COLLECT],
|
||||
},
|
||||
});
|
||||
return dossierFixture({
|
||||
evidenceCount: 5,
|
||||
evidence: fourEventRows(),
|
||||
latestResult: snapshot,
|
||||
conversationSummary: conversationSummaryFixture({
|
||||
activeFocus: activeFocusFixture({
|
||||
intent: "distinguish_candidates",
|
||||
targetDomain: "relocation",
|
||||
targetKind: "home_change",
|
||||
expectedAnswerSchema: {
|
||||
choice: {
|
||||
prompt: "2015 年 5 月前后,有没有搬家或长期住到外地?",
|
||||
option_a: "明确发生且时间吻合",
|
||||
option_b: "发生过但程度较弱",
|
||||
option_c: "明确没有发生",
|
||||
option_d: "这段记不清楚",
|
||||
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,
|
||||
},
|
||||
}),
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
function persistChoiceAccounting(
|
||||
dossier: ReturnType<typeof twoProbeDossier>,
|
||||
extra: Parameters<typeof fakeAccounting>[0] = {},
|
||||
) {
|
||||
return choiceAccounting({
|
||||
get_agentic_rectification_case_dossier: () => dossier,
|
||||
get_agentic_rectification_case_compute: () => computeFixture(),
|
||||
...extra,
|
||||
});
|
||||
}
|
||||
|
||||
function choiceAccounting(overrides: Parameters<typeof fakeAccounting>[0] = {}) {
|
||||
const receipts = new Map<string, Record<string, unknown>>();
|
||||
return fakeAccounting({
|
||||
@@ -172,6 +499,15 @@ test("choice quotes come from the option label, not the assistant question year"
|
||||
assert.equal(userVisibleChoiceLine(card, "A"), "A. 是,大概就在那段时间");
|
||||
});
|
||||
|
||||
test("synthetic choice taps are not kept as chat user lines", () => {
|
||||
assert.equal(isStructuredChoiceUserText("A. 发挥明显失常或压力很大"), true);
|
||||
assert.equal(isStructuredChoiceUserText("先这样"), true);
|
||||
assert.equal(isStructuredChoiceUserText("先这样,先看当前范围"), true);
|
||||
assert.equal(isStructuredChoiceUserText("盘外核对(不计分):A. 明确发生且时间吻合"), true);
|
||||
assert.equal(isStructuredChoiceUserText("2016年我上了大学"), false);
|
||||
assert.equal(isStructuredChoiceUserText("没有,那年很顺利"), false);
|
||||
});
|
||||
|
||||
test("clicking A applies the choice without invoking a language model", async () => {
|
||||
const accounting = choiceAccounting();
|
||||
const applied = await applyRectificationChoice(accounting.client, {
|
||||
@@ -202,7 +538,7 @@ test("clicking A applies the choice without invoking a language model", async ()
|
||||
}));
|
||||
const fns = accounting.calls.map((call) => call.fn);
|
||||
assert.ok(fns.includes("apply_agentic_rectification_choice_action"));
|
||||
assert.ok(fns.includes("append_agentic_rectification_turn"));
|
||||
assert.equal(fns.includes("append_agentic_rectification_turn"), false);
|
||||
assert.equal(fns.includes("record_agentic_rectification_evidence_batch"), false);
|
||||
assert.equal(fns.some((fn) => fn === "create_agentic_rectification_run_attempt" || fn.includes("stream")), false);
|
||||
const persist = accounting.calls.find((call) => call.fn === "apply_agentic_rectification_choice_action");
|
||||
@@ -219,6 +555,129 @@ test("clicking A applies the choice without invoking a language model", async ()
|
||||
|| applied.nextAction.type === "ask_fact_collection", true);
|
||||
});
|
||||
|
||||
test("answering a discriminator persists the next dated card so GET still has a tap target", async () => {
|
||||
const closed = parseV9CaseDossier(twoProbeDossier());
|
||||
assert.ok(closed);
|
||||
closed.conversationSummary.activeFocus = null;
|
||||
assert.equal(choiceCardFromCaseDossier(closed), null);
|
||||
|
||||
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: "C",
|
||||
expectedRevision: twoProbeInference().revision,
|
||||
});
|
||||
const setFocus = accounting.calls.find((call) => call.fn === "set_agentic_rectification_conversation_focus");
|
||||
assert.ok(setFocus, JSON.stringify(accounting.calls.map((call) => call.fn)));
|
||||
assert.equal(setFocus.args.p_intent, "distinguish_candidates");
|
||||
const schema = setFocus.args.p_expected_answer_schema as { semantic_key?: string; choice?: { prompt?: string } };
|
||||
assert.equal(schema.semantic_key, EDUCATION_2014_PROBE.semantic_key);
|
||||
assert.match(schema.choice?.prompt ?? "", /2014/);
|
||||
assert.doesNotMatch(schema.choice?.prompt ?? "", /2015/);
|
||||
assert.equal(applied.nextInterviewPersisted, true);
|
||||
assert.equal(applied.nextChoiceReady, true);
|
||||
assert.equal(shouldContinueAfterStructuredChoice(applied.nextAction, applied), false);
|
||||
assert.equal(applied.narration, "接下来请点选下面这一问。");
|
||||
const turn = accounting.calls.find((call) => call.fn === "append_agentic_rectification_turn");
|
||||
assert.equal(turn?.args.p_assistant_message, "接下来请点选下面这一问。");
|
||||
|
||||
const refreshed = parseV9CaseDossier(twoProbeDossier());
|
||||
assert.ok(refreshed);
|
||||
const answered = twoProbeInference();
|
||||
refreshed.latestResult = {
|
||||
...refreshed.latestResult!,
|
||||
decisionReceipt: {
|
||||
...(refreshed.latestResult?.decisionReceipt ?? {}),
|
||||
inference_state: {
|
||||
...answered,
|
||||
answered_probes: [{
|
||||
probe_id: RELOCATION_2015_PROBE.id,
|
||||
semantic_key: RELOCATION_2015_PROBE.semantic_key,
|
||||
candidate_split_hash: RELOCATION_2015_PROBE.candidate_split_hash,
|
||||
answer_class: "no",
|
||||
classified_from: "choice",
|
||||
}],
|
||||
},
|
||||
},
|
||||
};
|
||||
refreshed.conversationSummary.activeFocus = {
|
||||
id: NEXT_FOCUS_ID,
|
||||
caseId: CASE_ID,
|
||||
questionId: String(setFocus.args.p_question_id),
|
||||
intent: String(setFocus.args.p_intent),
|
||||
targetEvidenceId: null,
|
||||
targetDomain: typeof setFocus.args.p_target_domain === "string" ? setFocus.args.p_target_domain : "education",
|
||||
targetKind: null,
|
||||
expectedAnswerSchema: schema,
|
||||
status: "active",
|
||||
askedAt: "2026-08-28T07:37:50.000Z",
|
||||
resolvedAt: null,
|
||||
};
|
||||
const card = choiceCardFromCaseDossier(refreshed);
|
||||
assert.ok(card);
|
||||
assert.equal(card.focus_id, NEXT_FOCUS_ID);
|
||||
assert.match(card.prompt, /2014/);
|
||||
assert.doesNotMatch(card.prompt, /2015/);
|
||||
});
|
||||
|
||||
test("answering the last discriminator persists a year-locked family collect, not a yearless D24 card", async () => {
|
||||
const accounting = persistChoiceAccounting(familyCollectDossier(), {
|
||||
set_agentic_rectification_conversation_focus: () => {
|
||||
throw new Error("yearless D24 must not persist a scoring focus");
|
||||
},
|
||||
});
|
||||
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: "C",
|
||||
expectedRevision: familyCollectInference().revision,
|
||||
});
|
||||
assert.equal(
|
||||
accounting.calls.some((call) => call.fn === "set_agentic_rectification_conversation_focus"),
|
||||
false,
|
||||
JSON.stringify(accounting.calls.map((call) => call.fn)),
|
||||
);
|
||||
assert.equal(applied.nextInterviewPersisted, true);
|
||||
assert.equal(applied.nextChoiceReady, false);
|
||||
assert.equal(shouldContinueAfterStructuredChoice(applied.nextAction, applied), false);
|
||||
assert.match(applied.narration, /2021/);
|
||||
assert.match(applied.narration, /家人|结婚|添丁|住院/);
|
||||
assert.doesNotMatch(applied.narration, /点选/);
|
||||
assert.doesNotMatch(applied.narration, /D24/);
|
||||
const turn = accounting.calls.find((call) => call.fn === "append_agentic_rectification_turn");
|
||||
assert.match(String(turn?.args.p_assistant_message ?? ""), /2021/);
|
||||
});
|
||||
|
||||
test("replaying the same actionId does not duplicate the applied receipt", async () => {
|
||||
const accounting = choiceAccounting();
|
||||
const command = {
|
||||
@@ -286,6 +745,7 @@ test("stop_and_review does not write an inference transition", async () => {
|
||||
const persist = accounting.calls.find((call) => call.fn === "apply_agentic_rectification_choice_action");
|
||||
assert.equal(persist?.args.p_inference, null);
|
||||
assert.equal(persist?.args.p_focus_status, "skipped");
|
||||
assert.ok(accounting.calls.some((call) => call.fn === "append_agentic_rectification_turn"));
|
||||
});
|
||||
|
||||
test("turn_decision stays inside the configured byte budget", () => {
|
||||
@@ -356,6 +816,10 @@ test("structured choice only continues through the agent when another question i
|
||||
assert.equal(shouldContinueAfterStructuredChoice({ type }), false);
|
||||
}
|
||||
assert.equal(shouldContinueAfterStructuredChoice(null), false);
|
||||
assert.equal(
|
||||
shouldContinueAfterStructuredChoice({ type: "ask_candidate_discriminator" }, { nextInterviewPersisted: true }),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test("structured choice narration never persists a fake loading state", () => {
|
||||
@@ -384,8 +848,10 @@ test("the public agent route treats structured choice as a non-model command", (
|
||||
const chat = readFileSync(new URL("../src/components/rectification-agentic-chat.tsx", import.meta.url), "utf8");
|
||||
assert.match(chat, /isPersistedFocusId\(focusId\)/);
|
||||
assert.match(chat, /focusId,/);
|
||||
assert.match(chat, /shouldContinueAfterStructuredChoice\(payload\?\.nextAction\)/);
|
||||
assert.match(chat, /shouldContinueAfterStructuredChoice\(payload\?\.nextAction, payload\)/);
|
||||
assert.match(chat, /send\("read_only", ""\)/);
|
||||
assert.match(chat, /willContinue/);
|
||||
assert.match(chat, /current\.filter\(\(message\) => message\.renderKey !== assistantRenderKey\)/);
|
||||
assert.match(chat, /回到最新/);
|
||||
assert.match(chat, /followTailRef\.current/);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user