NODEDC_1C/llm_normalizer/backend/tests/assistantAddressOrchestrati...

1351 lines
55 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 { buildAssistantAddressOrchestrationRuntime } from "../src/services/assistantAddressOrchestrationRuntimeAdapter";
function buildInput(overrides: Record<string, unknown> = {}) {
const runAddressLlmPreDecompose = vi.fn(async () => ({
attempted: true,
applied: true,
effectiveMessage: "канон",
reason: "normalized_fragment_applied"
}));
const resolveAddressFollowupCarryoverContext = vi.fn(() => ({
followupContext: { id: "ctx" }
}));
const resolveAssistantOrchestrationDecision = vi.fn(() => ({
runAddressLane: true,
livingMode: "deep_analysis",
livingReason: "address_mode_classifier_detected",
toolGateDecision: "run_address_lane",
toolGateReason: "address_mode_classifier_detected",
orchestrationContract: { schema_version: "assistant_orchestration_contract_v1" }
}));
const buildAddressDialogContinuationContractV2 = vi.fn(() => ({
schema_version: "address_dialog_continuation_contract_v2"
}));
return {
userMessage: "сырой вопрос",
sessionItems: [],
llmProvider: "openai",
useMock: false,
featureAddressLlmPredecomposeV1: true,
runAddressLlmPreDecompose,
buildAddressLlmPredecomposeContractV1: () => ({ schema_version: "address_llm_predecompose_contract_v1" }),
sanitizeAddressMessageForFallback: () => "sanitized",
toNonEmptyString: (value: unknown) => {
if (typeof value !== "string") {
return null;
}
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : null;
},
resolveAddressFollowupCarryoverContext,
resolveAssistantOrchestrationDecision,
buildAddressDialogContinuationContractV2,
__spies: {
runAddressLlmPreDecompose,
resolveAddressFollowupCarryoverContext,
resolveAssistantOrchestrationDecision,
buildAddressDialogContinuationContractV2
},
...overrides
} as any;
}
describe("assistant address orchestration runtime adapter", () => {
it("uses llm predecompose payload when feature is enabled", async () => {
const input = buildInput();
const output = await buildAssistantAddressOrchestrationRuntime(input);
expect(output.addressPreDecompose.reason).toBe("normalized_fragment_applied");
expect(output.addressInputMessage).toBe("канон");
expect(output.orchestrationDecision.runAddressLane).toBe(true);
expect(output.livingModeDecision.mode).toBe("deep_analysis");
expect(output.addressRuntimeMeta.toolGateDecision).toBe("run_address_lane");
expect(output.addressRuntimeMeta.routePolicyContract).toEqual(
expect.objectContaining({
schema_version: "assistant_route_policy_runtime_v1",
policy_owner: "assistantRoutePolicyRuntimeAdapter",
living_mode: "deep_analysis",
tool_gate_decision: "run_address_lane",
has_followup_context: true,
has_orchestration_contract: true
})
);
expect(output.addressRuntimeMeta.dialogContinuationContract).toEqual({
schema_version: "address_dialog_continuation_contract_v2"
});
expect(output.addressRuntimeMeta.mcpDiscoveryRuntimeEntryPoint).toEqual(
expect.objectContaining({
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
entry_status: "skipped_not_applicable",
hot_runtime_wired: false
})
);
expect(input.__spies.runAddressLlmPreDecompose).toHaveBeenCalledTimes(1);
expect(input.__spies.resolveAddressFollowupCarryoverContext).toHaveBeenCalledTimes(1);
expect(input.__spies.resolveAssistantOrchestrationDecision).toHaveBeenCalledTimes(1);
});
it("runs MCP discovery entry point from real orchestration context without changing the route decision", async () => {
const runMcpDiscoveryRuntimeEntryPoint = vi.fn(async () => ({
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
policy_owner: "assistantMcpDiscoveryRuntimeEntryPoint",
entry_status: "bridge_executed",
hot_runtime_wired: false,
discovery_attempted: true
}));
const input = buildInput({
userMessage: "Сколько лет мы работаем с Группа СВК?",
runAddressLlmPreDecompose: vi.fn(async () => ({
attempted: true,
applied: false,
effectiveMessage: "Сколько лет мы работаем с Группа СВК?",
reason: "raw_kept",
predecomposeContract: {
entities: { counterparty: "Группа СВК" },
period: {}
}
})),
resolveAssistantOrchestrationDecision: vi.fn(() => ({
runAddressLane: false,
livingMode: "chat",
livingReason: "unsupported_current_turn_meaning_boundary",
toolGateDecision: "skip_address_lane",
toolGateReason: "unsupported_current_turn_meaning_boundary",
orchestrationContract: {
schema_version: "assistant_orchestration_contract_v1",
assistant_turn_meaning: {
schema_version: "assistant_turn_meaning_v1",
asked_domain_family: "counterparty",
asked_action_family: "counterparty_lifecycle",
unsupported_but_understood_family: "counterparty_lifecycle"
}
}
})),
runMcpDiscoveryRuntimeEntryPoint
});
const output = await buildAssistantAddressOrchestrationRuntime(input);
expect(output.livingModeDecision).toEqual({
mode: "chat",
reason: "unsupported_current_turn_meaning_boundary"
});
expect(output.addressRuntimeMeta.mcpDiscoveryRuntimeEntryPoint).toEqual(
expect.objectContaining({
entry_status: "bridge_executed",
discovery_attempted: true,
hot_runtime_wired: false
})
);
expect(runMcpDiscoveryRuntimeEntryPoint).toHaveBeenCalledWith(
expect.objectContaining({
userMessage: "Сколько лет мы работаем с Группа СВК?",
effectiveMessage: "Сколько лет мы работаем с Группа СВК?",
assistantTurnMeaning: expect.objectContaining({
unsupported_but_understood_family: "counterparty_lifecycle"
}),
predecomposeContract: expect.objectContaining({
entities: { counterparty: "Группа СВК" }
})
})
);
});
it("passes active session organization into MCP discovery when carryover context is absent", async () => {
const runMcpDiscoveryRuntimeEntryPoint = vi.fn(async () => ({
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
policy_owner: "assistantMcpDiscoveryRuntimeEntryPoint",
entry_status: "bridge_executed",
hot_runtime_wired: false,
discovery_attempted: true
}));
const input = buildInput({
userMessage: "money breakdown 2020",
sessionOrganizationScope: {
activeOrganization: "Org A",
selectedOrganization: null,
knownOrganizations: ["Org A"]
},
runAddressLlmPreDecompose: vi.fn(async () => ({
attempted: true,
applied: false,
effectiveMessage: "money breakdown 2020",
reason: "raw_kept",
predecomposeContract: {
mode: "unsupported",
intent: "unknown",
period: {
scope: "year",
period_from: "2020-01-01",
period_to: "2020-12-31",
has_explicit_period: true
}
}
})),
resolveAddressFollowupCarryoverContext: vi.fn(() => null),
resolveAssistantOrchestrationDecision: vi.fn(() => ({
runAddressLane: false,
livingMode: "chat",
livingReason: "unsupported_current_turn_meaning_boundary",
toolGateDecision: "skip_address_lane",
toolGateReason: "unsupported_current_turn_meaning_boundary",
orchestrationContract: {
schema_version: "assistant_orchestration_contract_v1",
assistant_turn_meaning: {
schema_version: "assistant_turn_meaning_v1",
asked_domain_family: "business_overview",
asked_action_family: "broad_evaluation",
unsupported_but_understood_family: "broad_business_evaluation"
}
}
})),
runMcpDiscoveryRuntimeEntryPoint
});
await buildAssistantAddressOrchestrationRuntime(input);
expect(runMcpDiscoveryRuntimeEntryPoint).toHaveBeenCalledWith(
expect.objectContaining({
followupContext: expect.objectContaining({
previous_anchor_type: "organization",
previous_anchor_value: "Org A",
previous_filters: expect.objectContaining({
organization: "Org A"
}),
root_filters: expect.objectContaining({
organization: "Org A"
})
}),
knownOrganizations: ["Org A"]
})
);
});
it("rebuilds business overview period carryover for compact cashflow follow-up when address carryover is absent", async () => {
const runMcpDiscoveryRuntimeEntryPoint = vi.fn(async () => ({
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
policy_owner: "assistantMcpDiscoveryRuntimeEntryPoint",
entry_status: "bridge_executed",
hot_runtime_wired: false,
discovery_attempted: true
}));
const input = buildInput({
userMessage:
"\u0434\u0430\u0439 \u043e\u0434\u043d\u043e\u0439 \u0441\u0442\u0440\u043e\u043a\u043e\u0439 \u0434\u0435\u043d\u044c\u0433\u0438 \u0431\u0435\u0437 \u0440\u0430\u0437\u0431\u0438\u0432\u043a\u0438",
sessionItems: [
{
role: "assistant",
debug: {
assistant_mcp_discovery_entry_point_v1: {
turn_input: {
data_need_graph: {
business_fact_family: "business_overview",
ranking_need: "top_desc"
},
turn_meaning_ref: {
explicit_date_scope: "2020",
explicit_organization_scope:
"\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441"
}
}
}
}
}
],
runAddressLlmPreDecompose: vi.fn(async () => ({
attempted: true,
applied: false,
effectiveMessage:
"\u0434\u0430\u0439 \u043e\u0434\u043d\u043e\u0439 \u0441\u0442\u0440\u043e\u043a\u043e\u0439 \u0434\u0435\u043d\u044c\u0433\u0438 \u0431\u0435\u0437 \u0440\u0430\u0437\u0431\u0438\u0432\u043a\u0438",
reason: "raw_kept",
predecomposeContract: {
mode: "unsupported",
intent: "unknown",
period: {}
}
})),
resolveAddressFollowupCarryoverContext: vi.fn(() => null),
resolveAssistantOrchestrationDecision: vi.fn(() => ({
runAddressLane: false,
livingMode: "chat",
livingReason: "unsupported_current_turn_meaning_boundary",
toolGateDecision: "skip_address_lane",
toolGateReason: "unsupported_current_turn_meaning_boundary",
orchestrationContract: {
schema_version: "assistant_orchestration_contract_v1",
assistant_turn_meaning: {
schema_version: "assistant_turn_meaning_v1",
asked_domain_family: "business_summary",
asked_action_family: "broad_evaluation",
unsupported_but_understood_family: "broad_business_evaluation"
}
}
})),
runMcpDiscoveryRuntimeEntryPoint
});
await buildAssistantAddressOrchestrationRuntime(input);
expect(runMcpDiscoveryRuntimeEntryPoint).toHaveBeenCalledWith(
expect.objectContaining({
followupContext: expect.objectContaining({
previous_discovery_pilot_scope: "business_overview_route_template_v1",
previous_seeded_ranking_need: "top_desc",
previous_filters: expect.objectContaining({
period_from: "2020-01-01",
period_to: "2020-12-31"
}),
root_filters: expect.objectContaining({
period_from: "2020-01-01",
period_to: "2020-12-31"
})
})
})
);
});
it("recovers business overview proof bundles from session history before MCP discovery", async () => {
const valueFlowBundle = {
counterparty: "Group SVK",
incoming_total: 20653490,
outgoing_total: 2129651,
net_amount: 18523839
};
const documentBundle = {
counterparty: "Group SVK",
document_count: 19
};
const runMcpDiscoveryRuntimeEntryPoint = vi.fn(async () => ({
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
policy_owner: "assistantMcpDiscoveryRuntimeEntryPoint",
entry_status: "bridge_executed",
hot_runtime_wired: false,
discovery_attempted: true
}));
const input = buildInput({
userMessage: "Alt Plus",
sessionItems: [
{
role: "assistant",
debug: {
assistant_mcp_discovery_entry_point_v1: {
turn_input: {
turn_meaning_ref: {
asked_domain_family: "business_overview",
asked_action_family: "broad_evaluation",
business_overview_separate_entity_candidates: ["Group SVK"],
previous_counterparty_value_flow_bundle: valueFlowBundle,
previous_counterparty_document_bundle: documentBundle,
metadata_scope_hint: "Group SVK"
}
}
}
}
}
],
runAddressLlmPreDecompose: vi.fn(async () => ({
attempted: true,
applied: false,
effectiveMessage: "Alt Plus",
reason: "raw_kept",
predecomposeContract: {
mode: "unsupported",
intent: "unknown",
entities: { organization: "Alt Plus" },
semantics: { anchor_kind: "organization", anchor_value: "Alt Plus" }
}
})),
resolveAddressFollowupCarryoverContext: vi.fn(() => ({
followupContext: {
previous_intent: "business_overview",
target_intent: "business_overview",
previous_discovery_pilot_scope: "business_overview_route_template_v1",
previous_discovery_loop_status: "awaiting_clarification",
previous_discovery_loop_selected_chain_id: "business_overview",
previous_discovery_loop_metadata_scope_hint: "Group SVK",
previous_anchor_type: "counterparty",
previous_anchor_value: "Group SVK",
previous_filters: {}
}
})),
resolveAssistantOrchestrationDecision: vi.fn(() => ({
runAddressLane: true,
livingMode: "address_data",
livingReason: "address_lane_triggered",
toolGateDecision: "run_address_lane",
toolGateReason: "followup_context_detected",
orchestrationContract: {
schema_version: "assistant_orchestration_contract_v1",
assistant_turn_meaning: {
schema_version: "assistant_turn_meaning_v1",
asked_domain_family: "business_overview",
asked_action_family: "broad_evaluation",
unsupported_but_understood_family: "broad_business_evaluation"
}
}
})),
runMcpDiscoveryRuntimeEntryPoint
});
await buildAssistantAddressOrchestrationRuntime(input);
expect(runMcpDiscoveryRuntimeEntryPoint).toHaveBeenCalledWith(
expect.objectContaining({
followupContext: expect.objectContaining({
previous_discovery_bidirectional_value_flow: valueFlowBundle,
previous_discovery_document_summary: documentBundle
})
})
);
});
it("recovers business overview proof bundles from previous assistant summary text", async () => {
const runMcpDiscoveryRuntimeEntryPoint = vi.fn(async () => ({
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
policy_owner: "assistantMcpDiscoveryRuntimeEntryPoint",
entry_status: "bridge_executed",
hot_runtime_wired: false,
discovery_attempted: true
}));
const input = buildInput({
userMessage: "Alt Plus",
sessionItems: [
{
role: "assistant",
text:
"\u041e\u0442\u0434\u0435\u043b\u044c\u043d\u043e \u043f\u043e \u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\u0443 Group SVK: " +
"\u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u043e \u043f\u043e\u043b\u0443\u0447\u0438\u043b\u0438 20 653 490 \u0440\u0443\u0431., " +
"\u0437\u0430\u043f\u043b\u0430\u0442\u0438\u043b\u0438 2 129 651 \u0440\u0443\u0431., " +
"\u0440\u0430\u0441\u0447\u0435\u0442\u043d\u043e\u0435 \u043d\u0435\u0442\u0442\u043e \u0432 \u043d\u0430\u0448\u0443 \u0441\u0442\u043e\u0440\u043e\u043d\u0443 18 523 839 \u0440\u0443\u0431. " +
"\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u044b \u043f\u043e \u0446\u0435\u043f\u043e\u0447\u043a\u0435: \u043d\u0430\u0439\u0434\u0435\u043d\u043e 19."
}
],
runAddressLlmPreDecompose: vi.fn(async () => ({
attempted: true,
applied: false,
effectiveMessage: "Alt Plus",
reason: "raw_kept",
predecomposeContract: {
mode: "unsupported",
intent: "unknown",
entities: { organization: "Alt Plus" }
}
})),
resolveAddressFollowupCarryoverContext: vi.fn(() => ({
followupContext: {
previous_intent: "business_overview",
target_intent: "business_overview",
previous_discovery_pilot_scope: "business_overview_route_template_v1",
previous_discovery_loop_selected_chain_id: "business_overview",
previous_discovery_loop_metadata_scope_hint: "Group SVK",
previous_anchor_type: "counterparty",
previous_anchor_value: "Group SVK",
previous_filters: {}
}
})),
resolveAssistantOrchestrationDecision: vi.fn(() => ({
runAddressLane: true,
livingMode: "address_data",
livingReason: "address_lane_triggered",
toolGateDecision: "run_address_lane",
toolGateReason: "followup_context_detected",
orchestrationContract: {
schema_version: "assistant_orchestration_contract_v1",
assistant_turn_meaning: {
schema_version: "assistant_turn_meaning_v1",
asked_domain_family: "business_overview",
asked_action_family: "broad_evaluation"
}
}
})),
runMcpDiscoveryRuntimeEntryPoint
});
await buildAssistantAddressOrchestrationRuntime(input);
expect(runMcpDiscoveryRuntimeEntryPoint).toHaveBeenCalledWith(
expect.objectContaining({
followupContext: expect.objectContaining({
previous_discovery_bidirectional_value_flow: expect.objectContaining({
counterparty: "Group SVK",
incoming_customer_revenue: { total_amount_human_ru: "20 653 490 \u0440\u0443\u0431." },
outgoing_supplier_payout: { total_amount_human_ru: "2 129 651 \u0440\u0443\u0431." },
net_amount_human_ru: "18 523 839 \u0440\u0443\u0431."
}),
previous_discovery_document_summary: expect.objectContaining({
counterparty: "Group SVK",
document_count: 19
})
})
})
);
});
it("recovers business overview proof bundles for a plain organization clarification", async () => {
const runMcpDiscoveryRuntimeEntryPoint = vi.fn(async () => ({
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
policy_owner: "assistantMcpDiscoveryRuntimeEntryPoint",
entry_status: "bridge_executed",
hot_runtime_wired: false,
discovery_attempted: true
}));
const input = buildInput({
userMessage: "Alt Plus",
sessionItems: [
{
role: "assistant",
text:
"Отдельно по контрагенту Group SVK: " +
"подтверждено получили 20 653 490 руб., " +
"заплатили 2 129 651 руб., " +
"расчетное нетто в нашу сторону 18 523 839 руб. " +
"Основа: проверенные входящие платежи и исходящие платежи и документы по цепочке: найдено 19."
}
],
runAddressLlmPreDecompose: vi.fn(async () => ({
attempted: true,
applied: false,
effectiveMessage: "Alt Plus",
reason: "raw_kept",
predecomposeContract: {
mode: "unsupported",
intent: "unknown",
entities: { organization: "Alt Plus" }
}
})),
resolveAddressFollowupCarryoverContext: vi.fn(() => ({ followupContext: null })),
resolveAssistantOrchestrationDecision: vi.fn(() => ({
runAddressLane: true,
livingMode: "address_data",
livingReason: "address_lane_triggered",
toolGateDecision: "run_address_lane",
toolGateReason: "followup_context_detected",
orchestrationContract: {
schema_version: "assistant_orchestration_contract_v1",
assistant_turn_meaning: {
schema_version: "assistant_turn_meaning_v1",
asked_domain_family: "business_overview",
asked_action_family: "broad_evaluation"
}
}
})),
runMcpDiscoveryRuntimeEntryPoint
});
await buildAssistantAddressOrchestrationRuntime(input);
expect(runMcpDiscoveryRuntimeEntryPoint).toHaveBeenCalledWith(
expect.objectContaining({
followupContext: expect.objectContaining({
previous_discovery_loop_selected_chain_id: "business_overview",
previous_discovery_loop_metadata_scope_hint: "Group SVK",
previous_discovery_bidirectional_value_flow: expect.objectContaining({
counterparty: "Group SVK",
net_amount_human_ru: "18 523 839 руб."
}),
previous_discovery_document_summary: expect.objectContaining({
counterparty: "Group SVK",
document_count: 19
})
})
})
);
});
it("recovers business overview proof bundles from navigation comparison state", async () => {
const valueFlowBundle = {
counterparty: "Group SVK",
incoming_customer_revenue: { total_amount_human_ru: "20 653 490 руб." },
outgoing_supplier_payout: { total_amount_human_ru: "2 129 651 руб." },
net_amount_human_ru: "18 523 839 руб."
};
const documentBundle = { counterparty: "Group SVK", document_count: 19 };
const runMcpDiscoveryRuntimeEntryPoint = vi.fn(async () => ({
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
policy_owner: "assistantMcpDiscoveryRuntimeEntryPoint",
entry_status: "bridge_executed",
hot_runtime_wired: false,
discovery_attempted: true
}));
const input = buildInput({
userMessage: "Alt Plus",
sessionAddressNavigationState: {
session_context: {
comparison_scope: {
organization: null,
counterparty: { label: "Group SVK" },
proof_bundles: {
counterparty_value_flow_bundle: valueFlowBundle,
counterparty_document_bundle: documentBundle
}
}
}
},
runAddressLlmPreDecompose: vi.fn(async () => ({
attempted: true,
applied: false,
effectiveMessage: "Alt Plus",
reason: "raw_kept",
predecomposeContract: {
mode: "unsupported",
intent: "unknown",
entities: { organization: "Alt Plus" }
}
})),
resolveAddressFollowupCarryoverContext: vi.fn(() => ({ followupContext: null })),
resolveAssistantOrchestrationDecision: vi.fn(() => ({
runAddressLane: true,
livingMode: "address_data",
livingReason: "address_lane_triggered",
toolGateDecision: "run_address_lane",
toolGateReason: "followup_context_detected",
orchestrationContract: {
schema_version: "assistant_orchestration_contract_v1",
assistant_turn_meaning: {
schema_version: "assistant_turn_meaning_v1",
asked_domain_family: "business_overview",
asked_action_family: "broad_evaluation"
}
}
})),
runMcpDiscoveryRuntimeEntryPoint
});
await buildAssistantAddressOrchestrationRuntime(input);
expect(runMcpDiscoveryRuntimeEntryPoint).toHaveBeenCalledWith(
expect.objectContaining({
followupContext: expect.objectContaining({
previous_discovery_loop_selected_chain_id: "business_overview",
previous_discovery_loop_pending_axes: ["organization"],
previous_discovery_loop_metadata_scope_hint: "Group SVK",
previous_discovery_bidirectional_value_flow: valueFlowBundle,
previous_discovery_document_summary: documentBundle
})
})
);
});
it("does not infer a counterparty proof bundle from company-only overview totals", async () => {
const runMcpDiscoveryRuntimeEntryPoint = vi.fn(async () => ({
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
policy_owner: "assistantMcpDiscoveryRuntimeEntryPoint",
entry_status: "bridge_executed",
hot_runtime_wired: false,
discovery_attempted: true
}));
const input = buildInput({
userMessage: "who brought money",
sessionItems: [
{
role: "assistant",
text:
"Деньги: входящие 47 628 853,03 руб., исходящие 43 763 351,53 руб., расчетное операционное нетто 3 865 501,50 руб.\n" +
"НДС: продажи 2 002 138,93 руб., покупки 1 784 850,48 руб., НДС к уплате 217 288,45 руб."
}
],
runAddressLlmPreDecompose: vi.fn(async () => ({
attempted: true,
applied: false,
effectiveMessage: "who brought money",
reason: "raw_kept",
predecomposeContract: {
mode: "unsupported",
intent: "unknown",
entities: { organization: "Alt Plus" }
}
})),
resolveAddressFollowupCarryoverContext: vi.fn(() => ({
followupContext: {
previous_intent: "business_overview",
target_intent: "business_overview",
previous_discovery_pilot_scope: "business_overview_route_template_v1",
previous_discovery_loop_selected_chain_id: "business_overview",
previous_filters: { organization: "Alt Plus" }
}
})),
resolveAssistantOrchestrationDecision: vi.fn(() => ({
runAddressLane: true,
livingMode: "address_data",
livingReason: "address_lane_triggered",
toolGateDecision: "run_address_lane",
toolGateReason: "followup_context_detected",
orchestrationContract: {
schema_version: "assistant_orchestration_contract_v1",
assistant_turn_meaning: {
schema_version: "assistant_turn_meaning_v1",
asked_domain_family: "business_overview",
asked_action_family: "broad_evaluation"
}
}
})),
runMcpDiscoveryRuntimeEntryPoint
});
await buildAssistantAddressOrchestrationRuntime(input);
const call = runMcpDiscoveryRuntimeEntryPoint.mock.calls[0]?.[0] as any;
expect(call.followupContext.previous_discovery_bidirectional_value_flow).toBeUndefined();
expect(call.followupContext.previous_discovery_document_summary).toBeUndefined();
});
it("passes grounded discovery follow-up carryover into MCP discovery entry point for a short year switch", async () => {
const runMcpDiscoveryRuntimeEntryPoint = vi.fn(async () => ({
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
policy_owner: "assistantMcpDiscoveryRuntimeEntryPoint",
entry_status: "bridge_executed",
hot_runtime_wired: false,
discovery_attempted: true
}));
const input = buildInput({
userMessage: "Р° Сеперь Р·Р° 2021?",
runAddressLlmPreDecompose: vi.fn(async () => ({
attempted: true,
applied: false,
effectiveMessage: "Р° Сеперь Р·Р° 2021?",
reason: "raw_kept",
predecomposeContract: {
mode: "unsupported",
intent: "unknown",
period: {
scope: "year",
period_from: "2021-01-01",
period_to: "2021-12-31",
has_explicit_period: true
}
}
})),
resolveAddressFollowupCarryoverContext: vi.fn(() => ({
followupContext: {
previous_intent: "supplier_payouts_profile",
target_intent: "supplier_payouts_profile",
previous_discovery_pilot_scope: "counterparty_supplier_payout_query_movements_v1",
previous_anchor_type: "counterparty",
previous_anchor_value: "РСЂСѓРїРїР° РЎРРљ",
previous_filters: {
counterparty: "РСЂСѓРїРїР° РЎРРљ",
organization: "РћРћРћ АльСернаСРёРІР° Плюс",
period_from: "2020-01-01",
period_to: "2020-12-31"
}
}
})),
resolveAssistantOrchestrationDecision: vi.fn(() => ({
runAddressLane: true,
livingMode: "address_data",
livingReason: "address_lane_triggered",
toolGateDecision: "run_address_lane",
toolGateReason: "followup_context_detected",
orchestrationContract: {
schema_version: "assistant_orchestration_contract_v1",
assistant_turn_meaning: {
schema_version: "assistant_turn_meaning_v1",
raw_message: "Р° Сеперь Р·Р° 2021?",
effective_message: "Р° Сеперь Р·Р° 2021?",
explicit_entity_candidates: []
}
}
})),
runMcpDiscoveryRuntimeEntryPoint
});
const output = await buildAssistantAddressOrchestrationRuntime(input);
expect(output.orchestrationDecision.runAddressLane).toBe(true);
expect(runMcpDiscoveryRuntimeEntryPoint).toHaveBeenCalledWith(
expect.objectContaining({
userMessage: "Р° Сеперь Р·Р° 2021?",
effectiveMessage: "Р° Сеперь Р·Р° 2021?",
followupContext: expect.objectContaining({
previous_intent: "supplier_payouts_profile",
target_intent: "supplier_payouts_profile",
previous_discovery_pilot_scope: "counterparty_supplier_payout_query_movements_v1",
previous_anchor_type: "counterparty",
previous_anchor_value: "РСЂСѓРїРїР° РЎРРљ",
previous_filters: expect.objectContaining({
counterparty: "РСЂСѓРїРїР° РЎРРљ",
organization: "РћРћРћ АльСернаСРёРІР° Плюс",
period_from: "2020-01-01",
period_to: "2020-12-31"
})
})
})
);
expect(output.addressRuntimeMeta.mcpDiscoveryRuntimeEntryPoint).toEqual(
expect.objectContaining({
entry_status: "bridge_executed",
discovery_attempted: true,
hot_runtime_wired: false
})
);
});
it("keeps address orchestration alive when MCP discovery entry point fails", async () => {
const input = buildInput({
runMcpDiscoveryRuntimeEntryPoint: vi.fn(async () => {
throw new Error("discovery transport failed");
})
});
const output = await buildAssistantAddressOrchestrationRuntime(input);
expect(output.orchestrationDecision.runAddressLane).toBe(true);
expect(output.addressRuntimeMeta.mcpDiscoveryRuntimeEntryPoint).toBeNull();
expect(output.addressRuntimeMeta.mcpDiscoveryRuntimeEntryPointError).toBe("discovery transport failed");
});
it("builds deterministic fallback predecompose payload when feature is disabled", async () => {
const input = buildInput({
featureAddressLlmPredecomposeV1: false,
llmProvider: "local",
runAddressLlmPreDecompose: vi.fn(async () => {
throw new Error("must not be called");
}),
resolveAssistantOrchestrationDecision: vi.fn(() => ({
runAddressLane: false,
livingMode: "chat",
livingReason: "predecompose_unsupported_mode",
toolGateDecision: "skip_address_lane",
toolGateReason: "llm_predecompose_unsupported_mode",
orchestrationContract: { schema_version: "assistant_orchestration_contract_v1" }
}))
});
const output = await buildAssistantAddressOrchestrationRuntime(input);
expect(output.addressPreDecompose.attempted).toBe(false);
expect(output.addressPreDecompose.applied).toBe(false);
expect(output.addressPreDecompose.provider).toBe("local");
expect(output.addressPreDecompose.reason).toBe("disabled_by_feature_flag");
expect(output.addressPreDecompose.sanitizedUserMessage).toBe("sanitized");
expect(output.addressInputMessage).toBe("сырой вопрос");
expect(output.livingModeDecision.mode).toBe("chat");
expect(output.addressRuntimeMeta.toolGateDecision).toBe("skip_address_lane");
});
it("prefers raw short follow-up over unsupported llm rewrite when carryover context exists", async () => {
const resolveAddressFollowupCarryoverContext = vi.fn(() => ({
followupContext: {
previous_intent: "inventory_on_hand_as_of_date",
previous_filters: {
organization: "ООО \\Альтернатива Плюс\\",
as_of_date: "2026-04-15"
}
}
}));
const resolveAssistantOrchestrationDecision = vi.fn(() => ({
runAddressLane: true,
livingMode: "address_data",
livingReason: "address_lane_triggered",
toolGateDecision: "run_address_lane",
toolGateReason: "followup_context_detected",
orchestrationContract: { schema_version: "assistant_orchestration_contract_v1" }
}));
const buildAddressLlmPredecomposeContractV1 = vi.fn(({ sourceMessage, canonicalMessage }: { sourceMessage: string; canonicalMessage: string }) => ({
schema_version: "address_llm_predecompose_contract_v1",
source_message: sourceMessage,
canonical_message: canonicalMessage,
mode: canonicalMessage === sourceMessage ? "address_query" : "unsupported",
intent: canonicalMessage === sourceMessage ? "inventory_on_hand_as_of_date" : "unknown"
}));
const output = await buildAssistantAddressOrchestrationRuntime(
buildInput({
userMessage: "ахуен а на март 2020",
runAddressLlmPreDecompose: vi.fn(async () => ({
attempted: true,
applied: true,
effectiveMessage: "что не так в бухгалтерии за март 2020 года?",
reason: "normalized_fragment_applied",
predecomposeContract: {
mode: "unsupported",
intent: "unknown"
}
})),
buildAddressLlmPredecomposeContractV1,
resolveAddressFollowupCarryoverContext,
resolveAssistantOrchestrationDecision
})
);
expect(output.addressInputMessage).toBe("ахуен а на март 2020");
expect(output.addressPreDecompose.applied).toBe(false);
expect(output.addressPreDecompose.reason).toBe("followup_raw_message_preferred_over_llm_rewrite");
expect(output.addressPreDecompose.predecomposeContract).toEqual(
expect.objectContaining({
canonical_message: "ахуен а на март 2020",
mode: "address_query",
intent: "inventory_on_hand_as_of_date"
})
);
expect(buildAddressLlmPredecomposeContractV1).toHaveBeenCalledWith({
sourceMessage: "ахуен а на март 2020",
canonicalMessage: "ахуен а на март 2020"
});
expect(resolveAddressFollowupCarryoverContext).toHaveBeenCalledTimes(2);
expect(resolveAssistantOrchestrationDecision).toHaveBeenCalledWith(
expect.objectContaining({
rawUserMessage: "ахуен а на март 2020",
effectiveAddressUserMessage: "ахуен а на март 2020"
})
);
});
it("prefers raw inventory temporal root follow-up over account-balance canonical drift", async () => {
const resolveAddressFollowupCarryoverContext = vi.fn(() => ({
followupContext: {
previous_intent: "inventory_purchase_provenance_for_item",
previous_filters: {
item: "Четки Пост (84*117)",
organization: "ООО \\Альтернатива Плюс\\"
},
previous_anchor_type: "item",
previous_anchor_value: "Четки Пост (84*117)",
root_intent: "inventory_on_hand_as_of_date",
root_filters: {
as_of_date: "2020-03-31",
period_from: "2020-03-01",
period_to: "2020-03-31",
organization: "ООО \\Альтернатива Плюс\\"
},
current_frame_kind: "inventory_drilldown"
}
}));
const resolveAssistantOrchestrationDecision = vi.fn(() => ({
runAddressLane: true,
livingMode: "address_data",
livingReason: "address_lane_triggered",
toolGateDecision: "run_address_lane",
toolGateReason: "followup_context_detected",
orchestrationContract: { schema_version: "assistant_orchestration_contract_v1" }
}));
const buildAddressLlmPredecomposeContractV1 = vi.fn(
({ sourceMessage, canonicalMessage }: { sourceMessage: string; canonicalMessage: string }) => ({
schema_version: "address_llm_predecompose_contract_v1",
source_message: sourceMessage,
canonical_message: canonicalMessage,
mode: "address_query",
intent: canonicalMessage === sourceMessage ? "inventory_on_hand_as_of_date" : "account_balance_snapshot"
})
);
const rawMessage = "остатки на июль 2019";
const output = await buildAssistantAddressOrchestrationRuntime(
buildInput({
userMessage: rawMessage,
runAddressLlmPreDecompose: vi.fn(async () => ({
attempted: true,
applied: true,
effectiveMessage: "проверить остатки по счетам на июль 2019 года",
reason: "normalized_fragment_applied",
predecomposeContract: {
mode: "address_query",
intent: "account_balance_snapshot"
}
})),
buildAddressLlmPredecomposeContractV1,
resolveAddressFollowupCarryoverContext,
resolveAssistantOrchestrationDecision
})
);
expect(output.addressInputMessage).toBe(rawMessage);
expect(output.addressPreDecompose.applied).toBe(false);
expect(output.addressPreDecompose.reason).toBe("followup_raw_message_preferred_over_llm_rewrite");
expect(buildAddressLlmPredecomposeContractV1).toHaveBeenCalledWith({
sourceMessage: rawMessage,
canonicalMessage: rawMessage
});
expect(resolveAddressFollowupCarryoverContext).toHaveBeenCalledTimes(2);
expect(resolveAssistantOrchestrationDecision).toHaveBeenCalledWith(
expect.objectContaining({
rawUserMessage: rawMessage,
effectiveAddressUserMessage: rawMessage
})
);
});
it("prefers raw margin-ranking account correction over account-balance canonical drift", async () => {
const resolveAddressFollowupCarryoverContext = vi.fn(() => ({
followupContext: {
previous_intent: "inventory_margin_ranking_for_nomenclature",
target_intent: "inventory_margin_ranking_for_nomenclature",
root_intent: "inventory_margin_ranking_for_nomenclature",
previous_filters: {
period_from: "2017-01-01",
period_to: "2017-12-31",
organization: "OOO Alternative Plus"
},
previous_anchor_type: "organization",
previous_anchor_value: "OOO Alternative Plus"
}
}));
const resolveAssistantOrchestrationDecision = vi.fn(() => ({
runAddressLane: true,
livingMode: "address_data",
livingReason: "address_lane_triggered",
toolGateDecision: "run_address_lane",
toolGateReason: "followup_context_detected",
orchestrationContract: { schema_version: "assistant_orchestration_contract_v1" }
}));
const buildAddressLlmPredecomposeContractV1 = vi.fn(({ sourceMessage, canonicalMessage }: { sourceMessage: string; canonicalMessage: string }) => ({
schema_version: "address_llm_predecompose_contract_v1",
source_message: sourceMessage,
canonical_message: canonicalMessage,
mode: canonicalMessage === sourceMessage ? "unsupported" : "address_query",
intent: canonicalMessage === sourceMessage ? "unknown" : "account_balance_snapshot"
}));
const rawMessage = "\u0430\u043d\u0430\u043b\u0438\u0437 \u043f\u043e 41 \u0441\u0447\u0435\u0442\u0443 \u0430 \u043d\u0435 01";
const output = await buildAssistantAddressOrchestrationRuntime(
buildInput({
userMessage: rawMessage,
runAddressLlmPreDecompose: vi.fn(async () => ({
attempted: true,
applied: true,
effectiveMessage:
"\u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u0430\u043d\u0430\u043b\u0438\u0437 \u043f\u043e \u0441\u0447\u0435\u0442\u0443 41 \u0432\u043c\u0435\u0441\u0442\u043e \u0441\u0447\u0435\u0442\u0430 01",
reason: "normalized_fragment_applied",
predecomposeContract: {
mode: "address_query",
intent: "account_balance_snapshot"
}
})),
buildAddressLlmPredecomposeContractV1,
resolveAddressFollowupCarryoverContext,
resolveAssistantOrchestrationDecision
})
);
expect(output.addressInputMessage).toBe(rawMessage);
expect(output.addressPreDecompose.applied).toBe(false);
expect(output.addressPreDecompose.reason).toBe("followup_raw_message_preferred_over_llm_rewrite");
expect(resolveAddressFollowupCarryoverContext).toHaveBeenCalledTimes(2);
expect(resolveAssistantOrchestrationDecision).toHaveBeenCalledWith(
expect.objectContaining({
rawUserMessage: rawMessage,
effectiveAddressUserMessage: rawMessage
})
);
});
it("prefers raw selected-object inventory action over generic canonical drift intent", async () => {
const resolveAddressFollowupCarryoverContext = vi.fn(() => ({
followupContext: {
previous_intent: "inventory_on_hand_as_of_date",
previous_filters: {
organization: "ООО \\Альтернатива Плюс\\",
as_of_date: "2016-06-30",
period_from: "2016-06-01",
period_to: "2016-06-30"
}
}
}));
const resolveAssistantOrchestrationDecision = vi.fn(() => ({
runAddressLane: true,
livingMode: "address_data",
livingReason: "address_lane_triggered",
toolGateDecision: "run_address_lane",
toolGateReason: "address_mode_classifier_detected",
orchestrationContract: { schema_version: "assistant_orchestration_contract_v1" }
}));
const buildAddressLlmPredecomposeContractV1 = vi.fn(({ sourceMessage, canonicalMessage }: { sourceMessage: string; canonicalMessage: string }) => ({
schema_version: "address_llm_predecompose_contract_v1",
source_message: sourceMessage,
canonical_message: canonicalMessage,
mode: "address_query",
intent: "unknown"
}));
const rawMessage =
'По выбранному объекту "Рабочая станция универсального специалиста (индивидуальное изготовление)": кому мы это продали в итоге';
const output = await buildAssistantAddressOrchestrationRuntime(
buildInput({
userMessage: rawMessage,
runAddressLlmPreDecompose: vi.fn(async () => ({
attempted: true,
applied: true,
effectiveMessage:
"Определить контрагента, которому была реализована позиция «Рабочая станция универсального специалиста (индивидуальное изготовление)» по выбранному объекту",
reason: "normalized_fragment_applied",
predecomposeContract: {
mode: "address_query",
intent: "open_items_by_counterparty_or_contract",
semantics: {
selected_object_scope_detected: true
}
}
})),
buildAddressLlmPredecomposeContractV1,
resolveAddressFollowupCarryoverContext,
resolveAssistantOrchestrationDecision
})
);
expect(output.addressInputMessage).toBe(rawMessage);
expect(output.addressPreDecompose.applied).toBe(false);
expect(output.addressPreDecompose.reason).toBe("followup_raw_message_preferred_over_llm_rewrite");
expect(buildAddressLlmPredecomposeContractV1).toHaveBeenCalledWith({
sourceMessage: rawMessage,
canonicalMessage: rawMessage
});
expect(resolveAddressFollowupCarryoverContext).toHaveBeenCalledTimes(2);
expect(resolveAssistantOrchestrationDecision).toHaveBeenCalledWith(
expect.objectContaining({
rawUserMessage: rawMessage,
effectiveAddressUserMessage: rawMessage
})
);
});
it("prefers raw selected-object sale-destination wording over generic canonical drift intent", async () => {
const resolveAddressFollowupCarryoverContext = vi.fn(() => ({
followupContext: {
previous_intent: "inventory_on_hand_as_of_date",
previous_filters: {
as_of_date: "2020-05-31",
period_from: "2020-05-01",
period_to: "2020-05-31"
}
}
}));
const resolveAssistantOrchestrationDecision = vi.fn(() => ({
runAddressLane: true,
livingMode: "address_data",
livingReason: "address_lane_triggered",
toolGateDecision: "run_address_lane",
toolGateReason: "address_mode_classifier_detected",
orchestrationContract: { schema_version: "assistant_orchestration_contract_v1" }
}));
const buildAddressLlmPredecomposeContractV1 = vi.fn(({ sourceMessage, canonicalMessage }: { sourceMessage: string; canonicalMessage: string }) => ({
schema_version: "address_llm_predecompose_contract_v1",
source_message: sourceMessage,
canonical_message: canonicalMessage,
mode: "address_query",
intent: "unknown"
}));
const rawMessage = 'По выбранному объекту "Пуф арий": куда мы продали эту позицию';
const output = await buildAssistantAddressOrchestrationRuntime(
buildInput({
userMessage: rawMessage,
runAddressLlmPreDecompose: vi.fn(async () => ({
attempted: true,
applied: true,
effectiveMessage: "Определить контрагента по реализации позиции «Пуф арий»",
reason: "normalized_fragment_applied",
predecomposeContract: {
mode: "address_query",
intent: "open_items_by_counterparty_or_contract",
semantics: {
selected_object_scope_detected: true
}
}
})),
buildAddressLlmPredecomposeContractV1,
resolveAddressFollowupCarryoverContext,
resolveAssistantOrchestrationDecision
})
);
expect(output.addressInputMessage).toBe(rawMessage);
expect(output.addressPreDecompose.applied).toBe(false);
expect(output.addressPreDecompose.reason).toBe("followup_raw_message_preferred_over_llm_rewrite");
expect(buildAddressLlmPredecomposeContractV1).toHaveBeenCalledWith({
sourceMessage: rawMessage,
canonicalMessage: rawMessage
});
expect(resolveAddressFollowupCarryoverContext).toHaveBeenCalledTimes(2);
expect(resolveAssistantOrchestrationDecision).toHaveBeenCalledWith(
expect.objectContaining({
rawUserMessage: rawMessage,
effectiveAddressUserMessage: rawMessage
})
);
});
it("prefers raw selected-object delivery wording over generic canonical drift intent", async () => {
const resolveAddressFollowupCarryoverContext = vi.fn(() => ({
followupContext: {
previous_intent: "inventory_on_hand_as_of_date",
previous_filters: {
as_of_date: "2021-03-31",
period_from: "2021-03-01",
period_to: "2021-03-31"
}
}
}));
const resolveAssistantOrchestrationDecision = vi.fn(() => ({
runAddressLane: true,
livingMode: "address_data",
livingReason: "address_lane_triggered",
toolGateDecision: "run_address_lane",
toolGateReason: "address_mode_classifier_detected",
orchestrationContract: { schema_version: "assistant_orchestration_contract_v1" }
}));
const buildAddressLlmPredecomposeContractV1 = vi.fn(({ sourceMessage, canonicalMessage }: { sourceMessage: string; canonicalMessage: string }) => ({
schema_version: "address_llm_predecompose_contract_v1",
source_message: sourceMessage,
canonical_message: canonicalMessage,
mode: "address_query",
intent: "unknown"
}));
const rawMessage = 'По выбранному объекту "Кромка с клеем 33 дуб ниагара 137 м": кому мы это поставили';
const output = await buildAssistantAddressOrchestrationRuntime(
buildInput({
userMessage: rawMessage,
runAddressLlmPreDecompose: vi.fn(async () => ({
attempted: true,
applied: true,
effectiveMessage: "Покажи взаиморасчеты с контрагентом по выбранной позиции",
reason: "normalized_fragment_applied",
predecomposeContract: {
mode: "address_query",
intent: "bank_operations_by_counterparty",
semantics: {
selected_object_scope_detected: true
}
}
})),
buildAddressLlmPredecomposeContractV1,
resolveAddressFollowupCarryoverContext,
resolveAssistantOrchestrationDecision
})
);
expect(output.addressInputMessage).toBe(rawMessage);
expect(output.addressPreDecompose.applied).toBe(false);
expect(output.addressPreDecompose.reason).toBe("followup_raw_message_preferred_over_llm_rewrite");
expect(resolveAddressFollowupCarryoverContext).toHaveBeenCalledTimes(2);
expect(resolveAssistantOrchestrationDecision).toHaveBeenCalledWith(
expect.objectContaining({
rawUserMessage: rawMessage,
effectiveAddressUserMessage: rawMessage
})
);
});
it("prefers raw selected-object profitability wording over customer revenue canonical drift", async () => {
const resolveAddressFollowupCarryoverContext = vi.fn(() => ({
followupContext: {
previous_intent: "inventory_on_hand_as_of_date",
previous_filters: {
as_of_date: "2020-05-31",
period_from: "2020-05-01",
period_to: "2020-05-31"
}
}
}));
const resolveAssistantOrchestrationDecision = vi.fn(() => ({
runAddressLane: true,
livingMode: "address_data",
livingReason: "address_lane_triggered",
toolGateDecision: "run_address_lane",
toolGateReason: "address_mode_classifier_detected",
orchestrationContract: { schema_version: "assistant_orchestration_contract_v1" }
}));
const buildAddressLlmPredecomposeContractV1 = vi.fn(({ sourceMessage, canonicalMessage }: { sourceMessage: string; canonicalMessage: string }) => ({
schema_version: "address_llm_predecompose_contract_v1",
source_message: sourceMessage,
canonical_message: canonicalMessage,
mode: "address_query",
intent: "unknown"
}));
const rawMessage =
'По выбранному объекту "Четки Пост (84*117)": а сколько денег мы заработали с продажжи этих четок';
const output = await buildAssistantAddressOrchestrationRuntime(
buildInput({
userMessage: rawMessage,
runAddressLlmPreDecompose: vi.fn(async () => ({
attempted: true,
applied: true,
effectiveMessage: "Покажи выручку по контрагенту по выбранной позиции",
reason: "normalized_fragment_applied",
predecomposeContract: {
mode: "address_query",
intent: "customer_revenue_and_payments",
semantics: {
selected_object_scope_detected: true
}
}
})),
buildAddressLlmPredecomposeContractV1,
resolveAddressFollowupCarryoverContext,
resolveAssistantOrchestrationDecision
})
);
expect(output.addressInputMessage).toBe(rawMessage);
expect(output.addressPreDecompose.applied).toBe(false);
expect(output.addressPreDecompose.reason).toBe("followup_raw_message_preferred_over_llm_rewrite");
expect(resolveAddressFollowupCarryoverContext).toHaveBeenCalledTimes(2);
expect(resolveAssistantOrchestrationDecision).toHaveBeenCalledWith(
expect.objectContaining({
rawUserMessage: rawMessage,
effectiveAddressUserMessage: rawMessage
})
);
});
it("prefers raw same-date follow-up over canonical rewrite to current date", async () => {
const resolveAddressFollowupCarryoverContext = vi.fn(() => ({
followupContext: {
previous_intent: "vat_payable_confirmed_as_of_date",
previous_filters: {
as_of_date: "2019-09-30",
period_from: "2019-09-01",
period_to: "2019-09-30"
}
}
}));
const resolveAssistantOrchestrationDecision = vi.fn(() => ({
runAddressLane: true,
livingMode: "address_data",
livingReason: "address_lane_triggered",
toolGateDecision: "run_address_lane",
toolGateReason: "followup_context_detected",
orchestrationContract: { schema_version: "assistant_orchestration_contract_v1" }
}));
const rawMessage = "какие остатки по складу на эту же дату";
const output = await buildAssistantAddressOrchestrationRuntime(
buildInput({
userMessage: rawMessage,
runAddressLlmPreDecompose: vi.fn(async () => ({
attempted: true,
applied: true,
effectiveMessage: "получить остатки по складу на текущую дату",
reason: "normalized_fragment_applied",
predecomposeContract: {
mode: "address_query",
intent: "inventory_on_hand_as_of_date"
}
})),
resolveAddressFollowupCarryoverContext,
resolveAssistantOrchestrationDecision
})
);
expect(output.addressInputMessage).toBe(rawMessage);
expect(output.addressPreDecompose.applied).toBe(false);
expect(output.addressPreDecompose.reason).toBe("followup_raw_message_preferred_over_llm_rewrite");
expect(resolveAddressFollowupCarryoverContext).toHaveBeenCalledTimes(2);
expect(resolveAssistantOrchestrationDecision).toHaveBeenCalledWith(
expect.objectContaining({
rawUserMessage: rawMessage,
effectiveAddressUserMessage: rawMessage
})
);
});
});