173 lines
6.4 KiB
TypeScript
173 lines
6.4 KiB
TypeScript
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("Что сломано:");
|
|
});
|
|
});
|