import { describe, expect, it } from "vitest"; import { composeAssistantAnswer } from "../src/services/answerComposer"; import type { UnifiedRetrievalResult } from "../src/types/assistant"; function buildRetrievalWithMojibake(): UnifiedRetrievalResult { return { fragment_id: "F1", requirement_ids: ["R1"], route: "hybrid_store_plus_live", status: "ok", result_type: "chain", items: [ { counterparty_id: "CP-1", operations_count: 12, document_refs_count: 3 } ], summary: { route_focus: "cross_entity_breakage", source_records: 262, filtered_records_after_narrowing: 24, checked_records: 24, semantic_narrowing_applied: true, ranking_basis: ["closure_risk", "repeatability", "financial_impact"] }, evidence: [], why_included: [ "Семантическое сужение выполнено РїРѕ профилю cross_entity_breakage.", "После narrowing осталось 24 РёР· 262 записей." ], selection_reason: [ "Отбор основан РЅР° account_scope + domain_scope + document_types + relation_patterns + anomaly_patterns.", "Ранжирование РїРѕ basis: closure_risk, repeatability, financial_impact." ], risk_factors: ["broken_chain", "period_close_risk"], business_interpretation: [], confidence: "medium", limitations: [], errors: [] }; } describe("assistant answer encoding sanitizer", () => { it("filters mojibake in explainable answer and falls back to readable reasoning", () => { const output = composeAssistantAnswer({ userMessage: "Разложи цепочку и покажи хвосты по расчетам за 2020-06.", routeSummary: { mode: "deterministic_v2", message_in_scope: true, scope_confidence: "high", planner: { total_fragments: 1, in_scope_fragments: 1, out_of_scope_fragments: 0, discarded_fragments: 0, contains_multiple_tasks: false }, decisions: [], fallback: { type: "none", message: null } }, retrievalResults: [buildRetrievalWithMojibake()], requirements: [ { requirement_id: "R1", source_fragment_id: "F1", requirement_text: "Проверка цепочки расчетов", subject_tokens: ["chain", "account_60"], status: "covered", route: "hybrid_store_plus_live" } ], coverageReport: { requirements_total: 1, requirements_covered: 1, requirements_uncovered: [], requirements_partially_covered: [], clarification_needed_for: [], out_of_scope_requirements: [] }, groundingCheck: { status: "grounded", route_subject_match: true, missing_requirements: [], reasons: [], why_included_summary: [], selection_reason_summary: [] }, enableAnswerPolicyV11: false }); expect(output.reply_type).toBe("factual_with_explanation"); expect(output.assistant_reply).toContain("Почему это попало в ответ:"); expect(output.assistant_reply).not.toMatch(/(?:Р.|С.){5,}/u); expect(output.assistant_reply).toContain("Проверка выполнена по профилю cross_entity_breakage."); expect(output.assistant_reply).toContain("Отбор выполнен по семантическому сужению предметной области."); }); });