import { describe, expect, it } from "vitest"; import { composeAssistantAnswer } from "../src/services/answerComposer"; function buildRouteSummary(fallbackType: "none" | "clarification" | "out_of_scope" = "none") { return { mode: "deterministic_v2" as const, message_in_scope: true, scope_confidence: "high" as const, planner: { total_fragments: 1, in_scope_fragments: 1, out_of_scope_fragments: 0, discarded_fragments: 0, contains_multiple_tasks: false }, decisions: [], fallback: { type: fallbackType, message: null } }; } function buildCoverageReport() { return { requirements_total: 0, requirements_covered: 0, requirements_uncovered: [], requirements_partially_covered: [], clarification_needed_for: [], out_of_scope_requirements: [] }; } describe("assistant boundary fallback reply", () => { it("uses soft refusal without template sections when domain is not covered", () => { const output = composeAssistantAnswer({ userMessage: "Скажи курс доллара на завтра и дай прогноз инфляции.", routeSummary: buildRouteSummary("none"), retrievalResults: [], requirements: [], coverageReport: buildCoverageReport(), groundingCheck: { status: "no_grounded_answer", route_subject_match: false, missing_requirements: [], reasons: ["no grounded support"], why_included_summary: [], selection_reason_summary: [] }, enableAnswerPolicyV11: true }); expect(output.reply_type).toBe("clarification_required"); expect(output.assistant_reply).toMatch(/мягкий отказ/i); expect(output.assistant_reply).toContain("Что именно проверено:"); expect(output.assistant_reply).toContain("Что найдено:"); expect(output.assistant_reply).toContain("Что пока не доказано:"); expect(output.assistant_reply).toContain("Что могу сделать сейчас:"); expect(output.assistant_reply).not.toContain("Что сломано:"); }); it("for covered domain without anchors uses soft clarification with nearby capability", () => { const output = composeAssistantAnswer({ userMessage: "Покажи где по контрагентам хвосты по оплатам.", routeSummary: buildRouteSummary("none"), retrievalResults: [], requirements: [], coverageReport: buildCoverageReport(), groundingCheck: { status: "no_grounded_answer", route_subject_match: false, missing_requirements: [], reasons: ["no grounded support"], why_included_summary: [], selection_reason_summary: [] }, enableAnswerPolicyV11: true }); expect(output.reply_type).toBe("clarification_required"); expect(output.assistant_reply).toMatch(/не могу надежно ответить по сценарию|не хватает подтвержденной опоры/i); expect(output.assistant_reply).toContain("Что именно проверено:"); expect(output.assistant_reply).toContain("Что найдено:"); expect(output.assistant_reply).toContain("Что пока не доказано:"); expect(output.assistant_reply).toContain("Что могу сделать сейчас:"); expect(output.assistant_reply).not.toContain("Что сломано:"); }); it("keeps out_of_scope reply type but responds with soft fallback text", () => { const output = composeAssistantAnswer({ userMessage: "Скажи прогноз погоды на выходные.", routeSummary: buildRouteSummary("out_of_scope"), retrievalResults: [], requirements: [], coverageReport: buildCoverageReport(), groundingCheck: { status: "no_grounded_answer", route_subject_match: false, missing_requirements: [], reasons: ["out of scope"], why_included_summary: [], selection_reason_summary: [] }, enableAnswerPolicyV11: true }); expect(output.reply_type).toBe("out_of_scope"); expect(output.assistant_reply).toMatch(/мягкий отказ|не могу надежно|не хватает подтвержденной опоры/i); expect(output.assistant_reply).toContain("Что могу сделать сейчас:"); expect(output.assistant_reply).not.toContain("Что сломано:"); }); it("uses soft refusal in broad_partial mode when domain is not covered and evidence is weak", () => { const output = composeAssistantAnswer({ userMessage: "What is the expected inflation next quarter and FX trend?", routeSummary: buildRouteSummary("none"), retrievalResults: [ { fragment_id: "F1", requirement_ids: ["R1"], route: "store_canonical", status: "partial", result_type: "summary", items: [{ note: "weak candidate" }], summary: { note: "weak" }, evidence: [], why_included: [], selection_reason: [], risk_factors: [], business_interpretation: [], confidence: "low", limitations: ["weak_source_mapping"], errors: [] } as any ], requirements: [ { requirement_id: "R1", source_fragment_id: "F1", requirement_text: "forecast and trend", subject_tokens: [], status: "clarification_needed", route: null } ], coverageReport: { requirements_total: 1, requirements_covered: 0, requirements_uncovered: [], requirements_partially_covered: [], clarification_needed_for: ["R1"], out_of_scope_requirements: [] }, groundingCheck: { status: "partial", route_subject_match: false, missing_requirements: ["R1"], reasons: ["weak_source_mapping"], why_included_summary: [], selection_reason_summary: [] }, enableAnswerPolicyV11: true }); expect(output.reply_type).toBe("partial_coverage"); expect(output.assistant_reply).toContain("Что найдено:"); expect(output.assistant_reply).toContain("Что пока не доказано:"); expect(output.assistant_reply).toContain("Что могу сделать сейчас:"); expect(output.assistant_reply).not.toContain("Что сломано:"); }); });