When dated choice probes are exhausted after the training gate, stop treating yearless D9/D10 cards as the next discriminator and persist a range carrier in the same answer transaction. Co-authored-by: Cursor <cursoragent@cursor.com>
648 lines
22 KiB
TypeScript
648 lines
22 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
|
|
import { candidateSetId } from "../src/lib/rectification-agentic/core/build-state.ts";
|
|
import { asInferenceState } from "../src/lib/rectification-agentic/core/compose-receipt.ts";
|
|
import { INFERENCE_ALGORITHM_VERSION } from "../src/lib/rectification-agentic/core/types.ts";
|
|
import type { ConflictProbe } from "../src/lib/rectification-agentic/core/types.ts";
|
|
import {
|
|
decideFromDossier,
|
|
type DecisionDossier,
|
|
} from "../src/lib/rectification-agentic/v9/decision-from-dossier.ts";
|
|
import {
|
|
persistNextInterviewAfterChoice,
|
|
persistNextInterviewIfIdle,
|
|
} from "../src/lib/rectification-agentic/v9/answer-choice.ts";
|
|
import { resetDeliveryTurnGuardForTests } from "../src/lib/rectification-agentic/v9/delivery-turn-guard.ts";
|
|
import { publicNextAction } from "../src/lib/rectification-agentic/core/rectification-decision.ts";
|
|
import { evidenceLedgerFingerprint } from "../src/lib/rectification-agentic/v9/tool-service.ts";
|
|
import { COLLECT_FLOW_BANNED_PHRASES } from "../src/lib/rectification-agentic/v9/collection-question-pool.ts";
|
|
import { rangeDeliveryForSnapshot } from "../src/lib/rectification-agentic/v9/divergence-panel.ts";
|
|
import {
|
|
buildMethodFollowupPlan,
|
|
buildNextUserAction,
|
|
} from "../src/lib/rectification-agentic/v9/method-followup.ts";
|
|
import {
|
|
persistServerOwnedFocus,
|
|
type PersistServerFocusStatus,
|
|
} from "../src/lib/rectification-agentic/v9/server-focus.ts";
|
|
import { rectificationQuestionGapState } from "../src/lib/rectification-surface-state.ts";
|
|
import {
|
|
CASE_ID,
|
|
FOCUS_ID,
|
|
TURN_ID,
|
|
USER_ID,
|
|
activeFocusFixture,
|
|
candidateSnapshotFixture,
|
|
computeFixture,
|
|
conversationSummaryFixture,
|
|
dossierFixture,
|
|
fakeAccounting,
|
|
receiptHandlers,
|
|
} from "./rectification-v9-test-support.ts";
|
|
|
|
const EXISTENCE_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 VARGA_OPTIONS = [
|
|
{ label: "相处里更在意照顾对方的感受", answer_class: "yes" as const, sign: "巨蟹座" },
|
|
{ label: "习惯带头,也不排斥站到台前", answer_class: "weak_yes" as const, sign: "狮子座" },
|
|
];
|
|
const TIMES = ["04:48", "04:53", "04:54", "04:59", "05:06", "05:07"] as const;
|
|
const SCORES: Record<string, number> = {
|
|
"04:48": 10,
|
|
"04:53": 15,
|
|
"04:54": 14,
|
|
"04:59": 13,
|
|
"05:06": 13,
|
|
"05:07": 12,
|
|
};
|
|
|
|
const educationStart = {
|
|
id: "e-edu-start",
|
|
status: "confirmed" as const,
|
|
domain: "education",
|
|
datePrecision: "month" as const,
|
|
occurredFrom: "2016-09-01",
|
|
occurredTo: "2016-09-30",
|
|
eventKind: "education_start",
|
|
summary: "2016年9月上大学",
|
|
};
|
|
const educationEnd = {
|
|
id: "e-edu-end",
|
|
status: "confirmed" as const,
|
|
domain: "education",
|
|
datePrecision: "month" as const,
|
|
occurredFrom: "2020-06-01",
|
|
occurredTo: "2020-06-30",
|
|
eventKind: "education_completion",
|
|
summary: "2020年6月毕业",
|
|
};
|
|
const careerIntern = {
|
|
id: "e-career-intern",
|
|
status: "confirmed" as const,
|
|
domain: "career",
|
|
datePrecision: "month" as const,
|
|
occurredFrom: "2020-04-01",
|
|
occurredTo: null,
|
|
eventKind: "career_entry",
|
|
summary: "2020年4月入职实习",
|
|
};
|
|
const careerLeave = {
|
|
id: "e-career-leave",
|
|
status: "confirmed" as const,
|
|
domain: "career",
|
|
datePrecision: "month" as const,
|
|
occurredFrom: "2020-10-01",
|
|
occurredTo: null,
|
|
eventKind: "career_exit",
|
|
summary: "2020年10月离职",
|
|
};
|
|
const EVIDENCE = [educationStart, educationEnd, careerIntern, careerLeave];
|
|
|
|
function uuidAt(index: number) {
|
|
return `00000000-0000-4000-8000-${String(index + 1).padStart(12, "0")}`;
|
|
}
|
|
|
|
function existenceProbe(input: {
|
|
key: string;
|
|
domain: string;
|
|
year: number;
|
|
month?: number;
|
|
question: string;
|
|
source?: string;
|
|
choiceKind?: ConflictProbe["choice_kind"];
|
|
}): ConflictProbe {
|
|
return {
|
|
id: `probe:${input.key}`,
|
|
semantic_key: input.key,
|
|
candidate_split_hash: input.key,
|
|
domain: input.domain,
|
|
year: input.year,
|
|
question: input.question,
|
|
candidate_ids: [...TIMES],
|
|
expected_outcomes: [
|
|
{ answer_class: "yes", supports: ["04:54"], conflicts: ["05:06"] },
|
|
{ answer_class: "weak_yes", supports: [], conflicts: [] },
|
|
{ answer_class: "no", supports: ["05:06"], conflicts: ["04:54"] },
|
|
{ answer_class: "unsure", supports: [], conflicts: [] },
|
|
],
|
|
information_gain: 0.4,
|
|
source: input.source ?? "dasha_boundary",
|
|
choice_kind: input.choiceKind ?? "existence",
|
|
style_options: EXISTENCE_OPTIONS,
|
|
};
|
|
}
|
|
|
|
const ASKED_PROBES = [
|
|
existenceProbe({
|
|
key: "career.2023.05.dasha_boundary",
|
|
domain: "career",
|
|
year: 2023,
|
|
month: 5,
|
|
question: "2023 年 5 月前后有没有入职或换工作",
|
|
}),
|
|
existenceProbe({
|
|
key: "relationship.2023.05.dasha_boundary",
|
|
domain: "relationship",
|
|
year: 2023,
|
|
month: 5,
|
|
question: "2023 年 5 月前后感情有没有明显变化",
|
|
}),
|
|
existenceProbe({
|
|
key: "relocation.2015.05.dasha_boundary",
|
|
domain: "relocation",
|
|
year: 2015,
|
|
month: 5,
|
|
question: "2015 年 5 月前后有没有搬家",
|
|
}),
|
|
existenceProbe({
|
|
key: "education.2016.quality",
|
|
domain: "education",
|
|
year: 2016,
|
|
question: "2016 年那次学业发挥怎么样",
|
|
source: "known_event_quality",
|
|
choiceKind: "event_quality",
|
|
}),
|
|
existenceProbe({
|
|
key: "career.2024.04.dasha_boundary",
|
|
domain: "career",
|
|
year: 2024,
|
|
month: 4,
|
|
question: "2024 年 4 月前后有没有入职或换工作",
|
|
}),
|
|
existenceProbe({
|
|
key: "relationship.2024.04.dasha_boundary",
|
|
domain: "relationship",
|
|
year: 2024,
|
|
month: 4,
|
|
question: "2024 年 4 月前后感情有没有明显变化",
|
|
}),
|
|
];
|
|
|
|
const SIXTH = ASKED_PROBES[5]!;
|
|
|
|
const LEFTOVER_SAME_YEAR = existenceProbe({
|
|
key: "career.2023.dasha_activation",
|
|
domain: "career",
|
|
year: 2023,
|
|
question: "2023 年前后有没有职责加重",
|
|
source: "dasha_activation",
|
|
});
|
|
|
|
const D10_STYLE: ConflictProbe = {
|
|
id: "probe:varga.d10.狮子座/处女座",
|
|
semantic_key: "varga.d10.狮子座/处女座",
|
|
candidate_split_hash: "04:48-05:07:04:49,04:53:varga.d10.狮子座/处女座",
|
|
domain: "career",
|
|
year: 0,
|
|
question: "平时做事,你更接近下面哪一种?",
|
|
candidate_ids: [...TIMES],
|
|
expected_outcomes: [
|
|
{ answer_class: "yes", supports: ["04:54"], conflicts: ["05:06"] },
|
|
{ answer_class: "weak_yes", supports: ["05:06"], conflicts: ["04:54"] },
|
|
{ answer_class: "no", supports: [], conflicts: [] },
|
|
{ answer_class: "unsure", supports: [], conflicts: [] },
|
|
],
|
|
information_gain: 0.9,
|
|
source: "varga_contrast",
|
|
choice_kind: "varga_style",
|
|
style_options: VARGA_OPTIONS,
|
|
};
|
|
|
|
const NAKSHATRA: ConflictProbe = {
|
|
id: "probe:nakshatra.boundary",
|
|
semantic_key: "nakshatra.boundary.a/b",
|
|
candidate_split_hash: "nakshatra.boundary.a/b",
|
|
domain: "other",
|
|
year: 0,
|
|
question: "两组月宿性格里更接近哪一种?",
|
|
candidate_ids: ["04:54", "05:06"],
|
|
expected_outcomes: [
|
|
{ answer_class: "yes", supports: ["04:54"], conflicts: ["05:06"] },
|
|
{ answer_class: "weak_yes", supports: ["05:06"], conflicts: ["04:54"] },
|
|
{ answer_class: "no", supports: [], conflicts: [] },
|
|
{ answer_class: "unsure", supports: [], conflicts: [] },
|
|
],
|
|
information_gain: 0.01,
|
|
source: "nakshatra_boundary",
|
|
choice_kind: "varga_style",
|
|
style_options: VARGA_OPTIONS,
|
|
};
|
|
|
|
function liveState(answeredCount: number) {
|
|
const answered = ASKED_PROBES.slice(0, answeredCount);
|
|
const leftoverYearless = [1, 2, 3, 4, 5].map((index) => existenceProbe({
|
|
key: `yearless.existence.${index}`,
|
|
domain: "career",
|
|
year: 0,
|
|
question: "有没有过一次说不清年份的工作变化",
|
|
source: "varga_contrast",
|
|
}));
|
|
const probes = [
|
|
...answered,
|
|
LEFTOVER_SAME_YEAR,
|
|
...leftoverYearless,
|
|
D10_STYLE,
|
|
NAKSHATRA,
|
|
];
|
|
const candidates = TIMES.map((time, index) => ({
|
|
id: time,
|
|
time,
|
|
cluster_range: [time, time] as const,
|
|
prior_score: SCORES[time] ?? 0,
|
|
posterior_score: SCORES[time] ?? 0,
|
|
probability: (SCORES[time] ?? 0) / 76,
|
|
status: "active" as const,
|
|
rank: index + 1,
|
|
strong_conflict_count: 0,
|
|
}));
|
|
const raw = {
|
|
algorithm_version: INFERENCE_ALGORITHM_VERSION,
|
|
candidate_set_id: candidateSetId("04:48", "05:07", TIMES),
|
|
revision: answeredCount,
|
|
phase: "discrimination" as const,
|
|
result_status: "discriminating" as const,
|
|
range_start: "04:48",
|
|
range_end: "05:07",
|
|
candidates,
|
|
events: [
|
|
{ id: educationStart.id, domain: "education", year: 2016, precision: "month" as const, usage: "training" as const },
|
|
{ id: educationEnd.id, domain: "education", year: 2020, precision: "month" as const, usage: "training" as const },
|
|
{ id: careerIntern.id, domain: "career", year: 2020, precision: "month" as const, usage: "training" as const },
|
|
{ id: careerLeave.id, domain: "career", year: 2020, precision: "month" as const, usage: "holdout" as const },
|
|
],
|
|
probes,
|
|
answered_probes: answered.map((probe) => ({
|
|
probe_id: probe.id,
|
|
semantic_key: probe.semantic_key,
|
|
candidate_split_hash: probe.candidate_split_hash,
|
|
answer_class: probe.semantic_key.includes("2024.04") && probe.domain === "career" ? "yes" as const : "no" as const,
|
|
classified_from: "choice" as const,
|
|
})),
|
|
rounds: [],
|
|
last_inference_round: null,
|
|
entropy: 1.4,
|
|
representative_time: "04:53",
|
|
credible_range: ["04:48", "05:07"] as const,
|
|
holdout_passed: null,
|
|
};
|
|
const loaded = asInferenceState(raw);
|
|
assert.ok(loaded);
|
|
return loaded;
|
|
}
|
|
|
|
function eventProbeRow(probe: ConflictProbe) {
|
|
return {
|
|
year: probe.year,
|
|
year_label: probe.year > 0 ? `${probe.year} 年前后` : "",
|
|
domain: probe.domain,
|
|
event_family: probe.domain,
|
|
source: probe.source,
|
|
tracks: ["vimshottari", "narayana"],
|
|
tracks_agree: true,
|
|
unique_minute_claim: false,
|
|
user_meaning: probe.question,
|
|
role: "distinguish",
|
|
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,
|
|
choice_kind: probe.choice_kind,
|
|
style_options: probe.style_options,
|
|
};
|
|
}
|
|
|
|
function accidentDossier(answeredCount: number, extra: {
|
|
activeFocus?: ReturnType<typeof activeFocusFixture> | null;
|
|
} = {}): DecisionDossier {
|
|
const state = liveState(answeredCount);
|
|
const fingerprint = evidenceLedgerFingerprint(EVIDENCE as never);
|
|
return {
|
|
evidence: EVIDENCE,
|
|
conversationSummary: {
|
|
activeFocus: extra.activeFocus
|
|
? {
|
|
id: extra.activeFocus.id,
|
|
intent: extra.activeFocus.intent,
|
|
targetDomain: extra.activeFocus.target_domain,
|
|
targetKind: extra.activeFocus.target_kind,
|
|
expectedAnswerSchema: extra.activeFocus.expected_answer_schema,
|
|
}
|
|
: null,
|
|
declinedSkippedTopics: [{
|
|
target_domain: "other",
|
|
status: "declined",
|
|
intent: "collect_method_evidence",
|
|
questionId: "collect:invite:more",
|
|
target_kind: "invite_more",
|
|
}],
|
|
},
|
|
latestResult: {
|
|
resultId: "55555555-5555-4555-8555-555555555555",
|
|
selectionAllowed: true,
|
|
confirmationAllowed: false,
|
|
evidenceLedgerFingerprint: fingerprint,
|
|
candidates: TIMES.map((time, index) => ({
|
|
candidateId: uuidAt(index),
|
|
time,
|
|
rank: index + 1,
|
|
relativeSupport: SCORES[time] ?? 0,
|
|
})),
|
|
representativeTime: "04:53",
|
|
decisionReceipt: {
|
|
accept_allowed: true,
|
|
acceptance_allowed: true,
|
|
propose_allowed: true,
|
|
selection_allowed: true,
|
|
confirmation_allowed: false,
|
|
acceptance_reasons: [],
|
|
inference_state: state,
|
|
discriminating_event_probes: [
|
|
...ASKED_PROBES.map(eventProbeRow),
|
|
eventProbeRow(LEFTOVER_SAME_YEAR),
|
|
eventProbeRow(D10_STYLE),
|
|
],
|
|
oos_blind_prompts: [],
|
|
},
|
|
},
|
|
case: { acceptedTime: null, status: "collecting_evidence" },
|
|
};
|
|
}
|
|
|
|
function rpcDossier(decision: DecisionDossier, extra: {
|
|
activeFocus?: ReturnType<typeof activeFocusFixture> | null;
|
|
} = {}) {
|
|
const evidence = decision.evidence.map((item) => ({
|
|
id: item.id ?? "e-unknown",
|
|
source_turn_id: TURN_ID,
|
|
subject: "self",
|
|
event_kind: item.eventKind ?? item.domain,
|
|
domain: item.domain,
|
|
occurred_from: item.occurredFrom,
|
|
occurred_to: item.occurredTo,
|
|
date_precision: item.datePrecision,
|
|
summary: item.summary ?? item.domain,
|
|
status: item.status,
|
|
supersedes_evidence_id: null,
|
|
created_at: "2026-09-11T00:00:00.000Z",
|
|
}));
|
|
return dossierFixture({
|
|
evidence,
|
|
evidenceCount: evidence.length,
|
|
latestResult: candidateSnapshotFixture({
|
|
selectionAllowed: true,
|
|
confirmationAllowed: false,
|
|
representativeTime: "04:53",
|
|
evidenceLedgerFingerprint: evidenceLedgerFingerprint(decision.evidence as never),
|
|
candidates: decision.latestResult?.candidates?.map((item, index) => ({
|
|
candidate_id: item.candidateId ?? uuidAt(index),
|
|
time: item.time,
|
|
rank: item.rank ?? index + 1,
|
|
relative_support: Math.max(0, Math.min(100, item.relativeSupport ?? 0)),
|
|
tied_minute_count: 1,
|
|
})) ?? [],
|
|
decisionReceipt: { ...(decision.latestResult?.decisionReceipt ?? {}) },
|
|
}),
|
|
conversationSummary: conversationSummaryFixture({
|
|
activeFocus: extra.activeFocus ?? null,
|
|
declinedSkippedTopics: [...decision.conversationSummary.declinedSkippedTopics],
|
|
}),
|
|
});
|
|
}
|
|
|
|
function idleHandlers(decision: DecisionDossier, extra: {
|
|
activeFocus?: ReturnType<typeof activeFocusFixture> | null;
|
|
throwOnFocus?: boolean;
|
|
} = {}) {
|
|
return fakeAccounting({
|
|
...receiptHandlers,
|
|
get_agentic_rectification_case_dossier: () => rpcDossier(decision, extra),
|
|
get_agentic_rectification_case_compute: () => computeFixture(),
|
|
append_agentic_rectification_turn: () => ({ turn_id: TURN_ID, idempotent: false }),
|
|
apply_agentic_rectification_choice_action: (_fn, args) => ({
|
|
action_id: args.p_action_id,
|
|
status: "applied",
|
|
idempotent: false,
|
|
question_id: args.p_question_id,
|
|
option_id: args.p_option_id,
|
|
probe_id: SIXTH.id,
|
|
revision: Number(args.p_expected_revision) + 1,
|
|
source_quote: args.p_source_quote,
|
|
derived_context: args.p_derived_context,
|
|
narration: args.p_narration,
|
|
focus_status: args.p_focus_status,
|
|
}),
|
|
set_agentic_rectification_conversation_focus: extra.throwOnFocus
|
|
? () => {
|
|
throw new Error("persist skipped");
|
|
}
|
|
: (_fn, args) => ({
|
|
focus: {
|
|
id: FOCUS_ID,
|
|
case_id: CASE_ID,
|
|
question_id: args.p_question_id,
|
|
intent: args.p_intent,
|
|
target_evidence_id: args.p_target_evidence_id,
|
|
target_domain: args.p_target_domain,
|
|
target_kind: args.p_target_kind,
|
|
expected_answer_schema: args.p_expected_answer_schema,
|
|
status: "active",
|
|
asked_at: "2026-09-11T00:00:00.000Z",
|
|
resolved_at: null,
|
|
asked_turn_id: args.p_asked_turn_id ?? null,
|
|
},
|
|
idempotent: false,
|
|
}),
|
|
finalize_agentic_rectification_turn: () => ({ turn_id: TURN_ID, status: "completed", idempotent: false }),
|
|
get_agentic_rectification_turn_receipt: () => null,
|
|
});
|
|
}
|
|
|
|
function warnLines(run: () => Promise<unknown> | unknown) {
|
|
const lines: string[] = [];
|
|
const original = console.warn;
|
|
console.warn = (...args: unknown[]) => {
|
|
lines.push(args.map((item) => String(item)).join(" "));
|
|
original.apply(console, args);
|
|
};
|
|
return Promise.resolve(run()).finally(() => {
|
|
console.warn = original;
|
|
}).then((result) => ({ result, lines }));
|
|
}
|
|
|
|
function followupPlan(dossier: DecisionDossier, sessionOutcome: string) {
|
|
return buildMethodFollowupPlan({
|
|
evidence: dossier.evidence,
|
|
declinedTopics: dossier.conversationSummary.declinedSkippedTopics,
|
|
sessionOutcome: sessionOutcome as never,
|
|
eventProbes: (dossier.latestResult?.decisionReceipt?.discriminating_event_probes ?? []) as never,
|
|
askedProbeKeys: ASKED_PROBES.flatMap((probe) => [probe.id, probe.semantic_key, probe.candidate_split_hash]),
|
|
candidatesSeparated: false,
|
|
topCandidateTimes: [...TIMES],
|
|
birthDate: "1997-08-08",
|
|
});
|
|
}
|
|
|
|
test("T0: sixth dated answer prints persist_status then must deliver a range card", async () => {
|
|
resetDeliveryTurnGuardForTests();
|
|
const dossier = accidentDossier(6);
|
|
const decision = decideFromDossier(dossier, { birthDate: "1997-08-08" });
|
|
const plan = followupPlan(dossier, decision.sessionOutcome);
|
|
const accounting = idleHandlers(dossier);
|
|
const { result: persist, lines } = await warnLines(() => persistServerOwnedFocus({
|
|
accounting: accounting.client,
|
|
userId: USER_ID,
|
|
caseId: CASE_ID,
|
|
activeFocus: null,
|
|
decisionReceipt: dossier.latestResult?.decisionReceipt ?? null,
|
|
followup: plan.next_followup,
|
|
}));
|
|
const persistStatus = (persist as { status: PersistServerFocusStatus }).status;
|
|
console.warn(JSON.stringify({
|
|
event: "t0_probe_pool_exhausted",
|
|
nextAction: decision.nextAction,
|
|
sessionOutcome: decision.sessionOutcome,
|
|
semantic_key: plan.next_followup?.semantic_key ?? null,
|
|
choice_kind: plan.next_followup?.choice_kind ?? null,
|
|
followup_intent: plan.next_followup?.intent ?? null,
|
|
persist_status: persistStatus,
|
|
probe_id: decision.probe?.probeId ?? null,
|
|
probe_year: decision.probe?.year ?? null,
|
|
probe_choiceKind: decision.probe?.choiceKind ?? null,
|
|
probe_semanticKey: decision.probe?.semanticKey ?? null,
|
|
}));
|
|
assert.ok(
|
|
decision.nextAction === "offer_provisional_range"
|
|
|| decision.nextAction === "ready_to_adopt"
|
|
|| decision.nextAction === "complete_with_range",
|
|
`T0 nextAction=${decision.nextAction} persist_status=${persistStatus} next=${plan.next_followup?.semantic_key}`,
|
|
);
|
|
assert.notEqual(plan.next_followup?.choice_kind, "varga_style");
|
|
assert.notEqual(plan.next_followup?.source, "nakshatra_boundary");
|
|
const publicAction = publicNextAction(decision);
|
|
assert.equal(publicAction.can_offer_range, true);
|
|
const nextUser = buildNextUserAction({
|
|
scorableCount: dossier.evidence.length,
|
|
evidenceCount: dossier.evidence.length,
|
|
hasLatestResult: true,
|
|
selectionAllowed: publicAction.can_adopt,
|
|
sessionOutcome: decision.sessionOutcome,
|
|
nextFollowup: plan.next_followup,
|
|
workingTime: decision.representativeTime,
|
|
});
|
|
assert.ok(
|
|
nextUser.id === "offer_provisional_range" || nextUser.id === "adopt_representative",
|
|
nextUser.id,
|
|
);
|
|
const idleAccounting = idleHandlers(dossier);
|
|
const { result: idle } = await warnLines(() => persistNextInterviewIfIdle({
|
|
accounting: idleAccounting.client,
|
|
userId: USER_ID,
|
|
caseId: CASE_ID,
|
|
}));
|
|
const persisted = idle as Awaited<ReturnType<typeof persistNextInterviewIfIdle>>;
|
|
const host = persisted.hostNarration ?? "";
|
|
assert.ok(host.trim(), "answer/idle transaction must leave a carrier");
|
|
assert.match(host, /再收一截|再补|如果还记得/);
|
|
for (const phrase of COLLECT_FLOW_BANNED_PHRASES) {
|
|
if (phrase === "领域") continue;
|
|
assert.equal(host.includes(phrase), false, phrase);
|
|
}
|
|
const delivery = rangeDeliveryForSnapshot({
|
|
decisionReceipt: dossier.latestResult?.decisionReceipt,
|
|
candidates: dossier.latestResult?.candidates,
|
|
representativeTime: decision.representativeTime,
|
|
credibleRange: decision.credibleRange,
|
|
});
|
|
assert.ok((delivery.columns?.length ?? 0) >= 3, JSON.stringify(delivery.columns?.map((item) => item.time)));
|
|
assert.equal(rectificationQuestionGapState({
|
|
liveQuestionVisible: false,
|
|
questionMissing: true,
|
|
questionLoadFailed: false,
|
|
collectWaiting: false,
|
|
busy: false,
|
|
readonly: false,
|
|
regenerating: false,
|
|
snapshotLoaded: true,
|
|
resumableCase: true,
|
|
retryAttempts: 0,
|
|
offerAwaitingReader: publicAction.can_offer_range,
|
|
}), "idle");
|
|
assert.ok(lines.length >= 0);
|
|
});
|
|
|
|
test("T1: the sixth-answer persist transaction delivers a range carrier", async () => {
|
|
resetDeliveryTurnGuardForTests();
|
|
const dossier = accidentDossier(6);
|
|
const decision = decideFromDossier(dossier, { birthDate: "1997-08-08" });
|
|
const accounting = idleHandlers(dossier);
|
|
const next = await persistNextInterviewAfterChoice({
|
|
accounting: accounting.client,
|
|
userId: USER_ID,
|
|
caseId: CASE_ID,
|
|
dossier,
|
|
decisionState: liveState(6),
|
|
nextAction: publicNextAction(decision),
|
|
decision,
|
|
birthDate: "1997-08-08",
|
|
});
|
|
assert.ok(
|
|
decision.nextAction === "offer_provisional_range"
|
|
|| decision.nextAction === "ready_to_adopt"
|
|
|| decision.nextAction === "complete_with_range",
|
|
decision.nextAction,
|
|
);
|
|
assert.equal(decision.canOfferRange, true);
|
|
assert.match(next.hostNarration, /再收一截|再补|如果还记得/);
|
|
assert.doesNotMatch(next.hostNarration, /做不了|才会变|没有拿到下一个问题/);
|
|
assert.equal(next.choiceReady, false);
|
|
});
|
|
|
|
test("T3: skipped discriminator persist still leaves a non-empty delivery carrier", async () => {
|
|
resetDeliveryTurnGuardForTests();
|
|
const dossier = accidentDossier(6);
|
|
const decision = decideFromDossier(dossier, { birthDate: "1997-08-08" });
|
|
const accounting = idleHandlers(dossier, { throwOnFocus: true });
|
|
const { result, lines } = await warnLines(() => persistNextInterviewAfterChoice({
|
|
accounting: accounting.client,
|
|
userId: USER_ID,
|
|
caseId: CASE_ID,
|
|
dossier,
|
|
decisionState: liveState(6),
|
|
nextAction: publicNextAction(decision),
|
|
decision,
|
|
birthDate: "1997-08-08",
|
|
}));
|
|
const next = result as Awaited<ReturnType<typeof persistNextInterviewAfterChoice>>;
|
|
assert.ok((next.hostNarration ?? "").trim());
|
|
assert.equal(decision.canOfferRange, true);
|
|
const skipped = lines.find((line) => line.includes("rectification_discriminator_persist_skipped"));
|
|
const skippedDirect = await persistServerOwnedFocus({
|
|
accounting: accounting.client,
|
|
userId: USER_ID,
|
|
caseId: CASE_ID,
|
|
activeFocus: null,
|
|
decisionReceipt: dossier.latestResult?.decisionReceipt ?? null,
|
|
followup: {
|
|
method_id: "d10_career",
|
|
intent: "distinguish_candidates",
|
|
ask_theme: "career_style",
|
|
domain: "career",
|
|
kind_hint: null,
|
|
user_prompt_hint: "ask",
|
|
must_not_label: false,
|
|
choice_frame: null,
|
|
source: "event_probe",
|
|
semantic_key: D10_STYLE.semantic_key,
|
|
choice_kind: "varga_style",
|
|
},
|
|
});
|
|
assert.equal(skippedDirect.status, "skipped");
|
|
assert.ok(skipped || skippedDirect.status === "skipped");
|
|
});
|