import { describe, expect, it, vi } from "vitest"; import { AssistantService } from "../src/services/assistantService"; import { AssistantSessionStore } from "../src/services/assistantSessionStore"; function buildFailedNormalizer(traceId: string) { return { normalize: vi.fn().mockResolvedValue({ ok: false, trace_id: traceId, prompt_version: "normalizer_v2_0_2", schema_version: "v2_0_2", normalized: null, validation: { passed: false, errors: ["mock"] }, route_hint_summary: null, raw_model_output: {}, usage: { input_tokens: 0, output_tokens: 0, total_tokens: 0 }, latency_ms: 1, request_count_for_case: 1 }) } as any; } function buildHandledAddressLaneMojibakeReply() { return { handled: true, reply_text: "Найдено документов: 5.", reply_type: "factual", response_type: "FACTUAL_LIST", debug: { detected_mode: "address_query", detected_mode_confidence: "high", query_shape: "DOCUMENT_LIST", query_shape_confidence: "high", detected_intent: "list_documents_by_counterparty", detected_intent_confidence: "high", extracted_filters: { sort: "period_desc", limit: 20, counterparty: "svk" }, missing_required_filters: [], selected_recipe: "address_documents_by_counterparty_v1", mcp_call_status_legacy: "matched_non_empty", account_scope_mode: "preferred", account_scope_fallback_applied: false, anchor_type: "counterparty", anchor_value_raw: "svk", anchor_value_resolved: "СВК", resolver_confidence: "high", ambiguity_count: 0, match_failure_stage: "none", match_failure_reason: null, mcp_call_status: "matched_non_empty", rows_fetched: 5, raw_rows_received: 5, rows_after_account_scope: 5, rows_after_recipe_filter: 5, rows_materialized: 5, rows_matched: 5, raw_row_keys_sample: [], materialization_drop_reason: "none", account_token_raw: null, account_token_normalized: null, account_scope_fields_checked: ["account_dt", "account_kt", "registrator", "analytics"], account_scope_match_strategy: "account_code_regex_plus_alias_map_v1", account_scope_drop_reason: "not_applicable", runtime_readiness: "LIVE_QUERYABLE_WITH_LIMITS", limited_reason_category: null, response_type: "FACTUAL_LIST", reasons: ["address_action_detected", "address_entity_detected", "document_list_signal_detected"] } } as any; } describe("assistant outgoing encoding repair", () => { it("repairs mojibake in address-lane replies before returning to user", async () => { const normalizer = buildFailedNormalizer("norm-address-mojibake-out"); const sessions = new AssistantSessionStore(); const addressQueryService = { tryHandle: vi.fn().mockResolvedValue(buildHandledAddressLaneMojibakeReply()) } as any; const chatClient = { chat: vi.fn().mockResolvedValue({ raw: { id: "chat-should-not-run" }, outputText: "unused", usage: { input_tokens: 0, output_tokens: 0, total_tokens: 0 } }) } as any; const service = new AssistantService(normalizer as any, sessions, undefined as any, undefined as any, addressQueryService, chatClient); const response = await service.handleMessage({ session_id: "asst-address-mojibake-out", user_message: "покажи документы по свк за 2020", llmProvider: "local", model: "qwen2.5", useMock: false } as any); expect(response.ok).toBe(true); expect(response.reply_type).toBe("factual"); expect(String(response.assistant_reply)).toContain("Найдено документов"); expect(String(response.assistant_reply)).not.toContain("РќР°"); expect(addressQueryService.tryHandle).toHaveBeenCalledTimes(1); expect(chatClient.chat).toHaveBeenCalledTimes(0); }); it("repairs mojibake in living-chat LLM replies before script guard", async () => { const normalizer = buildFailedNormalizer("norm-chat-mojibake-out"); const sessions = new AssistantSessionStore(); const addressQueryService = { tryHandle: vi.fn().mockResolvedValue({ handled: false }) } as any; const chatClient = { chat: vi.fn().mockResolvedValue({ raw: { id: "chat-mojibake-out" }, outputText: "Привет! Готов помочь.", usage: { input_tokens: 0, output_tokens: 0, total_tokens: 0 } }) } as any; const service = new AssistantService(normalizer as any, sessions, undefined as any, undefined as any, addressQueryService, chatClient); const response = await service.handleMessage({ session_id: "asst-chat-mojibake-out", user_message: "че как", llmProvider: "local", model: "qwen2.5", useMock: false } as any); expect(response.ok).toBe(true); expect(response.reply_type).toBe("factual_with_explanation"); expect(String(response.assistant_reply)).toContain("Привет"); expect(String(response.assistant_reply)).not.toContain("РџС"); expect(chatClient.chat).toHaveBeenCalledTimes(1); expect(addressQueryService.tryHandle).toHaveBeenCalledTimes(0); }); });