Files
Jyotisha/frontend/tests/rectification-eight-method.test.ts
T
Jesse_Chen ca4e2408d8 fix(rectification): keep collecting while dated evidence domains remain open
Separated candidates were treated as adoptable while finance, relocation, health, or occupation collect was still pending, so clickable cards and a verification report appeared before those questions were asked.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-05 21:06:38 +08:00

3311 lines
128 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import test from "node:test";
import { buildMethodFollowupPlan, buildNextUserAction, conversationalSessionOutcome, isOfferBlockingFollowup, spokenFollowupForUser, blockingMethodsCovered } from "../src/lib/rectification-agentic/v9/method-followup.ts";
import { trainingScoreableGate } from "../src/lib/rectification-agentic/v9/evidence-model.ts";
import {
mentionedVargaKeysFromLedgerEvidence,
buildCandidateContrastPacket,
} from "../src/lib/rectification-agentic/core/candidate-contrast-packet.ts";
import {
internalObservationsFromWindowScan,
parseWindowScan,
} from "../src/lib/rectification-agentic/v9/varga-observations.ts";
import { WINDOW_SCAN_DISPLAY_LAYER_ORDER } from "../src/lib/rectification-agentic/v9/refinement-packet.ts";
import { USER_COLLECT_QUESTION } from "../src/lib/rectification-agentic/user-copy.ts";
import { RECTIFICATION_SKILL_VERSION } from "../src/lib/rectification-agentic/v9/case-status.ts";
import { buildInferenceState } from "../src/lib/rectification-agentic/core/build-state.ts";
import { decideRectification } from "../src/lib/rectification-agentic/core/rectification-decision.ts";
import { readVedastroMinuteSensitiveStatus } from "../src/lib/rectification-agentic/v9/confirmation-gate.ts";
import { authoritativeCandidateProjection } from "../src/lib/rectification-agentic/v9/inference-adapter.ts";
import { createRectificationV9Tools, latestResultToolProjection } from "../src/mastra/rectification-v9-tools.ts";
import { PUBLIC_RECTIFICATION_TOOLS } from "../src/lib/rectification-agentic/v9/public-receipt.ts";
import {
candidateRangeFingerprint,
evidenceLedgerFingerprint,
parseV9CaseDossier,
RectificationToolServiceError,
} from "../src/lib/rectification-agentic/v9/tool-service.ts";
import {
resolveExactSkillPackage,
} from "../src/lib/skill-package-registry.ts";
import {
OPEN_ENGINE_CAPABILITY_CEILING,
CASE_ID,
CANDIDATE_ID,
FOCUS_ID,
RESULT_ID,
SECOND_CANDIDATE_ID,
TURN_ID,
USER_ID,
candidateSnapshotFixture,
computeFixture,
conversationSummaryFixture,
dossierFixture,
activeFocusFixture,
fakeAccounting,
receiptHandlers,
} from "./rectification-v9-test-support.ts";
const THIRD_CANDIDATE_ID = "88888888-8888-4888-8888-888888888883";
const EDUCATION_ID = "44444444-4444-4444-8444-444444444441";
const RELATIONSHIP_ID = "44444444-4444-4444-8444-444444444442";
const FAMILY_ID = "44444444-4444-4444-8444-444444444443";
const CAREER_ID = "44444444-4444-4444-8444-444444444444";
const UNIQUE_MINUTE_COPY = /±5 分钟确定性/;
// 本单回退 3847e9c9 把训练门未开的下一问改成家人的断言。见各断言旁三栏。
function collectDecision(candidates: readonly { time: string; relativeSupport?: number }[]) {
return decideRectification({
engineCeiling: OPEN_ENGINE_CAPABILITY_CEILING,
methodCoverageAll: false,
trainingGateOpen: false,
candidateScores: candidates.map((item) => ({ time: item.time, score: item.relativeSupport ?? 0 })),
});
}
function producedInferenceState(
candidates: readonly Readonly<{ id: string; time: string; relative_support: number }>[],
) {
const times = candidates.map((item) => item.time).sort();
return buildInferenceState({
range_start: times[0]!,
range_end: times[times.length - 1]!,
candidates: candidates.map((item) => ({
id: item.id,
time: item.time,
relative_support: item.relative_support,
})),
events: [],
probes: [],
});
}
const DYNAMIC_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 CLASSIC_COVERAGE = [
{ status: "confirmed", domain: "education", datePrecision: "year" as const, occurredFrom: "2016-01-01", occurredTo: null },
{ status: "confirmed", domain: "relationship", datePrecision: "year" as const, occurredFrom: "2018-01-01", occurredTo: null },
{ status: "confirmed", domain: "career", datePrecision: "year" as const, occurredFrom: "2019-01-01", occurredTo: null },
{ status: "confirmed", domain: "family", datePrecision: "year" as const, occurredFrom: "2020-01-01", occurredTo: null },
{ status: "confirmed", domain: "appearance", datePrecision: "unknown" as const, occurredFrom: null, occurredTo: null },
{ status: "confirmed", domain: "marks", datePrecision: "unknown" as const, occurredFrom: null, occurredTo: null },
{ status: "confirmed", domain: "occupation", datePrecision: "unknown" as const, occurredFrom: null, occurredTo: null },
{ status: "confirmed", domain: "horary", datePrecision: "day" as const, occurredFrom: "2024-01-01", occurredTo: null },
];
function datedEvidence(
domain: string,
year: string,
extra: {
eventKind?: string | null;
summary?: string | null;
datePrecision?: "year" | "month" | "day";
} = {},
) {
return {
status: "confirmed" as const,
domain,
datePrecision: extra.datePrecision ?? ("year" as const),
occurredFrom: `${year}-01-01`,
occurredTo: null,
...(extra.eventKind !== undefined ? { eventKind: extra.eventKind } : {}),
...(extra.summary !== undefined ? { summary: extra.summary } : {}),
};
}
const CAREER_CONFLICT_PROBE = {
year: 2018,
year_label: "2018 年前后",
domain: "career" as const,
event_family: "入职、升职或职责明显加重",
source: "dasha_activation" as const,
tracks: ["vimshottari", "narayana"] as const,
tracks_agree: true,
unique_minute_claim: false as const,
user_meaning: "年份锁定 2018 年前后。请写成一句自然语言,问是否入职或职责加重。",
role: "distinguish" as const,
phase: "candidate_discriminator" as const,
information_gain: 0.21,
semantic_key: "career.2018.dasha_activation",
candidate_set_version: "set-test",
candidate_split_hash: "set-test:career:2018",
candidate_ids: ["05:00", "05:20"],
expected_outcomes: [
{ answer_class: "yes", supports: ["05:00"], conflicts: ["05:20"] },
{ answer_class: "no", supports: ["05:20"], conflicts: ["05:00"] },
],
style_options: DYNAMIC_STYLE_OPTIONS,
};
const EDUCATION_QUALITY_PROBE = {
year: 2016,
year_label: "2016 年前后",
domain: "education" as const,
event_family: "高考或重要考试发挥明显失常、压力很大",
source: "known_event_quality" as const,
tracks: ["vimshottari", "narayana"] as const,
tracks_agree: true,
unique_minute_claim: false as const,
user_meaning: "年份锁定 2016 年前后。已有高考或考试经历。请写成一句自然语言,问那次是否发挥失常或压力特别大。",
role: "clarify" as const,
phase: "event_clarification" as const,
choice_kind: "event_quality" as const,
information_gain: 0,
semantic_key: "education.2016",
style_options: DYNAMIC_STYLE_OPTIONS,
};
const CAREER_QUALITY_PROBE = {
year: 2020,
year_label: "2020 年前后",
domain: "career" as const,
event_family: "入职、升职或职责明显加重",
source: "known_event_quality" as const,
tracks: ["vimshottari", "narayana"] as const,
tracks_agree: true,
unique_minute_claim: false as const,
user_meaning: "年份锁定 2020 年前后。已有相关经历。请写成一句自然语言,问入职、升职或职责明显加重有没有发生过。不得改年份。",
role: "clarify" as const,
phase: "event_clarification" as const,
choice_kind: "event_quality" as const,
information_gain: 0,
semantic_key: "career.2020",
style_options: DYNAMIC_STYLE_OPTIONS,
};
const ENGINE_SCORE = {
success: true,
endpoint: "rectification_v5_score",
result_id: "e4fbf2e0-85dc-5b42-a5a3-34e5dd4b7e62",
algorithm_version: "rectification-event-contract-v2",
event_contract_version: "rectification-event-contract-v2",
decision_policy_version: "rectification-candidate-policy-v2",
execution_ledger_version: "rectification-execution-ledger-v2",
candidate_decisions: [
{ candidate_id: CANDIDATE_ID, time: "04:50", rank: 1, relative_support: 57, tied_minute_count: 1 },
{ candidate_id: SECOND_CANDIDATE_ID, time: "04:51", rank: 2, relative_support: 25, tied_minute_count: 2 },
{ candidate_id: THIRD_CANDIDATE_ID, time: "04:52", rank: 3, relative_support: 18, tied_minute_count: 2 },
],
decision_receipt: {
receipt_version: "candidate-decision-receipt-v2",
contract_version: "v2",
event_contract_version: "rectification-event-contract-v2",
policy_version: "rectification-candidate-policy-v2",
decision_policy_version: "rectification-candidate-policy-v2",
display_allowed: true,
selection_allowed: true,
acceptance_allowed: true,
propose_allowed: true,
confirmation_allowed: false,
accept_allowed: true,
confirm_allowed: false,
representative_candidate_id: CANDIDATE_ID,
representative_time: "04:50",
overall_confidence: "high",
margin_percent: 42.5,
},
execution_ledger: [
{ ledger_version: "rectification-execution-ledger-v2", stage: "technique_layer", method: "d1-rashi", status: "executed", source: "python-engine" },
{ ledger_version: "rectification-execution-ledger-v2", stage: "technique_layer", method: "d9-navamsa", status: "executed", source: "python-engine" },
],
diagnostics: {
window_scan: {
scanned: true,
confirmation_allowed: false,
unique_minute_claim: false,
d9_lagna_count: 2,
d10_lagna_count: 1,
d9_candidates_differ: true,
d10_candidates_differ: false,
d9_sign_names: ["白羊座", "天蝎"],
},
},
};
const educationEvidence = {
id: EDUCATION_ID,
source_turn_id: TURN_ID,
subject: "self",
event_kind: "education_milestone",
domain: "education",
occurred_from: "2016-06-01",
occurred_to: null,
date_precision: "month",
summary: "2016年6月一次学业节点",
status: "confirmed",
supersedes_evidence_id: null,
created_at: "2026-08-12T10:00:06.000Z",
};
const relationshipEvidence = {
id: RELATIONSHIP_ID,
source_turn_id: TURN_ID,
subject: "self",
event_kind: "relationship_start",
domain: "relationship",
occurred_from: "2018-01-01",
occurred_to: null,
date_precision: "year",
summary: "2018年一段感情开始",
status: "confirmed",
supersedes_evidence_id: null,
created_at: "2026-08-12T10:00:07.000Z",
};
const familyEvidence = {
id: FAMILY_ID,
source_turn_id: TURN_ID,
subject: "family",
event_kind: "family_event",
domain: "family",
occurred_from: "2020-01-01",
occurred_to: null,
date_precision: "year",
summary: "2020年家人相关变化",
status: "confirmed",
supersedes_evidence_id: null,
created_at: "2026-08-12T10:00:08.000Z",
};
const careerEvidence = {
id: CAREER_ID,
source_turn_id: TURN_ID,
subject: "self",
event_kind: "career_entry",
domain: "career",
occurred_from: "2019-07-01",
occurred_to: null,
date_precision: "year",
summary: "2019年开始工作",
status: "confirmed",
supersedes_evidence_id: null,
created_at: "2026-08-12T10:00:09.000Z",
};
const methodCoverageTieEvidence = [
{ ...educationEvidence, domain: "education" },
{
id: "44444444-4444-4444-8444-444444444442",
source_turn_id: TURN_ID,
subject: "self",
event_kind: "relationship_start",
domain: "relationship",
occurred_from: "2018-01-01",
occurred_to: null,
date_precision: "year",
summary: "感情变化",
status: "confirmed",
supersedes_evidence_id: null,
created_at: "2026-08-12T10:00:07.000Z",
},
{
id: "44444444-4444-4444-8444-444444444443",
source_turn_id: TURN_ID,
subject: "self",
event_kind: "career_entry",
domain: "career",
occurred_from: "2019-01-01",
occurred_to: null,
date_precision: "year",
summary: "工作变化",
status: "confirmed",
supersedes_evidence_id: null,
created_at: "2026-08-12T10:00:08.000Z",
},
{
id: "44444444-4444-4444-8444-444444444446",
source_turn_id: TURN_ID,
subject: "self",
event_kind: "family_event",
domain: "family",
occurred_from: "2020-01-01",
occurred_to: null,
date_precision: "year",
summary: "家人变化",
status: "confirmed",
supersedes_evidence_id: null,
created_at: "2026-08-12T10:00:09.000Z",
},
{
id: "44444444-4444-4444-8444-444444444445",
source_turn_id: TURN_ID,
subject: "self",
event_kind: "occupation_note",
domain: "occupation",
occurred_from: null,
occurred_to: null,
date_precision: "unknown",
summary: "长期一直是程序员",
status: "confirmed",
supersedes_evidence_id: null,
created_at: "2026-08-12T10:00:10.000Z",
},
{
id: "44444444-4444-4444-8444-444444444447",
source_turn_id: TURN_ID,
subject: "self",
event_kind: "income_change",
domain: "finance",
occurred_from: "2017-01-01",
occurred_to: null,
date_precision: "year",
summary: "收入变化",
status: "confirmed",
supersedes_evidence_id: null,
created_at: "2026-08-12T10:00:11.000Z",
},
{
id: "44444444-4444-4444-8444-444444444448",
source_turn_id: TURN_ID,
subject: "self",
event_kind: "home_change",
domain: "relocation",
occurred_from: "2015-01-01",
occurred_to: null,
date_precision: "year",
summary: "搬家",
status: "confirmed",
supersedes_evidence_id: null,
created_at: "2026-08-12T10:00:12.000Z",
},
{
id: "44444444-4444-4444-8444-444444444449",
source_turn_id: TURN_ID,
subject: "self",
event_kind: "self_health_event",
domain: "health_pressure",
occurred_from: "2013-01-01",
occurred_to: null,
date_precision: "year",
summary: "身体压力变化",
status: "confirmed",
supersedes_evidence_id: null,
created_at: "2026-08-12T10:00:13.000Z",
},
];
function scoreableFingerprintForRawEvidence(evidence: unknown[]): string {
const parsed = parseV9CaseDossier(dossierFixture({ evidence }));
assert.ok(parsed);
return evidenceLedgerFingerprint(parsed.evidence);
}
function stubEngine(response: unknown) {
const previous = globalThis.fetch;
globalThis.fetch = (async () => ({
ok: true,
status: 200,
json: async () => response,
})) as unknown as typeof fetch;
return () => {
globalThis.fetch = previous;
};
}
test("eight-method routing asks relationship after dated education, not relocation", () => {
const plan = buildMethodFollowupPlan({
evidence: [{
status: "confirmed",
domain: "education",
datePrecision: "month",
occurredFrom: "2016-06-01",
occurredTo: null,
}],
});
// 原值: relatives / family
// 新值: d9_relationship / relationship
// 原因: dated 分支移到家人之后,前两问恢复感情 / 事业
assert.equal(plan.next_followup?.method_id, "d9_relationship");
assert.equal(plan.next_followup?.domain, "relationship");
assert.equal(plan.stop_domain_rotation, true);
assert.deepEqual([...plan.not_in_rotation], ["relocation"]);
assert.equal(plan.methods.find((item) => item.method_id === "appearance")?.status, "skipped_by_policy");
});
test("dasha conflict probe does not jump ahead of method rotation before acceptance event quality", () => {
const plan = buildMethodFollowupPlan({
evidence: [datedEvidence("education", "2016")],
eventProbes: [CAREER_CONFLICT_PROBE],
});
// 原值: method_coverage / relatives
// 新值: method_coverage / d9_relationship
// 原因: dated 分支移到家人之后,前两问恢复感情 / 事业
assert.equal(plan.next_followup?.source, "method_coverage");
assert.equal(plan.next_followup?.method_id, "d9_relationship");
assert.equal(plan.next_followup?.choice_frame, null);
assert.notEqual(plan.next_followup?.source, "event_probe");
});
test("known exam quality does not create a scoring card after one recorded event", () => {
const plan = buildMethodFollowupPlan({
evidence: [datedEvidence("education", "2016")],
eventProbes: [EDUCATION_QUALITY_PROBE, CAREER_CONFLICT_PROBE],
});
// 原值: method_coverage / relatives
// 新值: method_coverage / d9_relationship
// 原因: dated 分支移到家人之后,前两问恢复感情 / 事业
assert.equal(plan.next_followup?.source, "method_coverage");
assert.equal(plan.next_followup?.method_id, "d9_relationship");
assert.equal(plan.next_followup?.choice_frame, null);
assert.notEqual(plan.next_followup?.choice_kind, "event_quality");
});
test("career known-event quality does not jump the adoption gate", () => {
const plan = buildMethodFollowupPlan({
evidence: [
datedEvidence("career", "2020", { eventKind: "career_entry", datePrecision: "month" }),
datedEvidence("career", "2020", { eventKind: "career_exit", datePrecision: "month" }),
],
eventProbes: [CAREER_QUALITY_PROBE, CAREER_CONFLICT_PROBE],
});
// 原值: method_coverage / relatives
// 新值: method_coverage / d9_relationship
// 原因: dated 分支移到家人之后,前两问恢复感情 / 事业
assert.equal(plan.next_followup?.source, "method_coverage");
assert.equal(plan.next_followup?.method_id, "d9_relationship");
assert.equal(plan.next_followup?.choice_frame, null);
assert.notEqual(plan.next_followup?.source, "event_probe");
assert.notEqual(plan.next_followup?.choice_kind, "event_quality");
});
test("encoded exam quality does not stamp another card and keeps method rotation", () => {
const plan = buildMethodFollowupPlan({
evidence: [datedEvidence("education", "2016", { summary: "2016年高考发挥异常" })],
eventProbes: [EDUCATION_QUALITY_PROBE, CAREER_CONFLICT_PROBE],
});
// 原值: method_coverage / relatives
// 新值: method_coverage / d9_relationship
// 原因: dated 分支移到家人之后,前两问恢复感情 / 事业
assert.equal(plan.next_followup?.source, "method_coverage");
assert.equal(plan.next_followup?.method_id, "d9_relationship");
assert.equal(plan.next_followup?.choice_frame, null);
});
test("dasha conflict probe jumps after four scoreable events leave three training domains", () => {
const plan = buildMethodFollowupPlan({
evidence: [
datedEvidence("education", "2016"),
datedEvidence("education", "2020"),
datedEvidence("relationship", "2018"),
datedEvidence("family", "2023"),
],
eventProbes: [CAREER_CONFLICT_PROBE],
});
assert.equal(plan.next_followup?.source, "event_probe");
assert.equal(plan.next_followup?.intent, "distinguish_candidates");
assert.equal(plan.next_followup?.domain, "career");
assert.equal(plan.next_followup?.choice_frame?.scoring, true);
assert.equal(plan.next_followup?.choice_frame?.period, "2018 年前后");
assert.deepEqual(plan.next_followup?.candidate_ids, ["05:00", "05:20"]);
assert.ok((plan.next_followup?.information_gain ?? 0) > 0);
assert.equal(isOfferBlockingFollowup(plan.next_followup, plan.methods), true);
assert.equal(conversationalSessionOutcome({
selectionAllowed: true,
proposeAllowed: true,
confirmationAllowed: false,
nextFollowup: plan.next_followup,
methods: plan.methods,
}), "discriminate_candidates");
});
test("dasha conflict probe keeps the engine month on the choice card", () => {
const plan = buildMethodFollowupPlan({
evidence: [
datedEvidence("education", "2016"),
datedEvidence("education", "2020"),
datedEvidence("relationship", "2018"),
datedEvidence("family", "2023"),
],
eventProbes: [{
...CAREER_CONFLICT_PROBE,
month: 3,
year_label: "2018 年 3 月前后",
source: "dasha_boundary",
user_meaning: "时间范围锁定 2018 年 3 月前后;领域锁定 career。",
semantic_key: "career.2018.03.dasha_boundary",
}],
});
assert.equal(plan.next_followup?.source, "event_probe");
assert.equal(plan.next_followup?.choice_frame?.period, "2018 年 3 月前后");
assert.equal(plan.next_followup?.year_label, "2018 年 3 月前后");
assert.equal(plan.next_followup?.probe_month, 3);
assert.match(plan.next_followup?.user_prompt_hint ?? "", /2018 年 3 月前后/);
});
test("three scoreable events in one domain still rotate methods instead of reverse-inferring", () => {
const plan = buildMethodFollowupPlan({
evidence: [
datedEvidence("education", "2012"),
datedEvidence("education", "2016"),
datedEvidence("education", "2020"),
],
eventProbes: [CAREER_CONFLICT_PROBE],
});
// 原值: method_coverage / relatives
// 新值: method_coverage / d9_relationship
// 原因: dated 分支移到家人之后,前两问恢复感情 / 事业
assert.equal(plan.next_followup?.source, "method_coverage");
assert.equal(plan.next_followup?.method_id, "d9_relationship");
assert.equal(plan.next_followup?.choice_frame, null);
});
test("occupation_note does not count toward reverse-inference event quality", () => {
const plan = buildMethodFollowupPlan({
evidence: [
datedEvidence("education", "2016"),
datedEvidence("education", "2020"),
{
status: "confirmed",
domain: "occupation",
datePrecision: "unknown",
occurredFrom: null,
occurredTo: null,
eventKind: "occupation_note",
},
],
eventProbes: [CAREER_CONFLICT_PROBE],
});
// 原值: method_coverage / relatives
// 新值: method_coverage / d9_relationship
// 原因: dated 分支移到家人之后,前两问恢复感情 / 事业
assert.equal(plan.next_followup?.source, "method_coverage");
assert.equal(plan.next_followup?.method_id, "d9_relationship");
assert.equal(plan.next_followup?.choice_frame, null);
});
test("age-band probe does not jump ahead of uncovered relationship", () => {
const plan = buildMethodFollowupPlan({
evidence: [{
status: "confirmed",
domain: "education",
datePrecision: "year",
occurredFrom: "2016-01-01",
occurredTo: null,
}],
eventProbes: [{
year: 2018,
year_label: "2018 年前后",
domain: "relocation",
event_family: "搬家、离乡或长期异地",
source: "age_band",
tracks: ["vimshottari", "narayana"],
tracks_agree: false,
unique_minute_claim: false,
user_meaning: "年份锁定 2018 年前后。",
role: "reverse_verify",
}],
});
// 原值: relatives
// 新值: d9_relationship
// 原因: dated 分支移到家人之后,前两问恢复感情 / 事业
assert.equal(plan.next_followup?.method_id, "d9_relationship");
assert.equal(plan.next_followup?.source, "method_coverage");
assert.equal(plan.next_followup?.choice_frame, null);
});
test("user-stop action records stated events when the ledger is empty", () => {
const action = buildNextUserAction({
scorableCount: 0,
evidenceCount: 0,
hasLatestResult: false,
selectionAllowed: false,
sessionOutcome: "collect_evidence",
nextFollowup: null,
workingTime: "12:00",
});
assert.equal(action.id, "record_stated_events");
assert.equal(action.on_user_stop.id, "record_stated_events");
assert.match(action.on_user_stop.user_meaning, /batch/);
});
test("user-stop action offers a range when candidates already exist", () => {
const plan = buildMethodFollowupPlan({
evidence: [{
status: "confirmed",
domain: "education",
datePrecision: "month",
occurredFrom: "2016-06-01",
occurredTo: null,
}],
});
const withCandidates = buildNextUserAction({
scorableCount: 1,
evidenceCount: 1,
hasLatestResult: true,
selectionAllowed: false,
sessionOutcome: "collect_evidence",
nextFollowup: plan.next_followup,
workingTime: "12:00",
});
assert.equal(withCandidates.id, "ask_method_followup");
assert.equal(withCandidates.on_user_stop.id, "offer_provisional_range");
const withoutCandidates = buildNextUserAction({
scorableCount: 0,
evidenceCount: 1,
hasLatestResult: false,
selectionAllowed: false,
sessionOutcome: "collect_evidence",
nextFollowup: plan.next_followup,
workingTime: "12:00",
});
assert.equal(withoutCandidates.id, "ask_method_followup");
assert.equal(withoutCandidates.on_user_stop.id, "explain_current_window");
assert.match(withoutCandidates.on_user_stop.user_meaning, /12:00/);
assert.match(withoutCandidates.on_user_stop.user_meaning, /不要只说会话会保留/);
assert.equal(plan.methods.find((item) => item.method_id === "marks")?.status, "skipped_by_policy");
assert.equal(plan.methods.find((item) => item.method_id === "horary")?.status, "uncovered");
assert.equal(plan.methods.find((item) => item.method_id === "occupation")?.status, "uncovered");
// 原值: relatives
// 新值: d9_relationship
// 原因: dated 分支移到家人之后,前两问恢复感情 / 事业
assert.equal(plan.next_followup?.method_id, "d9_relationship");
assert.match(plan.next_followup?.user_prompt_hint ?? "", /自然语言/);
assert.doesNotMatch(plan.next_followup?.user_prompt_hint ?? "", /A\/B\/C\/D/);
assert.equal(plan.next_followup?.choice_frame, null);
assert.doesNotMatch(JSON.stringify(plan), UNIQUE_MINUTE_COPY);
});
test("selectionAllowed with remaining method follow-up keeps collecting and offers a range on stop", () => {
const plan = buildMethodFollowupPlan({
evidence: [{
status: "confirmed",
domain: "education",
datePrecision: "month",
occurredFrom: "2016-06-01",
occurredTo: null,
}],
});
const action = buildNextUserAction({
scorableCount: 3,
evidenceCount: 3,
hasLatestResult: true,
selectionAllowed: true,
sessionOutcome: "collect_evidence",
nextFollowup: plan.next_followup,
workingTime: "05:07",
});
// 原值: relatives
// 新值: d9_relationship
// 原因: dated 分支移到家人之后,前两问恢复感情 / 事业
assert.equal(plan.next_followup?.method_id, "d9_relationship");
assert.equal(action.id, "ask_method_followup");
assert.equal(action.on_user_stop.id, "offer_provisional_range");
});
test("adopt_representative still asks remaining evidence collect this turn", () => {
const plan = buildMethodFollowupPlan({
evidence: [{
status: "confirmed",
domain: "education",
datePrecision: "month",
occurredFrom: "2016-06-01",
occurredTo: null,
}],
sessionOutcome: "adopt_representative",
});
// 原值: next_followup null,感情采集进 deferred
// 新值: 本轮仍问感情采集
// 原因: 家人之后还有能区分分钟的经历时,不得因 canAdopt 早退(BUG-546
assert.equal(plan.next_followup?.method_id, "d9_relationship");
assert.equal(plan.next_followup?.intent, "collect_method_evidence");
assert.equal(plan.deferred_followup, null);
assert.equal(plan.session_outcome, "adopt_representative");
});
test("adopt_representative ignores leftover distinguish focus", () => {
const plan = buildMethodFollowupPlan({
evidence: CLASSIC_COVERAGE.filter((item) => item.domain !== "horary"),
sessionOutcome: "adopt_representative",
activeFocus: {
intent: "distinguish_candidates",
targetDomain: "relocation",
targetKind: "home_change",
},
});
// 原值: next_followup null(采用早退)
// 新值: 丢掉无用区分焦点后仍收财务经历
// 原因: 未用的 dated collect 不得被 adopt 早退吃掉(BUG-546
assert.equal(plan.next_followup?.domain, "finance");
assert.equal(plan.next_followup?.intent, "collect_method_evidence");
assert.notEqual(plan.next_followup?.intent, "distinguish_candidates");
assert.equal(plan.session_outcome, "adopt_representative");
});
test("declined relationship skips to career and leaves horary uncovered", () => {
const plan = buildMethodFollowupPlan({
evidence: [{
status: "confirmed",
domain: "education",
datePrecision: "year",
occurredFrom: "2016-01-01",
occurredTo: null,
}],
declinedTopics: [{ target_domain: "relationship", status: "declined" }],
});
// 原值: relatives / family
// 新值: d10_career / career
// 原因: dated 分支移到家人之后,前两问恢复感情 / 事业
assert.equal(plan.next_followup?.method_id, "d10_career");
assert.equal(plan.next_followup?.domain, "career");
assert.deepEqual([...plan.do_not_poll], []);
assert.equal(plan.methods.find((item) => item.method_id === "horary")?.status, "uncovered");
});
test("D9 differ keeps sign names for the type-table report and still forbids unique-minute claims", () => {
const scan = parseWindowScan({
scanned: true,
confirmation_allowed: true,
unique_minute_claim: true,
d9_lagna_count: 2,
d10_lagna_count: 1,
d9_candidates_differ: true,
d10_candidates_differ: false,
d9_sign_names: ["白羊座", "天蝎"],
type_table: "热情冲动",
transitions: [{
layer: "d9",
at: "05:14",
user_meaning: "白羊座在 05:14 换成天蝎",
}],
});
assert.ok(scan);
assert.equal(scan.confirmation_allowed, false);
assert.equal(scan.unique_minute_claim, false);
assert.equal(scan.d9_candidates_differ, true);
assert.deepEqual(scan.d9_sign_names, ["白羊座", "天蝎"]);
assert.deepEqual(scan.transitions, [{
layer: "d9",
at: "05:14",
user_meaning: "D9 在 05:14 发生变化",
}]);
const observations = internalObservationsFromWindowScan(scan);
assert.deepEqual(observations, [
{ layer: "d9", candidates_differ: true, ask_theme: "relationship_style" },
{ layer: "d10", candidates_differ: false, ask_theme: null },
{ layer: "d4", candidates_differ: false, ask_theme: null },
{ layer: "d5", candidates_differ: false, ask_theme: null },
{ layer: "d7", candidates_differ: false, ask_theme: null },
{ layer: "d12", candidates_differ: false, ask_theme: null },
{ layer: "d11", candidates_differ: false, ask_theme: null },
{ layer: "d30", candidates_differ: false, ask_theme: null },
]);
assert.doesNotMatch(JSON.stringify({ scan, observations }), UNIQUE_MINUTE_COPY);
const plan = buildMethodFollowupPlan({
evidence: CLASSIC_COVERAGE,
observations,
});
// 原值: next_followup null
// 新值: 财务采集
// 原因: 经典八法覆盖后仍收未用带年份域,D9 观察不再抢在前面(BUG-546)
assert.equal(plan.next_followup?.method_id, "d2_finance");
assert.equal(plan.next_followup?.intent, "collect_method_evidence");
assert.doesNotMatch(JSON.stringify(plan), UNIQUE_MINUTE_COPY);
});
test("D24-only window change folds into education follow-up without a second layer", () => {
const scan = parseWindowScan({
scanned: true,
d9_lagna_count: 1,
d10_lagna_count: 1,
d5_lagna_count: 1,
d24_lagna_count: 2,
d5_candidates_differ: false,
d24_candidates_differ: true,
transitions: [
{ layer: "d24", at: "05:14" },
{ layer: "pada", at: "05:14" },
],
});
assert.ok(scan);
assert.equal(scan.d24_candidates_differ, true);
assert.equal(scan.d5_candidates_differ, false);
assert.deepEqual(scan.transitions, [
{ layer: "d24", at: "05:14", user_meaning: "D24 在 05:14 发生变化" },
{ layer: "pada", at: "05:14", user_meaning: "Nakshatra pada 在 05:14 发生变化" },
]);
const observations = internalObservationsFromWindowScan(scan);
assert.equal(observations.find((item) => item.layer === "d5")?.candidates_differ, true);
assert.equal(observations.find((item) => item.layer === "d5")?.ask_theme, "education_style");
assert.deepEqual(
observations.map((item) => item.layer),
["d9", "d10", "d4", "d5", "d7", "d12", "d11", "d30"],
);
const plan = buildMethodFollowupPlan({
evidence: CLASSIC_COVERAGE,
observations,
});
// 原值: d5_education / education_styleD24 观察)
// 新值: d2_finance 采集
// 原因: 家人之后先收未用带年份经历,再进分盘观察(BUG-546)
assert.equal(plan.next_followup?.method_id, "d2_finance");
assert.equal(plan.next_followup?.intent, "collect_method_evidence");
});
test("D11-only window change folds into finance follow-up without delaying adopt", () => {
const scan = parseWindowScan({
scanned: true,
d9_lagna_count: 1,
d10_lagna_count: 1,
d2_lagna_count: 1,
d11_lagna_count: 2,
d30_lagna_count: 2,
d2_candidates_differ: false,
d11_candidates_differ: true,
d30_candidates_differ: true,
transitions: [
{ layer: "d11", at: "05:14" },
{ layer: "d30", at: "05:14" },
{ layer: "bhava", at: "05:14" },
{ layer: "pranapada", at: "05:14" },
],
});
assert.ok(scan);
assert.equal(scan.d11_candidates_differ, true);
assert.equal(scan.d2_candidates_differ, false);
assert.deepEqual(scan.transitions, [
{ layer: "d11", at: "05:14", user_meaning: "D11 在 05:14 发生变化" },
{ layer: "d30", at: "05:14", user_meaning: "D30 在 05:14 发生变化" },
{ layer: "bhava", at: "05:14", user_meaning: "Bhava Lagna 在 05:14 发生变化" },
{ layer: "pranapada", at: "05:14", user_meaning: "Pranapada Lagna 在 05:14 发生变化" },
]);
const observations = internalObservationsFromWindowScan(scan);
assert.equal(observations.find((item) => item.layer === "d11")?.candidates_differ, true);
assert.equal(observations.find((item) => item.layer === "d11")?.ask_theme, "finance_change");
assert.equal(observations.find((item) => item.layer === "d30")?.ask_theme, "health_pressure");
assert.deepEqual(
observations.map((item) => item.layer),
["d9", "d10", "d4", "d5", "d7", "d12", "d11", "d30"],
);
const plan = buildMethodFollowupPlan({
evidence: [
...CLASSIC_COVERAGE,
{ status: "confirmed", domain: "finance", datePrecision: "year", occurredFrom: "2021-01-01", occurredTo: null },
],
observations,
});
// 原值: d2_finance / varga_observation(不挡采用)
// 新值: d4_home 搬家采集
// 原因: 财务已有带年份证据后,下一未用域是搬家(BUG-546)
assert.equal(plan.next_followup?.method_id, "d4_home");
assert.equal(plan.next_followup?.intent, "collect_method_evidence");
assert.equal(plan.next_followup?.source, "method_coverage");
});
test("window scan displays KP sub-lord changes without opening confirmation", () => {
assert.deepEqual(
["kp1", "kp4", "kp7", "kp10"].every((layer) => WINDOW_SCAN_DISPLAY_LAYER_ORDER.includes(layer as typeof WINDOW_SCAN_DISPLAY_LAYER_ORDER[number])),
true,
);
const scan = parseWindowScan({
scanned: true,
confirmation_allowed: true,
unique_minute_claim: true,
d9_lagna_count: 1,
d10_lagna_count: 1,
transitions: [
{ layer: "kp1", at: "05:14" },
{ layer: "kp10", at: "05:15" },
],
});
assert.ok(scan);
assert.equal(scan.confirmation_allowed, false);
assert.equal(scan.unique_minute_claim, false);
assert.deepEqual(scan.transitions, [
{ layer: "kp1", at: "05:14", user_meaning: "KP 1宫子主 在 05:14 发生变化" },
{ layer: "kp10", at: "05:15", user_meaning: "KP 10宫子主 在 05:15 发生变化" },
]);
});
test("read-case follows method plan and keeps D9/D10 type tables when SQL missing categories rotate", async () => {
const accounting = fakeAccounting({
...receiptHandlers,
get_agentic_rectification_case_dossier: () => dossierFixture({
evidence: [educationEvidence, familyEvidence, careerEvidence],
conversationSummary: {
confirmed_evidence_summary: [],
pending_revisions: [],
active_focus: null,
declined_skipped_topics: [],
candidate_divergence_summary: null,
missing_evidence_categories: ["relocation", "health", "finance"],
last_result_policy: null,
summary_version: 1,
updated_at: "2026-08-12T10:00:06.000Z",
},
latestResult: {
...candidateSnapshotFixture({
confirmationAllowed: true,
representativeTime: "04:45",
candidates: [
{ candidate_id: CANDIDATE_ID, rank: 1, time: "04:45", relative_support: 40, tied_minute_count: 25 },
{ candidate_id: SECOND_CANDIDATE_ID, rank: 2, time: "04:46", relative_support: 35, tied_minute_count: 25 },
{ candidate_id: THIRD_CANDIDATE_ID, rank: 3, time: "04:47", relative_support: 25, tied_minute_count: 25 },
],
}),
selection_allowed: true,
confirmation_allowed: true,
decision_receipt: {
receipt_version: "candidate-decision-receipt-v2",
policy_version: "rectification-candidate-policy-v2",
selection_allowed: true,
acceptance_allowed: true,
propose_allowed: true,
confirmation_allowed: false,
representative_candidate_id: CANDIDATE_ID,
overall_confidence: "medium",
window_scan: {
scanned: true,
confirmation_allowed: false,
unique_minute_claim: false,
d9_lagna_count: 2,
d10_lagna_count: 1,
d9_candidates_differ: true,
d10_candidates_differ: false,
d9_sign_names: ["白羊", "天蝎"],
},
},
},
}),
get_agentic_rectification_case_compute: () => computeFixture(),
});
const tools = createRectificationV9Tools({
userId: USER_ID,
caseId: CASE_ID,
turnId: TURN_ID,
accounting: accounting.client as never,
});
const projection = await (tools["rectification-read-case"] as unknown as {
execute(input: unknown): Promise<{
conversation_summary: { missing_evidence_categories: string[] };
method_followup_plan: {
next_followup: { method_id: string; domain: string | null } | null;
deferred_followup: { method_id: string; domain: string | null } | null;
session_outcome: string;
};
internal_observations: Array<{ layer: string; ask_theme: string | null }>;
latest_result: {
confirmation_allowed: boolean;
indistinguishable_width_minutes: number;
window_scan: { d9_candidates_differ: boolean } | null;
session_outcome: string;
};
}>;
}).execute({ caseId: CASE_ID, projection: "full_diagnostics" });
assert.deepEqual(projection.conversation_summary.missing_evidence_categories, ["relocation", "health", "finance"]);
// 原值: d2_finance / finance
// 新值: d9_relationship / relationship
// 原因: dated 分支移到家人之后,前两问恢复感情 / 事业
assert.equal(projection.method_followup_plan.next_followup?.method_id, "d9_relationship");
assert.equal(projection.method_followup_plan.next_followup?.domain, "relationship");
assert.equal(projection.method_followup_plan.deferred_followup, null);
assert.equal(projection.method_followup_plan.session_outcome, "collect_evidence");
assert.equal(
(projection as { next_user_action?: { id?: string; on_user_stop?: { id?: string } } }).next_user_action?.id,
"ask_method_followup",
);
assert.equal(
(projection as { next_user_action?: { on_user_stop?: { id?: string } } }).next_user_action?.on_user_stop?.id,
"offer_provisional_range",
);
assert.equal(projection.latest_result.session_outcome, "collect_evidence");
assert.equal(projection.internal_observations.find((item) => item.layer === "d9")?.ask_theme, "relationship_style");
assert.equal(projection.latest_result.confirmation_allowed, false);
assert.ok(projection.latest_result.indistinguishable_width_minutes >= 25);
assert.equal(projection.latest_result.window_scan?.d9_candidates_differ, true);
assert.doesNotMatch(JSON.stringify(projection), UNIQUE_MINUTE_COPY);
assert.doesNotMatch(JSON.stringify(projection), /A\/B\/C\/D/);
assert.match(JSON.stringify(projection.method_followup_plan.next_followup), /自然语言/);
});
test("accepted batch evidence resolves a spoken collect focus before the next question", async () => {
const restore = stubEngine(ENGINE_SCORE);
let collectFocusResolved = false;
try {
const accounting = fakeAccounting({
...receiptHandlers,
get_agentic_rectification_case_dossier: () => dossierFixture({
evidence: [educationEvidence],
latestResult: null,
conversationSummary: conversationSummaryFixture({
activeFocus: collectFocusResolved
? null
: activeFocusFixture({
intent: "collect_method_evidence",
targetDomain: "education",
expectedAnswerSchema: { collect: true, prompt: "有没有记得住时间的升学经历?" },
}),
}),
}),
get_agentic_rectification_case_compute: () => computeFixture(),
record_agentic_rectification_evidence_batch: () => ({
items: [{
index: 0,
outcome: "accepted",
evidence_id: EDUCATION_ID,
status: "confirmed",
idempotent: false,
clarification_fields: [],
error_code: null,
}],
accepted_count: 1,
needs_clarification_count: 0,
rejected_count: 0,
focus_id: null,
}),
resolve_agentic_rectification_conversation_focus: (_fn, args) => {
collectFocusResolved = true;
return {
focus_id: args.p_focus_id,
status: args.p_status,
evidence_id: args.p_evidence_id,
idempotent: false,
};
},
persist_agentic_rectification_candidate_v2: () => ({
...candidateSnapshotFixture(),
cached: false,
}),
});
const tools = createRectificationV9Tools({
userId: USER_ID,
caseId: CASE_ID,
turnId: TURN_ID,
userMessage: "2016年6月高考结束",
accounting: accounting.client as never,
});
const result = await (tools["rectification-record-evidence-batch"] as unknown as {
execute(input: unknown): Promise<{
accepted_count: number;
rescore: { status: string; executed_methods: string[]; error_code: string | null };
open_question: { prompt?: string } | null;
}>;
}).execute({
caseId: CASE_ID,
items: [{
quote: "2016年6月高考结束",
proposedKind: "education_milestone",
subject: "self",
domain: "education",
datePrecision: "month",
occurredFrom: "2016-06",
summary: "2016年6月一次学业节点",
}],
});
assert.equal(result.accepted_count, 1);
assert.equal(result.rescore.status, "completed");
assert.ok(result.rescore.executed_methods.includes("d1-rashi"));
assert.equal(accounting.calls[0]?.fn, "get_agentic_rectification_case_dossier");
const resolvedFocus = accounting.calls.find((call) => call.fn === "resolve_agentic_rectification_conversation_focus");
assert.equal(resolvedFocus?.args.p_focus_id, FOCUS_ID);
assert.equal(resolvedFocus?.args.p_status, "resolved");
assert.equal(resolvedFocus?.args.p_evidence_id, EDUCATION_ID);
const recordIndex = accounting.calls.findIndex((call) => call.fn === "record_agentic_rectification_evidence_batch");
const resolveIndex = accounting.calls.findIndex((call) => call.fn === "resolve_agentic_rectification_conversation_focus");
assert.ok(recordIndex >= 0 && resolveIndex > recordIndex);
assert.doesNotMatch(result.open_question?.prompt ?? "", /升学经历/);
const persistCall = accounting.calls.find((call) => call.fn === "persist_agentic_rectification_candidate_v2");
assert.ok(persistCall);
const receipt = persistCall.args.p_decision_receipt as { window_scan?: { d9_candidates_differ?: boolean; d9_sign_names?: unknown } };
assert.equal(receipt.window_scan?.d9_candidates_differ, true);
assert.deepEqual((receipt.window_scan as { d9_sign_names?: string[] } | undefined)?.d9_sign_names, ["白羊座", "天蝎"]);
const completedReceipt = accounting.calls.find((call) =>
call.fn === "insert_agentic_rectification_tool_receipt"
&& call.args.p_tool_name === "rectification-record-evidence-batch"
&& call.args.p_status === "completed"
);
assert.ok(completedReceipt);
assert.ok((completedReceipt.args.p_executed_methods as string[]).includes("d1-rashi"));
assert.equal(
accounting.calls.some((call) =>
call.fn === "transition_agentic_rectification_case_status"
&& call.args.p_to_status === "candidate_ready"
),
false,
);
assert.doesNotMatch(JSON.stringify(result), UNIQUE_MINUTE_COPY);
} finally {
restore();
}
});
test("cached candidates retry a failed VedAstro validation without recomputing ranking", async () => {
const validationResponse = {
status: "passed",
can_confirm_exact_minute: true,
event_validation: {
search_events_primary_supports_local_winner: true,
},
minute_sensitive_validation: {
status: "passed",
},
};
const restore = stubEngine(validationResponse);
try {
const compute = computeFixture();
const evidence = [educationEvidence];
const evidenceFingerprint = evidenceLedgerFingerprint([{
id: educationEvidence.id,
sourceTurnId: educationEvidence.source_turn_id,
subject: educationEvidence.subject,
eventKind: educationEvidence.event_kind,
domain: educationEvidence.domain,
occurredFrom: educationEvidence.occurred_from,
occurredTo: educationEvidence.occurred_to,
datePrecision: educationEvidence.date_precision,
summary: educationEvidence.summary,
status: educationEvidence.status,
supersedesEvidenceId: educationEvidence.supersedes_evidence_id,
createdAt: educationEvidence.created_at,
}]);
const rangeFingerprint = candidateRangeFingerprint(
compute.candidate_range,
compute.baseline_profile_fingerprint,
);
const cached = {
...candidateSnapshotFixture({
representativeTime: "05:02",
evidenceLedgerFingerprint: evidenceFingerprint,
decisionReceipt: {
inference_state: producedInferenceState([
{ id: CANDIDATE_ID, time: "05:02", relative_support: 58 },
{ id: SECOND_CANDIDATE_ID, time: "04:55", relative_support: 42 },
]),
gates: {
exact_confirmation: {
external_validation_status: "failed",
vedastro_event_validation: {
status: "failed",
search_events_primary_supports_local_winner: false,
can_confirm_exact_minute: false,
failure: { code: "timeout" },
},
},
},
},
}),
candidate_range_fingerprint: rangeFingerprint,
};
const refreshedReceipt = {
...cached.decision_receipt,
gates: {
exact_confirmation: {
external_validation_status: "passed",
vedastro_event_validation: {
status: "passed",
search_events_primary_supports_local_winner: true,
can_confirm_exact_minute: true,
failure: null,
},
},
},
};
const parsedCached = parseV9CaseDossier(dossierFixture({ evidence, latestResult: cached }));
assert.equal(parsedCached?.latestResult?.selectionAllowed, true);
assert.equal(parsedCached?.latestResult?.evidenceLedgerFingerprint, evidenceFingerprint);
assert.equal(parsedCached?.latestResult?.candidateRangeFingerprint, rangeFingerprint);
assert.equal(readVedastroMinuteSensitiveStatus(parsedCached?.latestResult?.decisionReceipt), "failed");
assert.equal(authoritativeCandidateProjection(parsedCached!.latestResult!).candidates.length, 2);
const accounting = fakeAccounting({
...receiptHandlers,
get_agentic_rectification_case_dossier: () => dossierFixture({
evidence,
latestResult: cached,
}),
get_agentic_rectification_case_compute: () => compute,
refresh_agentic_rectification_vedastro_validation: () => ({
result_id: RESULT_ID,
decision_receipt: refreshedReceipt,
}),
set_agentic_rectification_conversation_focus: (_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-08-27T00:00:00.000Z",
resolved_at: null,
},
idempotent: false,
}),
});
const tools = createRectificationV9Tools({
userId: USER_ID,
caseId: CASE_ID,
turnId: TURN_ID,
userMessage: "继续比较",
accounting: accounting.client as never,
});
const result = await (tools["rectification-compare-candidates"] as unknown as {
execute(input: unknown): Promise<{
cached: boolean;
candidates: Array<{ time: string; relativeSupport: number }>;
confirmation_gate: { blockers: Array<{ id: string; status: string }> };
}>;
}).execute({ caseId: CASE_ID });
const refreshCall = accounting.calls.find((call) =>
call.fn === "refresh_agentic_rectification_vedastro_validation"
);
assert.ok(refreshCall, JSON.stringify(accounting.calls.map((call) => call.fn)));
assert.deepEqual(refreshCall.args.p_validation, {
status: "passed",
search_events_primary_supports_local_winner: true,
can_confirm_exact_minute: true,
failure: null,
});
assert.equal(refreshCall.args.p_minute_sensitive_status, "passed");
assert.equal(
accounting.calls.some((call) => call.fn === "persist_agentic_rectification_candidate_v2"),
false,
);
assert.equal(result.cached, true);
assert.deepEqual(result.candidates.map((item) => [item.time, item.relativeSupport]), [
["05:02", 58],
["04:55", 42],
]);
assert.ok(result.confirmation_gate, JSON.stringify(result));
assert.equal(
result.confirmation_gate.blockers.find((gate) => gate.id === "vedastro_minute_sensitive")?.status,
"passed",
);
} finally {
restore();
}
});
test("evidence batch returns the persisted choice prompt as open_question", async () => {
const restore = stubEngine({
...ENGINE_SCORE,
decision_receipt: {
...ENGINE_SCORE.decision_receipt,
discriminating_event_probes: [{
year: 2023,
year_label: "2023 年前后",
domain: "relocation",
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: 1.09,
semantic_key: "relocation.2023.dasha_activation",
candidate_set_version: "set-test",
candidate_split_hash: "set-test:relocation:2023",
candidate_ids: ["04:50", "04:51"],
expected_outcomes: [
{ answer_class: "yes", supports: ["04:50"], conflicts: ["04:51"] },
{ answer_class: "no", supports: ["04:51"], conflicts: ["04:50"] },
],
style_options: DYNAMIC_STYLE_OPTIONS,
choice_kind: "existence",
}],
},
});
try {
const accounting = fakeAccounting({
...receiptHandlers,
get_agentic_rectification_case_dossier: () => dossierFixture({
evidence: [educationEvidence, relationshipEvidence, familyEvidence, careerEvidence],
evidenceCount: 4,
latestResult: null,
}),
get_agentic_rectification_case_compute: () => computeFixture(),
record_agentic_rectification_evidence_batch: () => ({
items: [{
index: 0,
outcome: "accepted",
evidence_id: EDUCATION_ID,
status: "confirmed",
idempotent: false,
clarification_fields: [],
error_code: null,
}],
accepted_count: 1,
needs_clarification_count: 0,
rejected_count: 0,
focus_id: null,
}),
persist_agentic_rectification_candidate_v2: (_fn, args) => ({
...candidateSnapshotFixture({
decisionReceipt: args.p_decision_receipt as Record<string, unknown>,
}),
cached: false,
}),
set_agentic_rectification_conversation_focus: (_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-08-25T14:47:09.000Z",
resolved_at: null,
},
idempotent: false,
}),
});
const tools = createRectificationV9Tools({
userId: USER_ID,
caseId: CASE_ID,
turnId: TURN_ID,
userMessage: "2016年6月高考结束",
accounting: accounting.client as never,
});
const result = await (tools["rectification-record-evidence-batch"] as unknown as {
execute(input: unknown): Promise<{
accepted_count: number;
rescore: { status: string };
open_question: { prompt?: string } | null;
}>;
}).execute({
caseId: CASE_ID,
items: [{
quote: "2016年6月高考结束",
proposedKind: "education_milestone",
subject: "self",
domain: "education",
datePrecision: "month",
occurredFrom: "2016-06",
summary: "2016年6月一次学业节点",
}],
});
assert.equal(result.accepted_count, 1);
assert.equal(result.rescore.status, "completed");
const setFocus = accounting.calls.find((call) => call.fn === "set_agentic_rectification_conversation_focus");
const schema = setFocus?.args.p_expected_answer_schema as { choice?: { prompt?: string } } | undefined;
assert.match(schema?.choice?.prompt ?? "", /2023 年前后/);
assert.match(result.open_question?.prompt ?? "", /2023 年前后/);
assert.match(result.open_question?.prompt ?? "", /搬家、离乡或长期异地/);
assert.doesNotMatch(result.open_question?.prompt ?? "", /高考/);
} finally {
restore();
}
});
test("rescore failure does not fail the evidence write", async () => {
const accounting = fakeAccounting({
...receiptHandlers,
get_agentic_rectification_case_dossier: () => dossierFixture({
evidence: [educationEvidence],
}),
get_agentic_rectification_case_compute: () => computeFixture(),
record_agentic_rectification_evidence_batch: () => ({
items: [{
index: 0,
outcome: "accepted",
evidence_id: EDUCATION_ID,
status: "confirmed",
idempotent: false,
clarification_fields: [],
error_code: null,
}],
accepted_count: 1,
needs_clarification_count: 0,
rejected_count: 0,
focus_id: null,
}),
});
const tools = createRectificationV9Tools({
userId: USER_ID,
caseId: CASE_ID,
turnId: TURN_ID,
userMessage: "2016年6月高考结束",
accounting: accounting.client as never,
});
const result = await (tools["rectification-record-evidence-batch"] as unknown as {
execute(input: unknown): Promise<{
accepted_count: number;
rescore: { status: string; error_code: string | null };
}>;
}).execute({
caseId: CASE_ID,
items: [{
quote: "2016年6月高考结束",
proposedKind: "education_milestone",
subject: "self",
domain: "education",
datePrecision: "month",
occurredFrom: "2016-06",
summary: "2016年6月一次学业节点",
}],
});
assert.equal(result.accepted_count, 1);
assert.equal(result.rescore.status, "failed");
assert.ok(result.rescore.error_code);
});
test("public tool surface stays at 14 and new cases bind 10.0.14", () => {
assert.equal(PUBLIC_RECTIFICATION_TOOLS.length, 14);
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.14");
const deprecated = resolveExactSkillPackage(
"jyotish-birth-time-rectification",
"10.0.2",
"8d7aa2d4bea0414e9a89ef908ccbc8c708c98f79f5b78ae4f7dc229b5f7dbb30",
);
assert.equal(deprecated.status, "deprecated");
const plateauCandidates = [
{ candidateId: CANDIDATE_ID, time: "04:45", rank: 1, relativeSupport: 40, tiedMinuteCount: 25 },
{ candidateId: SECOND_CANDIDATE_ID, time: "04:46", rank: 2, relativeSupport: 35, tiedMinuteCount: 25 },
{ candidateId: THIRD_CANDIDATE_ID, time: "04:47", rank: 3, relativeSupport: 25, tiedMinuteCount: 25 },
];
const plateau = latestResultToolProjection({
resultId: RESULT_ID,
candidates: plateauCandidates,
selectionAllowed: true,
confirmationAllowed: true,
representativeTime: "04:45",
selectedTime: null,
selectionKind: null,
algorithmVersion: "rectification-v5",
decisionReceipt: {
acceptance_allowed: true,
selection_allowed: true,
propose_allowed: true,
confirmation_allowed: false,
inference_state: producedInferenceState(plateauCandidates.map((item) => ({
id: item.candidateId,
time: item.time,
relative_support: item.relativeSupport,
}))),
execution_ledger: [
{ method: "vimshottari-dasha", status: "executed" },
{ method: "narayana-dasha", status: "executed" },
],
},
}, collectDecision(plateauCandidates));
assert.equal(plateau.confirmation_allowed, false);
assert.equal(plateau.unique_minute_claim, false);
assert.match(String(plateau.skill_verification_report), /Dasha \+ Gochara/);
assert.match(String(plateau.skill_verification_report), /candidate_range_not_birth_time_truth/);
assert.doesNotMatch(String(plateau.skill_verification_report), UNIQUE_MINUTE_COPY);
assert.equal(plateau.session_outcome, "collect_evidence");
// Previously this asserted multi_adapter_consensus from execution names alone.
// The execution ledger does not prove same-range support or raw-result agreement.
assert.equal(plateau.rectification_label, "manual_pattern_consensus");
assert.deepEqual(plateau.executed_methods, ["vimshottari-dasha", "narayana-dasha"]);
const collecting = latestResultToolProjection({
resultId: RESULT_ID,
candidates: plateauCandidates,
selectionAllowed: true,
confirmationAllowed: false,
representativeTime: "04:45",
selectedTime: null,
selectionKind: null,
algorithmVersion: "rectification-v5",
decisionReceipt: {
execution_ledger: [
{ method: "vimshottari-dasha", status: "executed" },
{ method: "narayana-dasha", status: "executed" },
],
},
}, decideRectification({
engineCeiling: OPEN_ENGINE_CAPABILITY_CEILING,
methodCoverageAll: false,
trainingGateOpen: false,
datedEventCount: 2,
datedDomainCount: 2,
candidateScores: plateauCandidates.map((item) => ({
time: item.time,
score: item.relativeSupport,
})),
}));
assert.equal(collecting.rectification_label, "user_history_verification_required");
assert.equal(collecting.evidence_stop_reason, "insufficient_dated_events");
assert.deepEqual(collecting.executed_methods, ["vimshottari-dasha", "narayana-dasha"]);
const exhaustedCandidates = plateauCandidates.map((item, index) => ({
...item,
relativeSupport: index < 2 ? 40 : 20,
}));
const completed = latestResultToolProjection({
resultId: RESULT_ID,
candidates: exhaustedCandidates,
selectionAllowed: true,
confirmationAllowed: false,
representativeTime: "04:45",
selectedTime: null,
selectionKind: null,
algorithmVersion: "rectification-v5",
decisionReceipt: {
acceptance_allowed: true,
selection_allowed: true,
propose_allowed: true,
confirmation_allowed: false,
},
}, decideRectification({
engineCeiling: OPEN_ENGINE_CAPABILITY_CEILING,
methodCoverageAll: true,
trainingGateOpen: true,
datedEventCount: 3,
datedDomainCount: 2,
candidateScores: exhaustedCandidates.map((item) => ({
time: item.time,
score: item.relativeSupport,
})),
}));
assert.equal(completed.termination_copy, "当前最优结果是候选时间段,而不是已经确认的唯一出生分钟。临时代表时间仅用于下一轮验证与比较。");
const skill = readFileSync(new URL("../../skills/jyotish-birth-time-rectification/SKILL.md", import.meta.url), "utf8");
assert.match(skill, /method_followup_plan/);
assert.match(skill, /感情 → 事业 → 家人 → 职业 → 占问/);
assert.match(skill, /外貌、体质、胎记或疤痕不得追问/);
assert.match(skill, /KP 观察不计分、不挡提出门/);
assert.match(skill, /唯一领先和宽度≤5只挡确认门/);
assert.match(skill, /D9\/D10 类型表是校时方法/);
assert.doesNotMatch(skill, /±5 分钟确定性/);
assert.doesNotMatch(skill, /KP 政策跳过不挡提出门/);
const tools = readFileSync(new URL("../src/mastra/rectification-v9-tools.ts", import.meta.url), "utf8");
assert.match(tools, /function agentVisibleLatestProjection/);
assert.match(tools, /candidate_contrast_packet: _packet/);
assert.match(tools, /current_probe: null/);
});
test("Mastra hides active candidates when the receipt range is corrupted", () => {
const candidates = [
{ candidateId: CANDIDATE_ID, time: "05:00", rank: 1, relativeSupport: 58, tiedMinuteCount: 1 },
{ candidateId: SECOND_CANDIDATE_ID, time: "05:07", rank: 2, relativeSupport: 42, tiedMinuteCount: 1 },
];
const inferenceState = producedInferenceState([
{ id: CANDIDATE_ID, time: "05:00", relative_support: 58 },
{ id: SECOND_CANDIDATE_ID, time: "05:07", relative_support: 42 },
]);
const latest = {
resultId: RESULT_ID,
candidates,
selectionAllowed: true,
confirmationAllowed: false,
representativeTime: "05:00",
selectedTime: null,
selectionKind: null,
algorithmVersion: "rectification-v5",
decisionReceipt: {
acceptance_allowed: true,
selection_allowed: true,
propose_allowed: true,
confirmation_allowed: false,
inference_state: inferenceState,
},
};
const session = decideRectification({
engineCeiling: OPEN_ENGINE_CAPABILITY_CEILING,
methodCoverageAll: true,
userStopped: true,
candidateScores: candidates.map((item) => ({ time: item.time, score: item.relativeSupport })),
holdoutValidation: "passed",
snapshotCurrent: true,
trainingGateOpen: true,
});
const valid = latestResultToolProjection(latest, session);
assert.deepEqual((valid.candidates as typeof candidates).map((item) => item.time), ["05:00", "05:07"]);
assert.equal(valid.selection_allowed, true);
const invalid = latestResultToolProjection({
...latest,
decisionReceipt: {
...latest.decisionReceipt,
inference_state: { ...inferenceState, credible_range: ["04:00", "04:10"] },
},
}, session);
assert.deepEqual(invalid.candidates, []);
assert.equal(invalid.representative_time, null);
assert.equal(invalid.selection_allowed, false);
});
test("family then unused dated collect then occupation then horary follow the method plan without appearance or marks", () => {
const afterFamily = buildMethodFollowupPlan({
evidence: [
{ status: "confirmed", domain: "education", datePrecision: "year", occurredFrom: "2016-01-01", occurredTo: null },
{ status: "confirmed", domain: "relationship", datePrecision: "year", occurredFrom: "2018-01-01", occurredTo: null },
{ status: "confirmed", domain: "career", datePrecision: "year", occurredFrom: "2019-01-01", occurredTo: null },
{ status: "confirmed", domain: "family", datePrecision: "year", occurredFrom: "2020-01-01", occurredTo: null },
],
});
// 原值: occupation
// 新值: d2_finance
// 原因: 家人之后按 dated 顺序收财务/搬家/健康,职业排在后面(BUG-546)
assert.equal(afterFamily.next_followup?.method_id, "d2_finance");
assert.equal(afterFamily.next_followup?.domain, "finance");
assert.equal(afterFamily.methods.find((item) => item.method_id === "appearance")?.status, "skipped_by_policy");
assert.equal(afterFamily.methods.find((item) => item.method_id === "marks")?.status, "skipped_by_policy");
assert.equal(conversationalSessionOutcome({
selectionAllowed: true,
proposeAllowed: true,
confirmationAllowed: false,
nextFollowup: afterFamily.next_followup,
methods: afterFamily.methods,
}), "collect_evidence");
assert.equal(afterFamily.next_followup?.choice_frame, null);
assert.match(afterFamily.next_followup?.user_prompt_hint ?? "", /自然语言/);
assert.doesNotMatch(afterFamily.next_followup?.user_prompt_hint ?? "", /A\/B\/C\/D/);
const afterDated = buildMethodFollowupPlan({
evidence: [
{ status: "confirmed", domain: "education", datePrecision: "year", occurredFrom: "2016-01-01", occurredTo: null },
{ status: "confirmed", domain: "relationship", datePrecision: "year", occurredFrom: "2018-01-01", occurredTo: null },
{ status: "confirmed", domain: "career", datePrecision: "year", occurredFrom: "2019-01-01", occurredTo: null },
{ status: "confirmed", domain: "family", datePrecision: "year", occurredFrom: "2020-01-01", occurredTo: null },
{ status: "confirmed", domain: "finance", datePrecision: "year", occurredFrom: "2021-01-01", occurredTo: null },
{ status: "confirmed", domain: "relocation", datePrecision: "year", occurredFrom: "2015-01-01", occurredTo: null },
{ status: "confirmed", domain: "health_pressure", datePrecision: "year", occurredFrom: "2017-01-01", occurredTo: null },
],
});
assert.equal(afterDated.next_followup?.method_id, "occupation");
assert.equal(afterDated.next_followup?.domain, "occupation");
const afterOccupation = buildMethodFollowupPlan({
evidence: [
{ status: "confirmed", domain: "education", datePrecision: "year", occurredFrom: "2016-01-01", occurredTo: null },
{ status: "confirmed", domain: "relationship", datePrecision: "year", occurredFrom: "2018-01-01", occurredTo: null },
{ status: "confirmed", domain: "career", datePrecision: "year", occurredFrom: "2019-01-01", occurredTo: null },
{ status: "confirmed", domain: "family", datePrecision: "year", occurredFrom: "2020-01-01", occurredTo: null },
{ status: "confirmed", domain: "finance", datePrecision: "year", occurredFrom: "2021-01-01", occurredTo: null },
{ status: "confirmed", domain: "relocation", datePrecision: "year", occurredFrom: "2015-01-01", occurredTo: null },
{ status: "confirmed", domain: "health_pressure", datePrecision: "year", occurredFrom: "2017-01-01", occurredTo: null },
{ status: "confirmed", domain: "occupation", datePrecision: "unknown", occurredFrom: null, occurredTo: null },
],
});
assert.equal(afterOccupation.next_followup?.method_id, "horary");
assert.match(afterOccupation.next_followup?.user_prompt_hint ?? "", /第 10 宫|D10|占问/);
assert.match(afterOccupation.next_followup?.user_prompt_hint ?? "", /自然语言/);
assert.doesNotMatch(afterOccupation.next_followup?.user_prompt_hint ?? "", /A\/B\/C\/D/);
assert.doesNotMatch(JSON.stringify(afterFamily), /外貌|疤痕|胎记/);
const afterHorary = buildMethodFollowupPlan({
evidence: [
{ status: "confirmed", domain: "education", datePrecision: "year", occurredFrom: "2016-01-01", occurredTo: null },
{ status: "confirmed", domain: "relationship", datePrecision: "year", occurredFrom: "2018-01-01", occurredTo: null },
{ status: "confirmed", domain: "career", datePrecision: "year", occurredFrom: "2019-01-01", occurredTo: null },
{ status: "confirmed", domain: "family", datePrecision: "year", occurredFrom: "2020-01-01", occurredTo: null },
{ status: "confirmed", domain: "finance", datePrecision: "year", occurredFrom: "2021-01-01", occurredTo: null },
{ status: "confirmed", domain: "relocation", datePrecision: "year", occurredFrom: "2015-01-01", occurredTo: null },
{ status: "confirmed", domain: "health_pressure", datePrecision: "year", occurredFrom: "2017-01-01", occurredTo: null },
{ status: "confirmed", domain: "occupation", datePrecision: "unknown", occurredFrom: null, occurredTo: null },
{ status: "confirmed", domain: "horary", datePrecision: "day", occurredFrom: "2024-01-01", occurredTo: null },
],
});
assert.equal(afterHorary.next_followup, null);
});
test("precision stage lagna_frame waits for uncovered career before asking another dated event", () => {
const plan = buildMethodFollowupPlan({
evidence: [
{ status: "confirmed", domain: "education", datePrecision: "year", occurredFrom: "2016-01-01", occurredTo: null },
{ status: "confirmed", domain: "relationship", datePrecision: "year", occurredFrom: "2018-01-01", occurredTo: null },
],
precisionStage: "lagna_frame",
});
// 原值: relatives
// 新值: d10_career
// 原因: dated 分支移到家人之后,前两问恢复感情 / 事业
assert.equal(plan.next_followup?.method_id, "d10_career");
assert.equal(plan.next_followup?.source, "method_coverage");
assert.equal(isOfferBlockingFollowup(plan.next_followup, plan.methods), true);
assert.doesNotMatch(JSON.stringify(plan), UNIQUE_MINUTE_COPY);
});
test("lagna_frame after classic coverage does not keep a tie in discrimination without a remaining split", () => {
const plan = buildMethodFollowupPlan({
evidence: CLASSIC_COVERAGE,
precisionStage: "lagna_frame",
});
// 原值: precision_stage / dated_event
// 新值: 财务采集
// 原因: 未用带年份域仍能区分分钟时,精度层让路(BUG-546)
assert.equal(plan.next_followup?.source, "method_coverage");
assert.equal(plan.next_followup?.domain, "finance");
assert.equal(plan.next_followup?.intent, "collect_method_evidence");
assert.equal(conversationalSessionOutcome({
selectionAllowed: true,
proposeAllowed: true,
confirmationAllowed: false,
nextFollowup: plan.next_followup,
methods: plan.methods,
discriminatorProbe: null,
candidateScores: [
{ time: "05:00", score: 34 },
{ time: "05:01", score: 33 },
{ time: "05:02", score: 33 },
],
// 原断言 provisional_range(并列不采用)→ 新断言 adopt_representative。
}), "adopt_representative");
});
test("precision stage d4 asks home change not family, and d5 asks education", () => {
const covered = CLASSIC_COVERAGE;
const d4 = buildMethodFollowupPlan({
evidence: covered,
precisionStage: "d4_refine",
});
// 原值: precision_stage / d4_home
// 新值: 财务采集
// 原因: 家人之后先收未用带年份域(BUG-546)
assert.equal(d4.next_followup?.source, "method_coverage");
assert.equal(d4.next_followup?.method_id, "d2_finance");
assert.equal(d4.next_followup?.domain, "finance");
assert.doesNotMatch(d4.next_followup?.user_prompt_hint ?? "", /家人/);
const datedThenHome = [
...covered,
{ status: "confirmed" as const, domain: "finance", datePrecision: "year" as const, occurredFrom: "2021-01-01", occurredTo: null },
{ status: "confirmed" as const, domain: "relocation", datePrecision: "year" as const, occurredFrom: "2015-01-01", occurredTo: null },
{ status: "confirmed" as const, domain: "health_pressure", datePrecision: "year" as const, occurredFrom: "2017-01-01", occurredTo: null },
];
const d4AfterDated = buildMethodFollowupPlan({
evidence: datedThenHome,
precisionStage: "d4_refine",
});
assert.equal(d4AfterDated.next_followup?.source, "precision_stage");
assert.equal(d4AfterDated.next_followup?.method_id, "d4_home");
assert.equal(d4AfterDated.next_followup?.domain, "relocation");
assert.equal(d4AfterDated.next_followup?.ask_theme, "home_change");
const legacyTheme = buildMethodFollowupPlan({
evidence: datedThenHome,
precisionStage: "theme_refine",
});
assert.equal(legacyTheme.next_followup?.ask_theme, "home_change");
const d5 = buildMethodFollowupPlan({
evidence: covered.filter((item) => item.domain !== "education"),
precisionStage: "d5_refine",
});
// 原值: d5_education 精度层
// 新值: 学业采集(education 尚未覆盖)
assert.equal(d5.next_followup?.method_id, "d5_education");
assert.equal(d5.next_followup?.domain, "education");
assert.equal(d5.next_followup?.intent, "collect_method_evidence");
assert.doesNotMatch(JSON.stringify(d4), UNIQUE_MINUTE_COPY);
});
test("accepted representative time reverse-verifies predicted events then hands off", () => {
const careerProbe = {
year: 2018,
year_label: "2018 年前后",
domain: "career" as const,
event_family: "入职、升职或职责明显加重",
source: "dasha_activation" as const,
tracks: ["vimshottari", "narayana"] as const,
tracks_agree: true,
unique_minute_claim: false as const,
user_meaning: "年份锁定 2018 年前后。请写成一句自然语言,问是否入职或职责加重。",
role: "reverse_verify" as const,
style_options: DYNAMIC_STYLE_OPTIONS,
};
const plan = buildMethodFollowupPlan({
evidence: [{
status: "confirmed",
domain: "education",
datePrecision: "month",
occurredFrom: "2016-06-01",
occurredTo: null,
}],
accepted: true,
eventProbes: [
{
...careerProbe,
domain: "education",
source: "known_event_quality",
role: "distinguish",
event_family: "高考或重要考试发挥明显失常、压力很大",
},
careerProbe,
],
oosBlindPrompts: [{
domain: "family",
user_meaning: "校时还没用过家人这条线。有没有一件没提过、但记得大概时间的家人变化?",
used_for_scoring: false,
}],
activeFocus: {
intent: "collect_method_evidence",
targetDomain: "relationship",
targetKind: null,
},
});
assert.equal(plan.next_followup?.method_id, "reverse_verify");
assert.equal(plan.next_followup?.domain, "career");
assert.equal(plan.next_followup?.choice_frame?.scoring, true);
assert.match(plan.next_followup?.choice_frame?.why ?? "", /2018 年前后/);
assert.equal(plan.deferred_followup, null);
assert.equal(plan.next_followup?.source, "reverse_verify");
const action = buildNextUserAction({
scorableCount: 3,
evidenceCount: 3,
hasLatestResult: true,
selectionAllowed: true,
sessionOutcome: "adopt_representative",
nextFollowup: plan.next_followup,
workingTime: "05:07",
accepted: true,
});
assert.equal(action.id, "verify_adopted_time");
assert.equal(action.on_user_stop.id, "start_consultation");
assert.doesNotMatch(JSON.stringify({ plan, action }), UNIQUE_MINUTE_COPY);
});
test("accepted representative time without remaining probes hands off to consultation", () => {
const plan = buildMethodFollowupPlan({
evidence: [{
status: "confirmed",
domain: "education",
datePrecision: "month",
occurredFrom: "2016-06-01",
occurredTo: null,
}],
accepted: true,
eventProbes: [{
year: 2016,
year_label: "2016 年前后",
domain: "education",
event_family: "高考或重要考试发挥明显失常、压力很大",
source: "known_event_quality",
tracks: ["vimshottari", "narayana"],
tracks_agree: true,
unique_minute_claim: false,
user_meaning: "年份锁定 2016 年前后。已有高考经历。",
role: "distinguish",
}],
});
assert.equal(plan.next_followup, null);
assert.equal(plan.deferred_followup, null);
const action = buildNextUserAction({
scorableCount: 3,
evidenceCount: 3,
hasLatestResult: true,
selectionAllowed: true,
sessionOutcome: "adopt_representative",
nextFollowup: plan.next_followup,
workingTime: "05:07",
accepted: true,
});
assert.equal(action.id, "start_consultation");
assert.match(action.user_meaning, /看盘/);
assert.match(action.user_meaning, /改选/);
assert.doesNotMatch(JSON.stringify({ plan, action }), UNIQUE_MINUTE_COPY);
});
test("confirmed relationship evidence skips generic D9 followups unless a real probe is bound", () => {
const evidence = CLASSIC_COVERAGE;
const precision = buildMethodFollowupPlan({ evidence, precisionStage: "d9_refine" });
const varga = buildMethodFollowupPlan({
evidence,
observations: [{ layer: "d9", candidates_differ: true, ask_theme: "relationship_style" }],
});
assert.notEqual(precision.next_followup?.domain, "relationship");
assert.notEqual(varga.next_followup?.domain, "relationship");
const probed = buildMethodFollowupPlan({
evidence,
precisionStage: "d9_refine",
contrastPacket: {
candidateSetVersion: "05:00-05:14",
vargaDifferences: [],
probes: [{
probeId: "contrast:varga.d9.05:00|05:14",
candidateSetVersion: "05:00-05:14",
question: "当前几个候选在关系盘上还分得开。",
expectedOutcomes: [
{ outcomeId: "yes", supportsCandidateIds: ["05:00"], conflictsCandidateIds: ["05:14"] },
{ outcomeId: "weak_yes", supportsCandidateIds: ["05:14"], conflictsCandidateIds: ["05:00"] },
],
candidateSplitHash: "varga.d9.05:00|05:14",
informationGain: 1.4,
sourceFeatures: [{ technique: "D9", calculationResultId: RESULT_ID }],
domain: "relationship",
year: null,
semanticKey: "varga.d9.05:00|05:14",
choiceKind: "varga_style",
styleOptions: [
{ label: "相处更主动热情", answerClass: "yes", sign: "白羊座" },
{ label: "相处更深刻占有", answerClass: "weak_yes", sign: "天蝎座" },
],
}],
},
eventProbes: [{
...CAREER_CONFLICT_PROBE,
year: 2021,
year_label: "2021 年前后",
domain: "relationship",
event_family: "关系状态或相处方式明显变化",
semantic_key: "relationship.2021.dasha_activation",
candidate_split_hash: "set-test:relationship:2021",
}],
});
assert.equal(probed.next_followup?.domain, "relationship");
assert.equal(probed.next_followup?.source, "event_probe");
assert.equal(probed.next_followup?.semantic_key, "varga.d9.05:00|05:14");
assert.doesNotMatch(probed.next_followup?.semantic_key ?? "", /relationship\.2021/);
});
test("d9_refine after relationship still asks uncovered career first", () => {
const plan = buildMethodFollowupPlan({
evidence: [
{ status: "confirmed", domain: "education", datePrecision: "month", occurredFrom: "2016-09-01", occurredTo: null },
{ status: "confirmed", domain: "education", datePrecision: "month", occurredFrom: "2020-06-01", occurredTo: null },
{ status: "confirmed", domain: "relationship", datePrecision: "day", occurredFrom: "2024-05-01", occurredTo: null },
{ status: "confirmed", domain: "relationship", datePrecision: "day", occurredFrom: "2024-08-08", occurredTo: null },
],
precisionStage: "d9_refine",
});
assert.equal(plan.next_followup?.method_id, "d10_career");
assert.equal(plan.next_followup?.source, "method_coverage");
assert.equal(conversationalSessionOutcome({
selectionAllowed: true,
proposeAllowed: false,
confirmationAllowed: false,
nextFollowup: plan.next_followup,
methods: plan.methods,
}), "collect_evidence");
assert.equal(conversationalSessionOutcome({
selectionAllowed: true,
proposeAllowed: true,
confirmationAllowed: false,
nextFollowup: plan.next_followup,
methods: plan.methods,
}), "collect_evidence");
});
test("career evidence does not cover occupation method; occupation no longer blocks adopt", () => {
const plan = buildMethodFollowupPlan({
evidence: [
{ status: "confirmed", domain: "education", datePrecision: "year", occurredFrom: "2016-01-01", occurredTo: null },
{ status: "confirmed", domain: "relationship", datePrecision: "day", occurredFrom: "2024-05-01", occurredTo: null },
{ status: "confirmed", domain: "career", datePrecision: "day", occurredFrom: "2019-09-01", occurredTo: null },
{ status: "confirmed", domain: "family", datePrecision: "year", occurredFrom: "2020-01-01", occurredTo: null },
],
});
// 原值: occupation
// 新值: d2_finance
// 原因: 家人之后先收财务经历;职业仍未覆盖,也不挡 adopt 能力(BUG-546
assert.equal(plan.next_followup?.method_id, "d2_finance");
assert.equal(plan.methods.find((item) => item.method_id === "occupation")?.status, "uncovered");
assert.equal(blockingMethodsCovered(plan.methods), true);
assert.equal(conversationalSessionOutcome({
selectionAllowed: true,
proposeAllowed: true,
confirmationAllowed: false,
nextFollowup: plan.next_followup,
methods: plan.methods,
candidateScores: [
{ time: "05:00", score: 34 },
{ time: "05:01", score: 33 },
{ time: "05:02", score: 33 },
],
}), "adopt_representative");
});
test("draft occupation_note without a date covers occupation and does not adopt a tie", () => {
const plan = buildMethodFollowupPlan({
evidence: [
{ status: "confirmed", domain: "education", datePrecision: "year", occurredFrom: "2016-01-01", occurredTo: null },
{ status: "confirmed", domain: "relationship", datePrecision: "day", occurredFrom: "2024-05-01", occurredTo: null },
{ status: "confirmed", domain: "career", datePrecision: "day", occurredFrom: "2019-09-01", occurredTo: null },
{ status: "confirmed", domain: "family", datePrecision: "year", occurredFrom: "2020-01-01", occurredTo: null },
{
status: "draft",
domain: "occupation",
datePrecision: "unknown",
occurredFrom: null,
occurredTo: null,
eventKind: "occupation_note",
summary: "职业类型轨迹为技术开发",
},
],
});
assert.equal(plan.methods.find((item) => item.method_id === "occupation")?.status, "covered");
assert.notEqual(plan.next_followup?.method_id, "occupation");
// 原断言 sessionOutcome≠adopt_representative(并列不采用)→ 新断言 adopt_representative。
// 为什么:occupation 覆盖后并列候选仍应交付代表性采用;确认门保持关闭。
assert.equal(conversationalSessionOutcome({
selectionAllowed: true,
proposeAllowed: true,
confirmationAllowed: false,
nextFollowup: plan.next_followup,
methods: plan.methods,
candidateScores: [
{ time: "05:00", score: 34 },
{ time: "05:01", score: 33 },
{ time: "05:02", score: 33 },
],
}), "adopt_representative");
});
test("stale occupation collect focus does not keep interviewing after occupation is covered", () => {
const plan = buildMethodFollowupPlan({
evidence: [
{ status: "confirmed", domain: "education", datePrecision: "year", occurredFrom: "2016-01-01", occurredTo: null },
{ status: "confirmed", domain: "relationship", datePrecision: "day", occurredFrom: "2024-05-01", occurredTo: null },
{ status: "confirmed", domain: "career", datePrecision: "day", occurredFrom: "2019-09-01", occurredTo: null },
{ status: "confirmed", domain: "family", datePrecision: "year", occurredFrom: "2020-01-01", occurredTo: null },
{
status: "draft",
domain: "occupation",
datePrecision: "unknown",
occurredFrom: null,
occurredTo: null,
eventKind: "occupation_note",
summary: "前端工程师",
},
],
activeFocus: {
intent: "collect_method_evidence",
targetDomain: "occupation",
targetKind: "occupation_note",
},
});
// 原断言 sessionOutcome≠adopt_representative → 新断言 adopt_representative。
// 为什么:职业已覆盖后不得继续追问;并列候选走代表性采用,而不是停在无出口访谈。
assert.equal(conversationalSessionOutcome({
selectionAllowed: true,
proposeAllowed: true,
confirmationAllowed: false,
nextFollowup: plan.next_followup,
methods: plan.methods,
candidateScores: [
{ time: "05:00", score: 34 },
{ time: "05:01", score: 33 },
{ time: "05:02", score: 33 },
],
}), "adopt_representative");
});
test("D9/D10 contrast after occupation coverage asks a discriminator, not adopt", () => {
const plan = buildMethodFollowupPlan({
evidence: CLASSIC_COVERAGE.filter((item) => item.domain !== "horary"),
contrastPacket: {
candidateSetVersion: "05:00-05:02",
vargaDifferences: [
{ layer: "d9", signs: ["天秤", "天蝎", "射手"] },
{ layer: "d10", signs: ["巨蟹", "狮子", "处女"] },
],
probes: [{
probeId: "contrast:varga.d10.05:00|05:01|05:02",
candidateSetVersion: "05:00-05:02",
question: "当前几个候选在事业盘上还分得开。请核对一段还没用进评分的职业前事。",
expectedOutcomes: [
{ 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.05:00|05:01|05:02",
informationGain: 0.12,
sourceFeatures: [{ technique: "D10", calculationResultId: RESULT_ID }],
domain: "career",
year: null,
semanticKey: "varga.d10.05:00|05:01|05:02",
choiceKind: "varga_style",
styleOptions: [
{ label: "做事偏领导推进", answerClass: "yes", sign: "白羊座" },
{ label: "做事偏研究转化", answerClass: "weak_yes", sign: "天蝎座" },
],
}],
},
});
assert.equal(plan.next_followup?.intent, "distinguish_candidates");
assert.match(plan.next_followup?.user_prompt_hint ?? "", /职业前事|事业盘/);
assert.equal(conversationalSessionOutcome({
selectionAllowed: true,
proposeAllowed: true,
confirmationAllowed: false,
nextFollowup: plan.next_followup,
methods: plan.methods,
discriminatorProbe: {
probeId: "contrast:varga.d10",
candidateSetVersion: "05:00-05:02",
question: "核对一段还没用进评分的职业前事",
expectedOutcomes: [
{ outcomeId: "a", supportsCandidateIds: ["05:00"], conflictsCandidateIds: ["05:01"] },
{ outcomeId: "b", supportsCandidateIds: ["05:01"], conflictsCandidateIds: ["05:00"] },
],
candidateSplitHash: "varga.d10",
informationGain: 0.12,
sourceFeatures: [{ technique: "D10", calculationResultId: RESULT_ID }],
domain: "career",
year: null,
semanticKey: "varga.d10",
},
candidateScores: [
{ time: "05:00", score: 34 },
{ time: "05:01", score: 33 },
{ time: "05:02", score: 33 },
],
}), "discriminate_candidates");
});
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: [
{ layer: "d10", signs: ["巨蟹座", "狮子座", "处女座"] },
{ layer: "d24", signs: ["05:00", "05:06|05:07"] },
],
probes: [{
probeId: "contrast:varga.d24.05:00/05:06|05:07",
candidateSetVersion: "05:00-05:07",
question: "当前几个候选在学业盘上还分得开。请核对一段还没用进评分的学业前事。",
expectedOutcomes: [
{ outcomeId: "yes", supportsCandidateIds: ["05:00"], conflictsCandidateIds: ["05:06", "05:07"] },
{ outcomeId: "no", supportsCandidateIds: ["05:06", "05:07"], conflictsCandidateIds: ["05:00"] },
],
candidateSplitHash: "varga.d24.05:00/05:06|05:07",
informationGain: 0.16,
sourceFeatures: [{ technique: "D24", calculationResultId: RESULT_ID }],
domain: "education",
year: null,
semanticKey: "varga.d24.05:00/05:06|05:07",
}],
};
const evidence = [
...CLASSIC_COVERAGE.filter((item) => item.domain !== "horary"),
{
status: "draft" as const,
domain: "occupation",
datePrecision: "unknown" as const,
occurredFrom: null,
occurredTo: null,
eventKind: "occupation_note",
summary: "医疗器械算法,第三个(技术执行)",
},
];
const plan = buildMethodFollowupPlan({
evidence,
contrastPacket: packet,
});
assert.equal(plan.next_followup?.domain, "education");
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("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: [{
...CAREER_CONFLICT_PROBE,
year: 2023,
year_label: "2023 年前后",
semantic_key: "career.2023.dasha_activation",
information_gain: 0.56,
candidate_split_hash: "set-test:career:2023",
}],
contrastPacket: {
candidateSetVersion: "05:00-05:14",
vargaDifferences: [],
probes: [{
probeId: "contrast:varga.d24.05:00/05:07|05:10|05:14",
candidateSetVersion: "05:00-05:14",
question: "当前几个候选在学业盘上还分得开。",
expectedOutcomes: [
{ outcomeId: "yes", supportsCandidateIds: ["05:00"], conflictsCandidateIds: ["05:07", "05:10", "05:14"] },
{ outcomeId: "no", supportsCandidateIds: ["05:07", "05:10", "05:14"], conflictsCandidateIds: ["05:00"] },
],
candidateSplitHash: "varga.d24.05:00/05:07|05:10|05:14",
informationGain: 2.503258,
sourceFeatures: [{ technique: "D24", calculationResultId: RESULT_ID }],
domain: "education",
year: null,
semanticKey: "varga.d24.05:00/05:07|05:10|05:14",
choiceKind: "event_quality",
}],
},
candidatesSeparated: false,
});
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/);
});
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 asks dated family collect", () => {
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,
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",
}],
});
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.equal(plan.dropped_probes.some((item) => (
item.semantic_key.startsWith("varga.d12") && item.reason === "yearless_ungrounded_contrast"
)), true);
});
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);
// 旧:year_label / spoken 带「2021 年前后,」前缀
// 新:probe_year 仍是 2021,口语等于 USER_COLLECT_QUESTION.family,不填 year_label
// 原因:决策 7,年份只用于计分与去重,不进采集题干
assert.equal(plan.next_followup?.year_label, undefined);
assert.doesNotMatch(plan.next_followup?.semantic_key ?? "", /varga\.d24/);
assert.equal(spokenFollowupForUser(plan.next_followup), USER_COLLECT_QUESTION.family);
});
test("already-open yearless family card is abandoned instead of kept as 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,
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",
}],
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.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);
});
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: [{
...CAREER_CONFLICT_PROBE,
year: 2023,
year_label: "2023 年前后",
semantic_key: "career.2023.dasha_activation",
information_gain: 0.56,
candidate_split_hash: "set-test:career:2023",
}],
contrastPacket: {
candidateSetVersion: "05:00-05:14",
vargaDifferences: [],
probes: [{
probeId: "contrast:varga.d24.05:00/05:07|05:10|05:14",
candidateSetVersion: "05:00-05:14",
question: "当前几个候选在学业盘上还分得开。",
expectedOutcomes: [
{ outcomeId: "yes", supportsCandidateIds: ["05:00"], conflictsCandidateIds: ["05:07", "05:10", "05:14"] },
{ outcomeId: "no", supportsCandidateIds: ["05:07", "05:10", "05:14"], conflictsCandidateIds: ["05:00"] },
],
candidateSplitHash: "varga.d24.05:00/05:07|05:10|05:14",
informationGain: 2.503258,
sourceFeatures: [{ technique: "D24", calculationResultId: RESULT_ID }],
domain: "education",
year: null,
semanticKey: "varga.d24.05:00/05:07|05:10|05:14",
choiceKind: "event_quality",
}],
},
candidatesSeparated: false,
activeFocus: {
intent: "distinguish_candidates",
targetDomain: "career",
targetKind: "career_entry",
expectedAnswerSchema: {
semantic_key: "career.2023.dasha_activation",
candidate_split_hash: "set-test: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 = [
{
status: "confirmed" as const,
domain: "education",
datePrecision: "month" as const,
occurredFrom: "2016-09-01",
occurredTo: null,
eventKind: "education_start",
summary: "上大学",
},
{
status: "confirmed" as const,
domain: "education",
datePrecision: "month" as const,
occurredFrom: "2015-06-01",
occurredTo: null,
eventKind: "education_interruption",
summary: "高考失利复读",
},
{
status: "confirmed" as const,
domain: "relationship",
datePrecision: "day" as const,
occurredFrom: "2024-05-01",
occurredTo: null,
eventKind: "relationship_start",
summary: "开始一段感情",
},
{
status: "confirmed" as const,
domain: "relationship",
datePrecision: "day" as const,
occurredFrom: "2024-08-08",
occurredTo: null,
eventKind: "relationship_end",
summary: "感情结束",
},
{
status: "confirmed" as const,
domain: "career",
datePrecision: "day" as const,
occurredFrom: "2024-04-07",
occurredTo: null,
eventKind: "career_entry",
summary: "入职",
},
{
status: "confirmed" as const,
domain: "family",
datePrecision: "month" as const,
occurredFrom: "2016-05-01",
occurredTo: null,
eventKind: "family_event",
summary: "家人变化",
},
{
status: "confirmed" as const,
domain: "occupation",
datePrecision: "unknown" as const,
occurredFrom: null,
occurredTo: null,
eventKind: "occupation_note",
summary: "互联网程序员 / 前端 / Agent 开发",
},
];
const DUMP_TRANSITIONS = [
{ layer: "d4", at: "05:00" },
{ layer: "d4", at: "05:03" },
{ layer: "d10", at: "05:00" },
{ layer: "d10", at: "05:03" },
{ layer: "d24", at: "05:00" },
{ layer: "d24", at: "05:03" },
{ layer: "d9", at: "04:52" },
{ layer: "d9", at: "05:08" },
{ layer: "d5", at: "05:15" },
];
const DUMP_SCORES = [
{ time: "05:00", score: 34 },
{ time: "05:03", score: 33 },
{ time: "05:04", score: 33 },
];
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),
transitions: DUMP_TRANSITIONS,
mentionedKeys: mentionedVargaKeysFromLedgerEvidence(DUMP_COVERAGE),
});
const plan = buildMethodFollowupPlan({
evidence: DUMP_COVERAGE,
contrastPacket: packet,
});
assert.equal(plan.next_followup?.domain, "relocation");
assert.equal(plan.next_followup?.kind_hint, "home_change");
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.equal(conversationalSessionOutcome({
selectionAllowed: true,
proposeAllowed: true,
confirmationAllowed: false,
nextFollowup: plan.next_followup,
methods: plan.methods,
discriminatorProbe: packet.probes[0] ?? null,
candidateScores: DUMP_SCORES,
}), "discriminate_candidates");
});
test("provisional range still exposes method coverage followup", () => {
const packet = buildCandidateContrastPacket({
candidateSetVersion: "05:00-05:04",
candidateTimes: DUMP_SCORES.map((item) => item.time),
transitions: DUMP_TRANSITIONS,
askedKeys: ["varga.d4", "varga.d5", "varga.d9", "varga.d10", "varga.d24"],
});
const plan = buildMethodFollowupPlan({
evidence: [
...DUMP_COVERAGE,
{
status: "confirmed" as const,
domain: "relocation",
datePrecision: "year" as const,
occurredFrom: "2016-09-01",
occurredTo: null,
eventKind: "home_change",
summary: "搬家离乡",
},
{
status: "confirmed" as const,
domain: "finance",
datePrecision: "year" as const,
occurredFrom: "2021-01-01",
occurredTo: null,
eventKind: "finance_change",
summary: "收入变化",
},
{
status: "confirmed" as const,
domain: "health_pressure",
datePrecision: "year" as const,
occurredFrom: "2017-01-01",
occurredTo: null,
eventKind: "self_health_event",
summary: "健康压力",
},
],
contrastPacket: packet,
sessionOutcome: "provisional_range",
});
assert.equal(plan.next_followup?.intent, "collect_method_evidence");
assert.equal(plan.next_followup?.domain, "horary");
assert.equal(plan.deferred_followup, null);
assert.equal(conversationalSessionOutcome({
selectionAllowed: true,
proposeAllowed: true,
confirmationAllowed: false,
nextFollowup: {
method_id: "d9_relationship",
intent: "distinguish_candidates",
ask_theme: "relationship_style",
domain: "relationship",
kind_hint: "relationship_change",
user_prompt_hint: "当前候选在关系主题上仍分不开。",
must_not_label: false,
choice_frame: null,
source: "varga_observation",
},
methods: plan.methods,
discriminatorProbe: null,
candidateScores: DUMP_SCORES,
// 原断言 provisional_range → 新断言 adopt_representative。
}), "adopt_representative");
const action = buildNextUserAction({
scorableCount: 6,
evidenceCount: 8,
hasLatestResult: true,
selectionAllowed: true,
sessionOutcome: "provisional_range",
nextFollowup: null,
workingTime: "05:00",
});
assert.equal(action.id, "offer_provisional_range");
});
test("structured paused state ends evidence collection without parsing user copy", () => {
assert.equal(conversationalSessionOutcome({
selectionAllowed: true,
proposeAllowed: true,
confirmationAllowed: false,
nextFollowup: null,
methods: buildMethodFollowupPlan({
evidence: CLASSIC_COVERAGE.filter((item) => item.domain !== "horary"),
}).methods,
userStopped: true,
candidateScores: [
{ time: "05:00", score: 34 },
{ time: "05:06", score: 33 },
{ time: "05:07", score: 33 },
],
// 原断言 provisional_range_user_stopped → 新断言 adopt_representative。
// 为什么:用户停止后仍可交付代表性采用;validated / 确认门保持关闭。
}), "adopt_representative");
});
test("same-year existence probes are skipped; a different-year dasha still ranks", () => {
const evidence = [
datedEvidence("education", "2016"),
datedEvidence("relationship", "2018"),
datedEvidence("career", "2015"),
datedEvidence("family", "2023"),
];
const skipped = buildMethodFollowupPlan({
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.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",
candidateSetVersion: "05:00-05:14",
question: "当前几个候选在学业盘上还分得开。",
expectedOutcomes: [
{ outcomeId: "yes", supportsCandidateIds: ["05:00"], conflictsCandidateIds: ["05:07", "05:14"] },
{ outcomeId: "no", supportsCandidateIds: ["05:07", "05:14"], conflictsCandidateIds: ["05:00"] },
],
candidateSplitHash: "varga.d24.05:00|05:07|05:14",
informationGain: 2.5,
sourceFeatures: [{ technique: "D24", calculationResultId: RESULT_ID }],
domain: "education",
year: null,
semanticKey: "varga.d24.05:00|05:07|05:14",
choiceKind: "event_quality" as const,
};
const d10 = {
probeId: "contrast:varga.d10.05:00|05:07|05:14",
candidateSetVersion: "05:00-05:14",
question: "当前几个候选在事业盘上还分得开。",
expectedOutcomes: [
{ outcomeId: "yes", supportsCandidateIds: ["05:00"], conflictsCandidateIds: ["05:07", "05:14"] },
{ outcomeId: "weak_yes", supportsCandidateIds: ["05:07"], conflictsCandidateIds: ["05:00", "05:14"] },
{ outcomeId: "no", supportsCandidateIds: ["05:14"], conflictsCandidateIds: ["05:00", "05:07"] },
],
candidateSplitHash: "varga.d10.05:00|05:07|05:14",
informationGain: 3.1,
sourceFeatures: [{ technique: "D10", calculationResultId: RESULT_ID }],
domain: "career",
year: null,
semanticKey: "varga.d10.05:00|05:07|05:14",
choiceKind: "varga_style" as const,
styleOptions: [
{ label: "做事偏领导推进", answerClass: "yes" as const, sign: "白羊座" },
{ label: "做事偏研究转化", answerClass: "weak_yes" as const, sign: "天蝎座" },
],
};
const highest = buildMethodFollowupPlan({
evidence,
eventProbes: [{
...CAREER_CONFLICT_PROBE,
information_gain: 0.56,
semantic_key: "career.2018.dasha_activation",
}],
contrastPacket: {
candidateSetVersion: "05:00-05:14",
vargaDifferences: [],
probes: [d24, d10],
},
candidatesSeparated: false,
});
assert.equal(highest.next_followup?.semantic_key, d10.semanticKey);
assert.doesNotMatch(highest.next_followup?.semantic_key ?? "", /career\.2018/);
const d24Wins = buildMethodFollowupPlan({
evidence,
contrastPacket: {
candidateSetVersion: "05:00-05:14",
vargaDifferences: [],
probes: [d24, { ...d10, informationGain: 1.1 }],
},
candidatesSeparated: false,
});
assert.equal(d24Wins.next_followup?.semantic_key, d10.semanticKey);
});
test("three dated events with one holdout keep collecting instead of discriminating", () => {
const evidence = [
datedEvidence("education", "2016"),
datedEvidence("career", "2020"),
datedEvidence("relationship", "2024"),
{
status: "confirmed" as const,
domain: "occupation",
datePrecision: "unknown" as const,
occurredFrom: null,
occurredTo: null,
eventKind: "occupation_note",
},
];
const gate = trainingScoreableGate(evidence);
assert.equal(gate.open, false);
assert.equal(gate.trainingCount, 2);
assert.equal(gate.holdoutCount, 1);
const plan = buildMethodFollowupPlan({
evidence,
declinedTopics: [{ target_domain: "family", status: "declined" }],
precisionStage: "lagna_frame",
eventProbes: [CAREER_CONFLICT_PROBE],
});
// 原值: method_coverage / finance3847e9c9 新增 domain 锁)
// 新值: method_coverage / finance
// 原因: dated 分支在家人之后;家人已拒答,下一件带年份仍是财务,不是轮转抢跑。
assert.equal(plan.next_followup?.source, "method_coverage");
assert.equal(plan.next_followup?.intent, "collect_method_evidence");
assert.equal(plan.next_followup?.domain, "finance");
assert.notEqual(plan.next_followup?.source, "event_probe");
assert.notEqual(plan.next_followup, null);
assert.equal(conversationalSessionOutcome({
selectionAllowed: true,
proposeAllowed: true,
confirmationAllowed: false,
nextFollowup: plan.next_followup,
methods: plan.methods,
evidence,
candidateScores: [
{ time: "05:00", score: 34 },
{ time: "05:01", score: 33 },
{ time: "05:02", score: 33 },
],
discriminatorProbe: {
probeId: "p-cd",
candidateSetVersion: "set-test",
question: "2018 年前后事业是否有明显变化?",
expectedOutcomes: [
{ outcomeId: "yes", supportsCandidateIds: ["05:00"], conflictsCandidateIds: ["05:20"] },
{ outcomeId: "no", supportsCandidateIds: ["05:20"], conflictsCandidateIds: ["05:00"] },
],
candidateSplitHash: "split",
informationGain: 0.4,
sourceFeatures: [{ technique: "dasha_activation", calculationResultId: null }],
domain: "career",
year: 2018,
semanticKey: "career.2018",
},
}), "collect_evidence");
});
test("adjacent education year does not re-ask enrollment after a recorded start", () => {
const plan = buildMethodFollowupPlan({
evidence: [{
status: "confirmed",
domain: "education",
eventKind: "education_start",
datePrecision: "month",
occurredFrom: "2016-09-01",
occurredTo: null,
summary: "2016年9月上大学",
}, {
status: "confirmed",
domain: "relationship",
eventKind: "relationship_end",
datePrecision: "day",
occurredFrom: "2024-08-08",
occurredTo: "2024-08-08",
summary: "一段感情结束",
}],
eventProbes: [{
year: 2015,
year_label: "2015 年前后",
domain: "education",
event_family: "升学、高考、转学或学习环境变化",
source: "dasha_activation",
tracks: ["vimshottari", "narayana"],
tracks_agree: true,
unique_minute_claim: false,
user_meaning: "年份锁定 2015 年前后。事件家族:升学、高考、转学或学习环境变化。请写成一句自然语言是/否题。不得改年份。",
role: "reverse_verify",
information_gain: 0.21,
semantic_key: "education.2015.dasha_activation",
}],
});
assert.notEqual(plan.next_followup?.domain, "education");
assert.notEqual(plan.next_followup?.source, "event_probe");
// 原值: relatives
// 新值: d10_career
// 原因: dated 分支移到家人之后,前两问恢复感情 / 事业
assert.equal(plan.next_followup?.method_id, "d10_career");
assert.match(plan.next_followup?.user_prompt_hint ?? "", /2016 年入学/);
assert.match(plan.next_followup?.user_prompt_hint ?? "", /2024 年感情结束/);
assert.match(plan.next_followup?.user_prompt_hint ?? "", /不要再问这些事发生在哪一年/);
assert.doesNotMatch(plan.next_followup?.user_prompt_hint ?? "", /高考是 2015/);
});
test("high information_gain leftover probe still blocks offering after coverage", () => {
const plan = buildMethodFollowupPlan({
evidence: CLASSIC_COVERAGE.filter((item) => item.domain !== "horary"),
eventProbes: [{
year: 2018,
year_label: "2018 年前后",
domain: "relocation",
event_family: "搬家、离乡或长期异地",
source: "dasha_activation",
tracks: ["vimshottari", "narayana"],
tracks_agree: true,
unique_minute_claim: false,
user_meaning: "年份锁定 2018 年前后。请写成一句自然语言,问是否搬家。",
role: "distinguish",
phase: "candidate_discriminator",
information_gain: 0.21,
semantic_key: "relocation.2018.dasha_activation",
candidate_ids: ["05:00", "05:20"],
expected_outcomes: [
{ answer_class: "yes", supports: ["05:00"], conflicts: ["05:20"] },
{ answer_class: "no", supports: ["05:20"], conflicts: ["05:00"] },
],
}],
});
assert.equal(plan.next_followup?.source, "event_probe");
assert.equal(isOfferBlockingFollowup(plan.next_followup, plan.methods), true);
assert.equal(conversationalSessionOutcome({
selectionAllowed: true,
proposeAllowed: true,
confirmationAllowed: false,
nextFollowup: plan.next_followup,
methods: plan.methods,
}), "discriminate_candidates");
});
test("event_probe still discriminates after coverage when candidates remain tied", () => {
const plan = buildMethodFollowupPlan({
evidence: CLASSIC_COVERAGE.filter((item) => item.domain !== "horary"),
eventProbes: [{
year: 2018,
year_label: "2018 年前后",
domain: "relocation",
event_family: "搬家、离乡或长期异地",
source: "dasha_activation",
tracks: ["vimshottari", "narayana"],
tracks_agree: true,
unique_minute_claim: false,
user_meaning: "年份锁定 2018 年前后。请写成一句自然语言,问是否搬家。",
role: "distinguish",
phase: "candidate_discriminator",
information_gain: 0.21,
semantic_key: "relocation.2018.dasha_activation",
candidate_ids: ["05:00", "05:20"],
expected_outcomes: [
{ answer_class: "yes", supports: ["05:00"], conflicts: ["05:20"] },
{ answer_class: "no", supports: ["05:20"], conflicts: ["05:00"] },
],
}],
});
assert.equal(plan.next_followup?.source, "event_probe");
assert.equal(isOfferBlockingFollowup(plan.next_followup, plan.methods), true);
assert.equal(conversationalSessionOutcome({
selectionAllowed: true,
proposeAllowed: true,
confirmationAllowed: false,
nextFollowup: plan.next_followup,
methods: plan.methods,
candidateScores: [
{ time: "05:00", score: 34 },
{ time: "05:01", score: 33 },
{ time: "05:02", score: 33 },
],
}), "discriminate_candidates");
});
test("accepted time reverse-verifies an uncovered year in a covered domain", () => {
const plan = buildMethodFollowupPlan({
evidence: [{
status: "confirmed",
domain: "career",
datePrecision: "day",
occurredFrom: "2024-04-07",
occurredTo: null,
}],
accepted: true,
eventProbes: [{
year: 2018,
year_label: "2018 年前后",
domain: "career",
event_family: "入职、升职或职责明显加重",
source: "dasha_activation",
tracks: ["vimshottari", "narayana"],
tracks_agree: true,
unique_minute_claim: false,
user_meaning: "年份锁定 2018 年前后。请写成一句自然语言,问是否入职或职责加重。",
role: "reverse_verify",
style_options: DYNAMIC_STYLE_OPTIONS,
}],
});
assert.equal(plan.next_followup?.source, "reverse_verify");
assert.equal(plan.next_followup?.domain, "career");
assert.equal(plan.next_followup?.choice_frame?.period, "2018 年前后");
});
test("declining occupation covers the method; declining horary is skipped_by_policy", () => {
const plan = buildMethodFollowupPlan({
evidence: [
{ status: "confirmed", domain: "education", datePrecision: "year", occurredFrom: "2016-01-01", occurredTo: null },
{ status: "confirmed", domain: "relationship", datePrecision: "year", occurredFrom: "2018-01-01", occurredTo: null },
{ status: "confirmed", domain: "career", datePrecision: "year", occurredFrom: "2019-01-01", occurredTo: null },
{ status: "confirmed", domain: "family", datePrecision: "year", occurredFrom: "2020-01-01", occurredTo: null },
{ status: "confirmed", domain: "appearance", datePrecision: "unknown", occurredFrom: null, occurredTo: null },
{ status: "confirmed", domain: "marks", datePrecision: "unknown", occurredFrom: null, occurredTo: null },
],
declinedTopics: [
{ target_domain: "occupation", status: "declined" },
{ target_domain: "horary", status: "declined" },
{ target_domain: "finance", status: "declined" },
{ target_domain: "relocation", status: "declined" },
{ target_domain: "health_pressure", status: "declined" },
],
});
assert.equal(plan.methods.find((item) => item.method_id === "occupation")?.status, "covered");
assert.equal(plan.methods.find((item) => item.method_id === "horary")?.status, "skipped_by_policy");
assert.equal(plan.next_followup, null);
assert.equal(conversationalSessionOutcome({
selectionAllowed: true,
proposeAllowed: true,
confirmationAllowed: false,
nextFollowup: plan.next_followup,
methods: plan.methods,
candidateScores: [
{ time: "05:00", score: 62 },
{ time: "05:01", score: 22 },
{ time: "05:02", score: 16 },
],
holdoutValidation: "passed",
}), "validated_range");
});
test("horary follow-up does not block propose once occupation is covered", () => {
const plan = buildMethodFollowupPlan({
evidence: [
...CLASSIC_COVERAGE.filter((item) => item.domain !== "horary"),
{ status: "confirmed", domain: "finance", datePrecision: "year", occurredFrom: "2021-01-01", occurredTo: null },
{ status: "confirmed", domain: "relocation", datePrecision: "year", occurredFrom: "2015-01-01", occurredTo: null },
{ status: "confirmed", domain: "health_pressure", datePrecision: "year", occurredFrom: "2017-01-01", occurredTo: null },
],
});
assert.equal(plan.next_followup?.method_id, "horary");
assert.equal(isOfferBlockingFollowup(plan.next_followup, plan.methods), false);
assert.equal(conversationalSessionOutcome({
selectionAllowed: true,
proposeAllowed: true,
confirmationAllowed: false,
nextFollowup: plan.next_followup,
methods: plan.methods,
candidateScores: [
{ time: "05:00", score: 62 },
{ time: "05:01", score: 22 },
{ time: "05:02", score: 16 },
],
holdoutValidation: "passed",
}), "validated_range");
});
test("offer-candidates refuses while method coverage remains", async () => {
const accounting = fakeAccounting({
...receiptHandlers,
get_agentic_rectification_case_dossier: () => dossierFixture({
evidence: [educationEvidence, relationshipEvidence, familyEvidence],
latestResult: candidateSnapshotFixture({
selectionAllowed: true,
representativeTime: "04:48",
evidenceLedgerFingerprint: scoreableFingerprintForRawEvidence(
[educationEvidence, relationshipEvidence, familyEvidence],
),
candidates: [
{ candidate_id: CANDIDATE_ID, rank: 1, time: "04:48", relative_support: 58, tied_minute_count: 1 },
{ candidate_id: SECOND_CANDIDATE_ID, rank: 2, time: "04:49", relative_support: 42, tied_minute_count: 2 },
],
}),
}),
});
const tools = createRectificationV9Tools({
userId: USER_ID,
caseId: CASE_ID,
turnId: TURN_ID,
accounting: accounting.client as never,
});
await assert.rejects(
() => (tools["rectification-offer-candidates"] as unknown as {
execute(input: unknown): Promise<unknown>;
}).execute({ caseId: CASE_ID }),
(error: unknown) => error instanceof RectificationToolServiceError && error.code === "offer_not_allowed",
);
assert.equal(
accounting.calls.some((call) =>
call.fn === "transition_agentic_rectification_case_status"
&& call.args.p_status === "candidate_ready"
),
false,
);
});
test("paused case with selection_allowed may offer the escape hatch", async () => {
const accounting = fakeAccounting({
...receiptHandlers,
get_agentic_rectification_case_dossier: () => dossierFixture({
status: "paused",
evidence: [educationEvidence, familyEvidence, careerEvidence],
latestResult: candidateSnapshotFixture({
selectionAllowed: true,
representativeTime: "04:48",
evidenceLedgerFingerprint: scoreableFingerprintForRawEvidence([
educationEvidence,
familyEvidence,
careerEvidence,
]),
candidates: [
{ candidate_id: CANDIDATE_ID, rank: 1, time: "04:48", relative_support: 58, tied_minute_count: 1 },
{ candidate_id: SECOND_CANDIDATE_ID, rank: 2, time: "04:49", relative_support: 42, tied_minute_count: 2 },
],
decisionReceipt: {
inference_state: producedInferenceState([
{ id: CANDIDATE_ID, time: "04:48", relative_support: 58 },
{ id: SECOND_CANDIDATE_ID, time: "04:49", relative_support: 42 },
]),
},
}),
}),
transition_agentic_rectification_case_status: () => ({
case_id: CASE_ID,
status: "candidate_ready",
idempotent: false,
}),
});
const tools = createRectificationV9Tools({
userId: USER_ID,
caseId: CASE_ID,
turnId: TURN_ID,
accounting: accounting.client as never,
});
const projection = await (tools["rectification-offer-candidates"] as unknown as {
execute(input: unknown): Promise<{ session_outcome: string }>;
}).execute({ caseId: CASE_ID });
assert.equal(projection.session_outcome, "provisional_range_user_stopped");
assert.equal(
accounting.calls.some((call) =>
call.fn === "transition_agentic_rectification_case_status"
&& call.args.p_status === "candidate_ready"
),
false,
);
});
test("offer-candidates refuses a stored snapshot with a missing fingerprint", async () => {
const accounting = fakeAccounting({
...receiptHandlers,
get_agentic_rectification_case_dossier: () => dossierFixture({
status: "paused",
evidence: [educationEvidence],
latestResult: candidateSnapshotFixture({
selectionAllowed: true,
representativeTime: "04:48",
evidenceLedgerFingerprint: null,
candidates: [
{ candidate_id: CANDIDATE_ID, rank: 1, time: "04:48", relative_support: 58, tied_minute_count: 1 },
{ candidate_id: SECOND_CANDIDATE_ID, rank: 2, time: "04:49", relative_support: 42, tied_minute_count: 2 },
],
}),
}),
});
const tools = createRectificationV9Tools({
userId: USER_ID,
caseId: CASE_ID,
turnId: TURN_ID,
accounting: accounting.client as never,
});
await assert.rejects(
() => (tools["rectification-offer-candidates"] as unknown as {
execute(input: unknown): Promise<unknown>;
}).execute({ caseId: CASE_ID }),
(error: unknown) => error instanceof RectificationToolServiceError && error.code === "offer_not_allowed",
);
});
test("paused case resumes only when the Agent explicitly requests it", async () => {
let reads = 0;
const accounting = fakeAccounting({
...receiptHandlers,
get_agentic_rectification_case_dossier: () => dossierFixture({
status: reads++ === 0 ? "paused" : "collecting_evidence",
evidence: [],
}),
transition_agentic_rectification_case_status: (_fn, args) => ({
case_id: CASE_ID,
status: args.p_status,
idempotent: false,
}),
});
const tools = createRectificationV9Tools({
userId: USER_ID,
caseId: CASE_ID,
turnId: TURN_ID,
userMessage: "继续校正",
accounting: accounting.client as never,
});
const projection = await (tools["rectification-read-case"] as unknown as {
execute(input: unknown): Promise<{ status: string }>;
}).execute({ caseId: CASE_ID, resume: true });
assert.equal(projection.status, "collecting_evidence");
assert.equal(
accounting.calls.some((call) =>
call.fn === "transition_agentic_rectification_case_status"
&& call.args.p_status === "collecting_evidence"
),
true,
);
});
test("offer-candidates allows a 34/33/33 tie after method coverage when remaining minutes do not split", async () => {
const accounting = fakeAccounting({
...receiptHandlers,
get_agentic_rectification_case_dossier: () => dossierFixture({
evidence: methodCoverageTieEvidence,
latestResult: candidateSnapshotFixture({
selectionAllowed: true,
representativeTime: "05:00",
evidenceLedgerFingerprint: scoreableFingerprintForRawEvidence(methodCoverageTieEvidence),
decisionReceipt: {
propose_allowed: true,
window_scan: {
scanned: true,
d9_lagna_count: 3,
d10_lagna_count: 3,
d9_candidates_differ: true,
d10_candidates_differ: true,
d9_sign_names: ["天秤", "天蝎", "射手"],
d10_sign_names: ["巨蟹", "狮子", "处女"],
},
},
candidates: [
{ candidate_id: CANDIDATE_ID, rank: 1, time: "05:00", relative_support: 34, tied_minute_count: 1 },
{ candidate_id: SECOND_CANDIDATE_ID, rank: 2, time: "05:01", relative_support: 33, tied_minute_count: 1 },
{ candidate_id: THIRD_CANDIDATE_ID, rank: 3, time: "05:02", relative_support: 33, tied_minute_count: 1 },
],
}),
}),
});
const tools = createRectificationV9Tools({
userId: USER_ID,
caseId: CASE_ID,
turnId: TURN_ID,
accounting: accounting.client as never,
});
const projection = await (tools["rectification-offer-candidates"] as unknown as {
execute(input: unknown): Promise<{ session_outcome: string }>;
}).execute({ caseId: CASE_ID });
// 原断言 session_outcome=provisional_range(并列不采用)→ 新断言 adopt_representative。
assert.equal(projection.session_outcome, "adopt_representative");
assert.equal(
accounting.calls.some((call) =>
call.fn === "transition_agentic_rectification_case_status"
&& call.args.p_status === "candidate_ready"
),
true,
);
});