import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import test from "node:test"; import { buildMethodFollowupPlan, buildNextUserAction } from "../src/lib/rectification-agentic/v9/method-followup.ts"; import { internalObservationsFromWindowScan, parseWindowScan, } from "../src/lib/rectification-agentic/v9/varga-observations.ts"; import { RECTIFICATION_SKILL_VERSION } from "../src/lib/rectification-agentic/v9/case-status.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 { resolveExactSkillPackage, } from "../src/lib/skill-package-registry.ts"; import { CASE_ID, CANDIDATE_ID, EVIDENCE_ID, RESULT_ID, SECOND_CANDIDATE_ID, TURN_ID, USER_ID, candidateSnapshotFixture, computeFixture, dossierFixture, 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 FORBIDDEN_LABELS = /白羊|金牛|双子|巨蟹|狮子|处女|天秤|天蝎|射手|摩羯|水瓶|双鱼|热情冲动|配偶类型|事业特质/; 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, 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", }; 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, }], }); assert.equal(plan.next_followup?.method_id, "d9_relationship"); assert.equal(plan.next_followup?.domain, "relationship"); assert.equal(plan.stop_domain_rotation, true); assert.deepEqual([...plan.not_in_rotation], ["relocation", "finance", "health"]); assert.equal(plan.methods.find((item) => item.method_id === "appearance")?.status, "skipped_by_policy"); }); 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 explains the window when follow-up remains but the user stops", () => { const plan = buildMethodFollowupPlan({ evidence: [{ status: "confirmed", domain: "education", datePrecision: "month", occurredFrom: "2016-06-01", occurredTo: null, }], }); const action = buildNextUserAction({ scorableCount: 1, evidenceCount: 1, hasLatestResult: true, selectionAllowed: false, sessionOutcome: "collect_evidence", nextFollowup: plan.next_followup, workingTime: "12:00", }); assert.equal(action.id, "ask_method_followup"); assert.equal(action.on_user_stop.id, "explain_current_window"); assert.match(action.on_user_stop.user_meaning, /12:00/); assert.match(action.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, "skipped_by_policy"); assert.notEqual(plan.next_followup?.method_id, "appearance"); assert.doesNotMatch(JSON.stringify(plan), FORBIDDEN_LABELS); }); test("adopt_representative defers method follow-up instead of asking this turn", () => { const plan = buildMethodFollowupPlan({ evidence: [{ status: "confirmed", domain: "education", datePrecision: "month", occurredFrom: "2016-06-01", occurredTo: null, }], sessionOutcome: "adopt_representative", }); assert.equal(plan.next_followup, null); assert.equal(plan.deferred_followup?.method_id, "d9_relationship"); assert.equal(plan.session_outcome, "adopt_representative"); }); test("declined relationship skips to career and never polls appearance", () => { const plan = buildMethodFollowupPlan({ evidence: [{ status: "confirmed", domain: "education", datePrecision: "year", occurredFrom: "2016-01-01", occurredTo: null, }], declinedTopics: [{ target_domain: "relationship", status: "declined" }], }); assert.equal(plan.next_followup?.method_id, "d10_career"); assert.equal(plan.next_followup?.domain, "career"); assert.equal(plan.do_not_poll.includes("appearance"), true); }); test("D9 differ becomes an internal ask theme without sign labels", () => { 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: "热情冲动", }); assert.ok(scan); assert.equal(scan.confirmation_allowed, false); assert.equal(scan.unique_minute_claim, false); assert.equal(scan.d9_candidates_differ, true); const observations = internalObservationsFromWindowScan(scan); assert.deepEqual(observations, [ { layer: "d9", candidates_differ: true, ask_theme: "relationship_style" }, { layer: "d10", candidates_differ: false, ask_theme: null }, ]); assert.doesNotMatch(JSON.stringify({ scan, observations }), FORBIDDEN_LABELS); 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 }, ], observations, }); assert.equal(plan.next_followup?.source, "varga_observation"); assert.equal(plan.next_followup?.ask_theme, "relationship_style"); assert.doesNotMatch(JSON.stringify(plan), FORBIDDEN_LABELS); }); test("read-case follows method plan and hides D9/D10 labels even when SQL missing categories rotate", async () => { const accounting = fakeAccounting({ ...receiptHandlers, get_agentic_rectification_case_dossier: () => dossierFixture({ evidence: [educationEvidence], 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, confirmation_allowed: false, representative_candidate_id: CANDIDATE_ID, overall_confidence: "medium", window_scan: { scanned: true, confirmation_allowed: false, unique_minute_claim: false, d9_lagna_count: 2, d10_lagna_count: 1, d9_candidates_differ: true, d10_candidates_differ: false, d9_sign_names: ["白羊", "天蝎"], }, }, }, }), get_agentic_rectification_case_compute: () => computeFixture(), }); const tools = createRectificationV9Tools({ userId: USER_ID, caseId: CASE_ID, turnId: TURN_ID, accounting: accounting.client as never, }); const projection = await (tools["rectification-read-case"] as unknown as { execute(input: unknown): Promise<{ conversation_summary: { missing_evidence_categories: string[] }; method_followup_plan: { next_followup: { method_id: string; domain: string | null } | null; deferred_followup: { method_id: string; domain: string | null } | null; session_outcome: string; }; internal_observations: Array<{ layer: string; ask_theme: string | null }>; latest_result: { confirmation_allowed: boolean; indistinguishable_width_minutes: number; window_scan: { d9_candidates_differ: boolean } | null; session_outcome: { kind: string }; }; }>; }).execute({ caseId: CASE_ID }); assert.deepEqual(projection.conversation_summary.missing_evidence_categories, ["relocation", "health", "finance"]); assert.equal(projection.method_followup_plan.next_followup, null); assert.equal(projection.method_followup_plan.deferred_followup?.method_id, "d9_relationship"); assert.equal(projection.method_followup_plan.deferred_followup?.domain, "relationship"); assert.equal(projection.method_followup_plan.session_outcome, "adopt_representative"); assert.equal( (projection as { next_user_action?: { id?: string; on_user_stop?: { id?: string } } }).next_user_action?.id, "adopt_representative", ); assert.equal( (projection as { next_user_action?: { on_user_stop?: { id?: string } } }).next_user_action?.on_user_stop?.id, "adopt_representative", ); assert.equal(projection.latest_result.session_outcome.kind, "adopt_representative"); assert.equal(projection.internal_observations.find((item) => item.layer === "d9")?.ask_theme, "relationship_style"); assert.equal(projection.latest_result.confirmation_allowed, false); assert.ok(projection.latest_result.indistinguishable_width_minutes >= 25); assert.equal(projection.latest_result.window_scan?.d9_candidates_differ, true); assert.doesNotMatch(JSON.stringify(projection), FORBIDDEN_LABELS); }); test("accepted batch evidence triggers server rescore without offering adoption", async () => { const restore = stubEngine(ENGINE_SCORE); try { const accounting = fakeAccounting({ ...receiptHandlers, get_agentic_rectification_case_dossier: () => dossierFixture({ evidence: [educationEvidence], 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: () => ({ ...candidateSnapshotFixture(), cached: false, }), }); const tools = createRectificationV9Tools({ userId: USER_ID, caseId: CASE_ID, turnId: TURN_ID, 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 }; }>; }).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")); 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.equal("d9_sign_names" in (receipt.window_scan ?? {}), false); 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), FORBIDDEN_LABELS); } 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, 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 13 and new cases bind 10.0.3", () => { assert.equal(PUBLIC_RECTIFICATION_TOOLS.length, 13); assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.3"); const deprecated = resolveExactSkillPackage( "jyotish-birth-time-rectification", "10.0.2", "8d7aa2d4bea0414e9a89ef908ccbc8c708c98f79f5b78ae4f7dc229b5f7dbb30", ); assert.equal(deprecated.status, "deprecated"); const plateau = latestResultToolProjection({ resultId: RESULT_ID, candidates: [ { 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 }, ], selectionAllowed: true, confirmationAllowed: true, representativeTime: "04:45", selectedTime: null, selectionKind: null, algorithmVersion: "rectification-v5", }); assert.equal(plateau.confirmation_allowed, false); assert.equal((plateau.session_outcome as { kind: string }).kind, "adopt_representative"); 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, /不得给用户贴 D9\/D10 星座或类型标签/); assert.doesNotMatch(skill, /±5 分钟确定性/); });