ГЛОБАЛЬНЫЙ РЕФАКТОРИНГ АРХИТЕКТУРЫ - Рефакторинг этапов 2.12.23: декомпозиция deep-turn пайплайна ассистента в runtime-адаптеры
This commit is contained in:
@@ -1654,6 +1654,11 @@ describe("address intent resolver expansion (M2.3a)", () => {
|
||||
expect(result.intent).toBe("customer_revenue_and_payments");
|
||||
});
|
||||
|
||||
it("resolves major-share revenue wording into customer revenue intent", () => {
|
||||
const result = resolveAddressIntent("какие контрагенты принесли основную часть нашей выручки за отчетный период?");
|
||||
expect(result.intent).toBe("customer_revenue_and_payments");
|
||||
});
|
||||
|
||||
it("resolves customer revenue intent from highest inflow slang wording", () => {
|
||||
const result = resolveAddressIntent("какие приходы самые высокие за все время");
|
||||
expect(result.intent).toBe("customer_revenue_and_payments");
|
||||
@@ -1725,6 +1730,74 @@ describe("address intent resolver expansion (M2.3a)", () => {
|
||||
const result = resolveAddressIntent("покажи документы по этому же договору");
|
||||
expect(result.intent).toBe("list_documents_by_contract");
|
||||
});
|
||||
|
||||
it("routes supplier tail-risk wording into payables intent", () => {
|
||||
const result = resolveAddressIntent(
|
||||
"Кто из поставщиков имеет хвосты с документами на конец месяца, которые уже больше похожи на систематическую проблему, а не на обычную задержку?"
|
||||
);
|
||||
expect(result.intent).toBe("list_payables_counterparties");
|
||||
});
|
||||
|
||||
it("keeps out-of-scope supplier control wording as unknown intent", () => {
|
||||
const result = resolveAddressIntent(
|
||||
"Какие поставщики у нас уже пару месяцев сдают акты без приходок. Может, их надо проконтролировать отдельно чтоб не засорять бухгалтерию дальше?"
|
||||
);
|
||||
expect(result.intent).toBe("unknown");
|
||||
});
|
||||
|
||||
it("routes long shipment-to-payment lag wording into receivables intent", () => {
|
||||
const result = resolveAddressIntent(
|
||||
"Где у нас висят покупатели со слишком длинным периодом между отправкой товара и его оплатой, и это уже вызывает тревогу?"
|
||||
);
|
||||
expect(result.intent).toBe("list_receivables_counterparties");
|
||||
});
|
||||
|
||||
it("routes non-paying counterparties month-risk wording into receivables intent", () => {
|
||||
const result = resolveAddressIntent(
|
||||
"какие контрагенты пока вообще не платят за текущий месяц и это уже тревожный знак для нас?"
|
||||
);
|
||||
expect(result.intent).toBe("list_receivables_counterparties");
|
||||
});
|
||||
|
||||
it("routes reconciliation mismatch wording into open contracts intent", () => {
|
||||
const result = resolveAddressIntent(
|
||||
"Покажи контрагентов, по которым сальдо скорее всего не совпадет с их актом сверки. Может, стоит поторопиться и запросить сверку?"
|
||||
);
|
||||
expect(result.intent).toBe("list_open_contracts");
|
||||
});
|
||||
|
||||
it("routes reconciliation mismatch wording without explicit lookup verb into open contracts intent", () => {
|
||||
const result = resolveAddressIntent(
|
||||
"По каким поставщикам у нас сальдо явно расходится с тем, что они сами указывают в своих актах сверок?"
|
||||
);
|
||||
expect(result.intent).toBe("list_open_contracts");
|
||||
});
|
||||
|
||||
it("routes payments-without-closing-docs wording into open contracts intent", () => {
|
||||
const result = resolveAddressIntent(
|
||||
"Где у нас есть платежи, но нет документов для закрытия взаиморасчетов? Это уже требует ручной проверки."
|
||||
);
|
||||
expect(result.intent).toBe("list_open_contracts");
|
||||
});
|
||||
|
||||
it("routes documents-without-payments wording into open contracts intent", () => {
|
||||
const result = resolveAddressIntent(
|
||||
"По каким контрагентам документы есть, а оплат нет. Может, стоит взять на карандаш такие ситуации чтоб не тянуть дальше?"
|
||||
);
|
||||
expect(result.intent).toBe("list_open_contracts");
|
||||
});
|
||||
|
||||
it("routes stale advances without closing docs wording into open contracts intent", () => {
|
||||
const result = resolveAddressIntent(
|
||||
"по каким поставщикам мы видим проблемные авансы, которые давно не закрыты документами?"
|
||||
);
|
||||
expect(result.intent).toBe("list_open_contracts");
|
||||
});
|
||||
|
||||
it("routes buyers with open debt wording into open-items intent", () => {
|
||||
const result = resolveAddressIntent("по каким покупателям у нас есть открытые задолженности на конец месяца?");
|
||||
expect(result.intent).toBe("open_items_by_counterparty_or_contract");
|
||||
});
|
||||
});
|
||||
|
||||
describe("address filter extraction for balance drilldown", () => {
|
||||
@@ -1810,6 +1883,14 @@ describe("address filter extraction for balance drilldown", () => {
|
||||
expect(extracted.warnings).toContain("counterparty_anchor_dropped_low_quality");
|
||||
});
|
||||
|
||||
it("does not derive fake counterparty anchor for open-contracts stale-advance wording", () => {
|
||||
const extracted = extractAddressFilters(
|
||||
"по каким поставщикам мы видим проблемные авансы, которые давно не закрыты документами?",
|
||||
"list_open_contracts"
|
||||
);
|
||||
expect(extracted.extracted_filters.counterparty).toBeUndefined();
|
||||
});
|
||||
|
||||
it("derives VAT forecast quarter-to-date window when plain date phrase is present", () => {
|
||||
const extracted = extractAddressFilters(
|
||||
"мож прикинусь плиз скока ндс надо заплатить на 15 марта 2020 года",
|
||||
@@ -2250,6 +2331,98 @@ describe("address filter extraction for balance drilldown", () => {
|
||||
});
|
||||
|
||||
describe("address query limited taxonomy and stage diagnostics", () => {
|
||||
it("injects as_of_date from analysis context when user message has no explicit period", async () => {
|
||||
const service = new AddressQueryService();
|
||||
const result = await service.tryHandle("Покажи контрагентов с незакрытыми хвостами", {
|
||||
analysisDateHint: "2020-07-31"
|
||||
});
|
||||
expect(result?.handled).toBe(true);
|
||||
expect(result?.debug.extracted_filters?.as_of_date).toBe("2020-07-31");
|
||||
expect(Array.isArray(result?.debug.reasons)).toBe(true);
|
||||
expect(result?.debug.reasons).toContain("as_of_date_from_analysis_context");
|
||||
});
|
||||
|
||||
it("returns soft out-of-scope reply without technical jargon for unsupported supplier-control wording", async () => {
|
||||
const service = new AddressQueryService();
|
||||
const result = await service.tryHandle(
|
||||
"Какие поставщики у нас уже пару месяцев сдают акты без приходок. Может, их надо проконтролировать отдельно чтоб не засорять бухгалтерию дальше?"
|
||||
);
|
||||
expect(result?.handled).toBe(true);
|
||||
expect(result?.response_type).toBe("LIMITED_WITH_REASON");
|
||||
expect(result?.debug.detected_intent).toBe("unknown");
|
||||
expect(result?.debug.limited_reason_category).toBe("unsupported");
|
||||
const reply = String(result?.reply_text ?? "");
|
||||
expect(reply.toLowerCase()).toContain("вне поддерживаемого контура");
|
||||
expect(reply).not.toMatch(/address_query|V1|lookup|materialized|якор/iu);
|
||||
});
|
||||
|
||||
it("routes supplier tail-risk wording without forcing missing-anchor fallback", async () => {
|
||||
const service = new AddressQueryService();
|
||||
const result = await service.tryHandle(
|
||||
"Кто из поставщиков имеет хвосты с документами на конец месяца, которые уже больше похожи на систематическую проблему, а не на обычную задержку?"
|
||||
);
|
||||
expect(result?.handled).toBe(true);
|
||||
expect(result?.debug.detected_intent).toBe("list_payables_counterparties");
|
||||
expect(result?.debug.limited_reason_category).not.toBe("missing_anchor");
|
||||
expect(result?.debug.limited_reason_category).not.toBe("unsupported");
|
||||
});
|
||||
|
||||
it("routes shipment-to-payment lag wording into receivables lane without missing-anchor fallback", async () => {
|
||||
const service = new AddressQueryService();
|
||||
const result = await service.tryHandle(
|
||||
"Где у нас висят покупатели со слишком длинным периодом между отправкой товара и его оплатой, и это уже вызывает тревогу?"
|
||||
);
|
||||
expect(result?.handled).toBe(true);
|
||||
expect(result?.debug.detected_intent).toBe("list_receivables_counterparties");
|
||||
expect(result?.debug.limited_reason_category).not.toBe("missing_anchor");
|
||||
expect(result?.debug.limited_reason_category).not.toBe("unsupported");
|
||||
});
|
||||
|
||||
it("routes payments-without-closing-docs wording into open contracts lane", async () => {
|
||||
const service = new AddressQueryService();
|
||||
const result = await service.tryHandle(
|
||||
"Где у нас есть платежи, но нет документов для закрытия взаиморасчетов? Это уже требует ручной проверки."
|
||||
);
|
||||
expect(result?.handled).toBe(true);
|
||||
expect(result?.debug.detected_intent).toBe("list_open_contracts");
|
||||
expect(result?.debug.limited_reason_category).not.toBe("missing_anchor");
|
||||
expect(result?.debug.limited_reason_category).not.toBe("unsupported");
|
||||
});
|
||||
|
||||
it("routes stale advances wording into open contracts lane without missing-anchor fallback", async () => {
|
||||
const service = new AddressQueryService();
|
||||
const result = await service.tryHandle(
|
||||
"по каким поставщикам мы видим проблемные авансы, которые давно не закрыты документами?"
|
||||
);
|
||||
expect(result?.handled).toBe(true);
|
||||
expect(result?.debug.detected_intent).toBe("list_open_contracts");
|
||||
expect(result?.debug.limited_reason_category).not.toBe("missing_anchor");
|
||||
expect(result?.debug.limited_reason_category).not.toBe("unsupported");
|
||||
});
|
||||
|
||||
it("routes non-paying counterparties month-risk wording into receivables lane", async () => {
|
||||
const service = new AddressQueryService();
|
||||
const result = await service.tryHandle(
|
||||
"какие контрагенты пока вообще не платят за текущий месяц и это уже тревожный знак для нас?"
|
||||
);
|
||||
expect(result?.handled).toBe(true);
|
||||
expect(result?.debug.detected_intent).toBe("list_receivables_counterparties");
|
||||
expect(result?.debug.selected_recipe).toBe("address_movements_receivables_v1");
|
||||
expect(result?.debug.limited_reason_category).not.toBe("missing_anchor");
|
||||
expect(result?.debug.limited_reason_category).not.toBe("unsupported");
|
||||
});
|
||||
|
||||
it("routes documents-without-payments wording into open contracts lane", async () => {
|
||||
const service = new AddressQueryService();
|
||||
const result = await service.tryHandle(
|
||||
"По каким контрагентам документы есть, а оплат нет. Может, стоит взять на карандаш такие ситуации чтоб не тянуть дальше?"
|
||||
);
|
||||
expect(result?.handled).toBe(true);
|
||||
expect(result?.debug.detected_intent).toBe("list_open_contracts");
|
||||
expect(result?.debug.limited_reason_category).not.toBe("missing_anchor");
|
||||
expect(result?.debug.limited_reason_category).not.toBe("unsupported");
|
||||
});
|
||||
|
||||
it("routes period coverage profile question into dedicated aggregate recipe", async () => {
|
||||
const service = new AddressQueryService();
|
||||
const result = await service.tryHandle("За какие годы в базе есть данные?");
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { buildAssistantAnswerStructureV11 } from "../src/services/assistantAnswerPackageBuilder";
|
||||
|
||||
function buildRetrieval(input?: Partial<any>): any {
|
||||
return {
|
||||
fragment_id: "F1",
|
||||
requirement_ids: ["R1"],
|
||||
route: "hybrid_store_plus_live",
|
||||
status: "ok",
|
||||
result_type: "summary",
|
||||
items: [],
|
||||
summary: {},
|
||||
evidence: [],
|
||||
why_included: [],
|
||||
selection_reason: [],
|
||||
risk_factors: [],
|
||||
business_interpretation: [],
|
||||
confidence: "medium",
|
||||
limitations: [],
|
||||
errors: [],
|
||||
...input
|
||||
};
|
||||
}
|
||||
|
||||
describe("assistant answer package builder v11", () => {
|
||||
it("builds baseline answer structure with unresolved mechanism", () => {
|
||||
const structure = buildAssistantAnswerStructureV11({
|
||||
assistantReply: "Первая строка\nВторая строка",
|
||||
coverageReport: {
|
||||
requirements_total: 2,
|
||||
requirements_covered: 1,
|
||||
requirements_uncovered: ["R2"],
|
||||
requirements_partially_covered: [],
|
||||
clarification_needed_for: [],
|
||||
out_of_scope_requirements: []
|
||||
},
|
||||
groundingCheck: {
|
||||
status: "partial",
|
||||
route_subject_match: true,
|
||||
missing_requirements: ["R2"],
|
||||
reasons: ["limited coverage"],
|
||||
why_included_summary: [],
|
||||
selection_reason_summary: []
|
||||
},
|
||||
retrievalResults: [buildRetrieval()]
|
||||
});
|
||||
|
||||
expect(structure.schema_version).toBe("answer_structure_v1_1");
|
||||
expect(structure.answer_summary).toBe("Первая строка");
|
||||
expect(structure.mechanism_block.status).toBe("unresolved");
|
||||
expect(structure.evidence_block.coverage_note).toBe("coverage_partial_or_limited");
|
||||
expect(structure.uncertainty_block.open_uncertainties).toEqual(["R2"]);
|
||||
});
|
||||
|
||||
it("adds claim-evidence links when enrichment is explicitly enabled", () => {
|
||||
const structure = buildAssistantAnswerStructureV11({
|
||||
assistantReply: "Ответ",
|
||||
coverageReport: {
|
||||
requirements_total: 1,
|
||||
requirements_covered: 1,
|
||||
requirements_uncovered: [],
|
||||
requirements_partially_covered: [],
|
||||
clarification_needed_for: [],
|
||||
out_of_scope_requirements: []
|
||||
},
|
||||
groundingCheck: {
|
||||
status: "grounded",
|
||||
route_subject_match: true,
|
||||
missing_requirements: [],
|
||||
reasons: [],
|
||||
why_included_summary: [],
|
||||
selection_reason_summary: []
|
||||
},
|
||||
retrievalResults: [
|
||||
buildRetrieval({
|
||||
evidence: [
|
||||
{
|
||||
evidence_id: "ev-1",
|
||||
claim_ref: "requirement:R1",
|
||||
source_type: "retrieval_item",
|
||||
source_ref: {
|
||||
schema_version: "evidence_source_ref_v1",
|
||||
namespace: "snapshot_2020",
|
||||
entity: "document",
|
||||
id: "doc-1",
|
||||
period: "2020-07",
|
||||
canonical_ref: "evidence_source_ref_v1|snapshot_2020|document|doc-1|2020-07"
|
||||
},
|
||||
pointer: {
|
||||
fragment_id: "F1",
|
||||
route: "hybrid_store_plus_live",
|
||||
source: {
|
||||
namespace: "snapshot_2020",
|
||||
entity: "document",
|
||||
id: "doc-1",
|
||||
period: "2020-07"
|
||||
},
|
||||
locator: {
|
||||
field_path: "amount",
|
||||
item_index: 0
|
||||
}
|
||||
},
|
||||
evidence_kind: "mechanism_link",
|
||||
mechanism_note: "trace confirmed",
|
||||
confidence: "high",
|
||||
limitation: null,
|
||||
payload: {}
|
||||
}
|
||||
]
|
||||
})
|
||||
],
|
||||
options: {
|
||||
enableEvidenceEnrichment: true
|
||||
}
|
||||
});
|
||||
|
||||
expect(Array.isArray(structure.evidence_block.claim_evidence_links)).toBe(true);
|
||||
expect(structure.evidence_block.claim_evidence_links?.[0]?.claim_ref).toBe("requirement:R1");
|
||||
expect(structure.evidence_block.claim_evidence_links?.[0]?.evidence_ids).toContain("ev-1");
|
||||
});
|
||||
|
||||
it("omits claim-evidence links when enrichment is disabled", () => {
|
||||
const structure = buildAssistantAnswerStructureV11({
|
||||
assistantReply: "Ответ",
|
||||
coverageReport: {
|
||||
requirements_total: 1,
|
||||
requirements_covered: 1,
|
||||
requirements_uncovered: [],
|
||||
requirements_partially_covered: [],
|
||||
clarification_needed_for: [],
|
||||
out_of_scope_requirements: []
|
||||
},
|
||||
groundingCheck: {
|
||||
status: "grounded",
|
||||
route_subject_match: true,
|
||||
missing_requirements: [],
|
||||
reasons: [],
|
||||
why_included_summary: [],
|
||||
selection_reason_summary: []
|
||||
},
|
||||
retrievalResults: [
|
||||
buildRetrieval({
|
||||
evidence: [
|
||||
{
|
||||
evidence_id: "ev-1",
|
||||
claim_ref: "requirement:R1",
|
||||
source_type: "retrieval_item",
|
||||
source_ref: {
|
||||
schema_version: "evidence_source_ref_v1",
|
||||
namespace: "snapshot_2020",
|
||||
entity: "document",
|
||||
id: "doc-1",
|
||||
period: "2020-07",
|
||||
canonical_ref: "evidence_source_ref_v1|snapshot_2020|document|doc-1|2020-07"
|
||||
},
|
||||
pointer: {
|
||||
fragment_id: "F1",
|
||||
route: "hybrid_store_plus_live",
|
||||
source: {
|
||||
namespace: "snapshot_2020",
|
||||
entity: "document",
|
||||
id: "doc-1",
|
||||
period: "2020-07"
|
||||
},
|
||||
locator: {
|
||||
field_path: "amount",
|
||||
item_index: 0
|
||||
}
|
||||
},
|
||||
evidence_kind: "mechanism_link",
|
||||
mechanism_note: "trace confirmed",
|
||||
confidence: "high",
|
||||
limitation: null,
|
||||
payload: {}
|
||||
}
|
||||
]
|
||||
})
|
||||
],
|
||||
options: {
|
||||
enableEvidenceEnrichment: false
|
||||
}
|
||||
});
|
||||
|
||||
expect(structure.evidence_block.claim_evidence_links).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,123 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { AssistantRequirement, UnifiedRetrievalResult } from "../src/types/assistant";
|
||||
import { buildAssistantEvidenceBundleContractV1 } from "../src/services/assistantOrchestrationContracts";
|
||||
import { assembleAssistantContractsBundleV1 } from "../src/services/assistantContractsBundleAssembler";
|
||||
|
||||
function buildRequirement(): AssistantRequirement {
|
||||
return {
|
||||
requirement_id: "R1",
|
||||
source_fragment_id: "F1",
|
||||
requirement_text: "req1",
|
||||
subject_tokens: ["account_60.01"],
|
||||
status: "covered",
|
||||
route: "hybrid_store_plus_live"
|
||||
};
|
||||
}
|
||||
|
||||
function buildRetrieval(input?: Partial<UnifiedRetrievalResult>): UnifiedRetrievalResult {
|
||||
return {
|
||||
fragment_id: "F1",
|
||||
requirement_ids: ["R1"],
|
||||
route: "hybrid_store_plus_live",
|
||||
status: "ok",
|
||||
result_type: "summary",
|
||||
items: [],
|
||||
summary: {},
|
||||
evidence: [],
|
||||
why_included: [],
|
||||
selection_reason: [],
|
||||
risk_factors: [],
|
||||
business_interpretation: [],
|
||||
confidence: "medium",
|
||||
limitations: [],
|
||||
errors: [],
|
||||
...input
|
||||
};
|
||||
}
|
||||
|
||||
describe("assistant contracts bundle assembler", () => {
|
||||
it("assembles query/execution/coverage contracts with outcome class", () => {
|
||||
const retrievalResults = [buildRetrieval({ status: "ok" })];
|
||||
const bundle = assembleAssistantContractsBundleV1({
|
||||
userMessage: "проверь хвосты по 60.01",
|
||||
normalizedQuestion: "проверь хвосты по 60.01",
|
||||
normalized: {
|
||||
schema_version: "normalized_query_v2_0_2",
|
||||
user_message_raw: "проверь хвосты по 60.01",
|
||||
message_in_scope: true,
|
||||
scope_confidence: "high",
|
||||
contains_multiple_tasks: false,
|
||||
fragments: [{ fragment_id: "F1" }],
|
||||
discarded_fragments: [],
|
||||
global_notes: {
|
||||
needs_clarification: false,
|
||||
clarification_reason: null
|
||||
}
|
||||
} as any,
|
||||
routeSummary: {
|
||||
mode: "deterministic_v2",
|
||||
message_in_scope: true,
|
||||
scope_confidence: "high",
|
||||
planner: {
|
||||
total_fragments: 1,
|
||||
in_scope_fragments: 1,
|
||||
out_of_scope_fragments: 0,
|
||||
discarded_fragments: 0,
|
||||
contains_multiple_tasks: false
|
||||
},
|
||||
decisions: [],
|
||||
fallback: {
|
||||
type: "none",
|
||||
message: null
|
||||
}
|
||||
},
|
||||
droppedIntentSegments: [],
|
||||
analysisContext: {
|
||||
as_of_date: "2020-07-31",
|
||||
period_from: null,
|
||||
period_to: null,
|
||||
source: "eval_analysis_date",
|
||||
snapshot_mode: "auto"
|
||||
},
|
||||
executionPlan: [
|
||||
{
|
||||
fragment_id: "F1",
|
||||
requirement_ids: ["R1"],
|
||||
route: "hybrid_store_plus_live",
|
||||
should_execute: true,
|
||||
no_route_reason: null,
|
||||
clarification_reason: null
|
||||
}
|
||||
],
|
||||
requirements: [buildRequirement()],
|
||||
evidenceBundleContractV1: buildAssistantEvidenceBundleContractV1({
|
||||
retrievalCalls: [{ route: "hybrid_store_plus_live" }],
|
||||
retrievalResults
|
||||
}),
|
||||
replyType: "factual_with_explanation",
|
||||
coverageReport: {
|
||||
requirements_total: 1,
|
||||
requirements_covered: 1,
|
||||
requirements_uncovered: [],
|
||||
requirements_partially_covered: [],
|
||||
clarification_needed_for: [],
|
||||
out_of_scope_requirements: []
|
||||
},
|
||||
grounding: {
|
||||
status: "grounded",
|
||||
route_subject_match: true,
|
||||
missing_requirements: [],
|
||||
reasons: [],
|
||||
why_included_summary: [],
|
||||
selection_reason_summary: []
|
||||
},
|
||||
retrievalResults
|
||||
});
|
||||
|
||||
expect(bundle.queryFrameContractV1.schema_version).toBe("assistant_query_frame_v1");
|
||||
expect(bundle.executionPlanContractV1.schema_version).toBe("assistant_execution_plan_v1");
|
||||
expect(bundle.coverageContractV1.schema_version).toBe("assistant_coverage_contract_v1");
|
||||
expect(bundle.outcomeClassV1).toBe("FULLY_ANSWERED");
|
||||
expect(bundle.assistantOrchestrationContractsV1.evidence_bundle.schema_version).toBe("assistant_evidence_bundle_v1");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,199 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
checkGroundingForRequirements,
|
||||
evaluateCoverageForRequirements,
|
||||
extractRequirementsForRoute
|
||||
} from "../src/services/assistantCoverageGrounding";
|
||||
|
||||
describe("assistant coverage-grounding module", () => {
|
||||
it("extracts requirements from deterministic route summary", () => {
|
||||
const extracted = extractRequirementsForRoute({
|
||||
routeSummary: {
|
||||
mode: "deterministic_v2",
|
||||
message_in_scope: true,
|
||||
scope_confidence: "high",
|
||||
planner: {
|
||||
total_fragments: 2,
|
||||
in_scope_fragments: 2,
|
||||
out_of_scope_fragments: 0,
|
||||
discarded_fragments: 0,
|
||||
contains_multiple_tasks: false
|
||||
},
|
||||
decisions: [
|
||||
{
|
||||
fragment_id: "F1",
|
||||
route: "no_route",
|
||||
no_route_reason: "insufficient_specificity",
|
||||
reason: "missing anchor"
|
||||
},
|
||||
{
|
||||
fragment_id: "F2",
|
||||
route: "hybrid_store_plus_live",
|
||||
reason: "ok route"
|
||||
}
|
||||
],
|
||||
fallback: {
|
||||
type: "none",
|
||||
message: null
|
||||
}
|
||||
} as any,
|
||||
userMessage: "base question",
|
||||
fragmentTextById: new Map([
|
||||
["F1", "need more details"],
|
||||
["F2", "check account 60"]
|
||||
]),
|
||||
extractSubjectTokens: (text) => (text.includes("60") ? ["account_60"] : ["counterparty"])
|
||||
});
|
||||
|
||||
expect(extracted.requirements).toHaveLength(2);
|
||||
expect(extracted.requirements[0].status).toBe("clarification_needed");
|
||||
expect(extracted.requirements[0].route).toBeNull();
|
||||
expect(extracted.requirements[1].status).toBe("covered");
|
||||
expect(extracted.requirements[1].route).toBe("hybrid_store_plus_live");
|
||||
expect(extracted.byFragment.get("F2")).toEqual(["R2"]);
|
||||
});
|
||||
|
||||
it("evaluates coverage from retrieval outcomes", () => {
|
||||
const requirements = [
|
||||
{
|
||||
requirement_id: "R1",
|
||||
source_fragment_id: "F1",
|
||||
requirement_text: "req1",
|
||||
subject_tokens: ["account_60"],
|
||||
status: "covered" as const,
|
||||
route: "hybrid_store_plus_live"
|
||||
},
|
||||
{
|
||||
requirement_id: "R2",
|
||||
source_fragment_id: "F2",
|
||||
requirement_text: "req2",
|
||||
subject_tokens: ["counterparty"],
|
||||
status: "covered" as const,
|
||||
route: "store_feature_risk"
|
||||
}
|
||||
];
|
||||
const retrievalResults = [
|
||||
{
|
||||
fragment_id: "F1",
|
||||
requirement_ids: ["R1"],
|
||||
route: "hybrid_store_plus_live",
|
||||
status: "ok",
|
||||
result_type: "summary",
|
||||
items: [],
|
||||
summary: {},
|
||||
evidence: [
|
||||
{
|
||||
evidence_id: "ev-1",
|
||||
claim_ref: "requirement:R1",
|
||||
source_type: "retrieval_item",
|
||||
source_ref: {
|
||||
schema_version: "evidence_source_ref_v1",
|
||||
namespace: "snapshot_2020",
|
||||
entity: "document",
|
||||
id: "doc-1",
|
||||
period: "2020-07",
|
||||
canonical_ref: "evidence_source_ref_v1|snapshot_2020|document|doc-1|2020-07"
|
||||
},
|
||||
pointer: {
|
||||
fragment_id: "F1",
|
||||
route: "hybrid_store_plus_live",
|
||||
source: {
|
||||
namespace: "snapshot_2020",
|
||||
entity: "document",
|
||||
id: "doc-1",
|
||||
period: "2020-07"
|
||||
},
|
||||
locator: {
|
||||
field_path: "amount",
|
||||
item_index: 0
|
||||
}
|
||||
},
|
||||
evidence_kind: "mechanism_link",
|
||||
mechanism_note: "ok",
|
||||
confidence: "high",
|
||||
limitation: null,
|
||||
payload: {}
|
||||
}
|
||||
],
|
||||
why_included: ["why"],
|
||||
selection_reason: ["sel"],
|
||||
risk_factors: [],
|
||||
business_interpretation: [],
|
||||
confidence: "high",
|
||||
limitations: [],
|
||||
errors: []
|
||||
},
|
||||
{
|
||||
fragment_id: "F2",
|
||||
requirement_ids: ["R2"],
|
||||
route: "store_feature_risk",
|
||||
status: "empty",
|
||||
result_type: "summary",
|
||||
items: [],
|
||||
summary: {},
|
||||
evidence: [],
|
||||
why_included: [],
|
||||
selection_reason: [],
|
||||
risk_factors: [],
|
||||
business_interpretation: [],
|
||||
confidence: "low",
|
||||
limitations: [],
|
||||
errors: []
|
||||
}
|
||||
] as any;
|
||||
|
||||
const evaluation = evaluateCoverageForRequirements(requirements as any, retrievalResults);
|
||||
expect(evaluation.coverage.requirements_total).toBe(2);
|
||||
expect(evaluation.coverage.requirements_covered).toBe(1);
|
||||
expect(evaluation.coverage.requirements_uncovered).toContain("R2");
|
||||
expect(evaluation.requirements.find((item) => item.requirement_id === "R1")?.status).toBe("covered");
|
||||
});
|
||||
|
||||
it("produces route mismatch grounding when critical subject token is absent", () => {
|
||||
const grounded = checkGroundingForRequirements({
|
||||
userMessage: "Проверь НДС цепочку",
|
||||
requirements: [
|
||||
{
|
||||
requirement_id: "R1",
|
||||
source_fragment_id: "F1",
|
||||
requirement_text: "vat chain",
|
||||
subject_tokens: ["nds"],
|
||||
status: "covered",
|
||||
route: "hybrid_store_plus_live"
|
||||
}
|
||||
] as any,
|
||||
coverage: {
|
||||
requirements_total: 1,
|
||||
requirements_covered: 1,
|
||||
requirements_uncovered: [],
|
||||
requirements_partially_covered: [],
|
||||
clarification_needed_for: [],
|
||||
out_of_scope_requirements: []
|
||||
},
|
||||
retrievalResults: [
|
||||
{
|
||||
fragment_id: "F1",
|
||||
requirement_ids: ["R1"],
|
||||
route: "hybrid_store_plus_live",
|
||||
status: "ok",
|
||||
result_type: "summary",
|
||||
items: [],
|
||||
summary: { note: "no tax markers" },
|
||||
evidence: [],
|
||||
why_included: [],
|
||||
selection_reason: [],
|
||||
risk_factors: [],
|
||||
business_interpretation: [],
|
||||
confidence: "medium",
|
||||
limitations: [],
|
||||
errors: []
|
||||
}
|
||||
] as any,
|
||||
extractSubjectTokens: () => ["nds"]
|
||||
});
|
||||
|
||||
expect(grounded.status).toBe("route_mismatch_blocked");
|
||||
expect(grounded.route_subject_match).toBe(false);
|
||||
expect(grounded.reasons.some((item) => item.includes("Ключевые ориентиры вопроса"))).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,130 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { buildDeepAnalysisDebugPayload } from "../src/services/assistantDebugPayloadAssembler";
|
||||
|
||||
function baseInput() {
|
||||
return {
|
||||
traceId: "trace-1",
|
||||
promptVersion: "normalizer_v2_0_2",
|
||||
schemaVersion: "normalized_query_v2_0_2",
|
||||
fallbackType: "none",
|
||||
routeSummary: { mode: "deterministic_v2" },
|
||||
fragments: [{ fragment_id: "F1" }],
|
||||
requirementsExtracted: [{ requirement_id: "R1", status: "covered" }],
|
||||
coverageReport: { requirements_total: 1, requirements_covered: 1 },
|
||||
routes: [{ fragment_id: "F1", route: "hybrid_store_plus_live" }],
|
||||
retrievalStatus: [
|
||||
{
|
||||
fragment_id: "F1",
|
||||
requirement_ids: ["R1"],
|
||||
route: "hybrid_store_plus_live",
|
||||
status: "ok",
|
||||
result_type: "summary"
|
||||
}
|
||||
],
|
||||
retrievalResults: [{ fragment_id: "F1", status: "ok" }],
|
||||
groundingCheck: { status: "grounded" },
|
||||
droppedIntentSegments: [],
|
||||
questionTypeClass: "factual_lookup",
|
||||
companyAnchors: { companies: ["demo"] },
|
||||
runtimeAnalysisContext: {
|
||||
active: true,
|
||||
as_of_date: "2020-07-31",
|
||||
period_from: null,
|
||||
period_to: null,
|
||||
source: "eval_analysis_date",
|
||||
snapshot_mode: "auto" as const
|
||||
},
|
||||
businessScopeResolution: {
|
||||
business_scope_raw: ["company_specific_accounting"],
|
||||
business_scope_resolved: ["company_specific_accounting"],
|
||||
company_grounding_applied: true,
|
||||
scope_resolution_reason: ["resolved"]
|
||||
},
|
||||
temporalGuard: {
|
||||
raw_time_anchor: "2020-07",
|
||||
raw_time_scope: "month",
|
||||
resolved_time_anchor: "2020-07",
|
||||
resolved_primary_period: { from: "2020-07-01", to: "2020-07-31", granularity: "day" },
|
||||
effective_primary_period: { from: "2020-07-01", to: "2020-07-31", granularity: "day" },
|
||||
temporal_guard_input: "2020-07",
|
||||
temporal_alignment_status: "aligned",
|
||||
temporal_resolution_source: "analysis_context",
|
||||
temporal_guard_basis: "analysis_context",
|
||||
temporal_guard_applied: true,
|
||||
temporal_guard_outcome: "pass"
|
||||
},
|
||||
polarityAudit: {
|
||||
raw_numeric_tokens: ["60.01"],
|
||||
classified_numeric_tokens: [{ token: "60.01" }],
|
||||
rejected_as_non_accounts: [],
|
||||
resolved_account_anchors: ["60.01"]
|
||||
},
|
||||
claimAnchorAudit: {
|
||||
settlement_role: "supplier",
|
||||
settlement_role_resolution_reason: ["account_60_detected"],
|
||||
polarity_resolution_status: "resolved"
|
||||
},
|
||||
targetedEvidenceAudit: { targeted_evidence_hit_rate: 1 },
|
||||
evidenceAdmissibilityGateAudit: { admissible_evidence_count: 1 },
|
||||
rbpLiveRouteAudit: null,
|
||||
faLiveRouteAudit: null,
|
||||
groundedAnswerEligibilityGuard: { eligibility_time_basis: "analysis_context", eligible: true },
|
||||
followupStateUsage: null,
|
||||
compositionDebug: {
|
||||
problem_centric_answer_applied: true,
|
||||
problem_units_used_count: 2,
|
||||
problem_answer_mode: "stage3_lifecycle_aware_v1",
|
||||
problem_unit_ids_used: ["pu-1", "pu-2"]
|
||||
},
|
||||
addressRuntimeMetaForDeep: {
|
||||
attempted: true,
|
||||
applied: true,
|
||||
reason: "ok",
|
||||
provider: "openai",
|
||||
fallbackRuleHit: null,
|
||||
toolGateDecision: "run_address_lane",
|
||||
toolGateReason: "detected",
|
||||
predecomposeContract: { schema_version: "x" },
|
||||
orchestrationContract: { schema_version: "y" }
|
||||
},
|
||||
outcomeClassV1: "FULLY_ANSWERED",
|
||||
assistantOrchestrationContractsV1: { query_frame: {}, execution_plan: {}, evidence_bundle: {}, coverage: {} },
|
||||
answerStructureV11: { schema_version: "answer_structure_v1_1" },
|
||||
investigationStateSnapshot: { status: "active" },
|
||||
normalizedPayload: { schema_version: "normalized_query_v2_0_2" }
|
||||
};
|
||||
}
|
||||
|
||||
describe("assistant debug payload assembler", () => {
|
||||
it("builds deep debug payload with analysis context and optional sections", () => {
|
||||
const payload = buildDeepAnalysisDebugPayload(baseInput());
|
||||
|
||||
expect(payload.trace_id).toBe("trace-1");
|
||||
expect(payload.analysis_context_applied).toBe(true);
|
||||
expect(payload.analysis_context).toMatchObject({
|
||||
as_of_date: "2020-07-31",
|
||||
source: "eval_analysis_date"
|
||||
});
|
||||
expect(payload.problem_unit_ids_used).toEqual(["pu-1", "pu-2"]);
|
||||
expect(payload.address_llm_predecompose_applied).toBe(true);
|
||||
expect(payload.assistant_outcome_class_v1).toBe("FULLY_ANSWERED");
|
||||
});
|
||||
|
||||
it("omits optional fields when they are not provided", () => {
|
||||
const input = baseInput();
|
||||
input.runtimeAnalysisContext.active = false;
|
||||
input.followupStateUsage = null;
|
||||
input.compositionDebug.problem_unit_ids_used = [];
|
||||
input.rbpLiveRouteAudit = null;
|
||||
input.faLiveRouteAudit = null;
|
||||
input.addressRuntimeMetaForDeep = null;
|
||||
|
||||
const payload = buildDeepAnalysisDebugPayload(input);
|
||||
|
||||
expect(payload.analysis_context).toBeNull();
|
||||
expect(Object.prototype.hasOwnProperty.call(payload, "followup_state_usage")).toBe(false);
|
||||
expect(Object.prototype.hasOwnProperty.call(payload, "problem_unit_ids_used")).toBe(false);
|
||||
expect(payload.address_llm_predecompose_applied).toBe(false);
|
||||
expect(payload.address_llm_predecompose_contract).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,131 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { buildAssistantConversationItem, buildDeepAnswerArtifacts } from "../src/services/assistantDeepResponseAssembler";
|
||||
|
||||
describe("assistant deep response assembler", () => {
|
||||
it("strips technical tail and builds fallback answer structure when missing in composition", () => {
|
||||
const artifacts = buildDeepAnswerArtifacts({
|
||||
safeAssistantReplyBase: "Короткий ответ\n\ndebug_payload_json: {\"x\":1}",
|
||||
featureContractsV11: true,
|
||||
featureAnswerPolicyV11: true,
|
||||
compositionAnswerStructureV11: null,
|
||||
coverageReport: {
|
||||
requirements_total: 1,
|
||||
requirements_covered: 1,
|
||||
requirements_uncovered: [],
|
||||
requirements_partially_covered: [],
|
||||
clarification_needed_for: [],
|
||||
out_of_scope_requirements: []
|
||||
},
|
||||
groundingCheck: {
|
||||
status: "grounded",
|
||||
route_subject_match: true,
|
||||
missing_requirements: [],
|
||||
reasons: [],
|
||||
why_included_summary: [],
|
||||
selection_reason_summary: []
|
||||
},
|
||||
retrievalResults: []
|
||||
});
|
||||
|
||||
expect(artifacts.safeAssistantReply).toBe("Короткий ответ");
|
||||
expect(artifacts.answerStructureV11?.schema_version).toBe("answer_structure_v1_1");
|
||||
});
|
||||
|
||||
it("uses provided composition answer structure and creates assistant conversation item", () => {
|
||||
const provided = {
|
||||
schema_version: "answer_structure_v1_1",
|
||||
answer_summary: "sum",
|
||||
direct_answer: "direct",
|
||||
mechanism_block: {
|
||||
status: "grounded" as const,
|
||||
mechanism_notes: [],
|
||||
limitation_reason_codes: []
|
||||
},
|
||||
evidence_block: {
|
||||
evidence_ids: [],
|
||||
mechanism_notes: [],
|
||||
coverage_note: "ok"
|
||||
},
|
||||
uncertainty_block: {
|
||||
open_uncertainties: [],
|
||||
limitations: []
|
||||
},
|
||||
next_step_block: {
|
||||
recommended_actions: [],
|
||||
clarification_questions: []
|
||||
}
|
||||
};
|
||||
|
||||
const artifacts = buildDeepAnswerArtifacts({
|
||||
safeAssistantReplyBase: "Готово",
|
||||
featureContractsV11: true,
|
||||
featureAnswerPolicyV11: true,
|
||||
compositionAnswerStructureV11: provided as any,
|
||||
coverageReport: {
|
||||
requirements_total: 1,
|
||||
requirements_covered: 1,
|
||||
requirements_uncovered: [],
|
||||
requirements_partially_covered: [],
|
||||
clarification_needed_for: [],
|
||||
out_of_scope_requirements: []
|
||||
},
|
||||
groundingCheck: {
|
||||
status: "grounded",
|
||||
route_subject_match: true,
|
||||
missing_requirements: [],
|
||||
reasons: [],
|
||||
why_included_summary: [],
|
||||
selection_reason_summary: []
|
||||
},
|
||||
retrievalResults: []
|
||||
});
|
||||
|
||||
expect(artifacts.answerStructureV11).toEqual(provided);
|
||||
|
||||
const item = buildAssistantConversationItem({
|
||||
messageId: "msg-1",
|
||||
sessionId: "asst-1",
|
||||
text: artifacts.safeAssistantReply,
|
||||
replyType: "factual",
|
||||
traceId: "trace-1",
|
||||
debug: {
|
||||
trace_id: "trace-1",
|
||||
prompt_version: "normalizer_v2_0_2",
|
||||
schema_version: "normalized_query_v2_0_2",
|
||||
fallback_type: "none",
|
||||
route_summary: null,
|
||||
fragments: [],
|
||||
requirements_extracted: [],
|
||||
coverage_report: {
|
||||
requirements_total: 0,
|
||||
requirements_covered: 0,
|
||||
requirements_uncovered: [],
|
||||
requirements_partially_covered: [],
|
||||
clarification_needed_for: [],
|
||||
out_of_scope_requirements: []
|
||||
},
|
||||
routes: [],
|
||||
retrieval_status: [],
|
||||
retrieval_results: [],
|
||||
answer_grounding_check: {
|
||||
status: "no_grounded_answer",
|
||||
route_subject_match: false,
|
||||
missing_requirements: [],
|
||||
reasons: [],
|
||||
why_included_summary: [],
|
||||
selection_reason_summary: []
|
||||
},
|
||||
dropped_intent_segments: [],
|
||||
answer_structure_v11: null,
|
||||
investigation_state_snapshot: null,
|
||||
normalized: null
|
||||
} as any
|
||||
});
|
||||
|
||||
expect(item.message_id).toBe("msg-1");
|
||||
expect(item.session_id).toBe("asst-1");
|
||||
expect(item.reply_type).toBe("factual");
|
||||
expect(item.text).toBe("Готово");
|
||||
expect(typeof item.created_at).toBe("string");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,126 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { buildAssistantDeepTurnComposition } from "../src/services/assistantDeepTurnCompositionRuntimeAdapter";
|
||||
|
||||
describe("assistant deep turn composition runtime adapter", () => {
|
||||
it("uses followup domain hint and company-anchor period signal", () => {
|
||||
let capturedInput: Record<string, unknown> | null = null;
|
||||
const output = buildAssistantDeepTurnComposition({
|
||||
userMessage: "проверь хвосты по 60.01",
|
||||
routeSummary: null,
|
||||
retrievalResults: [],
|
||||
requirements: [],
|
||||
coverageReport: {
|
||||
requirements_total: 0,
|
||||
requirements_covered: 0,
|
||||
requirements_uncovered: [],
|
||||
requirements_partially_covered: [],
|
||||
clarification_needed_for: [],
|
||||
out_of_scope_requirements: []
|
||||
},
|
||||
groundingCheck: {
|
||||
status: "no_grounded_answer",
|
||||
route_subject_match: false,
|
||||
missing_requirements: [],
|
||||
reasons: [],
|
||||
why_included_summary: [],
|
||||
selection_reason_summary: []
|
||||
},
|
||||
followupUsage: { applied: true },
|
||||
investigationState: {
|
||||
schema_version: "investigation_state_v1",
|
||||
session_id: "asst-1",
|
||||
status: "active",
|
||||
turn_index: 1,
|
||||
updated_at: "2026-04-10T10:00:00.000Z",
|
||||
question_id: "msg-1",
|
||||
question_scope_id: null,
|
||||
scope_origin: null,
|
||||
focus: {
|
||||
domain: "settlements_60_62",
|
||||
period: null,
|
||||
primary_accounts: [],
|
||||
active_query_subject: null
|
||||
},
|
||||
narrowing_status: "unknown",
|
||||
evidence_refs: [],
|
||||
open_uncertainties: [],
|
||||
last_answer_mode: null,
|
||||
followup_context: null,
|
||||
query_mode_hint: "direct_answer"
|
||||
} as any,
|
||||
companyAnchors: {
|
||||
periods: ["2020-07"],
|
||||
dates: []
|
||||
},
|
||||
normalizedPayload: { schema_version: "normalized_query_v2_0_2" } as any,
|
||||
featureAnswerPolicyV11: true,
|
||||
featureProblemCentricAnswerV1: true,
|
||||
featureLifecycleAnswerV1: true,
|
||||
hasExplicitPeriodAnchor: () => false,
|
||||
resolveQuestionTypeFn: () => "factual_lookup",
|
||||
composeAssistantAnswerFn: ((input: Record<string, unknown>) => {
|
||||
capturedInput = input;
|
||||
return {
|
||||
assistant_reply: "ok",
|
||||
fallback_type: "none",
|
||||
reply_type: "factual"
|
||||
};
|
||||
}) as any
|
||||
});
|
||||
|
||||
expect(output.focusDomainHint).toBe("settlements_60_62");
|
||||
expect(output.questionTypeClass).toBe("factual_lookup");
|
||||
expect(output.hasPeriodInCompanyAnchors).toBe(true);
|
||||
expect(output.normalizationPeriodExplicit).toBe(true);
|
||||
expect(output.composition.reply_type).toBe("factual");
|
||||
expect(capturedInput?.focusDomainHint).toBe("settlements_60_62");
|
||||
expect(capturedInput?.normalizationPeriodExplicit).toBe(true);
|
||||
});
|
||||
|
||||
it("falls back to explicit period from normalized payload when anchors are absent", () => {
|
||||
const output = buildAssistantDeepTurnComposition({
|
||||
userMessage: "проверь закрытие",
|
||||
routeSummary: null,
|
||||
retrievalResults: [],
|
||||
requirements: [],
|
||||
coverageReport: {
|
||||
requirements_total: 0,
|
||||
requirements_covered: 0,
|
||||
requirements_uncovered: [],
|
||||
requirements_partially_covered: [],
|
||||
clarification_needed_for: [],
|
||||
out_of_scope_requirements: []
|
||||
},
|
||||
groundingCheck: {
|
||||
status: "no_grounded_answer",
|
||||
route_subject_match: false,
|
||||
missing_requirements: [],
|
||||
reasons: [],
|
||||
why_included_summary: [],
|
||||
selection_reason_summary: []
|
||||
},
|
||||
followupUsage: { applied: false },
|
||||
investigationState: null,
|
||||
companyAnchors: {
|
||||
periods: [],
|
||||
dates: []
|
||||
},
|
||||
normalizedPayload: { schema_version: "normalized_query_v2_0_2" } as any,
|
||||
featureAnswerPolicyV11: true,
|
||||
featureProblemCentricAnswerV1: true,
|
||||
featureLifecycleAnswerV1: true,
|
||||
hasExplicitPeriodAnchor: () => true,
|
||||
resolveQuestionTypeFn: () => "verification",
|
||||
composeAssistantAnswerFn: (() => ({
|
||||
assistant_reply: "ok",
|
||||
fallback_type: "none",
|
||||
reply_type: "factual"
|
||||
})) as any
|
||||
});
|
||||
|
||||
expect(output.focusDomainHint).toBeNull();
|
||||
expect(output.questionTypeClass).toBe("verification");
|
||||
expect(output.hasPeriodInCompanyAnchors).toBe(false);
|
||||
expect(output.normalizationPeriodExplicit).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,106 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { buildAssistantDeepTurnRuntimeContext } from "../src/services/assistantDeepTurnContextRuntimeAdapter";
|
||||
|
||||
describe("assistant deep turn context runtime adapter", () => {
|
||||
it("assembles context in deterministic order and propagates followup flag", () => {
|
||||
const callOrder: string[] = [];
|
||||
const companyAnchors = { accounts: ["60.01"] };
|
||||
const temporalGuard = {
|
||||
effective_primary_period: { from: "2020-07-01", to: "2020-07-31" },
|
||||
primary_period_window: { from: "2020-07-01", to: "2020-07-31" }
|
||||
};
|
||||
const businessScope = { route_summary_resolved: { mode: "deterministic_v2", decisions: [] as any[] } } as any;
|
||||
|
||||
const output = buildAssistantDeepTurnRuntimeContext({
|
||||
userMessage: "почему не закрыт 60.01",
|
||||
normalizedPayload: { schema_version: "normalized_query_v2_0_2" } as any,
|
||||
routeSummary: { mode: "deterministic_v2", decisions: [] } as any,
|
||||
runtimeAnalysisContext: {
|
||||
active: true,
|
||||
as_of_date: "2020-07-31",
|
||||
period_from: null,
|
||||
period_to: null,
|
||||
source: "analysis_context"
|
||||
},
|
||||
followupUsage: { applied: true },
|
||||
resolveCompanyAnchors: () => {
|
||||
callOrder.push("anchors");
|
||||
return companyAnchors;
|
||||
},
|
||||
resolveBusinessScopeAlignment: () => {
|
||||
callOrder.push("scope_align");
|
||||
return businessScope;
|
||||
},
|
||||
inferP0DomainFromMessage: () => {
|
||||
callOrder.push("infer_domain");
|
||||
return "settlements_60_62";
|
||||
},
|
||||
resolveTemporalGuard: (input) => {
|
||||
callOrder.push("temporal");
|
||||
expect(input.analysisContext).toEqual({
|
||||
as_of_date: "2020-07-31",
|
||||
period_from: null,
|
||||
period_to: null,
|
||||
source: "analysis_context"
|
||||
});
|
||||
return temporalGuard as any;
|
||||
},
|
||||
resolveDomainPolarityGuard: (input) => {
|
||||
callOrder.push("polarity");
|
||||
expect(input.focusDomainHint).toBe("settlements_60_62");
|
||||
return { polarity: "supplier_payable" };
|
||||
},
|
||||
resolveClaimBoundAnchors: (input) => {
|
||||
callOrder.push("claim");
|
||||
expect(input.primaryPeriod).toEqual(temporalGuard.effective_primary_period);
|
||||
return { claim_type: "prove_settlement_closure_state" } as any;
|
||||
},
|
||||
resolveBusinessScopeFromLiveContext: (input) => {
|
||||
callOrder.push("scope_live");
|
||||
expect(input.followupApplied).toBe(true);
|
||||
return {
|
||||
...businessScope,
|
||||
live_scope_used: true
|
||||
} as any;
|
||||
}
|
||||
});
|
||||
|
||||
expect(callOrder).toEqual(["anchors", "scope_align", "infer_domain", "temporal", "polarity", "claim", "scope_live"]);
|
||||
expect(output.companyAnchors).toBe(companyAnchors);
|
||||
expect(output.focusDomainForGuards).toBe("settlements_60_62");
|
||||
expect(output.claimAnchorAudit.claim_type).toBe("prove_settlement_closure_state");
|
||||
expect(output.liveTemporalHint).toEqual({
|
||||
as_of_date: "2020-07-31",
|
||||
period_from: null,
|
||||
period_to: null,
|
||||
source: "analysis_context"
|
||||
});
|
||||
});
|
||||
|
||||
it("drops unknown inferred domain and disables live temporal hint when context is inactive", () => {
|
||||
const output = buildAssistantDeepTurnRuntimeContext({
|
||||
userMessage: "какой-нибудь вопрос",
|
||||
normalizedPayload: null as any,
|
||||
routeSummary: null,
|
||||
runtimeAnalysisContext: {
|
||||
active: false,
|
||||
as_of_date: null,
|
||||
period_from: null,
|
||||
period_to: null,
|
||||
source: null
|
||||
},
|
||||
followupUsage: null,
|
||||
resolveCompanyAnchors: () => ({}),
|
||||
resolveBusinessScopeAlignment: () => ({ route_summary_resolved: null }),
|
||||
inferP0DomainFromMessage: () => "unknown_domain",
|
||||
resolveTemporalGuard: () => ({ primary_period_window: null }),
|
||||
resolveDomainPolarityGuard: () => ({ polarity: "not_applicable" }),
|
||||
resolveClaimBoundAnchors: () => ({ claim_type: "unknown" } as any),
|
||||
resolveBusinessScopeFromLiveContext: () => ({ route_summary_resolved: null })
|
||||
});
|
||||
|
||||
expect(output.focusDomainForGuards).toBeNull();
|
||||
expect(output.resolvedRouteSummary).toBeNull();
|
||||
expect(output.liveTemporalHint).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,142 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { runAssistantDeepTurnGroundingRuntime } from "../src/services/assistantDeepTurnGroundingRuntimeAdapter";
|
||||
|
||||
describe("assistant deep turn grounding runtime adapter", () => {
|
||||
it("runs audits, coverage-grounding pipeline and eligibility overlay in stable order", () => {
|
||||
const callOrder: string[] = [];
|
||||
const retrievalResults = [{ fragment_id: "F1" }] as any[];
|
||||
const coverageEvaluation = {
|
||||
requirements: [{ requirement_id: "R1" }],
|
||||
coverage: {
|
||||
requirements_total: 1,
|
||||
requirements_covered: 1,
|
||||
requirements_uncovered: [],
|
||||
requirements_partially_covered: [],
|
||||
clarification_needed_for: [],
|
||||
out_of_scope_requirements: []
|
||||
}
|
||||
} as any;
|
||||
const groundingCheckBase = {
|
||||
status: "grounded_positive",
|
||||
reasons: [],
|
||||
route_subject_match: true,
|
||||
missing_requirements: [],
|
||||
why_included_summary: [],
|
||||
selection_reason_summary: []
|
||||
} as any;
|
||||
|
||||
const output = runAssistantDeepTurnGroundingRuntime({
|
||||
claimType: "prove_settlement_closure_state",
|
||||
retrievalResults,
|
||||
rbpPlanAudit: { rbp: true },
|
||||
faPlanAudit: { fa: true },
|
||||
routeSummary: { mode: "deterministic_v2", decisions: [] } as any,
|
||||
normalizedPayload: { schema_version: "normalized_query_v2_0_2" } as any,
|
||||
userMessage: "check",
|
||||
requirementExtraction: {
|
||||
requirements: [{ requirement_id: "R1" }] as any,
|
||||
byFragment: new Map([["F1", ["R1"]]])
|
||||
} as any,
|
||||
extractRequirements: (() => {
|
||||
throw new Error("should not be called when requirementExtraction is provided");
|
||||
}) as any,
|
||||
evaluateCoverage: (() => {
|
||||
throw new Error("should not be called directly from adapter");
|
||||
}) as any,
|
||||
checkGrounding: (() => {
|
||||
throw new Error("should not be called directly from adapter");
|
||||
}) as any,
|
||||
temporalGuard: { temporal_guard_outcome: "passed" } as any,
|
||||
polarityAudit: { outcome: "passed" } as any,
|
||||
evidenceAudit: { admissible_evidence_count: 2 } as any,
|
||||
claimAnchorAudit: { claim_type: "prove_settlement_closure_state" } as any,
|
||||
targetedEvidenceHitRate: 0.5,
|
||||
businessScopeResolved: ["company_specific_accounting"],
|
||||
collectRbpLiveRouteAudit: (input) => {
|
||||
callOrder.push("rbp_audit");
|
||||
expect(input.planAudit).toEqual({ rbp: true });
|
||||
return { rbp_live: 1 };
|
||||
},
|
||||
collectFaLiveRouteAudit: (input) => {
|
||||
callOrder.push("fa_audit");
|
||||
expect(input.planAudit).toEqual({ fa: true });
|
||||
return { fa_live: 1 };
|
||||
},
|
||||
runCoverageGroundingPipelineFn: ((input: Record<string, unknown>) => {
|
||||
callOrder.push("coverage_pipeline");
|
||||
expect(input.retrievalResults).toBe(retrievalResults);
|
||||
return {
|
||||
requirementExtraction: input.requirementExtraction,
|
||||
coverageEvaluation,
|
||||
groundingCheckBase
|
||||
};
|
||||
}) as any,
|
||||
applyGroundingEligibilityFn: ((input: Record<string, unknown>) => {
|
||||
callOrder.push("eligibility");
|
||||
expect(input.groundingCheckBase).toBe(groundingCheckBase);
|
||||
return {
|
||||
groundedAnswerEligibilityGuard: {
|
||||
eligible: true
|
||||
},
|
||||
groundingCheck: groundingCheckBase
|
||||
};
|
||||
}) as any
|
||||
});
|
||||
|
||||
expect(callOrder).toEqual(["rbp_audit", "fa_audit", "coverage_pipeline", "eligibility"]);
|
||||
expect(output.rbpLiveRouteAudit).toEqual({ rbp_live: 1 });
|
||||
expect(output.faLiveRouteAudit).toEqual({ fa_live: 1 });
|
||||
expect(output.coverageEvaluation).toBe(coverageEvaluation);
|
||||
expect(output.groundedAnswerEligibilityGuard).toEqual({ eligible: true });
|
||||
expect(output.groundingCheck).toBe(groundingCheckBase);
|
||||
});
|
||||
|
||||
it("threads default pipeline output through without custom hooks", () => {
|
||||
const output = runAssistantDeepTurnGroundingRuntime({
|
||||
claimType: "unknown",
|
||||
retrievalResults: [],
|
||||
rbpPlanAudit: null,
|
||||
faPlanAudit: null,
|
||||
routeSummary: null,
|
||||
normalizedPayload: null as any,
|
||||
userMessage: "q",
|
||||
requirementExtraction: {
|
||||
requirements: [],
|
||||
byFragment: new Map()
|
||||
} as any,
|
||||
extractRequirements: () => ({
|
||||
requirements: [],
|
||||
byFragment: new Map()
|
||||
}) as any,
|
||||
evaluateCoverage: () => ({
|
||||
requirements: [],
|
||||
coverage: {
|
||||
requirements_total: 0,
|
||||
requirements_covered: 0,
|
||||
requirements_uncovered: [],
|
||||
requirements_partially_covered: [],
|
||||
clarification_needed_for: [],
|
||||
out_of_scope_requirements: []
|
||||
}
|
||||
}),
|
||||
checkGrounding: () =>
|
||||
({
|
||||
status: "no_grounded_answer",
|
||||
route_subject_match: false,
|
||||
missing_requirements: [],
|
||||
reasons: [],
|
||||
why_included_summary: [],
|
||||
selection_reason_summary: []
|
||||
}) as any,
|
||||
temporalGuard: { temporal_guard_outcome: "passed", temporal_guard_basis: "none" } as any,
|
||||
polarityAudit: { applied: false, outcome: "not_applicable" } as any,
|
||||
evidenceAudit: { admissible_evidence_count: 0 } as any,
|
||||
claimAnchorAudit: null,
|
||||
collectRbpLiveRouteAudit: () => null,
|
||||
collectFaLiveRouteAudit: () => null
|
||||
});
|
||||
|
||||
expect(output.coverageEvaluation.requirements).toEqual([]);
|
||||
expect(output.groundingCheck.status).toBe("no_grounded_answer");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,121 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
applyAssistantDeepTurnGroundingEligibility,
|
||||
applyAssistantDeepTurnRetrievalGuards
|
||||
} from "../src/services/assistantDeepTurnGuardRuntimeAdapter";
|
||||
|
||||
describe("assistant deep turn guard runtime adapter", () => {
|
||||
it("runs retrieval guards in expected order and threads outputs", () => {
|
||||
const callOrder: string[] = [];
|
||||
const seedResults = [{ fragment_id: "F1" }] as any[];
|
||||
const afterPolarity = [{ fragment_id: "P1" }] as any[];
|
||||
const afterTargeted = [{ fragment_id: "T1" }] as any[];
|
||||
const afterGate = [{ fragment_id: "G1" }] as any[];
|
||||
|
||||
const output = applyAssistantDeepTurnRetrievalGuards({
|
||||
retrievalResults: seedResults as any,
|
||||
domainPolarityGuardInitial: { applied: true, polarity: "supplier_payable" } as any,
|
||||
claimAnchorAudit: { claim_type: "prove_settlement_closure_state" } as any,
|
||||
temporalGuard: { temporal_guard_outcome: "passed", temporal_guard_basis: "none" } as any,
|
||||
focusDomainForGuards: "settlements_60_62" as any,
|
||||
companyAnchors: { accounts: ["60.01"] } as any,
|
||||
userMessage: "check settlements",
|
||||
applyDomainPolarityGuardFn: ((input: Record<string, unknown>) => {
|
||||
callOrder.push("polarity");
|
||||
expect(input.retrievalResults).toBe(seedResults);
|
||||
return {
|
||||
retrievalResults: afterPolarity,
|
||||
audit: {
|
||||
applied: true,
|
||||
polarity: "supplier_payable",
|
||||
outcome: "passed",
|
||||
reason_codes: []
|
||||
}
|
||||
};
|
||||
}) as any,
|
||||
applyTargetedEvidenceFn: ((input: Record<string, unknown>) => {
|
||||
callOrder.push("targeted");
|
||||
expect(input.retrievalResults).toBe(afterPolarity);
|
||||
return {
|
||||
retrievalResults: afterTargeted,
|
||||
audit: {
|
||||
targeted_evidence_hit_rate: 0.5,
|
||||
reason_codes: []
|
||||
}
|
||||
};
|
||||
}) as any,
|
||||
applyEvidenceAdmissibilityGateFn: ((input: Record<string, unknown>) => {
|
||||
callOrder.push("gate");
|
||||
expect(input.retrievalResults).toBe(afterTargeted);
|
||||
expect(input.polarity).toBe("supplier_payable");
|
||||
expect(input.userMessage).toBe("check settlements");
|
||||
return {
|
||||
retrievalResults: afterGate,
|
||||
audit: {
|
||||
admissible_evidence_count: 2,
|
||||
reason_codes: []
|
||||
}
|
||||
};
|
||||
}) as any
|
||||
});
|
||||
|
||||
expect(callOrder).toEqual(["polarity", "targeted", "gate"]);
|
||||
expect(output.retrievalResults).toBe(afterGate);
|
||||
expect(output.polarityGuardResult.retrievalResults).toBe(afterPolarity);
|
||||
expect(output.targetedEvidenceResult.retrievalResults).toBe(afterTargeted);
|
||||
expect(output.evidenceGateResult.retrievalResults).toBe(afterGate);
|
||||
});
|
||||
|
||||
it("evaluates grounding eligibility and applies status overlay", () => {
|
||||
const callOrder: string[] = [];
|
||||
const groundingCheckBase = {
|
||||
status: "grounded_positive",
|
||||
reasons: ["base"],
|
||||
route_subject_match: true
|
||||
};
|
||||
|
||||
const output = applyAssistantDeepTurnGroundingEligibility({
|
||||
groundingCheckBase,
|
||||
temporalGuard: { temporal_guard_outcome: "passed", temporal_guard_basis: "none" } as any,
|
||||
polarityAudit: { applied: true, outcome: "passed", polarity: "supplier_payable" } as any,
|
||||
evidenceAudit: { admissible_evidence_count: 0 } as any,
|
||||
claimAnchorAudit: { claim_anchor_resolution_rate: 1, missing_anchors: [], required_anchors: [] } as any,
|
||||
targetedEvidenceHitRate: 0,
|
||||
businessScopeResolved: ["company_specific_accounting"],
|
||||
evaluateGroundedAnswerEligibilityFn: ((input: Record<string, unknown>) => {
|
||||
callOrder.push("eligibility");
|
||||
expect(input.targetedEvidenceHitRate).toBe(0);
|
||||
return {
|
||||
eligible: false,
|
||||
temporal_passed: true,
|
||||
eligibility_time_basis: "none",
|
||||
business_scope_passed: true,
|
||||
polarity_passed: true,
|
||||
claim_anchors_passed: true,
|
||||
claim_anchor_resolution_rate: 1,
|
||||
missing_required_anchors: 0,
|
||||
admissible_evidence_count: 0,
|
||||
critical_contradiction: false,
|
||||
outcome: "limited_or_insufficient_evidence",
|
||||
grounding_mode: "limited_or_insufficient_evidence",
|
||||
reason_codes: ["admissible_evidence_count_zero"]
|
||||
};
|
||||
}) as any,
|
||||
applyEligibilityToGroundingCheckFn: ((check: Record<string, unknown>, eligibility: Record<string, unknown>) => {
|
||||
callOrder.push("overlay");
|
||||
expect(check.status).toBe("grounded_positive");
|
||||
expect(eligibility.eligible).toBe(false);
|
||||
return {
|
||||
...check,
|
||||
status: "no_grounded_answer",
|
||||
reasons: ["base", "not_enough_evidence"]
|
||||
};
|
||||
}) as any
|
||||
});
|
||||
|
||||
expect(callOrder).toEqual(["eligibility", "overlay"]);
|
||||
expect(output.groundedAnswerEligibilityGuard.eligible).toBe(false);
|
||||
expect(output.groundingCheck.status).toBe("no_grounded_answer");
|
||||
expect(output.groundingCheck.reasons).toEqual(["base", "not_enough_evidence"]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,140 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { buildAssistantDeepTurnPackagingInput, type AssistantDeepTurnInputBuilderArgs } from "../src/services/assistantDeepTurnInputBuilder";
|
||||
|
||||
function baseArgs(): AssistantDeepTurnInputBuilderArgs {
|
||||
return {
|
||||
sessionId: "asst-1",
|
||||
messageId: "msg-1",
|
||||
userMessage: "проверь 60.01",
|
||||
normalized: {
|
||||
trace_id: "trace-1",
|
||||
prompt_version: "normalizer_v2_0_2",
|
||||
schema_version: "normalized_query_v2_0_2",
|
||||
normalized: {
|
||||
schema_version: "normalized_query_v2_0_2",
|
||||
user_message_raw: "проверь 60.01",
|
||||
message_in_scope: true,
|
||||
scope_confidence: "high",
|
||||
contains_multiple_tasks: false,
|
||||
fragments: [],
|
||||
discarded_fragments: [],
|
||||
global_notes: {
|
||||
needs_clarification: false,
|
||||
clarification_reason: null
|
||||
}
|
||||
}
|
||||
},
|
||||
normalizedQuestion: "проверь 60.01",
|
||||
routeSummary: null,
|
||||
droppedIntentSegments: [],
|
||||
analysisContextForContract: null,
|
||||
executionPlan: [],
|
||||
requirementExtractionRequirements: [],
|
||||
coverageEvaluationRequirements: [],
|
||||
coverageReport: {
|
||||
requirements_total: 0,
|
||||
requirements_covered: 0,
|
||||
requirements_uncovered: [],
|
||||
requirements_partially_covered: [],
|
||||
clarification_needed_for: [],
|
||||
out_of_scope_requirements: []
|
||||
},
|
||||
groundingCheck: {
|
||||
status: "no_grounded_answer",
|
||||
route_subject_match: false,
|
||||
missing_requirements: [],
|
||||
reasons: [],
|
||||
why_included_summary: [],
|
||||
selection_reason_summary: []
|
||||
},
|
||||
retrievalCalls: [],
|
||||
retrievalResultsRaw: [],
|
||||
retrievalResults: [],
|
||||
routesForDebug: [],
|
||||
resolvedExecutionState: {},
|
||||
questionTypeClass: "factual_lookup",
|
||||
companyAnchors: {},
|
||||
runtimeAnalysisContext: {
|
||||
active: false,
|
||||
as_of_date: null,
|
||||
period_from: null,
|
||||
period_to: null,
|
||||
source: null,
|
||||
snapshot_mode: "auto"
|
||||
},
|
||||
businessScopeResolution: {},
|
||||
temporalGuard: {},
|
||||
polarityAudit: {},
|
||||
claimAnchorAudit: {},
|
||||
targetedEvidenceAudit: null,
|
||||
evidenceAdmissibilityGateAudit: null,
|
||||
rbpLiveRouteAudit: null,
|
||||
faLiveRouteAudit: null,
|
||||
groundedAnswerEligibilityGuard: {},
|
||||
followupStateUsage: undefined,
|
||||
composition: {
|
||||
reply_type: "factual",
|
||||
fallback_type: "none"
|
||||
},
|
||||
safeAssistantReplyBase: "ok",
|
||||
featureContractsV11: true,
|
||||
featureAnswerPolicyV11: true,
|
||||
investigationStateSnapshot: null,
|
||||
addressRuntimeMetaForDeep: null
|
||||
};
|
||||
}
|
||||
|
||||
describe("assistant deep turn input builder", () => {
|
||||
it("applies stable defaults for optional composition and followup fields", () => {
|
||||
const built = buildAssistantDeepTurnPackagingInput(baseArgs());
|
||||
|
||||
expect(built.followupStateUsage).toBeNull();
|
||||
expect(built.composition.answer_structure_v11).toBeNull();
|
||||
expect(built.composition.problem_centric_answer_applied).toBe(false);
|
||||
expect(built.composition.problem_units_used_count).toBe(0);
|
||||
expect(built.composition.problem_answer_mode).toBe("stage1_policy_v11");
|
||||
expect(built.composition.problem_unit_ids_used).toEqual([]);
|
||||
});
|
||||
|
||||
it("preserves explicit composition fields and normalizes unit ids array", () => {
|
||||
const args = baseArgs();
|
||||
args.followupStateUsage = { applied: true };
|
||||
args.composition.answer_structure_v11 = {
|
||||
schema_version: "answer_structure_v1_1",
|
||||
answer_summary: "sum",
|
||||
direct_answer: "direct",
|
||||
mechanism_block: {
|
||||
status: "grounded",
|
||||
mechanism_notes: [],
|
||||
limitation_reason_codes: []
|
||||
},
|
||||
evidence_block: {
|
||||
evidence_ids: [],
|
||||
source_refs: [],
|
||||
mechanism_notes: [],
|
||||
coverage_note: "ok"
|
||||
},
|
||||
uncertainty_block: {
|
||||
open_uncertainties: [],
|
||||
limitations: []
|
||||
},
|
||||
next_step_block: {
|
||||
recommended_actions: [],
|
||||
clarification_questions: []
|
||||
}
|
||||
} as any;
|
||||
args.composition.problem_centric_answer_applied = true;
|
||||
args.composition.problem_units_used_count = 3;
|
||||
args.composition.problem_answer_mode = "stage3_lifecycle_aware_v1";
|
||||
args.composition.problem_unit_ids_used = ["pu-1", "pu-2"];
|
||||
|
||||
const built = buildAssistantDeepTurnPackagingInput(args);
|
||||
|
||||
expect(built.followupStateUsage).toEqual({ applied: true });
|
||||
expect(built.composition.answer_structure_v11?.schema_version).toBe("answer_structure_v1_1");
|
||||
expect(built.composition.problem_centric_answer_applied).toBe(true);
|
||||
expect(built.composition.problem_units_used_count).toBe(3);
|
||||
expect(built.composition.problem_answer_mode).toBe("stage3_lifecycle_aware_v1");
|
||||
expect(built.composition.problem_unit_ids_used).toEqual(["pu-1", "pu-2"]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,179 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { assembleAssistantDeepTurnPackaging } from "../src/services/assistantDeepTurnPackaging";
|
||||
|
||||
function buildRetrieval() {
|
||||
return {
|
||||
fragment_id: "F1",
|
||||
requirement_ids: ["R1"],
|
||||
route: "hybrid_store_plus_live",
|
||||
status: "ok",
|
||||
result_type: "summary",
|
||||
items: [],
|
||||
summary: {},
|
||||
evidence: [],
|
||||
why_included: [],
|
||||
selection_reason: [],
|
||||
risk_factors: [],
|
||||
business_interpretation: [],
|
||||
confidence: "medium",
|
||||
limitations: [],
|
||||
errors: []
|
||||
};
|
||||
}
|
||||
|
||||
function baseInput() {
|
||||
return {
|
||||
sessionId: "asst-1",
|
||||
messageId: "msg-1",
|
||||
userMessage: "проверь хвосты по 60.01",
|
||||
normalized: {
|
||||
trace_id: "trace-1",
|
||||
prompt_version: "normalizer_v2_0_2",
|
||||
schema_version: "normalized_query_v2_0_2",
|
||||
normalized: {
|
||||
schema_version: "normalized_query_v2_0_2",
|
||||
user_message_raw: "проверь хвосты по 60.01",
|
||||
message_in_scope: true,
|
||||
scope_confidence: "high",
|
||||
contains_multiple_tasks: false,
|
||||
fragments: [{ fragment_id: "F1" }],
|
||||
discarded_fragments: [],
|
||||
global_notes: {
|
||||
needs_clarification: false,
|
||||
clarification_reason: null
|
||||
}
|
||||
}
|
||||
},
|
||||
normalizedQuestion: "проверь хвосты по 60.01",
|
||||
routeSummary: {
|
||||
mode: "deterministic_v2",
|
||||
message_in_scope: true,
|
||||
scope_confidence: "high",
|
||||
planner: {
|
||||
total_fragments: 1,
|
||||
in_scope_fragments: 1,
|
||||
out_of_scope_fragments: 0,
|
||||
discarded_fragments: 0,
|
||||
contains_multiple_tasks: false
|
||||
},
|
||||
decisions: [],
|
||||
fallback: {
|
||||
type: "none",
|
||||
message: null
|
||||
}
|
||||
},
|
||||
droppedIntentSegments: [],
|
||||
analysisContextForContract: {
|
||||
as_of_date: "2020-07-31",
|
||||
period_from: null,
|
||||
period_to: null,
|
||||
source: "eval_analysis_date",
|
||||
snapshot_mode: "auto" as const
|
||||
},
|
||||
executionPlan: [
|
||||
{
|
||||
fragment_id: "F1",
|
||||
requirement_ids: ["R1"],
|
||||
route: "hybrid_store_plus_live",
|
||||
should_execute: true,
|
||||
no_route_reason: null,
|
||||
clarification_reason: null
|
||||
}
|
||||
],
|
||||
requirementExtractionRequirements: [
|
||||
{
|
||||
requirement_id: "R1",
|
||||
source_fragment_id: "F1",
|
||||
requirement_text: "проверить хвосты 60.01",
|
||||
subject_tokens: ["account_60.01"],
|
||||
status: "covered",
|
||||
route: "hybrid_store_plus_live"
|
||||
}
|
||||
],
|
||||
coverageEvaluationRequirements: [
|
||||
{
|
||||
requirement_id: "R1",
|
||||
source_fragment_id: "F1",
|
||||
requirement_text: "проверить хвосты 60.01",
|
||||
subject_tokens: ["account_60.01"],
|
||||
status: "covered",
|
||||
route: "hybrid_store_plus_live"
|
||||
}
|
||||
],
|
||||
coverageReport: {
|
||||
requirements_total: 1,
|
||||
requirements_covered: 1,
|
||||
requirements_uncovered: [],
|
||||
requirements_partially_covered: [],
|
||||
clarification_needed_for: [],
|
||||
out_of_scope_requirements: []
|
||||
},
|
||||
groundingCheck: {
|
||||
status: "grounded",
|
||||
route_subject_match: true,
|
||||
missing_requirements: [],
|
||||
reasons: [],
|
||||
why_included_summary: [],
|
||||
selection_reason_summary: []
|
||||
},
|
||||
retrievalCalls: [{ route: "hybrid_store_plus_live" }],
|
||||
retrievalResultsRaw: [buildRetrieval()],
|
||||
retrievalResults: [buildRetrieval()],
|
||||
routesForDebug: [{ fragment_id: "F1", route: "hybrid_store_plus_live" }],
|
||||
resolvedExecutionState: { executable: 1 },
|
||||
questionTypeClass: "factual_lookup",
|
||||
companyAnchors: { companies: ["demo"] },
|
||||
runtimeAnalysisContext: {
|
||||
active: true,
|
||||
as_of_date: "2020-07-31",
|
||||
period_from: null,
|
||||
period_to: null,
|
||||
source: "eval_analysis_date",
|
||||
snapshot_mode: "auto" as const
|
||||
},
|
||||
businessScopeResolution: {
|
||||
business_scope_raw: ["company_specific_accounting"],
|
||||
business_scope_resolved: ["company_specific_accounting"],
|
||||
company_grounding_applied: true,
|
||||
scope_resolution_reason: ["resolved"]
|
||||
},
|
||||
temporalGuard: { temporal_guard_applied: true },
|
||||
polarityAudit: { resolved_account_anchors: ["60.01"] },
|
||||
claimAnchorAudit: { settlement_role: "supplier" },
|
||||
targetedEvidenceAudit: { targeted_evidence_hit_rate: 1 },
|
||||
evidenceAdmissibilityGateAudit: { admissible_evidence_count: 1 },
|
||||
rbpLiveRouteAudit: null,
|
||||
faLiveRouteAudit: null,
|
||||
groundedAnswerEligibilityGuard: { eligible: true },
|
||||
followupStateUsage: null,
|
||||
composition: {
|
||||
reply_type: "factual" as const,
|
||||
fallback_type: "none",
|
||||
answer_structure_v11: null,
|
||||
problem_centric_answer_applied: true,
|
||||
problem_units_used_count: 1,
|
||||
problem_answer_mode: "stage3_lifecycle_aware_v1",
|
||||
problem_unit_ids_used: ["pu-1"]
|
||||
},
|
||||
safeAssistantReplyBase: "Короткий ответ\n\ndebug_payload_json: {\"x\":1}",
|
||||
featureContractsV11: true,
|
||||
featureAnswerPolicyV11: true,
|
||||
investigationStateSnapshot: { status: "active" },
|
||||
addressRuntimeMetaForDeep: null
|
||||
};
|
||||
}
|
||||
|
||||
describe("assistant deep turn packaging", () => {
|
||||
it("assembles deep artifacts, debug payload and processed log in one call", () => {
|
||||
const input = baseInput();
|
||||
const output = assembleAssistantDeepTurnPackaging(input as any);
|
||||
|
||||
expect(output.deepAnswerArtifacts.safeAssistantReply).toBe("Короткий ответ");
|
||||
expect(output.contractsBundleV1.outcomeClassV1).toBe("FULLY_ANSWERED");
|
||||
expect(output.debug.trace_id).toBe("trace-1");
|
||||
expect(output.assistantItem.message_id).toBe("msg-1");
|
||||
expect(output.assistantItem.text).toBe("Короткий ответ");
|
||||
expect(output.deepAnalysisLogDetails.session_id).toBe("asst-1");
|
||||
expect(output.deepAnalysisLogDetails.message_id).toBe("msg-1");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,184 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { runAssistantDeepTurnPackagingRuntime } from "../src/services/assistantDeepTurnPackagingRuntimeAdapter";
|
||||
|
||||
function buildBaseInput() {
|
||||
return {
|
||||
featureInvestigationStateV1: true,
|
||||
sessionId: "asst-1",
|
||||
questionId: "msg-user-1",
|
||||
userMessage: "проверь кейс",
|
||||
normalized: {
|
||||
trace_id: "trace-1",
|
||||
prompt_version: "p1",
|
||||
schema_version: "s1",
|
||||
normalized: { schema_version: "normalized_query_v2_0_2" } as any
|
||||
},
|
||||
normalizedQuestion: "проверь кейс",
|
||||
routeSummary: null,
|
||||
executionPlan: [],
|
||||
requirementExtractionRequirements: [],
|
||||
coverageEvaluationRequirements: [],
|
||||
coverageReport: {
|
||||
requirements_total: 0,
|
||||
requirements_covered: 0,
|
||||
requirements_uncovered: [],
|
||||
requirements_partially_covered: [],
|
||||
clarification_needed_for: [],
|
||||
out_of_scope_requirements: []
|
||||
},
|
||||
groundingCheck: {
|
||||
status: "no_grounded_answer",
|
||||
route_subject_match: false,
|
||||
missing_requirements: [],
|
||||
reasons: [],
|
||||
why_included_summary: [],
|
||||
selection_reason_summary: []
|
||||
},
|
||||
retrievalCalls: [],
|
||||
retrievalResultsRaw: [],
|
||||
retrievalResults: [],
|
||||
questionTypeClass: "factual_lookup",
|
||||
companyAnchors: {},
|
||||
runtimeAnalysisContext: {
|
||||
active: false,
|
||||
as_of_date: null,
|
||||
period_from: null,
|
||||
period_to: null,
|
||||
source: null,
|
||||
snapshot_mode: "auto" as const
|
||||
},
|
||||
businessScopeResolution: {},
|
||||
temporalGuard: {},
|
||||
polarityAudit: {},
|
||||
claimAnchorAudit: {},
|
||||
targetedEvidenceAudit: {},
|
||||
evidenceAdmissibilityGateAudit: {},
|
||||
rbpLiveRouteAudit: null,
|
||||
faLiveRouteAudit: null,
|
||||
groundedAnswerEligibilityGuard: {},
|
||||
followupStateUsage: null,
|
||||
followupApplied: false,
|
||||
composition: {
|
||||
assistant_reply: "raw-reply",
|
||||
reply_type: "factual" as const,
|
||||
fallback_type: "none"
|
||||
},
|
||||
featureContractsV11: true,
|
||||
featureAnswerPolicyV11: true,
|
||||
previousInvestigationState: null,
|
||||
addressRuntimeMetaForDeep: null,
|
||||
extractDroppedIntentSegments: () => [],
|
||||
buildDebugRoutes: () => [],
|
||||
extractExecutionState: () => null,
|
||||
sanitizeReply: (value: string) => value,
|
||||
persistInvestigationState: () => {},
|
||||
messageIdFactory: () => "msg-fixed"
|
||||
};
|
||||
}
|
||||
|
||||
describe("assistant deep turn packaging runtime adapter", () => {
|
||||
it("executes pre-packaging, snapshot, persist, input-build and assembly in stable order", () => {
|
||||
const callOrder: string[] = [];
|
||||
let persistedByCallback = 0;
|
||||
|
||||
const output = runAssistantDeepTurnPackagingRuntime({
|
||||
...buildBaseInput(),
|
||||
persistInvestigationState: () => {
|
||||
persistedByCallback += 1;
|
||||
},
|
||||
buildPrePackagingContextFn: (() => {
|
||||
callOrder.push("pre");
|
||||
return {
|
||||
droppedIntentSegments: ["F2_dropped"],
|
||||
analysisContextForContract: null,
|
||||
routesForDebug: [{ fragment_id: "F1" }],
|
||||
resolvedExecutionState: [{ fragment_id: "F1", execution_readiness: "ready" }],
|
||||
safeAssistantReplyBase: "safe-base"
|
||||
};
|
||||
}) as any,
|
||||
buildInvestigationStateSnapshotFn: (() => {
|
||||
callOrder.push("snapshot");
|
||||
return { schema_version: "investigation_state_v1" };
|
||||
}) as any,
|
||||
persistInvestigationStateSnapshotFn: ((input: Record<string, unknown>) => {
|
||||
callOrder.push("persist");
|
||||
(input.persist as Function)(input.sessionId, input.snapshot);
|
||||
return true;
|
||||
}) as any,
|
||||
buildDeepTurnPackagingInputFn: ((input: Record<string, unknown>) => {
|
||||
callOrder.push("input");
|
||||
expect(input.messageId).toBe("msg-fixed");
|
||||
expect(input.droppedIntentSegments).toEqual(["F2_dropped"]);
|
||||
expect(input.safeAssistantReplyBase).toBe("safe-base");
|
||||
return input;
|
||||
}) as any,
|
||||
assembleDeepTurnPackagingFn: (() => {
|
||||
callOrder.push("assemble");
|
||||
return {
|
||||
deepAnswerArtifacts: {
|
||||
safeAssistantReply: "assistant-safe"
|
||||
},
|
||||
debug: { ok: true },
|
||||
assistantItem: {
|
||||
message_id: "msg-fixed",
|
||||
session_id: "asst-1",
|
||||
role: "assistant",
|
||||
text: "assistant-safe",
|
||||
reply_type: "factual",
|
||||
created_at: "2026-04-10T10:00:00.000Z",
|
||||
trace_id: "trace-1",
|
||||
debug: null
|
||||
},
|
||||
deepAnalysisLogDetails: { stage: "deep_analysis" }
|
||||
};
|
||||
}) as any
|
||||
});
|
||||
|
||||
expect(callOrder).toEqual(["pre", "snapshot", "persist", "input", "assemble"]);
|
||||
expect(persistedByCallback).toBe(1);
|
||||
expect(output.messageId).toBe("msg-fixed");
|
||||
expect(output.safeAssistantReply).toBe("assistant-safe");
|
||||
expect(output.debug).toEqual({ ok: true });
|
||||
expect(output.deepAnalysisLogDetails).toEqual({ stage: "deep_analysis" });
|
||||
});
|
||||
|
||||
it("does not persist investigation snapshot when feature is disabled", () => {
|
||||
let persistedByCallback = 0;
|
||||
|
||||
const output = runAssistantDeepTurnPackagingRuntime({
|
||||
...buildBaseInput(),
|
||||
featureInvestigationStateV1: false,
|
||||
persistInvestigationState: () => {
|
||||
persistedByCallback += 1;
|
||||
},
|
||||
buildPrePackagingContextFn: (() => ({
|
||||
droppedIntentSegments: [],
|
||||
analysisContextForContract: null,
|
||||
routesForDebug: [],
|
||||
resolvedExecutionState: null,
|
||||
safeAssistantReplyBase: "safe-base"
|
||||
})) as any,
|
||||
buildDeepTurnPackagingInputFn: ((input: Record<string, unknown>) => input) as any,
|
||||
assembleDeepTurnPackagingFn: (() => ({
|
||||
deepAnswerArtifacts: {
|
||||
safeAssistantReply: "assistant-safe"
|
||||
},
|
||||
debug: {},
|
||||
assistantItem: {
|
||||
message_id: "msg-fixed",
|
||||
session_id: "asst-1",
|
||||
role: "assistant",
|
||||
text: "assistant-safe",
|
||||
reply_type: "factual",
|
||||
created_at: "2026-04-10T10:00:00.000Z",
|
||||
trace_id: "trace-1",
|
||||
debug: null
|
||||
},
|
||||
deepAnalysisLogDetails: {}
|
||||
})) as any
|
||||
});
|
||||
|
||||
expect(output.investigationStateSnapshot).toBeNull();
|
||||
expect(persistedByCallback).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,96 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { buildAssistantDeepTurnExecutionPlan } from "../src/services/assistantDeepTurnPlanRuntimeAdapter";
|
||||
|
||||
describe("assistant deep turn plan runtime adapter", () => {
|
||||
it("builds execution plan through extraction, enforcement and guard hints in stable order", () => {
|
||||
const callOrder: string[] = [];
|
||||
const byFragment = new Map<string, string[]>([["F1", ["R1"]]]);
|
||||
const planInitial = [{ fragment_id: "F1", route: "store_canonical" }] as any[];
|
||||
const planAfterRbp = [{ fragment_id: "F1", route: "hybrid_store_plus_live" }] as any[];
|
||||
const planAfterFa = [{ fragment_id: "F1", route: "hybrid_store_plus_live", fa: true }] as any[];
|
||||
const planAfterTemporal = [{ fragment_id: "F1", route: "hybrid_store_plus_live", temporal: true }] as any[];
|
||||
const planAfterPolarity = [{ fragment_id: "F1", route: "hybrid_store_plus_live", temporal: true, polarity: true }] as any[];
|
||||
|
||||
const output = buildAssistantDeepTurnExecutionPlan({
|
||||
routeSummary: { mode: "deterministic_v2", decisions: [] } as any,
|
||||
normalizedPayload: { schema_version: "normalized_query_v2_0_2" } as any,
|
||||
userMessage: "check tails",
|
||||
claimType: "prove_settlement_closure_state",
|
||||
temporalGuard: { temporal_guard_outcome: "passed" } as any,
|
||||
domainPolarityGuardInitial: { polarity: "supplier_payable" } as any,
|
||||
extractRequirements: () => {
|
||||
callOrder.push("extract");
|
||||
return {
|
||||
requirements: [{ requirement_id: "R1" }] as any[],
|
||||
byFragment
|
||||
};
|
||||
},
|
||||
toExecutionPlan: (_routeSummary, _normalizedPayload, _userMessage, requirementByFragment) => {
|
||||
callOrder.push("plan");
|
||||
expect(requirementByFragment).toBe(byFragment);
|
||||
return planInitial as any;
|
||||
},
|
||||
enforceRbpLiveRoutePlan: ({ executionPlan }) => {
|
||||
callOrder.push("rbp");
|
||||
expect(executionPlan).toBe(planInitial);
|
||||
return {
|
||||
executionPlan: planAfterRbp as any,
|
||||
audit: { rbp: true }
|
||||
};
|
||||
},
|
||||
enforceFaLiveRoutePlan: ({ executionPlan }) => {
|
||||
callOrder.push("fa");
|
||||
expect(executionPlan).toBe(planAfterRbp);
|
||||
return {
|
||||
executionPlan: planAfterFa as any,
|
||||
audit: { fa: true }
|
||||
};
|
||||
},
|
||||
applyTemporalHintToExecutionPlan: (executionPlan) => {
|
||||
callOrder.push("temporal");
|
||||
expect(executionPlan).toBe(planAfterFa);
|
||||
return planAfterTemporal as any;
|
||||
},
|
||||
applyPolarityHintToExecutionPlan: (executionPlan) => {
|
||||
callOrder.push("polarity");
|
||||
expect(executionPlan).toBe(planAfterTemporal);
|
||||
return planAfterPolarity as any;
|
||||
}
|
||||
});
|
||||
|
||||
expect(callOrder).toEqual(["extract", "plan", "rbp", "fa", "temporal", "polarity"]);
|
||||
expect(output.requirementExtraction.byFragment).toBe(byFragment);
|
||||
expect(output.executionPlan).toBe(planAfterPolarity);
|
||||
expect(output.rbpRoutePlanEnforcement.audit).toEqual({ rbp: true });
|
||||
expect(output.faRoutePlanEnforcement.audit).toEqual({ fa: true });
|
||||
});
|
||||
|
||||
it("preserves empty execution plan end-to-end", () => {
|
||||
const output = buildAssistantDeepTurnExecutionPlan({
|
||||
routeSummary: null,
|
||||
normalizedPayload: null as any,
|
||||
userMessage: "noop",
|
||||
claimType: "unknown",
|
||||
temporalGuard: null,
|
||||
domainPolarityGuardInitial: null,
|
||||
extractRequirements: () => ({
|
||||
requirements: [],
|
||||
byFragment: new Map()
|
||||
}),
|
||||
toExecutionPlan: () => [],
|
||||
enforceRbpLiveRoutePlan: ({ executionPlan }) => ({
|
||||
executionPlan,
|
||||
audit: { rbp: false }
|
||||
}),
|
||||
enforceFaLiveRoutePlan: ({ executionPlan }) => ({
|
||||
executionPlan,
|
||||
audit: { fa: false }
|
||||
}),
|
||||
applyTemporalHintToExecutionPlan: (executionPlan) => executionPlan,
|
||||
applyPolarityHintToExecutionPlan: (executionPlan) => executionPlan
|
||||
});
|
||||
|
||||
expect(output.executionPlan).toEqual([]);
|
||||
expect(output.requirementExtraction.requirements).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,60 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { buildAssistantDeepTurnPrePackagingContext } from "../src/services/assistantDeepTurnPrePackagingContext";
|
||||
|
||||
describe("assistant deep turn pre-packaging context", () => {
|
||||
it("builds all pre-packaging fields with active analysis context", () => {
|
||||
const output = buildAssistantDeepTurnPrePackagingContext({
|
||||
normalizedPayload: { schema_version: "normalized_query_v2_0_2" } as any,
|
||||
routeSummary: null,
|
||||
runtimeAnalysisContext: {
|
||||
active: true,
|
||||
as_of_date: "2020-07-31",
|
||||
period_from: null,
|
||||
period_to: null,
|
||||
source: "eval_analysis_date",
|
||||
snapshot_mode: "auto"
|
||||
},
|
||||
assistantReply: "raw",
|
||||
extractDroppedIntentSegments: () => ["segment_1"],
|
||||
buildDebugRoutes: () => [{ fragment_id: "F1", route: "hybrid_store_plus_live" }],
|
||||
extractExecutionState: () => ({ executable: 1 }),
|
||||
sanitizeReply: (value, fallback) => `${value}::${fallback}`
|
||||
});
|
||||
|
||||
expect(output.droppedIntentSegments).toEqual(["segment_1"]);
|
||||
expect(output.analysisContextForContract).toEqual({
|
||||
as_of_date: "2020-07-31",
|
||||
period_from: null,
|
||||
period_to: null,
|
||||
source: "eval_analysis_date",
|
||||
snapshot_mode: "auto"
|
||||
});
|
||||
expect(output.routesForDebug).toEqual([{ fragment_id: "F1", route: "hybrid_store_plus_live" }]);
|
||||
expect(output.resolvedExecutionState).toEqual({ executable: 1 });
|
||||
expect(output.safeAssistantReplyBase).toContain("Нужны уточнения для надежного ответа.");
|
||||
});
|
||||
|
||||
it("returns null analysis context when runtime context is inactive", () => {
|
||||
const output = buildAssistantDeepTurnPrePackagingContext({
|
||||
normalizedPayload: { schema_version: "normalized_query_v2_0_2" } as any,
|
||||
routeSummary: null,
|
||||
runtimeAnalysisContext: {
|
||||
active: false,
|
||||
as_of_date: "2020-07-31",
|
||||
period_from: "2020-07-01",
|
||||
period_to: "2020-07-31",
|
||||
source: "eval_analysis_date",
|
||||
snapshot_mode: "auto"
|
||||
},
|
||||
assistantReply: "ok",
|
||||
extractDroppedIntentSegments: () => [],
|
||||
buildDebugRoutes: () => [],
|
||||
extractExecutionState: () => null,
|
||||
sanitizeReply: (value) => value
|
||||
});
|
||||
|
||||
expect(output.analysisContextForContract).toBeNull();
|
||||
expect(output.routesForDebug).toEqual([]);
|
||||
expect(output.safeAssistantReplyBase).toBe("ok");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,69 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { buildAssistantDeepTurnSuccessResponse } from "../src/services/assistantDeepTurnResponseBuilder";
|
||||
import type { AssistantConversationItem } from "../src/types/assistant";
|
||||
|
||||
function buildAssistantItem(): AssistantConversationItem {
|
||||
return {
|
||||
message_id: "msg-1",
|
||||
session_id: "asst-1",
|
||||
role: "assistant",
|
||||
text: "ok",
|
||||
reply_type: "factual",
|
||||
created_at: "2026-04-10T10:00:00.000Z",
|
||||
trace_id: "trace-1",
|
||||
debug: null
|
||||
};
|
||||
}
|
||||
|
||||
describe("assistant deep turn response builder", () => {
|
||||
it("builds canonical assistant message response envelope", () => {
|
||||
const assistantItem = buildAssistantItem();
|
||||
const response = buildAssistantDeepTurnSuccessResponse({
|
||||
sessionId: "asst-1",
|
||||
assistantReply: "ответ",
|
||||
replyType: "factual",
|
||||
conversationItem: assistantItem,
|
||||
debug: {
|
||||
trace_id: "trace-1",
|
||||
prompt_version: "normalizer_v2_0_2",
|
||||
schema_version: "normalized_query_v2_0_2",
|
||||
fallback_type: "none",
|
||||
route_summary: null,
|
||||
fragments: [],
|
||||
requirements_extracted: [],
|
||||
coverage_report: {
|
||||
requirements_total: 0,
|
||||
requirements_covered: 0,
|
||||
requirements_uncovered: [],
|
||||
requirements_partially_covered: [],
|
||||
clarification_needed_for: [],
|
||||
out_of_scope_requirements: []
|
||||
},
|
||||
routes: [],
|
||||
retrieval_status: [],
|
||||
retrieval_results: [],
|
||||
answer_grounding_check: {
|
||||
status: "no_grounded_answer",
|
||||
route_subject_match: false,
|
||||
missing_requirements: [],
|
||||
reasons: [],
|
||||
why_included_summary: [],
|
||||
selection_reason_summary: []
|
||||
},
|
||||
dropped_intent_segments: [],
|
||||
answer_structure_v11: null,
|
||||
investigation_state_snapshot: null,
|
||||
normalized: null
|
||||
} as any,
|
||||
conversation: [assistantItem]
|
||||
});
|
||||
|
||||
expect(response.ok).toBe(true);
|
||||
expect(response.session_id).toBe("asst-1");
|
||||
expect(response.assistant_reply).toBe("ответ");
|
||||
expect(response.reply_type).toBe("factual");
|
||||
expect(response.conversation_item.message_id).toBe("msg-1");
|
||||
expect(response.conversation).toEqual([assistantItem]);
|
||||
expect(response.debug.trace_id).toBe("trace-1");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,158 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { AssistantExecutionPlanItem } from "../src/services/assistantQueryPlanning";
|
||||
import { executeAssistantDeepTurnRetrievalPlan } from "../src/services/assistantDeepTurnRetrievalRuntimeAdapter";
|
||||
|
||||
describe("assistant deep turn retrieval runtime adapter", () => {
|
||||
it("handles skipped, executed and failed plan items with stable call records", async () => {
|
||||
const executionPlan: AssistantExecutionPlanItem[] = [
|
||||
{
|
||||
fragment_id: "F1",
|
||||
requirement_ids: ["R1"],
|
||||
route: "no_route",
|
||||
should_execute: false,
|
||||
fragment_text: "clarify period",
|
||||
no_route_reason: "insufficient_specificity",
|
||||
clarification_reason: "domain_or_scope_unclear"
|
||||
},
|
||||
{
|
||||
fragment_id: "F2",
|
||||
requirement_ids: ["R2"],
|
||||
route: "store_canonical",
|
||||
should_execute: true,
|
||||
fragment_text: "show balances",
|
||||
no_route_reason: null,
|
||||
clarification_reason: null
|
||||
},
|
||||
{
|
||||
fragment_id: "F3",
|
||||
requirement_ids: ["R3"],
|
||||
route: "live_mcp_drilldown",
|
||||
should_execute: true,
|
||||
fragment_text: "tail check",
|
||||
no_route_reason: null,
|
||||
clarification_reason: null
|
||||
}
|
||||
];
|
||||
|
||||
const normalizeCalls: Array<{ fragmentId: string; route: string; rawStatus: string | null }> = [];
|
||||
|
||||
const output = await executeAssistantDeepTurnRetrievalPlan({
|
||||
executionPlan,
|
||||
liveTemporalHint: null,
|
||||
executeRouteRuntime: async (route) => {
|
||||
if (route === "live_mcp_drilldown") {
|
||||
throw new Error("route failed");
|
||||
}
|
||||
return {
|
||||
status: "ok",
|
||||
result_type: "summary",
|
||||
items: [],
|
||||
summary: { route },
|
||||
evidence: [],
|
||||
why_included: [],
|
||||
selection_reason: [],
|
||||
risk_factors: [],
|
||||
business_interpretation: [],
|
||||
confidence: "high",
|
||||
limitations: [],
|
||||
errors: []
|
||||
};
|
||||
},
|
||||
mapNoRouteReason: (reason) => (reason === "insufficient_specificity" ? "Needs clarification." : "No-route decision."),
|
||||
buildSkippedResult: () =>
|
||||
({
|
||||
fragment_id: "F1",
|
||||
route: "no_route",
|
||||
status: "partial"
|
||||
}) as any,
|
||||
normalizeRetrievalResultFn: ((fragmentId: string, _requirementIds: string[], route: string, raw: Record<string, unknown>) => {
|
||||
normalizeCalls.push({
|
||||
fragmentId,
|
||||
route,
|
||||
rawStatus: typeof raw.status === "string" ? raw.status : null
|
||||
});
|
||||
return {
|
||||
fragment_id: fragmentId,
|
||||
route,
|
||||
status: raw.status
|
||||
} as any;
|
||||
}) as any
|
||||
});
|
||||
|
||||
expect(output.retrievalCalls).toEqual([
|
||||
{
|
||||
fragment_id: "F1",
|
||||
requirement_ids: ["R1"],
|
||||
route: "no_route",
|
||||
status: "skipped",
|
||||
query_text: "clarify period",
|
||||
reason: "Needs clarification."
|
||||
},
|
||||
{
|
||||
fragment_id: "F2",
|
||||
requirement_ids: ["R2"],
|
||||
route: "store_canonical",
|
||||
status: "executed",
|
||||
query_text: "show balances",
|
||||
reason: null
|
||||
},
|
||||
{
|
||||
fragment_id: "F3",
|
||||
requirement_ids: ["R3"],
|
||||
route: "live_mcp_drilldown",
|
||||
status: "failed",
|
||||
query_text: "tail check",
|
||||
reason: "route failed"
|
||||
}
|
||||
]);
|
||||
expect(output.retrievalResultsRaw).toHaveLength(2);
|
||||
expect((output.retrievalResultsRaw[0].raw_result as Record<string, unknown>).status).toBe("ok");
|
||||
expect((output.retrievalResultsRaw[1].raw_result as Record<string, unknown>).status).toBe("error");
|
||||
expect((output.retrievalResultsRaw[1].raw_result as Record<string, unknown>).limitations).toEqual(["Route executor failed."]);
|
||||
expect(output.retrievalResults).toHaveLength(3);
|
||||
expect(output.retrievalResults[0].status).toBe("partial");
|
||||
expect(normalizeCalls).toEqual([
|
||||
{ fragmentId: "F2", route: "store_canonical", rawStatus: "ok" },
|
||||
{ fragmentId: "F3", route: "live_mcp_drilldown", rawStatus: "error" }
|
||||
]);
|
||||
});
|
||||
|
||||
it("passes live temporal hint into route runtime execution", async () => {
|
||||
const executionPlan: AssistantExecutionPlanItem[] = [
|
||||
{
|
||||
fragment_id: "F1",
|
||||
requirement_ids: ["R1"],
|
||||
route: "hybrid_store_plus_live",
|
||||
should_execute: true,
|
||||
fragment_text: "check as of date",
|
||||
no_route_reason: null,
|
||||
clarification_reason: null
|
||||
}
|
||||
];
|
||||
let capturedTemporalHint: Record<string, unknown> | null = null;
|
||||
|
||||
await executeAssistantDeepTurnRetrievalPlan({
|
||||
executionPlan,
|
||||
liveTemporalHint: {
|
||||
as_of_date: "2020-07-31",
|
||||
period_from: null,
|
||||
period_to: null,
|
||||
source: "analysis_context"
|
||||
},
|
||||
executeRouteRuntime: async (_route, _fragmentText, options) => {
|
||||
capturedTemporalHint = options.temporalHint as unknown as Record<string, unknown>;
|
||||
return { status: "ok" };
|
||||
},
|
||||
mapNoRouteReason: () => "No-route decision.",
|
||||
buildSkippedResult: (() => ({ status: "partial" })) as any,
|
||||
normalizeRetrievalResultFn: (() => ({ status: "ok" })) as any
|
||||
});
|
||||
|
||||
expect(capturedTemporalHint).toEqual({
|
||||
as_of_date: "2020-07-31",
|
||||
period_from: null,
|
||||
period_to: null,
|
||||
source: "analysis_context"
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,105 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { UnifiedRetrievalResult } from "../src/types/assistant";
|
||||
import { assembleAssistantEvidenceBundle } from "../src/services/assistantEvidenceBundleAssembler";
|
||||
|
||||
function buildRetrieval(input?: Partial<UnifiedRetrievalResult>): UnifiedRetrievalResult {
|
||||
return {
|
||||
fragment_id: "F1",
|
||||
requirement_ids: ["R1"],
|
||||
route: "hybrid_store_plus_live",
|
||||
status: "ok",
|
||||
result_type: "summary",
|
||||
items: [],
|
||||
summary: {},
|
||||
evidence: [],
|
||||
why_included: [],
|
||||
selection_reason: [],
|
||||
risk_factors: [],
|
||||
business_interpretation: [],
|
||||
confidence: "medium",
|
||||
limitations: [],
|
||||
errors: [],
|
||||
...input
|
||||
};
|
||||
}
|
||||
|
||||
describe("assistant evidence bundle assembler", () => {
|
||||
it("builds evidence contract and retrieval status from the same retrieval set", () => {
|
||||
const assembled = assembleAssistantEvidenceBundle({
|
||||
retrievalCalls: [{ route: "hybrid_store_plus_live" }, { route: "store_canonical" }],
|
||||
retrievalResults: [
|
||||
buildRetrieval({
|
||||
fragment_id: "F1",
|
||||
requirement_ids: ["R1"],
|
||||
route: "hybrid_store_plus_live",
|
||||
status: "ok",
|
||||
evidence: [
|
||||
{
|
||||
evidence_id: "ev-1",
|
||||
claim_ref: "requirement:R1",
|
||||
source_type: "retrieval_item",
|
||||
source_ref: {
|
||||
schema_version: "evidence_source_ref_v1",
|
||||
namespace: "snapshot_2020",
|
||||
entity: "document",
|
||||
id: "doc-1",
|
||||
period: "2020-07",
|
||||
canonical_ref: "evidence_source_ref_v1|snapshot_2020|document|doc-1|2020-07"
|
||||
},
|
||||
pointer: {
|
||||
fragment_id: "F1",
|
||||
route: "hybrid_store_plus_live",
|
||||
source: {
|
||||
namespace: "snapshot_2020",
|
||||
entity: "document",
|
||||
id: "doc-1",
|
||||
period: "2020-07"
|
||||
},
|
||||
locator: {
|
||||
field_path: "amount",
|
||||
item_index: 0
|
||||
}
|
||||
},
|
||||
evidence_kind: "mechanism_link",
|
||||
mechanism_note: "signal",
|
||||
confidence: "medium",
|
||||
limitation: null,
|
||||
payload: {}
|
||||
}
|
||||
]
|
||||
}),
|
||||
buildRetrieval({
|
||||
fragment_id: "F2",
|
||||
requirement_ids: ["R2"],
|
||||
route: "store_canonical",
|
||||
status: "error",
|
||||
result_type: "list",
|
||||
errors: ["timeout"]
|
||||
})
|
||||
]
|
||||
});
|
||||
|
||||
expect(assembled.evidenceBundleContractV1.retrieval_calls_total).toBe(2);
|
||||
expect(assembled.evidenceBundleContractV1.retrieval_results_total).toBe(2);
|
||||
expect(assembled.evidenceBundleContractV1.retrieval_status_breakdown.ok).toBe(1);
|
||||
expect(assembled.evidenceBundleContractV1.retrieval_status_breakdown.error).toBe(1);
|
||||
expect(assembled.evidenceBundleContractV1.evidence_total).toBe(1);
|
||||
expect(assembled.evidenceBundleContractV1.source_refs_total).toBe(1);
|
||||
expect(assembled.retrievalStatus).toEqual([
|
||||
{
|
||||
fragment_id: "F1",
|
||||
requirement_ids: ["R1"],
|
||||
route: "hybrid_store_plus_live",
|
||||
status: "ok",
|
||||
result_type: "summary"
|
||||
},
|
||||
{
|
||||
fragment_id: "F2",
|
||||
requirement_ids: ["R2"],
|
||||
route: "store_canonical",
|
||||
status: "error",
|
||||
result_type: "list"
|
||||
}
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,158 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { RouteHintSummary } from "../src/types/normalizer";
|
||||
import type { UnifiedRetrievalResult } from "../src/types/assistant";
|
||||
import { createEmptyInvestigationState } from "../src/services/investigationState";
|
||||
import {
|
||||
buildAssistantInvestigationStateSnapshot,
|
||||
persistAssistantInvestigationStateSnapshot
|
||||
} from "../src/services/assistantInvestigationStateRuntimeAdapter";
|
||||
|
||||
function buildRouteSummary(): RouteHintSummary {
|
||||
return {
|
||||
mode: "deterministic_v2",
|
||||
message_in_scope: true,
|
||||
scope_confidence: "high",
|
||||
planner: {
|
||||
total_fragments: 1,
|
||||
in_scope_fragments: 1,
|
||||
out_of_scope_fragments: 0,
|
||||
discarded_fragments: 0,
|
||||
contains_multiple_tasks: false
|
||||
},
|
||||
decisions: [
|
||||
{
|
||||
fragment_id: "F1",
|
||||
domain_relevance: "in_scope",
|
||||
business_scope: "company_specific_accounting",
|
||||
candidate_labels: ["anomaly_probe"],
|
||||
decision_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: true,
|
||||
asks_for_anomaly_scan: true,
|
||||
asks_for_exact_object_trace: false,
|
||||
asks_for_evidence: true,
|
||||
mentions_period_close_context: false
|
||||
},
|
||||
route: "store_feature_risk",
|
||||
reason: "test-route"
|
||||
}
|
||||
],
|
||||
fallback: {
|
||||
type: "none",
|
||||
message: null
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function buildRetrievalResult(): UnifiedRetrievalResult {
|
||||
return {
|
||||
fragment_id: "F1",
|
||||
requirement_ids: ["R1"],
|
||||
route: "store_feature_risk",
|
||||
status: "ok",
|
||||
result_type: "summary",
|
||||
items: [],
|
||||
summary: {},
|
||||
evidence: [],
|
||||
why_included: [],
|
||||
selection_reason: [],
|
||||
risk_factors: [],
|
||||
business_interpretation: [],
|
||||
confidence: "medium",
|
||||
limitations: [],
|
||||
errors: []
|
||||
};
|
||||
}
|
||||
|
||||
describe("assistant investigation state runtime adapter", () => {
|
||||
it("returns null and skips persist when feature disabled", () => {
|
||||
const snapshot = buildAssistantInvestigationStateSnapshot({
|
||||
featureEnabled: false,
|
||||
previousState: createEmptyInvestigationState("asst-1", "2026-04-10T10:00:00.000Z"),
|
||||
timestamp: "2026-04-10T10:01:00.000Z",
|
||||
questionId: "msg-1",
|
||||
userMessage: "проверь 60.01",
|
||||
routeSummary: buildRouteSummary(),
|
||||
requirements: [],
|
||||
coverageReport: {
|
||||
requirements_total: 0,
|
||||
requirements_covered: 0,
|
||||
requirements_uncovered: [],
|
||||
requirements_partially_covered: [],
|
||||
clarification_needed_for: [],
|
||||
out_of_scope_requirements: []
|
||||
},
|
||||
retrievalResults: [],
|
||||
replyType: "factual",
|
||||
followupApplied: false
|
||||
});
|
||||
expect(snapshot).toBeNull();
|
||||
|
||||
let persistCalled = false;
|
||||
const persisted = persistAssistantInvestigationStateSnapshot({
|
||||
featureEnabled: false,
|
||||
sessionId: "asst-1",
|
||||
snapshot: null,
|
||||
persist: () => {
|
||||
persistCalled = true;
|
||||
}
|
||||
});
|
||||
expect(persisted).toBe(false);
|
||||
expect(persistCalled).toBe(false);
|
||||
});
|
||||
|
||||
it("builds snapshot and persists it when feature enabled", () => {
|
||||
const previous = createEmptyInvestigationState("asst-2", "2026-04-10T10:00:00.000Z");
|
||||
const snapshot = buildAssistantInvestigationStateSnapshot({
|
||||
featureEnabled: true,
|
||||
previousState: previous,
|
||||
timestamp: "2026-04-10T10:01:00.000Z",
|
||||
questionId: "msg-2",
|
||||
userMessage: "проверь счет 60.01 за 2020-07",
|
||||
routeSummary: buildRouteSummary(),
|
||||
requirements: [
|
||||
{
|
||||
requirement_id: "R1",
|
||||
source_fragment_id: "F1",
|
||||
requirement_text: "проверить счет 60.01",
|
||||
subject_tokens: ["account_60.01"],
|
||||
status: "covered",
|
||||
route: "store_feature_risk"
|
||||
}
|
||||
],
|
||||
coverageReport: {
|
||||
requirements_total: 1,
|
||||
requirements_covered: 1,
|
||||
requirements_uncovered: [],
|
||||
requirements_partially_covered: [],
|
||||
clarification_needed_for: [],
|
||||
out_of_scope_requirements: []
|
||||
},
|
||||
retrievalResults: [buildRetrievalResult()],
|
||||
replyType: "factual",
|
||||
followupApplied: false
|
||||
});
|
||||
|
||||
expect(snapshot).not.toBeNull();
|
||||
expect(snapshot?.turn_index).toBe(1);
|
||||
expect(snapshot?.question_id).toBe("msg-2");
|
||||
|
||||
let persistedSessionId: string | null = null;
|
||||
let persistedQuestionId: string | null = null;
|
||||
const persisted = persistAssistantInvestigationStateSnapshot({
|
||||
featureEnabled: true,
|
||||
sessionId: "asst-2",
|
||||
snapshot: snapshot,
|
||||
persist: (sessionId, state) => {
|
||||
persistedSessionId = sessionId;
|
||||
persistedQuestionId = state.question_id;
|
||||
}
|
||||
});
|
||||
expect(persisted).toBe(true);
|
||||
expect(persistedSessionId).toBe("asst-2");
|
||||
expect(persistedQuestionId).toBe("msg-2");
|
||||
});
|
||||
});
|
||||
@@ -209,6 +209,33 @@ describe("assistant orchestration contract", () => {
|
||||
expect(decision.livingReason).toBe("address_lane_triggered");
|
||||
});
|
||||
|
||||
it("keeps explicit address-mode unknown-intent data query in address lane", () => {
|
||||
const decision = resolveAssistantOrchestrationDecision({
|
||||
rawUserMessage:
|
||||
"Покажи контрагентов, по которым сальдо скорее всего не совпадет с их актом сверки. Может, стоит поторопиться и запросить сверку?",
|
||||
effectiveAddressUserMessage:
|
||||
"Показать контрагентов с вероятным несогласием между сальдо и актом сверки. Рекомендовать запросить сверку.",
|
||||
followupContext: null,
|
||||
llmPreDecomposeMeta: {
|
||||
applied: true,
|
||||
llmCanonicalCandidateDetected: true,
|
||||
predecomposeContract: {
|
||||
mode: "address_query",
|
||||
mode_confidence: "high",
|
||||
intent: "unknown",
|
||||
intent_confidence: "low"
|
||||
}
|
||||
} as any,
|
||||
useMock: false
|
||||
});
|
||||
|
||||
expect(decision.runAddressLane).toBe(true);
|
||||
expect(decision.toolGateDecision).toBe("run_address_lane");
|
||||
expect(decision.livingMode).toBe("address_data");
|
||||
expect(decision.livingReason).toBe("address_lane_triggered");
|
||||
expect(decision.orchestrationContract?.unsupported_address_intent_fallback_to_deep).toBe(false);
|
||||
});
|
||||
|
||||
it("does not force address lane for deep-analysis unknown intent query with date-like token", () => {
|
||||
const decision = resolveAssistantOrchestrationDecision({
|
||||
rawUserMessage: "найди какие либо ошибки на 21 мая 2022 года",
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { buildDeepAnalysisProcessedLogDetails } from "../src/services/assistantMessageLogAssembler";
|
||||
|
||||
function baseInput() {
|
||||
return {
|
||||
sessionId: "asst-1",
|
||||
messageId: "msg-1",
|
||||
userMessage: "проверь 60.01",
|
||||
normalizerOutput: { schema_version: "normalized_query_v2_0_2" },
|
||||
executionPlan: [{ fragment_id: "F1", route: "hybrid_store_plus_live", should_execute: true }],
|
||||
resolvedExecutionState: { executable: 1 },
|
||||
routes: [{ fragment_id: "F1", route: "hybrid_store_plus_live" }],
|
||||
retrievalCalls: [{ route: "hybrid_store_plus_live" }],
|
||||
retrievalResultsRaw: [{ status: "ok" }],
|
||||
retrievalResultsNormalized: [{ status: "ok" }],
|
||||
requirementsExtracted: [{ requirement_id: "R1" }],
|
||||
coverageReport: {
|
||||
requirements_total: 1,
|
||||
requirements_covered: 1,
|
||||
requirements_uncovered: [],
|
||||
requirements_partially_covered: [],
|
||||
clarification_needed_for: [],
|
||||
out_of_scope_requirements: []
|
||||
},
|
||||
groundingCheck: {
|
||||
status: "grounded",
|
||||
route_subject_match: true,
|
||||
missing_requirements: [],
|
||||
reasons: [],
|
||||
why_included_summary: ["signal"],
|
||||
selection_reason_summary: ["ranked"]
|
||||
},
|
||||
replyType: "factual",
|
||||
droppedIntentSegments: [],
|
||||
questionTypeClass: "factual_lookup",
|
||||
companyAnchors: { companies: ["demo"] },
|
||||
runtimeAnalysisContext: {
|
||||
active: true,
|
||||
as_of_date: "2020-07-31",
|
||||
period_from: null,
|
||||
period_to: null,
|
||||
source: "eval_analysis_date",
|
||||
snapshot_mode: "auto" as const
|
||||
},
|
||||
businessScopeResolution: {
|
||||
business_scope_raw: ["company_specific_accounting"],
|
||||
business_scope_resolved: ["company_specific_accounting"],
|
||||
company_grounding_applied: true,
|
||||
scope_resolution_reason: ["resolved"]
|
||||
},
|
||||
temporalGuard: {
|
||||
raw_time_anchor: "2020-07",
|
||||
raw_time_scope: "month",
|
||||
resolved_time_anchor: "2020-07",
|
||||
resolved_primary_period: { from: "2020-07-01", to: "2020-07-31", granularity: "day" },
|
||||
effective_primary_period: { from: "2020-07-01", to: "2020-07-31", granularity: "day" },
|
||||
temporal_guard_input: "2020-07",
|
||||
temporal_alignment_status: "aligned",
|
||||
temporal_resolution_source: "analysis_context",
|
||||
temporal_guard_basis: "analysis_context",
|
||||
temporal_guard_applied: true,
|
||||
temporal_guard_outcome: "pass"
|
||||
},
|
||||
polarityAudit: {
|
||||
raw_numeric_tokens: ["60.01"],
|
||||
classified_numeric_tokens: [{ token: "60.01" }],
|
||||
rejected_as_non_accounts: [],
|
||||
resolved_account_anchors: ["60.01"]
|
||||
},
|
||||
claimAnchorAudit: {
|
||||
settlement_role: "supplier",
|
||||
settlement_role_resolution_reason: ["account_60_detected"],
|
||||
polarity_resolution_status: "resolved"
|
||||
},
|
||||
targetedEvidenceAudit: { targeted_evidence_hit_rate: 1 },
|
||||
evidenceAdmissibilityGateAudit: { admissible_evidence_count: 1 },
|
||||
rbpLiveRouteAudit: null,
|
||||
faLiveRouteAudit: null,
|
||||
groundedAnswerEligibilityGuard: { eligibility_time_basis: "analysis_context", eligible: true },
|
||||
followupStateUsage: null,
|
||||
compositionDebug: {
|
||||
problem_centric_answer_applied: true,
|
||||
problem_units_used_count: 1,
|
||||
problem_answer_mode: "stage3_lifecycle_aware_v1",
|
||||
problem_unit_ids_used: ["pu-1"],
|
||||
fallback_type: "none"
|
||||
},
|
||||
outcomeClassV1: "FULLY_ANSWERED",
|
||||
assistantOrchestrationContractsV1: { query_frame: {}, execution_plan: {}, evidence_bundle: {}, coverage: {} },
|
||||
answerStructureV11: { schema_version: "answer_structure_v1_1" },
|
||||
investigationStateSnapshot: { status: "active" },
|
||||
assistantReply: "ok",
|
||||
traceId: "trace-1"
|
||||
};
|
||||
}
|
||||
|
||||
describe("assistant message log assembler", () => {
|
||||
it("builds deep analysis log details and resolves full coverage status", () => {
|
||||
const details = buildDeepAnalysisProcessedLogDetails(baseInput());
|
||||
expect(details.session_id).toBe("asst-1");
|
||||
expect(details.coverage_status).toBe("full");
|
||||
expect(details.analysis_context).toMatchObject({
|
||||
as_of_date: "2020-07-31"
|
||||
});
|
||||
expect(details.problem_unit_ids_used).toEqual(["pu-1"]);
|
||||
expect(details.reply_type).toBe("factual");
|
||||
});
|
||||
|
||||
it("marks partial coverage and omits optional sections when empty", () => {
|
||||
const input = baseInput();
|
||||
input.coverageReport.requirements_covered = 0;
|
||||
input.coverageReport.requirements_uncovered = ["R1"];
|
||||
input.runtimeAnalysisContext.active = false;
|
||||
input.followupStateUsage = null;
|
||||
input.compositionDebug.problem_unit_ids_used = [];
|
||||
|
||||
const details = buildDeepAnalysisProcessedLogDetails(input);
|
||||
expect(details.coverage_status).toBe("partial_or_limited");
|
||||
expect(details.analysis_context).toBeNull();
|
||||
expect(Object.prototype.hasOwnProperty.call(details, "followup_state_usage")).toBe(false);
|
||||
expect(Object.prototype.hasOwnProperty.call(details, "problem_unit_ids_used")).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,304 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { AnswerGroundingCheck, RequirementCoverageReport, UnifiedRetrievalResult } from "../src/types/assistant";
|
||||
import type { RouteHintSummary } from "../src/types/normalizer";
|
||||
import {
|
||||
buildAssistantCoverageContractV1,
|
||||
buildAssistantEvidenceBundleContractV1,
|
||||
buildAssistantExecutionPlanContractV1,
|
||||
buildAssistantQueryFrameContractV1,
|
||||
classifyAssistantOutcomeClassV1
|
||||
} from "../src/services/assistantOrchestrationContracts";
|
||||
|
||||
function buildCoverage(input?: Partial<RequirementCoverageReport>): RequirementCoverageReport {
|
||||
return {
|
||||
requirements_total: 1,
|
||||
requirements_covered: 0,
|
||||
requirements_uncovered: ["R1"],
|
||||
requirements_partially_covered: [],
|
||||
clarification_needed_for: [],
|
||||
out_of_scope_requirements: [],
|
||||
...input
|
||||
};
|
||||
}
|
||||
|
||||
function buildGrounding(input?: Partial<AnswerGroundingCheck>): AnswerGroundingCheck {
|
||||
return {
|
||||
status: "no_grounded_answer",
|
||||
route_subject_match: true,
|
||||
missing_requirements: ["R1"],
|
||||
reasons: [],
|
||||
why_included_summary: [],
|
||||
selection_reason_summary: [],
|
||||
...input
|
||||
};
|
||||
}
|
||||
|
||||
function buildRetrieval(input?: Partial<UnifiedRetrievalResult>): UnifiedRetrievalResult {
|
||||
return {
|
||||
fragment_id: "F1",
|
||||
requirement_ids: ["R1"],
|
||||
route: "hybrid_store_plus_live",
|
||||
status: "ok",
|
||||
result_type: "summary",
|
||||
items: [],
|
||||
summary: {},
|
||||
evidence: [],
|
||||
why_included: [],
|
||||
selection_reason: [],
|
||||
risk_factors: [],
|
||||
business_interpretation: [],
|
||||
confidence: "medium",
|
||||
limitations: [],
|
||||
errors: [],
|
||||
...input
|
||||
};
|
||||
}
|
||||
|
||||
function buildRouteSummary(): RouteHintSummary {
|
||||
return {
|
||||
mode: "deterministic_v2",
|
||||
message_in_scope: true,
|
||||
scope_confidence: "high",
|
||||
planner: {
|
||||
total_fragments: 2,
|
||||
in_scope_fragments: 2,
|
||||
out_of_scope_fragments: 0,
|
||||
discarded_fragments: 0,
|
||||
contains_multiple_tasks: false
|
||||
},
|
||||
decisions: [],
|
||||
fallback: {
|
||||
type: "none",
|
||||
message: null
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
describe("assistant orchestration contracts v1", () => {
|
||||
it("builds query frame and execution plan contracts with normalized analysis context", () => {
|
||||
const queryFrame = buildAssistantQueryFrameContractV1({
|
||||
userMessage: "Покажи хвосты по счету 60",
|
||||
normalizedQuestion: "Покажи хвосты по счету 60",
|
||||
normalized: {
|
||||
schema_version: "normalized_query_v2_0_2",
|
||||
user_message_raw: "Покажи хвосты по счету 60",
|
||||
message_in_scope: true,
|
||||
scope_confidence: "high",
|
||||
contains_multiple_tasks: false,
|
||||
fragments: [{ fragment_id: "F1" }, { fragment_id: "F2" }],
|
||||
discarded_fragments: [],
|
||||
global_notes: {
|
||||
needs_clarification: false,
|
||||
clarification_reason: null
|
||||
}
|
||||
} as any,
|
||||
routeSummary: buildRouteSummary(),
|
||||
droppedIntentSegments: ["лишний сегмент"],
|
||||
analysisContext: {
|
||||
as_of_date: "2020-07-31",
|
||||
source: "eval_analysis_date",
|
||||
snapshot_mode: "unexpected_mode"
|
||||
}
|
||||
});
|
||||
|
||||
const executionPlan = buildAssistantExecutionPlanContractV1({
|
||||
executionPlan: [
|
||||
{
|
||||
fragment_id: "F1",
|
||||
requirement_ids: ["R1"],
|
||||
route: "hybrid_store_plus_live",
|
||||
should_execute: true,
|
||||
no_route_reason: null,
|
||||
clarification_reason: null
|
||||
},
|
||||
{
|
||||
fragment_id: "F2",
|
||||
requirement_ids: ["R2"],
|
||||
route: "no_route",
|
||||
should_execute: false,
|
||||
no_route_reason: "insufficient_specificity",
|
||||
clarification_reason: "need_period"
|
||||
}
|
||||
],
|
||||
requirements: [
|
||||
{
|
||||
requirement_id: "R1",
|
||||
source_fragment_id: "F1",
|
||||
requirement_text: "req1",
|
||||
subject_tokens: [],
|
||||
status: "covered",
|
||||
route: "hybrid_store_plus_live"
|
||||
},
|
||||
{
|
||||
requirement_id: "R2",
|
||||
source_fragment_id: "F2",
|
||||
requirement_text: "req2",
|
||||
subject_tokens: [],
|
||||
status: "clarification_needed",
|
||||
route: null
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
expect(queryFrame.schema_version).toBe("assistant_query_frame_v1");
|
||||
expect(queryFrame.route_summary_mode).toBe("deterministic_v2");
|
||||
expect(queryFrame.fragments_total).toBe(2);
|
||||
expect(queryFrame.analysis_context?.as_of_date).toBe("2020-07-31");
|
||||
expect(queryFrame.analysis_context?.snapshot_mode).toBe("auto");
|
||||
expect(executionPlan.schema_version).toBe("assistant_execution_plan_v1");
|
||||
expect(executionPlan.steps).toHaveLength(2);
|
||||
expect(executionPlan.requirements_total).toBe(2);
|
||||
});
|
||||
|
||||
it("classifies fully answered and misrouted outcomes", () => {
|
||||
const fullyAnswered = classifyAssistantOutcomeClassV1({
|
||||
replyType: "factual_with_explanation",
|
||||
coverageReport: buildCoverage({
|
||||
requirements_total: 1,
|
||||
requirements_covered: 1,
|
||||
requirements_uncovered: [],
|
||||
requirements_partially_covered: [],
|
||||
clarification_needed_for: [],
|
||||
out_of_scope_requirements: []
|
||||
}),
|
||||
grounding: buildGrounding({
|
||||
status: "grounded",
|
||||
missing_requirements: []
|
||||
}),
|
||||
retrievalResults: [buildRetrieval({ status: "ok" })]
|
||||
});
|
||||
|
||||
const misrouted = classifyAssistantOutcomeClassV1({
|
||||
replyType: "route_mismatch_blocked",
|
||||
coverageReport: buildCoverage(),
|
||||
grounding: buildGrounding({
|
||||
status: "route_mismatch_blocked"
|
||||
}),
|
||||
retrievalResults: [buildRetrieval({ status: "partial" })]
|
||||
});
|
||||
|
||||
expect(fullyAnswered).toBe("FULLY_ANSWERED");
|
||||
expect(misrouted).toBe("MISROUTED");
|
||||
});
|
||||
|
||||
it("classifies tooling and entity-binding failures", () => {
|
||||
const toolingBlocked = classifyAssistantOutcomeClassV1({
|
||||
replyType: "factual",
|
||||
coverageReport: buildCoverage(),
|
||||
grounding: buildGrounding(),
|
||||
retrievalResults: [buildRetrieval({ status: "error" }), buildRetrieval({ status: "error" })]
|
||||
});
|
||||
|
||||
const entityBindingFailure = classifyAssistantOutcomeClassV1({
|
||||
replyType: "no_grounded_answer",
|
||||
coverageReport: buildCoverage({
|
||||
requirements_total: 1,
|
||||
requirements_covered: 0,
|
||||
requirements_uncovered: ["R1"]
|
||||
}),
|
||||
grounding: buildGrounding({
|
||||
status: "no_grounded_answer",
|
||||
route_subject_match: true,
|
||||
missing_requirements: ["R1"]
|
||||
}),
|
||||
retrievalResults: [buildRetrieval({ status: "empty" })]
|
||||
});
|
||||
|
||||
expect(toolingBlocked).toBe("BLOCKED_BY_TOOLING");
|
||||
expect(entityBindingFailure).toBe("FAILED_TO_BIND_ENTITIES");
|
||||
});
|
||||
|
||||
it("builds evidence bundle and coverage contracts", () => {
|
||||
const evidenceBundle = buildAssistantEvidenceBundleContractV1({
|
||||
retrievalCalls: [{ id: 1 }, { id: 2 }, { id: 3 }],
|
||||
retrievalResults: [
|
||||
buildRetrieval({
|
||||
status: "ok",
|
||||
evidence: [
|
||||
{
|
||||
evidence_id: "ev-1",
|
||||
claim_ref: "requirement:R1",
|
||||
source_type: "retrieval_item",
|
||||
source_ref: {
|
||||
schema_version: "evidence_source_ref_v1",
|
||||
namespace: "snapshot_2020",
|
||||
entity: "document",
|
||||
id: "doc-1",
|
||||
period: "2020-07",
|
||||
canonical_ref: "evidence_source_ref_v1|snapshot_2020|document|doc-1|2020-07"
|
||||
},
|
||||
pointer: {
|
||||
fragment_id: "F1",
|
||||
route: "hybrid_store_plus_live",
|
||||
source: {
|
||||
namespace: "snapshot_2020",
|
||||
entity: "document",
|
||||
id: "doc-1",
|
||||
period: "2020-07"
|
||||
},
|
||||
locator: {
|
||||
field_path: "amount",
|
||||
item_index: 0
|
||||
}
|
||||
},
|
||||
evidence_kind: "mechanism_link",
|
||||
mechanism_note: "signal",
|
||||
confidence: "medium",
|
||||
limitation: null,
|
||||
payload: {}
|
||||
}
|
||||
],
|
||||
limitations: ["needs_extra_period"]
|
||||
}),
|
||||
buildRetrieval({
|
||||
status: "partial",
|
||||
evidence: [],
|
||||
errors: ["timeout"]
|
||||
}),
|
||||
buildRetrieval({
|
||||
status: "error",
|
||||
evidence: [],
|
||||
errors: ["mcp_unavailable"]
|
||||
})
|
||||
]
|
||||
});
|
||||
|
||||
const outcomeClass = classifyAssistantOutcomeClassV1({
|
||||
replyType: "partial_coverage",
|
||||
coverageReport: buildCoverage({
|
||||
requirements_total: 2,
|
||||
requirements_covered: 1,
|
||||
requirements_uncovered: ["R2"]
|
||||
}),
|
||||
grounding: buildGrounding({
|
||||
status: "partial",
|
||||
route_subject_match: true,
|
||||
missing_requirements: ["R2"]
|
||||
}),
|
||||
retrievalResults: [buildRetrieval({ status: "ok" }), buildRetrieval({ status: "partial" })]
|
||||
});
|
||||
|
||||
const coverageContract = buildAssistantCoverageContractV1({
|
||||
coverageReport: buildCoverage({
|
||||
requirements_total: 2,
|
||||
requirements_covered: 1,
|
||||
requirements_uncovered: ["R2"]
|
||||
}),
|
||||
grounding: buildGrounding({
|
||||
status: "partial",
|
||||
missing_requirements: ["R2"]
|
||||
}),
|
||||
outcomeClass
|
||||
});
|
||||
|
||||
expect(evidenceBundle.retrieval_calls_total).toBe(3);
|
||||
expect(evidenceBundle.retrieval_results_total).toBe(3);
|
||||
expect(evidenceBundle.retrieval_status_breakdown.ok).toBe(1);
|
||||
expect(evidenceBundle.retrieval_status_breakdown.partial).toBe(1);
|
||||
expect(evidenceBundle.retrieval_status_breakdown.error).toBe(1);
|
||||
expect(evidenceBundle.evidence_total).toBe(1);
|
||||
expect(evidenceBundle.source_refs_total).toBe(1);
|
||||
expect(evidenceBundle.error_total).toBe(2);
|
||||
expect(coverageContract.outcome_class).toBe("PARTIALLY_ANSWERED");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,104 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { runAssistantCoverageGroundingPipeline } from "../src/services/assistantOrchestrationRuntimeAdapter";
|
||||
|
||||
describe("assistant orchestration runtime adapter", () => {
|
||||
it("runs requirement -> coverage -> grounding pipeline in order", () => {
|
||||
const requirementExtraction = {
|
||||
requirements: [
|
||||
{
|
||||
requirement_id: "R1",
|
||||
source_fragment_id: "F1",
|
||||
requirement_text: "req",
|
||||
subject_tokens: ["account_60"],
|
||||
status: "covered" as const,
|
||||
route: "hybrid_store_plus_live"
|
||||
}
|
||||
],
|
||||
byFragment: new Map<string, string[]>([["F1", ["R1"]]])
|
||||
};
|
||||
const coverageEvaluation = {
|
||||
requirements: requirementExtraction.requirements,
|
||||
coverage: {
|
||||
requirements_total: 1,
|
||||
requirements_covered: 1,
|
||||
requirements_uncovered: [],
|
||||
requirements_partially_covered: [],
|
||||
clarification_needed_for: [],
|
||||
out_of_scope_requirements: []
|
||||
}
|
||||
};
|
||||
const groundingCheck = {
|
||||
status: "grounded" as const,
|
||||
route_subject_match: true,
|
||||
missing_requirements: [],
|
||||
reasons: [],
|
||||
why_included_summary: ["why"],
|
||||
selection_reason_summary: ["selection"]
|
||||
};
|
||||
|
||||
const extractRequirements = vi.fn(() => requirementExtraction);
|
||||
const evaluateCoverage = vi.fn(() => coverageEvaluation);
|
||||
const checkGrounding = vi.fn(() => groundingCheck);
|
||||
|
||||
const output = runAssistantCoverageGroundingPipeline({
|
||||
routeSummary: null,
|
||||
normalized: null,
|
||||
userMessage: "test",
|
||||
retrievalResults: [],
|
||||
extractRequirements,
|
||||
evaluateCoverage,
|
||||
checkGrounding
|
||||
});
|
||||
|
||||
expect(extractRequirements).toHaveBeenCalledTimes(1);
|
||||
expect(evaluateCoverage).toHaveBeenCalledTimes(1);
|
||||
expect(checkGrounding).toHaveBeenCalledTimes(1);
|
||||
expect(output.requirementExtraction).toBe(requirementExtraction);
|
||||
expect(output.coverageEvaluation).toBe(coverageEvaluation);
|
||||
expect(output.groundingCheckBase).toBe(groundingCheck);
|
||||
});
|
||||
|
||||
it("reuses precomputed requirement extraction when provided", () => {
|
||||
const precomputed = {
|
||||
requirements: [],
|
||||
byFragment: new Map<string, string[]>()
|
||||
};
|
||||
const extractRequirements = vi.fn(() => {
|
||||
throw new Error("extractRequirements should not be called");
|
||||
});
|
||||
const evaluateCoverage = vi.fn(() => ({
|
||||
requirements: [],
|
||||
coverage: {
|
||||
requirements_total: 0,
|
||||
requirements_covered: 0,
|
||||
requirements_uncovered: [],
|
||||
requirements_partially_covered: [],
|
||||
clarification_needed_for: [],
|
||||
out_of_scope_requirements: []
|
||||
}
|
||||
}));
|
||||
const checkGrounding = vi.fn(() => ({
|
||||
status: "no_grounded_answer" as const,
|
||||
route_subject_match: true,
|
||||
missing_requirements: [],
|
||||
reasons: [],
|
||||
why_included_summary: [],
|
||||
selection_reason_summary: []
|
||||
}));
|
||||
|
||||
const output = runAssistantCoverageGroundingPipeline({
|
||||
routeSummary: null,
|
||||
normalized: null,
|
||||
userMessage: "test",
|
||||
retrievalResults: [],
|
||||
requirementExtraction: precomputed,
|
||||
extractRequirements,
|
||||
evaluateCoverage,
|
||||
checkGrounding
|
||||
});
|
||||
|
||||
expect(extractRequirements).not.toHaveBeenCalled();
|
||||
expect(evaluateCoverage).toHaveBeenCalledWith(precomputed.requirements, []);
|
||||
expect(output.requirementExtraction).toBe(precomputed);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,107 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
buildDebugRoutesFromRoute,
|
||||
buildExecutionPlanFromRoute,
|
||||
buildFragmentTextById
|
||||
} from "../src/services/assistantQueryPlanning";
|
||||
|
||||
describe("assistant query planning module", () => {
|
||||
it("builds fragment text map with account hints enrichment", () => {
|
||||
const map = buildFragmentTextById([
|
||||
{
|
||||
fragment_id: "F1",
|
||||
raw_fragment_text: "проверить хвосты",
|
||||
normalized_fragment_text: "",
|
||||
account_hints: ["60.01", "60.02"]
|
||||
},
|
||||
{
|
||||
fragment_id: "F2",
|
||||
raw_fragment_text: "проверить 60.01 по поставщику",
|
||||
normalized_fragment_text: "",
|
||||
account_hints: ["60.01"]
|
||||
}
|
||||
]);
|
||||
|
||||
expect(map.get("F1")).toBe("проверить хвосты, по счету 60.01, 60.02");
|
||||
expect(map.get("F2")).toBe("проверить 60.01 по поставщику");
|
||||
});
|
||||
|
||||
it("builds deterministic execution plan from route summary", () => {
|
||||
const executionPlan = buildExecutionPlanFromRoute({
|
||||
routeSummary: {
|
||||
mode: "deterministic_v2",
|
||||
message_in_scope: true,
|
||||
scope_confidence: "high",
|
||||
planner: {
|
||||
total_fragments: 2,
|
||||
in_scope_fragments: 2,
|
||||
out_of_scope_fragments: 0,
|
||||
discarded_fragments: 0,
|
||||
contains_multiple_tasks: false
|
||||
},
|
||||
decisions: [
|
||||
{
|
||||
fragment_id: "F1",
|
||||
route: "no_route",
|
||||
no_route_reason: "insufficient_specificity",
|
||||
clarification_reason: "missing anchor",
|
||||
reason: "needs clarification"
|
||||
},
|
||||
{
|
||||
fragment_id: "F2",
|
||||
route: "hybrid_store_plus_live",
|
||||
reason: "route selected"
|
||||
}
|
||||
],
|
||||
fallback: {
|
||||
type: "none",
|
||||
message: null
|
||||
}
|
||||
} as any,
|
||||
userMessage: "base question",
|
||||
fragmentTextById: new Map([
|
||||
["F1", "уточни период"],
|
||||
["F2", "проверь по 60.01"]
|
||||
]),
|
||||
requirementByFragment: new Map([
|
||||
["F1", ["R1"]],
|
||||
["F2", ["R2"]]
|
||||
])
|
||||
});
|
||||
|
||||
expect(executionPlan).toHaveLength(2);
|
||||
expect(executionPlan[0]).toMatchObject({
|
||||
fragment_id: "F1",
|
||||
route: "no_route",
|
||||
should_execute: false,
|
||||
no_route_reason: "insufficient_specificity",
|
||||
clarification_reason: "missing anchor"
|
||||
});
|
||||
expect(executionPlan[1]).toMatchObject({
|
||||
fragment_id: "F2",
|
||||
route: "hybrid_store_plus_live",
|
||||
should_execute: true,
|
||||
no_route_reason: null
|
||||
});
|
||||
});
|
||||
|
||||
it("builds legacy debug routes via resolver", () => {
|
||||
const routes = buildDebugRoutesFromRoute({
|
||||
routeSummary: {
|
||||
mode: "legacy_v1",
|
||||
intent_class: "partner_reconciliation",
|
||||
route_hint: "store_canonical",
|
||||
confidence: "medium"
|
||||
} as any,
|
||||
resolveLegacyRouteReason: (route) => `legacy:${route}`
|
||||
});
|
||||
|
||||
expect(routes).toHaveLength(1);
|
||||
expect(routes[0]).toMatchObject({
|
||||
fragment_id: "F1",
|
||||
route: "store_canonical",
|
||||
reason: "legacy:store_canonical",
|
||||
confidence: "medium"
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,93 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { AssistantConversationItem, AssistantSessionState } from "../src/types/assistant";
|
||||
import { commitAssistantTurnAndLog } from "../src/services/assistantTurnCommitRuntimeAdapter";
|
||||
|
||||
function buildAssistantItem(): AssistantConversationItem {
|
||||
return {
|
||||
message_id: "msg-1",
|
||||
session_id: "asst-1",
|
||||
role: "assistant",
|
||||
text: "ok",
|
||||
reply_type: "factual",
|
||||
created_at: "2026-04-10T10:01:00.000Z",
|
||||
trace_id: "trace-1",
|
||||
debug: null
|
||||
};
|
||||
}
|
||||
|
||||
describe("assistant turn commit runtime adapter", () => {
|
||||
it("appends item, persists existing session, clones conversation and logs event", () => {
|
||||
const assistantItem = buildAssistantItem();
|
||||
const storedSession: AssistantSessionState = {
|
||||
session_id: "asst-1",
|
||||
updated_at: "2026-04-10T10:01:00.000Z",
|
||||
items: [assistantItem],
|
||||
investigation_state: null
|
||||
};
|
||||
|
||||
const calls = {
|
||||
append: 0,
|
||||
persist: 0,
|
||||
log: 0
|
||||
};
|
||||
let loggedPayload: Record<string, unknown> | null = null;
|
||||
|
||||
const result = commitAssistantTurnAndLog({
|
||||
sessionId: "asst-1",
|
||||
assistantItem,
|
||||
eventType: "assistant_message",
|
||||
logDetails: { some: "details" },
|
||||
appendItem: () => {
|
||||
calls.append += 1;
|
||||
},
|
||||
getSession: () => storedSession,
|
||||
persistSession: () => {
|
||||
calls.persist += 1;
|
||||
},
|
||||
cloneConversation: (items) => items.map((item) => ({ ...item, debug: item.debug ? { ...item.debug } : null })),
|
||||
logEvent: (payload) => {
|
||||
calls.log += 1;
|
||||
loggedPayload = payload as unknown as Record<string, unknown>;
|
||||
},
|
||||
nowIso: () => "2026-04-10T10:02:00.000Z"
|
||||
});
|
||||
|
||||
expect(calls.append).toBe(1);
|
||||
expect(calls.persist).toBe(1);
|
||||
expect(calls.log).toBe(1);
|
||||
expect(result.currentSession?.session_id).toBe("asst-1");
|
||||
expect(result.conversation).toEqual([assistantItem]);
|
||||
expect(result.conversation).not.toBe(storedSession.items);
|
||||
expect(loggedPayload?.["sessionId"]).toBe("asst-1");
|
||||
expect(loggedPayload?.["eventType"]).toBe("assistant_message");
|
||||
expect(loggedPayload?.["message"]).toBe("assistant_message_processed");
|
||||
expect(loggedPayload?.["timestamp"]).toBe("2026-04-10T10:02:00.000Z");
|
||||
});
|
||||
|
||||
it("skips persist when session is missing and still logs with empty conversation", () => {
|
||||
const assistantItem = buildAssistantItem();
|
||||
let persistCalled = false;
|
||||
let logCalled = false;
|
||||
|
||||
const result = commitAssistantTurnAndLog({
|
||||
sessionId: "asst-missing",
|
||||
assistantItem,
|
||||
eventType: "assistant_message",
|
||||
logDetails: { x: 1 },
|
||||
appendItem: () => {},
|
||||
getSession: () => null,
|
||||
persistSession: () => {
|
||||
persistCalled = true;
|
||||
},
|
||||
cloneConversation: (items) => items.map((item) => ({ ...item })),
|
||||
logEvent: () => {
|
||||
logCalled = true;
|
||||
}
|
||||
});
|
||||
|
||||
expect(persistCalled).toBe(false);
|
||||
expect(logCalled).toBe(true);
|
||||
expect(result.currentSession).toBeNull();
|
||||
expect(result.conversation).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -11,7 +11,8 @@ const FLAG_KEYS = [
|
||||
"FEATURE_ASSISTANT_ANSWER_POLICY_V11",
|
||||
"FEATURE_ASSISTANT_PROBLEM_CENTRIC_ANSWER_V1",
|
||||
"FEATURE_ASSISTANT_PROBLEM_UNIT_CONTINUITY_V1",
|
||||
"FEATURE_ASSISTANT_PROBLEM_UNITS_V1"
|
||||
"FEATURE_ASSISTANT_PROBLEM_UNITS_V1",
|
||||
"FEATURE_ASSISTANT_ADDRESS_QUERY_V1"
|
||||
] as const;
|
||||
|
||||
const ORIGINAL_FLAGS: Record<string, string | undefined> = Object.fromEntries(
|
||||
@@ -623,6 +624,7 @@ describe("wave10 settlement corrective regression", () => {
|
||||
process.env.FEATURE_ASSISTANT_PROBLEM_CENTRIC_ANSWER_V1 = "1";
|
||||
process.env.FEATURE_ASSISTANT_PROBLEM_UNIT_CONTINUITY_V1 = "1";
|
||||
process.env.FEATURE_ASSISTANT_PROBLEM_UNITS_V1 = "1";
|
||||
process.env.FEATURE_ASSISTANT_ADDRESS_QUERY_V1 = "0";
|
||||
|
||||
vi.resetModules();
|
||||
const { createApp } = await import("../src/server");
|
||||
|
||||
@@ -31,4 +31,9 @@ describe("questionTypeResolver", () => {
|
||||
expect(resolveQuestionType("Почему не сходится 62.01/62.02?"))
|
||||
.toBe("why_breaks");
|
||||
});
|
||||
|
||||
it("keeps generic non-why questions as unknown", () => {
|
||||
expect(resolveQuestionType("Какие реализации стоит проверить заранее, чтобы не испортить отчетность за месяц?"))
|
||||
.toBe("unknown");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user