import { describe, expect, it } from "vitest"; import { composeAssistantAnswer } from "../src/services/answerComposer"; import type { AnswerGroundingCheck, RequirementCoverageReport, UnifiedRetrievalResult } from "../src/types/assistant"; import type { ProblemUnit } from "../src/types/stage2ProblemUnits"; function buildRouteSummary() { return { mode: "deterministic_v2" as const, message_in_scope: true, scope_confidence: "high" as const, planner: { total_fragments: 1, in_scope_fragments: 1, out_of_scope_fragments: 0, discarded_fragments: 0, contains_multiple_tasks: false }, decisions: [], fallback: { type: "none" as const, message: null } }; } function buildCoverage(input?: Partial): RequirementCoverageReport { return { requirements_total: 1, requirements_covered: 1, requirements_uncovered: [], requirements_partially_covered: [], clarification_needed_for: [], out_of_scope_requirements: [], ...input }; } function buildGrounding(input?: Partial): AnswerGroundingCheck { return { status: "grounded", route_subject_match: true, missing_requirements: [], reasons: [], why_included_summary: ["wave12-test"], selection_reason_summary: ["wave12-test"], ...input }; } function buildProblemUnit(input: { id: string; type: ProblemUnit["problem_unit_type"]; account: string; defect: string; lifecycleDomain?: ProblemUnit["lifecycle_domain"]; }): ProblemUnit { return { schema_version: "problem_unit_v0_1", problem_unit_id: input.id, problem_unit_type: input.type, title: "Wave12 problem unit", mechanism_summary: `Mechanism candidate: ${input.defect}.`, business_defect_class: input.defect, severity: { score: 0.72, grade: "high" }, confidence: { score: 0.58, grade: "medium" }, affected_entities: ["Document:DOC-1"], affected_documents: ["Document:DOC-1"], affected_postings: ["Posting:POST-1"], affected_accounts: [input.account], affected_counterparties: ["Counterparty:CP-1"], affected_contracts: ["Contract:CTR-1"], failed_expected_edge: input.defect, period_impact: { is_period_sensitive: true, impact_class: "close_risk" }, evidence_pack: ["cand-1"], entity_backlinks: [{ entity: "Document", id: "DOC-1" }], snapshot_limitations: [], ...(input.lifecycleDomain ? { lifecycle_domain: input.lifecycleDomain } : {}) }; } function buildRetrieval(input: { requirementId: string; status: UnifiedRetrievalResult["status"]; units?: ProblemUnit[]; accountScope?: string[]; domainScope?: string[]; relationPatterns?: string[]; limitations?: string[]; confidence?: UnifiedRetrievalResult["confidence"]; withEvidence?: boolean; }): UnifiedRetrievalResult { const units = input.units ?? []; const withEvidence = input.withEvidence ?? input.status !== "empty"; return { fragment_id: `F-${input.requirementId}`, requirement_ids: [input.requirementId], route: "hybrid_store_plus_live", status: input.status, result_type: "chain", items: input.status === "empty" ? [] : [ { source_entity: "Document", source_id: "DOC-1", account_context: input.accountScope ?? ["60"], graph_domain_scope: input.domainScope ?? ["bank_settlement"], relation_pattern_hits: input.relationPatterns ?? ["payment_to_settlement"] } ], summary: { broad_query_detected: false, broad_result_flag: false, minimum_evidence_failed: false, degraded_to: null, narrowing_strength: "strong", semantic_profile: { account_scope: input.accountScope ?? ["60", "62"], domain_scope: input.domainScope ?? ["bank_settlement", "customer_settlement"], relation_patterns: input.relationPatterns ?? ["payment_to_settlement"] } }, evidence: input.status === "empty" || !withEvidence ? [] : [ { evidence_id: `ev-${input.requirementId}`, claim_ref: `requirement:${input.requirementId}`, source_type: "retrieval_item", source_ref: { schema_version: "evidence_source_ref_v1", namespace: "snapshot_2020", entity: "document", id: "DOC-1", period: "2020-07", canonical_ref: "evidence_source_ref_v1|snapshot_2020|document|doc-1|2020-07" }, pointer: { fragment_id: `F-${input.requirementId}`, route: "hybrid_store_plus_live", source: { namespace: "snapshot_2020", entity: "document", id: "DOC-1", period: "2020-07" }, locator: { field_path: "risk_score", item_index: 0 } }, evidence_kind: "mechanism_link", mechanism_note: (input.relationPatterns ?? ["payment_to_settlement"])[0], confidence: input.status === "ok" ? "medium" : "low", limitation: null, payload: { risk_score: 4 } } ], candidate_evidence: [], problem_units: units, problem_unit_summary: units.length > 0 ? { schema_version: "problem_unit_summary_v0_1", units_total: units.length, duplicate_collapses: 0, unit_types: units.map((unit) => unit.problem_unit_type), type_distribution: { [units[0]?.problem_unit_type ?? "broken_chain_segment"]: units.length }, severity_distribution: { low: 0, medium: 0, high: units.length }, confidence_distribution: { low: 0, medium: units.length, high: 0 }, primary_unit_type: units[0]?.problem_unit_type ?? null } : null, why_included: ["wave12-test"], selection_reason: ["wave12-test"], risk_factors: ["wave12"], business_interpretation: ["wave12"], confidence: input.confidence ?? (input.status === "ok" ? "medium" : "low"), limitations: input.limitations ?? [], errors: [] }; } function composeCase(input: { userMessage: string; focusDomainHint: string | null; retrievalResults: UnifiedRetrievalResult[]; coverage?: Partial; grounding?: Partial; }) { return composeAssistantAnswer({ userMessage: input.userMessage, routeSummary: buildRouteSummary(), retrievalResults: input.retrievalResults, requirements: [ { requirement_id: "R1", source_fragment_id: "F-R1", requirement_text: "Wave 12 requirement", subject_tokens: [], status: "covered", route: "hybrid_store_plus_live" } ], coverageReport: buildCoverage(input.coverage), groundingCheck: buildGrounding(input.grounding), focusDomainHint: input.focusDomainHint, enableAnswerPolicyV11: true, enableProblemCentricAnswerV1: true, enableLifecycleAnswerV1: true }); } describe("wave12 vat/month-close consistency + confidence reconciliation", () => { it("vat_query_with_strong_signal_must_override_stale_settlement_focus_hint", () => { const vatUnit = buildProblemUnit({ id: "pu-vat-1", type: "cross_branch_inconsistency_cluster", account: "68", defect: "invoice_to_vat", lifecycleDomain: "vat_flow" }); const output = composeCase({ userMessage: "VAT chain July: invoice exists, but purchase book is empty for accounts 19/68. Why?", focusDomainHint: "settlements_60_62", retrievalResults: [ buildRetrieval({ requirementId: "R1", status: "ok", units: [vatUnit], accountScope: ["19", "68"], domainScope: ["vat_flow"], relationPatterns: ["invoice_to_vat"] }) ] }); expect(output.assistant_reply).toMatch(/НДС|vat|книг|счет[-\s]?фактур/i); expect(output.assistant_reply).not.toMatch(/60\/62|закрыти[ея]\s+расч[её]т/i); }); it("month_close_query_with_strong_signal_must_override_stale_vat_focus_hint", () => { const closeUnit = buildProblemUnit({ id: "pu-close-1", type: "period_risk_cluster", account: "25", defect: "close_operation_runs_missing", lifecycleDomain: "period_close" }); const output = composeCase({ userMessage: "Month close July: costs on accounts 20/44 were allocated partially. Where is the break?", focusDomainHint: "vat_document_register_book", retrievalResults: [ buildRetrieval({ requirementId: "R1", status: "ok", units: [closeUnit], accountScope: ["25", "26"], domainScope: ["period_close"], relationPatterns: ["close_operation_runs"] }) ] }); expect(output.assistant_reply).toMatch(/закрыти|месяц|затрат|распредел|month close|20\/44/i); expect(output.assistant_reply).not.toMatch(/НДС|счет[-\s]?фактур|книг/i); }); it("vat_domain_with_foreign_primary_evidence_must_degrade_to_clarification", () => { const output = composeCase({ userMessage: "Проверь НДС-цепочку по июлю: документ -> регистр -> книга.", focusDomainHint: "vat_document_register_book", retrievalResults: [ buildRetrieval({ requirementId: "R1", status: "ok", units: [], accountScope: ["20"], domainScope: ["period_close", "fixed_asset"], relationPatterns: ["allocation_rules_resolved"] }) ] }); expect(output.reply_type).toBe("clarification_required"); expect(output.assistant_reply).toContain("Что пока не доказано:"); expect(output.assistant_reply).not.toContain("Опора достаточна для первичного вывода."); }); it("month_close_domain_with_vat_primary_evidence_must_degrade_to_clarification", () => { const output = composeCase({ userMessage: "Проверь закрытие месяца и контур затрат 20-44 за июль.", focusDomainHint: "month_close_costs_20_44", retrievalResults: [ buildRetrieval({ requirementId: "R1", status: "ok", units: [], accountScope: ["19", "68"], domainScope: ["vat_flow"], relationPatterns: ["invoice_to_vat"] }) ] }); expect(output.reply_type).toBe("clarification_required"); expect(output.assistant_reply).toContain("Что пока не доказано:"); expect(output.assistant_reply).not.toContain("Опора достаточна для первичного вывода."); }); it("confidence_limitation_must_not_contradict_each_other", () => { const vatUnit = buildProblemUnit({ id: "pu-vat-2", type: "lifecycle_anomaly_node", account: "68", defect: "invoice_to_vat", lifecycleDomain: "vat_flow" }); const output = composeCase({ userMessage: "Почему по НДС есть сигнал, но механизм выглядит неполным?", focusDomainHint: "vat_document_register_book", retrievalResults: [ buildRetrieval({ requirementId: "R1", status: "ok", units: [vatUnit], accountScope: ["19", "68"], domainScope: ["vat_flow"], relationPatterns: ["invoice_to_vat"], limitations: ["Source mapping is weak for part of the evidence."] }) ], grounding: { status: "partial", reasons: ["Mechanism is unresolved for part of the evidence."] } }); expect(output.answer_structure_v11?.mechanism_block?.status).toBe("limited"); expect(output.assistant_reply).toContain("Что пока не доказано:"); expect(output.assistant_reply).not.toContain("Опора достаточна для первичного вывода."); }); it("settlement_regression_must_remain_pass", () => { const settlementUnit = buildProblemUnit({ id: "pu-settlement-1", type: "broken_chain_segment", account: "62", defect: "failed_edge:payment_to_settlement", lifecycleDomain: "customer_settlement" }); const output = composeCase({ userMessage: "Оплата есть, но 62.01/62.02 не сходятся. Почему долг остался?", focusDomainHint: "settlements_60_62", retrievalResults: [ buildRetrieval({ requirementId: "R1", status: "ok", units: [settlementUnit], accountScope: ["62.01", "62.02"], domainScope: ["customer_settlement"], relationPatterns: ["payment_to_settlement"] }) ] }); expect(output.assistant_reply).toMatch(/62\.01|62\.02|расч[её]т|зач[её]т/i); expect(output.assistant_reply).not.toMatch(/НДС|книг|закрыти[ея]\s+месяц/i); }); });