141 lines
5.3 KiB
TypeScript
141 lines
5.3 KiB
TypeScript
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);
|
||
});
|
||
});
|
||
|