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

288 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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");
});
it("routes counterparty received-paid-net wording to hybrid instead of canonical fact lookup", () => {
const summary = toRouteHintSummary({
schema_version: "normalized_query_v2_0_2",
user_message_raw: "А теперь по Группа СВК за 2020: сколько денег получили, сколько заплатили и какое нетто?",
message_in_scope: true,
scope_confidence: "high",
contains_multiple_tasks: false,
fragments: [
{
fragment_id: "F1",
raw_fragment_text: "А теперь по Группа СВК за 2020: сколько денег получили, сколько заплатили и какое нетто?",
normalized_fragment_text:
"Определить сумму полученных средств, сумму выплаченных средств и чистый остаток (нетто) для контрагента Группа СВК за период 2020 года.",
domain_relevance: "in_scope",
business_scope: "company_specific_accounting",
entity_hints: ["Группа СВК"],
account_hints: [],
document_hints: [],
register_hints: [],
time_scope: {
type: "explicit",
value: "2020",
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("hybrid_store_plus_live");
expect(summary.decisions[0]?.reason).toContain("bidirectional_value_flow");
});
});