Spoken collect without askedTurnId now hangs on the last assistant message, remaining-candidate copy uses credible_range, and engine representativeTime no longer keeps eliminated minutes in model house tables.
211 lines
7.0 KiB
TypeScript
211 lines
7.0 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
|
|
import { slimDecisionReceipt } from "../src/lib/rectification-agentic/v9/case-receipt-projection.ts";
|
|
import { buildInferenceState } from "../src/lib/rectification-agentic/core/build-state.ts";
|
|
import { TARGETED_YEAR_PROMPT } from "../src/lib/rectification-agentic/v9/collection-question-pool.ts";
|
|
import { parseV9CaseDossier } from "../src/lib/rectification-agentic/v9/tool-service.ts";
|
|
import { projectCurrentQuestion } from "../src/lib/rectification-agentic/v9/turn-decision.ts";
|
|
import {
|
|
attachQuestionsToTurns,
|
|
questionIsAnswered,
|
|
} from "../src/lib/rectification-agentic/v9/turn-question.ts";
|
|
import {
|
|
interviewChoiceCardUnavailable,
|
|
rectificationQuestionGapState,
|
|
} from "../src/lib/rectification-surface-state.ts";
|
|
import {
|
|
CASE_ID,
|
|
FOCUS_ID,
|
|
TURN_ID,
|
|
activeFocusFixture,
|
|
candidateSnapshotFixture,
|
|
conversationSummaryFixture,
|
|
dossierFixture,
|
|
} from "./rectification-v9-test-support.ts";
|
|
|
|
const YEAR_FOCUS_ID = "acacacac-acac-4cac-8cac-acacacacacac";
|
|
const CHOICE_FOCUS_ID = "adadadad-adad-4dad-8dad-adadadadadad";
|
|
|
|
function spokenYearFocus() {
|
|
return {
|
|
id: FOCUS_ID,
|
|
caseId: CASE_ID,
|
|
questionId: "collect:targeted:finance:year",
|
|
intent: "collect_method_evidence",
|
|
targetEvidenceId: null,
|
|
targetDomain: "finance",
|
|
targetKind: "targeted:finance:year",
|
|
expectedAnswerSchema: {
|
|
collect: true,
|
|
prompt: TARGETED_YEAR_PROMPT,
|
|
collect_kind: "targeted:finance:year",
|
|
},
|
|
status: "active" as const,
|
|
askedAt: "2026-09-14T00:00:00.000Z",
|
|
resolvedAt: null,
|
|
askedTurnId: null,
|
|
answerOption: null,
|
|
};
|
|
}
|
|
|
|
test("spoken year follow-up without asked_turn_id hangs on the last assistant turn", () => {
|
|
const focus = spokenYearFocus();
|
|
const current = projectCurrentQuestion(focus);
|
|
assert.equal(current?.kind, "collect_spoken");
|
|
assert.equal(current?.prompt, "大概哪年几月?");
|
|
assert.equal(current?.question_id, "collect:targeted:finance:year");
|
|
assert.equal(
|
|
interviewChoiceCardUnavailable({
|
|
questionKind: current?.kind,
|
|
hasChoiceCard: false,
|
|
questionId: current?.question_id,
|
|
}),
|
|
false,
|
|
);
|
|
const attached = attachQuestionsToTurns(
|
|
[{ id: TURN_ID, role: "assistant" as const, text: "记下了。" }],
|
|
[focus],
|
|
);
|
|
assert.equal(attached[0]?.question?.kind, "collect_spoken");
|
|
assert.equal(attached[0]?.question?.prompt, "大概哪年几月?");
|
|
assert.equal(attached[0]?.question?.question_id, "collect:targeted:finance:year");
|
|
assert.equal(attached[0]?.question?.focus_id, FOCUS_ID);
|
|
const live = Boolean(
|
|
attached[0]?.question
|
|
&& current
|
|
&& attached[0].question.focus_id === current.focus_id
|
|
&& !questionIsAnswered(attached[0].question),
|
|
);
|
|
assert.equal(live, true);
|
|
assert.equal(
|
|
rectificationQuestionGapState({
|
|
liveQuestionVisible: live,
|
|
questionMissing: false,
|
|
questionLoadFailed: false,
|
|
questionPersisted: true,
|
|
busy: false,
|
|
readonly: false,
|
|
regenerating: false,
|
|
snapshotLoaded: true,
|
|
resumableCase: true,
|
|
retryAttempts: 0,
|
|
}),
|
|
"idle",
|
|
);
|
|
});
|
|
|
|
test("hanging spoken focus does not replace a question already on the last assistant turn", () => {
|
|
const prompt = "结过婚或订过婚吗?";
|
|
const attached = attachQuestionsToTurns(
|
|
[{ id: TURN_ID, role: "assistant" as const, text: "记下了,这方面先跳过。" }],
|
|
[
|
|
{
|
|
id: CHOICE_FOCUS_ID,
|
|
caseId: CASE_ID,
|
|
questionId: "collect:targeted:relationship",
|
|
intent: "collect_method_evidence",
|
|
targetEvidenceId: null,
|
|
targetDomain: "relationship",
|
|
targetKind: "targeted:relationship",
|
|
expectedAnswerSchema: {
|
|
targeted_collect: true,
|
|
prompt,
|
|
choice: {
|
|
prompt,
|
|
option_a: "有过这件事",
|
|
option_b: "没有发生过",
|
|
option_c: "记不太清楚",
|
|
option_d: "这条先跳过",
|
|
options: [
|
|
{ key: "A", label: "有过这件事", answer_class: "yes" },
|
|
{ key: "B", label: "没有发生过", answer_class: "no" },
|
|
{ key: "C", label: "记不太清楚", answer_class: "unsure" },
|
|
{ key: "D", label: "这条先跳过", answer_class: "weak_yes" },
|
|
],
|
|
},
|
|
},
|
|
status: "active",
|
|
askedAt: "2026-09-14T00:00:00.000Z",
|
|
resolvedAt: null,
|
|
askedTurnId: TURN_ID,
|
|
answerOption: null,
|
|
},
|
|
{
|
|
...spokenYearFocus(),
|
|
id: YEAR_FOCUS_ID,
|
|
},
|
|
],
|
|
);
|
|
assert.equal(attached[0]?.question?.kind, "choice");
|
|
assert.equal(attached[0]?.question?.focus_id, CHOICE_FOCUS_ID);
|
|
assert.equal(attached[0]?.question?.question_id, "collect:targeted:relationship");
|
|
});
|
|
|
|
test("engine representative time of an eliminated minute is not kept in model house tables", () => {
|
|
const inference = buildInferenceState({
|
|
range_start: "04:48",
|
|
range_end: "05:14",
|
|
candidates: [
|
|
{ id: "04:53", time: "04:53", relative_support: 16 },
|
|
{ id: "05:06", time: "05:06", relative_support: 15 },
|
|
{ id: "05:14", time: "05:14", relative_support: 7 },
|
|
],
|
|
events: [],
|
|
probes: [],
|
|
});
|
|
const fullInference = {
|
|
...inference,
|
|
representative_time: "04:53",
|
|
credible_range: ["04:48", "05:07"] as const,
|
|
candidates: inference.candidates.map((candidate) => (
|
|
candidate.time === "05:14" ? { ...candidate, status: "eliminated" as const } : candidate
|
|
)),
|
|
};
|
|
const houseTables = {
|
|
"04:53": { time: "04:53", houses: Array(12).fill({ sign: "x" }) },
|
|
"05:06": { time: "05:06", houses: Array(12).fill({ sign: "x" }) },
|
|
"05:14": { time: "05:14", houses: Array(12).fill({ sign: "x" }) },
|
|
};
|
|
const receipt = {
|
|
inference_state: fullInference,
|
|
house_tables_by_time: houseTables,
|
|
};
|
|
const dropped = parseV9CaseDossier(dossierFixture({
|
|
latestResult: candidateSnapshotFixture({
|
|
representativeTime: "05:14",
|
|
decisionReceipt: { inference_state: fullInference },
|
|
}),
|
|
conversationSummary: conversationSummaryFixture({
|
|
activeFocus: activeFocusFixture({
|
|
expectedAnswerSchema: { collect: true, prompt: TARGETED_YEAR_PROMPT },
|
|
}),
|
|
}),
|
|
}));
|
|
assert.ok(dropped);
|
|
const slim = slimDecisionReceipt(receipt, dropped!);
|
|
assert.deepEqual(
|
|
Object.keys(slim.house_tables_by_time as Record<string, unknown>).sort(),
|
|
["04:53", "05:06"],
|
|
);
|
|
|
|
const kept = parseV9CaseDossier(dossierFixture({
|
|
latestResult: candidateSnapshotFixture({
|
|
representativeTime: "05:14",
|
|
selectedTime: "05:14",
|
|
decisionReceipt: { inference_state: fullInference },
|
|
}),
|
|
conversationSummary: conversationSummaryFixture({
|
|
activeFocus: activeFocusFixture({
|
|
expectedAnswerSchema: { collect: true, prompt: TARGETED_YEAR_PROMPT },
|
|
}),
|
|
}),
|
|
}));
|
|
assert.ok(kept);
|
|
const slimKept = slimDecisionReceipt(receipt, kept!);
|
|
assert.deepEqual(
|
|
Object.keys(slimKept.house_tables_by_time as Record<string, unknown>).sort(),
|
|
["04:53", "05:06", "05:14"],
|
|
);
|
|
});
|