108 lines
3.9 KiB
TypeScript
108 lines
3.9 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).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).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).not.toContain("Что сломано:");
|
||
});
|
||
});
|