ГЛОБАЛЬНЫЙ РЕФАКТОРИНГ АРХИТЕКТУРЫ - Рефакторинг этапов 2.2.1 - фикс деградаций по старым доменам + легкая доводка регрессий перед стартом 3его этапа

This commit is contained in:
2026-04-11 16:19:53 +03:00
parent b5bd4fd737
commit 66402439dc
22 changed files with 2640 additions and 61 deletions
@@ -0,0 +1,107 @@
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("Что сломано:");
});
});
@@ -126,6 +126,18 @@ describe("assistant living router mode decision", () => {
expect(decision.reason).toBe("assistant_data_scope_query_detected");
});
it("routes slang data-scope wording 'по каким конторам можем общаться' to chat", () => {
const decision = resolveLivingAssistantModeDecision({
userMessage: "\u043f\u043e \u043a\u0430\u043a\u0438\u043c \u043a\u043e\u043d\u0442\u043e\u0440\u0430\u043c \u043c\u043e\u0436\u0435\u043c \u043e\u0431\u0449\u0430\u0442\u044c\u0441\u044f?",
addressLaneTriggered: false,
useMock: false,
predecomposeMode: "unsupported",
predecomposeModeConfidence: "low"
});
expect(decision.mode).toBe("chat");
expect(decision.reason).toBe("assistant_data_scope_query_detected");
});
it("routes data-scope wording without question mark when interrogative token is present", () => {
const decision = resolveLivingAssistantModeDecision({
userMessage: "каза какой компании подключена к 1с",
@@ -185,6 +197,54 @@ describe("assistant orchestration contract", () => {
expect(decision.livingReason).toBe("address_lane_triggered");
});
it("routes unsupported turnover-by-organization query to deep analysis", () => {
const decision = resolveAssistantOrchestrationDecision({
rawUserMessage: "\u043a\u0430\u043a\u0438\u0435 \u043e\u0431\u043e\u0440\u043e\u0442\u044b \u043f\u043e \u0430\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0435 \u0437\u0430 2020 \u0433\u043e\u0434",
effectiveAddressUserMessage: "\u041e\u0431\u043e\u0440\u043e\u0442\u044b \u043f\u043e \u0441\u0447\u0435\u0442\u0443 '\u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430' \u0437\u0430 2020 \u0433\u043e\u0434.",
followupContext: null,
llmPreDecomposeMeta: {
applied: true,
llmCanonicalCandidateDetected: true,
predecomposeContract: {
mode: "unsupported",
mode_confidence: "low",
intent: "account_balance_snapshot",
intent_confidence: "high"
}
} as any,
useMock: false
});
expect(decision.runAddressLane).toBe(false);
expect(decision.toolGateDecision).toBe("skip_address_lane");
expect(decision.livingMode).toBe("deep_analysis");
expect([
"address_signal_unsupported_intent_fallback_to_deep",
"aggregate_analytics_signal_fallback_to_deep"
]).toContain(String(decision.toolGateReason));
expect([
"unsupported_address_intent_fallback_to_deep",
"aggregate_analytics_signal_fallback_to_deep"
]).toContain(String(decision.livingReason));
});
it("routes profitability ranking query to deep analysis instead of address lane", () => {
const decision = resolveAssistantOrchestrationDecision({
rawUserMessage: "\u043a\u0430\u043a\u043e\u0439 \u0441\u0430\u043c\u044b\u0439 \u0434\u043e\u0445\u043e\u0434\u043d\u044b\u0439 \u0433\u043e\u0434?",
effectiveAddressUserMessage: "\u043a\u0430\u043a\u043e\u0439 \u0441\u0430\u043c\u044b\u0439 \u0434\u043e\u0445\u043e\u0434\u043d\u044b\u0439 \u0433\u043e\u0434?",
followupContext: null,
llmPreDecomposeMeta: null as any,
useMock: false
} as any);
expect(decision.runAddressLane).toBe(false);
expect(decision.toolGateDecision).toBe("skip_address_lane");
expect(decision.toolGateReason).toBe("aggregate_analytics_signal_fallback_to_deep");
expect(decision.livingMode).toBe("deep_analysis");
expect(decision.livingReason).toBe("aggregate_analytics_signal_fallback_to_deep");
expect(decision.orchestrationContract?.aggregate_analytics_signal_fallback_to_deep).toBe(true);
});
it("keeps VAT explain follow-up in address lane when followup context is present", () => {
const decision = resolveAssistantOrchestrationDecision({
rawUserMessage: "почему прогноз к уплате 0?",
@@ -236,6 +296,33 @@ describe("assistant orchestration contract", () => {
expect(decision.orchestrationContract?.unsupported_address_intent_fallback_to_deep).toBe(false);
});
it("keeps list_open_contracts query in address lane despite 'unclosed' wording", () => {
const decision = resolveAssistantOrchestrationDecision({
rawUserMessage: "\u041f\u043e\u043a\u0430\u0436\u0438 \u043d\u0435\u0437\u0430\u043a\u0440\u044b\u0442\u044b\u0435 \u0434\u043e\u0433\u043e\u0432\u043e\u0440\u044b \u043d\u0430 2020-12-31",
effectiveAddressUserMessage: "\u041f\u043e\u043a\u0430\u0437\u0430\u0442\u044c \u043d\u0435\u0437\u0430\u043a\u0440\u044b\u0442\u044b\u0435 \u0434\u043e\u0433\u043e\u0432\u043e\u0440\u044b \u043f\u043e \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044e \u043d\u0430 \u043a\u043e\u043d\u0435\u0446 \u0434\u0435\u043a\u0430\u0431\u0440\u044f 2020 \u0433\u043e\u0434\u0430.",
followupContext: {
previous_intent: "month_close_costs_20_44"
},
llmPreDecomposeMeta: {
applied: true,
llmCanonicalCandidateDetected: true,
predecomposeContract: {
mode: "address_query",
mode_confidence: "high",
intent: "list_open_contracts",
intent_confidence: "medium"
}
} as any,
useMock: false
} as any);
expect(decision.runAddressLane).toBe(true);
expect(decision.toolGateDecision).toBe("run_address_lane");
expect(decision.livingMode).toBe("address_data");
expect(decision.livingReason).toBe("address_lane_triggered");
expect(decision.toolGateReason).toBe("address_mode_classifier_detected");
});
it("does not force address lane for deep-analysis unknown intent query with date-like token", () => {
const decision = resolveAssistantOrchestrationDecision({
rawUserMessage: "найди какие либо ошибки на 21 мая 2022 года",
@@ -0,0 +1,34 @@
import { describe, expect, it } from "vitest";
import { __autoRunsQuestionTestUtils } from "../src/routes/autoRuns";
describe("autoruns question extraction", () => {
it("merges conversational tails instead of producing micro-prompts", () => {
const parsed = __autoRunsQuestionTestUtils.splitQuestionCandidates(
"Покажи рисковые хвосты по расчетам с поставщиками? и коротко? без воды?"
);
expect(parsed).toHaveLength(1);
expect(parsed[0]).toMatch(/коротко/i);
expect(parsed[0]).toMatch(/без воды/i);
});
it("keeps independent questions separated", () => {
const parsed = __autoRunsQuestionTestUtils.splitQuestionCandidates(
"Где зависли оплаты по счету 60? Какие акты сверки с риском расхождения по 62?"
);
expect(parsed).toHaveLength(2);
});
it("extracts questions from JSON payload and skips placeholders", () => {
const parsed = __autoRunsQuestionTestUtils.extractQuestionsFromAutogenOutput(
JSON.stringify({
questions: ["Вопросы", "Покажи хвосты по поставщикам", "и коротко?"]
})
);
expect(parsed).toHaveLength(1);
expect(parsed[0]).toMatch(/поставщик/i);
expect(parsed[0]).toMatch(/коротко/i);
});
});
@@ -0,0 +1,36 @@
import { describe, expect, it } from "vitest";
import { __evalRouteTestUtils } from "../src/routes/eval";
describe("eval runtime question splitting", () => {
it("merges short conversational tails into previous question", () => {
const parsed = __evalRouteTestUtils.splitQuestionCandidate(
"Покажи контрагентов с риском несверки по акту? и коротко? без воды?"
);
expect(parsed).toHaveLength(1);
expect(parsed[0]).toMatch(/коротко/i);
expect(parsed[0]).toMatch(/без воды/i);
});
it("keeps independent full questions as separate items", () => {
const parsed = __evalRouteTestUtils.splitQuestionCandidate(
"Где зависли оплаты по счету 60? Какие документы не закрылись по 62 за июль 2020?"
);
expect(parsed).toHaveLength(2);
expect(parsed[0]).toMatch(/\?/);
expect(parsed[1]).toMatch(/\?/);
});
it("normalizes list input and removes placeholders and duplicates", () => {
const parsed = __evalRouteTestUtils.normalizeRuntimeQuestions([
"Вопросы",
"Покажи хвосты по поставщикам",
"и коротко?"
]);
expect(parsed).toHaveLength(1);
expect(parsed[0]).toMatch(/поставщик/i);
expect(parsed[0]).toMatch(/коротко/i);
});
});