АРЧ АП11 - Архитектура после ге :

This commit is contained in:
2026-04-17 23:49:21 +03:00
parent 8f9364e7c9
commit a5ea9adf53
72 changed files with 7353 additions and 4027 deletions
@@ -54,6 +54,13 @@ describe("addressCounterpartyIntentSignals", () => {
expect(result?.reasons).toContain("counterparty_item_flow_signal_detected");
});
it("classifies passive shipment wording with instrumental counterparty phrasing", () => {
const result = resolveAddressIntent("какие товары или услуги были отгружены нашей компании контрагентом чапурновым?");
expect(result.intent).toBe("list_documents_by_counterparty");
expect(result.reasons).toContain("counterparty_item_flow_signal_detected");
});
it("keeps the main resolver behavior stable through counterparty-owner delegation", () => {
const result = resolveAddressIntent("хвосты покажи по счету 60 на август 2022");
@@ -0,0 +1,55 @@
import { describe, expect, it } from "vitest";
import { resolveCounterpartyAddressIntent } from "../src/services/addressCounterpartyIntentSignals";
import { resolveAddressIntent } from "../src/services/addressIntentResolver";
const utf8Deps = {
hasAny: (text: string, hints: readonly string[]) => hints.some((hint) => text.includes(hint)),
openItemsHints: ["хвост", "долг", "open items"],
openContractsHints: ["договор", "контракт", "open contracts"],
documentsByCounterpartyHints: ["документы по", "documents by counterparty"],
bankOperationsByCounterpartyHints: ["банк", "выписка", "bank operations by counterparty"],
documentsByContractHints: ["документы по договору", "documents by contract"],
hasCounterpartyDebtLongevitySignal: () => false,
hasInventoryAgingSignal: () => false,
hasInventoryProvenanceSignalV2: () => false,
hasInventoryPurchaseDocumentsSignalV2: () => false,
hasInventorySaleTraceSignalV2: () => false,
hasAccountNumberAnchor: (text: string) => /(?:60|62|76)/.test(text),
hasCompactAccountCodeToken: () => false,
hasPeriodCoverageProfileSignal: () => false,
hasPartyAnchorMention: () => false,
hasContractAnchorSignal: (text: string) => text.includes("договор"),
hasAccountBalanceSignal: () => false,
hasDocumentTypeAndAccountSectionProfileSignal: () => false,
hasCounterpartyPopulationAndRolesSignal: () => false,
hasCounterpartyActivityLifecycleSignal: () => false,
hasContractUsageOverviewSignal: () => false,
hasOpenContractsListSignal: () => false,
hasCustomerRevenueAndPaymentsSignal: () => false,
hasSupplierPayoutsProfileSignal: () => false,
hasContractUsageAndValueSignal: () => false,
hasContractListByCounterpartySignal: () => false,
hasBankOperationSignal: (text: string) => text.includes("банк"),
hasDocumentSignal: (text: string) => text.includes("док"),
hasLooseByAnchorMention: () => false,
hasHeuristicCounterpartyAnchor: () => false,
hasCounterpartyShipmentItemFlowSignal: (text: string) => text.includes("отгружал"),
hasImplicitCounterpartyAnchorAroundDocs: () => false,
hasGenericAddressLookupSignal: (text: string) => text.includes("покажи")
};
describe("address counterparty utf8 regression", () => {
it("classifies direct documents-by-counterparty wording with a loose human anchor", () => {
const result = resolveCounterpartyAddressIntent("покажи все документы по чепурнову", utf8Deps);
expect(result?.intent).toBe("list_documents_by_counterparty");
expect(result?.reasons).toContain("documents_by_counterparty_signal_detected");
});
it("keeps the main resolver in the supported contour for direct documents-by-counterparty wording", () => {
const result = resolveAddressIntent("покажи все документы по чепурнову");
expect(result.intent).toBe("list_documents_by_counterparty");
});
});
@@ -4,6 +4,13 @@ import { resolveInventoryAddressIntent } from "../src/services/addressInventoryI
import { resolveAddressIntent } from "../src/services/addressIntentResolver";
describe("addressInventoryIntentSignals", () => {
it("classifies plain Russian stock wording from the agent replay as inventory on hand", () => {
const result = resolveAddressIntent("какие остатки на складе на март 2021");
expect(result.intent).toBe("inventory_on_hand_as_of_date");
expect(result.reasons).toContain("inventory_on_hand_signal_detected");
});
it("classifies warehouse snapshot wording through the extracted inventory owner", () => {
const result = resolveInventoryAddressIntent("show inventory on hand as of 2020-03-15");
@@ -12,6 +12,48 @@ vi.mock("../src/services/addressMcpClient", async () => {
...actual,
executeAddressMcpQuery: executeAddressMcpQueryMock
};
it("keeps plain March stock wording in the inventory contour and asks to choose the company", async () => {
executeAddressMcpQueryMock.mockResolvedValueOnce({
fetched_rows: 2,
matched_rows: 2,
raw_rows: [
{
Period: "2021-03-31T23:59:59Z",
Registrator: "Остатки товаров на складах",
AccountDt: "41.01",
AccountKt: "00.00",
Amount: 6490,
Quantity: 1,
SubcontoDt1: "Пуф арий",
Warehouse: "Основной склад",
Organization: "ООО Альтернатива Плюс"
},
{
Period: "2021-03-31T23:59:59Z",
Registrator: "Остатки товаров на складах",
AccountDt: "41.01",
AccountKt: "00.00",
Amount: 34490,
Quantity: 1,
SubcontoDt1: "Диван трехместный",
Warehouse: "Основной склад",
Organization: "ООО Лайсвуд"
}
],
rows: [],
error: null
});
const service = new AddressQueryService();
const result = await service.tryHandle("какие остатки на складе на март 2021");
expect(result?.handled).toBe(true);
expect(result?.debug.detected_intent).toBe("inventory_on_hand_as_of_date");
expect(result?.response_type).toBe("LIMITED_WITH_REASON");
expect(result?.debug.limited_reason_category).toBe("missing_anchor");
expect(result?.debug.organization_candidates).toEqual(["ООО Альтернатива Плюс", "ООО Лайсвуд"]);
expect(executeAddressMcpQueryMock).toHaveBeenCalledTimes(1);
});
});
import { AddressQueryService } from "../src/services/addressQueryService";
@@ -143,4 +185,52 @@ describe("inventory organization scope grounding", () => {
expect(String(result?.reply_text ?? "")).toContain("ООО Лайсвуд");
expect(executeAddressMcpQueryMock).toHaveBeenCalledTimes(1);
});
it("defers company clarification for item-focused inventory queries and grounds the company from observed rows", async () => {
const item = "Столешница 600*3050*26 дуб ниагара";
executeAddressMcpQueryMock.mockResolvedValueOnce({
fetched_rows: 1,
matched_rows: 1,
raw_rows: [
{
Period: "2019-02-11T00:00:00Z",
Registrator: "Поступление товаров и услуг 00000000077 от 11.02.2019 0:00:00",
AccountDt: "41.01",
AccountKt: "60.01",
Amount: 3724.17,
Quantity: 1,
SubcontoDt1: item,
SubcontoDt3: "Основной склад",
SubcontoKt1: "Торговый дом \\Союз МСК\\",
SubcontoKt2: "Договор поставки № 12 от 01.02.2019",
Organization: "ООО \\Альтернатива Плюс\\"
}
],
rows: [],
error: null
});
const service = new AddressQueryService();
const result = await service.tryHandle("покажи документы по этой позиции", {
knownOrganizations: ["ООО Альтернатива Плюс", "ООО Лайсвуд"],
followupContext: {
previous_intent: "inventory_purchase_provenance_for_item",
previous_filters: {
item,
as_of_date: "2021-03-31",
period_from: "2021-03-01",
period_to: "2021-03-31"
},
previous_anchor_type: "unknown",
previous_anchor_value: null
}
});
expect(result?.handled).toBe(true);
expect(result?.response_type).toBe("FACTUAL_LIST");
expect(result?.debug.detected_intent).toBe("inventory_purchase_documents_for_item");
expect(result?.debug.extracted_filters?.organization).toBe("ООО \\Альтернатива Плюс\\");
expect(result?.debug.reasons).toContain("organization_grounded_from_observed_rows");
expect(executeAddressMcpQueryMock).toHaveBeenCalledTimes(1);
});
});
@@ -549,6 +549,61 @@ describe("inventory selected-object follow-up", () => {
expect(String(result?.reply_text ?? "")).toContain("Поступление товаров и услуг 00000000077");
});
it("does not let carried counterparty scope steal selected-item document follow-up into open items", async () => {
executeAddressMcpQueryMock.mockResolvedValueOnce({
fetched_rows: 1,
matched_rows: 1,
raw_rows: [
{
Period: "2019-02-12T00:00:00Z",
Registrator: "Поступление товаров и услуг 00000000003 от 12.02.2019 0:00:00",
AccountDt: "41.01",
AccountKt: "60.01",
Amount: 3690,
SubcontoDt1: "Столешница 600*3050*26 альмандин",
SubcontoDt3: "Основной склад",
SubcontoKt1: "Торговый дом \\Союз",
SubcontoKt2: "Договор поставки № 12 от 01.02.2019",
Organization: "ООО \\Альтернатива Плюс\\"
}
],
rows: [],
error: null
});
const service = new AddressQueryService();
const result = await service.tryHandle("покажи документы по этой позиции", {
followupContext: {
previous_intent: "inventory_purchase_provenance_for_item",
previous_filters: {
item: "Столешница 600*3050*26 альмандин",
organization: "ООО \\Альтернатива Плюс\\",
counterparty: "Альтернатива Плюс, ООО",
as_of_date: "2021-03-31",
period_from: "2021-03-01",
period_to: "2021-03-31"
},
previous_anchor_type: "counterparty",
previous_anchor_value: "ООО \\Альтернатива Плюс\\",
root_intent: "inventory_on_hand_as_of_date",
root_filters: {
organization: "ООО \\Альтернатива Плюс\\",
as_of_date: "2021-03-31",
period_from: "2021-03-01",
period_to: "2021-03-31"
},
current_frame_kind: "inventory_drilldown"
}
});
expect(result?.handled).toBe(true);
expect(result?.debug.detected_intent).toBe("inventory_purchase_documents_for_item");
expect(result?.debug.selected_recipe).toBe("address_inventory_purchase_documents_for_item_v1");
expect(result?.debug.capability_id).toBe("inventory_inventory_purchase_documents_for_item");
expect(result?.debug.reasons).not.toContain("open_items_from_followup_context");
expect(String(result?.reply_text ?? "")).toContain("Поступление товаров и услуг 00000000003");
});
it("routes buyer follow-up over the same selected item into sale trace instead of replaying provenance", async () => {
executeAddressMcpQueryMock.mockResolvedValueOnce({
fetched_rows: 1,
@@ -0,0 +1,609 @@
import { describe, expect, it, vi } from "vitest";
import { AssistantService } from "../src/services/assistantService";
import { AssistantSessionStore } from "../src/services/assistantSessionStore";
function buildAddressLaneResult(overrides?: Record<string, unknown>): any {
return {
handled: true,
reply_text: "Подтвержден адресный ответ.",
reply_type: "factual",
response_type: "FACTUAL_SUMMARY",
debug: {
detected_mode: "address_query",
detected_intent: "inventory_on_hand_as_of_date",
extracted_filters: {},
selected_recipe: "address_inventory_on_hand_as_of_date_v1",
reasons: ["address_action_detected", "address_entity_detected"],
answer_grounding_check: {
status: "grounded"
}
},
...(overrides ?? {})
};
}
function buildAddressLimitedLaneResult(overrides?: Record<string, unknown>): any {
return {
handled: true,
reply_text: "Нужно уточнить организацию.",
reply_type: "partial_coverage",
response_type: "LIMITED_WITH_REASON",
debug: {
detected_mode: "address_query",
detected_intent: "inventory_on_hand_as_of_date",
extracted_filters: {
period_from: "2021-03-01",
period_to: "2021-03-31",
as_of_date: "2021-03-31"
},
selected_recipe: null,
limited_reason_category: "missing_anchor",
organization_candidates: ['ООО "Альтернатива Плюс"', 'ООО "Лайсвуд"'],
reasons: ["organization_clarification_required", "multiple_known_organizations_detected"]
},
...(overrides ?? {})
};
}
describe("agent semantic inventory regressions", () => {
it("continues the stock query after exact clarification phrase 'давай по Альтернативе Плюс'", async () => {
const calls: Array<{ message: string; options?: any }> = [];
const firstMessage = "какие остатки на складе на март 2021";
const secondMessage = "давай по Альтернативе Плюс";
const organization = 'ООО "Альтернатива Плюс"';
const addressQueryService = {
tryHandle: vi.fn(async (message: string, options?: any) => {
calls.push({ message, options });
if (message === firstMessage) {
return buildAddressLimitedLaneResult();
}
if (
message === secondMessage &&
options?.activeOrganization === organization &&
options?.followupContext?.previous_intent === "inventory_on_hand_as_of_date"
) {
return buildAddressLaneResult({
reply_text: "На 31.03.2021 по ООО \"Альтернатива Плюс\" подтвержден складской срез.",
debug: {
...buildAddressLaneResult().debug,
extracted_filters: {
organization,
period_from: "2021-03-01",
period_to: "2021-03-31",
as_of_date: "2021-03-31"
},
reasons: ["address_followup_context_applied", "organization_grounded_from_scope_candidates"]
}
});
}
return null;
})
} 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 = `agent-semantic-org-${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("partial_coverage");
const second = await service.handleMessage({
session_id: sessionId,
user_message: secondMessage,
useMock: true
} as any);
expect(second.ok).toBe(true);
expect(second.reply_type).toBe("factual");
expect(calls).toHaveLength(2);
expect(calls[1].options?.activeOrganization).toBe(organization);
expect(calls[1].options?.followupContext?.previous_filters?.organization).toBe(organization);
expect(calls[1].options?.followupContext?.root_filters?.organization).toBe(organization);
expect(normalizerService.normalize).not.toHaveBeenCalled();
});
it("restores root stock slice after selected-object drilldown and does not rerun address lane for memory recap", async () => {
const calls: Array<{ message: string; options?: any }> = [];
const restatementMessage = "покажи еще раз остатки на эту же дату";
const recapMessage = "а что мы уже выяснили по этой позиции?";
const item = "Столешница 600*3050*26 альмандин";
const organization = 'ООО "Альтернатива Плюс"';
const addressQueryService = {
tryHandle: vi.fn(async (message: string, options?: any) => {
calls.push({ message, options });
if (
message === restatementMessage &&
options?.followupContext?.root_intent === "inventory_on_hand_as_of_date" &&
options?.followupContext?.root_filters?.as_of_date === "2021-03-31" &&
options?.followupContext?.root_filters?.period_from === "2021-03-01" &&
options?.followupContext?.root_filters?.period_to === "2021-03-31"
) {
return buildAddressLaneResult({
reply_text: "На 31.03.2021 по ООО \"Альтернатива Плюс\" подтвержден складской остаток.",
debug: {
...buildAddressLaneResult().debug,
extracted_filters: {
organization,
period_from: "2021-03-01",
period_to: "2021-03-31",
as_of_date: "2021-03-31"
},
reasons: ["address_followup_context_applied", "intent_restored_to_inventory_root_frame"]
}
});
}
return null;
})
} 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 = `agent-semantic-inventory-${Date.now()}`;
sessions.appendItem(sessionId, {
message_id: "msg-root",
session_id: sessionId,
role: "assistant",
text: "На 31.03.2021 по ООО \"Альтернатива Плюс\" подтвержден складской срез.",
reply_type: "factual",
created_at: "2026-04-17T16:37:39.000Z",
trace_id: "address-root-seed",
debug: {
execution_lane: "address_query",
detected_mode: "address_query",
detected_intent: "inventory_on_hand_as_of_date",
extracted_filters: {
organization,
period_from: "2021-03-01",
period_to: "2021-03-31",
as_of_date: "2021-03-31"
},
selected_recipe: "address_inventory_on_hand_as_of_date_v1",
answer_grounding_check: {
status: "grounded"
}
}
} as any);
sessions.appendItem(sessionId, {
message_id: "msg-provenance",
session_id: sessionId,
role: "assistant",
text: `По позиции ${item} подтвержден поставщик: Торговый дом "Союз".`,
reply_type: "factual",
created_at: "2026-04-17T16:37:52.000Z",
trace_id: "address-provenance-seed",
debug: {
execution_lane: "address_query",
detected_mode: "address_query",
detected_intent: "inventory_purchase_provenance_for_item",
extracted_filters: {
item,
organization,
as_of_date: "2021-03-31"
},
anchor_type: "item",
anchor_value_raw: item,
anchor_value_resolved: item,
selected_recipe: "address_inventory_purchase_provenance_for_item_v1",
answer_grounding_check: {
status: "grounded"
}
}
} as any);
sessions.appendItem(sessionId, {
message_id: "msg-docs",
session_id: sessionId,
role: "assistant",
text: `По позиции ${item} найден документ закупки.`,
reply_type: "factual",
created_at: "2026-04-17T16:38:16.000Z",
trace_id: "address-docs-seed",
debug: {
execution_lane: "address_query",
detected_mode: "address_query",
detected_intent: "inventory_purchase_documents_for_item",
extracted_filters: {
item,
organization,
as_of_date: "2021-03-31"
},
anchor_type: "item",
anchor_value_raw: item,
anchor_value_resolved: item,
selected_recipe: "address_inventory_purchase_documents_for_item_v1",
answer_grounding_check: {
status: "grounded"
}
}
} as any);
const restatement = await service.handleMessage({
session_id: sessionId,
user_message: restatementMessage,
useMock: true
} as any);
expect(restatement.ok).toBe(true);
expect(restatement.reply_type).toBe("factual");
expect(calls).toHaveLength(1);
expect(calls[0].options?.followupContext?.root_filters?.organization).toBe(organization);
expect(calls[0].options?.followupContext?.root_filters?.period_from).toBe("2021-03-01");
expect(calls[0].options?.followupContext?.root_filters?.period_to).toBe("2021-03-31");
expect(calls[0].options?.followupContext?.root_filters?.as_of_date).toBe("2021-03-31");
expect(calls[0].options?.followupContext?.previous_filters?.item).toBeUndefined();
const recap = await service.handleMessage({
session_id: sessionId,
user_message: recapMessage,
useMock: true
} as any);
expect(recap.ok).toBe(true);
expect(recap.reply_type).toBe("factual_with_explanation");
expect(recap.debug?.tool_gate_reason).toBe("memory_recap_followup_detected");
expect(recap.debug?.living_chat_response_source).toBe("deterministic_memory_recap_contract");
expect(String(recap.assistant_reply ?? "")).toContain(item);
expect(calls).toHaveLength(1);
expect(normalizerService.normalize).not.toHaveBeenCalled();
});
it("restores inventory root for bare 'остатки по складу на эту же дату' after receivables drift", async () => {
const calls: Array<{ message: string; options?: any }> = [];
const message = "остатки по складу на эту же дату";
const organization = 'ООО "Альтернатива Плюс"';
const addressQueryService = {
tryHandle: vi.fn(async (requestMessage: string, options?: any) => {
calls.push({ message: requestMessage, options });
if (
requestMessage === message &&
options?.followupContext?.root_intent === "inventory_on_hand_as_of_date" &&
options?.followupContext?.root_filters?.as_of_date === "2020-03-31"
) {
return buildAddressLaneResult({
reply_text: "На 31.03.2020 по ООО \"Альтернатива Плюс\" подтвержден складской срез.",
debug: {
...buildAddressLaneResult().debug,
extracted_filters: {
organization,
period_from: "2020-03-01",
period_to: "2020-03-31",
as_of_date: "2020-03-31"
},
reasons: ["address_followup_context_applied", "intent_restored_to_inventory_root_frame"]
}
});
}
return null;
})
} 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 = `agent-semantic-same-date-${Date.now()}`;
sessions.appendItem(sessionId, {
message_id: "msg-root-stock",
session_id: sessionId,
role: "assistant",
text: "На 31.03.2020 по ООО \"Альтернатива Плюс\" подтвержден складской срез.",
reply_type: "factual",
created_at: "2026-04-17T17:20:00.000Z",
trace_id: "address-root-stock-seed",
debug: {
execution_lane: "address_query",
detected_mode: "address_query",
detected_intent: "inventory_on_hand_as_of_date",
extracted_filters: {
organization,
period_from: "2020-03-01",
period_to: "2020-03-31",
as_of_date: "2020-03-31"
},
selected_recipe: "address_inventory_on_hand_as_of_date_v1",
answer_grounding_check: {
status: "grounded"
}
}
} as any);
sessions.appendItem(sessionId, {
message_id: "msg-receivables",
session_id: sessionId,
role: "assistant",
text: "Итого подтвержденная дебиторская задолженность на 31.03.2020: 15 404 897,08 ₽.",
reply_type: "factual",
created_at: "2026-04-17T17:25:00.000Z",
trace_id: "address-receivables-seed",
debug: {
execution_lane: "address_query",
detected_mode: "address_query",
detected_intent: "receivables_confirmed_as_of_date",
extracted_filters: {
organization,
as_of_date: "2020-03-31",
period_from: "2020-03-01",
period_to: "2020-03-31"
},
selected_recipe: "address_receivables_confirmed_as_of_date_v1",
answer_grounding_check: {
status: "grounded"
}
}
} as any);
const response = await service.handleMessage({
session_id: sessionId,
user_message: message,
useMock: true
} as any);
expect(response.ok).toBe(true);
expect(calls).toHaveLength(1);
expect(calls[0].options?.followupContext?.root_intent).toBe("inventory_on_hand_as_of_date");
expect(calls[0].options?.followupContext?.root_filters?.organization).toBe(organization);
expect(calls[0].options?.followupContext?.root_context_only).toBe(true);
expect(response.reply_type).toBe("factual");
expect(normalizerService.normalize).not.toHaveBeenCalled();
});
it("keeps selected-object document follow-up in inventory contour after an intermediate capability chat turn", async () => {
const calls: Array<{ message: string; options?: any }> = [];
const item = "Столешница 600*3050*26 альмандин";
const organization = 'ООО "Альтернатива Плюс"';
const message = `По выбранному объекту "${item}": покажи документы по этой позиции`;
const addressQueryService = {
tryHandle: vi.fn(async (requestMessage: string, options?: any) => {
calls.push({ message: requestMessage, options });
if (
requestMessage === message &&
options?.followupContext?.previous_intent === "inventory_purchase_provenance_for_item" &&
options?.followupContext?.previous_filters?.item === item &&
options?.followupContext?.previous_filters?.organization === organization
) {
return buildAddressLaneResult({
reply_text: `По позиции ${item} найден подтвержденный документ закупки.`,
debug: {
...buildAddressLaneResult().debug,
detected_intent: "inventory_purchase_documents_for_item",
extracted_filters: {
item,
organization,
as_of_date: "2021-03-31"
},
reasons: ["address_followup_context_applied", "intent_adjusted_to_inventory_followup_context"]
}
});
}
return null;
})
} 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 = `agent-semantic-meta-docs-${Date.now()}`;
sessions.appendItem(sessionId, {
message_id: "msg-root-inventory",
session_id: sessionId,
role: "assistant",
text: "На 31.03.2021 по ООО \"Альтернатива Плюс\" подтвержден складской срез.",
reply_type: "factual",
created_at: "2026-04-17T17:29:00.000Z",
trace_id: "address-root-seed-2",
debug: {
execution_lane: "address_query",
detected_mode: "address_query",
detected_intent: "inventory_on_hand_as_of_date",
extracted_filters: {
organization,
period_from: "2021-03-01",
period_to: "2021-03-31",
as_of_date: "2021-03-31"
},
selected_recipe: "address_inventory_on_hand_as_of_date_v1",
answer_grounding_check: {
status: "grounded"
}
}
} as any);
sessions.appendItem(sessionId, {
message_id: "msg-item-provenance",
session_id: sessionId,
role: "assistant",
text: `По позиции ${item} подтвержден поставщик: Торговый дом "Союз".`,
reply_type: "factual",
created_at: "2026-04-17T17:30:00.000Z",
trace_id: "address-provenance-seed-2",
debug: {
execution_lane: "address_query",
detected_mode: "address_query",
detected_intent: "inventory_purchase_provenance_for_item",
extracted_filters: {
item,
organization,
as_of_date: "2021-03-31"
},
anchor_type: "item",
anchor_value_raw: item,
anchor_value_resolved: item,
selected_recipe: "address_inventory_purchase_provenance_for_item_v1",
answer_grounding_check: {
status: "grounded"
}
}
} as any);
sessions.appendItem(sessionId, {
message_id: "msg-capability-chat",
session_id: sessionId,
role: "assistant",
text: "Могу работать с остатками, документами и взаиморасчетами.",
reply_type: "factual_with_explanation",
created_at: "2026-04-17T17:31:00.000Z",
trace_id: "chat-capability-seed",
debug: {
execution_lane: "living_chat",
living_chat_response_source: "deterministic_capability_contract"
}
} as any);
const response = await service.handleMessage({
session_id: sessionId,
user_message: message,
useMock: true
} as any);
expect(response.ok).toBe(true);
expect(response.reply_type).toBe("factual");
expect(calls).toHaveLength(1);
expect(calls[0].options?.followupContext?.previous_filters?.item).toBe(item);
expect(calls[0].options?.followupContext?.previous_filters?.organization).toBe(organization);
expect(normalizerService.normalize).not.toHaveBeenCalled();
});
it("reuses grounded organization from the last factual address answer instead of re-asking for company", async () => {
const calls: Array<{ message: string; options?: any }> = [];
const organization = 'ООО "Альтернатива Плюс"';
const message = "какие остатки на складе на март 2021";
const addressQueryService = {
tryHandle: vi.fn(async (requestMessage: string, options?: any) => {
calls.push({ message: requestMessage, options });
if (requestMessage === message && options?.activeOrganization === organization) {
return buildAddressLaneResult({
reply_text: "На 31.03.2021 по ООО \"Альтернатива Плюс\" подтвержден складской срез.",
debug: {
...buildAddressLaneResult().debug,
extracted_filters: {
organization,
period_from: "2021-03-01",
period_to: "2021-03-31",
as_of_date: "2021-03-31"
},
reasons: ["organization_scope_restored_from_grounded_address_history"]
}
});
}
return buildAddressLimitedLaneResult();
})
} 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 = `agent-semantic-grounded-org-${Date.now()}`;
sessions.appendItem(sessionId, {
message_id: "msg-counterparty-seed",
session_id: sessionId,
role: "assistant",
text: "По Чапурнову подтверждены документы по ООО \"Альтернатива Плюс\".",
reply_type: "factual",
created_at: "2026-04-17T18:05:00.000Z",
trace_id: "address-counterparty-seed",
debug: {
execution_lane: "address_query",
detected_mode: "address_query",
detected_intent: "list_documents_by_counterparty",
extracted_filters: {
organization,
counterparty: "Чапурнов",
period_from: "2021-03-01",
period_to: "2021-03-31"
},
selected_recipe: "address_list_documents_by_counterparty_v1",
answer_grounding_check: {
status: "grounded"
}
}
} as any);
const response = await service.handleMessage({
session_id: sessionId,
user_message: message,
useMock: true
} as any);
expect(response.ok).toBe(true);
expect(response.reply_type).toBe("factual");
expect(calls).toHaveLength(1);
expect(calls[0].options?.activeOrganization).toBe(organization);
expect(normalizerService.normalize).not.toHaveBeenCalled();
});
});
@@ -32,9 +32,10 @@ describe("assistantBoundaryPolicy", () => {
organizations: ["ООО Альтернатива Плюс"]
});
expect(reply).toContain("MCP-канале `finance`");
expect(reply).toContain("Сейчас доступна организация");
expect(reply).toContain("ООО Альтернатива Плюс");
expect(reply.toLowerCase()).toContain("read-only");
expect(reply).not.toContain("MCP");
expect(reply.toLowerCase()).not.toContain("read-only");
});
it("strips unexpected CJK fragments from live chat reply", () => {
@@ -7,7 +7,7 @@ describe("assistant living chat mode", () => {
const items = [
{
role: "assistant",
text: "Сейчас в активном MCP-канале `default` доступны организации (3): ООО Альтернатива Плюс, ООО Лайсвуд, РАЙМ.",
text: "Сейчас доступны организации (3): ООО Альтернатива Плюс, ООО Лайсвуд, РАЙМ. Скажите, по какой организации смотреть данные.",
debug: {
trace_id: "chat-org-scope",
living_chat_data_scope_probe_status: "resolved",
@@ -91,7 +91,7 @@ describe("assistant living chat mode", () => {
message_id: "msg-seed-org-scope",
session_id: sessionId,
role: "assistant",
text: "Сейчас в активном MCP-канале `default` доступны организации (3): ООО Альтернатива Плюс, ООО Лайсвуд, РАЙМ.",
text: "Сейчас доступны организации (3): ООО Альтернатива Плюс, ООО Лайсвуд, РАЙМ. Скажите, по какой организации смотреть данные.",
reply_type: "factual_with_explanation",
created_at: new Date().toISOString(),
trace_id: "chat-seed-org-scope",
@@ -780,7 +780,8 @@ describe("assistant living chat mode", () => {
expect(response.ok).toBe(true);
expect(response.reply_type).toBe("factual_with_explanation");
expect(String(response.assistant_reply).toLowerCase()).toContain("mcp-канал");
expect(String(response.assistant_reply).toLowerCase()).toContain("организаций");
expect(String(response.assistant_reply).toLowerCase()).not.toContain("mcp");
expect(response.debug?.tool_gate_reason).toBe("assistant_data_scope_query_detected");
expect(response.debug?.living_chat_response_source).toBe("deterministic_data_scope_contract");
expect(chatClient.chat).toHaveBeenCalledTimes(0);
@@ -827,7 +828,8 @@ describe("assistant living chat mode", () => {
expect(response.ok).toBe(true);
expect(response.reply_type).toBe("factual_with_explanation");
expect(String(response.assistant_reply).toLowerCase()).toContain("read-only");
expect(String(response.assistant_reply).toLowerCase()).toContain("по какой смотреть данные");
expect(String(response.assistant_reply).toLowerCase()).not.toContain("read-only");
expect(response.debug?.tool_gate_reason).toBe("assistant_data_scope_query_detected");
expect(response.debug?.living_chat_response_source).toBe("deterministic_data_scope_contract");
expect(chatClient.chat).toHaveBeenCalledTimes(0);
@@ -42,6 +42,12 @@ describe("assistantLivingModePolicy", () => {
).toBe(true);
});
it("detects bare recap wording without 'помнишь' as memory signal", () => {
const policy = buildPolicy();
expect(policy.hasConversationMemoryRecallFollowupSignal("а что мы уже выяснили по этой позиции?")).toBe(true);
});
it("routes casual small-talk to chat mode", () => {
const policy = buildPolicy();
@@ -1169,6 +1169,55 @@ describe("assistant orchestration contract", () => {
expect(decision.livingReason).toBe("address_lane_triggered");
});
it("keeps exact open-items lookup in address lane even when semantic guard overflags deep investigation", () => {
const rawUserMessage = "хвосты покажи по счету 60 на август 2022";
const effectiveAddressUserMessage = "хвосты по счету 60 на август 2022";
const predecomposeContract = buildAddressLlmPredecomposeContractV1({
sourceMessage: rawUserMessage,
canonicalMessage: effectiveAddressUserMessage,
semanticHints: {
exact_data_request_detected: true,
account_scope_kind: "explicit",
account_scope_text: "60",
date_scope_kind: "explicit_period",
date_scope_text: "август 2022"
}
});
const semanticExtractionContract = buildAddressSemanticExtractionContractV1({
sourceMessage: rawUserMessage,
canonicalMessage: effectiveAddressUserMessage,
predecomposeContract
});
const decision = resolveAssistantOrchestrationDecision({
rawUserMessage,
effectiveAddressUserMessage,
followupContext: null,
llmPreDecomposeMeta: {
applied: true,
llmCanonicalCandidateDetected: true,
predecomposeContract,
semanticExtractionContract: {
...semanticExtractionContract,
guard_hints: {
...(semanticExtractionContract.guard_hints ?? {}),
deep_investigation_signal_detected: true
}
}
} 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");
expect(decision.orchestrationContract?.deep_analysis_signal_fallback_to_deep).toBe(false);
expect(
decision.orchestrationContract?.semantic_route_arbitration?.exact_address_intent_protected_from_semantic_deep_hint
).toBe(true);
});
it("keeps open-contracts request in address lane even with stale deep followup context when LLM contract is absent", () => {
const decision = resolveAssistantOrchestrationDecision({
rawUserMessage: "Покажи незакрытые договоры на 2020-12-31",
@@ -48,7 +48,19 @@ describe("assistantMemoryRecapPolicy", () => {
strongDataSignal: false,
aggregateBusinessAnalyticsSignal: false,
lastGroundedAddressDebug: null,
hasPriorAddressDebug: true
hasPriorAddressDebug: true,
sessionItems: [
{
role: "assistant",
debug: {
execution_lane: "address_query",
answer_grounding_check: {
status: "grounded"
},
detected_intent: "list_documents_by_counterparty"
}
}
]
});
expect(signals.contextualHistoricalCapabilityFollowupDetected).toBe(false);
@@ -67,14 +79,60 @@ describe("assistantMemoryRecapPolicy", () => {
strongDataSignal: true,
aggregateBusinessAnalyticsSignal: false,
lastGroundedAddressDebug: null,
hasPriorAddressDebug: true
hasPriorAddressDebug: true,
sessionItems: [
{
role: "assistant",
debug: {
execution_lane: "address_query",
answer_grounding_check: {
status: "grounded"
},
detected_intent: "inventory_purchase_provenance_for_item",
extracted_filters: {
item: "Рабочая станция",
as_of_date: "2022-02-28"
}
}
}
]
});
expect(signals.contextualHistoricalCapabilityFollowupDetected).toBe(false);
expect(signals.contextualMemoryRecapFollowupDetected).toBe(true);
});
it("builds deterministic recap from prior selected object context", () => {
it("does not trigger recap from ungrounded address history", () => {
const signals = policy.resolveRouteMemorySignals({
rawUserMessage: "а ты помнишь что мы обсуждали?",
repairedRawUserMessage: "",
effectiveAddressUserMessage: "",
repairedEffectiveAddressUserMessage: "",
dataScopeMetaQuery: false,
capabilityMetaQuery: false,
dataRetrievalSignal: false,
strongDataSignal: false,
aggregateBusinessAnalyticsSignal: false,
lastGroundedAddressDebug: null,
hasPriorAddressDebug: true,
sessionItems: [
{
role: "assistant",
debug: {
execution_lane: "address_query",
detected_intent: "inventory_purchase_documents_for_item",
extracted_filters: {
item: "Рабочая станция"
}
}
}
]
});
expect(signals.contextualMemoryRecapFollowupDetected).toBe(false);
});
it("builds deterministic recap summary from recent selected-object facts", () => {
const context = resolveAssistantLivingChatMemoryContext({
modeDecisionReason: "memory_recap_followup_detected",
sessionItems: [
@@ -82,6 +140,9 @@ describe("assistantMemoryRecapPolicy", () => {
role: "assistant",
debug: {
execution_lane: "address_query",
answer_grounding_check: {
status: "grounded"
},
anchor_type: "item",
anchor_value_resolved: "Рабочая станция",
extracted_filters: {
@@ -89,6 +150,34 @@ describe("assistantMemoryRecapPolicy", () => {
as_of_date: "2022-02-28"
}
}
},
{
role: "assistant",
debug: {
execution_lane: "address_query",
answer_grounding_check: {
status: "grounded"
},
detected_intent: "inventory_purchase_provenance_for_item",
extracted_filters: {
item: "Рабочая станция",
as_of_date: "2022-02-28"
}
}
},
{
role: "assistant",
debug: {
execution_lane: "address_query",
answer_grounding_check: {
status: "grounded"
},
detected_intent: "inventory_purchase_documents_for_item",
extracted_filters: {
item: "Рабочая станция",
as_of_date: "2022-02-28"
}
}
}
]
});
@@ -96,6 +185,50 @@ describe("assistantMemoryRecapPolicy", () => {
const reply = buildAddressMemoryRecapReply({
organization: null,
addressDebug: context.lastMemoryAddressDebug,
sessionItems: [
{
role: "assistant",
debug: {
execution_lane: "address_query",
answer_grounding_check: {
status: "grounded"
},
detected_intent: "inventory_on_hand_as_of_date",
extracted_filters: {
organization: "ООО Альтернатива Плюс",
as_of_date: "2022-02-28"
}
}
},
{
role: "assistant",
debug: {
execution_lane: "address_query",
answer_grounding_check: {
status: "grounded"
},
detected_intent: "inventory_purchase_provenance_for_item",
extracted_filters: {
item: "Рабочая станция",
as_of_date: "2022-02-28"
}
}
},
{
role: "assistant",
debug: {
execution_lane: "address_query",
answer_grounding_check: {
status: "grounded"
},
detected_intent: "inventory_purchase_documents_for_item",
extracted_filters: {
item: "Рабочая станция",
as_of_date: "2022-02-28"
}
}
}
],
toNonEmptyString: (value: unknown) => {
const text = String(value ?? "").trim();
return text.length > 0 ? text : null;
@@ -104,6 +237,8 @@ describe("assistantMemoryRecapPolicy", () => {
expect(context.contextualMemoryRecapFollowup).toBe(true);
expect(reply).toContain("Рабочая станция");
expect(reply).toContain("28.02.2022");
expect(reply).toContain("мы уже выяснили");
expect(reply).toContain("разобрали, кто поставлял");
expect(reply).toContain("подняли документы закупки");
});
});
@@ -294,4 +294,27 @@ describe("assistantRoutePolicy", () => {
expect(decision.orchestrationContract?.unsupported_address_intent_fallback_to_deep).toBe(false);
expect(decision.orchestrationContract?.provider_execution?.llm_runtime_unavailable_detected).toBe(true);
});
it("does not classify colloquial VAT root query as non-domain when L0 address gate is positive", () => {
const policy = buildPolicy({
hasStrongDataIntentSignal: () => true,
resolveAddressToolGateDecision: () => ({
runAddressLane: true,
decision: "run_address_lane",
reason: "address_signal_detected"
})
});
const decision = policy.resolveAssistantOrchestrationDecision({
rawUserMessage: "скок ндс надо заплатить в налоговую на февраль 2017",
effectiveAddressUserMessage: "скок ндс надо заплатить в налоговую на февраль 2017",
followupContext: null,
llmPreDecomposeMeta: null,
useMock: true
});
expect(decision.runAddressLane).toBe(true);
expect(decision.toolGateReason).toBe("address_signal_detected");
expect(decision.livingMode).toBe("address_data");
});
});
@@ -0,0 +1,17 @@
import { describe, expect, it } from "vitest";
import { resolveDomainPolarityGuard } from "../src/services/assistantRuntimeGuards";
describe("assistant runtime guards utf8 regression", () => {
it("resolves supplier polarity for declined russian account wording", () => {
const guard = resolveDomainPolarityGuard({
userMessage: "хвосты покажи по счёту 60 на август 2022",
focusDomainHint: "settlements_60_62"
});
expect(guard.outcome).toBe("passed");
expect(guard.polarity).toBe("supplier_payable");
expect(guard.account_scope).toContain("60");
expect(guard.resolved_account_anchors).toContain("60");
});
});
@@ -115,6 +115,29 @@ describe("assistantTransitionPolicy", () => {
});
});
it("promotes same-date inventory restatement after drilldown into root-scoped carryover", () => {
const policy = buildPolicy({
hasInventoryRootTemporalFollowupSignal: () => false
});
const carryover = policy.resolveAddressFollowupCarryoverContext(
"покажи еще раз остатки на эту же дату",
[],
null,
null,
null
);
expect(carryover?.followupSelectionMode).toBe("carry_root_context");
expect(carryover?.followupContext?.root_context_only).toBe(true);
expect(carryover?.followupContext?.previous_intent).toBeUndefined();
expect(carryover?.followupContext?.previous_filters).toEqual({
as_of_date: "2020-03-31",
organization: 'ООО "Альтернатива Плюс"'
});
expect(carryover?.followupContext?.root_intent).toBe("inventory_on_hand_as_of_date");
});
it("builds continuation contract from extracted root carryover", () => {
const policy = buildPolicy();
@@ -146,4 +169,143 @@ describe("assistantTransitionPolicy", () => {
expect(contract.anchor_type).toBe("item");
expect(contract.anchor_value).toBe("Рабочая станция");
});
it("prefers carryover target intent over llm contract drift in continuation contract", () => {
const policy = buildPolicy();
const contract = policy.buildAddressDialogContinuationContractV2(
"покажи договор по гамме",
"покажи договор по гамме",
{
followupContext: {
previous_intent: "customer_revenue_and_payments",
target_intent: "list_contracts_by_counterparty",
previous_anchor_type: "counterparty",
previous_anchor_value: "Гамма-мебель, ООО"
},
previousSourceIntent: "customer_revenue_and_payments",
previousAddressIntent: "customer_revenue_and_payments",
followupSelectionMode: "carry_referenced_entity",
hasImplicitContinuationSignal: false
},
{
predecomposeContract: {
intent: "unknown"
}
}
);
expect(contract.target_intent).toBe("list_contracts_by_counterparty");
expect(contract.decision).toBe("continue_previous");
});
it("drops stale carryover for a fresh standalone topic from another intent family", () => {
const policy = buildPolicy({
findLastAddressAssistantItem: () => ({
text: "Прогноз НДС на март 2020 собран.",
debug: {
detected_intent: "vat_payable_forecast",
extracted_filters: {
period_from: "2020-03-01",
period_to: "2020-03-31"
}
}
}),
hasAddressFollowupContextSignal: () => true,
hasStandaloneAddressTopicSignal: () => true,
resolveAddressIntent: () => ({ intent: "inventory_on_hand_as_of_date" }),
resolveAddressIntentFamily: (intent: unknown) => {
if (String(intent ?? "").startsWith("vat_")) return "vat";
if (String(intent ?? "").startsWith("inventory_")) return "inventory";
return null;
}
});
const carryover = policy.resolveAddressFollowupCarryoverContext(
"остаток на складе за май 2020",
[],
null,
null,
null
);
expect(carryover).toBeNull();
});
it("keeps document intent for short counterparty retarget wording with action verb", () => {
const policy = buildPolicy({
findLastAddressAssistantItem: () => ({
text: "Собран список документов по контрагенту Чапурнов.",
debug: {
detected_intent: "list_documents_by_counterparty",
extracted_filters: {
counterparty: "Чапурнов"
},
anchor_type: "counterparty",
anchor_value_resolved: "Чапурнов"
}
}),
buildAddressFollowupOffer: () => ({
enabled: true,
source_intent: "list_documents_by_counterparty",
suggested_intents: ["bank_operations_by_counterparty"]
}),
isImplicitAddressContinuationByLlm: () => true
});
const carryover = policy.resolveAddressFollowupCarryoverContext("покажи по свк", [], null, null, null);
expect(carryover?.followupContext?.previous_intent).toBe("list_documents_by_counterparty");
expect(carryover?.followupSelectionMode).toBe("carry_previous_intent");
});
it("keeps root-scoped carryover for foreign accounting pivot over inventory drilldown", () => {
const policy = buildPolicy({
findLastAddressAssistantItem: () => ({
text: "Собран sale trace по позиции.",
debug: {
detected_intent: "inventory_sale_trace_for_item",
extracted_filters: {
item: "Кромка с клеем 33 дуб ниагара 137 м",
organization: 'ООО "Альтернатива Плюс"',
as_of_date: "2021-03-31"
},
anchor_type: "item",
anchor_value_resolved: "Кромка с клеем 33 дуб ниагара 137 м"
}
}),
hasAddressFollowupContextSignal: () => true,
resolveAddressIntent: () => ({ intent: "vat_payable_confirmed_as_of_date" }),
resolveAddressIntentFamily: (intent: unknown) => {
if (String(intent ?? "").startsWith("vat_")) return "vat";
if (String(intent ?? "").startsWith("inventory_")) return "inventory";
return null;
},
hasForeignAccountingPivotOverInventoryMessage: () => true,
findRecentInventoryRootFrame: () => ({
intent: "inventory_on_hand_as_of_date",
filters: {
organization: 'ООО "Альтернатива Плюс"',
warehouse: "Основной склад",
as_of_date: "2021-03-31",
period_from: "2021-03-01",
period_to: "2021-03-31"
},
anchorType: "organization",
anchorValue: 'ООО "Альтернатива Плюс"'
})
});
const carryover = policy.resolveAddressFollowupCarryoverContext("а ндс?", [], null, null, null);
expect(carryover?.followupSelectionMode).toBe("carry_root_context");
expect(carryover?.followupContext?.root_context_only).toBe(true);
expect(carryover?.followupContext?.previous_intent).toBeUndefined();
expect(carryover?.followupContext?.root_intent).toBe("inventory_on_hand_as_of_date");
expect(carryover?.followupContext?.previous_filters).toEqual({
organization: 'ООО "Альтернатива Плюс"',
warehouse: "Основной склад",
as_of_date: "2021-03-31",
period_from: "2021-03-01",
period_to: "2021-03-31"
});
});
});
@@ -0,0 +1,138 @@
import { describe, expect, it } from "vitest";
import { composeFactualReply } from "../src/services/address_runtime/composeStage";
describe("counterparty analytics reply builders", () => {
it("keeps counterparty role split answers business-first", () => {
const reply = composeFactualReply(
"counterparty_population_and_roles",
[
{
period: "2000-01-01T00:00:00Z",
registrator: "CP_TOTAL",
account_dt: "",
account_kt: "",
amount: 30,
analytics: []
},
{
period: "2000-01-01T00:00:00Z",
registrator: "CP_CUSTOMER_ACTIVE",
account_dt: "",
account_kt: "",
amount: 12,
analytics: []
},
{
period: "2000-01-01T00:00:00Z",
registrator: "CP_SUPPLIER_ACTIVE",
account_dt: "",
account_kt: "",
amount: 9,
analytics: []
},
{
period: "2000-01-01T00:00:00Z",
registrator: "CP_MIXED_ACTIVE",
account_dt: "",
account_kt: "",
amount: 4,
analytics: []
}
],
{ userMessage: "покажи роли контрагентов" }
);
expect(reply.responseType).toBe("FACTUAL_SUMMARY");
expect(reply.text).toContain("Распределение ролей по активности:");
expect(reply.text).not.toContain("supplier-роль");
expect(reply.text).not.toContain("customer-роль");
});
it("formats value rankings without technical 'max single' label", () => {
const reply = composeFactualReply(
"customer_revenue_and_payments",
[
{
period: "2020-03-01T00:00:00Z",
registrator: "Поступление 1",
account_dt: "",
account_kt: "",
amount: 500,
analytics: ["Клиент А", "Договор А-1"]
},
{
period: "2020-03-02T00:00:00Z",
registrator: "Поступление 2",
account_dt: "",
account_kt: "",
amount: 1200,
analytics: ["Клиент Б", "Договор Б-1"]
}
],
{ userMessage: "с каких клиенктов самый высокий чек" }
);
expect(reply.responseType).toBe("FACTUAL_LIST");
expect(reply.text).toContain("максимальной сумме одной входящей операции");
expect(reply.text).toContain("максимальная разовая сумма");
expect(reply.text).not.toContain("max single");
});
it("explains organization activity age as 1C activity rather than legal age", () => {
const reply = composeFactualReply(
"counterparty_activity_lifecycle",
[
{
period: "2020-01-15T00:00:00Z",
registrator: "CP_CUSTOMER_ACTIVITY",
account_dt: "62.01",
account_kt: "90.01",
amount: 12,
analytics: ['ООО "Ромашка"']
},
{
period: "2024-03-10T00:00:00Z",
registrator: "CP_CUSTOMER_ACTIVITY",
account_dt: "62.01",
account_kt: "90.01",
amount: 4,
analytics: ['ООО "Ландыш"']
}
],
{
userMessage: "сколько лет активности в базе 1с у нашей компании",
organizationHint: 'ООО "Альтернатива Плюс"'
}
);
expect(reply.responseType).toBe("FACTUAL_SUMMARY");
expect(reply.text).toContain('По активности организации ООО "Альтернатива Плюс" в базе 1С');
expect(reply.text).toContain("Это возраст активности организации в 1С");
});
it("renders contract usage overview with explicit confirmed wording", () => {
const reply = composeFactualReply("contract_usage_overview", [
{
period: "2000-01-01T00:00:00Z",
registrator: "CT_TOTAL",
account_dt: "",
account_kt: "",
amount: 520,
analytics: []
},
{
period: "2000-01-01T00:00:00Z",
registrator: "CT_USED",
account_dt: "",
account_kt: "",
amount: 148,
analytics: []
}
]);
expect(reply.responseType).toBe("FACTUAL_SUMMARY");
expect(reply.text).toContain("Профиль договорной базы собран по справочнику и подтвержденным операциям.");
expect(reply.text).toContain("Использованных договоров с подтвержденной связью с операциями: 148.");
});
});