ГЛОБАЛЬНЫЙ РЕФАКТОРИНГ АРХИТЕКТУРЫ - Рефакторинг этапов Stage 3.7 ХВОСТЫ фикс маршрутов по домену задолжностей

This commit is contained in:
2026-04-11 23:34:59 +03:00
parent 160ed18fe5
commit d65969d2ff
16 changed files with 2248 additions and 237 deletions
@@ -925,6 +925,90 @@ describe("address compose stage utf8 headers", () => {
expect(reply.text).toContain("Активные заказчики в 2020 году: 1.");
});
it("returns top-10 lifecycle ranking by years for longest-collaboration customer question", () => {
const reply = composeFactualReply(
"counterparty_activity_lifecycle",
[
{
period: "2019-01-01T00:00:00Z",
registrator: "CP_CUSTOMER_ACTIVITY_YEAR",
account_dt: "",
account_kt: "",
amount: 5,
analytics: ["НОРТОН"]
},
{
period: "2020-01-01T00:00:00Z",
registrator: "CP_CUSTOMER_ACTIVITY_YEAR",
account_dt: "",
account_kt: "",
amount: 7,
analytics: ["НОРТОН"]
},
{
period: "2021-01-01T00:00:00Z",
registrator: "CP_CUSTOMER_ACTIVITY_YEAR",
account_dt: "",
account_kt: "",
amount: 4,
analytics: ["Группа"]
},
{
period: "2022-05-12T12:00:00Z",
registrator: "CP_CUSTOMER_ACTIVITY",
account_dt: "",
account_kt: "",
amount: 12,
analytics: ["НОРТОН"]
}
],
{
userMessage: "Какие заказчики работают с нами дольше всего?"
}
);
expect(reply.responseType).toBe("FACTUAL_LIST");
expect(reply.text).toContain("Заказчиков с самым длинным горизонтом сотрудничества (по годам)");
expect(reply.text).toContain("Топ-");
expect(reply.text).toContain("лет в базе");
expect(reply.text).toContain("НОРТОН");
});
it("renders debt-aging ranking by as-of date for receivables debt-longevity question", () => {
const reply = composeFactualReply(
"list_receivables_counterparties",
[
{
period: "2022-01-01T00:00:00Z",
registrator: "Реализация 1",
account_dt: "62.01",
account_kt: "90.01",
amount: 1000,
analytics: ["Контрагент А", "Договор №A-01 от 10.02.2020"]
},
{
period: "2024-01-01T00:00:00Z",
registrator: "Реализация 2",
account_dt: "62.01",
account_kt: "90.01",
amount: 1500,
analytics: ["Контрагент Б", "Договор №B-01 от 15.03.2023"]
}
],
{
userMessage: "Сколько заказчиков у нас на этот момент могут считаться долгожителями по своим задолженностям?",
asOfDate: "2026-04-11"
}
);
expect(reply.responseType).toBe("FACTUAL_LIST");
expect(reply.text).toContain("Проверил должников по сроку жизни задолженности");
expect(reply.text).toContain("Дата среза: 11.04.2026.");
expect(reply.text).toContain("Приоритет ручной проверки (по возрасту долга, по убыванию):");
expect(reply.text).toContain("1. Контрагент А | договоры:");
expect(reply.text).toContain("2. Контрагент Б | договоры:");
});
it("returns contract usage overview summary", () => {
const reply = composeFactualReply("contract_usage_overview", [
{
@@ -1594,6 +1678,14 @@ describe("address intent resolver expansion (M2.3a)", () => {
expect(result.intent).toBe("counterparty_activity_lifecycle");
});
it("routes debt-longevity wording into receivables intent", () => {
const result = resolveAddressIntent(
"Сколько заказчиков у нас на этот момент могут считаться долгожителями по своим задолженностям?"
);
expect(result.intent).toBe("list_receivables_counterparties");
expect(result.reasons).toContain("receivables_debt_lifecycle_signal_detected");
});
it("resolves supplier lifecycle segmentation wording into lifecycle intent", () => {
const result = resolveAddressIntent("Раздели поставщиков на регулярных и эпизодических по активности.");
expect(result.intent).toBe("counterparty_activity_lifecycle");
@@ -1759,6 +1851,11 @@ describe("address intent resolver expansion (M2.3a)", () => {
expect(result.intent).toBe("list_receivables_counterparties");
});
it("routes overdue unpaid buyers wording into receivables intent", () => {
const result = resolveAddressIntent("какие покупатели пока не оплатили свои товары или услуги, хотя сроки давно прошли?");
expect(result.intent).toBe("list_receivables_counterparties");
});
it("routes reconciliation mismatch wording into open contracts intent", () => {
const result = resolveAddressIntent(
"Покажи контрагентов, по которым сальдо скорее всего не совпадет с их актом сверки. Может, стоит поторопиться и запросить сверку?"
@@ -1780,6 +1877,23 @@ describe("address intent resolver expansion (M2.3a)", () => {
expect(result.intent).toBe("list_open_contracts");
});
it("routes payments-without-settlement-closure wording into open contracts intent", () => {
const result = resolveAddressIntent("где у нас есть оплаты без закрытия взаиморасчетов, и это уже требует ручной проверки?");
expect(result.intent).toBe("list_open_contracts");
});
it("routes shipments-without-closing-docs wording into open contracts intent", () => {
const result = resolveAddressIntent("где у нас есть отгрузки без документов для их закрытия и это уже требует внимания?");
expect(result.intent).toBe("list_open_contracts");
});
it("routes closing-without-supporting-docs wording into open contracts intent", () => {
const result = resolveAddressIntent(
"где у нас есть закрытие счетов без подтверждающих документов и это уже требует ручной проверки?"
);
expect(result.intent).toBe("list_open_contracts");
});
it("routes documents-without-payments wording into open contracts intent", () => {
const result = resolveAddressIntent(
"По каким контрагентам документы есть, а оплат нет. Может, стоит взять на карандаш такие ситуации чтоб не тянуть дальше?"
@@ -2336,7 +2450,7 @@ describe("address filter extraction for balance drilldown", () => {
});
});
describe("address query limited taxonomy and stage diagnostics", () => {
describe("address query limited taxonomy and stage diagnostics", { timeout: 15000 }, () => {
it("injects as_of_date from analysis context when user message has no explicit period", async () => {
const service = new AddressQueryService();
const result = await service.tryHandle("Покажи контрагентов с незакрытыми хвостами", {
@@ -2385,6 +2499,22 @@ describe("address query limited taxonomy and stage diagnostics", () => {
expect(result?.debug.limited_reason_category).not.toBe("unsupported");
});
it("keeps strict account scope for receivables risk replies and excludes far-future leakage", async () => {
const service = new AddressQueryService();
const result = await service.tryHandle(
"Покажи контрагентов, чьи заказы на отгрузку еще не оплачены, но сальдо уже отрицательное."
);
expect(result?.handled).toBe(true);
expect(result?.debug.detected_intent).toBe("list_receivables_counterparties");
expect(result?.debug.account_scope_mode).toBe("strict");
expect(result?.debug.account_scope_fallback_applied).toBe(false);
if (result?.response_type === "FACTUAL_LIST") {
const reply = String(result?.reply_text ?? "");
expect(reply).not.toMatch(/68\.0?2\s*\/\s*19\.0?4/);
expect(reply).not.toContain("2030-");
}
});
it("routes payments-without-closing-docs wording into open contracts lane", async () => {
const service = new AddressQueryService();
const result = await service.tryHandle(
@@ -2396,6 +2526,47 @@ describe("address query limited taxonomy and stage diagnostics", () => {
expect(result?.debug.limited_reason_category).not.toBe("unsupported");
});
it("routes payments-without-settlement-closure wording into open contracts lane", async () => {
const service = new AddressQueryService();
const result = await service.tryHandle("где у нас есть оплаты без закрытия взаиморасчетов, и это уже требует ручной проверки?");
expect(result?.handled).toBe(true);
expect(result?.debug.detected_intent).toBe("list_open_contracts");
expect(result?.debug.selected_recipe).toBe("address_open_contracts_candidates_v1");
expect(result?.debug.limited_reason_category).not.toBe("missing_anchor");
expect(result?.debug.limited_reason_category).not.toBe("unsupported");
});
it("routes shipments-without-closing-docs wording into open contracts lane", async () => {
const service = new AddressQueryService();
const result = await service.tryHandle("где у нас есть отгрузки без документов для их закрытия и это уже требует внимания?");
expect(result?.handled).toBe(true);
expect(result?.debug.detected_intent).toBe("list_open_contracts");
expect(result?.debug.selected_recipe).toBe("address_open_contracts_candidates_v1");
expect(result?.debug.limited_reason_category).not.toBe("missing_anchor");
expect(result?.debug.limited_reason_category).not.toBe("unsupported");
});
it("routes closing-without-supporting-docs wording into open contracts lane", async () => {
const service = new AddressQueryService();
const result = await service.tryHandle(
"где у нас есть закрытие счетов без подтверждающих документов и это уже требует ручной проверки?"
);
expect(result?.handled).toBe(true);
expect(result?.debug.detected_intent).toBe("list_open_contracts");
expect(result?.debug.selected_recipe).toBe("address_open_contracts_candidates_v1");
expect(result?.debug.limited_reason_category).not.toBe("missing_anchor");
expect(result?.debug.limited_reason_category).not.toBe("unsupported");
});
it("keeps strict account scope for open-contract scans", async () => {
const service = new AddressQueryService();
const result = await service.tryHandle("Какие незакрытые документы по договорам у нас уже давно пора проверить?");
expect(result?.handled).toBe(true);
expect(result?.debug.detected_intent).toBe("list_open_contracts");
expect(result?.debug.account_scope_mode).toBe("strict");
expect(result?.debug.account_scope_fallback_applied).toBe(false);
});
it("routes stale advances wording into open contracts lane without missing-anchor fallback", async () => {
const service = new AddressQueryService();
const result = await service.tryHandle(
@@ -2419,6 +2590,18 @@ describe("address query limited taxonomy and stage diagnostics", () => {
expect(result?.debug.limited_reason_category).not.toBe("unsupported");
});
it("routes overdue unpaid buyers wording into receivables lane without missing-anchor fallback", async () => {
const service = new AddressQueryService();
const result = await service.tryHandle(
"какие покупатели пока не оплатили свои товары или услуги, хотя сроки давно прошли?"
);
expect(result?.handled).toBe(true);
expect(result?.debug.detected_intent).toBe("list_receivables_counterparties");
expect(result?.debug.selected_recipe).toBe("address_movements_receivables_v1");
expect(result?.debug.limited_reason_category).not.toBe("missing_anchor");
expect(result?.debug.limited_reason_category).not.toBe("unsupported");
});
it("routes documents-without-payments wording into open contracts lane", async () => {
const service = new AddressQueryService();
const result = await service.tryHandle(
@@ -2629,6 +2812,38 @@ describe("address query limited taxonomy and stage diagnostics", () => {
expect(["FACTUAL_LIST", "LIMITED_WITH_REASON", "FACTUAL_SUMMARY"]).toContain(result?.response_type);
});
it("routes longest-collaboration customer wording into lifecycle aggregate recipe", async () => {
const service = new AddressQueryService();
const result = await service.tryHandle(
"Какие заказчики работают с нами дольше всего?"
);
expect(result?.handled).toBe(true);
expect(result?.debug.detected_intent).toBe("counterparty_activity_lifecycle");
expect(result?.debug.selected_recipe).toBe("address_counterparty_activity_lifecycle_v1");
expect(result?.debug.mcp_call_status).not.toBe("skipped");
expect(["FACTUAL_LIST", "LIMITED_WITH_REASON", "FACTUAL_SUMMARY"]).toContain(result?.response_type);
if (result?.response_type === "FACTUAL_LIST") {
expect(String(result.reply_text)).toContain("лет в базе");
expect(String(result.reply_text)).toContain("Топ-");
}
});
it("routes debt-longevity wording into receivables lane with factual reply", async () => {
const service = new AddressQueryService();
const result = await service.tryHandle(
"Сколько заказчиков у нас на этот момент могут считаться долгожителями по своим задолженностям?"
);
expect(result?.handled).toBe(true);
expect(result?.debug.detected_intent).toBe("list_receivables_counterparties");
expect(result?.debug.selected_recipe).toBe("address_open_items_by_party_or_contract_v1");
expect(result?.debug.mcp_call_status).not.toBe("skipped");
expect(["FACTUAL_LIST", "LIMITED_WITH_REASON", "FACTUAL_SUMMARY"]).toContain(result?.response_type);
if (result?.response_type === "FACTUAL_LIST") {
expect(String(result.reply_text)).toContain("сроку жизни задолженности");
expect(String(result.reply_text)).toContain("Дата среза");
}
});
it("routes stale contracts wording into contract usage overview recipe", async () => {
const service = new AddressQueryService();
const result = await service.tryHandle("Какие договоры давно не использовались?");
@@ -2649,13 +2864,13 @@ describe("address query limited taxonomy and stage diagnostics", () => {
expect(["FACTUAL_LIST", "LIMITED_WITH_REASON", "FACTUAL_SUMMARY"]).toContain(result?.response_type);
});
it("returns missing_anchor for open items without concrete counterparty/contract anchor", async () => {
it("allows broad open items scan without forcing missing_anchor", async () => {
const service = new AddressQueryService();
const result = await service.tryHandle("show open items by contract");
expect(result?.handled).toBe(true);
expect(result?.response_type).toBe("LIMITED_WITH_REASON");
expect(result?.debug.limited_reason_category).toBe("missing_anchor");
expect(result?.debug.mcp_call_status).toBe("skipped");
expect(result?.debug.detected_intent).toBe("open_items_by_counterparty_or_contract");
expect(result?.debug.mcp_call_status).not.toBe("skipped");
expect(result?.debug.limited_reason_category).not.toBe("missing_anchor");
});
it("does not return fallback factual rows for unmatched open-items contract anchor", async () => {
@@ -278,7 +278,7 @@ describe("assistant orchestration contract", () => {
expect(decision.livingMode).toBe("address_data");
expect(decision.toolGateDecision).toBe("run_address_lane");
expect(decision.toolGateReason).toBe("address_signal_detected");
expect(["address_signal_detected", "address_intent_resolver_detected"]).toContain(String(decision.toolGateReason));
expect(decision.livingReason).toBe("address_lane_triggered");
});
@@ -0,0 +1,108 @@
import { describe, expect, it } from "vitest";
import { AddressQueryService } from "../src/services/addressQueryService";
import { resolveAssistantOrchestrationDecision, resolveLivingAssistantModeDecision } from "../src/services/assistantService";
describe("wave17 run regressions (2026-04-11 real runs)", () => {
it("keeps real run 17:51 data-heavy prompts in address lane", () => {
const realRunPrompts = [
"Где у нас накопились авансы к отгрузкам, которые уже давно пора закрыть или хотя бы перепроверить, чтобы не подозревать худшее?",
"Какие контрагенты у нас на этом моменте могут быть причислены к тем, кто вообще не платит уже несколько месяцев?",
"В каких случаях мы видим зависшие отгрузки, которые уже давно пора закрыть - это грозит проблемами в отчетности."
];
for (const prompt of realRunPrompts) {
const decision = resolveAssistantOrchestrationDecision({
rawUserMessage: prompt,
effectiveAddressUserMessage: prompt,
followupContext: null,
llmPreDecomposeMeta: null,
useMock: false
});
expect(decision.runAddressLane).toBe(true);
expect(["address_mode_classifier_detected", "address_signal_detected", "address_intent_resolver_detected"]).toContain(
decision.toolGateReason
);
expect(decision.livingMode).toBe("address_data");
expect(decision.livingReason).toBe("address_lane_triggered");
}
});
it("keeps short follow-up style prompts out of chat drift when predecompose says unsupported", () => {
const shortFollowups = ["без воды?", "и коротко?", "прям сейчас?"];
for (const prompt of shortFollowups) {
const decision = resolveLivingAssistantModeDecision({
userMessage: prompt,
addressLaneTriggered: false,
useMock: false,
predecomposeMode: "unsupported",
predecomposeModeConfidence: "low"
});
expect(decision.mode).toBe("deep_analysis");
expect(decision.reason).toBe("predecompose_unsupported_mode_fallback_to_deep");
}
});
it("routes data-scope slang wording to chat mode", () => {
const decision = resolveLivingAssistantModeDecision({
userMessage: "по каким конторам можем общаться?",
addressLaneTriggered: false,
useMock: false,
predecomposeMode: "unsupported",
predecomposeModeConfidence: "low"
});
expect(decision.mode).toBe("chat");
expect(decision.reason).toBe("assistant_data_scope_query_detected");
});
it("keeps open-contracts request in address lane even with stale deep followup context", () => {
const decision = resolveAssistantOrchestrationDecision({
rawUserMessage: "Покажи незакрытые договоры на 2020-12-31",
effectiveAddressUserMessage: "Покажи незакрытые договоры на 2020-12-31",
followupContext: {
previous_question_id: "msg-prev",
last_user_message: "почему так по закрытию месяца",
active_domain: "month_close_costs_20_44",
active_requirement_ids: ["R1"],
uncovered_requirement_ids: ["R1"],
referenced_requirement_ids: ["R1"]
} as any,
llmPreDecomposeMeta: {
applied: true,
llmCanonicalCandidateDetected: true,
reason: "normalized_fragment_applied",
predecomposeContract: {
mode: "address_query",
mode_confidence: "high",
intent: "list_open_contracts",
intent_confidence: "medium"
}
} as any,
useMock: false
});
expect(decision.runAddressLane).toBe(true);
expect(decision.livingMode).toBe("address_data");
expect(decision.livingReason).toBe("address_lane_triggered");
expect(decision.orchestrationContract?.deep_analysis_signal_fallback_to_deep).toBe(false);
});
it("uses soft unsupported aggregate replies instead of rigid old template", async () => {
const service = new AddressQueryService();
const prompts = ["какой самый доходный год?", "какие обороты по альтернативе за 2020 год"];
for (const prompt of prompts) {
const result = await service.tryHandle(prompt);
const reply = String(result?.reply_text ?? "");
expect(result?.handled).toBe(true);
expect(result?.reply_type).toBe("partial_coverage");
expect(result?.debug.limited_reason_category).toBe("unsupported");
expect(reply).toContain("Что могу сделать сейчас:");
expect(reply).not.toMatch(/Сейчас этот тип вопроса вне поддерживаемого контура адресного режима/iu);
}
});
});
@@ -0,0 +1,187 @@
import fs from "node:fs";
import path from "node:path";
import { describe, expect, it } from "vitest";
import { AddressQueryService } from "../src/services/addressQueryService";
import { resolveAssistantOrchestrationDecision } from "../src/services/assistantService";
import { resolveAddressIntent } from "../src/services/addressIntentResolver";
type ManualCaseDecision =
| "candidate_for_implementation"
| "needs_dialog_policy_fix"
| "needs_routing_extension"
| "bad_test_case";
interface AnnotationContext {
question_text?: string;
}
interface AnnotationRecord {
run_id: string;
case_id: string;
manual_case_decision: ManualCaseDecision;
resolved: boolean;
context?: AnnotationContext | null;
}
interface ManualCase {
runId: string;
caseId: string;
decision: ManualCaseDecision;
question: string;
resolved: boolean;
}
const MANUAL_CASE_KEYS = [
"assistant-stage1-UMKkFYfg2L::AUTO-003",
"assistant-stage1-UMKkFYfg2L::AUTO-007",
"assistant-stage1-UMKkFYfg2L::AUTO-009",
"assistant-stage1-UMKkFYfg2L::AUTO-012",
"assistant-stage1-UMKkFYfg2L::AUTO-015",
"assistant-stage1-UMKkFYfg2L::AUTO-017",
"assistant-stage1-ywEyJgFkC4::AUTO-002",
"assistant-stage1-ywEyJgFkC4::AUTO-004",
"assistant-stage1-ywEyJgFkC4::AUTO-005",
"assistant-stage1-ywEyJgFkC4::AUTO-006",
"assistant-stage1-ywEyJgFkC4::AUTO-009",
"assistant-stage1-ywEyJgFkC4::AUTO-013",
"assistant-stage1-ywEyJgFkC4::AUTO-014",
"assistant-stage1-ywEyJgFkC4::AUTO-015",
"assistant-stage1-ZL97weIIRG::AUTO-005",
"assistant-stage1-ZL97weIIRG::AUTO-008",
"assistant-stage1-ZL97weIIRG::AUTO-009",
"assistant-stage1-ZL97weIIRG::AUTO-010"
] as const;
const FORMER_UNKNOWN_INTENTS = new Set([
"assistant-stage1-ywEyJgFkC4::AUTO-002",
"assistant-stage1-ywEyJgFkC4::AUTO-009",
"assistant-stage1-ywEyJgFkC4::AUTO-013",
"assistant-stage1-ywEyJgFkC4::AUTO-015",
"assistant-stage1-ZL97weIIRG::AUTO-009",
"assistant-stage1-ZL97weIIRG::AUTO-010"
]);
function textMojibakeScore(value: string): number {
const lower = value.toLowerCase();
let score = 0;
const badFragments = ["рџ", "р°", "сѓ", "с‚", "рµ", "рё", "с€", "с‡", "сЏ", "сЊ", "с‹", "с“", "вђ", "в€"];
for (const fragment of badFragments) {
if (lower.includes(fragment)) {
score -= 4;
}
}
const cyrillic = value.match(/[А-Яа-яЁё]/g)?.length ?? 0;
score += cyrillic;
const replacementCount = value.match(//g)?.length ?? 0;
score -= replacementCount * 3;
return score;
}
function decodeUtf8FromWin1251Mojibake(value: string): string {
try {
const bytes = Uint8Array.from(Array.from(value).map((char) => char.charCodeAt(0) & 0xff));
const decoded = Buffer.from(bytes).toString("utf8");
return textMojibakeScore(decoded) > textMojibakeScore(value) ? decoded : value;
} catch {
return value;
}
}
function decodeUtf8FromLatin1Mojibake(value: string): string {
try {
const decoded = Buffer.from(value, "latin1").toString("utf8");
return textMojibakeScore(decoded) > textMojibakeScore(value) ? decoded : value;
} catch {
return value;
}
}
function repairTextMojibake(value: string): string {
const fromWin1251 = decodeUtf8FromWin1251Mojibake(value);
return decodeUtf8FromLatin1Mojibake(fromWin1251);
}
function buildManualCasesFromAnnotations(): ManualCase[] {
const filePath = path.resolve(__dirname, "../../data/autorun_annotations/annotations.json");
const rows = JSON.parse(fs.readFileSync(filePath, "utf8")) as AnnotationRecord[];
const byKey = new Map<string, AnnotationRecord>();
for (const row of rows) {
const key = `${row.run_id}::${row.case_id}`;
if (MANUAL_CASE_KEYS.includes(key as (typeof MANUAL_CASE_KEYS)[number])) {
byKey.set(key, row);
}
}
const result: ManualCase[] = [];
for (const key of MANUAL_CASE_KEYS) {
const row = byKey.get(key);
if (!row) {
throw new Error(`Missing annotation for ${key}`);
}
const rawQuestion = String(row.context?.question_text ?? "").trim();
if (!rawQuestion) {
throw new Error(`Missing question_text for ${key}`);
}
result.push({
runId: row.run_id,
caseId: row.case_id,
decision: row.manual_case_decision,
question: repairTextMojibake(rawQuestion),
resolved: row.resolved === true
});
}
return result;
}
const MANUAL_WAVE18_CASES = buildManualCasesFromAnnotations();
const MANUAL_LIVE_ASSERT_CASES = MANUAL_WAVE18_CASES.filter((entry) => !entry.resolved);
describe("wave18 manual comments regressions", { timeout: 120000 }, () => {
it("keeps manual-comment prompts in address lane (no capability/data-scope drift)", () => {
for (const entry of MANUAL_WAVE18_CASES) {
const decision = resolveAssistantOrchestrationDecision({
rawUserMessage: entry.question,
effectiveAddressUserMessage: entry.question,
followupContext: null,
llmPreDecomposeMeta: null,
useMock: false
});
expect(decision.runAddressLane, `${entry.runId} ${entry.caseId}`).toBe(true);
expect(decision.livingMode, `${entry.runId} ${entry.caseId}`).toBe("address_data");
expect(String(decision.toolGateReason), `${entry.runId} ${entry.caseId}`).not.toBe("assistant_capability_query_detected");
expect(String(decision.toolGateReason), `${entry.runId} ${entry.caseId}`).not.toBe("assistant_data_scope_query_detected");
}
});
it("resolves previously-unknown manual prompts to supported intents", () => {
for (const entry of MANUAL_WAVE18_CASES) {
const key = `${entry.runId}::${entry.caseId}`;
if (!FORMER_UNKNOWN_INTENTS.has(key)) {
continue;
}
const intent = resolveAddressIntent(entry.question);
expect(intent.intent, key).not.toBe("unknown");
}
});
it(
"returns handled address responses for manual-comment prompts without legacy rigid unsupported template",
async () => {
const service = new AddressQueryService();
for (const entry of MANUAL_LIVE_ASSERT_CASES) {
const result = await service.tryHandle(entry.question);
const reply = String(result?.reply_text ?? "");
expect(result, `${entry.runId} ${entry.caseId}`).not.toBeNull();
expect(result?.handled, `${entry.runId} ${entry.caseId}`).toBe(true);
expect(reply, `${entry.runId} ${entry.caseId}`).not.toMatch(
/Сейчас этот тип вопроса вне поддерживаемого контура адресного режима/iu
);
}
},
120_000
);
});