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 + relation_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("removes mojibake fragments from user-facing explainable answers", () => { const output = composeAssistantAnswer({ userMessage: "Check chain anomalies for June 2020.", 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: "Chain check", 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).toContain("Есть признаки незавершенной связки документов и проводок"); expect(output.assistant_reply).not.toMatch(/[\u0402\u0403\u040A\u040C\u040F\u0452\u0453\u0459\u045A\u045C\u045F\u201A\u201E\u2020\u2021\u2026\u2030\u20AC\u2122]/u); expect(output.assistant_reply).not.toContain("unknown_entity:"); expect(output.assistant_reply).not.toContain("batch_refresh_then_store:"); expect(output.assistant_reply).not.toMatch(/graph traversal mode|domain\/document\/relation|account_scope|relation_patterns/i); expect(output.assistant_reply).not.toContain("\uFFFD"); }); });