133 lines
5.2 KiB
TypeScript
133 lines
5.2 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
||
import { AssistantService } from "../src/services/assistantService";
|
||
import { AssistantSessionStore } from "../src/services/assistantSessionStore";
|
||
|
||
function buildAddressLaneResult(overrides?: Record<string, unknown>): any {
|
||
return {
|
||
handled: true,
|
||
reply_text: "Собран список документов по контрагенту.",
|
||
reply_type: "factual",
|
||
response_type: "FACTUAL_LIST",
|
||
debug: {
|
||
detected_mode: "address_query",
|
||
detected_mode_confidence: "high",
|
||
query_shape: "DOCUMENT_LIST",
|
||
query_shape_confidence: "medium",
|
||
detected_intent: "list_documents_by_counterparty",
|
||
detected_intent_confidence: "medium",
|
||
extracted_filters: {
|
||
sort: "period_desc",
|
||
limit: 20,
|
||
counterparty: "свк"
|
||
},
|
||
missing_required_filters: [],
|
||
selected_recipe: "address_documents_by_counterparty_v1",
|
||
mcp_call_status_legacy: "matched_non_empty",
|
||
account_scope_mode: "preferred",
|
||
account_scope_fallback_applied: false,
|
||
anchor_type: "counterparty",
|
||
anchor_value_raw: "свк",
|
||
anchor_value_resolved: "Группа СВК",
|
||
resolver_confidence: "medium",
|
||
ambiguity_count: 0,
|
||
match_failure_stage: "none",
|
||
match_failure_reason: null,
|
||
mcp_call_status: "matched_non_empty",
|
||
rows_fetched: 20,
|
||
raw_rows_received: 20,
|
||
rows_after_account_scope: 5,
|
||
rows_after_recipe_filter: 3,
|
||
rows_materialized: 5,
|
||
rows_matched: 3,
|
||
raw_row_keys_sample: [],
|
||
materialization_drop_reason: "none",
|
||
account_token_raw: null,
|
||
account_token_normalized: null,
|
||
account_scope_fields_checked: ["account_dt", "account_kt", "registrator", "analytics"],
|
||
account_scope_match_strategy: "account_code_regex_plus_alias_map_v1",
|
||
account_scope_drop_reason: "not_applicable",
|
||
runtime_readiness: "LIVE_QUERYABLE_WITH_LIMITS",
|
||
limited_reason_category: null,
|
||
response_type: "FACTUAL_LIST",
|
||
limitations: [],
|
||
reasons: ["address_action_detected", "address_entity_detected"]
|
||
},
|
||
...(overrides ?? {})
|
||
};
|
||
}
|
||
|
||
describe("assistant address follow-up carryover", () => {
|
||
it("keeps short follow-up in address lane by reusing previous anchor context", async () => {
|
||
const calls: Array<{ message: string; options?: any }> = [];
|
||
const addressQueryService = {
|
||
tryHandle: vi.fn(async (message: string, options?: any) => {
|
||
calls.push({ message, options });
|
||
if (message === "какие есть доки по свк с 2020 по 2025 год") {
|
||
return buildAddressLaneResult();
|
||
}
|
||
if (message === "а за все время?" && !options?.followupContext) {
|
||
return null;
|
||
}
|
||
if (message === "а за все время?" && options?.followupContext) {
|
||
return buildAddressLaneResult({
|
||
reply_text: "Собран список документов по контрагенту за все время.",
|
||
debug: {
|
||
...buildAddressLaneResult().debug,
|
||
reasons: ["address_action_detected", "address_entity_detected", "address_followup_context_applied"]
|
||
}
|
||
});
|
||
}
|
||
return null;
|
||
})
|
||
} as any;
|
||
|
||
const normalizerService = {
|
||
normalize: vi.fn(async () => ({
|
||
assistant_reply: "normalizer_fallback_should_not_be_used",
|
||
reply_type: "partial_coverage",
|
||
debug: {}
|
||
}))
|
||
} 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-${Date.now()}`;
|
||
const first = await service.handleMessage({
|
||
session_id: sessionId,
|
||
user_message: "какие есть доки по свк с 2020 по 2025 год",
|
||
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("factual");
|
||
expect(second.debug?.detected_mode).toBe("address_query");
|
||
expect(second.debug?.detected_intent).toBe("list_documents_by_counterparty");
|
||
expect(second.debug?.extracted_filters?.counterparty).toBe("свк");
|
||
expect(second.debug?.answer_grounding_check?.reasons).toContain("address_followup_context_applied");
|
||
|
||
expect(calls).toHaveLength(2);
|
||
expect(calls[0].message).toBe("какие есть доки по свк с 2020 по 2025 год");
|
||
expect(calls[1].message).toBe("а за все время?");
|
||
expect(calls[1].options?.followupContext?.previous_intent).toBe("list_documents_by_counterparty");
|
||
expect(calls[1].options?.followupContext?.previous_anchor_type).toBe("counterparty");
|
||
expect(calls[1].options?.followupContext?.previous_anchor_value).toBe("Группа СВК");
|
||
expect(calls[1].options?.followupContext?.previous_filters?.counterparty).toBe("свк");
|
||
expect(normalizerService.normalize).not.toHaveBeenCalled();
|
||
});
|
||
});
|