ДОМЕНЫ - ВОПРОСЫ - НДС: Исправить роутинг НДС-запросов с формулировкой мы должны и добавить регрессионные тесты

This commit is contained in:
2026-04-13 10:32:02 +03:00
parent 4205c6b3e6
commit fd159e13ac
13 changed files with 375 additions and 15 deletions
@@ -2093,6 +2093,13 @@ describe("address intent resolver expansion (M2.3a)", () => {
expect(result.intent).toBe("contract_usage_and_value");
});
it("resolves VAT wording with debt-phrase as confirmed VAT payable intent", () => {
const result = resolveAddressIntent(
"\u0441\u043a\u043e\u043a\u0430 \u043d\u0434\u0441\u0430 \u043c\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430 \u0441\u0435\u043d\u0442\u044f\u0431\u0440\u044c 2017"
);
expect(result.intent).toBe("vat_payable_confirmed_as_of_date");
expect(result.reasons).toContain("vat_payable_confirmed_signal_detected");
});
it("resolves contracts-by-counterparty intent from list wording", () => {
const result = resolveAddressIntent("покажи договора все по жуковке 51");
expect(result.intent).toBe("list_contracts_by_counterparty");
@@ -3335,6 +3342,27 @@ describe("address query limited taxonomy and stage diagnostics", { timeout: 1500
expect(result?.debug.mcp_call_status).not.toBe("skipped");
});
it("returns factual confirmed VAT snapshot instead of partial when payable rows are absent", async () => {
const service = new AddressQueryService();
const result = await service.tryHandle("скок ндс платить надо на март 2020");
expect(result?.handled).toBe(true);
expect(result?.debug.detected_intent).toBe("vat_payable_confirmed_as_of_date");
expect(["FACTUAL_LIST", "FACTUAL_SUMMARY"]).toContain(result?.response_type);
expect(result?.reply_type).not.toBe("partial_coverage");
expect(result?.debug.result_mode).toBe("confirmed_balance");
expect(result?.debug.balance_confirmed).toBe(true);
});
it("keeps mixed VAT + debt wording in VAT lane (not payables/contracts)", async () => {
const service = new AddressQueryService();
const result = await service.tryHandle(
"\u0441\u043a\u043e\u043a\u0430 \u043d\u0434\u0441\u0430 \u043c\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430 \u0441\u0435\u043d\u0442\u044f\u0431\u0440\u044c 2017"
);
expect(result?.handled).toBe(true);
expect(result?.debug.detected_intent).toBe("vat_payable_confirmed_as_of_date");
expect(result?.debug.selected_recipe).toBe("address_vat_payable_confirmed_as_of_date_v1");
expect(result?.debug.result_mode).toBe("confirmed_balance");
});
it("routes contracts-by-counterparty intent into dedicated catalog recipe", async () => {
const service = new AddressQueryService();
const result = await service.tryHandle("покажи договора все по жуковке 51");
@@ -3755,6 +3783,27 @@ describe("address decompose stage follow-up carryover", () => {
expect(result?.baseReasons).toContain("intent_adjusted_to_vat_followup_context");
expect(result?.baseReasons).toContain("as_of_date_from_followup_context");
});
it("keeps explicit current-date VAT follow-up and does not inherit stale as-of date", () => {
const result = runAddressDecomposeStage("а на текущую дату", {
previous_intent: "vat_payable_confirmed_as_of_date",
previous_filters: {
period_from: "2016-03-01",
period_to: "2016-03-31",
as_of_date: "2016-03-31"
},
previous_anchor_type: "unknown",
previous_anchor_value: null
});
const todayIso = new Date().toISOString().slice(0, 10);
expect(result).not.toBeNull();
expect(result?.mode.mode).toBe("address_query");
expect(result?.intent.intent).toBe("vat_payable_confirmed_as_of_date");
expect(result?.filters.extracted_filters.as_of_date).toBe(todayIso);
expect(result?.filters.extracted_filters.as_of_date).not.toBe("2016-03-31");
expect(result?.filters.extracted_filters.period_from).toBeUndefined();
expect(result?.filters.extracted_filters.period_to).toBeUndefined();
});
});
describe("address recipe catalog counterparty filtering", () => {
@@ -4033,3 +4082,4 @@ describe("address recipe catalog counterparty filtering", () => {
});
});
@@ -1415,6 +1415,187 @@ describe("assistant address follow-up carryover", () => {
expect(normalizerService.normalize).not.toHaveBeenCalled();
});
it("keeps month-only VAT follow-up phrase in address lane", async () => {
const calls: Array<{ message: string; options?: any }> = [];
const firstMessage = "\u0441\u043a\u043e\u043a \u043d\u0434\u0441 \u043d\u0430\u0434\u043e \u0437\u0430\u043f\u043b\u0430\u0442\u0438\u0442\u044c \u0432 \u043d\u0430\u043b\u043e\u0433\u043e\u0432\u0443\u044e \u043d\u0430 \u0444\u0435\u0432\u0440\u0430\u043b\u044c 2017";
const followupMessage = "\u0430 \u043d\u0430 \u043c\u0430\u0440\u0442";
const firstVatResult = buildAddressLaneResult({
debug: {
...buildAddressLaneResult().debug,
detected_intent: "vat_liability_confirmed_for_tax_period",
extracted_filters: {
sort: "period_desc",
limit: 20,
period_from: "2017-01-01",
period_to: "2017-03-31"
},
selected_recipe: "address_vat_liability_confirmed_tax_period_v1",
response_type: "FACTUAL_SUMMARY",
requested_result_mode: "confirmed_balance",
result_mode: "confirmed_balance",
balance_confirmed: true
}
});
const followupVatResult = buildAddressLaneResult({
debug: {
...buildAddressLaneResult().debug,
detected_intent: "vat_liability_confirmed_for_tax_period",
extracted_filters: {
sort: "period_desc",
limit: 20,
period_from: "2017-01-01",
period_to: "2017-03-31"
},
selected_recipe: "address_vat_liability_confirmed_tax_period_v1",
response_type: "FACTUAL_SUMMARY",
requested_result_mode: "confirmed_balance",
result_mode: "confirmed_balance",
balance_confirmed: true,
reasons: [
"address_action_detected",
"vat_liability_confirmed_tax_period_signal_detected",
"address_followup_context_applied"
]
}
});
const addressQueryService = {
tryHandle: vi.fn(async (message: string, options?: any) => {
calls.push({ message, options });
if (message === firstMessage) {
return firstVatResult;
}
if (!options?.followupContext) {
return null;
}
return followupVatResult;
})
} as any;
const normalizerService = {
normalize: vi.fn(async () => ({
assistant_reply: "normalizer_fallback_should_not_be_used",
reply_type: "partial_coverage",
debug: {}
}))
} as any;
const sessions = new AssistantSessionStore();
const service = new AssistantService(
normalizerService,
sessions as any,
{} as any,
{ persistSession: vi.fn() } as any,
addressQueryService
);
const sessionId = `asst-address-followup-vat-march-${Date.now()}`;
const first = await service.handleMessage({
session_id: sessionId,
user_message: firstMessage,
useMock: true
} as any);
expect(first.ok).toBe(true);
expect(first.reply_type).toBe("factual");
const second = await service.handleMessage({
session_id: sessionId,
user_message: followupMessage,
useMock: true
} as any);
expect(second.ok).toBe(true);
expect(second.reply_type).toBe("factual");
expect(second.debug?.detected_intent).toBe("vat_liability_confirmed_for_tax_period");
expect(second.debug?.selected_recipe).toBe("address_vat_liability_confirmed_tax_period_v1");
expect(calls).toHaveLength(2);
expect(calls[1].message).toBe(followupMessage);
expect(calls[1].options?.followupContext?.previous_intent).toBe("vat_liability_confirmed_for_tax_period");
expect(calls[1].options?.followupContext?.previous_filters?.period_from).toBe("2017-01-01");
expect(calls[1].options?.followupContext?.previous_filters?.period_to).toBe("2017-03-31");
expect(normalizerService.normalize).not.toHaveBeenCalled();
});
it("keeps 'a na tekushuyu datu' VAT follow-up in address lane", async () => {
const calls: Array<{ message: string; options?: any }> = [];
const firstMessage = "\u0441\u043a\u043e\u043a \u043d\u0430\u0434\u043e \u043d\u0434\u0441 \u043f\u043b\u0430\u0442\u0438\u0442\u044c \u0441 \u0430\u043f\u0440\u0435\u043b\u0435 2017";
const followupMessage = "\u0430 \u043d\u0430 \u0442\u0435\u043a\u0443\u0449\u0443\u044e \u0434\u0430\u0442\u0443";
const vatAsOfResult = buildAddressLaneResult({
debug: {
...buildAddressLaneResult().debug,
detected_intent: "vat_payable_confirmed_as_of_date",
extracted_filters: {
sort: "period_desc",
limit: 20,
period_from: "2017-04-01",
period_to: "2017-04-30",
as_of_date: "2017-04-30"
},
selected_recipe: "address_vat_payable_confirmed_as_of_date_v1",
response_type: "FACTUAL_SUMMARY",
requested_result_mode: "confirmed_balance",
result_mode: "confirmed_balance",
balance_confirmed: true
}
});
const addressQueryService = {
tryHandle: vi.fn(async (message: string, options?: any) => {
calls.push({ message, options });
if (message === firstMessage) {
return vatAsOfResult;
}
if (!options?.followupContext) {
return null;
}
return vatAsOfResult;
})
} as any;
const normalizerService = {
normalize: vi.fn(async () => ({
assistant_reply: "normalizer_fallback_should_not_be_used",
reply_type: "partial_coverage",
debug: {}
}))
} as any;
const sessions = new AssistantSessionStore();
const service = new AssistantService(
normalizerService,
sessions as any,
{} as any,
{ persistSession: vi.fn() } as any,
addressQueryService
);
const sessionId = `asst-address-followup-vat-current-date-${Date.now()}`;
const first = await service.handleMessage({
session_id: sessionId,
user_message: firstMessage,
useMock: true
} as any);
expect(first.ok).toBe(true);
expect(first.reply_type).toBe("factual");
const second = await service.handleMessage({
session_id: sessionId,
user_message: followupMessage,
useMock: true
} as any);
expect(second.ok).toBe(true);
expect(second.reply_type).toBe("factual");
expect(calls).toHaveLength(2);
expect(calls[1].message).toBe(followupMessage);
expect(calls[1].options?.followupContext?.previous_intent).toBe("vat_payable_confirmed_as_of_date");
expect(calls[1].options?.followupContext?.previous_filters?.as_of_date).toBe("2017-04-30");
expect(normalizerService.normalize).not.toHaveBeenCalled();
});
it("passes active organization scope into address lane follow-up context", async () => {
const calls: Array<{ message: string; options?: any }> = [];
const addressQueryService = {
@@ -558,6 +558,40 @@ describe("assistant orchestration contract", () => {
expect(decision.livingReason).toBe("address_lane_triggered");
});
it("keeps 'a na tekushuyu datu' follow-up in address lane when previous VAT context exists", () => {
const decision = resolveAssistantOrchestrationDecision({
rawUserMessage: "а на текущую дату",
effectiveAddressUserMessage: "а на текущую дату",
followupContext: {
previous_intent: "vat_payable_confirmed_as_of_date",
previous_filters: {
period_from: "2016-03-01",
period_to: "2016-03-31",
as_of_date: "2016-03-31"
},
previous_anchor_type: "unknown",
previous_anchor_value: null
},
llmPreDecomposeMeta: {
applied: false,
reason: "normalized_fragment_rejected_semantic_guard",
llmCanonicalCandidateDetected: true,
predecomposeContract: {
mode: "unsupported",
mode_confidence: "low",
intent: "unknown",
intent_confidence: "low"
}
} as any,
useMock: false
});
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");
});
it("keeps explicit address-mode unknown-intent data query in address lane", () => {
const decision = resolveAssistantOrchestrationDecision({
rawUserMessage: