Files
Jyotisha/frontend/tests/rectification-spoken-orphan-20260914.test.ts
T
jesse-ux 2dbcf2661b
Independent Staging Quality Gate / validate (push) Failing after 11m54s
Independent Staging Quality Gate / publish (push) Skipped
fix(rectification): hang spoken year follow-ups and drop engine representative from model tables (BUG-678/676)
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.
2026-09-14 12:12:27 +08:00

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"],
);
});