NODEDC_1C/llm_normalizer/backend/tests/routeHintAdapter.test.ts

229 lines
8.9 KiB
TypeScript

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<Record<string, unknown>>)[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");
});
});