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:
@@ -101,6 +101,7 @@ test("answer deltas preserve a still-running server activity", () => {
|
||||
);
|
||||
assert.match(deltaBranch, /state: raw\.trim\(\) \? "streaming" : "thinking"/);
|
||||
assert.match(deltaBranch, /text: raw,/);
|
||||
assert.match(deltaBranch, /event\.replace === true \? event\.text : raw \+ event\.text/);
|
||||
assert.doesNotMatch(deltaBranch, /settled\.spoken/);
|
||||
assert.match(deltaBranch, /正在组织回答/);
|
||||
assert.doesNotMatch(deltaBranch, /activeActivity:\s*undefined/);
|
||||
|
||||
@@ -250,7 +250,10 @@ test("usage completes or releases without hiding settlement failures", () => {
|
||||
assert.match(run, /activity.changed/);
|
||||
assert.match(run, /if \(!answerText\.trim\(\)\) \{[\s\S]*composeRectificationTurnNarration/);
|
||||
assert.match(run, /bindSpokenToOpenQuestion/);
|
||||
assert.match(run, /CHOICE_CARD_CONTINUATION_ACK/);
|
||||
assert.match(run, /openQuestionPromptFromToolResult/);
|
||||
assert.doesNotMatch(run, /heldSpoken/);
|
||||
assert.match(run, /replace: true/);
|
||||
const narration = readFileSync(
|
||||
new URL("../src/lib/rectification-agentic/v9/turn-narration.ts", import.meta.url),
|
||||
"utf8",
|
||||
@@ -293,9 +296,9 @@ test("Agentic rectification follows the conversation tail only while the reader
|
||||
assert.match(chat, /choiceCardsOpen/);
|
||||
assert.match(
|
||||
chat,
|
||||
/useLayoutEffect\(\(\) => \{\s*choiceCardsOpen\.current = showChoiceCards;\s*updateFollowState\(\);\s*\}, \[showChoiceCards, updateFollowState\]\)/,
|
||||
/useLayoutEffect\(\(\) => \{\s*choiceCardsOpen\.current = showLiveChoiceCard;\s*updateFollowState\(\);\s*\}, \[showLiveChoiceCard, updateFollowState\]\)/,
|
||||
);
|
||||
assert.doesNotMatch(chat, /choiceCardsOpen\.current = showChoiceCards;\s*useLayoutEffect/);
|
||||
assert.doesNotMatch(chat, /choiceCardsOpen\.current = showLiveChoiceCard;\s*useLayoutEffect/);
|
||||
assert.match(chat, /updateFollowState/);
|
||||
assert.match(chat, /\}, \[busy, candidateResult, choiceCard, error, messages, savedTime, followLatestContent\]\);/);
|
||||
const jumpLatestRule = styles.match(/\.rectification-jump-latest \{[^}]+\}/)?.[0] ?? "";
|
||||
@@ -539,26 +542,31 @@ test("time-selection cards appear under the latest settled agent bubble only aft
|
||||
assert.match(chat, /candidateResult\?\.selectionAllowed/);
|
||||
assert.match(chat, /turnOfferedSelection/);
|
||||
assert.match(chat, /rectification-offer-candidates/);
|
||||
assert.match(chat, /showChoiceCards = Boolean\(\s*choiceCard[\s\S]*!offeredThisTurn[\s\S]*!busy/);
|
||||
assert.match(chat, /showSelectionCards = Boolean\(\s*candidateResult\?\.selectionAllowed[\s\S]*offeredSelectionOnce[\s\S]*!showChoiceCards[\s\S]*!busy/);
|
||||
assert.match(chat, /showLiveChoiceCard = Boolean\(\s*choiceCard[\s\S]*answeredQuestionIds[\s\S]*!busy/);
|
||||
assert.match(chat, /showSelectionCards = Boolean\(\s*candidateResult\?\.selectionAllowed[\s\S]*offeredSelectionOnce[\s\S]*!showLiveChoiceCard[\s\S]*!busy/);
|
||||
assert.match(
|
||||
chat,
|
||||
/selectionCardMessageKey = showSelectionCards && latestSettledAssistant\s*\? latestSettledAssistant\.renderKey\s*: undefined/,
|
||||
);
|
||||
assert.match(messageLoop, /showSelectionCards && message\.renderKey === selectionCardMessageKey/);
|
||||
assert.match(messageLoop, /<RectificationChoiceCard/);
|
||||
assert.match(messageLoop, /key=\{`\$\{choiceCard\.question_id\}:\$\{choiceNonce\}`\}/);
|
||||
assert.match(messageLoop, /choiceAttachment/);
|
||||
assert.match(messageLoop, /choiceNonce/);
|
||||
assert.match(messageLoop, /answered:\$\{settledChoice\.selectedKey\}/);
|
||||
assert.doesNotMatch(choiceCardComponent, /setSelectedKey\(""\)/);
|
||||
assert.match(chat, /showChoiceCards = Boolean\(/);
|
||||
assert.match(chat, /choiceCardUserMessage/);
|
||||
assert.match(choiceCardComponent, /selectedKey/);
|
||||
assert.match(choiceCardComponent, /is-answered/);
|
||||
assert.match(chat, /showLiveChoiceCard = Boolean\(/);
|
||||
assert.match(chat, /isStructuredChoiceUserText/);
|
||||
assert.match(chat, /submitStructuredChoice\(CHOICE_ACTION/);
|
||||
assert.match(chat, /submitStructuredChoice\(STOP_ACTION/);
|
||||
assert.match(route, /answer_choice/);
|
||||
assert.match(route, /stop_and_review/);
|
||||
assert.match(route, /applyRectificationChoice\(accounting/);
|
||||
assert.doesNotMatch(chat, /v9-choice-user-/);
|
||||
assert.doesNotMatch(chat, /send\("message", choiceCardUserMessage/);
|
||||
assert.doesNotMatch(chat, /send\("message", choiceCard\?\.stop_message/);
|
||||
assert.match(chat, /CHOICE_STOP_MESSAGE/);
|
||||
assert.doesNotMatch(chat, /choiceCardUserMessage/);
|
||||
assert.match(caseRoute, /choice_card: choiceCardFromCaseDossier/);
|
||||
assert.match(caseRoute, /overlayPublicDecision/);
|
||||
assert.match(caseRoute, /interview: publicDecisionFields/);
|
||||
@@ -580,7 +588,7 @@ test("does not parse suggestions from an incomplete run", () => {
|
||||
});
|
||||
|
||||
test("live answer.delta is the model reply, not a spoken-thinking split", () => {
|
||||
assert.match(chat, /raw \+= event\.text/);
|
||||
assert.match(chat, /raw = event\.replace === true \? event\.text : raw \+ event\.text/);
|
||||
assert.match(chat, /text: raw,/);
|
||||
assert.doesNotMatch(chat, /settleRectificationSpokenAndThinking/);
|
||||
assert.doesNotMatch(chat, /text: settled\.spoken/);
|
||||
@@ -672,13 +680,20 @@ test("choice cards render the persisted prompt as the visible question stem", ()
|
||||
assert.doesNotMatch(choiceCardComponent, /<legend className="sr-only">\{props\.card\.prompt\}<\/legend>/);
|
||||
});
|
||||
|
||||
test("choice card answers and stop share one stacked primary list", () => {
|
||||
assert.match(choiceCardComponent, /birth-time-primary-choices/);
|
||||
assert.match(choiceCardComponent, /props\.card\.options\.map/);
|
||||
assert.doesNotMatch(choiceCardComponent, /birth-time-special-choices/);
|
||||
assert.doesNotMatch(choiceCardComponent, /is-secondary/);
|
||||
});
|
||||
|
||||
test("adopted time offers a consultation handoff without unique-minute copy", () => {
|
||||
assert.match(chat, /用这个时间看盘/);
|
||||
assert.match(chat, /onStartConsultation/);
|
||||
assert.match(board, /换升时刻/);
|
||||
assert.match(board, /经历与大运对照/);
|
||||
assert.match(chat, /<RectificationChoiceCard/);
|
||||
assert.match(chat, /choiceCardUserMessage/);
|
||||
assert.match(chat, /choiceAttachment/);
|
||||
assert.match(page, /startConsultationAfterRectification/);
|
||||
assert.match(page, /createSession\(modelCatalog\.defaultModelId\)/);
|
||||
assert.match(agent, /start_consultation/);
|
||||
|
||||
@@ -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/);
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
HOLDOUT_MESSAGE_PREFIX,
|
||||
isHoldoutVerificationQuote,
|
||||
lifePeriodLabel,
|
||||
preferConcreteChoicePrompt,
|
||||
mergeChoiceCard,
|
||||
parseAgentChoiceCopy,
|
||||
parseRectificationChoiceCard,
|
||||
@@ -14,6 +15,7 @@ import {
|
||||
} from "../src/lib/rectification-agentic/v9/choice-card.ts";
|
||||
import { buildMethodFollowupPlan, projectRectificationChoiceCard } from "../src/lib/rectification-agentic/v9/method-followup.ts";
|
||||
import { choiceCardFromCaseDossier } from "../src/lib/rectification-agentic/v9/interview-state.ts";
|
||||
import { QUALITY_STYLE_OPTIONS } from "../src/lib/rectification-agentic/v9/probe-question-contract.ts";
|
||||
import type { DiscriminatingEventProbe } from "../src/lib/rectification-agentic/v9/refinement-packet.ts";
|
||||
|
||||
const DYNAMIC_STYLE_OPTIONS = [
|
||||
@@ -288,6 +290,12 @@ test("life period only reuses years from the same domain", () => {
|
||||
assert.equal(lifePeriodLabel([
|
||||
{ status: "confirmed", domain: "education", datePrecision: "year", occurredFrom: "2016-01-01", occurredTo: null },
|
||||
], "relocation"), "那段时间");
|
||||
assert.equal(lifePeriodLabel([
|
||||
{ status: "confirmed", domain: "career", datePrecision: "month", occurredFrom: "2020-04-01", occurredTo: null },
|
||||
], "career"), "2020 年 4 月前后");
|
||||
assert.equal(lifePeriodLabel([
|
||||
{ status: "confirmed", domain: "relationship", datePrecision: "day", occurredFrom: "2024-08-08", occurredTo: null },
|
||||
], "relationship"), "2024 年 8 月前后");
|
||||
});
|
||||
|
||||
test("choice frame requires a real non-empty event family", () => {
|
||||
@@ -306,6 +314,124 @@ test("choice frame requires a real non-empty event family", () => {
|
||||
}, { probes: [{ ...MOVE_PROBE, event_family: " " }] }), null);
|
||||
});
|
||||
|
||||
test("yearless D24 quality cards do not borrow a recorded education year", () => {
|
||||
const frame = buildChoiceFrame({
|
||||
method_id: "d5_education",
|
||||
ask_theme: "education_style",
|
||||
domain: "education",
|
||||
user_prompt_hint: "unused",
|
||||
choice_kind: "event_quality",
|
||||
semantic_key: "varga.d24.05:00/05:07",
|
||||
style_options: QUALITY_STYLE_OPTIONS,
|
||||
}, {
|
||||
evidence: [{
|
||||
status: "confirmed",
|
||||
domain: "education",
|
||||
datePrecision: "year",
|
||||
occurredFrom: "2016-01-01",
|
||||
occurredTo: null,
|
||||
}],
|
||||
probes: [{
|
||||
...MOVE_PROBE,
|
||||
year: 0,
|
||||
year_label: "当前这几个候选",
|
||||
domain: "education",
|
||||
event_family: "学业或考试发挥失常、压力特别大",
|
||||
choice_kind: "event_quality",
|
||||
semantic_key: "varga.d24.05:00/05:07",
|
||||
style_options: QUALITY_STYLE_OPTIONS,
|
||||
}],
|
||||
});
|
||||
assert.equal(frame, null);
|
||||
});
|
||||
|
||||
test("yearless family cards do not attach a scoring frame", () => {
|
||||
const frame = buildChoiceFrame({
|
||||
method_id: "relatives",
|
||||
ask_theme: "family_event",
|
||||
domain: "family",
|
||||
user_prompt_hint: "unused",
|
||||
choice_kind: "existence",
|
||||
}, {
|
||||
evidence: [{
|
||||
status: "confirmed",
|
||||
domain: "education",
|
||||
datePrecision: "year",
|
||||
occurredFrom: "2016-01-01",
|
||||
occurredTo: null,
|
||||
}],
|
||||
probes: [{
|
||||
...MOVE_PROBE,
|
||||
year: 0,
|
||||
year_label: "当前这几个候选",
|
||||
domain: "family",
|
||||
event_family: "家人结婚、添丁或住院",
|
||||
choice_kind: "existence",
|
||||
semantic_key: "varga.d12.05:00/05:07",
|
||||
}],
|
||||
});
|
||||
assert.equal(frame, null);
|
||||
});
|
||||
|
||||
test("yearless family cards do not borrow a recorded family year", () => {
|
||||
const frame = buildChoiceFrame({
|
||||
method_id: "relatives",
|
||||
ask_theme: "family_event",
|
||||
domain: "family",
|
||||
user_prompt_hint: "unused",
|
||||
choice_kind: "existence",
|
||||
}, {
|
||||
evidence: [{
|
||||
status: "confirmed",
|
||||
domain: "family",
|
||||
datePrecision: "year",
|
||||
occurredFrom: "2018-01-01",
|
||||
occurredTo: null,
|
||||
}],
|
||||
probes: [{
|
||||
...MOVE_PROBE,
|
||||
year: 0,
|
||||
year_label: "当前这几个候选",
|
||||
domain: "family",
|
||||
event_family: "家人结婚、添丁或住院",
|
||||
choice_kind: "existence",
|
||||
semantic_key: "varga.d12.05:00/05:07",
|
||||
}],
|
||||
});
|
||||
assert.equal(frame, null);
|
||||
});
|
||||
|
||||
test("GET upgrades a persisted placeholder stem to the recorded year lock", () => {
|
||||
assert.equal(
|
||||
preferConcreteChoicePrompt(
|
||||
"2016 年前后,有没有学业或考试发挥失常、压力特别大的时候?",
|
||||
"当前这几个候选 · 学业、考试发挥或学习压力出现明显变化",
|
||||
),
|
||||
"2016 年前后,有没有学业或考试发挥失常、压力特别大的时候?",
|
||||
);
|
||||
assert.equal(
|
||||
preferConcreteChoicePrompt(
|
||||
"2016 年前后,有没有学业或考试发挥失常、压力特别大的时候?",
|
||||
"2016 年前后 · 学业、考试发挥或学习压力出现明显变化",
|
||||
),
|
||||
"2016 年前后,有没有学业或考试发挥失常、压力特别大的时候?",
|
||||
);
|
||||
assert.equal(
|
||||
preferConcreteChoicePrompt(
|
||||
"2018 年前后,有没有开始一段认真关系、分手或结婚?",
|
||||
"2018 年前后,有没有认真关系进入、结束或关系观明显转变?",
|
||||
),
|
||||
"2018 年前后,有没有开始一段认真关系、分手或结婚?",
|
||||
);
|
||||
assert.equal(
|
||||
preferConcreteChoicePrompt(
|
||||
"2018 年前后,有没有家人结婚、添丁或住院?",
|
||||
"那段时间,有没有家人相关的明显变化?",
|
||||
),
|
||||
"2018 年前后,有没有家人结婚、添丁或住院?",
|
||||
);
|
||||
});
|
||||
|
||||
test("choice frame completes existence options and rejects illegal or non-renderable varga styles", () => {
|
||||
const build = (style_options: DiscriminatingEventProbe["style_options"], extra: Partial<DiscriminatingEventProbe> = {}) => buildChoiceFrame({
|
||||
method_id: "d4_home",
|
||||
@@ -891,6 +1017,175 @@ test("GET choice_card stays after coverage when remaining minutes still split on
|
||||
assert.equal(card.prompt, SAMPLE_COPY.prompt);
|
||||
});
|
||||
|
||||
test("GET shows an engine-dated career dasha instead of locking D24 to the recorded education year", () => {
|
||||
const careerKey = "career.2012.11.dasha_boundary";
|
||||
const careerCopy = {
|
||||
prompt: "2012 年 11 月前后,有没有入职、升职或职责明显加重?",
|
||||
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: DYNAMIC_STYLE_OPTIONS.map((option, index) => ({
|
||||
key: (["A", "B", "C", "D"] as const)[index]!,
|
||||
label: option.label,
|
||||
answer_class: option.answer_class,
|
||||
})),
|
||||
};
|
||||
const inferenceCandidates = [
|
||||
{ id: "04:47", time: "04:47", cluster_range: ["04:47", "04:47"] as const, prior_score: 5, posterior_score: 5, probability: 0.05, status: "active", rank: 6, strong_conflict_count: 0 },
|
||||
{ id: "05:00", time: "05:00", cluster_range: ["05:00", "05:00"] as const, prior_score: 21, posterior_score: 21, probability: 0.21, status: "active", rank: 1, strong_conflict_count: 0 },
|
||||
{ id: "05:07", time: "05:07", cluster_range: ["05:07", "05:07"] as const, prior_score: 18, posterior_score: 18, probability: 0.18, status: "active", rank: 2, strong_conflict_count: 0 },
|
||||
{ id: "05:12", time: "05:12", cluster_range: ["05:12", "05:12"] as const, prior_score: 18, posterior_score: 18, probability: 0.18, status: "active", rank: 3, strong_conflict_count: 0 },
|
||||
{ id: "05:14", time: "05:14", cluster_range: ["05:14", "05:14"] as const, prior_score: 17, posterior_score: 17, probability: 0.17, status: "active", rank: 4, strong_conflict_count: 0 },
|
||||
{ id: "05:15", time: "05:15", cluster_range: ["05:15", "05:15"] as const, prior_score: 3, posterior_score: 3, probability: 0.03, status: "active", rank: 9, strong_conflict_count: 0 },
|
||||
];
|
||||
const candidateSetId = "04:45-05:15:04:47,05:00,05:07,05:12,05:14,05:15";
|
||||
const d24Key = "varga.d24.04:47/04:51|04:53/04:59/05:00/05:07|05:12/05:14|05:15";
|
||||
const d24Split = `${candidateSetId}:${d24Key}`;
|
||||
const d24Outcomes = [
|
||||
{ answer_class: "yes", supports: ["04:47"], conflicts: ["05:00", "05:07", "05:12", "05:14", "05:15"] },
|
||||
{ answer_class: "weak_yes", supports: ["05:00", "05:07"], conflicts: ["04:47", "05:12", "05:14", "05:15"] },
|
||||
{ answer_class: "no", supports: ["05:12", "05:14"], conflicts: ["04:47", "05:00", "05:07", "05:15"] },
|
||||
{ answer_class: "unsure", supports: [], conflicts: [] },
|
||||
];
|
||||
const card = choiceCardFromCaseDossier({
|
||||
evidence: [{
|
||||
status: "confirmed",
|
||||
domain: "education",
|
||||
datePrecision: "year",
|
||||
occurredFrom: "2016-01-01",
|
||||
occurredTo: "2016-01-01",
|
||||
eventKind: "education_start",
|
||||
}, {
|
||||
status: "confirmed",
|
||||
domain: "career",
|
||||
datePrecision: "month",
|
||||
occurredFrom: "2020-04-01",
|
||||
occurredTo: "2020-04-01",
|
||||
eventKind: "career_entry",
|
||||
}, {
|
||||
status: "confirmed",
|
||||
domain: "career",
|
||||
datePrecision: "month",
|
||||
occurredFrom: "2020-10-01",
|
||||
occurredTo: "2020-10-01",
|
||||
eventKind: "career_exit",
|
||||
}, {
|
||||
status: "confirmed",
|
||||
domain: "career",
|
||||
datePrecision: "day",
|
||||
occurredFrom: "2024-04-07",
|
||||
occurredTo: null,
|
||||
eventKind: "career_entry",
|
||||
}, {
|
||||
status: "confirmed",
|
||||
domain: "relationship",
|
||||
datePrecision: "month",
|
||||
occurredFrom: "2024-05-01",
|
||||
occurredTo: null,
|
||||
eventKind: "relationship_start",
|
||||
}, {
|
||||
status: "confirmed",
|
||||
domain: "relationship",
|
||||
datePrecision: "day",
|
||||
occurredFrom: "2024-08-08",
|
||||
occurredTo: null,
|
||||
eventKind: "relationship_end",
|
||||
}],
|
||||
conversationSummary: {
|
||||
activeFocus: {
|
||||
id: FOCUS_ID,
|
||||
questionId: `probe:${careerKey}`,
|
||||
intent: "distinguish_candidates",
|
||||
targetDomain: "career",
|
||||
targetKind: null,
|
||||
expectedAnswerSchema: {
|
||||
choice: careerCopy,
|
||||
probe_id: `event:${careerKey}`,
|
||||
semantic_key: careerKey,
|
||||
candidate_split_hash: "career.2012.11",
|
||||
choice_kind: "existence",
|
||||
},
|
||||
},
|
||||
declinedSkippedTopics: [],
|
||||
},
|
||||
latestResult: {
|
||||
resultId: "result-d24-open",
|
||||
selectionAllowed: true,
|
||||
confirmationAllowed: false,
|
||||
candidates: inferenceCandidates.map((item) => ({
|
||||
time: item.time,
|
||||
rank: item.rank,
|
||||
relativeSupport: item.posterior_score,
|
||||
})),
|
||||
decisionReceipt: {
|
||||
discriminating_event_probes: [{
|
||||
year: 2012,
|
||||
year_label: "2012 年 11 月前后",
|
||||
domain: "career",
|
||||
event_family: "入职、升职或职责明显加重",
|
||||
source: "dasha_boundary",
|
||||
tracks: ["vimshottari", "narayana"],
|
||||
tracks_agree: true,
|
||||
unique_minute_claim: false,
|
||||
user_meaning: "时间范围锁定 2012 年 11 月前后。",
|
||||
role: "distinguish",
|
||||
phase: "candidate_discriminator",
|
||||
information_gain: 1.3356,
|
||||
semantic_key: careerKey,
|
||||
candidate_split_hash: "career.2012.11",
|
||||
candidate_ids: ["04:47", "05:00", "05:15"],
|
||||
expected_outcomes: [
|
||||
{ answer_class: "yes", supports: ["04:47"], conflicts: ["05:00", "05:15"] },
|
||||
{ answer_class: "weak_yes", supports: ["04:47"], conflicts: ["05:00", "05:15"] },
|
||||
{ answer_class: "no", supports: ["05:00", "05:15"], conflicts: ["04:47"] },
|
||||
{ answer_class: "unsure", supports: [], conflicts: [] },
|
||||
],
|
||||
style_options: DYNAMIC_STYLE_OPTIONS,
|
||||
}],
|
||||
inference_state: {
|
||||
algorithm_version: "rectification-inference-v1",
|
||||
candidate_set_id: candidateSetId,
|
||||
revision: 1,
|
||||
phase: "discrimination",
|
||||
result_status: "discriminating",
|
||||
range_start: "04:45",
|
||||
range_end: "05:15",
|
||||
candidates: inferenceCandidates,
|
||||
events: [],
|
||||
probes: [{
|
||||
id: `contrast:${d24Key}`,
|
||||
year: 0,
|
||||
domain: "education",
|
||||
source: "varga_contrast",
|
||||
question: "引擎给出的区分机会绑定 D24。",
|
||||
semantic_key: d24Key,
|
||||
candidate_ids: inferenceCandidates.map((item) => item.time),
|
||||
information_gain: 2.503258334775646,
|
||||
expected_outcomes: d24Outcomes,
|
||||
candidate_split_hash: d24Split,
|
||||
}],
|
||||
answered_probes: [],
|
||||
rounds: [],
|
||||
entropy: 2.0,
|
||||
representative_time: "05:00",
|
||||
credible_range: ["05:00", "05:14"],
|
||||
},
|
||||
},
|
||||
},
|
||||
case: { acceptedTime: null, status: "collecting_evidence" },
|
||||
turns: [],
|
||||
});
|
||||
assert.ok(card);
|
||||
assert.equal(card.focus_id, FOCUS_ID);
|
||||
assert.match(card.prompt, /2012 年 11 月前后,有没有入职、升职或职责明显加重?/);
|
||||
assert.doesNotMatch(card.prompt, /2016 年前后/);
|
||||
assert.doesNotMatch(card.prompt, /当前这几个候选/);
|
||||
assert.doesNotMatch(card.prompt, / · /);
|
||||
assert.equal(card.options.length, 4);
|
||||
assert.equal(card.options.every((option) => option.role === "primary"), true);
|
||||
});
|
||||
|
||||
test("GET choice_card stays hidden without a persisted focus after 没有了", () => {
|
||||
const card = choiceCardFromCaseDossier({
|
||||
evidence: [{
|
||||
@@ -1058,8 +1353,8 @@ test("career event_quality uses the supplied dynamic labels", () => {
|
||||
assert.ok(copy);
|
||||
assert.match(copy.prompt, /2020 年前后/);
|
||||
assert.match(copy.prompt, /入职、升职或职责明显加重/);
|
||||
assert.doesNotMatch(copy.prompt, /有没有/);
|
||||
assert.match(copy.prompt, /·/);
|
||||
assert.match(copy.prompt, /有没有/);
|
||||
assert.doesNotMatch(copy.prompt, / · /);
|
||||
assert.equal(copy.option_a, DYNAMIC_STYLE_OPTIONS[0].label);
|
||||
assert.doesNotMatch(copy.option_a, /失常/);
|
||||
});
|
||||
|
||||
@@ -336,31 +336,36 @@ test("scored inference catalog outranks a low-gain Python career probe when snap
|
||||
|
||||
const plan = buildMethodFollowupPlan({
|
||||
evidence,
|
||||
eventProbes: packet.probes.flatMap((probe) => probe.semanticKey.startsWith("career.")
|
||||
? [{
|
||||
year: 2023,
|
||||
year_label: "2023 年前后",
|
||||
domain: "career",
|
||||
event_family: "入职、升职或职责明显加重",
|
||||
source: "dasha_activation",
|
||||
tracks: ["vimshottari", "narayana"],
|
||||
tracks_agree: false,
|
||||
unique_minute_claim: false,
|
||||
user_meaning: "时间范围锁定 2023 年前后。",
|
||||
role: "distinguish",
|
||||
phase: "candidate_discriminator",
|
||||
information_gain: 0.56,
|
||||
semantic_key: "career.2023.dasha_activation",
|
||||
candidate_split_hash: "500ce694938305201fbab9ba",
|
||||
candidate_ids: ["04:45", "05:00", "05:14", "05:15"],
|
||||
expected_outcomes: careerOutcomes,
|
||||
}]
|
||||
: []),
|
||||
eventProbes: [{
|
||||
year: 2023,
|
||||
year_label: "2023 年前后",
|
||||
domain: "career",
|
||||
event_family: "入职、升职或职责明显加重",
|
||||
source: "dasha_activation",
|
||||
tracks: ["vimshottari", "narayana"],
|
||||
tracks_agree: false,
|
||||
unique_minute_claim: false,
|
||||
user_meaning: "时间范围锁定 2023 年前后。",
|
||||
role: "distinguish",
|
||||
phase: "candidate_discriminator",
|
||||
information_gain: 0.56,
|
||||
semantic_key: "career.2023.dasha_activation",
|
||||
candidate_split_hash: "500ce694938305201fbab9ba",
|
||||
candidate_ids: ["04:45", "05:00", "05:14", "05:15"],
|
||||
expected_outcomes: careerOutcomes,
|
||||
style_options: [
|
||||
{ label: "明确发生且时间吻合", answer_class: "yes" },
|
||||
{ label: "发生过但程度较弱", answer_class: "weak_yes" },
|
||||
{ label: "明确没有发生", answer_class: "no" },
|
||||
{ label: "这段记不清楚", answer_class: "unsure" },
|
||||
],
|
||||
}],
|
||||
contrastPacket: packet,
|
||||
candidatesSeparated: false,
|
||||
});
|
||||
assert.match(plan.next_followup?.semantic_key ?? "", /^varga\.d24\./);
|
||||
assert.doesNotMatch(plan.next_followup?.semantic_key ?? "", /career\.2023/);
|
||||
assert.equal(plan.next_followup?.semantic_key, "career.2023.dasha_activation");
|
||||
assert.match(plan.next_followup?.choice_frame?.period ?? "", /2023 年前后/);
|
||||
assert.doesNotMatch(plan.next_followup?.semantic_key ?? "", /varga\.d24/);
|
||||
});
|
||||
|
||||
test("career and relationship training still discriminates before family or occupation coverage", () => {
|
||||
@@ -376,11 +381,41 @@ test("career and relationship training still discriminates before family or occu
|
||||
{ id: "05:12", time: "05:12", cluster_range: ["05:12", "05:12"], prior_score: 17, posterior_score: 17, probability: 0.21, status: "active", rank: 3, strong_conflict_count: 0 },
|
||||
];
|
||||
const evidence = [
|
||||
{ id: "edu-2016", status: "confirmed", domain: "education", datePrecision: "year", occurredFrom: "2016-01-01", occurredTo: null, eventKind: "education_milestone" },
|
||||
{ id: "career-exit", status: "confirmed", domain: "career", datePrecision: "month", occurredFrom: "2020-10-01", occurredTo: null, eventKind: "career_exit" },
|
||||
{ id: "career-entry", status: "confirmed", domain: "career", datePrecision: "month", occurredFrom: "2020-04-01", occurredTo: null, eventKind: "career_entry" },
|
||||
{ id: "rel-start", status: "confirmed", domain: "relationship", datePrecision: "month", occurredFrom: "2024-05-01", occurredTo: null, eventKind: "relationship_start" },
|
||||
{ id: "rel-end", status: "confirmed", domain: "relationship", datePrecision: "day", occurredFrom: "2024-08-08", occurredTo: null, eventKind: "relationship_end" },
|
||||
];
|
||||
const familyProbe = {
|
||||
year: 2013,
|
||||
year_label: "2013 年 3 月前后",
|
||||
domain: "family",
|
||||
event_family: "家人结婚、添丁或住院",
|
||||
source: "dasha_boundary",
|
||||
tracks: ["vimshottari", "narayana"],
|
||||
tracks_agree: true,
|
||||
unique_minute_claim: false,
|
||||
user_meaning: "时间范围锁定 2013 年 3 月前后。",
|
||||
role: "distinguish",
|
||||
phase: "candidate_discriminator",
|
||||
information_gain: 1.2,
|
||||
semantic_key: "family.2013.03.dasha_boundary",
|
||||
candidate_split_hash: "family.2013.03",
|
||||
candidate_ids: ["05:00", "05:07", "05:12"],
|
||||
expected_outcomes: [
|
||||
{ answer_class: "yes", supports: ["05:00"], conflicts: ["05:07", "05:12"] },
|
||||
{ answer_class: "weak_yes", supports: ["05:07"], conflicts: ["05:00", "05:12"] },
|
||||
{ answer_class: "no", supports: ["05:12"], conflicts: ["05:00", "05:07"] },
|
||||
{ answer_class: "unsure", supports: [], conflicts: [] },
|
||||
],
|
||||
style_options: [
|
||||
{ label: "明确发生且时间吻合", answer_class: "yes" },
|
||||
{ label: "发生过但程度较弱", answer_class: "weak_yes" },
|
||||
{ label: "明确没有发生", answer_class: "no" },
|
||||
{ label: "这段记不清楚", answer_class: "unsure" },
|
||||
],
|
||||
};
|
||||
const dossier = {
|
||||
evidence,
|
||||
conversationSummary: { activeFocus: null, declinedSkippedTopics: [] },
|
||||
@@ -388,6 +423,7 @@ test("career and relationship training still discriminates before family or occu
|
||||
resultId: "result-unseparated",
|
||||
candidates: [],
|
||||
decisionReceipt: {
|
||||
discriminating_event_probes: [familyProbe],
|
||||
inference_state: {
|
||||
algorithm_version: "rectification-inference-v1",
|
||||
candidate_set_id: "05:00-05:12:05:00,05:07,05:12",
|
||||
@@ -398,6 +434,7 @@ test("career and relationship training still discriminates before family or occu
|
||||
range_end: "05:12",
|
||||
candidates: inferenceCandidates,
|
||||
events: [
|
||||
{ id: "edu-2016", year: 2016, usage: "training", domain: "education", precision: "year" },
|
||||
{ id: "career-exit", year: 2020, usage: "training", domain: "career", precision: "month" },
|
||||
{ id: "career-entry", year: 2020, usage: "holdout", domain: "career", precision: "month" },
|
||||
{ id: "rel-start", year: 2024, usage: "training", domain: "relationship", precision: "month" },
|
||||
@@ -440,12 +477,15 @@ test("career and relationship training still discriminates before family or occu
|
||||
const packet = contrastPacketFromDossier(dossier);
|
||||
const plan = buildMethodFollowupPlan({
|
||||
evidence,
|
||||
eventProbes: [familyProbe],
|
||||
contrastPacket: packet,
|
||||
sessionOutcome: decision.sessionOutcome,
|
||||
});
|
||||
assert.equal(plan.next_followup?.intent, "distinguish_candidates");
|
||||
assert.match(plan.next_followup?.semantic_key ?? "", /^varga\.d24/);
|
||||
assert.equal(plan.next_followup?.semantic_key, "family.2013.03.dasha_boundary");
|
||||
assert.ok(plan.next_followup?.choice_frame);
|
||||
assert.match(plan.next_followup?.choice_frame?.period ?? "", /2013 年 3 月前后/);
|
||||
assert.doesNotMatch(plan.next_followup?.choice_frame?.prompt ?? "", /2016 年前后/);
|
||||
assert.equal(conversationalSessionOutcome({
|
||||
selectionAllowed: false,
|
||||
proposeAllowed: false,
|
||||
|
||||
@@ -2,7 +2,7 @@ import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import test from "node:test";
|
||||
|
||||
import { buildMethodFollowupPlan, buildNextUserAction, conversationalSessionOutcome, isOfferBlockingFollowup } from "../src/lib/rectification-agentic/v9/method-followup.ts";
|
||||
import { buildMethodFollowupPlan, buildNextUserAction, conversationalSessionOutcome, isOfferBlockingFollowup, spokenFollowupForUser } from "../src/lib/rectification-agentic/v9/method-followup.ts";
|
||||
import { trainingScoreableGate } from "../src/lib/rectification-agentic/v9/evidence-model.ts";
|
||||
import {
|
||||
askedKeysFromLedgerEvidence,
|
||||
@@ -1741,20 +1741,25 @@ test("D9/D10 contrast after occupation coverage asks a discriminator, not adopt"
|
||||
{ layer: "d10", signs: ["巨蟹", "狮子", "处女"] },
|
||||
],
|
||||
probes: [{
|
||||
probeId: "contrast:varga.d10.巨蟹|狮子|处女",
|
||||
probeId: "contrast:varga.d10.05:00|05:01|05:02",
|
||||
candidateSetVersion: "05:00-05:02",
|
||||
question: "当前几个候选在事业盘上还分得开。请核对一段还没用进评分的职业前事。",
|
||||
expectedOutcomes: [
|
||||
{ outcomeId: "supports_巨蟹", supportsCandidateIds: ["巨蟹"], conflictsCandidateIds: ["狮子", "处女"] },
|
||||
{ outcomeId: "supports_狮子", supportsCandidateIds: ["狮子"], conflictsCandidateIds: ["巨蟹", "处女"] },
|
||||
{ outcomeId: "supports_处女", supportsCandidateIds: ["处女"], conflictsCandidateIds: ["巨蟹", "狮子"] },
|
||||
{ outcomeId: "yes", supportsCandidateIds: ["05:00"], conflictsCandidateIds: ["05:01", "05:02"] },
|
||||
{ outcomeId: "weak_yes", supportsCandidateIds: ["05:01"], conflictsCandidateIds: ["05:00", "05:02"] },
|
||||
{ outcomeId: "no", supportsCandidateIds: ["05:02"], conflictsCandidateIds: ["05:00", "05:01"] },
|
||||
],
|
||||
candidateSplitHash: "varga.d10.巨蟹|狮子|处女",
|
||||
candidateSplitHash: "varga.d10.05:00|05:01|05:02",
|
||||
informationGain: 0.12,
|
||||
sourceFeatures: [{ technique: "D10", calculationResultId: RESULT_ID }],
|
||||
domain: "career",
|
||||
year: null,
|
||||
semanticKey: "varga.d10.巨蟹|狮子|处女",
|
||||
semanticKey: "varga.d10.05:00|05:01|05:02",
|
||||
choiceKind: "varga_style",
|
||||
styleOptions: [
|
||||
{ label: "做事偏领导推进", answerClass: "yes", sign: "白羊座" },
|
||||
{ label: "做事偏研究转化", answerClass: "weak_yes", sign: "天蝎座" },
|
||||
],
|
||||
}],
|
||||
},
|
||||
});
|
||||
@@ -1789,7 +1794,7 @@ test("D9/D10 contrast after occupation coverage asks a discriminator, not adopt"
|
||||
}), "discriminate_candidates");
|
||||
});
|
||||
|
||||
test("answered duty language skips window D10 and uses remaining D24", () => {
|
||||
test("answered duty language skips window D10 and collects a dated education event instead of a yearless D24 card", () => {
|
||||
const packet = {
|
||||
candidateSetVersion: "05:00-05:07",
|
||||
vargaDifferences: [
|
||||
@@ -1829,11 +1834,13 @@ test("answered duty language skips window D10 and uses remaining D24", () => {
|
||||
contrastPacket: packet,
|
||||
});
|
||||
assert.equal(plan.next_followup?.domain, "education");
|
||||
assert.equal(plan.next_followup?.semantic_key, "varga.d24.05:00/05:06|05:07");
|
||||
assert.equal(plan.next_followup?.intent, "collect_method_evidence");
|
||||
assert.equal(plan.next_followup?.choice_frame, null);
|
||||
assert.match(plan.next_followup?.user_prompt_hint ?? "", /记得住年份/);
|
||||
assert.doesNotMatch(plan.next_followup?.semantic_key ?? "", /varga\.d10/);
|
||||
});
|
||||
|
||||
test("low-gain career event probe does not outrank a renderable high-gain D24 contrast probe", () => {
|
||||
test("yearless D24 yields to a dated career dasha instead of borrowing the recorded education year", () => {
|
||||
const plan = buildMethodFollowupPlan({
|
||||
evidence: CLASSIC_COVERAGE.filter((item) => item.domain !== "horary"),
|
||||
eventProbes: [{
|
||||
@@ -1866,15 +1873,183 @@ test("low-gain career event probe does not outrank a renderable high-gain D24 co
|
||||
},
|
||||
candidatesSeparated: false,
|
||||
});
|
||||
assert.equal(plan.next_followup?.semantic_key, "varga.d24.05:00/05:07|05:10|05:14");
|
||||
assert.equal(plan.next_followup?.choice_frame?.option_d_answer_class, "unsure");
|
||||
assert.equal(plan.next_followup?.choice_frame?.period, "当前这几个候选");
|
||||
assert.match(plan.next_followup?.choice_frame?.prompt ?? "", /学业|考试发挥|学习压力/);
|
||||
assert.doesNotMatch(plan.next_followup?.choice_frame?.prompt ?? "", /入职、升职/);
|
||||
assert.ok((plan.next_followup?.selection_score ?? 0) > 0.56);
|
||||
assert.equal(plan.next_followup?.semantic_key, "career.2023.dasha_activation");
|
||||
assert.equal(plan.next_followup?.choice_frame?.period, "2023 年前后");
|
||||
assert.match(plan.next_followup?.choice_frame?.prompt ?? "", /2023 年前后,有没有/);
|
||||
assert.doesNotMatch(plan.next_followup?.choice_frame?.prompt ?? "", /2016 年前后/);
|
||||
assert.doesNotMatch(plan.next_followup?.semantic_key ?? "", /varga\.d24/);
|
||||
});
|
||||
|
||||
test("already-open low-gain career card yields to the high-gain D24 catalog winner", () => {
|
||||
const YEARLESS_D12 = {
|
||||
probeId: "contrast:varga.d12.05:00/05:07",
|
||||
candidateSetVersion: "05:00-05:07",
|
||||
question: "当前几个候选在六亲盘上还分得开。请核对一段还没用进评分的家人前事。",
|
||||
expectedOutcomes: [
|
||||
{ outcomeId: "yes", supportsCandidateIds: ["05:00"], conflictsCandidateIds: ["05:07"] },
|
||||
{ outcomeId: "no", supportsCandidateIds: ["05:07"], conflictsCandidateIds: ["05:00"] },
|
||||
],
|
||||
candidateSplitHash: "varga.d12.05:00/05:07",
|
||||
informationGain: 3.5,
|
||||
sourceFeatures: [{ technique: "D12", calculationResultId: RESULT_ID }],
|
||||
domain: "family",
|
||||
year: null,
|
||||
semanticKey: "varga.d12.05:00/05:07",
|
||||
choiceKind: "existence" as const,
|
||||
};
|
||||
|
||||
test("yearless family contrast yields to a dated career discriminator", () => {
|
||||
const plan = buildMethodFollowupPlan({
|
||||
evidence: [
|
||||
datedEvidence("education", "2016"),
|
||||
datedEvidence("relationship", "2018"),
|
||||
datedEvidence("relocation", "2014"),
|
||||
datedEvidence("health_pressure", "2012"),
|
||||
{
|
||||
status: "confirmed" as const,
|
||||
domain: "occupation",
|
||||
datePrecision: "unknown" as const,
|
||||
occurredFrom: null,
|
||||
occurredTo: null,
|
||||
eventKind: "occupation_note",
|
||||
},
|
||||
],
|
||||
eventProbes: [{
|
||||
...CAREER_CONFLICT_PROBE,
|
||||
year: 2020,
|
||||
year_label: "2020 年 4 月前后",
|
||||
information_gain: 0.56,
|
||||
semantic_key: "career.2020.dasha_activation",
|
||||
}],
|
||||
contrastPacket: {
|
||||
candidateSetVersion: "05:00-05:07",
|
||||
vargaDifferences: [],
|
||||
probes: [YEARLESS_D12],
|
||||
},
|
||||
candidatesSeparated: false,
|
||||
});
|
||||
assert.notEqual(plan.next_followup?.domain, "family");
|
||||
assert.equal(plan.next_followup?.choice_frame != null, true);
|
||||
assert.match(plan.next_followup?.choice_frame?.period ?? "", /2020 年 4 月前后/);
|
||||
assert.match(plan.next_followup?.choice_frame?.prompt ?? "", /2020 年 4 月前后,有没有/);
|
||||
assert.doesNotMatch(plan.next_followup?.choice_frame?.prompt ?? "", /那段时间/);
|
||||
});
|
||||
|
||||
test("yearless family contrast without a dated discriminator collects a dated family event", () => {
|
||||
const plan = buildMethodFollowupPlan({
|
||||
evidence: [
|
||||
...CLASSIC_COVERAGE.filter((item) => item.domain !== "horary" && item.domain !== "family"),
|
||||
datedEvidence("relocation", "2014"),
|
||||
],
|
||||
contrastPacket: {
|
||||
candidateSetVersion: "05:00-05:07",
|
||||
vargaDifferences: [],
|
||||
probes: [YEARLESS_D12],
|
||||
},
|
||||
candidatesSeparated: false,
|
||||
});
|
||||
assert.equal(plan.next_followup?.intent, "collect_method_evidence");
|
||||
assert.equal(plan.next_followup?.domain, "family");
|
||||
assert.equal(plan.next_followup?.choice_frame, null);
|
||||
assert.match(plan.next_followup?.user_prompt_hint ?? "", /记得住时间/);
|
||||
assert.doesNotMatch(plan.next_followup?.user_prompt_hint ?? "", /A\/B\/C\/D/);
|
||||
});
|
||||
|
||||
test("family collect attaches the collection-probe year instead of a yearless D24 card", () => {
|
||||
const plan = buildMethodFollowupPlan({
|
||||
evidence: [
|
||||
datedEvidence("career", "2020"),
|
||||
datedEvidence("career", "2024"),
|
||||
datedEvidence("relationship", "2024"),
|
||||
datedEvidence("career", "2026"),
|
||||
datedEvidence("relationship", "2024", { datePrecision: "day" }),
|
||||
datedEvidence("relationship", "2025", { datePrecision: "day" }),
|
||||
],
|
||||
askedProbeKeys: ["relocation.2015.05.dasha_boundary"],
|
||||
contrastPacket: {
|
||||
candidateSetVersion: "05:00-05:14",
|
||||
vargaDifferences: [],
|
||||
probes: [{
|
||||
probeId: "contrast:varga.d24.05:00/05:14",
|
||||
candidateSetVersion: "05:00-05:14",
|
||||
question: "引擎给出的区分机会绑定 D24。",
|
||||
expectedOutcomes: [
|
||||
{ outcomeId: "yes", supportsCandidateIds: ["05:00"], conflictsCandidateIds: ["05:14"] },
|
||||
{ outcomeId: "no", supportsCandidateIds: ["05:14"], conflictsCandidateIds: ["05:00"] },
|
||||
],
|
||||
candidateSplitHash: "varga.d24.05:00/05:14",
|
||||
informationGain: 2.5,
|
||||
sourceFeatures: [{ technique: "D24", calculationResultId: RESULT_ID }],
|
||||
domain: "education",
|
||||
year: null,
|
||||
semanticKey: "varga.d24.05:00/05:14",
|
||||
}],
|
||||
},
|
||||
evidenceCollectionProbes: [{
|
||||
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",
|
||||
}],
|
||||
candidatesSeparated: false,
|
||||
});
|
||||
assert.equal(plan.next_followup?.intent, "collect_method_evidence");
|
||||
assert.equal(plan.next_followup?.domain, "family");
|
||||
assert.equal(plan.next_followup?.choice_frame, null);
|
||||
assert.equal(plan.next_followup?.probe_year, 2021);
|
||||
assert.match(plan.next_followup?.year_label ?? "", /2021/);
|
||||
assert.doesNotMatch(plan.next_followup?.semantic_key ?? "", /varga\.d24/);
|
||||
assert.match(spokenFollowupForUser(plan.next_followup) ?? "", /2021 年前后,家里有没有/);
|
||||
});
|
||||
|
||||
test("already-open yearless family card does not keep a scoring frame", () => {
|
||||
const plan = buildMethodFollowupPlan({
|
||||
evidence: [
|
||||
...CLASSIC_COVERAGE.filter((item) => item.domain !== "horary" && item.domain !== "family"),
|
||||
datedEvidence("relocation", "2014"),
|
||||
],
|
||||
contrastPacket: {
|
||||
candidateSetVersion: "05:00-05:07",
|
||||
vargaDifferences: [],
|
||||
probes: [YEARLESS_D12],
|
||||
},
|
||||
candidatesSeparated: false,
|
||||
activeFocus: {
|
||||
intent: "distinguish_candidates",
|
||||
targetDomain: "family",
|
||||
targetKind: "family_event",
|
||||
expectedAnswerSchema: {
|
||||
semantic_key: "varga.d12.05:00/05:07",
|
||||
choice: {
|
||||
prompt: "那段时间,有没有家人相关的明显变化?",
|
||||
option_a: "明确发生且时间吻合",
|
||||
option_b: "发生过但程度较弱",
|
||||
option_c: "明确没有发生",
|
||||
option_d: "这段记不清楚",
|
||||
options: DYNAMIC_STYLE_OPTIONS.map((option, index) => ({
|
||||
key: (["A", "B", "C", "D"] as const)[index]!,
|
||||
label: option.label,
|
||||
answer_class: option.answer_class,
|
||||
})),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
assert.notEqual(plan.next_followup?.choice_frame?.prompt, "那段时间,有没有家人相关的明显变化?");
|
||||
assert.equal(plan.next_followup?.choice_frame, null);
|
||||
assert.equal(plan.next_followup?.intent, "collect_method_evidence");
|
||||
assert.equal(plan.next_followup?.domain, "family");
|
||||
});
|
||||
|
||||
test("already-open dated career card stays ahead of a yearless D24 catalog row", () => {
|
||||
const plan = buildMethodFollowupPlan({
|
||||
evidence: CLASSIC_COVERAGE.filter((item) => item.domain !== "horary"),
|
||||
eventProbes: [{
|
||||
@@ -1916,8 +2091,8 @@ test("already-open low-gain career card yields to the high-gain D24 catalog winn
|
||||
},
|
||||
},
|
||||
});
|
||||
assert.equal(plan.next_followup?.semantic_key, "varga.d24.05:00/05:07|05:10|05:14");
|
||||
assert.doesNotMatch(plan.next_followup?.semantic_key ?? "", /career\.2023/);
|
||||
assert.equal(plan.next_followup?.semantic_key, "career.2023.dasha_activation");
|
||||
assert.doesNotMatch(plan.next_followup?.semantic_key ?? "", /varga\.d24/);
|
||||
});
|
||||
|
||||
const DUMP_COVERAGE = [
|
||||
@@ -2004,7 +2179,7 @@ const DUMP_SCORES = [
|
||||
{ time: "05:04", score: 33 },
|
||||
];
|
||||
|
||||
test("coverage-complete tie with encoded D24/D10 asks remaining D4, not enrollment or breakup", () => {
|
||||
test("coverage-complete tie with encoded D24/D10 collects a dated move instead of a yearless D4 card", () => {
|
||||
const packet = buildCandidateContrastPacket({
|
||||
candidateSetVersion: "05:00-05:04",
|
||||
candidateTimes: DUMP_SCORES.map((item) => item.time),
|
||||
@@ -2018,9 +2193,12 @@ test("coverage-complete tie with encoded D24/D10 asks remaining D4, not enrollme
|
||||
});
|
||||
assert.equal(plan.next_followup?.domain, "relocation");
|
||||
assert.equal(plan.next_followup?.kind_hint, "home_change");
|
||||
assert.match(plan.next_followup?.semantic_key ?? "", /varga\.d4/);
|
||||
assert.equal(plan.next_followup?.intent, "collect_method_evidence");
|
||||
assert.equal(plan.next_followup?.choice_frame, null);
|
||||
assert.match(plan.next_followup?.user_prompt_hint ?? "", /记得住时间的搬家/);
|
||||
assert.doesNotMatch(plan.next_followup?.kind_hint ?? "", /education_start|relationship_end/);
|
||||
assert.doesNotMatch(plan.next_followup?.user_prompt_hint ?? "", /大学哪年入学|高考是 \d{4}|哪年毕业/);
|
||||
assert.doesNotMatch(plan.next_followup?.choice_frame?.prompt ?? "", /那段时间/);
|
||||
assert.equal(conversationalSessionOutcome({
|
||||
selectionAllowed: true,
|
||||
proposeAllowed: true,
|
||||
@@ -2114,7 +2292,7 @@ test("structured paused state ends evidence collection without parsing user copy
|
||||
});
|
||||
|
||||
|
||||
test("covered-domain existence probes are skipped; the highest remaining discriminator wins", () => {
|
||||
test("same-year existence probes are skipped; a different-year dasha still ranks", () => {
|
||||
const evidence = [
|
||||
datedEvidence("education", "2016"),
|
||||
datedEvidence("relationship", "2018"),
|
||||
@@ -2125,12 +2303,69 @@ test("covered-domain existence probes are skipped; the highest remaining discrim
|
||||
evidence,
|
||||
eventProbes: [{
|
||||
...CAREER_CONFLICT_PROBE,
|
||||
year: 2015,
|
||||
year_label: "2015 年前后",
|
||||
information_gain: 0.56,
|
||||
semantic_key: "career.2015.dasha_activation",
|
||||
candidate_split_hash: "set-test:career:2015",
|
||||
}],
|
||||
});
|
||||
assert.notEqual(skipped.next_followup?.semantic_key, "career.2015.dasha_activation");
|
||||
|
||||
const dated = buildMethodFollowupPlan({
|
||||
evidence,
|
||||
eventProbes: [{
|
||||
...CAREER_CONFLICT_PROBE,
|
||||
year: 2018,
|
||||
year_label: "2018 年前后",
|
||||
information_gain: 0.56,
|
||||
semantic_key: "career.2018.dasha_activation",
|
||||
}],
|
||||
});
|
||||
assert.notEqual(skipped.next_followup?.domain, "career");
|
||||
assert.notEqual(skipped.next_followup?.source, "event_probe");
|
||||
assert.equal(dated.next_followup?.semantic_key, "career.2018.dasha_activation");
|
||||
assert.match(dated.next_followup?.choice_frame?.period ?? "", /2018 年前后/);
|
||||
|
||||
const childhood = buildMethodFollowupPlan({
|
||||
evidence,
|
||||
birthDate: "1997-08-08",
|
||||
eventProbes: [{
|
||||
...CAREER_CONFLICT_PROBE,
|
||||
year: 2012,
|
||||
year_label: "2012 年 11 月前后",
|
||||
information_gain: 1.3,
|
||||
semantic_key: "career.2012.11.dasha_boundary",
|
||||
candidate_split_hash: "set-test:career:2012",
|
||||
}, {
|
||||
...CAREER_CONFLICT_PROBE,
|
||||
year: 2019,
|
||||
year_label: "2019 年前后",
|
||||
information_gain: 0.4,
|
||||
semantic_key: "career.2019.dasha_activation",
|
||||
candidate_split_hash: "set-test:career:2019",
|
||||
}],
|
||||
});
|
||||
assert.notEqual(childhood.next_followup?.semantic_key, "career.2012.11.dasha_boundary");
|
||||
assert.equal(childhood.next_followup?.semantic_key, "career.2019.dasha_activation");
|
||||
|
||||
const adjacentRelationship = buildMethodFollowupPlan({
|
||||
evidence: [
|
||||
datedEvidence("education", "2016"),
|
||||
datedEvidence("career", "2020"),
|
||||
datedEvidence("relationship", "2024"),
|
||||
datedEvidence("family", "2023"),
|
||||
],
|
||||
eventProbes: [{
|
||||
...CAREER_CONFLICT_PROBE,
|
||||
domain: "relationship",
|
||||
event_family: "开始一段认真关系、分手或结婚",
|
||||
year: 2023,
|
||||
year_label: "2023 年 5 月前后",
|
||||
information_gain: 0.9,
|
||||
semantic_key: "relationship.2023.05.dasha_boundary",
|
||||
candidate_split_hash: "set-test:relationship:2023",
|
||||
}],
|
||||
});
|
||||
assert.notEqual(adjacentRelationship.next_followup?.semantic_key, "relationship.2023.05.dasha_boundary");
|
||||
|
||||
const d24 = {
|
||||
probeId: "contrast:varga.d24.05:00|05:07|05:14",
|
||||
@@ -2195,7 +2430,7 @@ test("covered-domain existence probes are skipped; the highest remaining discrim
|
||||
},
|
||||
candidatesSeparated: false,
|
||||
});
|
||||
assert.equal(d24Wins.next_followup?.semantic_key, d24.semanticKey);
|
||||
assert.equal(d24Wins.next_followup?.semantic_key, d10.semanticKey);
|
||||
});
|
||||
|
||||
test("three dated events with one holdout keep collecting instead of discriminating", () => {
|
||||
|
||||
@@ -209,7 +209,7 @@ test("complete dynamic event options persist a server-owned focus", async () =>
|
||||
assert.equal(result.status, "created");
|
||||
assert.equal(accounting.calls.length, 1);
|
||||
assert.deepEqual(result.focus?.expectedAnswerSchema.choice, {
|
||||
prompt: "2016 年前后 · 升学结果或学习环境出现明显变化",
|
||||
prompt: "2016 年前后,有没有升学结果或学习环境出现明显变化?",
|
||||
option_a: DYNAMIC_STYLE_OPTIONS[0].label,
|
||||
option_b: DYNAMIC_STYLE_OPTIONS[1].label,
|
||||
option_c: DYNAMIC_STYLE_OPTIONS[2].label,
|
||||
|
||||
@@ -28,9 +28,14 @@ test("does not publish intermediate tool-step text as answer.delta", () => {
|
||||
isPublicTool,
|
||||
);
|
||||
assert.equal(afterTool.kind, "discard");
|
||||
applyStepAnswerChunk(state, chunk("text-delta", { text: "记下了,2020年4月开始实习。" }), isPublicTool);
|
||||
const live = applyStepAnswerChunk(
|
||||
state,
|
||||
chunk("text-delta", { text: "记下了,2020年4月开始实习。" }),
|
||||
isPublicTool,
|
||||
);
|
||||
assert.deepEqual(live, { kind: "live", text: "记下了,2020年4月开始实习。" });
|
||||
const finish = applyStepAnswerChunk(state, chunk("step-finish", { reason: "stop" }), isPublicTool);
|
||||
assert.deepEqual(finish, { kind: "publish", pieces: ["记下了,2020年4月开始实习。"] });
|
||||
assert.equal(finish.kind, "none");
|
||||
});
|
||||
|
||||
test("never publishes reasoning-delta to the browser", () => {
|
||||
@@ -47,7 +52,76 @@ test("publishes only the terminal no-tool step as assistant text", () => {
|
||||
applyStepAnswerChunk(state, chunk("step-finish", { stepResult: { reason: "tool-calls" } }), isPublicTool);
|
||||
assert.equal(shouldPublishStepText({ calledTool: true, text: "_probe" }, "tool-calls"), false);
|
||||
applyStepAnswerChunk(state, chunk("step-start"), isPublicTool);
|
||||
applyStepAnswerChunk(state, chunk("text-delta", { text: "已经记下实习。" }), isPublicTool);
|
||||
const live = applyStepAnswerChunk(state, chunk("text-delta", { text: "已经记下实习。" }), isPublicTool);
|
||||
assert.deepEqual(live, { kind: "live", text: "已经记下实习。" });
|
||||
const finish = flushStepAnswerOnStreamFinish(state, "stop");
|
||||
assert.deepEqual(finish, { kind: "publish", pieces: ["已经记下实习。"] });
|
||||
assert.equal(finish.kind, "none");
|
||||
});
|
||||
|
||||
test("streams terminal Chinese tokens live after compare", () => {
|
||||
const state = createStepAnswerState();
|
||||
applyStepAnswerChunk(
|
||||
state,
|
||||
chunk("tool-result", { toolName: "rectification-compare-candidates" }),
|
||||
isPublicTool,
|
||||
);
|
||||
const first = applyStepAnswerChunk(state, chunk("text-delta", { text: "记下了," }), isPublicTool);
|
||||
const second = applyStepAnswerChunk(
|
||||
state,
|
||||
chunk("text-delta", { text: "2020年4月开始实习。" }),
|
||||
isPublicTool,
|
||||
);
|
||||
assert.deepEqual(first, { kind: "live", text: "记下了," });
|
||||
assert.deepEqual(second, { kind: "live", text: "2020年4月开始实习。" });
|
||||
assert.equal(applyStepAnswerChunk(state, chunk("step-finish", { reason: "stop" }), isPublicTool).kind, "none");
|
||||
});
|
||||
|
||||
test("retracts a live spoken prefix if that step later calls a public tool", () => {
|
||||
const state = createStepAnswerState();
|
||||
applyStepAnswerChunk(
|
||||
state,
|
||||
chunk("tool-result", { toolName: "rectification-compare-candidates" }),
|
||||
isPublicTool,
|
||||
);
|
||||
assert.deepEqual(
|
||||
applyStepAnswerChunk(state, chunk("text-delta", { text: "记下了," }), isPublicTool),
|
||||
{ kind: "live", text: "记下了," },
|
||||
);
|
||||
const retracted = applyStepAnswerChunk(
|
||||
state,
|
||||
chunk("tool-call", { toolName: "rectification-read-diagnostics" }),
|
||||
isPublicTool,
|
||||
);
|
||||
assert.equal(retracted.kind, "retract");
|
||||
applyStepAnswerChunk(
|
||||
state,
|
||||
chunk("tool-result", { toolName: "rectification-read-diagnostics" }),
|
||||
isPublicTool,
|
||||
);
|
||||
const spoken = applyStepAnswerChunk(
|
||||
state,
|
||||
chunk("text-delta", { text: "诊断已经看过。接下来确认入职年份。" }),
|
||||
isPublicTool,
|
||||
);
|
||||
assert.deepEqual(spoken, { kind: "live", text: "诊断已经看过。接下来确认入职年份。" });
|
||||
});
|
||||
|
||||
test("does not live-publish English planning before a tool-call", () => {
|
||||
const state = createStepAnswerState();
|
||||
assert.equal(
|
||||
applyStepAnswerChunk(state, chunk("text-delta", { text: "Let me set" }), isPublicTool).kind,
|
||||
"none",
|
||||
);
|
||||
assert.equal(
|
||||
applyStepAnswerChunk(state, chunk("text-delta", { text: " _probe" }), isPublicTool).kind,
|
||||
"none",
|
||||
);
|
||||
assert.equal(
|
||||
applyStepAnswerChunk(
|
||||
state,
|
||||
chunk("tool-call", { toolName: "rectification-record-evidence-batch" }),
|
||||
isPublicTool,
|
||||
).kind,
|
||||
"none",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -205,6 +205,14 @@ test("streamToolNames exposes only allowlisted rectification tools", () => {
|
||||
|
||||
test("safePublicEvent drops anything outside the allowlist", () => {
|
||||
assert.deepEqual(safePublicEvent({ type: "answer.delta", text: "你好" }), { type: "answer.delta", text: "你好" });
|
||||
assert.deepEqual(
|
||||
safePublicEvent({ type: "answer.delta", text: "你好", replace: true }),
|
||||
{ type: "answer.delta", text: "你好", replace: true },
|
||||
);
|
||||
assert.deepEqual(
|
||||
safePublicEvent({ type: "answer.delta", text: "你好", replace: false }),
|
||||
{ type: "answer.delta", text: "你好" },
|
||||
);
|
||||
assert.equal(safePublicEvent({ type: "thinking.delta", text: "先核对升学" }), null);
|
||||
assert.deepEqual(
|
||||
safePublicEvent({ type: "activity.changed", activity: "reading_case" }),
|
||||
@@ -482,6 +490,8 @@ test("answer deltas stream in order and reasoning is never forwarded", async ()
|
||||
chunk("tool-result", { toolName: "skill" }),
|
||||
chunk("tool-call", { toolName: "rectification-read-case", args: { caseId: CASE_ID } }),
|
||||
chunk("tool-result", { toolName: "rectification-read-case" }),
|
||||
chunk("tool-call", { toolName: "rectification-compare-candidates", args: { caseId: CASE_ID } }),
|
||||
chunk("tool-result", { toolName: "rectification-compare-candidates" }),
|
||||
chunk("reasoning-start", { id: "r1" }),
|
||||
chunk("reasoning-delta", { text: "我应该先……" }),
|
||||
chunk("reasoning-end"),
|
||||
@@ -495,8 +505,10 @@ test("answer deltas stream in order and reasoning is never forwarded", async ()
|
||||
assert.equal(result.ok, true);
|
||||
const deltas = emitted.filter((event) => event.type === "answer.delta");
|
||||
assert.deepEqual(deltas, [
|
||||
{ type: "answer.delta", text: "好的,先确认一下:" },
|
||||
{ type: "answer.delta", text: "好的," },
|
||||
{ type: "answer.delta", text: "先确认一下:" },
|
||||
]);
|
||||
assert.equal(deltas.map((event) => event.text).join(""), "好的,先确认一下:");
|
||||
assert.deepEqual(
|
||||
emitted.filter((event) => event.type === "thinking.delta"),
|
||||
[],
|
||||
@@ -671,6 +683,40 @@ test("a length-limited spoken answer is not billed or persisted as a completed t
|
||||
assert.equal(turnFinalize?.args.p_successful_attempt_id, null);
|
||||
});
|
||||
|
||||
test("length after a stamped open_question still completes without failing the choice card", async () => {
|
||||
const prompt = "2023 年 5 月前后,有没有认真关系进入、结束或关系观明显转变?";
|
||||
const accounting = fakeAccounting({
|
||||
...receiptHandlers,
|
||||
get_agentic_rectification_case_dossier: () => dossierFixture(),
|
||||
append_agentic_rectification_turn: () => ({ turn_id: TURN_ID }),
|
||||
finalize_agentic_rectification_turn: () => ({ turn_id: TURN_ID, status: "completed", idempotent: false }),
|
||||
});
|
||||
const { options, emitted, billing } = runOptions({
|
||||
accounting: accounting.client,
|
||||
buildAgent: async () => fakeAgentStream([
|
||||
chunk("start"),
|
||||
chunk("tool-call", { toolName: "skill", args: { name: RECTIFICATION_SKILL_NAME } }),
|
||||
chunk("tool-result", { toolName: "skill" }),
|
||||
chunk("tool-call", { toolName: "rectification-read-case", args: { caseId: CASE_ID } }),
|
||||
chunk("tool-result", { toolName: "rectification-read-case" }),
|
||||
chunk("tool-call", { toolName: "rectification-record-evidence-batch", args: { caseId: CASE_ID } }),
|
||||
chunk("tool-result", {
|
||||
toolName: "rectification-record-evidence-batch",
|
||||
result: { accepted_count: 1, open_question: { prompt } },
|
||||
}),
|
||||
chunk("text-delta", { text: "实习和离职的时间点都记下了,谢谢。" }),
|
||||
chunk("finish", { stepResult: { reason: "length" } }),
|
||||
]) as never,
|
||||
});
|
||||
const result = await runV9AgentTurn(options);
|
||||
assert.equal(result.ok, true);
|
||||
assert.equal(result.errorCode, null);
|
||||
assert.equal(result.answerText, "接下来看下面这一问。");
|
||||
assert.equal(emitted.some((event) => event.type === "run.failed"), false);
|
||||
assert.equal(emitted.some((event) => event.type === "run.completed"), true);
|
||||
assert.deepEqual(billing, { reserved: 1, completed: 1, released: 0 });
|
||||
});
|
||||
|
||||
test("answer deltas and tool activity are published before billing settles", async () => {
|
||||
let billingStarted = false;
|
||||
const seenBeforeBilling: string[] = [];
|
||||
@@ -1399,16 +1445,51 @@ test("timeout after a stamped open_question still completes without pasting the
|
||||
const result = await runV9AgentTurn(options);
|
||||
assert.equal(result.ok, true);
|
||||
assert.equal(result.errorCode, null);
|
||||
assert.equal(result.answerText, "记下了。");
|
||||
assert.equal(result.answerText, "接下来看下面这一问。");
|
||||
assert.doesNotMatch(result.answerText, /有没有/);
|
||||
assert.equal(emitted.some((event) => event.type === "run.failed"), false);
|
||||
assert.equal(emitted.some((event) => event.type === "run.completed"), true);
|
||||
assert.deepEqual(
|
||||
emitted.filter((event) => event.type === "answer.delta"),
|
||||
[{ type: "answer.delta", text: "记下了。" }],
|
||||
[{ type: "answer.delta", text: "接下来看下面这一问。" }],
|
||||
);
|
||||
});
|
||||
|
||||
test("open_question acknowledgements stream live instead of waiting for flush", async () => {
|
||||
const prompt = "2023 年前后 · 入职、升职或职责明显加重";
|
||||
const { options, emitted } = runOptions({
|
||||
buildAgent: async () => fakeAgentStream([
|
||||
chunk("start"),
|
||||
chunk("tool-call", { toolName: "skill", args: { name: RECTIFICATION_SKILL_NAME } }),
|
||||
chunk("tool-result", { toolName: "skill" }),
|
||||
chunk("tool-call", { toolName: "rectification-read-case", args: { caseId: CASE_ID } }),
|
||||
chunk("tool-result", { toolName: "rectification-read-case" }),
|
||||
chunk("tool-call", { toolName: "rectification-record-evidence-batch", args: { caseId: CASE_ID } }),
|
||||
chunk("tool-result", {
|
||||
toolName: "rectification-record-evidence-batch",
|
||||
result: { accepted_count: 1, open_question: { prompt } },
|
||||
}),
|
||||
chunk("tool-call", { toolName: "rectification-compare-candidates", args: { caseId: CASE_ID } }),
|
||||
chunk("tool-result", { toolName: "rectification-compare-candidates" }),
|
||||
chunk("text-delta", { text: "好的," }),
|
||||
chunk("text-delta", { text: "毕业这条也记下了。" }),
|
||||
chunk("text-delta", { text: "\n\n再问你一件:2016 年前后那场重要的入学考试有没有发生过?" }),
|
||||
chunk("finish"),
|
||||
]) as never,
|
||||
});
|
||||
const result = await runV9AgentTurn(options);
|
||||
assert.equal(result.ok, true);
|
||||
assert.equal(result.answerText, "好的,毕业这条也记下了。");
|
||||
assert.deepEqual(
|
||||
emitted.filter((event) => event.type === "answer.delta"),
|
||||
[
|
||||
{ type: "answer.delta", text: "好的," },
|
||||
{ type: "answer.delta", text: "毕业这条也记下了。" },
|
||||
],
|
||||
);
|
||||
assert.doesNotMatch(JSON.stringify(emitted.filter((event) => event.type === "answer.delta")), /入学考试/);
|
||||
});
|
||||
|
||||
test("persisted choice prompt replaces a competing model follow-up without a topic denylist", async () => {
|
||||
const spoken = "好的,2020 年 6 月毕业这条也记下了。\n\n再问你一件:2016 年前后那场重要的入学考试,你当时发挥明显失常、或者压力特别大,有没有发生过?";
|
||||
const prompt = "2023 年前后 · 入职、升职或职责明显加重";
|
||||
@@ -1480,6 +1561,35 @@ test("persisted choice card owns a matching year-locked follow-up", async () =>
|
||||
);
|
||||
});
|
||||
|
||||
test("structured-choice acknowledgements are not kept as a second assistant reply", () => {
|
||||
const prompt = "2012 年 11 月前后,有没有入职、升职或职责明显加重?";
|
||||
assert.equal(
|
||||
bindSpokenToOpenQuestion("已记下你刚才的选择,候选比较也随之更新了。", prompt),
|
||||
"",
|
||||
);
|
||||
assert.equal(
|
||||
bindSpokenToOpenQuestion("已记录你的选择,并更新了候选比较。接下来这一问和家里有关,请看下方选项。", prompt),
|
||||
"",
|
||||
);
|
||||
assert.equal(bindSpokenToOpenQuestion("记下了。", prompt), "记下了。");
|
||||
});
|
||||
|
||||
test("choice-card acknowledgements do not repeat a different-domain event", () => {
|
||||
const intern = "实习和离职的时间点都记下了,谢谢。";
|
||||
assert.equal(
|
||||
bindSpokenToOpenQuestion(intern, "2005 年 5 月前后,有没有搬家、离乡或长期异地?"),
|
||||
"",
|
||||
);
|
||||
assert.equal(
|
||||
bindSpokenToOpenQuestion(intern, "2023 年 5 月前后,有没有认真关系进入、结束或关系观明显转变?"),
|
||||
"",
|
||||
);
|
||||
assert.equal(
|
||||
bindSpokenToOpenQuestion("好,实习和离职都记下了。", "2023 年前后 · 入职、升职或职责明显加重"),
|
||||
"好,实习和离职都记下了。",
|
||||
);
|
||||
});
|
||||
|
||||
test("lock-only prompt is not spliced into speech", () => {
|
||||
const lock = "2023 年前后 · 入职、升职或职责明显加重";
|
||||
assert.equal(bindSpokenToOpenQuestion("", lock), "");
|
||||
|
||||
Reference in New Issue
Block a user