ГЛОБАЛЬНЫЙ РЕФАКТОРИНГ АРХИТЕКТУРЫ - Рефакторинг этапов Stage 3.7 ХВОСТЫ фикс маршрутов по домену задолжностей
This commit is contained in:
@@ -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
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user