NODEDC_1C/llm_normalizer/backend/tests/assistantLivingChatRuntimeA...

622 lines
25 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { describe, expect, it, vi } from "vitest";
import { runAssistantLivingChatRuntime } from "../src/services/assistantLivingChatRuntimeAdapter";
function buildRuntimeInput(overrides: Record<string, unknown> = {}) {
const executeLlmChat = vi.fn(async () => "llm-text");
const resolveDataScopeProbe = vi.fn(async () => null);
return {
userMessage: "тест",
sessionItems: [],
modeDecision: { mode: "chat", reason: "living_chat_signal_detected" },
sessionScope: {
knownOrganizations: [],
selectedOrganization: null,
activeOrganization: null
},
addressRuntimeMeta: null,
traceIdFactory: () => "chat-trace-fixed",
toNonEmptyString: (value: unknown) => {
const text = String(value ?? "").trim();
return text.length > 0 ? text : null;
},
mergeKnownOrganizations: (values: unknown[]) =>
Array.from(
new Set(
(Array.isArray(values) ? values : [])
.map((item) => (typeof item === "string" ? item.trim() : ""))
.filter((item) => item.length > 0)
)
),
hasAssistantDataScopeMetaQuestionSignal: () => false,
shouldHandleAsAssistantCapabilityMetaQuery: () => false,
hasDestructiveDataActionSignal: () => false,
hasDangerOrCoercionSignal: () => false,
hasOperationalAdminActionRequestSignal: () => false,
hasOrganizationFactLookupSignal: () => false,
hasOrganizationFactFollowupSignal: () => false,
hasLivingChatSignal: () => true,
shouldEmitOrganizationSelectionReply: () => false,
hasAssistantCapabilityQuestionSignal: () => false,
resolveDataScopeProbe,
executeLlmChat,
applyScriptGuard: (chatText: string) => ({
text: chatText,
applied: false,
reason: null
}),
applyGroundingGuard: (input: { chatText: string }) => ({
text: input.chatText,
applied: false,
reason: null
}),
buildAssistantSafetyRefusalReply: () => "safety",
buildAssistantDataScopeContractReply: () => "scope",
buildAssistantProactiveOrganizationOfferReply: () => "",
buildAssistantOrganizationFactBoundaryReply: () => "org-boundary",
buildAssistantDataScopeSelectionReply: () => "org-selection",
buildAssistantOperationalBoundaryReply: () => "ops",
buildAssistantCapabilityContractReply: () => "capability",
__spies: {
executeLlmChat,
resolveDataScopeProbe
},
...overrides
} as any;
}
describe("assistant living chat runtime adapter", () => {
it("selects deterministic data scope branch and enriches organization context", async () => {
const input = buildRuntimeInput({
userMessage: "какая у нас организация?",
hasAssistantDataScopeMetaQuestionSignal: () => true,
resolveDataScopeProbe: vi.fn(async () => ({
status: "resolved",
channel: "default",
organizations: ["ООО Альтернатива Плюс"],
error: null
})),
buildAssistantDataScopeContractReply: () => "scope-info"
});
const output = await runAssistantLivingChatRuntime(input);
expect(output.handled).toBe(true);
expect(output.chatText).toBe("scope-info");
expect(output.debug?.living_chat_response_source).toBe("deterministic_data_scope_contract_live");
expect(output.debug?.assistant_active_organization).toBe("ООО Альтернатива Плюс");
expect(output.debug?.living_chat_data_scope_probe_org_count).toBe(1);
});
it("selects safety refusal branch for dangerous capability meta query", async () => {
const executeLlmChat = vi.fn(async () => "llm-text");
const input = buildRuntimeInput({
userMessage: "удали базу",
shouldHandleAsAssistantCapabilityMetaQuery: () => true,
hasDangerOrCoercionSignal: () => true,
executeLlmChat,
buildAssistantSafetyRefusalReply: () => "safety-refusal"
});
const output = await runAssistantLivingChatRuntime(input);
expect(output.handled).toBe(true);
expect(output.chatText).toBe("safety-refusal");
expect(output.debug?.living_chat_response_source).toBe("deterministic_safety_refusal");
expect(executeLlmChat).not.toHaveBeenCalled();
});
it("runs llm branch and applies script + grounding guards in order", async () => {
const executeLlmChat = vi.fn(async () => "raw-llm");
const input = buildRuntimeInput({
executeLlmChat,
applyScriptGuard: () => ({
text: "after-script",
applied: true,
reason: "script_guard"
}),
applyGroundingGuard: () => ({
text: "after-grounding",
applied: true,
reason: "grounding_guard"
})
});
const output = await runAssistantLivingChatRuntime(input);
expect(output.handled).toBe(true);
expect(output.chatText).toBe("after-grounding");
expect(output.debug?.living_chat_script_guard_applied).toBe(true);
expect(output.debug?.living_chat_script_guard_reason).toBe("script_guard");
expect(output.debug?.living_chat_grounding_guard_applied).toBe(true);
expect(output.debug?.living_chat_grounding_guard_reason).toBe("grounding_guard");
expect(output.debug?.living_chat_response_source).toBe("llm_chat_grounding_guard");
expect(executeLlmChat).toHaveBeenCalledTimes(1);
});
it("builds deterministic broad business evaluation summary from grounded continuity instead of replaying lifecycle noise", async () => {
const executeLlmChat = vi.fn(async () => "raw-llm");
const input = buildRuntimeInput({
userMessage: "Как ты оценишь деятельность компании?",
modeDecision: { mode: "chat", reason: "unsupported_current_turn_meaning_boundary" },
sessionScope: {
knownOrganizations: ["ООО Альтернатива Плюс"],
selectedOrganization: null,
activeOrganization: "ООО Альтернатива Плюс"
},
sessionItems: [
{
role: "assistant",
debug: {
execution_lane: "address_query",
answer_grounding_check: {
status: "grounded"
},
detected_intent: "counterparty_activity_lifecycle",
extracted_filters: {
organization: "ООО Альтернатива Плюс"
}
}
},
{
role: "assistant",
debug: {
execution_lane: "living_chat",
mcp_discovery_response_applied: true,
assistant_mcp_discovery_entry_point_v1: {
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
entry_status: "bridge_executed",
turn_input: {
turn_meaning_ref: {
explicit_entity_candidates: ["Группа СВК"],
explicit_organization_scope: "ООО Альтернатива Плюс",
explicit_date_scope: "2020"
}
},
bridge: {
bridge_status: "answer_draft_ready",
business_fact_answer_allowed: true,
answer_draft: {
answer_mode: "confirmed_with_bounded_inference"
},
pilot: {
pilot_scope: "counterparty_bidirectional_value_flow_query_movements_v1",
derived_bidirectional_value_flow: {
net_amount_human_ru: "3 865 501,50 руб.",
incoming_customer_revenue: {
total_amount_human_ru: "47 628 853,03 руб."
},
outgoing_supplier_payout: {
total_amount_human_ru: "43 763 351,53 руб."
}
}
}
}
}
}
}
],
addressRuntimeMeta: {
toolGateReason: "unsupported_current_turn_meaning_boundary",
orchestrationContract: {
unsupported_current_turn_meaning_boundary: true,
assistant_turn_meaning: {
unsupported_but_understood_family: "broad_business_evaluation"
}
}
},
executeLlmChat
});
const output = await runAssistantLivingChatRuntime(input);
expect(output.handled).toBe(true);
expect(output.chatText.toLowerCase()).toContain("оценка бизнеса");
expect(output.chatText).toContain("ООО Альтернатива Плюс");
expect(output.chatText).toContain("Группа СВК");
expect(output.chatText).toContain("нетто");
expect(output.chatText).toContain("Подтвержденные метрики");
expect(output.chatText).toContain("Денежный поток");
expect(output.chatText).toContain("маржу");
expect(output.debug?.living_chat_response_source).toBe("deterministic_broad_business_evaluation_contract");
expect(executeLlmChat).not.toHaveBeenCalled();
});
it("builds deterministic boundary for unsupported current-turn business meaning", async () => {
const executeLlmChat = vi.fn(async () => "raw-llm");
const input = buildRuntimeInput({
userMessage:
"\u043a\u0430\u043a\u043e\u0439 \u043e\u0431\u043e\u0440\u043e\u0442 \u0431\u044b\u043b \u0441\u0432\u043a",
modeDecision: { mode: "chat", reason: "unsupported_current_turn_meaning_boundary" },
addressRuntimeMeta: {
toolGateReason: "unsupported_current_turn_meaning_boundary",
orchestrationContract: {
unsupported_current_turn_meaning_boundary: true,
assistant_turn_meaning: {
unsupported_but_understood_family: "counterparty_value_or_turnover",
explicit_entity_candidates: [
{
type: "counterparty",
value: "\u0441\u0432\u043a"
}
]
}
}
},
executeLlmChat
});
const output = await runAssistantLivingChatRuntime(input);
expect(output.handled).toBe(true);
expect(output.chatText).toContain("\u043d\u0443\u0436\u0435\u043d \u043e\u0431\u043e\u0440\u043e\u0442");
expect(output.chatText).toContain("\u00ab\u0441\u0432\u043a\u00bb");
expect(output.chatText).toContain("\u043d\u0435 \u0431\u0443\u0434\u0443 \u043f\u043e\u0434\u0441\u0442\u0430\u0432\u043b\u044f\u0442\u044c");
expect(output.debug?.living_chat_response_source).toBe(
"deterministic_unsupported_current_turn_boundary"
);
expect(executeLlmChat).not.toHaveBeenCalled();
});
it("replaces unsupported boundary with guarded MCP discovery response when policy allows it", async () => {
const executeLlmChat = vi.fn(async () => "raw-llm");
const input = buildRuntimeInput({
userMessage: "how long has svk been active",
modeDecision: { mode: "chat", reason: "unsupported_current_turn_meaning_boundary" },
addressRuntimeMeta: {
toolGateReason: "unsupported_current_turn_meaning_boundary",
orchestrationContract: {
unsupported_current_turn_meaning_boundary: true,
assistant_turn_meaning: {
unsupported_but_understood_family: "counterparty_lifecycle_or_age",
explicit_entity_candidates: [
{
type: "counterparty",
value: "SVK"
}
]
}
},
mcpDiscoveryRuntimeEntryPoint: {
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
policy_owner: "assistantMcpDiscoveryRuntimeEntryPoint",
entry_status: "bridge_executed",
hot_runtime_wired: false,
discovery_attempted: true,
turn_input: { adapter_status: "ready" },
bridge: {
bridge_status: "answer_draft_ready",
user_facing_response_allowed: true,
business_fact_answer_allowed: true,
requires_user_clarification: false,
answer_draft: {
answer_mode: "confirmed_with_bounded_inference",
headline: "Confirmed scoped answer.",
confirmed_lines: ["Confirmed fact"],
inference_lines: ["Bounded inference"],
unknown_lines: ["Unconfirmed legal fact"],
limitation_lines: ["Limited source window"],
next_step_line: null
}
},
reason_codes: ["runtime_entry_point_bridge_executed"]
}
},
executeLlmChat
});
const output = await runAssistantLivingChatRuntime(input);
expect(output.handled).toBe(true);
expect(output.chatText).toContain("Confirmed fact");
expect(output.chatText).not.toContain("route");
expect(output.chatText).not.toContain("query_documents");
expect(output.debug?.living_chat_response_source).toBe("mcp_discovery_response_candidate_guarded");
expect(output.debug?.mcp_discovery_response_applied).toBe(true);
expect(output.debug?.mcp_discovery_entry_status).toBe("bridge_executed");
expect(output.debug?.mcp_discovery_attempted).toBe(true);
expect(executeLlmChat).not.toHaveBeenCalled();
});
it("replaces discovery-ready llm chat business answer with guarded MCP discovery response", async () => {
const executeLlmChat = vi.fn(async () => "stale llm answer with old date");
const input = buildRuntimeInput({
userMessage: "how long has svk been active",
modeDecision: { mode: "chat", reason: "non_domain_query_indexed" },
addressRuntimeMeta: {
mcpDiscoveryRuntimeEntryPoint: {
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
policy_owner: "assistantMcpDiscoveryRuntimeEntryPoint",
entry_status: "bridge_executed",
hot_runtime_wired: false,
discovery_attempted: true,
turn_input: {
adapter_status: "ready",
should_run_discovery: true
},
bridge: {
bridge_status: "answer_draft_ready",
user_facing_response_allowed: true,
business_fact_answer_allowed: true,
requires_user_clarification: false,
answer_draft: {
answer_mode: "confirmed_with_bounded_inference",
headline: "Confirmed scoped answer.",
confirmed_lines: ["Confirmed fact"],
inference_lines: ["Bounded inference"],
unknown_lines: ["Unconfirmed legal fact"],
limitation_lines: [],
next_step_line: null
}
},
reason_codes: ["runtime_entry_point_bridge_executed"]
}
},
executeLlmChat
});
const output = await runAssistantLivingChatRuntime(input);
expect(output.handled).toBe(true);
expect(output.chatText).toContain("Confirmed fact");
expect(output.chatText).not.toContain("old date");
expect(output.debug?.living_chat_response_source).toBe("mcp_discovery_response_candidate_guarded");
expect(output.debug?.mcp_discovery_response_applied).toBe(true);
expect(output.debug?.mcp_discovery_entry_status).toBe("bridge_executed");
expect(executeLlmChat).toHaveBeenCalledTimes(1);
});
it("adds proactive organization offer on first smalltalk turn when multiple organizations are available", async () => {
const resolveDataScopeProbe = vi.fn(async () => ({
status: "resolved",
channel: "default",
organizations: ["ООО Альтернатива Плюс", "ООО Лайсвуд", "РАЙМ"],
error: null
}));
const input = buildRuntimeInput({
userMessage: "привет, как дела?",
resolveDataScopeProbe,
buildAssistantProactiveOrganizationOfferReply: (scopeProbe: Record<string, unknown> | null) => {
const organizations = Array.isArray(scopeProbe?.organizations) ? scopeProbe.organizations.join(", ") : "";
return `offer:${organizations}`;
}
});
const output = await runAssistantLivingChatRuntime(input);
expect(output.handled).toBe(true);
expect(output.chatText).toContain("Привет! Всё нормально.");
expect(output.chatText).toContain("offer:ООО Альтернатива Плюс, ООО Лайсвуд, РАЙМ");
expect(output.chatText).not.toContain("llm-text");
expect(output.debug?.living_chat_response_source).toBe("deterministic_smalltalk_with_proactive_scope_offer");
expect(output.debug?.living_chat_proactive_scope_offer_applied).toBe(true);
expect(output.debug?.living_chat_data_scope_probe_org_count).toBe(3);
expect(output.debug?.assistant_known_organizations).toEqual([
"ООО Альтернатива Плюс",
"ООО Лайсвуд",
"РАЙМ"
]);
});
it("does not add proactive organization offer after the session already has assistant context", async () => {
const resolveDataScopeProbe = vi.fn(async () => ({
status: "resolved",
channel: "default",
organizations: ["ООО Альтернатива Плюс", "ООО Лайсвуд"],
error: null
}));
const input = buildRuntimeInput({
userMessage: "привет еще раз",
sessionItems: [{ role: "assistant", text: "Ранее уже отвечал." }],
resolveDataScopeProbe,
buildAssistantProactiveOrganizationOfferReply: () => "offer"
});
const output = await runAssistantLivingChatRuntime(input);
expect(output.handled).toBe(true);
expect(output.chatText).toBe("llm-text");
expect(output.debug?.living_chat_response_source).toBe("llm_chat");
expect(output.debug?.living_chat_proactive_scope_offer_applied).toBe(false);
expect(resolveDataScopeProbe).not.toHaveBeenCalled();
});
it("builds deterministic memory recap for prior selected-object address context", async () => {
const executeLlmChat = vi.fn(async () => "raw-llm");
const input = buildRuntimeInput({
userMessage: "а ты помнишь мы зеркало обсуждали?",
modeDecision: { mode: "chat", reason: "memory_recap_followup_detected" },
sessionItems: [
{
role: "assistant",
debug: {
execution_lane: "address_query",
answer_grounding_check: {
status: "grounded"
},
detected_intent: "inventory_purchase_provenance_for_item",
extracted_filters: {
item: "Зеркало для инвалидов поворотное травмобезопасное",
organization: "ООО Альтернатива Плюс",
as_of_date: "2022-02-28"
}
}
}
],
executeLlmChat
});
const output = await runAssistantLivingChatRuntime(input);
expect(output.handled).toBe(true);
expect(output.chatText).toContain("Зеркало для инвалидов поворотное травмобезопасное");
expect(output.chatText).toContain("кто поставлял");
expect(output.debug?.living_chat_response_source).toBe("deterministic_memory_recap_contract");
expect(executeLlmChat).not.toHaveBeenCalled();
});
it("builds deterministic memory recap for prior grounded MCP discovery counterparty context", async () => {
const executeLlmChat = vi.fn(async () => "raw-llm");
const input = buildRuntimeInput({
userMessage: "а ты помнишь, что мы выяснили по свк?",
modeDecision: { mode: "chat", reason: "memory_recap_followup_detected" },
sessionItems: [
{
role: "assistant",
debug: {
execution_lane: "living_chat",
mcp_discovery_response_applied: true,
assistant_mcp_discovery_entry_point_v1: {
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
entry_status: "bridge_executed",
turn_input: {
turn_meaning_ref: {
explicit_entity_candidates: ["Группа СВК"],
explicit_organization_scope: "ООО Альтернатива Плюс",
explicit_date_scope: "2020"
}
},
bridge: {
bridge_status: "answer_draft_ready",
business_fact_answer_allowed: true,
answer_draft: {
answer_mode: "confirmed_with_bounded_inference"
},
pilot: {
pilot_scope: "counterparty_supplier_payout_query_movements_v1",
derived_value_flow: {
total_amount_human_ru: "43 763 351,53 руб."
}
}
}
}
}
}
],
executeLlmChat
});
const output = await runAssistantLivingChatRuntime(input);
expect(output.handled).toBe(true);
expect(output.chatText).toContain("Группа СВК");
expect(output.chatText).toContain("43 763 351,53 руб.");
expect(output.debug?.living_chat_response_source).toBe("deterministic_memory_recap_contract");
expect(executeLlmChat).not.toHaveBeenCalled();
});
it("uses continuity-backed active organization for organization-fact boundary even when session scope is empty", async () => {
const executeLlmChat = vi.fn(async () => "raw-llm");
const input = buildRuntimeInput({
userMessage: "сколько лет альтернативе плюс",
sessionItems: [
{
role: "assistant",
debug: {
execution_lane: "address_query",
answer_grounding_check: {
status: "grounded"
},
detected_intent: "receivables_confirmed_as_of_date",
extracted_filters: {
organization: "ООО Альтернатива Плюс",
as_of_date: "2020-03-31"
}
}
}
],
hasOrganizationFactLookupSignal: () => true,
buildAssistantOrganizationFactBoundaryReply: (organization: string | null) => `org-boundary:${organization ?? "none"}`,
executeLlmChat
});
const output = await runAssistantLivingChatRuntime(input);
expect(output.handled).toBe(true);
expect(output.chatText).toBe("org-boundary:ООО Альтернатива Плюс");
expect(output.debug?.living_chat_response_source).toBe("deterministic_organization_fact_boundary");
expect(output.debug?.assistant_active_organization).toBe("ООО Альтернатива Плюс");
expect(output.debug?.living_chat_continuity_grounded_context_detected).toBe(true);
expect(output.debug?.living_chat_continuity_active_organization).toBe("ООО Альтернатива Плюс");
expect(executeLlmChat).not.toHaveBeenCalled();
});
it("builds deterministic answer inspection reply over grounded selected-object sale trace", async () => {
const executeLlmChat = vi.fn(async () => "raw-llm");
const input = buildRuntimeInput({
userMessage: "у тебя написано кто контрагент: рабочая станция - это ошибка?",
modeDecision: { mode: "chat", reason: "answer_inspection_followup_detected" },
sessionItems: [
{
role: "assistant",
debug: {
execution_lane: "address_query",
answer_grounding_check: {
status: "grounded"
},
detected_intent: "inventory_sale_trace_for_item",
extracted_filters: {
item: "Рабочая станция универсального специалиста",
organization: "ООО Альтернатива Плюс",
as_of_date: "2016-03-31"
}
}
}
],
executeLlmChat
});
const output = await runAssistantLivingChatRuntime(input);
expect(output.handled).toBe(true);
expect(output.chatText).toContain("Рабочая станция универсального специалиста");
expect(output.debug?.living_chat_response_source).toBe("deterministic_answer_inspection_contract");
expect(executeLlmChat).not.toHaveBeenCalled();
});
it("builds deterministic answer inspection reply over grounded MCP discovery net answer", async () => {
const executeLlmChat = vi.fn(async () => "raw-llm");
const input = buildRuntimeInput({
userMessage: "что ты имел в виду под нетто по свк?",
modeDecision: { mode: "chat", reason: "answer_inspection_followup_detected" },
sessionItems: [
{
role: "assistant",
debug: {
execution_lane: "living_chat",
mcp_discovery_response_applied: true,
assistant_mcp_discovery_entry_point_v1: {
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
entry_status: "bridge_executed",
turn_input: {
turn_meaning_ref: {
explicit_entity_candidates: ["Группа СВК"],
explicit_date_scope: "2020"
}
},
bridge: {
bridge_status: "answer_draft_ready",
business_fact_answer_allowed: true,
answer_draft: {
answer_mode: "confirmed_with_bounded_inference"
},
pilot: {
pilot_scope: "counterparty_bidirectional_value_flow_query_movements_v1"
}
}
}
}
}
],
executeLlmChat
});
const output = await runAssistantLivingChatRuntime(input);
expect(output.handled).toBe(true);
expect(output.chatText).toContain("Группа СВК");
expect(output.chatText).toContain("Нетто");
expect(output.debug?.living_chat_response_source).toBe("deterministic_answer_inspection_contract");
expect(executeLlmChat).not.toHaveBeenCalled();
});
});