433 lines
14 KiB
TypeScript
433 lines
14 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 {
|
|
openQuestionFromPersistedFocus,
|
|
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 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 = "升学结果或学习环境出现明显变化",
|
|
): MethodFollowup {
|
|
const valid = discriminatorFollowup();
|
|
return {
|
|
...valid,
|
|
choice_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: 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",
|
|
style_options: styleOptions,
|
|
}],
|
|
}),
|
|
};
|
|
}
|
|
|
|
async function assertInvalidChoiceSkipped(followup: MethodFollowup) {
|
|
const accounting = fakeAccounting({}, {
|
|
fallback: () => {
|
|
throw new Error("invalid choice frame must not call the database");
|
|
},
|
|
});
|
|
const result = await persistServerOwnedFocus({
|
|
accounting: accounting.client,
|
|
userId: USER_ID,
|
|
caseId: CASE_ID,
|
|
activeFocus: null,
|
|
decisionReceipt: null,
|
|
followup,
|
|
});
|
|
assert.equal(result.status, "invalid_choice_schema");
|
|
assert.equal(accounting.calls.length, 0);
|
|
}
|
|
|
|
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,
|
|
});
|
|
}
|
|
});
|
|
|
|
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);
|
|
assert.equal(openQuestionFromPersistedFocus({
|
|
status: "created",
|
|
focus: { ...focus, expectedAnswerSchema: { choice: { prompt: "不完整" } } },
|
|
questionId: focus.questionId,
|
|
prompt: "不能展示",
|
|
}), null);
|
|
});
|
|
|
|
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("missing, duplicate, incomplete, or illegal dynamic options skip focus persistence", async () => {
|
|
await assertInvalidChoiceSkipped(invalidStyleFollowup(undefined));
|
|
await assertInvalidChoiceSkipped(invalidStyleFollowup(DYNAMIC_STYLE_OPTIONS.slice(0, 3)));
|
|
await assertInvalidChoiceSkipped(invalidStyleFollowup([
|
|
...DYNAMIC_STYLE_OPTIONS,
|
|
{ label: "另一种明确发生", answer_class: "yes" },
|
|
]));
|
|
await assertInvalidChoiceSkipped(invalidStyleFollowup([
|
|
{ ...DYNAMIC_STYLE_OPTIONS[0], label: "08:12 左右发生" },
|
|
...DYNAMIC_STYLE_OPTIONS.slice(1),
|
|
]));
|
|
await assertInvalidChoiceSkipped(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/);
|
|
});
|