ГЛОБАЛЬНЫЙ РЕФАКТОРИНГ АРХИТЕКТУРЫ - Рефакторинг этапов 2.28 - вынос retry-orchestration адресного лейна (контекстный прогон, primary, retry сырого вопроса, fallback limited) в отдельный адаптер.
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
runAssistantAddressLaneRuntime,
|
||||
type AssistantAddressFollowupCarryoverLike,
|
||||
type AssistantAddressLaneLike
|
||||
} from "../src/services/assistantAddressLaneRuntimeAdapter";
|
||||
|
||||
function limitedLane(category: string): AssistantAddressLaneLike {
|
||||
return {
|
||||
handled: true,
|
||||
debug: {
|
||||
limited_reason_category: category
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function factualLane(): AssistantAddressLaneLike {
|
||||
return {
|
||||
handled: true,
|
||||
debug: {}
|
||||
};
|
||||
}
|
||||
|
||||
function unhandledLane(): AssistantAddressLaneLike {
|
||||
return {
|
||||
handled: false,
|
||||
debug: {}
|
||||
};
|
||||
}
|
||||
|
||||
describe("assistant address lane runtime adapter", () => {
|
||||
it("returns contextual lane immediately when preferred contextual attempt is factual", async () => {
|
||||
const carryover: AssistantAddressFollowupCarryoverLike = { followupContext: { scope: "ctx" } };
|
||||
const runAddressLaneAttempt = vi.fn(async () => factualLane());
|
||||
|
||||
const result = await runAssistantAddressLaneRuntime({
|
||||
userMessage: "сырой вопрос",
|
||||
addressInputMessage: "нормализованный вопрос",
|
||||
carryover,
|
||||
shouldPreferContextualLane: true,
|
||||
canRetryWithRawUserMessage: true,
|
||||
runAddressLaneAttempt,
|
||||
isRetryableAddressLimitedResult: (lane) => Boolean(lane?.debug?.limited_reason_category)
|
||||
});
|
||||
|
||||
expect(result.handled).toBe(true);
|
||||
expect(result.selection?.messageUsed).toBe("нормализованный вопрос");
|
||||
expect(result.selection?.carryMeta).toBe(carryover);
|
||||
expect(result.retryAudit.attempted).toBe(false);
|
||||
expect(runAddressLaneAttempt).toHaveBeenCalledTimes(1);
|
||||
expect(runAddressLaneAttempt).toHaveBeenCalledWith("нормализованный вопрос", carryover);
|
||||
});
|
||||
|
||||
it("retries with raw message after limited result and returns factual retry", async () => {
|
||||
const carryover: AssistantAddressFollowupCarryoverLike = { followupContext: { scope: "ctx" } };
|
||||
const runAddressLaneAttempt = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce(limitedLane("empty_match")) // primary
|
||||
.mockResolvedValueOnce(limitedLane("empty_match")) // contextual
|
||||
.mockResolvedValueOnce(factualLane()); // raw contextual retry
|
||||
|
||||
const result = await runAssistantAddressLaneRuntime({
|
||||
userMessage: "сырой вопрос",
|
||||
addressInputMessage: "нормализованный вопрос",
|
||||
carryover,
|
||||
shouldPreferContextualLane: false,
|
||||
canRetryWithRawUserMessage: true,
|
||||
runAddressLaneAttempt,
|
||||
isRetryableAddressLimitedResult: (lane) => Boolean(lane?.debug?.limited_reason_category)
|
||||
});
|
||||
|
||||
expect(result.handled).toBe(true);
|
||||
expect(result.selection?.messageUsed).toBe("сырой вопрос");
|
||||
expect(result.selection?.carryMeta).toBe(carryover);
|
||||
expect(result.retryAudit.attempted).toBe(true);
|
||||
expect(result.retryAudit.reason).toBe("limited_result_retry_with_raw_message");
|
||||
expect(result.retryAudit.initial_limited_category).toBe("empty_match");
|
||||
expect(result.retryAudit.retry_used_followup_context).toBe(true);
|
||||
expect(result.retryAudit.retry_result_category).toBe(null);
|
||||
expect(runAddressLaneAttempt).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
it("returns pending limited result when retry is disabled", async () => {
|
||||
const runAddressLaneAttempt = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce(limitedLane("missing_anchor")) // primary
|
||||
.mockResolvedValueOnce(unhandledLane()); // contextual fallback
|
||||
|
||||
const result = await runAssistantAddressLaneRuntime({
|
||||
userMessage: "сырой вопрос",
|
||||
addressInputMessage: "нормализованный вопрос",
|
||||
carryover: { followupContext: { scope: "ctx" } },
|
||||
shouldPreferContextualLane: false,
|
||||
canRetryWithRawUserMessage: false,
|
||||
runAddressLaneAttempt,
|
||||
isRetryableAddressLimitedResult: (lane) => Boolean(lane?.debug?.limited_reason_category)
|
||||
});
|
||||
|
||||
expect(result.handled).toBe(true);
|
||||
expect(result.selection?.messageUsed).toBe("нормализованный вопрос");
|
||||
expect(result.selection?.addressLane.debug?.limited_reason_category).toBe("missing_anchor");
|
||||
expect(result.retryAudit.attempted).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { buildAssistantAddressOrchestrationRuntime } from "../src/services/assistantAddressOrchestrationRuntimeAdapter";
|
||||
|
||||
function buildInput(overrides: Record<string, unknown> = {}) {
|
||||
const runAddressLlmPreDecompose = vi.fn(async () => ({
|
||||
attempted: true,
|
||||
applied: true,
|
||||
effectiveMessage: "канон",
|
||||
reason: "normalized_fragment_applied"
|
||||
}));
|
||||
const resolveAddressFollowupCarryoverContext = vi.fn(() => ({
|
||||
followupContext: { id: "ctx" }
|
||||
}));
|
||||
const resolveAssistantOrchestrationDecision = vi.fn(() => ({
|
||||
runAddressLane: true,
|
||||
livingMode: "deep_analysis",
|
||||
livingReason: "address_mode_classifier_detected",
|
||||
toolGateDecision: "run_address_lane",
|
||||
toolGateReason: "address_mode_classifier_detected",
|
||||
orchestrationContract: { schema_version: "assistant_orchestration_contract_v1" }
|
||||
}));
|
||||
const buildAddressDialogContinuationContractV2 = vi.fn(() => ({
|
||||
schema_version: "address_dialog_continuation_contract_v2"
|
||||
}));
|
||||
|
||||
return {
|
||||
userMessage: "сырой вопрос",
|
||||
sessionItems: [],
|
||||
llmProvider: "openai",
|
||||
useMock: false,
|
||||
featureAddressLlmPredecomposeV1: true,
|
||||
runAddressLlmPreDecompose,
|
||||
buildAddressLlmPredecomposeContractV1: () => ({ schema_version: "address_llm_predecompose_contract_v1" }),
|
||||
sanitizeAddressMessageForFallback: () => "sanitized",
|
||||
toNonEmptyString: (value: unknown) => {
|
||||
if (typeof value !== "string") {
|
||||
return null;
|
||||
}
|
||||
const trimmed = value.trim();
|
||||
return trimmed.length > 0 ? trimmed : null;
|
||||
},
|
||||
resolveAddressFollowupCarryoverContext,
|
||||
resolveAssistantOrchestrationDecision,
|
||||
buildAddressDialogContinuationContractV2,
|
||||
__spies: {
|
||||
runAddressLlmPreDecompose,
|
||||
resolveAddressFollowupCarryoverContext,
|
||||
resolveAssistantOrchestrationDecision,
|
||||
buildAddressDialogContinuationContractV2
|
||||
},
|
||||
...overrides
|
||||
} as any;
|
||||
}
|
||||
|
||||
describe("assistant address orchestration runtime adapter", () => {
|
||||
it("uses llm predecompose payload when feature is enabled", async () => {
|
||||
const input = buildInput();
|
||||
|
||||
const output = await buildAssistantAddressOrchestrationRuntime(input);
|
||||
|
||||
expect(output.addressPreDecompose.reason).toBe("normalized_fragment_applied");
|
||||
expect(output.addressInputMessage).toBe("канон");
|
||||
expect(output.orchestrationDecision.runAddressLane).toBe(true);
|
||||
expect(output.livingModeDecision.mode).toBe("deep_analysis");
|
||||
expect(output.addressRuntimeMeta.toolGateDecision).toBe("run_address_lane");
|
||||
expect(output.addressRuntimeMeta.dialogContinuationContract).toEqual({
|
||||
schema_version: "address_dialog_continuation_contract_v2"
|
||||
});
|
||||
expect(input.__spies.runAddressLlmPreDecompose).toHaveBeenCalledTimes(1);
|
||||
expect(input.__spies.resolveAddressFollowupCarryoverContext).toHaveBeenCalledTimes(1);
|
||||
expect(input.__spies.resolveAssistantOrchestrationDecision).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("builds deterministic fallback predecompose payload when feature is disabled", async () => {
|
||||
const input = buildInput({
|
||||
featureAddressLlmPredecomposeV1: false,
|
||||
llmProvider: "local",
|
||||
runAddressLlmPreDecompose: vi.fn(async () => {
|
||||
throw new Error("must not be called");
|
||||
}),
|
||||
resolveAssistantOrchestrationDecision: vi.fn(() => ({
|
||||
runAddressLane: false,
|
||||
livingMode: "chat",
|
||||
livingReason: "predecompose_unsupported_mode",
|
||||
toolGateDecision: "skip_address_lane",
|
||||
toolGateReason: "llm_predecompose_unsupported_mode",
|
||||
orchestrationContract: { schema_version: "assistant_orchestration_contract_v1" }
|
||||
}))
|
||||
});
|
||||
|
||||
const output = await buildAssistantAddressOrchestrationRuntime(input);
|
||||
|
||||
expect(output.addressPreDecompose.attempted).toBe(false);
|
||||
expect(output.addressPreDecompose.applied).toBe(false);
|
||||
expect(output.addressPreDecompose.provider).toBe("local");
|
||||
expect(output.addressPreDecompose.reason).toBe("disabled_by_feature_flag");
|
||||
expect(output.addressPreDecompose.sanitizedUserMessage).toBe("sanitized");
|
||||
expect(output.addressInputMessage).toBe("сырой вопрос");
|
||||
expect(output.livingModeDecision.mode).toBe("chat");
|
||||
expect(output.addressRuntimeMeta.toolGateDecision).toBe("skip_address_lane");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user