35e5781e66
Public can_adopt follows session_outcome; reverse_verify reuses the persisted question id; offer cards settle on the owning message with a status-bar handoff. Co-authored-by: Cursor <cursoragent@cursor.com>
745 lines
26 KiB
TypeScript
745 lines
26 KiB
TypeScript
import assert from "node:assert/strict";
|
||
import test from "node:test";
|
||
|
||
import { buildInferenceState } from "../src/lib/rectification-agentic/core/build-state.ts";
|
||
import {
|
||
COLLECT_FOCUS_RETRY_SUFFIX,
|
||
openQuestionFromPersistedFocus,
|
||
persistableFocusDomain,
|
||
persistServerOwnedFocus,
|
||
shouldSkipDiscriminatorFollowup,
|
||
stableFollowupQuestionId,
|
||
} from "../src/lib/rectification-agentic/v9/server-focus.ts";
|
||
import { buildChoiceFrame, serverOwnedChoiceCopy } from "../src/lib/rectification-agentic/v9/choice-card.ts";
|
||
import {
|
||
buildMethodFollowupPlan,
|
||
spokenCollectFallbackFollowup,
|
||
spokenFollowupForUser,
|
||
type MethodFollowup,
|
||
} from "../src/lib/rectification-agentic/v9/method-followup.ts";
|
||
import type { EventProbeStyleOption } from "../src/lib/rectification-agentic/v9/refinement-packet.ts";
|
||
import { fakeAccounting, CASE_ID, FOCUS_ID, USER_ID, activeFocusFixture } from "./rectification-v9-test-support.ts";
|
||
|
||
const DYNAMIC_STYLE_OPTIONS = [
|
||
{ label: "明确发生且时间吻合", answer_class: "yes" },
|
||
{ label: "发生过但程度较弱", answer_class: "weak_yes" },
|
||
{ label: "明确没有发生", answer_class: "no" },
|
||
{ label: "这段记不清楚", answer_class: "unsure" },
|
||
] as const;
|
||
|
||
function discriminatorFollowup(overrides: Partial<MethodFollowup> = {}): MethodFollowup {
|
||
const frame = buildChoiceFrame({
|
||
method_id: "dasha_events",
|
||
ask_theme: "dated_event",
|
||
domain: "education",
|
||
user_prompt_hint: "ask",
|
||
}, {
|
||
probes: [{
|
||
year: 2016,
|
||
year_label: "2016 年前后",
|
||
domain: "education",
|
||
event_family: "升学结果或学习环境出现明显变化",
|
||
source: "dasha_activation",
|
||
tracks: ["vimshottari", "narayana"],
|
||
tracks_agree: true,
|
||
unique_minute_claim: false,
|
||
user_meaning: "2016 年前后升学结果或学习环境出现明显变化",
|
||
role: "distinguish",
|
||
information_gain: 0.4,
|
||
semantic_key: "education:2016",
|
||
style_options: DYNAMIC_STYLE_OPTIONS,
|
||
}],
|
||
});
|
||
assert.ok(frame);
|
||
return {
|
||
method_id: "dasha_events",
|
||
intent: "distinguish_candidates",
|
||
ask_theme: "dated_event",
|
||
domain: "education",
|
||
kind_hint: null,
|
||
user_prompt_hint: "ask",
|
||
must_not_label: false,
|
||
choice_frame: frame,
|
||
source: "event_probe",
|
||
information_gain: 0.4,
|
||
semantic_key: "education:2016",
|
||
probe_year: 2016,
|
||
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"] },
|
||
],
|
||
...overrides,
|
||
};
|
||
}
|
||
|
||
function invalidStyleFollowup(
|
||
styleOptions: readonly EventProbeStyleOption[] | undefined,
|
||
eventFamily = "升学结果或学习环境出现明显变化",
|
||
choiceKind: "existence" | "varga_style" | "event_quality" = "existence",
|
||
): MethodFollowup {
|
||
const valid = discriminatorFollowup();
|
||
return {
|
||
...valid,
|
||
choice_kind: choiceKind,
|
||
choice_frame: buildChoiceFrame({
|
||
method_id: "dasha_events",
|
||
ask_theme: "dated_event",
|
||
domain: "education",
|
||
user_prompt_hint: "ask",
|
||
choice_kind: choiceKind,
|
||
}, {
|
||
probes: [{
|
||
year: 2016,
|
||
year_label: "2016 年前后",
|
||
domain: "education",
|
||
event_family: eventFamily,
|
||
source: "dasha_activation",
|
||
tracks: ["vimshottari", "narayana"],
|
||
tracks_agree: true,
|
||
unique_minute_claim: false,
|
||
user_meaning: "2016 年前后升学结果或学习环境出现明显变化",
|
||
role: "distinguish",
|
||
information_gain: 0.4,
|
||
semantic_key: "education:2016",
|
||
choice_kind: choiceKind,
|
||
style_options: styleOptions,
|
||
}],
|
||
}),
|
||
};
|
||
}
|
||
|
||
async function assertUnrenderableChoiceFallsBackToSpoken(followup: MethodFollowup) {
|
||
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: null,
|
||
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-31T00:00:00.000Z",
|
||
resolved_at: null,
|
||
idempotent: false,
|
||
}),
|
||
});
|
||
const result = await persistServerOwnedFocus({
|
||
accounting: accounting.client,
|
||
userId: USER_ID,
|
||
caseId: CASE_ID,
|
||
activeFocus: null,
|
||
decisionReceipt: null,
|
||
followup,
|
||
});
|
||
assert.ok(result.status === "created" || result.status === "already_open");
|
||
assert.equal(result.focus?.intent, "collect_method_evidence");
|
||
const open = openQuestionFromPersistedFocus(result);
|
||
assert.equal(open?.kind, "collect_spoken");
|
||
assert.ok(open?.prompt?.trim());
|
||
}
|
||
|
||
function persistedFocus(questionId = "probe:education:2016") {
|
||
const copy = serverOwnedChoiceCopy(discriminatorFollowup().choice_frame!);
|
||
assert.ok(copy);
|
||
return {
|
||
id: FOCUS_ID,
|
||
caseId: CASE_ID,
|
||
questionId,
|
||
intent: "distinguish_candidates",
|
||
targetEvidenceId: null,
|
||
targetDomain: "education",
|
||
targetKind: null,
|
||
expectedAnswerSchema: {
|
||
choice: copy,
|
||
semantic_key: "education:2016",
|
||
candidate_split_hash: "education:2016",
|
||
},
|
||
status: "active" as const,
|
||
askedAt: "2026-08-27T00:00:00.000Z",
|
||
resolvedAt: null,
|
||
};
|
||
}
|
||
|
||
test("only a successfully persisted focus can expose its discriminator question", () => {
|
||
const focus = persistedFocus();
|
||
for (const status of ["created", "already_open"] as const) {
|
||
assert.deepEqual(openQuestionFromPersistedFocus({
|
||
status,
|
||
focus,
|
||
questionId: focus.questionId,
|
||
prompt: "2016 年前后 · 升学结果或学习环境出现明显变化",
|
||
}), {
|
||
question_id: focus.questionId,
|
||
prompt: "2016 年前后 · 升学结果或学习环境出现明显变化",
|
||
status,
|
||
kind: "choice",
|
||
});
|
||
}
|
||
});
|
||
|
||
test("an unpersisted, mismatched, or incomplete focus cannot expose a discriminator question", () => {
|
||
const focus = persistedFocus();
|
||
assert.equal(openQuestionFromPersistedFocus({
|
||
status: "duplicate_focus",
|
||
focus,
|
||
questionId: focus.questionId,
|
||
prompt: "不能展示",
|
||
}), null);
|
||
assert.equal(openQuestionFromPersistedFocus({
|
||
status: "created",
|
||
focus: { ...focus, questionId: "another-question" },
|
||
questionId: focus.questionId,
|
||
prompt: "不能展示",
|
||
}), null);
|
||
const incomplete = openQuestionFromPersistedFocus({
|
||
status: "created",
|
||
focus: { ...focus, expectedAnswerSchema: { choice: { prompt: "不完整" } } },
|
||
questionId: focus.questionId,
|
||
prompt: "不能展示",
|
||
});
|
||
assert.equal(incomplete?.unrenderable, true);
|
||
assert.equal(incomplete?.prompt, null);
|
||
assert.equal(incomplete?.reason, "invalid_choice_schema");
|
||
});
|
||
|
||
test("complete dynamic event options persist a server-owned focus", async () => {
|
||
const followup = discriminatorFollowup({ source: "precision_stage", information_gain: 0.2 });
|
||
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: null,
|
||
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-27T00:00:00.000Z",
|
||
resolved_at: null,
|
||
idempotent: false,
|
||
}),
|
||
});
|
||
const result = await persistServerOwnedFocus({
|
||
accounting: accounting.client,
|
||
userId: USER_ID,
|
||
caseId: CASE_ID,
|
||
activeFocus: null,
|
||
decisionReceipt: null,
|
||
followup,
|
||
});
|
||
assert.equal(result.status, "created");
|
||
assert.equal(accounting.calls.length, 1);
|
||
assert.deepEqual(result.focus?.expectedAnswerSchema.choice, {
|
||
prompt: "2016 年前后,有没有升学结果或学习环境出现明显变化?",
|
||
option_a: DYNAMIC_STYLE_OPTIONS[0].label,
|
||
option_b: DYNAMIC_STYLE_OPTIONS[1].label,
|
||
option_c: DYNAMIC_STYLE_OPTIONS[2].label,
|
||
option_d: DYNAMIC_STYLE_OPTIONS[3].label,
|
||
options: [
|
||
{ key: "A", label: DYNAMIC_STYLE_OPTIONS[0].label, answer_class: "yes" },
|
||
{ key: "B", label: DYNAMIC_STYLE_OPTIONS[1].label, answer_class: "weak_yes" },
|
||
{ key: "C", label: DYNAMIC_STYLE_OPTIONS[2].label, answer_class: "no" },
|
||
{ key: "D", label: DYNAMIC_STYLE_OPTIONS[3].label, answer_class: "unsure" },
|
||
],
|
||
});
|
||
});
|
||
|
||
test("non-renderable varga styles and empty event family fall back to spoken collect", async () => {
|
||
await assertUnrenderableChoiceFallsBackToSpoken(invalidStyleFollowup(
|
||
[{ label: "巨蟹相处主动热情", answer_class: "yes" }],
|
||
"升学结果或学习环境出现明显变化",
|
||
"varga_style",
|
||
));
|
||
await assertUnrenderableChoiceFallsBackToSpoken(invalidStyleFollowup(DYNAMIC_STYLE_OPTIONS, " "));
|
||
});
|
||
|
||
test("does not ask an already-answered discriminator probe again", async () => {
|
||
const followup = discriminatorFollowup();
|
||
const active = activeFocusFixture({
|
||
expectedAnswerSchema: { probe_id: "education:2016", choice: serverOwnedChoiceCopy(followup.choice_frame!) },
|
||
});
|
||
const accounting = fakeAccounting({
|
||
set_agentic_rectification_conversation_focus: () => {
|
||
throw new Error("should not create a second focus");
|
||
},
|
||
});
|
||
const result = await persistServerOwnedFocus({
|
||
accounting: accounting.client,
|
||
userId: USER_ID,
|
||
caseId: CASE_ID,
|
||
activeFocus: {
|
||
...active,
|
||
id: FOCUS_ID,
|
||
caseId: CASE_ID,
|
||
questionId: stableFollowupQuestionId(followup),
|
||
intent: "distinguish_candidates",
|
||
targetEvidenceId: null,
|
||
targetDomain: "education",
|
||
targetKind: null,
|
||
expectedAnswerSchema: { probe_id: "education:2016" },
|
||
status: "active",
|
||
askedAt: "2026-08-24T00:00:00.000Z",
|
||
resolvedAt: null,
|
||
},
|
||
decisionReceipt: {
|
||
inference_state: {
|
||
answered_probes: [{ probe_id: "education:2016", semantic_key: "education:2016", answer_class: "yes" }],
|
||
probes: [],
|
||
},
|
||
},
|
||
followup,
|
||
});
|
||
assert.equal(result.status, "probe_already_answered");
|
||
assert.equal(accounting.calls.length, 0);
|
||
});
|
||
|
||
test("zero information gain does not open a discriminator", () => {
|
||
assert.equal(
|
||
shouldSkipDiscriminatorFollowup(discriminatorFollowup({ information_gain: 0 })),
|
||
"zero_information_gain",
|
||
);
|
||
});
|
||
|
||
test("recorded event quality cannot open a zero-information scoring card", () => {
|
||
assert.equal(
|
||
shouldSkipDiscriminatorFollowup(discriminatorFollowup({
|
||
choice_kind: "event_quality",
|
||
information_gain: 0,
|
||
})),
|
||
"zero_information_gain",
|
||
);
|
||
});
|
||
|
||
test("an unmatched scoring identity is skipped instead of being synthesized", async () => {
|
||
const followup = discriminatorFollowup({
|
||
semantic_key: "education.synthetic",
|
||
candidate_split_hash: "education:synthetic",
|
||
});
|
||
const accounting = fakeAccounting({
|
||
set_agentic_rectification_conversation_focus: () => {
|
||
throw new Error("must not persist an unmatched scoring focus");
|
||
},
|
||
});
|
||
const result = await persistServerOwnedFocus({
|
||
accounting: accounting.client,
|
||
userId: USER_ID,
|
||
caseId: CASE_ID,
|
||
activeFocus: null,
|
||
decisionReceipt: {
|
||
inference_state: {
|
||
algorithm_version: "birth-time-event-scoring-v1",
|
||
candidates: [
|
||
{ id: "05:00", birth_time: "05:00", posterior: 0.5 },
|
||
{ id: "05:20", birth_time: "05:20", posterior: 0.5 },
|
||
],
|
||
answered_probes: [],
|
||
probes: [{
|
||
id: "p-live",
|
||
semantic_key: "education.live",
|
||
candidate_split_hash: "education:live",
|
||
domain: "education",
|
||
year: 2016,
|
||
question: "live",
|
||
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"] },
|
||
],
|
||
information_gain: 0.2,
|
||
source: "dasha_activation",
|
||
}],
|
||
},
|
||
},
|
||
followup,
|
||
});
|
||
assert.equal(result.status, "invalid_choice_schema");
|
||
assert.equal(accounting.calls.length, 0);
|
||
});
|
||
|
||
test("duplicate focus conflict does not throw", async () => {
|
||
const followup = discriminatorFollowup({ source: "precision_stage", information_gain: 0.2 });
|
||
const accounting = fakeAccounting({
|
||
set_agentic_rectification_conversation_focus: () => {
|
||
throw new Error("agentic_rectification_focus_idempotency_conflict");
|
||
},
|
||
});
|
||
const result = await persistServerOwnedFocus({
|
||
accounting: accounting.client,
|
||
userId: USER_ID,
|
||
caseId: CASE_ID,
|
||
activeFocus: null,
|
||
decisionReceipt: null,
|
||
followup,
|
||
});
|
||
assert.equal(result.status, "duplicate_focus");
|
||
});
|
||
|
||
test("contrast probe is not replaced by an already-answered education quality probe", async () => {
|
||
const followup = discriminatorFollowup({
|
||
domain: "education",
|
||
ask_theme: "education_style",
|
||
information_gain: 0.16,
|
||
semantic_key: "varga.d24.05:00/05:06|05:07",
|
||
candidate_split_hash: "varga.d24.05:00/05:06|05:07",
|
||
probe_year: undefined,
|
||
});
|
||
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: null,
|
||
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-25T00:00:00.000Z",
|
||
resolved_at: null,
|
||
idempotent: false,
|
||
}),
|
||
});
|
||
const result = await persistServerOwnedFocus({
|
||
accounting: accounting.client,
|
||
userId: USER_ID,
|
||
caseId: CASE_ID,
|
||
activeFocus: null,
|
||
decisionReceipt: {
|
||
inference_state: buildInferenceState({
|
||
range_start: "05:00",
|
||
range_end: "05:20",
|
||
candidates: [
|
||
{ id: "05:00", time: "05:00", relative_support: 10 },
|
||
{ id: "05:20", time: "05:20", relative_support: 10 },
|
||
],
|
||
events: [],
|
||
probes: [{
|
||
id: "probe:education.2016",
|
||
semantic_key: "education.2016",
|
||
candidate_split_hash: "education:2016",
|
||
domain: "education",
|
||
year: 2016,
|
||
question: "发挥失常",
|
||
candidate_ids: [],
|
||
expected_outcomes: [],
|
||
information_gain: 0,
|
||
source: "known_event_quality",
|
||
}, {
|
||
id: "contrast:varga.d24.05:00/05:06|05:07",
|
||
semantic_key: "varga.d24.05:00/05:06|05:07",
|
||
candidate_split_hash: "varga.d24.05:00/05:06|05:07",
|
||
domain: "education",
|
||
year: 0,
|
||
question: "学业盘候选差异",
|
||
candidate_ids: ["05:00", "05:06", "05:07"],
|
||
expected_outcomes: [
|
||
{ answer_class: "yes", supports: ["05:00"], conflicts: ["05:06", "05:07"] },
|
||
{ answer_class: "no", supports: ["05:06", "05:07"], conflicts: ["05:00"] },
|
||
],
|
||
information_gain: 0.16,
|
||
source: "varga_contrast",
|
||
}],
|
||
}),
|
||
},
|
||
followup,
|
||
});
|
||
assert.equal(result.status, "created");
|
||
assert.ok(result.focus);
|
||
const schema = result.focus.expectedAnswerSchema;
|
||
assert.equal(schema.semantic_key, "varga.d24.05:00/05:06|05:07");
|
||
assert.notEqual(schema.probe_id, "probe:education.2016");
|
||
assert.match(String(schema.probe_id), /varga\.d24/);
|
||
});
|
||
|
||
test("spoken collect followup persists a collect focus without a choice card", async () => {
|
||
const followup: MethodFollowup = {
|
||
method_id: "relatives",
|
||
intent: "collect_method_evidence",
|
||
ask_theme: "family_event",
|
||
domain: "family",
|
||
kind_hint: "family_event",
|
||
user_prompt_hint: "collect",
|
||
must_not_label: false,
|
||
choice_frame: null,
|
||
source: "method_coverage",
|
||
probe_year: 2021,
|
||
year_label: "2021 年前后",
|
||
semantic_key: "family.2021",
|
||
};
|
||
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: null,
|
||
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-27T00:00:00.000Z",
|
||
resolved_at: null,
|
||
idempotent: false,
|
||
}),
|
||
});
|
||
const result = await persistServerOwnedFocus({
|
||
accounting: accounting.client,
|
||
userId: USER_ID,
|
||
caseId: CASE_ID,
|
||
activeFocus: null,
|
||
decisionReceipt: null,
|
||
followup,
|
||
});
|
||
assert.equal(result.status, "created");
|
||
assert.equal(result.focus?.intent, "collect_method_evidence");
|
||
assert.equal(result.focus?.expectedAnswerSchema.collect, true);
|
||
assert.equal(result.focus?.expectedAnswerSchema.choice, undefined);
|
||
assert.match(String(result.focus?.expectedAnswerSchema.prompt ?? ""), /2021/);
|
||
const openCollect = openQuestionFromPersistedFocus(result);
|
||
assert.equal(openCollect?.kind, "collect_spoken");
|
||
assert.match(openCollect?.prompt ?? "", /2021/);
|
||
assert.notEqual(openCollect?.unrenderable, true);
|
||
});
|
||
|
||
test("zero-evidence opening persists the server-owned first question", async () => {
|
||
const followup = buildMethodFollowupPlan({ evidence: [] }).next_followup;
|
||
if (!followup) throw new Error("zero-evidence opening must produce a follow-up");
|
||
// 旧:开场 domain 落成 unknown,题干是 GENERIC_COLLECT_QUESTION。
|
||
// 新:开场 domain 取轮转第一个领域 education,题干走该领域采集句。
|
||
// 保留语义:仍是 collect_spoken,仍由服务端 persist,Agent 不自拟开场题。
|
||
assert.equal(followup.domain, "education");
|
||
assert.doesNotMatch(stableFollowupQuestionId(followup), /unknown/);
|
||
const expected = spokenFollowupForUser(followup);
|
||
|
||
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: null,
|
||
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-31T00:00:00.000Z",
|
||
resolved_at: null,
|
||
idempotent: false,
|
||
}),
|
||
});
|
||
const persisted = await persistServerOwnedFocus({
|
||
accounting: accounting.client,
|
||
userId: USER_ID,
|
||
caseId: CASE_ID,
|
||
activeFocus: null,
|
||
decisionReceipt: null,
|
||
followup,
|
||
});
|
||
const open = openQuestionFromPersistedFocus(persisted);
|
||
assert.deepEqual({
|
||
narration: spokenFollowupForUser(followup),
|
||
persistedPrompt: persisted.focus?.expectedAnswerSchema.prompt,
|
||
openKind: open?.kind,
|
||
openPrompt: open?.prompt,
|
||
}, {
|
||
narration: expected,
|
||
persistedPrompt: expected,
|
||
openKind: "collect_spoken",
|
||
openPrompt: expected,
|
||
});
|
||
});
|
||
|
||
test("degraded spoken collect does not keep discriminator identity or block a later card", async () => {
|
||
const discriminator = discriminatorFollowup({
|
||
year_label: "2016 年前后",
|
||
probe_id: "probe:education:2016",
|
||
});
|
||
const spokenFollowup = spokenCollectFallbackFollowup(discriminator);
|
||
const spoken = spokenFollowupForUser(spokenFollowup);
|
||
assert.equal("semantic_key" in spokenFollowup, false);
|
||
assert.equal("candidate_split_hash" in spokenFollowup, false);
|
||
assert.equal("probe_year" in spokenFollowup, false);
|
||
assert.equal("year_label" in spokenFollowup, false);
|
||
assert.equal("probe_id" in spokenFollowup, false);
|
||
assert.equal(stableFollowupQuestionId(spokenFollowup).includes("education:2016"), false);
|
||
assert.doesNotMatch(spoken ?? "", /2016/);
|
||
const accounting = fakeAccounting({
|
||
set_agentic_rectification_conversation_focus: (_fn, args) => ({
|
||
id: args.p_intent === "collect_method_evidence"
|
||
? FOCUS_ID
|
||
: "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb",
|
||
case_id: CASE_ID,
|
||
question_id: args.p_question_id,
|
||
intent: args.p_intent,
|
||
target_evidence_id: null,
|
||
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-27T00:00:00.000Z",
|
||
resolved_at: null,
|
||
idempotent: false,
|
||
}),
|
||
});
|
||
const collected = await persistServerOwnedFocus({
|
||
accounting: accounting.client,
|
||
userId: USER_ID,
|
||
caseId: CASE_ID,
|
||
activeFocus: null,
|
||
decisionReceipt: null,
|
||
followup: {
|
||
...spokenFollowup,
|
||
user_prompt_hint: spoken ?? spokenFollowup.user_prompt_hint,
|
||
},
|
||
});
|
||
assert.equal(collected.status, "created");
|
||
assert.equal(collected.questionId?.includes("education:2016"), false);
|
||
assert.equal(collected.focus?.expectedAnswerSchema.semantic_key, undefined);
|
||
assert.doesNotMatch(String(collected.focus?.expectedAnswerSchema.prompt ?? ""), /2016/);
|
||
assert.equal(collected.focus?.expectedAnswerSchema.prompt, spoken);
|
||
const openCollect = openQuestionFromPersistedFocus(collected);
|
||
assert.equal(openCollect?.kind, "collect_spoken");
|
||
assert.equal(openCollect?.prompt, spoken);
|
||
assert.notEqual(openCollect?.unrenderable, true);
|
||
|
||
const card = await persistServerOwnedFocus({
|
||
accounting: accounting.client,
|
||
userId: USER_ID,
|
||
caseId: CASE_ID,
|
||
activeFocus: collected.focus ?? null,
|
||
decisionReceipt: null,
|
||
followup: discriminator,
|
||
});
|
||
assert.equal(card.status, "created");
|
||
assert.ok(card.focus?.expectedAnswerSchema.choice);
|
||
const open = openQuestionFromPersistedFocus(card);
|
||
assert.ok(open);
|
||
assert.notEqual(open?.unrenderable, true);
|
||
});
|
||
|
||
function collectFollowup(overrides: Partial<MethodFollowup> = {}): MethodFollowup {
|
||
return {
|
||
method_id: "dasha_events",
|
||
intent: "collect_method_evidence",
|
||
ask_theme: "dated_event",
|
||
domain: "relationship",
|
||
kind_hint: null,
|
||
user_prompt_hint: "collect",
|
||
must_not_label: false,
|
||
choice_frame: null,
|
||
source: "method_coverage",
|
||
...overrides,
|
||
};
|
||
}
|
||
|
||
function focusRowFromArgs(args: Record<string, unknown>) {
|
||
return {
|
||
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-31T00:00:00.000Z",
|
||
resolved_at: null,
|
||
idempotent: false,
|
||
};
|
||
}
|
||
|
||
test("collect persist maps occupation to other and health_pressure to health", async () => {
|
||
assert.equal(persistableFocusDomain("occupation"), "other");
|
||
assert.equal(persistableFocusDomain("health_pressure"), "health");
|
||
assert.equal(persistableFocusDomain("education"), "education");
|
||
assert.equal(persistableFocusDomain("horary"), "horary");
|
||
assert.equal(persistableFocusDomain(null), null);
|
||
assert.equal(persistableFocusDomain("unknown"), null);
|
||
assert.equal(stableFollowupQuestionId(collectFollowup({ domain: "unknown" })), "collect:education:collect_method_evidence");
|
||
assert.equal(stableFollowupQuestionId(collectFollowup({ domain: null })), "collect:education:collect_method_evidence");
|
||
|
||
const occupation = collectFollowup({
|
||
method_id: "occupation",
|
||
ask_theme: "occupation",
|
||
domain: "occupation",
|
||
kind_hint: "occupation_note",
|
||
});
|
||
assert.equal(stableFollowupQuestionId(occupation), "collect:occupation:collect_method_evidence");
|
||
const health = collectFollowup({
|
||
method_id: "d30_health",
|
||
ask_theme: "health_pressure",
|
||
domain: "health_pressure",
|
||
});
|
||
const generic = collectFollowup({ domain: "other" });
|
||
assert.equal(stableFollowupQuestionId(generic), "collect:other:collect_method_evidence");
|
||
assert.doesNotMatch(stableFollowupQuestionId(generic), /unknown/);
|
||
|
||
const accounting = fakeAccounting({
|
||
set_agentic_rectification_conversation_focus: (_fn, args) => focusRowFromArgs(args),
|
||
});
|
||
const occupationPersisted = await persistServerOwnedFocus({
|
||
accounting: accounting.client,
|
||
userId: USER_ID,
|
||
caseId: CASE_ID,
|
||
activeFocus: null,
|
||
decisionReceipt: null,
|
||
followup: occupation,
|
||
});
|
||
const healthPersisted = await persistServerOwnedFocus({
|
||
accounting: accounting.client,
|
||
userId: USER_ID,
|
||
caseId: CASE_ID,
|
||
activeFocus: null,
|
||
decisionReceipt: null,
|
||
followup: health,
|
||
});
|
||
const occupationWrite = accounting.calls.find((item) => (
|
||
item.fn === "set_agentic_rectification_conversation_focus"
|
||
&& String(item.args.p_question_id).startsWith("collect:occupation:")
|
||
));
|
||
const healthWrite = accounting.calls.find((item) => (
|
||
item.fn === "set_agentic_rectification_conversation_focus"
|
||
&& String(item.args.p_question_id).startsWith("collect:health_pressure:")
|
||
));
|
||
assert.equal(occupationPersisted.status, "created");
|
||
assert.equal(healthPersisted.status, "created");
|
||
assert.equal(occupationWrite?.args.p_target_domain, "other");
|
||
assert.equal(healthWrite?.args.p_target_domain, "health");
|
||
});
|
||
|
||
test("collect focus unique conflict retries with a :next question id", async () => {
|
||
const followup = collectFollowup();
|
||
let writes = 0;
|
||
const accounting = fakeAccounting({
|
||
set_agentic_rectification_conversation_focus: (_fn, args) => {
|
||
writes += 1;
|
||
if (writes === 1) throw new Error("agentic_rectification_focus_idempotency_conflict");
|
||
return focusRowFromArgs(args);
|
||
},
|
||
});
|
||
const persisted = await persistServerOwnedFocus({
|
||
accounting: accounting.client,
|
||
userId: USER_ID,
|
||
caseId: CASE_ID,
|
||
activeFocus: null,
|
||
decisionReceipt: null,
|
||
followup,
|
||
});
|
||
assert.equal(persisted.status, "created");
|
||
assert.equal(writes, 2);
|
||
assert.equal(
|
||
persisted.questionId,
|
||
`collect:relationship:collect_method_evidence:${COLLECT_FOCUS_RETRY_SUFFIX}`,
|
||
);
|
||
const retryWrite = accounting.calls.at(-1);
|
||
assert.equal(
|
||
retryWrite?.args.p_question_id,
|
||
`collect:relationship:collect_method_evidence:${COLLECT_FOCUS_RETRY_SUFFIX}`,
|
||
);
|
||
});
|