import { describe, expect, it } from "vitest"; import { toRouteHintSummary, toRouterInput } from "../src/services/routeHintAdapter"; import { normalizedFixture, normalizedFixtureV2, normalizedFixtureV2_0_1, normalizedFixtureV2_0_2 } from "./fixtures"; describe("routeHintAdapter", () => { it("builds v1 route hint summary", () => { const summary = toRouteHintSummary(normalizedFixture()); expect(summary.mode).toBe("legacy_v1"); if (summary.mode !== "legacy_v1") { throw new Error("Expected legacy_v1 summary"); } expect(summary.route_hint).toBe("hybrid_store_plus_live"); expect(summary.decision_flags.needs_cross_entity_join).toBe(true); expect(summary.entities.accounts_mentioned).toEqual(["60"]); }); it("builds v2 deterministic route simulation", () => { const summary = toRouteHintSummary(normalizedFixtureV2()); expect(summary.mode).toBe("deterministic_v2"); if (summary.mode !== "deterministic_v2") { throw new Error("Expected deterministic_v2 summary"); } expect(summary.planner.total_fragments).toBe(1); expect(summary.decisions[0]?.route).toBe("hybrid_store_plus_live"); }); it("builds router input contract for v1", () => { const routerInput = toRouterInput(normalizedFixture()); expect(routerInput.route_hint).toBe("hybrid_store_plus_live"); expect(routerInput.intent_class).toBe("cross_entity"); }); it("keeps v2.0.1 soft assumptions executable in deterministic routing", () => { const summary = toRouteHintSummary(normalizedFixtureV2_0_1()); expect(summary.mode).toBe("deterministic_v2"); if (summary.mode !== "deterministic_v2") { throw new Error("Expected deterministic_v2 summary"); } expect(summary.fallback.type).toBe("none"); expect(summary.decisions[0]?.execution_readiness).toBe("executable_with_soft_assumptions"); expect(summary.decisions[0]?.route).toBe("hybrid_store_plus_live"); }); it("uses explicit v2.0.2 route_status/no_route_reason contract", () => { const summary = toRouteHintSummary(normalizedFixtureV2_0_2()); expect(summary.mode).toBe("deterministic_v2"); if (summary.mode !== "deterministic_v2") { throw new Error("Expected deterministic_v2 summary"); } expect(summary.decisions[0]?.route_status).toBe("routed"); expect(summary.decisions[0]?.no_route_reason).toBeNull(); expect(summary.decisions[0]?.route).toBe("hybrid_store_plus_live"); const routerInput = toRouterInput(normalizedFixtureV2_0_2()); const first = (routerInput.fragments as Array>)[0]; expect(first.route_status).toBe("routed"); expect(first.no_route_reason).toBeNull(); }); it("promotes lifecycle chain intent to hybrid route even without multi-entity flag", () => { const summary = toRouteHintSummary({ schema_version: "normalized_query_v2_0_2", user_message_raw: "Проверь по 97-му где расходы зависли и не дошли до списания", message_in_scope: true, scope_confidence: "high", contains_multiple_tasks: false, fragments: [ { fragment_id: "F1", raw_fragment_text: "где расходы будущих периодов зависли и не дошли до списания", normalized_fragment_text: "расходы будущих периодов зависли", domain_relevance: "in_scope", business_scope: "company_specific_accounting", entity_hints: [], account_hints: ["97"], document_hints: [], register_hints: [], time_scope: { type: "missing", value: null, confidence: "low" }, flags: { has_multi_entity_scope: false, asks_for_chain_explanation: true, asks_for_ranking_or_top: false, asks_for_period_summary: false, asks_for_rule_check: false, asks_for_anomaly_scan: false, asks_for_exact_object_trace: false, asks_for_evidence: false, mentions_period_close_context: false }, candidate_labels: ["cross_entity", "anomaly_probe"], confidence: "high", execution_readiness: "executable", clarification_reason: null, soft_assumption_used: [], route_status: "routed", no_route_reason: null } ], discarded_fragments: [], global_notes: { needs_clarification: false, clarification_reason: null } }); expect(summary.mode).toBe("deterministic_v2"); if (summary.mode !== "deterministic_v2") { throw new Error("Expected deterministic_v2 summary"); } expect(summary.decisions[0]?.route).toBe("hybrid_store_plus_live"); }); it("promotes mixed-ambiguity symptom fragment to hybrid when domain anchors are present", () => { const summary = toRouteHintSummary({ schema_version: "normalized_query_v2_0_2", user_message_raw: "2020-06 account 60: payment posted but settlement remains open", message_in_scope: false, scope_confidence: "low", contains_multiple_tasks: false, fragments: [ { fragment_id: "F1", raw_fragment_text: "2020-06 account 60: payment posted but settlement remains open", normalized_fragment_text: "2020-06 account 60 payment posted but settlement remains open", domain_relevance: "unclear", business_scope: "unclear", entity_hints: [], account_hints: ["60"], document_hints: [], register_hints: [], time_scope: { type: "explicit", value: "2020-06", confidence: "high" }, flags: { has_multi_entity_scope: false, asks_for_chain_explanation: false, asks_for_ranking_or_top: false, asks_for_period_summary: false, asks_for_rule_check: false, asks_for_anomaly_scan: false, asks_for_exact_object_trace: false, asks_for_evidence: false, mentions_period_close_context: false }, candidate_labels: [], confidence: "low", execution_readiness: "needs_clarification", clarification_reason: "domain_or_scope_unclear", soft_assumption_used: [], route_status: "no_route", no_route_reason: "insufficient_specificity" } ], discarded_fragments: [], global_notes: { needs_clarification: false, clarification_reason: null } }); expect(summary.mode).toBe("deterministic_v2"); if (summary.mode !== "deterministic_v2") { throw new Error("Expected deterministic_v2 summary"); } expect(summary.decisions[0]?.route).toBe("hybrid_store_plus_live"); expect(summary.decisions[0]?.route_status).toBe("routed"); }); it("keeps canonical path only for factual fragments without symptom/lifecycle markers", () => { const summary = toRouteHintSummary({ schema_version: "normalized_query_v2_0_2", user_message_raw: "Покажи последний документ по 10 счету за июнь 2020.", message_in_scope: true, scope_confidence: "high", contains_multiple_tasks: false, fragments: [ { fragment_id: "F1", raw_fragment_text: "последний документ по 10 счету за июнь 2020", normalized_fragment_text: "последний документ по 10 счету июнь 2020", domain_relevance: "in_scope", business_scope: "company_specific_accounting", entity_hints: ["документ"], account_hints: ["10"], document_hints: [], register_hints: [], time_scope: { type: "explicit", value: "2020-06", confidence: "high" }, flags: { has_multi_entity_scope: false, asks_for_chain_explanation: false, asks_for_ranking_or_top: false, asks_for_period_summary: false, asks_for_rule_check: false, asks_for_anomaly_scan: false, asks_for_exact_object_trace: false, asks_for_evidence: false, mentions_period_close_context: false }, candidate_labels: ["simple_factual"], confidence: "high", execution_readiness: "executable", clarification_reason: null, soft_assumption_used: [], route_status: "routed", no_route_reason: null } ], discarded_fragments: [], global_notes: { needs_clarification: false, clarification_reason: null } }); expect(summary.mode).toBe("deterministic_v2"); if (summary.mode !== "deterministic_v2") { throw new Error("Expected deterministic_v2 summary"); } expect(summary.decisions[0]?.route).toBe("store_canonical"); }); });