711 lines
29 KiB
TypeScript
711 lines
29 KiB
TypeScript
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: "list_documents_by_counterparty",
|
||
extracted_filters: {
|
||
organization: "ООО Альтернатива Плюс",
|
||
counterparty: "для"
|
||
}
|
||
}
|
||
},
|
||
{
|
||
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("builds executive summary from memory instead of running generic address lookup", async () => {
|
||
const executeLlmChat = vi.fn(async () => "raw-llm");
|
||
const input = buildRuntimeInput({
|
||
userMessage:
|
||
"Финально собери executive summary по всему диалогу: где подтверждено, где proxy и что смотреть руками.",
|
||
modeDecision: { mode: "chat", reason: "memory_recap_followup_detected" },
|
||
sessionItems: [
|
||
{
|
||
role: "assistant",
|
||
debug: {
|
||
execution_lane: "address_query",
|
||
answer_grounding_check: {
|
||
status: "grounded"
|
||
},
|
||
detected_intent: "list_documents_by_counterparty",
|
||
extracted_filters: {
|
||
organization: "ООО Альтернатива Плюс",
|
||
counterparty: "для"
|
||
}
|
||
}
|
||
},
|
||
{
|
||
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_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: {
|
||
period_scope: "2020",
|
||
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 руб."
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
],
|
||
executeLlmChat
|
||
});
|
||
|
||
const output = await runAssistantLivingChatRuntime(input);
|
||
|
||
expect(output.handled).toBe(true);
|
||
expect(output.chatText).toContain("Executive summary");
|
||
expect(output.chatText).toContain("Подтверждено");
|
||
expect(output.chatText).toContain("Proxy");
|
||
expect(output.chatText).toContain("3 865 501,50");
|
||
expect(output.chatText).toContain("директору смотреть руками");
|
||
expect(output.chatText).not.toContain("«для»");
|
||
expect(output.debug?.living_chat_response_source).toBe("deterministic_conversation_executive_summary_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();
|
||
});
|
||
});
|