import { describe, expect, it, vi } from "vitest"; import { buildAssistantAddressAttemptRuntimeInput, buildAssistantDeepTurnAttemptRuntimeInput, buildAssistantUserTurnBootstrapRuntimeInput } from "../src/services/assistantTurnRuntimeInputBuilder"; function buildDeps(overrides: Record = {}) { const noop = vi.fn(() => null); return { ensureSession: vi.fn(() => ({ session_id: "asst-1", items: [], investigation_state: null })), appendItem: vi.fn(), getSession: vi.fn(() => ({ session_id: "asst-1", items: [], investigation_state: null })), persistSession: vi.fn(), setInvestigationState: vi.fn(), normalize: vi.fn(async () => ({})), executeRouteRuntime: vi.fn(async () => ({})), tryAddressQueryHandle: vi.fn(async () => ({ response_type: "READY" })), chatClient: {}, messageIdFactory: vi.fn(() => "msg-1"), nowIso: vi.fn(() => "2026-04-11T00:00:00.000Z"), defaultApiKey: "key", logEvent: vi.fn(), featureAssistantAddressQueryV1: true, featureAddressLlmPredecomposeV1: true, featureInvestigationStateV1: true, featureStateFollowupBindingV1: true, featureContractsV11: true, featureAnswerPolicyV11: true, featureProblemCentricAnswerV1: true, featureLifecycleAnswerV1: true, defaultModel: "gpt-5", defaultBaseUrl: "http://localhost", compactWhitespace: vi.fn((value: unknown) => String(value ?? "").trim()), repairAddressMojibake: vi.fn((value: unknown) => String(value ?? "")), resolveRuntimeAnalysisContext: vi.fn(() => ({ as_of_date: "2020-07-31" })), runAddressLlmPreDecompose: vi.fn(async () => ({})), buildAddressLlmPredecomposeContractV1: noop, sanitizeAddressMessageForFallback: noop, toNonEmptyString: vi.fn((value: unknown) => typeof value === "string" && value.trim().length > 0 ? value.trim() : null ), resolveAddressFollowupCarryoverContext: noop, resolveAssistantOrchestrationDecision: noop, buildAddressDialogContinuationContractV2: noop, mergeFollowupContextWithOrganizationScope: noop, isRetryableAddressLimitedResult: noop, mergeKnownOrganizations: noop, hasAssistantDataScopeMetaQuestionSignal: noop, shouldHandleAsAssistantCapabilityMetaQuery: noop, hasDestructiveDataActionSignal: noop, hasDangerOrCoercionSignal: noop, hasOperationalAdminActionRequestSignal: noop, hasOrganizationFactLookupSignal: noop, hasOrganizationFactFollowupSignal: noop, shouldEmitOrganizationSelectionReply: noop, hasAssistantCapabilityQuestionSignal: noop, resolveDataScopeProbe: vi.fn(() => null), applyScriptGuard: noop, applyGroundingGuard: noop, buildAssistantSafetyRefusalReply: noop, buildAssistantDataScopeContractReply: noop, buildAssistantOrganizationFactBoundaryReply: noop, buildAssistantDataScopeSelectionReply: noop, buildAssistantOperationalBoundaryReply: noop, buildAssistantCapabilityContractReply: noop, loadAssistantCanonExcerpt: noop, sanitizeOutgoingAssistantText: vi.fn((value: unknown, fallback = "") => { const text = typeof value === "string" ? value.trim() : ""; return text || fallback; }), buildAddressDebugPayload: noop, buildAddressFollowupOffer: noop, buildFollowupStateBinding: noop, resolveBusinessScopeAlignment: noop, inferP0DomainFromMessage: noop, resolveBusinessScopeFromLiveContext: noop, extractRequirements: noop, toExecutionPlan: noop, enforceRbpLiveRoutePlan: noop, enforceFaLiveRoutePlan: noop, mapNoRouteReason: noop, buildSkippedResult: noop, evaluateCoverage: noop, checkGrounding: noop, collectRbpLiveRouteAudit: noop, collectFaLiveRouteAudit: noop, hasExplicitPeriodAnchorFromNormalized: noop, extractDroppedIntentSegments: noop, toDebugRoutes: noop, extractExecutionState: noop, ...overrides } as any; } describe("assistant turn runtime input builder", () => { it("builds bootstrap runtime input from shared deps", () => { const deps = buildDeps(); const payload = { session_id: "asst-1", user_message: "hello" }; const runtimeInput = buildAssistantUserTurnBootstrapRuntimeInput(payload, deps); expect(runtimeInput.payload).toBe(payload); expect(runtimeInput.ensureSession).toBe(deps.ensureSession); expect(runtimeInput.messageIdFactory?.()).toBe("msg-1"); expect(runtimeInput.nowIso?.()).toBe("2026-04-11T00:00:00.000Z"); }); it("builds address attempt input and preserves address context mapping", async () => { const runAddressLlmPreDecompose = vi.fn(async () => ({ mode: "supported" })); const deps = buildDeps({ runAddressLlmPreDecompose }); const runtimeInput = { payload: { context: { period_hint: "2020-07-31" } }, sessionId: "asst-1", userMessage: "где хвост", sessionItems: [], runtimeAnalysisContext: { as_of_date: "2020-07-31" }, sessionOrganizationScope: { knownOrganizations: ["Org A"], selectedOrganization: "Org A", activeOrganization: "Org A" } } as any; const built = buildAssistantAddressAttemptRuntimeInput(runtimeInput, deps); const predecompose = await built.runAddressLlmPreDecompose(); await built.runAddressQueryTryHandle("message", { analysisDateHint: "2020-07-31" }); expect(predecompose).toEqual({ mode: "supported" }); expect(runAddressLlmPreDecompose).toHaveBeenCalledWith(runtimeInput.payload, "где хвост"); expect(built.runtimeAnalysisContextAsOfDate).toBe("2020-07-31"); expect(built.sessionScope).toEqual(runtimeInput.sessionOrganizationScope); expect(deps.tryAddressQueryHandle).toHaveBeenCalledWith("message", { analysisDateHint: "2020-07-31" }); }); it("builds deep attempt input with shared guards and state hooks", () => { const setInvestigationState = vi.fn(); const sanitizeOutgoingAssistantText = vi.fn(() => "safe"); const deps = buildDeps({ setInvestigationState, sanitizeOutgoingAssistantText }); const runtimeInput = { payload: { useMock: true }, sessionId: "asst-1", questionId: "msg-q1", userMessage: "почему долг не закрыт", runtimeAnalysisContext: { as_of_date: "2020-07-31" }, sessionInvestigationState: { scope: "settlements_60_62" }, addressRuntimeMetaForDeep: { attempted: true } } as any; const built = buildAssistantDeepTurnAttemptRuntimeInput(runtimeInput, deps); const sanitized = built.sanitizeReply(" raw ", "fallback"); built.persistInvestigationState("asst-1", { scope: "next" }); expect(sanitized).toBe("safe"); expect(sanitizeOutgoingAssistantText).toHaveBeenCalledWith(" raw ", "fallback"); expect(setInvestigationState).toHaveBeenCalledWith("asst-1", { scope: "next" }); expect(built.sessionId).toBe("asst-1"); expect(built.questionId).toBe("msg-q1"); expect(built.addressRuntimeMetaForDeep).toEqual({ attempted: true }); expect(built.featureContractsV11).toBe(true); }); });