103 lines
3.7 KiB
TypeScript
103 lines
3.7 KiB
TypeScript
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("Отбор выполнен по семантическому сужению предметной области.");
|
||
});
|
||
});
|
||
|