import { describe, expect, it } from "vitest"; import { buildAssistantMcpDiscoveryResponseCandidate } from "../src/services/assistantMcpDiscoveryResponseCandidate"; function entryPoint(overrides: Record = {}) { return { schema_version: "assistant_mcp_discovery_runtime_entry_point_v1", policy_owner: "assistantMcpDiscoveryRuntimeEntryPoint", entry_status: "bridge_executed", hot_runtime_wired: false, discovery_attempted: true, turn_input: { adapter_status: "ready" }, bridge: { bridge_status: "answer_draft_ready", user_facing_response_allowed: true, business_fact_answer_allowed: true, requires_user_clarification: false, answer_draft: { answer_mode: "confirmed_with_bounded_inference", headline: "По данным 1С есть подтвержденная активность; длительность можно оценивать только как вывод.", confirmed_lines: ["1C activity rows were found for counterparty SVK"], inference_lines: ["Business activity duration may be inferred from first and latest confirmed 1C activity rows"], unknown_lines: ["Legal registration date is not proven by this MCP discovery pilot"], limitation_lines: ["query_documents was skipped internally", "MCP fetch window was limited"], next_step_line: null } }, reason_codes: ["runtime_entry_point_bridge_executed"], ...overrides } as any; } describe("assistant MCP discovery response candidate", () => { it("builds a Russian guarded candidate from a confirmed discovery draft", () => { const candidate = buildAssistantMcpDiscoveryResponseCandidate(entryPoint()); expect(candidate.candidate_status).toBe("ready_for_guarded_use"); expect(candidate.hot_runtime_wired).toBe(false); expect(candidate.reply_type).toBe("partial_coverage"); expect(candidate.eligible_for_future_hot_runtime).toBe(true); expect(candidate.reply_text).toContain("Коротко:"); expect(candidate.reply_text).toContain("В 1С найдены строки активности по контрагенту SVK."); expect(candidate.reply_text).toContain("Юридическая дата регистрации этим поиском не подтверждена."); expect(candidate.reply_text).not.toContain("query_documents"); expect(candidate.reply_text).not.toContain("primitive"); }); it("localizes value-flow evidence without leaking pilot mechanics", () => { const candidate = buildAssistantMcpDiscoveryResponseCandidate( entryPoint({ 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: "По данным 1С найдены строки входящих денежных поступлений; сумму можно называть только в рамках проверенного периода и найденных строк.", confirmed_lines: [ "1C value-flow rows were found for counterparty SVK", "По найденным строкам входящих денежных поступлений в 1С по контрагенту SVK за период 2020 сумма входящих денежных поступлений составляет 3 750 руб." ], inference_lines: ["Counterparty value-flow total was calculated from confirmed 1C movement rows"], unknown_lines: ["Full turnover outside the checked period is not proven by this MCP discovery pilot"], limitation_lines: ["pilot_value_flow_uses_query_movements_and_derives_aggregate"], next_step_line: null } } }) ); expect(candidate.candidate_status).toBe("ready_for_guarded_use"); expect(candidate.reply_text).toContain("В 1С найдены строки входящих денежных поступлений по контрагенту SVK."); expect(candidate.reply_text).toContain("3 750 руб."); expect(candidate.reply_text).toContain("Полный объем входящих поступлений вне проверенного периода этим поиском не подтвержден."); expect(candidate.reply_text).not.toContain("pilot_"); expect(candidate.reply_text).not.toContain("query_movements"); }); it("localizes supplier-payout evidence without leaking pilot mechanics", () => { const candidate = buildAssistantMcpDiscoveryResponseCandidate( entryPoint({ 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: "По данным 1С найдены строки исходящих платежей/списаний; сумму можно называть только в рамках проверенного периода и найденных строк.", confirmed_lines: [ "1C supplier-payout rows were found for counterparty SVK", "По найденным строкам исходящих платежей/списаний в 1С по контрагенту SVK за период 2020 сумма исходящих платежей/списаний составляет 5 000 руб." ], inference_lines: ["Counterparty supplier-payout total was calculated from confirmed 1C outgoing payment rows"], unknown_lines: [ "Complete requested-period coverage is not proven because the MCP discovery probe row limit was reached", "Full supplier-payout amount outside the checked period is not proven by this MCP discovery pilot" ], limitation_lines: ["pilot_value_flow_uses_query_movements_and_derives_aggregate"], next_step_line: null } } }) ); expect(candidate.candidate_status).toBe("ready_for_guarded_use"); expect(candidate.reply_text).toContain("В 1С найдены строки исходящих платежей/списаний по контрагенту SVK."); expect(candidate.reply_text).toContain("5 000 руб."); expect(candidate.reply_text).toContain("Полное покрытие запрошенного периода не подтверждено"); expect(candidate.reply_text).toContain("Полный объем исходящих платежей вне проверенного периода этим поиском не подтвержден."); expect(candidate.reply_text).not.toContain("pilot_"); expect(candidate.reply_text).not.toContain("query_movements"); }); it("localizes bidirectional net value-flow evidence without leaking pilot mechanics", () => { const candidate = buildAssistantMcpDiscoveryResponseCandidate( entryPoint({ 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: "По данным 1С найдены строки входящих и исходящих денежных движений; нетто можно называть только как расчет по найденным строкам и проверенному периоду.", confirmed_lines: [ "1C bidirectional value-flow rows were checked for counterparty SVK: incoming=found, outgoing=found", "По найденным строкам 1С по контрагенту SVK за период 2020: получили 12 500,50 руб. по входящим движениям, заплатили 4 000 руб. по исходящим платежам/списаниям. Расчетное нетто в нашу сторону: 8 500,50 руб." ], inference_lines: [ "Counterparty net value-flow was calculated as incoming confirmed 1C rows minus outgoing confirmed 1C rows" ], unknown_lines: [ "Complete requested-period coverage for bidirectional value-flow is not proven because at least one MCP discovery probe row limit was reached", "Full bidirectional value-flow outside the checked period is not proven by this MCP discovery pilot" ], limitation_lines: ["pilot_bidirectional_value_flow_uses_two_query_movements_and_derives_net"], next_step_line: null } } }) ); expect(candidate.candidate_status).toBe("ready_for_guarded_use"); expect(candidate.reply_text).toContain("В 1С проверены входящие и исходящие денежные строки по контрагенту SVK"); expect(candidate.reply_text).toContain("получили 12 500,50 руб."); expect(candidate.reply_text).toContain("заплатили 4 000 руб."); expect(candidate.reply_text).toContain("нетто в нашу сторону: 8 500,50 руб."); expect(candidate.reply_text).toContain("Полное покрытие запрошенного периода по двустороннему денежному потоку не подтверждено"); expect(candidate.reply_text).not.toContain("pilot_"); expect(candidate.reply_text).not.toContain("query_movements"); }); it("keeps monthly breakdown lines user-facing and localizes monthly inference basis", () => { const candidate = buildAssistantMcpDiscoveryResponseCandidate( entryPoint({ 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: "По данным 1С найдены строки входящих и исходящих денежных движений; нетто и помесячная раскладка могут называться только как расчет по найденным строкам и проверенному периоду.", confirmed_lines: [ "1C bidirectional value-flow rows were checked for counterparty SVK: incoming=found, outgoing=found", "Помесячно: янв 2020 — получили 10 000 руб., заплатили 4 000 руб., нетто в нашу сторону 6 000 руб." ], inference_lines: [ "Counterparty monthly net value-flow breakdown was grouped by month over confirmed incoming and outgoing 1C rows" ], unknown_lines: [], limitation_lines: [], next_step_line: null } } }) ); expect(candidate.reply_text).toContain("Помесячно: янв 2020"); expect(candidate.reply_text).toContain("Помесячная нетто-раскладка сгруппирована только по подтвержденным входящим и исходящим строкам 1С."); expect(candidate.reply_text).not.toContain("Counterparty monthly net value-flow breakdown"); }); it("localizes recovered coverage facts without leaking broad-probe wording", () => { const candidate = buildAssistantMcpDiscoveryResponseCandidate( entryPoint({ 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: "РџРѕ данным 1РЎ найдены строки исходящих платежей/списаний; СЃСѓРјРјСѓ РјРѕР¶РЅРѕ называть только РІ рамках проверенного периода Рё найденных строк.", confirmed_lines: ["1C supplier-payout rows were found for counterparty SVK"], inference_lines: [ "Requested period coverage was recovered through monthly 1C value-flow probes after the broad probe hit the row limit" ], unknown_lines: [], limitation_lines: [], next_step_line: null } } }) ); expect(candidate.reply_text).toContain("Покрытие запрошенного периода восстановлено помесячными проверками 1С"); expect(candidate.reply_text).not.toContain("broad probe hit the row limit"); }); it("localizes document evidence without leaking raw English facts", () => { const candidate = buildAssistantMcpDiscoveryResponseCandidate( entryPoint({ 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: "По данным 1С найдены строки документов; ответ ограничен проверенным периодом и найденными строками.", confirmed_lines: ["1C document rows were found for counterparty SVK"], inference_lines: ["Counterparty document evidence is limited to confirmed 1C document rows in the checked scope"], unknown_lines: ["Full document history outside the checked period is not proven by this MCP discovery pilot"], limitation_lines: [], next_step_line: null } } }) ); expect(candidate.reply_text).toContain("В 1С найдены строки документов по контрагенту SVK."); expect(candidate.reply_text).toContain("Срез документов ограничен только подтвержденными строками документов"); expect(candidate.reply_text).toContain("Полный исторический срез документов вне проверенного периода этим поиском не подтвержден."); expect(candidate.reply_text).not.toContain("1C document rows were found"); }); it("localizes movement evidence without leaking raw English facts", () => { const candidate = buildAssistantMcpDiscoveryResponseCandidate( entryPoint({ 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: "РџРѕ данным 1РЎ найдены строки движений; ответ ограничен проверенным периодом Рё найденными строками.", confirmed_lines: ["1C movement rows were found for counterparty SVK"], inference_lines: ["Counterparty movement evidence is limited to confirmed 1C movement rows in the checked scope"], unknown_lines: ["Full movement history outside the checked period is not proven by this MCP discovery pilot"], limitation_lines: [], next_step_line: null } } }) ); expect(candidate.reply_text).toContain("Р’ 1РЎ найдены строки движений РїРѕ контрагенту SVK."); expect(candidate.reply_text).toContain("Срез движений ограничен только подтвержденными строками движений"); expect(candidate.reply_text).toContain("Полный исторический срез движений РІРЅРµ проверенного периода этим РїРѕРёСЃРєРѕРј РЅРµ подтвержден."); expect(candidate.reply_text).not.toContain("1C movement rows were found"); }); it("localizes metadata evidence without leaking raw MCP wording", () => { const candidate = buildAssistantMcpDiscoveryResponseCandidate( entryPoint({ 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: "По данным 1С найдена подтвержденная metadata-поверхность.", confirmed_lines: [ 'Confirmed 1C metadata surface for scope "НДС": 7 rows and 3 matching objects', "Available metadata object sets: accumulation_register, document", "Selected metadata entity set: Документ", "Selected metadata objects: Документ.СчетФактураВыданный", "Available metadata fields/sections: amount, vat_rate, organization" ], inference_lines: [ "A likely next checked lane may be inferred as document_evidence from the confirmed metadata surface" ], unknown_lines: [ 'No matching 1C metadata objects were confirmed for scope "Прибыль"', "Exact downstream metadata surface remains ambiguous across: Документ, РегистрНакопления" ], limitation_lines: ["Detailed metadata fields were not returned by this MCP metadata probe"], next_step_line: null } } }) ); expect(candidate.reply_text).toContain('В 1С подтверждена metadata-поверхность по области "НДС"'); expect(candidate.reply_text).toContain("Доступные типы metadata-объектов"); expect(candidate.reply_text).toContain("Выбранное семейство metadata-объектов: Документ"); expect(candidate.reply_text).toContain("Выбранные metadata-объекты для следующего шага"); expect(candidate.reply_text).toContain("Доступные metadata-поля/секции"); expect(candidate.reply_text).toContain("контур документов"); expect(candidate.reply_text).toContain('В 1С не подтверждены metadata-объекты по области "Прибыль"'); expect(candidate.reply_text).toContain("неоднозначна между family"); expect(candidate.reply_text).toContain("Эта MCP-проверка metadata не вернула детальный список полей"); expect(candidate.reply_text).not.toContain("Confirmed 1C metadata surface"); }); it("returns not applicable when discovery was skipped for an exact supported route", () => { const candidate = buildAssistantMcpDiscoveryResponseCandidate({ schema_version: "assistant_mcp_discovery_runtime_entry_point_v1", policy_owner: "assistantMcpDiscoveryRuntimeEntryPoint", entry_status: "skipped_not_applicable", hot_runtime_wired: false, discovery_attempted: false, turn_input: { adapter_status: "not_applicable" }, bridge: null, reason_codes: [] } as any); expect(candidate.candidate_status).toBe("not_applicable"); expect(candidate.reply_text).toBeNull(); expect(candidate.eligible_for_future_hot_runtime).toBe(false); }); it("creates a clarification candidate from a clarification bridge", () => { const candidate = buildAssistantMcpDiscoveryResponseCandidate( entryPoint({ bridge: { bridge_status: "needs_clarification", user_facing_response_allowed: true, business_fact_answer_allowed: false, requires_user_clarification: true, answer_draft: { answer_mode: "needs_clarification", headline: "Нужно уточнить контекст перед поиском в 1С.", confirmed_lines: [], inference_lines: [], unknown_lines: [], limitation_lines: [], next_step_line: "Уточните контрагента, период или организацию." } } }) ); expect(candidate.candidate_status).toBe("clarification_candidate"); expect(candidate.reply_type).toBe("clarification_required"); expect(candidate.reply_text).toContain("Нужно уточнить контекст"); expect(candidate.eligible_for_future_hot_runtime).toBe(true); }); it("surfaces metadata lane-choice clarification as a user-facing clarification candidate", () => { const candidate = buildAssistantMcpDiscoveryResponseCandidate( entryPoint({ bridge: { bridge_status: "needs_clarification", user_facing_response_allowed: true, business_fact_answer_allowed: false, requires_user_clarification: true, answer_draft: { answer_mode: "needs_clarification", headline: "По подтвержденной metadata-поверхности видно несколько конкурирующих data-lane, и без явного выбора дальше идти нельзя.", confirmed_lines: [], inference_lines: [], unknown_lines: [], limitation_lines: [], next_step_line: "Уточните, в какой контур идти дальше: по документам или по движениям/регистрам." } } }) ); expect(candidate.candidate_status).toBe("clarification_candidate"); expect(candidate.reply_type).toBe("clarification_required"); expect(candidate.reply_text).toContain("data-lane"); expect(candidate.reply_text).toContain("по документам"); expect(candidate.reply_text).toContain("по движениям/регистрам"); }); it("does not expose unsupported bridge output as a future hot candidate", () => { const candidate = buildAssistantMcpDiscoveryResponseCandidate( entryPoint({ bridge: { bridge_status: "unsupported", user_facing_response_allowed: true, business_fact_answer_allowed: false, requires_user_clarification: false, answer_draft: null } }) ); expect(candidate.candidate_status).toBe("unsupported"); expect(candidate.reply_type).toBe("no_grounded_answer"); expect(candidate.reply_text).toBeNull(); expect(candidate.eligible_for_future_hot_runtime).toBe(false); }); it("localizes open-scope bidirectional comparison scope and probe-limit wording without contour garbage", () => { const candidate = buildAssistantMcpDiscoveryResponseCandidate( entryPoint({ 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: "\u041f\u043e \u0434\u0430\u043d\u043d\u044b\u043c 1\u0421 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0441\u0442\u0440\u043e\u043a\u0438 \u0432\u0445\u043e\u0434\u044f\u0449\u0438\u0445 \u0438 \u0438\u0441\u0445\u043e\u0434\u044f\u0449\u0438\u0445 \u0434\u0435\u043d\u0435\u0436\u043d\u044b\u0445 \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u0439; \u043d\u0435\u0442\u0442\u043e \u043c\u043e\u0436\u043d\u043e \u043d\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043a\u0430\u043a \u0440\u0430\u0441\u0447\u0435\u0442 \u043f\u043e \u043d\u0430\u0439\u0434\u0435\u043d\u043d\u044b\u043c \u0441\u0442\u0440\u043e\u043a\u0430\u043c \u0438 \u043f\u0440\u043e\u0432\u0435\u0440\u0435\u043d\u043d\u043e\u043c\u0443 \u043f\u0435\u0440\u0438\u043e\u0434\u0443.", confirmed_lines: [ "1C bidirectional value-flow rows were checked for the requested counterparty scope: incoming=found, outgoing=found" ], inference_lines: [], unknown_lines: [], limitation_lines: [ "Requested period hit the MCP row limit, but the approved monthly recovery probe budget is smaller than the required subperiod count" ], next_step_line: null } } }) ); expect(candidate.reply_text).toContain( "\u0412 1\u0421 \u043f\u0440\u043e\u0432\u0435\u0440\u0435\u043d\u044b \u0432\u0445\u043e\u0434\u044f\u0449\u0438\u0435 \u0438 \u0438\u0441\u0445\u043e\u0434\u044f\u0449\u0438\u0435 \u0434\u0435\u043d\u0435\u0436\u043d\u044b\u0435 \u0441\u0442\u0440\u043e\u043a\u0438 \u0432 \u0437\u0430\u043f\u0440\u043e\u0448\u0435\u043d\u043d\u043e\u043c \u0441\u0440\u0435\u0437\u0435" ); expect(candidate.reply_text).toContain( "\u0417\u0430\u043f\u0440\u043e\u0448\u0435\u043d\u043d\u044b\u0439 \u043f\u0435\u0440\u0438\u043e\u0434 \u0443\u043f\u0435\u0440\u0441\u044f \u0432 \u043b\u0438\u043c\u0438\u0442 \u0441\u0442\u0440\u043e\u043a MCP" ); expect(candidate.reply_text).not.toContain( "\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\u0441\u043a\u043e\u043c\u0443 \u043a\u043e\u043d\u0442\u0443\u0440\u0443" ); expect(candidate.reply_text).not.toContain("Requested period hit the MCP row limit"); }); it("filters MCP discovery scope-mechanics from clarification unknown and limitation blocks", () => { const candidate = buildAssistantMcpDiscoveryResponseCandidate( entryPoint({ bridge: { bridge_status: "needs_clarification", user_facing_response_allowed: true, business_fact_answer_allowed: false, requires_user_clarification: true, answer_draft: { answer_mode: "needs_clarification", headline: "Могу посчитать общий денежный поток в проверяемом окне, но для проверяемого поиска в 1С нужно проверяемый период и организацию.", confirmed_lines: [], inference_lines: [], unknown_lines: ["MCP discovery pilot needs more scope before execution"], limitation_lines: ["MCP discovery pilot needs more scope before execution"], next_step_line: "Уточните период и организацию, и я продолжу поиск по денежному потоку в 1С." } } }) ); expect(candidate.reply_text).toContain("Уточните период и организацию"); expect(candidate.reply_text).not.toContain("MCP discovery"); expect(candidate.reply_text).not.toContain("needs more scope before execution"); expect(candidate.reply_text).not.toContain("Что не подтверждено:"); expect(candidate.reply_text).not.toContain("Ограничения проверки:"); }); });