Files
Jyotisha/frontend/tests/rectification-walkthrough-polish.test.ts
T
Jesse_Chen aab3ad4b84
Independent Staging Quality Gate / validate (push) Successful in 7m44s
Independent Staging Quality Gate / publish (push) Successful in 1m48s
fix(rectification): keep choice stems visible and align overlay adopt with the engine
Walkthrough polish: fail-closed empty D9 prompts, stop asserting the next question is on screen, vary same-domain collect copy, switch to reverse-verify after adopt, and let coverage route interview without blocking can_adopt.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-02 13:11:22 +08:00

603 lines
21 KiB
TypeScript

import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import test from "node:test";
import { buildCandidateContrastPacket } from "../src/lib/rectification-agentic/core/candidate-contrast-packet.ts";
import { decideRectification } from "../src/lib/rectification-agentic/core/rectification-decision.ts";
import {
USER_COLLECT_QUESTION,
USER_COLLECT_QUESTION_RETRY,
} from "../src/lib/rectification-agentic/user-copy.ts";
import { isStalePreAdoptFocus } from "../src/lib/rectification-agentic/v9/answer-choice.ts";
import {
buildChoiceFrame,
parseRectificationChoiceCard,
serverOwnedChoiceCopy,
} from "../src/lib/rectification-agentic/v9/choice-card.ts";
import { choiceCardFromCaseDossier } from "../src/lib/rectification-agentic/v9/interview-state.ts";
import {
buildMethodFollowupPlan,
buildNextUserAction,
spokenFollowupForUser,
type MethodFollowup,
} from "../src/lib/rectification-agentic/v9/method-followup.ts";
import { completeStyleOptions } from "../src/lib/rectification-agentic/v9/probe-question-contract.ts";
import {
openQuestionFromPersistedFocus,
persistServerOwnedFocus,
} from "../src/lib/rectification-agentic/v9/server-focus.ts";
import { projectCurrentQuestion } from "../src/lib/rectification-agentic/v9/turn-decision.ts";
import { d9StyleLabel } from "../src/lib/rectification-agentic/v9/varga-type-tables.ts";
import {
CASE_ID,
FOCUS_ID,
OPEN_ENGINE_CAPABILITY_CEILING,
USER_ID,
fakeAccounting,
} from "./rectification-v9-test-support.ts";
const D9_QUESTION = "亲密关系里,你更接近哪一种相处方式?";
const WALKTHROUGH_EVIDENCE = [
{
status: "confirmed" as const,
domain: "education",
datePrecision: "year" as const,
occurredFrom: "2016-01-01",
occurredTo: null,
eventKind: "education_start",
},
{
status: "confirmed" as const,
domain: "relationship",
datePrecision: "year" as const,
occurredFrom: "2018-01-01",
occurredTo: null,
eventKind: "relationship_start",
},
{
status: "confirmed" as const,
domain: "career",
datePrecision: "year" as const,
occurredFrom: "2019-01-01",
occurredTo: null,
eventKind: "career_entry",
},
{
status: "confirmed" as const,
domain: "family",
datePrecision: "year" as const,
occurredFrom: "2020-01-01",
occurredTo: null,
eventKind: "family_event",
},
];
function d9WalkthroughPacket() {
return buildCandidateContrastPacket({
candidateSetVersion: "05:00-05:04",
candidateTimes: ["05:00", "05:04"],
transitions: [
{ layer: "d9", at: "05:04", from_sign: "巨蟹座", to_sign: "狮子座" },
],
});
}
function d9StyleOptions() {
const completed = completeStyleOptions({
choiceKind: "varga_style",
styleOptions: [
{ sign: "巨蟹座", label: d9StyleLabel("巨蟹座"), answer_class: "yes" },
{ sign: "狮子座", label: d9StyleLabel("狮子座"), answer_class: "weak_yes" },
],
});
assert.equal(completed.ok, true);
if (!completed.ok) throw new Error("D9 style options");
return completed.options;
}
function d9Followup(frame: NonNullable<ReturnType<typeof buildChoiceFrame>>): MethodFollowup {
const probe = d9WalkthroughPacket().probes.find((item) => item.semanticKey.startsWith("varga.d9."));
assert.ok(probe);
return {
method_id: "d9_relationship",
intent: "distinguish_candidates",
ask_theme: "relationship_style",
domain: "relationship",
kind_hint: "relationship_change",
user_prompt_hint: probe.question,
must_not_label: false,
choice_frame: frame,
source: "event_probe",
information_gain: probe.informationGain,
semantic_key: probe.semanticKey,
candidate_split_hash: probe.candidateSplitHash,
choice_kind: "varga_style",
candidate_ids: ["05:00", "05:04"],
expected_outcomes: probe.expectedOutcomes.map((row) => ({
answer_class: row.outcomeId,
supports: row.supportsCandidateIds,
conflicts: row.conflictsCandidateIds,
})),
style_options: d9StyleOptions(),
};
}
function focusFromArgs(args: Record<string, unknown>) {
return {
id: FOCUS_ID,
caseId: CASE_ID,
questionId: String(args.p_question_id ?? ""),
intent: String(args.p_intent ?? ""),
targetEvidenceId: null,
targetDomain: typeof args.p_target_domain === "string" ? args.p_target_domain : null,
targetKind: null,
expectedAnswerSchema: (args.p_expected_answer_schema ?? {}) as Record<string, unknown>,
status: "active" as const,
askedAt: "2026-09-02T00:00:00.000Z",
resolvedAt: null,
};
}
function persistAccounting() {
return fakeAccounting({
set_agentic_rectification_conversation_focus: (_fn, args) => ({
id: FOCUS_ID,
case_id: CASE_ID,
question_id: args.p_question_id,
intent: args.p_intent,
target_evidence_id: null,
target_domain: args.p_target_domain,
target_kind: args.p_target_kind,
expected_answer_schema: args.p_expected_answer_schema,
status: "active",
asked_at: "2026-09-02T00:00:00.000Z",
resolved_at: null,
idempotent: false,
}),
});
}
function assertChoiceInvariants(input: {
card: ReturnType<typeof parseRectificationChoiceCard> | ReturnType<typeof choiceCardFromCaseDossier>;
question: ReturnType<typeof projectCurrentQuestion>;
}) {
if (input.card) {
assert.ok(input.card.prompt?.trim(), "choice_card nonempty ⇒ prompt nonempty");
}
if (input.question?.kind === "choice") {
assert.ok(input.question.prompt?.trim(), "active choice focus ⇒ projectCurrentQuestion prompt nonempty");
}
}
test("walkthrough D9 probe question is in the 4-80 window and must survive frame assembly", () => {
const packet = d9WalkthroughPacket();
const probe = packet.probes.find((item) => item.semanticKey.startsWith("varga.d9."));
assert.ok(probe);
assert.equal(probe.question, D9_QUESTION);
assert.ok(probe.question.length >= 4 && probe.question.length <= 80);
const plan = buildMethodFollowupPlan({
evidence: WALKTHROUGH_EVIDENCE,
contrastPacket: packet,
sessionOutcome: "discriminate_candidates",
topCandidateTimes: ["05:00", "05:04"],
candidatesSeparated: false,
});
const followup = plan.next_followup;
assert.equal(followup?.intent, "distinguish_candidates", JSON.stringify({
intent: followup?.intent,
source: followup?.source,
method_id: followup?.method_id,
dropped: plan.dropped_probes,
}));
assert.equal(followup?.method_id, "d9_relationship");
const frame = followup?.choice_frame ?? null;
assert.ok(frame, "D9 probe must assemble a choice_frame");
assert.equal(frame?.prompt, D9_QUESTION);
const copy = frame ? serverOwnedChoiceCopy(frame) : null;
assert.ok(copy?.prompt, "assembled D9 copy must keep the probe question");
});
test("D9 probe shape with unwired prompt makes the choice invariants red", async () => {
const packet = d9WalkthroughPacket();
const probe = packet.probes.find((item) => item.semanticKey.startsWith("varga.d9."));
assert.ok(probe);
const wired = buildChoiceFrame({
method_id: "d9_relationship",
ask_theme: "relationship_style",
domain: "relationship",
user_prompt_hint: probe.question,
choice_kind: "varga_style",
semantic_key: probe.semanticKey,
style_options: d9StyleOptions(),
}, {
evidence: WALKTHROUGH_EVIDENCE,
probes: [{
year: 0,
year_label: "当前这几个候选",
domain: "relationship",
event_family: "相处方式更接近其中一种",
source: "dasha_activation",
tracks: ["vimshottari", "narayana"],
tracks_agree: true,
unique_minute_claim: false,
user_meaning: probe.question,
role: "distinguish",
information_gain: probe.informationGain,
semantic_key: probe.semanticKey,
candidate_split_hash: probe.candidateSplitHash,
candidate_ids: ["05:00", "05:04"],
expected_outcomes: probe.expectedOutcomes.map((row) => ({
answer_class: row.outcomeId,
supports: row.supportsCandidateIds,
conflicts: row.conflictsCandidateIds,
})),
choice_kind: "varga_style",
style_options: d9StyleOptions(),
}],
});
assert.ok(wired);
const unwired = { ...wired, prompt: "", period: "" };
assert.equal(serverOwnedChoiceCopy(unwired), null);
const accounting = persistAccounting();
const persisted = await persistServerOwnedFocus({
accounting: accounting.client,
userId: USER_ID,
caseId: CASE_ID,
activeFocus: null,
decisionReceipt: null,
followup: d9Followup(unwired),
});
const open = openQuestionFromPersistedFocus(persisted);
const question = persisted.focus
? projectCurrentQuestion(persisted.focus)
: null;
const card = persisted.focus
? choiceCardFromCaseDossier({
evidence: WALKTHROUGH_EVIDENCE,
conversationSummary: {
activeFocus: persisted.focus,
declinedSkippedTopics: [],
},
latestResult: {
decisionReceipt: null,
selectionAllowed: false,
confirmationAllowed: false,
candidates: [
{ time: "05:00", rank: 1, relativeSupport: 0.6 },
{ time: "05:04", rank: 2, relativeSupport: 0.4 },
],
},
case: { acceptedTime: null, status: "collecting_evidence" },
})
: null;
assertChoiceInvariants({ card, question });
assert.ok(persisted.status === "created" || persisted.status === "already_open");
assert.equal(open?.kind, "collect_spoken");
assert.ok(open?.prompt?.trim());
assert.notEqual(question?.kind, "choice");
});
test("agent persistPlanFocus path must fail-closed when D9 choice copy is null", () => {
const tools = readFileSync(new URL("../src/mastra/rectification-v9-tools.ts", import.meta.url), "utf8");
const persistPlan = tools.slice(tools.indexOf("const persistPlanFocus"));
assert.match(persistPlan, /spokenCollectFallbackFollowup/);
});
test("wired D9 persist keeps a nonempty choice prompt", async () => {
const packet = d9WalkthroughPacket();
const probe = packet.probes.find((item) => item.semanticKey.startsWith("varga.d9."));
assert.ok(probe);
const frame = buildChoiceFrame({
method_id: "d9_relationship",
ask_theme: "relationship_style",
domain: "relationship",
user_prompt_hint: probe.question,
choice_kind: "varga_style",
semantic_key: probe.semanticKey,
style_options: d9StyleOptions(),
}, {
evidence: WALKTHROUGH_EVIDENCE,
probes: [{
year: 0,
year_label: "当前这几个候选",
domain: "relationship",
event_family: "相处方式更接近其中一种",
source: "dasha_activation",
tracks: ["vimshottari", "narayana"],
tracks_agree: true,
unique_minute_claim: false,
user_meaning: probe.question,
role: "distinguish",
information_gain: probe.informationGain,
semantic_key: probe.semanticKey,
candidate_split_hash: probe.candidateSplitHash,
candidate_ids: ["05:00", "05:04"],
expected_outcomes: probe.expectedOutcomes.map((row) => ({
answer_class: row.outcomeId,
supports: row.supportsCandidateIds,
conflicts: row.conflictsCandidateIds,
})),
choice_kind: "varga_style",
style_options: d9StyleOptions(),
}],
});
assert.ok(frame);
const persisted = await persistServerOwnedFocus({
accounting: persistAccounting().client,
userId: USER_ID,
caseId: CASE_ID,
activeFocus: null,
decisionReceipt: null,
followup: d9Followup(frame),
});
const open = openQuestionFromPersistedFocus(persisted);
const question = persisted.focus ? projectCurrentQuestion(persisted.focus) : null;
const card = parseRectificationChoiceCard({
question_id: persisted.focus?.questionId,
method_id: "d9_relationship",
prompt: persisted.prompt,
choice_mode: "A/B/C/D",
options: [
{ key: "A", label: d9StyleOptions()[0]!.label, answer_class: "yes", role: "primary" },
{ key: "B", label: d9StyleOptions()[1]!.label, answer_class: "weak_yes", role: "primary" },
{ key: "C", label: d9StyleOptions()[2]!.label, answer_class: "no", role: "primary" },
{ key: "D", label: d9StyleOptions()[3]!.label, answer_class: "unsure", role: "primary" },
],
stop_label: "先这样,先看当前范围",
stop_message: "先这样",
scoring: true,
focus_id: FOCUS_ID,
});
assert.equal(persisted.status, "created");
assert.equal(open?.kind, "choice");
assert.equal(open?.prompt, D9_QUESTION);
assertChoiceInvariants({ card, question });
});
test("unwired D9 persist falls back to spoken collect instead of an empty-prompt card", async () => {
const packet = d9WalkthroughPacket();
const probe = packet.probes.find((item) => item.semanticKey.startsWith("varga.d9."));
assert.ok(probe);
const wired = buildChoiceFrame({
method_id: "d9_relationship",
ask_theme: "relationship_style",
domain: "relationship",
user_prompt_hint: probe.question,
choice_kind: "varga_style",
semantic_key: probe.semanticKey,
style_options: d9StyleOptions(),
}, {
evidence: WALKTHROUGH_EVIDENCE,
probes: [{
year: 0,
year_label: "当前这几个候选",
domain: "relationship",
event_family: "相处方式更接近其中一种",
source: "dasha_activation",
tracks: ["vimshottari", "narayana"],
tracks_agree: true,
unique_minute_claim: false,
user_meaning: probe.question,
role: "distinguish",
information_gain: probe.informationGain,
semantic_key: probe.semanticKey,
candidate_split_hash: probe.candidateSplitHash,
candidate_ids: ["05:00", "05:04"],
expected_outcomes: probe.expectedOutcomes.map((row) => ({
answer_class: row.outcomeId,
supports: row.supportsCandidateIds,
conflicts: row.conflictsCandidateIds,
})),
choice_kind: "varga_style",
style_options: d9StyleOptions(),
}],
});
assert.ok(wired);
const persisted = await persistServerOwnedFocus({
accounting: persistAccounting().client,
userId: USER_ID,
caseId: CASE_ID,
activeFocus: null,
decisionReceipt: null,
followup: d9Followup({ ...wired, prompt: "", period: "" }),
});
const open = openQuestionFromPersistedFocus(persisted);
const question = persisted.focus ? projectCurrentQuestion(persisted.focus) : null;
assert.ok(persisted.status === "created" || persisted.status === "already_open");
assert.equal(open?.kind, "collect_spoken");
assert.ok(open?.prompt?.trim());
assert.notEqual(question?.kind, "choice");
assertChoiceInvariants({ card: null, question });
});
test("agent instructions do not assert that the next question is already on screen", () => {
const agent = readFileSync(new URL("../src/mastra/agentic-rectification.ts", import.meta.url), "utf8");
const chat = readFileSync(new URL("../src/components/rectification-agentic-chat.tsx", import.meta.url), "utf8");
assert.match(agent, /正文不得断言界面当前状态/);
assert.match(agent, /接下来我们继续/);
assert.doesNotMatch(agent, /自然过渡到界面上的下一步/);
assert.match(chat, /await loadCaseSnapshot\(\)/);
assert.match(chat, /isLatestMessage/);
});
test("same-domain collect retry uses structured closed-focus state, not body matching", () => {
const base = {
method_id: "d9_relationship" as const,
intent: "collect_method_evidence",
ask_theme: "relationship_style" as const,
domain: "relationship",
kind_hint: "relationship_start",
user_prompt_hint: "ask",
must_not_label: false as const,
choice_frame: null,
source: "method_coverage" as const,
};
const first = spokenFollowupForUser(base);
const retry = spokenFollowupForUser({ ...base, collect_retry: true });
assert.equal(first, USER_COLLECT_QUESTION.relationship);
assert.equal(retry, USER_COLLECT_QUESTION_RETRY.relationship);
assert.notEqual(first, retry);
const plan = buildMethodFollowupPlan({
evidence: [{
status: "confirmed",
domain: "career",
datePrecision: "year",
occurredFrom: "2019-01-01",
occurredTo: null,
eventKind: "career_entry",
}],
closedCollectFocuses: [{
target_domain: "relationship",
intent: "collect_method_evidence",
status: "resolved",
question_id: "collect:relationship:collect_method_evidence",
}],
sessionOutcome: "collect_evidence",
});
assert.equal(plan.next_followup?.domain, "relationship");
assert.equal(plan.next_followup?.collect_retry, true);
assert.equal(spokenFollowupForUser(plan.next_followup), USER_COLLECT_QUESTION_RETRY.relationship);
const declined = buildMethodFollowupPlan({
evidence: [{
status: "confirmed",
domain: "career",
datePrecision: "year",
occurredFrom: "2019-01-01",
occurredTo: null,
eventKind: "career_entry",
}],
declinedTopics: [{
target_domain: "relationship",
intent: "collect_method_evidence",
status: "declined",
}],
sessionOutcome: "collect_evidence",
});
assert.notEqual(declined.next_followup?.domain, "relationship");
});
test("accepted time replaces leftover collect with reverse_verify or consultation handoff", () => {
assert.equal(isStalePreAdoptFocus("04:53", { intent: "collect_method_evidence" }), true);
assert.equal(isStalePreAdoptFocus("04:53", { intent: "distinguish_candidates" }), true);
assert.equal(isStalePreAdoptFocus("04:53", { intent: "reverse_verify" }), false);
assert.equal(isStalePreAdoptFocus(null, { intent: "collect_method_evidence" }), false);
const idle = readFileSync(new URL("../src/lib/rectification-agentic/v9/answer-choice.ts", import.meta.url), "utf8");
const accept = readFileSync(new URL("../src/app/api/rectification/cases/[caseId]/candidates/accept/route.ts", import.meta.url), "utf8");
assert.match(idle, /isStalePreAdoptFocus/);
assert.match(idle, /resolveV10ConversationFocus/);
assert.match(accept, /persistNextInterviewIfIdle/);
const leftover = {
intent: "collect_method_evidence",
targetDomain: "finance",
targetKind: null,
expectedAnswerSchema: { collect: true, prompt: USER_COLLECT_QUESTION.finance },
};
const plan = buildMethodFollowupPlan({
evidence: WALKTHROUGH_EVIDENCE,
activeFocus: leftover,
accepted: true,
eventProbes: [{
year: 2021,
year_label: "2021 年前后",
domain: "finance",
event_family: "收入或债务明显变化",
source: "dasha_activation",
tracks: ["vimshottari", "narayana"],
tracks_agree: true,
unique_minute_claim: false,
user_meaning: "2021 年前后财务变化",
role: "distinguish",
information_gain: 0.4,
semantic_key: "finance.2021",
candidate_split_hash: "finance:2021",
candidate_ids: ["04:53", "05:00"],
expected_outcomes: [
{ answer_class: "yes", supports: ["04:53"], conflicts: ["05:00"] },
{ answer_class: "no", supports: ["05:00"], conflicts: ["04:53"] },
],
}],
});
assert.equal(plan.next_followup?.intent, "reverse_verify");
const action = buildNextUserAction({
scorableCount: 4,
evidenceCount: 4,
hasLatestResult: true,
selectionAllowed: true,
sessionOutcome: "adopt_representative",
nextFollowup: plan.next_followup,
workingTime: "04:53",
accepted: true,
});
assert.equal(action.id, "verify_adopted_time");
const empty = buildMethodFollowupPlan({
evidence: WALKTHROUGH_EVIDENCE,
activeFocus: leftover,
accepted: true,
eventProbes: [],
});
assert.equal(empty.next_followup, null);
assert.equal(buildNextUserAction({
scorableCount: 4,
evidenceCount: 4,
hasLatestResult: true,
selectionAllowed: true,
sessionOutcome: "adopt_representative",
nextFollowup: empty.next_followup,
workingTime: "04:53",
accepted: true,
}).id, "start_consultation");
});
test("coverage does not block overlay can_adopt when the engine allows accept", () => {
const offered = decideRectification({
engineCeiling: {
acceptanceAllowed: true,
selectionAllowed: true,
proposeAllowed: true,
confirmationAllowed: false,
},
methodCoverageAll: false,
trainingGateOpen: true,
candidateScores: [
{ time: "04:53", score: 34 },
{ time: "04:51", score: 33 },
{ time: "04:47", score: 33 },
],
discriminatorProbe: null,
});
assert.equal(offered.canAdopt, true);
assert.equal(offered.canConfirmExactMinute, false);
const refused = decideRectification({
engineCeiling: {
acceptanceAllowed: false,
selectionAllowed: false,
proposeAllowed: false,
confirmationAllowed: false,
},
methodCoverageAll: true,
trainingGateOpen: true,
candidateScores: [
{ time: "04:53", score: 34 },
{ time: "04:51", score: 33 },
{ time: "04:47", score: 33 },
],
discriminatorProbe: null,
});
assert.equal(refused.canAdopt, false);
assert.equal(refused.canConfirmExactMinute, false);
const trainingClosed = decideRectification({
engineCeiling: OPEN_ENGINE_CAPABILITY_CEILING,
methodCoverageAll: true,
trainingGateOpen: false,
candidateScores: [
{ time: "04:53", score: 34 },
{ time: "04:51", score: 33 },
],
discriminatorProbe: null,
});
assert.equal(trainingClosed.canAdopt, false);
});