import { describe, expect, it, vi } from "vitest"; import { buildAssistantDeepTurnNormalizationRuntime } from "../src/services/assistantDeepTurnNormalizationRuntimeAdapter"; describe("assistant deep turn normalization runtime adapter", () => { it("uses followup state binding when feature flags are enabled and state exists", async () => { const followupBinding = { normalizedQuestion: "normalized question", mergedContext: { period_hint: "2020-07", business_context: "ctx-from-followup" }, usage: { applied: true } }; const buildFollowupStateBinding = vi.fn(() => followupBinding); const normalize = vi.fn(async (request) => ({ trace_id: "trace-1", ok: true, normalized: { schema_version: "normalized_query_v2_0_2" } as any, route_hint_summary: null, raw_model_output: {}, validation: { passed: true, errors: [] }, usage: { input_tokens: 10, output_tokens: 20, total_tokens: 30 }, latency_ms: 7, prompt_version: String(request.promptVersion ?? ""), schema_version: "normalized_query_v2_0_2", request_count_for_case: 1 })); const runtime = await buildAssistantDeepTurnNormalizationRuntime({ userMessage: "raw question", payload: { llmProvider: "openai", apiKey: "k", model: "m", baseUrl: "https://api.example.com", temperature: 0.2, maxOutputTokens: 333, promptVersion: "normalizer_v2_0_2", systemPrompt: "sys", developerPrompt: "dev", domainPrompt: "dom", fewShotExamples: "few", context: { period_hint: "2020-06" }, useMock: true }, featureInvestigationStateV1: true, featureStateFollowupBindingV1: true, sessionInvestigationState: { active_domain: "settlements_60_62" }, buildFollowupStateBinding, normalize }); expect(buildFollowupStateBinding).toHaveBeenCalledTimes(1); expect(normalize).toHaveBeenCalledTimes(1); expect(normalize).toHaveBeenCalledWith({ llmProvider: "openai", apiKey: "k", model: "m", baseUrl: "https://api.example.com", temperature: 0.2, maxOutputTokens: 333, promptVersion: "normalizer_v2_0_2", schemaVersion: "v2_0_2", systemPrompt: "sys", developerPrompt: "dev", domainPrompt: "dom", fewShotExamples: "few", userQuestion: "normalized question", context: { period_hint: "2020-07", business_context: "ctx-from-followup" }, useMock: true }); expect(runtime.followupBinding).toBe(followupBinding); expect(runtime.normalizePayload.userQuestion).toBe("normalized question"); }); it("falls back to raw user message when followup binding is disabled", async () => { const buildFollowupStateBinding = vi.fn(); const normalize = vi.fn(async () => ({ trace_id: "trace-2", ok: true, normalized: { schema_version: "normalized_query_v2_0_2" } as any, route_hint_summary: null, raw_model_output: {}, validation: { passed: true, errors: [] }, usage: { input_tokens: 1, output_tokens: 1, total_tokens: 2 }, latency_ms: 1, prompt_version: "address_query_runtime_v1", schema_version: "normalized_query_v2_0_2", request_count_for_case: 1 })); const runtime = await buildAssistantDeepTurnNormalizationRuntime({ userMessage: "raw fallback", payload: { llmProvider: "openai", context: { business_context: "payload-context" }, useMock: undefined }, featureInvestigationStateV1: false, featureStateFollowupBindingV1: true, sessionInvestigationState: { some: "state" }, buildFollowupStateBinding, normalize }); expect(buildFollowupStateBinding).not.toHaveBeenCalled(); expect(normalize).toHaveBeenCalledWith({ llmProvider: "openai", apiKey: undefined, model: undefined, baseUrl: undefined, temperature: undefined, maxOutputTokens: undefined, promptVersion: "normalizer_v2_0_2", schemaVersion: "v2_0_2", systemPrompt: undefined, developerPrompt: undefined, domainPrompt: undefined, fewShotExamples: undefined, userQuestion: "raw fallback", context: { business_context: "payload-context" }, useMock: false }); expect(runtime.followupBinding).toEqual({ normalizedQuestion: "raw fallback", mergedContext: { business_context: "payload-context" }, usage: null }); }); it("synthesizes route summary from normalized payload when normalize omits it", async () => { const normalize = vi.fn(async () => ({ trace_id: "trace-3", ok: true, normalized: { schema_version: "normalized_query_v2_0_2", user_message_raw: "Разложи хвосты по поставщикам", message_in_scope: true, scope_confidence: "high", contains_multiple_tasks: false, discarded_fragments: [], global_notes: { needs_clarification: false, clarification_reason: null }, fragments: [ { fragment_id: "F1", raw_fragment_text: "Разложи хвосты по поставщикам: где разрыв между оплатой и документами выглядит системным.", normalized_fragment_text: "Разложи хвосты по поставщикам: где разрыв между оплатой и документами выглядит системным.", domain_relevance: "in_scope", business_scope: "company_specific_accounting", entity_hints: ["контрагент", "документ"], account_hints: ["60"], document_hints: ["документ"], register_hints: [], time_scope: { type: "missing", value: null, confidence: "low" }, flags: { has_multi_entity_scope: true, asks_for_chain_explanation: true, asks_for_ranking_or_top: false, asks_for_period_summary: false, asks_for_rule_check: false, asks_for_anomaly_scan: true, asks_for_exact_object_trace: false, asks_for_evidence: true, mentions_period_close_context: false }, semantic_hints: { scope_target_kind: "none", scope_target_text: null, date_scope_kind: "missing", self_scope_detected: false, selected_object_scope_detected: false }, candidate_labels: ["cross_entity", "anomaly_probe"], confidence: "medium", execution_readiness: "executable_with_soft_assumptions", clarification_reason: null, soft_assumption_used: ["problem_scan_mode_enabled"], route_status: "routed", no_route_reason: null } ] }, route_hint_summary: null, raw_model_output: {}, validation: { passed: true, errors: [] }, usage: { input_tokens: 1, output_tokens: 1, total_tokens: 2 }, latency_ms: 1, prompt_version: "normalizer_v2_0_2", schema_version: "normalized_query_v2_0_2", request_count_for_case: 1 })); const runtime = await buildAssistantDeepTurnNormalizationRuntime({ userMessage: "Разложи хвосты по поставщикам", payload: { llmProvider: "openai", promptVersion: "address_query_runtime_v1", useMock: true }, featureInvestigationStateV1: false, featureStateFollowupBindingV1: false, sessionInvestigationState: null, buildFollowupStateBinding: vi.fn(), normalize }); expect(runtime.normalized.route_hint_summary).toEqual( expect.objectContaining({ mode: "deterministic_v2", fallback: expect.objectContaining({ type: "none" }), decisions: [ expect.objectContaining({ fragment_id: "F1", route: "hybrid_store_plus_live" }) ] }) ); expect(normalize).toHaveBeenCalledWith( expect.objectContaining({ promptVersion: "normalizer_v2_0_2", schemaVersion: "v2_0_2" }) ); }); });