Files
Jyotisha/frontend/tests/rectification-eight-method.test.ts
T
Jesse_ChenandCursor 6c9a089620
Independent Staging Quality Gate / validate (push) Successful in 13m54s
Independent Staging Quality Gate / publish (push) Successful in 10m50s
fix(rectification): refresh remaining probes and targeted collect before delivering range (BUG-653/654)
Dated-choice exhaustion is not convergence. Refresh probes from remaining
active candidates, then ask a targeted collect, then deliver. Skill 10.0.24.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-11 18:28:39 +08:00

3278 lines
126 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 { GENERIC_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 assertInviteCollect(plan: ReturnType<typeof buildMethodFollowupPlan>) {
// 原值: 训练门关时按感情 → 事业 → 家人轮转
// 新值: 收集池首条是「还有吗」邀请
// 原因: 用户先说完,再从已说的事追问(BUG-648)
assert.equal(plan.next_followup?.source, "method_coverage");
assert.equal(plan.next_followup?.method_id, "dasha_events");
assert.equal(plan.next_followup?.domain, "other");
assert.equal(plan.next_followup?.collection_key, "collect:invite:more");
assert.equal(plan.next_followup?.intent, "collect_method_evidence");
assert.equal(plan.next_followup?.choice_frame, null);
}
function assertNoLeftoverDatedCollect(plan: ReturnType<typeof buildMethodFollowupPlan>) {
// 原值: 训练门开后仍按未覆盖领域采集
// 新值: 训练门开后不再轮转采集
// 原因: S2/S3 走选择题或交付(BUG-648
assert.notEqual(plan.next_followup?.intent, "collect_method_evidence");
}
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,
}],
});
assertInviteCollect(plan);
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 或 d9_relationship
// 新值: 邀请「还有吗」,选择题不得抢在训练门前
// 原因: 收集池在训练门开之前优先邀请(BUG-648)
assertInviteCollect(plan);
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],
});
assertInviteCollect(plan);
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],
});
assertInviteCollect(plan);
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],
});
assertInviteCollect(plan);
});
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],
});
assertInviteCollect(plan);
});
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],
});
assertInviteCollect(plan);
});
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",
}],
});
assertInviteCollect(plan);
});
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");
assertInviteCollect(plan);
assert.doesNotMatch(plan.next_followup?.user_prompt_hint ?? "", /A\/B\/C\/D/);
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",
});
assertInviteCollect(plan);
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",
});
// 原值: 本轮仍问感情采集
// 新值: 训练门关时邀请不得因 adopt 早退
// 原因: 邀请也是收集池条目(BUG-648)
assertInviteCollect(plan);
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",
},
});
// 原值: 丢掉无用区分焦点后仍收财务经历
// 新值: 训练门开后不再轮转采集
// 原因: S3 交付,不再用未覆盖领域挡住采用(BUG-648)
assertNoLeftoverDatedCollect(plan);
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" }],
});
// 原值: d10_career / career
// 新值: 拒答感情不得跳过邀请
// 原因: 收集池邀请在产出期间排第一(BUG-648)
assertInviteCollect(plan);
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,
});
// 原值: 财务采集
// 新值: 训练门开后不再轮转采集
// 原因: 经典八法覆盖后进 S2/S3BUG-648
assertNoLeftoverDatedCollect(plan);
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 观察)
// 新值: 训练门开后不再轮转采集
// 原因: 家人之后先收未用带年份经历的口径已撤回(BUG-648)
assertNoLeftoverDatedCollect(plan);
});
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,
});
// 原值: d4_home 搬家采集
// 新值: 训练门开后不再轮转采集
// 原因: 财务已有带年份证据后也不再盘问搬家(BUG-648)
assertNoLeftoverDatedCollect(plan);
});
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; intent?: string; kind_hint?: string } | 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;
engine_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"]);
// 原值: 训练门开后不再按方法轮转采集
// 新值: D9 仍换升且感情未覆盖,先定向补事
// 原因: 带年月池空后按剩余层定向补事(BUG-654);SQL 缺类仍不得压过此问
assert.equal(projection.method_followup_plan.next_followup?.intent, "collect_method_evidence");
assert.equal(projection.method_followup_plan.next_followup?.domain, "relationship");
assert.match(projection.method_followup_plan.next_followup?.kind_hint ?? "", /targeted/);
assert.doesNotMatch(
projection.method_followup_plan.next_followup?.domain ?? "",
/relocation|health|finance/,
);
assert.equal(projection.method_followup_plan.deferred_followup, null);
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.engine_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/);
});
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.24", () => {
assert.equal(PUBLIC_RECTIFICATION_TOOLS.length, 14);
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.24");
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 as { markdown?: string }).markdown ?? plateau.skill_verification_report), /Dasha \+ Gochara/);
assert.match(String((plateau.skill_verification_report as { markdown?: string }).markdown ?? plateau.skill_verification_report), /candidate_range_not_birth_time_truth/);
assert.doesNotMatch(String((plateau.skill_verification_report as { markdown?: string }).markdown ?? 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.doesNotMatch(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 轮转
// 新值: 四件四类已开训练门,不再轮转采集
// 原因: S1 只问到训练门开(BUG-648
assertNoLeftoverDatedCollect(afterFamily);
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.notEqual(afterFamily.next_followup?.domain, "finance");
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.notEqual(afterDated.next_followup?.domain, "finance");
assert.notEqual(afterDated.next_followup?.domain, "relocation");
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
// 新值: 训练门关时邀请优先
// 原因: 精度层不得抢在收集池前(BUG-648)
assertInviteCollect(plan);
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-648)
assertNoLeftoverDatedCollect(plan);
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 },
],
}), "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-648)
assertNoLeftoverDatedCollect(d4);
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.dropped_probes.some((item) => item.reason === "frameless_distinguish"),
true,
);
assert.notEqual(d4AfterDated.next_followup?.intent, "distinguish_candidates");
assert.notEqual(spokenFollowupForUser(d4AfterDated.next_followup), GENERIC_COLLECT_QUESTION);
const legacyTheme = buildMethodFollowupPlan({
evidence: datedThenHome,
precisionStage: "theme_refine",
});
assert.notEqual(legacyTheme.next_followup?.intent, "distinguish_candidates");
const d5 = buildMethodFollowupPlan({
evidence: covered.filter((item) => item.domain !== "education"),
precisionStage: "d5_refine",
});
// 原值: d5_education 精度层 / 学业采集
// 新值: 训练门开后不再为未覆盖学业轮转采集
// 原因: S2 不再把精度层改写成领域采集(BUG-648)
assert.notEqual(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",
}],
});
// 原值: 绑定真实 D9 探针时 next 是 varga.d9,压过同域 2021 带年月题
// 新值: next 是 relationship.2021.dasha_activationD9 记 yearless_deferred
// 原因: BUG-629 决策 1,可问的带年月区分题优先于性格题
assert.equal(probed.next_followup?.domain, "relationship");
assert.equal(probed.next_followup?.source, "event_probe");
assert.equal(probed.next_followup?.semantic_key, "relationship.2021.dasha_activation");
assert.notEqual(probed.next_followup?.semantic_key, "varga.d9.05:00|05:14");
assert.equal(
probed.dropped_probes.some((item) => (
item.semantic_key === "varga.d9.05:00|05:14" && item.reason === "yearless_deferred"
)),
true,
JSON.stringify(probed.dropped_probes),
);
});
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",
});
// 原值: d10_career
// 新值: 四件两类已开训练门,不再问事业采集
// 原因: S1 只问到训练门开(BUG-648
assertNoLeftoverDatedCollect(plan);
});
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 仍在;轮转采集撤回 BUG-648)
assertNoLeftoverDatedCollect(plan);
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: "天蝎座" },
],
}],
},
});
// 原值: 职业覆盖后仍问 D10 性格对照
// 新值: 性格题 deferred,不再作为淘汰区分题
// 原因: BUG-651
assert.notEqual(plan.next_followup?.choice_kind, "varga_style");
assert.notEqual(plan.next_followup?.intent, "distinguish_candidates");
const outcome = 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 },
],
});
// 原值: discriminate_candidates(把 D10 性格题当区分探针)
// 新值: 无带年月探针即交付
// 原因: BUG-651
assert.ok(
outcome === "adopt_representative" || outcome === "provisional_range",
outcome,
);
});
test("answered duty language skips window D10 and does not re-ask covered education from 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,
});
// 原值: educationyearless D24)→ finance
// 新值: 不再把已覆盖学业改写成财务采集;可问占问,不得再出 D10/D24 卡
// 原因: yearless→领域轮转已撤(BUG-648
assert.notEqual(plan.next_followup?.domain, "education");
assert.notEqual(plan.next_followup?.domain, "finance");
assert.doesNotMatch(plan.next_followup?.semantic_key ?? "", /varga\.d(10|24)/);
});
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",
}],
});
// 原值: yearless D12 → 家人采集,题干带 2021 年前后
// 新值: 训练门开后不把年龄段采集探针写进题干
// 原因: 撤回 BUG-642yearless→领域采集已撤(BUG-648
assert.notEqual(plan.next_followup?.domain, "family");
assert.doesNotMatch(spokenFollowupForUser(plan.next_followup) ?? "", /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,
});
// 原值: 家人采集带 2021 年前后
// 新值: 训练门开后不把年龄段年份写进题干
// 原因: 撤回 BUG-642BUG-648
assert.notEqual(plan.next_followup?.domain, "family");
assert.doesNotMatch(spokenFollowupForUser(plan.next_followup) ?? "", /2021 年前后/);
});
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,
})),
},
},
},
});
// 原值: yearless D12 → 家人采集,题干带 2021 年前后
// 新值: 训练门开后不把年龄段采集探针写进题干,也不轮转家人
// 原因: 撤回 BUG-642yearless→领域采集已撤(BUG-648
assert.notEqual(plan.next_followup?.domain, "family");
assert.doesNotMatch(spokenFollowupForUser(plan.next_followup) ?? "", /2021 年前后/);
assert.equal(plan.dropped_probes.some((item) => (
item.semantic_key.startsWith("varga.d12") && item.reason === "yearless_ungrounded_contrast"
)), true);
});
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.doesNotMatch(plan.next_followup?.semantic_key ?? "", /varga\.d4/);
assert.notEqual(plan.next_followup?.choice_kind, "event_quality");
assert.doesNotMatch(plan.next_followup?.kind_hint ?? "", /education_start|relationship_end/);
assert.doesNotMatch(plan.next_followup?.user_prompt_hint ?? "", /大学哪年入学|高考是 \d{4}|哪年毕业/);
// 原值: 会话结果必须是 discriminate_candidates
// 新值: 允许 S2 区分或方法层占问采集;不得再走迁居/学业轮转
// 原因: yearless D4 改写成迁居采集已撤(BUG-648)
assert.ok(
plan.next_followup == null
|| plan.next_followup.intent === "distinguish_candidates"
|| plan.next_followup.method_id === "horary"
|| plan.next_followup.method_id === "occupation",
JSON.stringify({
intent: plan.next_followup?.intent,
method_id: plan.next_followup?.method_id,
domain: plan.next_followup?.domain,
}),
);
});
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,
});
// 原值: next = varga.d10(增益 3.1 压过 career.2018
// 新值: next = career.2018.dasha_activationD10 记 yearless_deferred
// 原因: BUG-629 决策 1,带年月区分题优先于性格题
assert.equal(highest.next_followup?.semantic_key, "career.2018.dasha_activation");
assert.notEqual(highest.next_followup?.semantic_key, d10.semanticKey);
assert.equal(
highest.dropped_probes.some((item) => (
item.semantic_key === d10.semanticKey && item.reason === "yearless_deferred"
)),
true,
JSON.stringify(highest.dropped_probes),
);
const d24Wins = buildMethodFollowupPlan({
evidence,
contrastPacket: {
candidateSetVersion: "05:00-05:14",
vargaDifferences: [],
probes: [d24, { ...d10, informationGain: 1.1 }],
},
candidatesSeparated: false,
});
assert.notEqual(d24Wins.next_followup?.semantic_key, d10.semanticKey);
assert.notEqual(d24Wins.next_followup?.choice_kind, "varga_style");
// 原值: D10 增益更高时压过 D24 成为 next
// 新值: D10 性格题 deferred,不得挡在带年月题或交付前面
// 原因: BUG-651
assert.equal(
d24Wins.dropped_probes.some((item) => (
item.semantic_key === d10.semanticKey && item.reason === "yearless_deferred"
)),
true,
JSON.stringify(d24Wins.dropped_probes),
);
});
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);
// 原值: holdout 从第 2 件起留 1 件,训练只剩 2,门关
// 新值: ≥4 件才留 holdout,3 件全训练,门开
// 原因: BUG-647
assert.equal(gate.open, true);
assert.equal(gate.trainingCount, 3);
assert.equal(gate.holdoutCount, 0);
const plan = buildMethodFollowupPlan({
evidence,
declinedTopics: [{ target_domain: "family", status: "declined" }],
precisionStage: "lagna_frame",
eventProbes: [CAREER_CONFLICT_PROBE],
});
assert.equal(plan.next_followup?.source, "event_probe");
assert.equal(plan.next_followup?.intent, "distinguish_candidates");
assert.notEqual(plan.next_followup?.intent, "collect_method_evidence");
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",
},
}), "discriminate_candidates");
});
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,题干复述已记年份
// 新值: 训练门关时邀请「还有吗」
// 原因: 收集池邀请优先,不再用生日或邻年探针改写成事业采集(BUG-648)
assertInviteCollect(plan);
assert.doesNotMatch(spokenFollowupForUser(plan.next_followup) ?? "", /高考是 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 may proceed once method coverage leftover collect is closed", 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,
});
// 原值: 三件三类仍因方法覆盖挡 offer
// 新值: 训练门开后 leftover 采集已关,offer 可走
// 原因: BUG-648
const result = await (tools["rectification-offer-candidates"] as unknown as {
execute(input: unknown): Promise<unknown>;
}).execute({ caseId: CASE_ID });
assert.ok(result);
});
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.ok(
projection.session_outcome === "provisional_range_user_stopped"
|| projection.session_outcome === "adopt_representative"
|| projection.session_outcome === "completed_with_range",
);
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,
inference_state: {
...producedInferenceState([
{ id: CANDIDATE_ID, time: "05:00", relative_support: 34 },
{ id: SECOND_CANDIDATE_ID, time: "05:01", relative_support: 33 },
{ id: THIRD_CANDIDATE_ID, time: "05:02", relative_support: 33 },
]),
refresh_count: 1,
},
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,
);
});