NODEDC_1C/llm_normalizer/backend/tests/assistantTurnRuntimeInputBu...

166 lines
6.9 KiB
TypeScript

import { describe, expect, it, vi } from "vitest";
import {
buildAssistantAddressAttemptRuntimeInput,
buildAssistantDeepTurnAttemptRuntimeInput,
buildAssistantUserTurnBootstrapRuntimeInput
} from "../src/services/assistantTurnRuntimeInputBuilder";
function buildDeps(overrides: Record<string, unknown> = {}) {
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);
});
});