АДРЕСНЫЙ РЕЖИМ -Step-4: Batch-2 lifecycle закрыт gate 36/36, nightly 102/102 + 25/25 PASS

This commit is contained in:
2026-04-08 13:39:40 +03:00
parent 1f9bb886ee
commit 71762af575
41 changed files with 50617 additions and 53 deletions
@@ -1403,6 +1403,41 @@ describe("address intent resolver expansion (M2.3a)", () => {
expect(result.intent).toBe("counterparty_activity_lifecycle");
});
it("keeps supplier lifecycle segmentation with operations wording in lifecycle intent", () => {
const result = resolveAddressIntent("Раздели поставщиков на регулярных и эпизодических по частоте операций.");
expect(result.intent).toBe("counterparty_activity_lifecycle");
});
it("keeps stale supplier operations wording in lifecycle intent", () => {
const result = resolveAddressIntent("Какие поставщики давно не использовались в операционной активности?");
expect(result.intent).toBe("counterparty_activity_lifecycle");
});
it("keeps slang all-customers-all-time wording in lifecycle intent", () => {
const result = resolveAddressIntent("выведи всех заков за все время");
expect(result.intent).toBe("counterparty_activity_lifecycle");
});
it("keeps churn wording with year in lifecycle intent", () => {
const result = resolveAddressIntent("кто был активен в 2020 и потом отвалился");
expect(result.intent).toBe("counterparty_activity_lifecycle");
});
it("keeps one-time-plus-churn wording in lifecycle intent", () => {
const result = resolveAddressIntent("кто с нами был ровно один раз и пропал");
expect(result.intent).toBe("counterparty_activity_lifecycle");
});
it("keeps oldest-collaboration slang wording in lifecycle intent", () => {
const result = resolveAddressIntent("самые старые по сотрудничеству кто");
expect(result.intent).toBe("counterparty_activity_lifecycle");
});
it("keeps regular-vs-one-off supplier slang in lifecycle intent (not population)", () => {
const result = resolveAddressIntent("разбей поставщиков на регуляр и разовые");
expect(result.intent).toBe("counterparty_activity_lifecycle");
});
it("resolves contract usage overview intent", () => {
const result = resolveAddressIntent("Сколько всего договоров заведено и сколько из них реально использовались?");
expect(result.intent).toBe("contract_usage_overview");
@@ -2219,6 +2254,22 @@ describe("address query limited taxonomy and stage diagnostics", () => {
});
describe("address decompose stage follow-up carryover", () => {
it("keeps slang all-customers-all-time wording in address lane via resolved intent fallback", () => {
const result = runAddressDecomposeStage("выведи всех заков за все время", null);
expect(result).not.toBeNull();
expect(result?.mode.mode).toBe("address_query");
expect(result?.intent.intent).toBe("counterparty_activity_lifecycle");
});
it("keeps churn wording with year in address lane via resolved intent fallback", () => {
const result = runAddressDecomposeStage("кто был активен в 2020 и потом отвалился", null);
expect(result).not.toBeNull();
expect(result?.mode.mode).toBe("address_query");
expect(result?.intent.intent).toBe("counterparty_activity_lifecycle");
expect(result?.filters.extracted_filters.period_from).toBe("2020-01-01");
expect(result?.filters.extracted_filters.period_to).toBe("2020-12-31");
});
it("uses short bare year in follow-up period switch", () => {
const result = runAddressDecomposeStage("теперь за 21", {
previous_intent: "counterparty_activity_lifecycle",
@@ -2235,6 +2286,24 @@ describe("address decompose stage follow-up carryover", () => {
expect(result?.filters.extracted_filters.period_to).toBe("2021-12-31");
});
it("keeps lifecycle follow-up phrasing with referential pointer and inherits period", () => {
const result = runAddressDecomposeStage("А кто из них новые?", {
previous_intent: "counterparty_activity_lifecycle",
previous_filters: {
period_from: "2020-01-01",
period_to: "2020-12-31"
},
previous_anchor_type: "unknown",
previous_anchor_value: null
});
expect(result).not.toBeNull();
expect(result?.mode.mode).toBe("address_query");
expect(result?.intent.intent).toBe("counterparty_activity_lifecycle");
expect(result?.filters.extracted_filters.period_from).toBe("2020-01-01");
expect(result?.filters.extracted_filters.period_to).toBe("2020-12-31");
expect(result?.baseReasons).toContain("address_followup_context_applied");
});
it("keeps short period follow-up in address lane and preserves previous counterparty anchor", () => {
const result = runAddressDecomposeStage("а теперь только за май 2020", {
previous_intent: "list_documents_by_counterparty",
@@ -192,4 +192,91 @@ describe("assistant address follow-up carryover", () => {
expect(String(calls[1].options?.followupContext?.previous_anchor_value ?? "").length).toBeGreaterThan(0);
expect(normalizerService.normalize).not.toHaveBeenCalled();
});
it("reuses last real address context after intermediate clarification fallback", async () => {
const calls: Array<{ message: string; options?: any }> = [];
const lifecycleFollowupMessage = "А кто из них новые?";
const addressQueryService = {
tryHandle: vi.fn(async (message: string, options?: any) => {
calls.push({ message, options });
if (message === "что там не так?") {
return null;
}
if (message === lifecycleFollowupMessage && !options?.followupContext) {
return null;
}
if (message === lifecycleFollowupMessage && options?.followupContext) {
return buildAddressLaneResult({
debug: {
...buildAddressLaneResult().debug,
detected_intent: "counterparty_activity_lifecycle",
extracted_filters: {
period_from: "2020-01-01",
period_to: "2020-12-31"
},
reasons: ["address_action_detected", "address_followup_context_applied"]
}
});
}
return buildAddressLaneResult();
})
} as any;
const normalizerService = {
normalize: vi.fn(async (payload: any) => ({
assistant_reply: payload?.userQuestion === "что там не так?" ? "Нужно уточнение по фокусу." : "unexpected_normalizer_call",
reply_type: payload?.userQuestion === "что там не так?" ? "clarification_required" : "partial_coverage",
debug: {
prompt_version: "address_query_runtime_v1",
detected_mode: null,
detected_intent: null
}
}))
} as any;
const sessions = new AssistantSessionStore();
const service = new AssistantService(
normalizerService,
sessions as any,
{} as any,
{ persistSession: vi.fn() } as any,
addressQueryService
);
const sessionId = `asst-address-followup-clar-${Date.now()}`;
const first = await service.handleMessage({
session_id: sessionId,
user_message: "покажи документы по свк за 2020",
useMock: true
} as any);
expect(first.ok).toBe(true);
expect(first.reply_type).toBe("factual");
const second = await service.handleMessage({
session_id: sessionId,
user_message: "что там не так?",
useMock: true
} as any);
expect(second.ok).toBe(true);
expect(second.reply_type).toBe("clarification_required");
const third = await service.handleMessage({
session_id: sessionId,
user_message: lifecycleFollowupMessage,
useMock: true
} as any);
expect(third.ok).toBe(true);
expect(third.reply_type).toBe("factual");
expect(third.debug?.detected_mode).toBe("address_query");
expect(third.debug?.detected_intent).toBe("counterparty_activity_lifecycle");
expect(calls.length).toBeGreaterThanOrEqual(2);
const contextualCall = calls.find((item) => item.message === lifecycleFollowupMessage && Boolean(item.options?.followupContext));
expect(contextualCall).toBeTruthy();
expect(contextualCall?.options?.followupContext?.previous_intent).toBe("list_documents_by_counterparty");
expect(contextualCall?.options?.followupContext?.previous_anchor_type).toBe("counterparty");
expect(String(contextualCall?.options?.followupContext?.previous_anchor_value ?? "").length).toBeGreaterThan(0);
expect(normalizerService.normalize).toHaveBeenCalledTimes(1);
});
});