365 lines
14 KiB
TypeScript
365 lines
14 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("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"
|
||
})
|
||
})
|
||
})
|
||
);
|
||
});
|
||
});
|