575 lines
22 KiB
TypeScript
575 lines
22 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
||
import { runAssistantAddressLaneResponseRuntime } from "../src/services/assistantAddressLaneResponseRuntimeAdapter";
|
||
|
||
describe("assistant address lane response runtime adapter", () => {
|
||
it("builds debug payload and finalizes address turn", () => {
|
||
const finalizeAddressTurn = vi.fn(() => ({
|
||
response: {
|
||
ok: true
|
||
}
|
||
}));
|
||
|
||
const runtime = runAssistantAddressLaneResponseRuntime({
|
||
sessionId: "asst-1",
|
||
userMessage: "raw",
|
||
effectiveAddressUserMessage: "canon",
|
||
addressLane: {
|
||
handled: true,
|
||
reply_text: "answer",
|
||
reply_type: "factual",
|
||
debug: {
|
||
extracted_filters: {
|
||
organization: "ООО Ромашка"
|
||
}
|
||
}
|
||
},
|
||
carryoverMeta: {
|
||
followupContext: {
|
||
previous_intent: "list_documents"
|
||
}
|
||
},
|
||
llmPreDecomposeMeta: {
|
||
attempted: true
|
||
},
|
||
knownOrganizations: ["ООО Ромашка", "ООО Лютик"],
|
||
activeOrganization: "ООО Ромашка",
|
||
sanitizeOutgoingAssistantText: (text) => String(text ?? "").trim(),
|
||
buildAddressDebugPayload: (addressDebug) => ({ ...(addressDebug as Record<string, unknown>) }),
|
||
buildAddressFollowupOffer: () => ({ suggestion: "continue_previous" }),
|
||
mergeKnownOrganizations: (items) => Array.from(new Set(items)),
|
||
toNonEmptyString: (value) => (typeof value === "string" && value.trim() ? value.trim() : null),
|
||
appendItem: () => {},
|
||
getSession: () => ({ session_id: "asst-1", updated_at: "", items: [], investigation_state: null } as any),
|
||
persistSession: () => {},
|
||
cloneConversation: (items) => items,
|
||
logEvent: () => {},
|
||
messageIdFactory: () => "msg-1",
|
||
finalizeAddressTurn
|
||
});
|
||
|
||
expect(finalizeAddressTurn).toHaveBeenCalledWith(
|
||
expect.objectContaining({
|
||
assistantReply: "answer",
|
||
replyType: "factual",
|
||
llmPreDecomposeMeta: {
|
||
attempted: true
|
||
}
|
||
})
|
||
);
|
||
expect(runtime.response).toEqual({ ok: true });
|
||
expect(runtime.debug).toEqual(
|
||
expect.objectContaining({
|
||
assistant_known_organizations: ["ООО Ромашка", "ООО Лютик"],
|
||
assistant_active_organization: "ООО Ромашка",
|
||
address_followup_offer: { suggestion: "continue_previous" },
|
||
assistant_runtime_contract_v1: expect.objectContaining({
|
||
schema_version: "assistant_runtime_contracts_v1"
|
||
}),
|
||
assistant_truth_answer_policy_v1: expect.objectContaining({
|
||
schema_version: "assistant_truth_answer_policy_runtime_v1",
|
||
policy_owner: "assistantTruthAnswerPolicyRuntimeAdapter"
|
||
}),
|
||
coverage_gate_contract: expect.objectContaining({
|
||
schema_version: "assistant_truth_answer_policy_runtime_v1"
|
||
}),
|
||
answer_shape_contract: expect.objectContaining({
|
||
reply_type: "factual"
|
||
}),
|
||
assistant_state_transition_v1: expect.objectContaining({
|
||
schema_version: "assistant_state_transition_runtime_v1",
|
||
state_owner: "assistantStateTransitionRuntimeAdapter"
|
||
}),
|
||
state_transition_contract: expect.objectContaining({
|
||
schema_version: "assistant_state_transition_runtime_v1"
|
||
}),
|
||
assistant_capability_binding_v1: expect.objectContaining({
|
||
schema_version: "assistant_capability_runtime_binding_v1",
|
||
binding_owner: "assistantCapabilityRuntimeBindingAdapter"
|
||
}),
|
||
capability_binding_contract: expect.objectContaining({
|
||
schema_version: "assistant_capability_runtime_binding_v1"
|
||
}),
|
||
capability_binding_response_guard: expect.objectContaining({
|
||
schema_version: "assistant_capability_binding_response_guard_v1",
|
||
applied: false
|
||
}),
|
||
assistant_mcp_discovery_entry_point_v1: null,
|
||
mcp_discovery_attempted: false,
|
||
mcp_discovery_hot_runtime_wired: false
|
||
})
|
||
);
|
||
});
|
||
|
||
it("attaches MCP discovery summary from predecompose runtime meta without changing the reply", () => {
|
||
const runtime = runAssistantAddressLaneResponseRuntime({
|
||
sessionId: "asst-mcp",
|
||
userMessage: "raw",
|
||
effectiveAddressUserMessage: "raw",
|
||
addressLane: {
|
||
handled: true,
|
||
reply_text: "answer",
|
||
reply_type: "partial_coverage",
|
||
debug: {}
|
||
},
|
||
llmPreDecomposeMeta: {
|
||
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" }
|
||
},
|
||
reason_codes: ["runtime_entry_point_bridge_executed"]
|
||
}
|
||
},
|
||
knownOrganizations: [],
|
||
activeOrganization: null,
|
||
sanitizeOutgoingAssistantText: (text) => String(text ?? ""),
|
||
buildAddressDebugPayload: () => ({}),
|
||
buildAddressFollowupOffer: () => null,
|
||
mergeKnownOrganizations: (items) => items,
|
||
toNonEmptyString: () => null,
|
||
appendItem: () => {},
|
||
getSession: () => ({ session_id: "asst-mcp", updated_at: "", items: [], investigation_state: null } as any),
|
||
persistSession: () => {},
|
||
cloneConversation: (items) => items,
|
||
logEvent: () => {},
|
||
messageIdFactory: () => "msg-mcp",
|
||
finalizeAddressTurn: () => ({
|
||
response: {
|
||
ok: true
|
||
}
|
||
})
|
||
});
|
||
|
||
expect(runtime.response).toEqual({ ok: true });
|
||
expect(runtime.debug).toEqual(
|
||
expect.objectContaining({
|
||
mcp_discovery_entry_status: "bridge_executed",
|
||
mcp_discovery_attempted: true,
|
||
mcp_discovery_bridge_status: "answer_draft_ready",
|
||
mcp_discovery_answer_mode: "confirmed_with_bounded_inference",
|
||
mcp_discovery_business_fact_answer_allowed: true,
|
||
mcp_discovery_hot_runtime_wired: false
|
||
})
|
||
);
|
||
});
|
||
|
||
it("can replace a stale address exact answer with a guarded MCP discovery candidate", () => {
|
||
const finalizeAddressTurn = vi.fn((input) => ({
|
||
response: {
|
||
ok: true,
|
||
assistant_reply: input.assistantReply,
|
||
reply_type: input.replyType,
|
||
debug: input.debug
|
||
}
|
||
}));
|
||
|
||
const runtime = runAssistantAddressLaneResponseRuntime({
|
||
sessionId: "asst-mcp-address",
|
||
userMessage: "сколько мы заплатили СВК за 2020?",
|
||
effectiveAddressUserMessage: "сколько мы заплатили СВК за 2020?",
|
||
addressLane: {
|
||
handled: true,
|
||
reply_text: "stale documents answer",
|
||
reply_type: "factual",
|
||
debug: {}
|
||
},
|
||
llmPreDecomposeMeta: {
|
||
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: "Discovery payout answer",
|
||
confirmed_lines: ["1C supplier-payout rows were found for counterparty SVK"],
|
||
inference_lines: ["Counterparty supplier-payout total was calculated from confirmed 1C outgoing payment rows"],
|
||
unknown_lines: ["Full supplier-payout amount outside the checked period is not proven by this MCP discovery pilot"],
|
||
limitation_lines: [],
|
||
next_step_line: null
|
||
}
|
||
},
|
||
reason_codes: ["runtime_entry_point_bridge_executed"]
|
||
}
|
||
},
|
||
knownOrganizations: [],
|
||
activeOrganization: null,
|
||
sanitizeOutgoingAssistantText: (text) => String(text ?? ""),
|
||
buildAddressDebugPayload: () => ({}),
|
||
buildAddressFollowupOffer: () => null,
|
||
mergeKnownOrganizations: (items) => items,
|
||
toNonEmptyString: () => null,
|
||
appendItem: () => {},
|
||
getSession: () => ({ session_id: "asst-mcp-address", updated_at: "", items: [], investigation_state: null } as any),
|
||
persistSession: () => {},
|
||
cloneConversation: (items) => items,
|
||
logEvent: () => {},
|
||
messageIdFactory: () => "msg-mcp-address",
|
||
finalizeAddressTurn
|
||
});
|
||
|
||
expect(finalizeAddressTurn).toHaveBeenCalledWith(
|
||
expect.objectContaining({
|
||
assistantReply: expect.stringContaining("Discovery payout answer"),
|
||
replyType: "partial_coverage",
|
||
debug: expect.objectContaining({
|
||
mcp_discovery_response_applied: true
|
||
})
|
||
})
|
||
);
|
||
expect(String((runtime.response as any).assistant_reply)).toContain("исходящих платежей/списаний");
|
||
});
|
||
|
||
it("aligns final route metadata when a business-overview discovery candidate replaces a stale exact reply", () => {
|
||
const finalizeAddressTurn = vi.fn((input) => ({
|
||
response: {
|
||
ok: true,
|
||
assistant_reply: input.assistantReply,
|
||
reply_type: input.replyType,
|
||
debug: input.debug
|
||
}
|
||
}));
|
||
|
||
const runtime = runAssistantAddressLaneResponseRuntime({
|
||
sessionId: "asst-business-overview-address",
|
||
userMessage: "Собери короткий бизнес-итог по компании и отдельно по СВК.",
|
||
effectiveAddressUserMessage: "Собери короткий бизнес-итог по компании и отдельно по СВК.",
|
||
addressLane: {
|
||
handled: true,
|
||
reply_text: "Контрагент: Группа СВК. Найдено документов: 19.",
|
||
reply_type: "factual",
|
||
debug: {
|
||
detected_intent: "list_documents_by_counterparty",
|
||
selected_recipe: "address_documents_by_counterparty_v1",
|
||
response_type: "FACTUAL_LIST"
|
||
}
|
||
},
|
||
llmPreDecomposeMeta: {
|
||
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,
|
||
turn_meaning_ref: {
|
||
asked_domain_family: "business_overview",
|
||
asked_action_family: "broad_evaluation",
|
||
explicit_organization_scope: "ООО Альтернатива Плюс",
|
||
explicit_date_scope: "2020",
|
||
raw_message: "Собери короткий бизнес-итог по компании и отдельно по СВК."
|
||
},
|
||
data_need_graph: {
|
||
business_fact_family: "business_overview",
|
||
reason_codes: ["data_need_graph_family_business_overview"]
|
||
}
|
||
},
|
||
bridge: {
|
||
bridge_status: "answer_draft_ready",
|
||
user_facing_response_allowed: true,
|
||
business_fact_answer_allowed: true,
|
||
requires_user_clarification: false,
|
||
planner: {
|
||
selected_chain_id: "business_overview"
|
||
},
|
||
pilot: {
|
||
pilot_scope: "business_overview_route_template_v1",
|
||
derived_business_overview: {
|
||
period_scope: "2020",
|
||
incoming_customer_revenue: {
|
||
total_amount_human_ru: "47 628 853,03 руб."
|
||
},
|
||
outgoing_supplier_payout: {
|
||
total_amount_human_ru: "43 763 351,53 руб."
|
||
},
|
||
net_amount_human_ru: "3 865 501,50 руб.",
|
||
net_direction: "net_incoming"
|
||
}
|
||
},
|
||
answer_draft: {
|
||
answer_mode: "confirmed_with_bounded_inference",
|
||
headline: "Business overview answer",
|
||
confirmed_lines: ["Confirmed company business overview facts"],
|
||
inference_lines: [],
|
||
unknown_lines: ["Pure profit is not proven"],
|
||
limitation_lines: [],
|
||
next_step_line: null
|
||
}
|
||
},
|
||
reason_codes: ["runtime_entry_point_bridge_executed"]
|
||
}
|
||
},
|
||
knownOrganizations: [],
|
||
activeOrganization: null,
|
||
sanitizeOutgoingAssistantText: (text) => String(text ?? ""),
|
||
buildAddressDebugPayload: (debug) => ({ ...(debug as Record<string, unknown>) }),
|
||
buildAddressFollowupOffer: () => null,
|
||
mergeKnownOrganizations: (items) => items,
|
||
toNonEmptyString: (value) => (typeof value === "string" && value.trim() ? value.trim() : null),
|
||
appendItem: () => {},
|
||
getSession: () => ({ session_id: "asst-business-overview-address", updated_at: "", items: [], investigation_state: null } as any),
|
||
persistSession: () => {},
|
||
cloneConversation: (items) => items,
|
||
logEvent: () => {},
|
||
messageIdFactory: () => "msg-business-overview-address",
|
||
finalizeAddressTurn
|
||
});
|
||
|
||
expect((runtime.response as any).debug).toEqual(
|
||
expect.objectContaining({
|
||
mcp_discovery_response_applied: true,
|
||
detected_intent: "business_overview",
|
||
selected_recipe: "business_overview",
|
||
mcp_discovery_effective_response_route: "business_overview"
|
||
})
|
||
);
|
||
expect(String((runtime.response as any).assistant_reply)).toContain("47 628 853,03");
|
||
});
|
||
|
||
it("keeps comparison-scope proof augmentation business-facing for organization clarification", () => {
|
||
const finalizeAddressTurn = vi.fn((input) => ({
|
||
response: {
|
||
ok: true,
|
||
assistant_reply: input.assistantReply,
|
||
reply_type: input.replyType,
|
||
debug: input.debug
|
||
}
|
||
}));
|
||
|
||
const runtime = runAssistantAddressLaneResponseRuntime({
|
||
sessionId: "asst-comparison-proof",
|
||
userMessage: "ООО Альтернатива Плюс",
|
||
effectiveAddressUserMessage: "ООО Альтернатива Плюс",
|
||
addressLane: {
|
||
handled: true,
|
||
reply_text:
|
||
"Коротко: по компании Альтернатива Плюс подтвержден company-level денежный срез: входящие 285 819 547,57 руб.",
|
||
reply_type: "partial_coverage",
|
||
debug: {}
|
||
},
|
||
llmPreDecomposeMeta: {
|
||
mcpDiscoveryRuntimeEntryPoint: {
|
||
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
|
||
policy_owner: "assistantMcpDiscoveryRuntimeEntryPoint",
|
||
entry_status: "bridge_executed",
|
||
hot_runtime_wired: true,
|
||
discovery_attempted: true,
|
||
turn_input: {
|
||
turn_meaning_ref: {
|
||
asked_domain_family: "business_overview",
|
||
business_overview_separate_entity_candidates: ["Группа СВК"],
|
||
explicit_organization_scope: "Альтернатива Плюс"
|
||
}
|
||
},
|
||
bridge: {
|
||
bridge_status: "not_applicable",
|
||
user_facing_response_allowed: false,
|
||
business_fact_answer_allowed: false,
|
||
requires_user_clarification: false
|
||
},
|
||
reason_codes: []
|
||
}
|
||
},
|
||
knownOrganizations: [],
|
||
activeOrganization: null,
|
||
sanitizeOutgoingAssistantText: (text) => String(text ?? ""),
|
||
buildAddressDebugPayload: () => ({}),
|
||
buildAddressFollowupOffer: () => null,
|
||
mergeKnownOrganizations: (items) => items,
|
||
toNonEmptyString: (value) => (typeof value === "string" && value.trim() ? value.trim() : null),
|
||
appendItem: () => {},
|
||
getSession: () =>
|
||
({
|
||
session_id: "asst-comparison-proof",
|
||
updated_at: "",
|
||
items: [],
|
||
investigation_state: null,
|
||
address_navigation_state: {
|
||
session_context: {
|
||
comparison_scope: {
|
||
organization: { label: "Альтернатива Плюс" },
|
||
counterparty: { label: "Группа СВК" },
|
||
proof_bundles: {
|
||
counterparty_value_flow_bundle: {
|
||
counterparty: "Группа СВК",
|
||
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 руб.",
|
||
net_direction: "net_incoming"
|
||
},
|
||
counterparty_document_bundle: {
|
||
document_count: 19
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}) as any,
|
||
persistSession: () => {},
|
||
cloneConversation: (items) => items,
|
||
logEvent: () => {},
|
||
messageIdFactory: () => "msg-comparison-proof",
|
||
finalizeAddressTurn
|
||
});
|
||
|
||
const reply = String((runtime.response as any).assistant_reply);
|
||
expect(reply).toContain("по компании ООО Альтернатива Плюс");
|
||
expect(reply).toContain("получили 20 653 490 руб.");
|
||
expect(reply).toContain("документы: 19");
|
||
expect(reply).not.toContain("company-level");
|
||
expect(reply).not.toContain("reusable bundle");
|
||
expect((runtime.response as any).debug.comparison_scope_response_augmentation_v1).toEqual(
|
||
expect.objectContaining({
|
||
applied: true,
|
||
organization: "ООО Альтернатива Плюс",
|
||
counterparty: "Группа СВК",
|
||
document_count: 19
|
||
})
|
||
);
|
||
});
|
||
|
||
it("keeps debug bounded to shadow contracts when optional enrichment is absent", () => {
|
||
const runtime = runAssistantAddressLaneResponseRuntime({
|
||
sessionId: "asst-2",
|
||
userMessage: "raw",
|
||
effectiveAddressUserMessage: "raw",
|
||
addressLane: {
|
||
handled: true,
|
||
reply_text: "answer",
|
||
reply_type: "partial_coverage",
|
||
debug: {}
|
||
},
|
||
knownOrganizations: [],
|
||
activeOrganization: null,
|
||
sanitizeOutgoingAssistantText: (text) => String(text ?? ""),
|
||
buildAddressDebugPayload: () => ({}),
|
||
buildAddressFollowupOffer: () => null,
|
||
mergeKnownOrganizations: (items) => items,
|
||
toNonEmptyString: () => null,
|
||
appendItem: () => {},
|
||
getSession: () => ({ session_id: "asst-2", updated_at: "", items: [], investigation_state: null } as any),
|
||
persistSession: () => {},
|
||
cloneConversation: (items) => items,
|
||
logEvent: () => {},
|
||
messageIdFactory: () => "msg-2",
|
||
finalizeAddressTurn: () => ({
|
||
response: {
|
||
ok: true
|
||
}
|
||
})
|
||
});
|
||
|
||
expect(runtime.debug).toEqual(
|
||
expect.objectContaining({
|
||
assistant_runtime_contract_v1: expect.objectContaining({
|
||
transition_contract_id: null,
|
||
capability_contract_id: null,
|
||
truth_gate_contract_status: "unknown"
|
||
}),
|
||
assistant_truth_answer_policy_v1: expect.objectContaining({
|
||
schema_version: "assistant_truth_answer_policy_runtime_v1"
|
||
}),
|
||
coverage_gate_contract: expect.objectContaining({
|
||
coverage_status: "blocked",
|
||
truth_mode: "unsupported"
|
||
}),
|
||
answer_shape_contract: expect.objectContaining({
|
||
answer_shape: "blocked_no_answer",
|
||
reply_type: "partial_coverage"
|
||
}),
|
||
assistant_state_transition_v1: expect.objectContaining({
|
||
application_status: "unresolved",
|
||
effective_carryover_depth: "none"
|
||
}),
|
||
transition_contract_id: null,
|
||
capability_contract_id: null,
|
||
truth_gate_contract_status: "unknown",
|
||
carryover_eligibility: "none",
|
||
state_transition_id: null,
|
||
state_transition_status: "unresolved",
|
||
effective_carryover_depth: "none",
|
||
capability_binding_status: "not_applicable",
|
||
capability_binding_action: "observe_only",
|
||
capability_binding_violations: [],
|
||
capability_binding_response_guard: expect.objectContaining({
|
||
applied: false,
|
||
action: "observe_only"
|
||
})
|
||
})
|
||
);
|
||
expect(runtime.response).toEqual({ ok: true });
|
||
});
|
||
|
||
it("guards blocked capability binding responses before finalizing the turn", () => {
|
||
const finalizeAddressTurn = vi.fn(() => ({
|
||
response: {
|
||
ok: true
|
||
}
|
||
}));
|
||
|
||
runAssistantAddressLaneResponseRuntime({
|
||
sessionId: "asst-3",
|
||
userMessage: "кто поставщик?",
|
||
effectiveAddressUserMessage: "кто поставщик?",
|
||
addressLane: {
|
||
handled: true,
|
||
reply_text: "unsafe factual answer",
|
||
reply_type: "factual",
|
||
debug: {
|
||
capability_id: "inventory_inventory_purchase_provenance_for_item",
|
||
limited_reason_category: "missing_anchor",
|
||
missing_required_filters: ["item"]
|
||
}
|
||
},
|
||
knownOrganizations: [],
|
||
activeOrganization: null,
|
||
sanitizeOutgoingAssistantText: (text) => String(text ?? ""),
|
||
buildAddressDebugPayload: (addressDebug) => ({ ...(addressDebug as Record<string, unknown>) }),
|
||
buildAddressFollowupOffer: () => null,
|
||
mergeKnownOrganizations: (items) => items,
|
||
toNonEmptyString: (value) => (typeof value === "string" && value.trim() ? value.trim() : null),
|
||
appendItem: () => {},
|
||
getSession: () => ({ session_id: "asst-3", updated_at: "", items: [], investigation_state: null } as any),
|
||
persistSession: () => {},
|
||
cloneConversation: (items) => items,
|
||
logEvent: () => {},
|
||
messageIdFactory: () => "msg-3",
|
||
finalizeAddressTurn
|
||
});
|
||
|
||
expect(finalizeAddressTurn).toHaveBeenCalledWith(
|
||
expect.objectContaining({
|
||
assistantReply: expect.stringContaining("Нужно уточнение"),
|
||
replyType: "partial_coverage",
|
||
debug: expect.objectContaining({
|
||
capability_binding_status: "blocked",
|
||
capability_binding_action: "clarify",
|
||
capability_binding_response_guard: expect.objectContaining({
|
||
applied: true,
|
||
action: "clarify",
|
||
guarded_reply_type: "partial_coverage"
|
||
})
|
||
})
|
||
})
|
||
);
|
||
});
|
||
});
|