Files
Jyotisha/frontend/tests/rectification-server-focus.test.ts
T
Jesse_Chen d4627a5019
Independent Staging Quality Gate / validate (push) Successful in 14m31s
Independent Staging Quality Gate / publish (push) Successful in 1m59s
fix(rectification): dedupe post-adopt verify and skip this probe only
Adopted reverse-verify repeated already-asked collect stems, treated 「这题跳过」 as stopping the case, and promised holdout/OOS checks that never ran. Collect spoken stems no longer prefix a year.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-04 21:51:59 +08:00

850 lines
30 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
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,
exhaustionSpokenCollectFollowup,
spokenCollectFallbackFollowup,
spokenFollowupForUser,
type MethodFollowup,
} from "../src/lib/rectification-agentic/v9/method-followup.ts";
import {
GENERIC_COLLECT_QUESTION,
USER_COLLECT_QUESTION,
USER_COLLECT_QUESTION_RETRY,
} from "../src/lib/rectification-agentic/user-copy.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);
// 旧:prompt 含 2021 → 新:口语等于 USER_COLLECT_QUESTION.family
// 原因:决策 7,采集题不拼年份前缀;fixture 仍带 probe_year 供计分
assert.equal(result.focus?.expectedAnswerSchema.prompt, USER_COLLECT_QUESTION.family);
const openCollect = openQuestionFromPersistedFocus(result);
assert.equal(openCollect?.kind, "collect_spoken");
assert.equal(openCollect?.prompt, USER_COLLECT_QUESTION.family);
assert.doesNotMatch(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,后一度写成 education。
// 新:开场是「随便一件带时间的经历」,domain = other。
// 保留语义:仍是 collect_spoken,仍由服务端 persistAgent 不自拟开场题,id 不含 unknown。
assert.equal(followup.domain, "other");
assert.equal(followup.source, "method_coverage");
assert.doesNotMatch(stableFollowupQuestionId(followup), /unknown/);
assert.equal(stableFollowupQuestionId(followup), "collect:other:collect_method_evidence");
// 旧:开场 domain=other 后查表落到「也可以再说一件」→ 新:method_coverage + other 且非 retry 用 GENERIC_COLLECT_QUESTION → 保留 开场题由服务端出、带具体例子
assert.equal(spokenFollowupForUser(followup), GENERIC_COLLECT_QUESTION);
assert.doesNotMatch(spokenFollowupForUser(followup) ?? "", /^也可以再/);
assert.match(spokenFollowupForUser(followup) ?? "", /比如/);
const expected = GENERIC_COLLECT_QUESTION;
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,
});
assert.equal(persisted.focus?.targetDomain, "other");
});
test("exhaustion other fallback and other retry keep the after-evidence copy", () => {
const dated = (domain: string, year: string, extra: { eventKind?: string } = {}) => ({
status: "confirmed" as const,
domain,
datePrecision: extra.eventKind ? "unknown" as const : "year" as const,
occurredFrom: extra.eventKind ? null : `${year}-01-01`,
occurredTo: null,
...(extra.eventKind ? { eventKind: extra.eventKind } : {}),
});
const exhausted = exhaustionSpokenCollectFollowup({
evidence: [
dated("education", "2016"),
dated("career", "2020"),
dated("relationship", "2018"),
dated("finance", "2024"),
dated("relocation", "2022"),
dated("health_pressure", "2021"),
dated("occupation", "2020", { eventKind: "occupation_note" }),
],
declinedTopics: [{ target_domain: "family", status: "declined" }],
});
assert.equal(exhausted?.domain, "other");
assert.equal(exhausted?.source, "oos_blind");
assert.equal(spokenFollowupForUser(exhausted), USER_COLLECT_QUESTION.other);
const retry: MethodFollowup = {
method_id: "dasha_events",
intent: "collect_method_evidence",
ask_theme: "dated_event",
domain: "other",
kind_hint: null,
user_prompt_hint: "retry",
must_not_label: false,
choice_frame: null,
source: "method_coverage",
collect_retry: true,
};
assert.equal(spokenFollowupForUser(retry), USER_COLLECT_QUESTION_RETRY.other);
});
test("GENERIC_COLLECT_QUESTION keeps concrete opening examples", () => {
const source = readFileSync(new URL("../src/lib/rectification-agentic/user-copy.ts", import.meta.url), "utf8");
assert.match(GENERIC_COLLECT_QUESTION, /比如/);
assert.ok(
[...GENERIC_COLLECT_QUESTION.matchAll(/上的大学|换的工作|搬的家/g)].length >= 2,
GENERIC_COLLECT_QUESTION,
);
// 旧:「…开始就好——比如哪年上的大学、哪年换的工作…」→ 新:加「哪年搬的家」→ 保留 开场题必须带具体例子、由服务端出
assert.match(source, /GENERIC_COLLECT_QUESTION = "从你最容易想起来的一件事开始就好/);
});
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");
// Was: persistableFocusDomain("horary") === "horary".
// Now: null. DB check excludes horary; the old assertion locked a write that always failed.
assert.equal(persistableFocusDomain("horary"), null);
assert.equal(persistableFocusDomain("appearance"), null);
assert.equal(persistableFocusDomain("nakshatra"), null);
assert.equal(persistableFocusDomain(null), null);
assert.equal(persistableFocusDomain("unknown"), null);
// 旧 collect:education → 新 collect:other → 保留 unknown/null 不得写入 unknown
assert.equal(stableFollowupQuestionId(collectFollowup({ domain: "unknown" })), "collect:other:collect_method_evidence");
assert.equal(stableFollowupQuestionId(collectFollowup({ domain: null })), "collect:other: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}`,
);
});
test("appearance distinguish without a frame is skipped with zero RPC", async () => {
const followup: MethodFollowup = {
method_id: "nakshatra_boundary",
intent: "distinguish_candidates",
ask_theme: "nakshatra_trait",
domain: "appearance",
kind_hint: null,
user_prompt_hint: "外表或体质更接近哪一种?",
must_not_label: false,
choice_frame: null,
source: "nakshatra_boundary",
};
const accounting = fakeAccounting({
set_agentic_rectification_conversation_focus: () => {
throw new Error("appearance collect must not be written");
},
});
const persisted = await persistServerOwnedFocus({
accounting: accounting.client,
userId: USER_ID,
caseId: CASE_ID,
activeFocus: null,
decisionReceipt: null,
followup,
});
assert.equal(persisted.status, "skipped");
assert.equal(
accounting.calls.some((item) => item.fn === "set_agentic_rectification_conversation_focus"),
false,
);
});