749 lines
31 KiB
TypeScript
749 lines
31 KiB
TypeScript
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 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 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
|
||
})
|
||
);
|
||
});
|
||
});
|