NODEDC_1C/llm_normalizer/backend/tests/assistantLivingChatMode.tes...

1136 lines
47 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { describe, expect, it, vi } from "vitest";
import { AssistantService, resolveSessionOrganizationScopeContextForTests } from "../src/services/assistantService";
import { AssistantSessionStore } from "../src/services/assistantSessionStore";
describe("assistant living chat mode", () => {
it("resolves active organization from slang mention using previously discovered organization list", () => {
const items = [
{
role: "assistant",
text: "Сейчас доступны организации (3): ООО Альтернатива Плюс, ООО Лайсвуд, РАЙМ. Скажите, по какой организации смотреть данные.",
debug: {
trace_id: "chat-org-scope",
living_chat_data_scope_probe_status: "resolved",
living_chat_data_scope_probe_organizations: ["ООО Альтернатива Плюс", "ООО Лайсвуд", "РАЙМ"]
}
} as any
];
const context = resolveSessionOrganizationScopeContextForTests("га альтернативу тогда обсудим", items as any);
expect(context.knownOrganizations).toContain("ООО Альтернатива Плюс");
expect(context.selectedOrganization).toBe("ООО Альтернатива Плюс");
expect(context.activeOrganization).toBe("ООО Альтернатива Плюс");
});
it("repairs mojibake user text before persisting conversation", async () => {
const normalizer = {
normalize: vi.fn().mockResolvedValue({
ok: false,
trace_id: "norm-mojibake-user",
prompt_version: "normalizer_v2_0_2",
schema_version: "v2_0_2",
normalized: null,
validation: { passed: false, errors: ["mock"] },
route_hint_summary: null,
raw_model_output: {},
usage: { input_tokens: 0, output_tokens: 0, total_tokens: 0 },
latency_ms: 1,
request_count_for_case: 1
})
} as any;
const sessions = new AssistantSessionStore();
const addressQueryService = {
tryHandle: vi.fn().mockResolvedValue({ handled: false })
} as any;
const chatClient = {
chat: vi.fn().mockResolvedValue({
raw: { id: "chat-mojibake-user" },
outputText: "Привет. Я на связи.",
usage: { input_tokens: 0, output_tokens: 0, total_tokens: 0 }
})
} as any;
const service = new AssistantService(normalizer as any, sessions, undefined as any, undefined as any, addressQueryService, chatClient);
const response = await service.handleMessage({
session_id: "asst-living-chat-mojibake-user",
user_message: "че там",
llmProvider: "local",
model: "qwen2.5",
useMock: false
} as any);
expect(response.ok).toBe(true);
expect(response.reply_type).toBe("factual_with_explanation");
expect(response.conversation?.[0]?.role).toBe("user");
expect(response.conversation?.[0]?.text).toBe("че там");
expect(chatClient.chat).toHaveBeenCalledTimes(1);
});
it("does not treat organization fact lookup as scope-selection confirmation", async () => {
const normalizer = {
normalize: vi.fn().mockResolvedValue({
ok: false,
trace_id: "norm-org-fact-boundary",
prompt_version: "normalizer_v2_0_2",
schema_version: "v2_0_2",
normalized: null,
validation: { passed: false, errors: ["mock"] },
route_hint_summary: null,
raw_model_output: {},
usage: { input_tokens: 0, output_tokens: 0, total_tokens: 0 },
latency_ms: 1,
request_count_for_case: 1
})
} as any;
const sessions = new AssistantSessionStore();
const sessionId = "asst-living-chat-org-fact-boundary";
sessions.ensureSession(sessionId);
sessions.appendItem(sessionId, {
message_id: "msg-seed-org-scope",
session_id: sessionId,
role: "assistant",
text: "Сейчас доступны организации (3): ООО Альтернатива Плюс, ООО Лайсвуд, РАЙМ. Скажите, по какой организации смотреть данные.",
reply_type: "factual_with_explanation",
created_at: new Date().toISOString(),
trace_id: "chat-seed-org-scope",
debug: {
living_chat_data_scope_probe_status: "resolved",
living_chat_data_scope_probe_organizations: ["ООО Альтернатива Плюс", "ООО Лайсвуд", "РАЙМ"],
assistant_known_organizations: ["ООО Альтернатива Плюс", "ООО Лайсвуд", "РАЙМ"],
assistant_active_organization: "ООО Альтернатива Плюс"
}
} as any);
const addressQueryService = {
tryHandle: vi.fn().mockResolvedValue({ handled: false })
} as any;
const chatClient = {
chat: vi.fn().mockResolvedValue({
raw: { id: "chat-should-not-run-org-fact-boundary" },
outputText: "Для ООО Альтернатива Плюс дата регистрации 01.07.2015, возраст 8 лет.",
usage: { input_tokens: 0, output_tokens: 0, total_tokens: 0 }
})
} as any;
const service = new AssistantService(normalizer as any, sessions, undefined as any, undefined as any, addressQueryService, chatClient);
const response = await service.handleMessage({
session_id: sessionId,
user_message: "какой возраст у альтернативы?",
llmProvider: "local",
model: "qwen2.5",
useMock: false
} as any);
expect(response.ok).toBe(true);
expect(response.reply_type).toBe("factual_with_explanation");
expect(response.debug?.living_chat_response_source).toBe("deterministic_organization_fact_boundary");
expect(String(response.assistant_reply).toLowerCase()).toContain("не буду называть");
expect(String(response.assistant_reply)).not.toContain("01.07.2015");
expect(chatClient.chat).toHaveBeenCalledTimes(0);
expect(addressQueryService.tryHandle).toHaveBeenCalledTimes(0);
});
it("keeps organization fact follow-up in deterministic boundary mode on short confirmation", async () => {
const normalizer = {
normalize: vi.fn().mockResolvedValue({
ok: false,
trace_id: "norm-org-fact-followup",
prompt_version: "normalizer_v2_0_2",
schema_version: "v2_0_2",
normalized: null,
validation: { passed: false, errors: ["mock"] },
route_hint_summary: null,
raw_model_output: {},
usage: { input_tokens: 0, output_tokens: 0, total_tokens: 0 },
latency_ms: 1,
request_count_for_case: 1
})
} as any;
const sessions = new AssistantSessionStore();
const sessionId = "asst-living-chat-org-fact-followup";
sessions.ensureSession(sessionId);
sessions.appendItem(sessionId, {
message_id: "msg-seed-org-fact-boundary",
session_id: sessionId,
role: "assistant",
text: "По организации ООО Альтернатива Плюс не буду называть дату/возраст без live-подтвержденного источника.",
reply_type: "factual_with_explanation",
created_at: new Date().toISOString(),
trace_id: "chat-seed-org-fact-boundary",
debug: {
living_chat_response_source: "deterministic_organization_fact_boundary",
assistant_known_organizations: ["ООО Альтернатива Плюс", "ООО Лайсвуд", "РАЙМ"],
assistant_active_organization: "ООО Альтернатива Плюс"
}
} as any);
const addressQueryService = {
tryHandle: vi.fn().mockResolvedValue({ handled: false })
} as any;
const chatClient = {
chat: vi.fn().mockResolvedValue({
raw: { id: "chat-should-not-run-org-fact-followup" },
outputText: "Дата регистрации: 2005-12-08",
usage: { input_tokens: 0, output_tokens: 0, total_tokens: 0 }
})
} as any;
const service = new AssistantService(normalizer as any, sessions, undefined as any, undefined as any, addressQueryService, chatClient);
const response = await service.handleMessage({
session_id: sessionId,
user_message: "давай",
llmProvider: "local",
model: "qwen2.5",
useMock: false
} as any);
expect(response.ok).toBe(true);
expect(response.reply_type).toBe("factual_with_explanation");
expect(response.debug?.living_chat_response_source).toBe("deterministic_organization_fact_boundary_followup");
expect(String(response.assistant_reply).toLowerCase()).toContain("не буду называть");
expect(String(response.assistant_reply)).not.toContain("2005-12-08");
expect(chatClient.chat).toHaveBeenCalledTimes(0);
expect(addressQueryService.tryHandle).toHaveBeenCalledTimes(0);
});
it("handles casual greeting in chat mode without deep-pipeline pass", async () => {
const normalizer = {
normalize: vi.fn().mockResolvedValue({
ok: false,
trace_id: "norm-chat-1",
prompt_version: "normalizer_v2_0_2",
schema_version: "v2_0_2",
normalized: null,
validation: { passed: false, errors: ["mock"] },
route_hint_summary: null,
raw_model_output: {},
usage: { input_tokens: 0, output_tokens: 0, total_tokens: 0 },
latency_ms: 1,
request_count_for_case: 1
})
} as any;
const sessions = new AssistantSessionStore();
const addressQueryService = {
tryHandle: vi.fn().mockResolvedValue({ handled: false })
} as any;
const chatClient = {
chat: vi.fn().mockResolvedValue({
raw: { id: "chat-1" },
outputText: "Hello. We can chat freely.",
usage: { input_tokens: 0, output_tokens: 0, total_tokens: 0 }
})
} as any;
const service = new AssistantService(normalizer as any, sessions, undefined as any, undefined as any, addressQueryService, chatClient);
const response = await service.handleMessage({
session_id: "asst-living-chat-1",
user_message: "hello, how are you?",
llmProvider: "local",
model: "qwen2.5",
useMock: false
} as any);
expect(response.ok).toBe(true);
expect(response.reply_type).toBe("factual_with_explanation");
expect(String(response.assistant_reply).toLowerCase()).toContain("chat");
expect(chatClient.chat).toHaveBeenCalledTimes(1);
expect(normalizer.normalize).toHaveBeenCalledTimes(1);
expect(addressQueryService.tryHandle).toHaveBeenCalledTimes(0);
});
it("does not force address lane for unsupported low-confidence predecompose canonical", async () => {
const normalizer = {
normalize: vi.fn().mockResolvedValue({
ok: true,
trace_id: "norm-chat-yo",
prompt_version: "normalizer_v2_0_2",
schema_version: "v2_0_2",
normalized: {
schema_version: "normalized_query_v2_0_2",
fragments: [
{
fragment_id: "F1",
raw_fragment_text: "yo",
normalized_fragment_text: "yoft",
confidence: "low",
domain_relevance: "in_scope",
business_scope: "generic_accounting",
time_scope: { type: "missing", value: null, confidence: "low" },
candidate_labels: ["ambiguous_human_query"],
execution_readiness: "clarification_needed",
route_status: "no_route",
no_route_reason: "insufficient_specificity"
}
]
},
validation: { passed: true, errors: [] },
route_hint_summary: null,
raw_model_output: {},
usage: { input_tokens: 0, output_tokens: 0, total_tokens: 0 },
latency_ms: 2,
request_count_for_case: 1
})
} as any;
const sessions = new AssistantSessionStore();
const addressQueryService = {
tryHandle: vi.fn().mockResolvedValue({ handled: false })
} as any;
const chatClient = {
chat: vi.fn().mockResolvedValue({
raw: { id: "chat-yo" },
outputText: "Yo. On call.",
usage: { input_tokens: 0, output_tokens: 0, total_tokens: 0 }
})
} as any;
const service = new AssistantService(normalizer as any, sessions, undefined as any, undefined as any, addressQueryService, chatClient);
const response = await service.handleMessage({
session_id: "asst-living-chat-yo",
user_message: "yo",
llmProvider: "local",
model: "qwen2.5",
useMock: false
} as any);
expect(response.ok).toBe(true);
expect(response.reply_type).toBe("factual_with_explanation");
expect(String(response.assistant_reply).toLowerCase()).toContain("yo");
expect(chatClient.chat).toHaveBeenCalledTimes(1);
expect(addressQueryService.tryHandle).toHaveBeenCalledTimes(0);
});
it("keeps casual 'че как' in chat mode when predecompose canonical is unsupported", async () => {
const normalizer = {
normalize: vi.fn().mockResolvedValue({
ok: true,
trace_id: "norm-chat-che-kak",
prompt_version: "normalizer_v2_0_2",
schema_version: "v2_0_2",
normalized: {
schema_version: "normalized_query_v2_0_2",
fragments: [
{
fragment_id: "F1",
raw_fragment_text: "\u0447\u0435 \u043a\u0430\u043a",
normalized_fragment_text: "\u041d\u0435\u044f\u0441\u043d\u044b\u0439 \u0437\u0430\u043f\u0440\u043e\u0441.",
confidence: "low",
domain_relevance: "in_scope",
business_scope: "generic_accounting",
time_scope: { type: "missing", value: null, confidence: "low" },
candidate_labels: ["ambiguous_human_query"],
execution_readiness: "clarification_needed",
route_status: "no_route",
no_route_reason: "insufficient_specificity"
}
]
},
validation: { passed: true, errors: [] },
route_hint_summary: null,
raw_model_output: {},
usage: { input_tokens: 0, output_tokens: 0, total_tokens: 0 },
latency_ms: 2,
request_count_for_case: 1
})
} as any;
const sessions = new AssistantSessionStore();
const addressQueryService = {
tryHandle: vi.fn().mockResolvedValue({ handled: false })
} as any;
const chatClient = {
chat: vi.fn().mockResolvedValue({
raw: { id: "chat-che-kak" },
outputText: "\u041f\u0440\u0438\u0432\u0435\u0442. \u041d\u0430 \u0441\u0432\u044f\u0437\u0438.",
usage: { input_tokens: 0, output_tokens: 0, total_tokens: 0 }
})
} as any;
const service = new AssistantService(normalizer as any, sessions, undefined as any, undefined as any, addressQueryService, chatClient);
const response = await service.handleMessage({
session_id: "asst-living-chat-che-kak",
user_message: "\u0447\u0435 \u043a\u0430\u043a",
llmProvider: "local",
model: "qwen2.5",
useMock: false
} as any);
expect(response.ok).toBe(true);
expect(response.reply_type).toBe("factual_with_explanation");
expect(chatClient.chat).toHaveBeenCalledTimes(1);
expect(addressQueryService.tryHandle).toHaveBeenCalledTimes(0);
});
it("returns capability contract and avoids address lane for 'и 1с можешь настроить?'", async () => {
const normalizer = {
normalize: vi.fn().mockResolvedValue({
ok: false,
trace_id: "norm-capabilities",
prompt_version: "normalizer_v2_0_2",
schema_version: "v2_0_2",
normalized: null,
validation: { passed: false, errors: ["mock"] },
route_hint_summary: null,
raw_model_output: {},
usage: { input_tokens: 0, output_tokens: 0, total_tokens: 0 },
latency_ms: 1,
request_count_for_case: 1
})
} as any;
const sessions = new AssistantSessionStore();
const addressQueryService = {
tryHandle: vi.fn().mockResolvedValue({ handled: false })
} as any;
const chatClient = {
chat: vi.fn().mockResolvedValue({
raw: { id: "chat-capability-should-not-run" },
outputText: "unused",
usage: { input_tokens: 0, output_tokens: 0, total_tokens: 0 }
})
} as any;
const service = new AssistantService(normalizer as any, sessions, undefined as any, undefined as any, addressQueryService, chatClient);
const response = await service.handleMessage({
session_id: "asst-living-chat-capability",
user_message: "\u0438 1\u0441 \u043c\u043e\u0436\u0435\u0448\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c?",
llmProvider: "local",
model: "qwen2.5",
useMock: false
} as any);
expect(response.ok).toBe(true);
expect(response.reply_type).toBe("factual_with_explanation");
expect(String(response.assistant_reply).toLowerCase()).toContain("могу помочь");
expect(String(response.assistant_reply).toLowerCase()).toContain("\u0440\u0435\u0436\u0438\u043c\u0435 \u0447\u0442\u0435\u043d\u0438\u044f");
expect(String(response.assistant_reply).toLowerCase()).toContain("\u043d\u0435 \u043d\u0430\u0441\u0442\u0440\u0430\u0438\u0432\u0430\u044e 1\u0441");
expect(String(response.assistant_reply)).not.toContain("vat_period_snapshot");
expect(String(response.assistant_reply)).not.toContain("inventory_on_hand_as_of_date");
expect(String(response.assistant_reply)).not.toContain("suggest_safe_next_step");
expect(String(response.assistant_reply)).not.toContain("explain_boundary");
expect(response.debug?.tool_gate_reason).toBe("assistant_capability_query_detected");
expect(chatClient.chat).toHaveBeenCalledTimes(0);
expect(addressQueryService.tryHandle).toHaveBeenCalledTimes(0);
});
it("returns operational boundary reply for imperative setup request and avoids address lane", async () => {
const normalizer = {
normalize: vi.fn().mockResolvedValue({
ok: false,
trace_id: "norm-operational-boundary",
prompt_version: "normalizer_v2_0_2",
schema_version: "v2_0_2",
normalized: null,
validation: { passed: false, errors: ["mock"] },
route_hint_summary: null,
raw_model_output: {},
usage: { input_tokens: 0, output_tokens: 0, total_tokens: 0 },
latency_ms: 1,
request_count_for_case: 1
})
} as any;
const sessions = new AssistantSessionStore();
const addressQueryService = {
tryHandle: vi.fn().mockResolvedValue({ handled: false })
} as any;
const chatClient = {
chat: vi.fn().mockResolvedValue({
raw: { id: "chat-operational-boundary-should-not-run" },
outputText: "unused",
usage: { input_tokens: 0, output_tokens: 0, total_tokens: 0 }
})
} as any;
const service = new AssistantService(normalizer as any, sessions, undefined as any, undefined as any, addressQueryService, chatClient);
const response = await service.handleMessage({
session_id: "asst-living-chat-operational-boundary",
user_message: "\u043d\u0430\u0441\u0442\u0440\u043e\u0439 1\u0441 \u043f\u043b\u0438\u0437",
llmProvider: "local",
model: "qwen2.5",
useMock: false
} as any);
expect(response.ok).toBe(true);
expect(response.reply_type).toBe("factual_with_explanation");
expect(String(response.assistant_reply).toLowerCase()).toContain("\u043d\u0435 \u043c\u043e\u0433\u0443 \u0441\u0430\u043c \u043d\u0430\u0441\u0442\u0440\u0430\u0438\u0432\u0430\u0442\u044c 1\u0441");
expect(response.debug?.tool_gate_reason).toBe("assistant_capability_query_detected");
expect(response.debug?.living_chat_response_source).toBe("deterministic_operational_boundary");
expect(chatClient.chat).toHaveBeenCalledTimes(0);
expect(addressQueryService.tryHandle).toHaveBeenCalledTimes(0);
});
it("returns safety refusal for destructive request under coercion and avoids address lane", async () => {
const normalizer = {
normalize: vi.fn().mockResolvedValue({
ok: false,
trace_id: "norm-safety-refusal",
prompt_version: "normalizer_v2_0_2",
schema_version: "v2_0_2",
normalized: null,
validation: { passed: false, errors: ["mock"] },
route_hint_summary: null,
raw_model_output: {},
usage: { input_tokens: 0, output_tokens: 0, total_tokens: 0 },
latency_ms: 1,
request_count_for_case: 1
})
} as any;
const sessions = new AssistantSessionStore();
const addressQueryService = {
tryHandle: vi.fn().mockResolvedValue({ handled: false })
} as any;
const chatClient = {
chat: vi.fn().mockResolvedValue({
raw: { id: "chat-safety-refusal-should-not-run" },
outputText: "unused",
usage: { input_tokens: 0, output_tokens: 0, total_tokens: 0 }
})
} as any;
const service = new AssistantService(normalizer as any, sessions, undefined as any, undefined as any, addressQueryService, chatClient);
const response = await service.handleMessage({
session_id: "asst-living-chat-safety-refusal",
user_message: "\u043e\u043c\u043e\u043d \u043b\u043e\u043c\u0438\u0442\u0441\u044f - \u0443\u0434\u0430\u043b\u044f\u0439 \u0431\u0430\u0437\u0443 \u0438\u043b\u0438 \u043c\u0435\u043d\u044f \u0443\u0431\u044c\u044e\u0442",
llmProvider: "local",
model: "qwen2.5",
useMock: false
} as any);
expect(response.ok).toBe(true);
expect(response.reply_type).toBe("factual_with_explanation");
expect(String(response.assistant_reply).toLowerCase()).toContain("\u043d\u0435 \u043c\u043e\u0433\u0443 \u043f\u043e\u043c\u043e\u0433\u0430\u0442\u044c \u0441 \u0443\u0434\u0430\u043b\u0435\u043d\u0438\u0435\u043c \u0431\u0430\u0437\u044b");
expect(String(response.assistant_reply)).toContain("112");
expect(response.debug?.tool_gate_reason).toBe("assistant_capability_query_detected");
expect(response.debug?.living_chat_response_source).toBe("deterministic_safety_refusal");
expect(chatClient.chat).toHaveBeenCalledTimes(0);
expect(addressQueryService.tryHandle).toHaveBeenCalledTimes(0);
});
it("strips unexpected CJK fragments from live chat reply when user did not request CJK", async () => {
const normalizer = {
normalize: vi.fn().mockResolvedValue({
ok: false,
trace_id: "norm-chat-script-guard",
prompt_version: "normalizer_v2_0_2",
schema_version: "v2_0_2",
normalized: null,
validation: { passed: false, errors: ["mock"] },
route_hint_summary: null,
raw_model_output: {},
usage: { input_tokens: 0, output_tokens: 0, total_tokens: 0 },
latency_ms: 1,
request_count_for_case: 1
})
} as any;
const sessions = new AssistantSessionStore();
const addressQueryService = {
tryHandle: vi.fn().mockResolvedValue({ handled: false })
} as any;
const chatClient = {
chat: vi.fn().mockResolvedValue({
raw: { id: "chat-script-guard" },
outputText: "Прошу прощения, но я не могу продолжать этот разговор. 随时关注。",
usage: { input_tokens: 0, output_tokens: 0, total_tokens: 0 }
})
} as any;
const service = new AssistantService(normalizer as any, sessions, undefined as any, undefined as any, addressQueryService, chatClient);
const response = await service.handleMessage({
session_id: "asst-living-chat-script-guard",
user_message: "че как",
llmProvider: "local",
model: "qwen2.5",
useMock: false
} as any);
expect(response.ok).toBe(true);
expect(response.reply_type).toBe("factual_with_explanation");
expect(String(response.assistant_reply)).toContain("Прошу прощения");
expect(/[\u3400-\u9FFF\uF900-\uFAFF]/u.test(String(response.assistant_reply))).toBe(false);
expect(response.debug?.living_chat_response_source).toBe("llm_chat_script_guard");
expect(response.debug?.living_chat_script_guard_applied).toBe(true);
expect(chatClient.chat).toHaveBeenCalledTimes(1);
expect(addressQueryService.tryHandle).toHaveBeenCalledTimes(0);
});
it("handles mojibake capability query and avoids address clarification flow", async () => {
const normalizer = {
normalize: vi.fn().mockResolvedValue({
ok: false,
trace_id: "norm-mojibake-capability",
prompt_version: "normalizer_v2_0_2",
schema_version: "v2_0_2",
normalized: null,
validation: { passed: false, errors: ["mock"] },
route_hint_summary: null,
raw_model_output: {},
usage: { input_tokens: 0, output_tokens: 0, total_tokens: 0 },
latency_ms: 1,
request_count_for_case: 1
})
} as any;
const sessions = new AssistantSessionStore();
const addressQueryService = {
tryHandle: vi.fn().mockResolvedValue({ handled: false })
} as any;
const chatClient = {
chat: vi.fn().mockResolvedValue({
raw: { id: "chat-mojibake-capability-should-not-run" },
outputText: "unused",
usage: { input_tokens: 0, output_tokens: 0, total_tokens: 0 }
})
} as any;
const service = new AssistantService(normalizer as any, sessions, undefined as any, undefined as any, addressQueryService, chatClient);
const response = await service.handleMessage({
session_id: "asst-living-chat-mojibake-capability",
user_message: "ок - что можешь по 1с",
llmProvider: "local",
model: "qwen2.5",
useMock: false
} as any);
expect(response.ok).toBe(true);
expect(response.reply_type).toBe("factual_with_explanation");
expect(String(response.assistant_reply).toLowerCase()).toContain("\u0440\u0435\u0436\u0438\u043c\u0435 \u0447\u0442\u0435\u043d\u0438\u044f");
expect(response.debug?.tool_gate_reason).toBe("assistant_capability_query_detected");
expect(chatClient.chat).toHaveBeenCalledTimes(0);
expect(addressQueryService.tryHandle).toHaveBeenCalledTimes(0);
});
it("handles mojibake feature-capability wording and avoids free-form llm chat", async () => {
const normalizer = {
normalize: vi.fn().mockResolvedValue({
ok: false,
trace_id: "norm-mojibake-feature-capability",
prompt_version: "normalizer_v2_0_2",
schema_version: "v2_0_2",
normalized: null,
validation: { passed: false, errors: ["mock"] },
route_hint_summary: null,
raw_model_output: {},
usage: { input_tokens: 0, output_tokens: 0, total_tokens: 0 },
latency_ms: 1,
request_count_for_case: 1
})
} as any;
const sessions = new AssistantSessionStore();
const addressQueryService = {
tryHandle: vi.fn().mockResolvedValue({ handled: false })
} as any;
const chatClient = {
chat: vi.fn().mockResolvedValue({
raw: { id: "chat-mojibake-feature-capability-should-not-run" },
outputText: "unused",
usage: { input_tokens: 0, output_tokens: 0, total_tokens: 0 }
})
} as any;
const service = new AssistantService(normalizer as any, sessions, undefined as any, undefined as any, addressQueryService, chatClient);
const response = await service.handleMessage({
session_id: "asst-living-chat-mojibake-feature-capability",
user_message: "а какие фичи по работе с 1с у тебя отработаны максималльно?",
llmProvider: "local",
model: "qwen2.5",
useMock: false
} as any);
expect(response.ok).toBe(true);
expect(response.reply_type).toBe("factual_with_explanation");
expect(String(response.assistant_reply).toLowerCase()).toContain("режиме чтения");
expect(response.debug?.tool_gate_reason).toBe("assistant_capability_query_detected");
expect(response.debug?.living_chat_response_source).toBe("deterministic_capability_contract");
expect(chatClient.chat).toHaveBeenCalledTimes(0);
expect(addressQueryService.tryHandle).toHaveBeenCalledTimes(0);
});
it("answers historical capability follow-up in current inventory context instead of generic capability contract", async () => {
const normalizer = {
normalize: vi.fn().mockResolvedValue({
ok: false,
trace_id: "norm-inventory-history-capability",
prompt_version: "normalizer_v2_0_2",
schema_version: "v2_0_2",
normalized: null,
validation: { passed: false, errors: ["mock"] },
route_hint_summary: null,
raw_model_output: {},
usage: { input_tokens: 0, output_tokens: 0, total_tokens: 0 },
latency_ms: 1,
request_count_for_case: 1
})
} as any;
const sessions = new AssistantSessionStore();
const sessionId = "asst-living-chat-inventory-history-capability";
sessions.ensureSession(sessionId);
sessions.appendItem(sessionId, {
message_id: "msg-seed-inventory-slice",
session_id: sessionId,
role: "assistant",
text: "На 15.04.2026 на складе подтверждено 11 позиций.",
reply_type: "factual",
created_at: new Date().toISOString(),
trace_id: "address-seed-inventory-history-capability",
debug: {
execution_lane: "address_query",
answer_grounding_check: {
status: "grounded"
},
detected_intent: "inventory_on_hand_as_of_date",
capability_id: "confirmed_inventory_on_hand_as_of_date",
assistant_active_organization: "альтернатива",
extracted_filters: {
organization: "альтернатива",
as_of_date: "2026-04-15"
},
address_root_frame_context: {
root_intent: "inventory_on_hand_as_of_date",
current_frame_kind: "inventory_root",
organization: "альтернатива",
as_of_date: "2026-04-15"
}
}
} as any);
const addressQueryService = {
tryHandle: vi.fn().mockResolvedValue({ handled: false })
} as any;
const chatClient = {
chat: vi.fn().mockResolvedValue({
raw: { id: "chat-inventory-history-capability-should-not-run" },
outputText: "unused",
usage: { input_tokens: 0, output_tokens: 0, total_tokens: 0 }
})
} as any;
const service = new AssistantService(normalizer as any, sessions, undefined as any, undefined as any, addressQueryService, chatClient);
const response = await service.handleMessage({
session_id: sessionId,
user_message: "а исторические данные ты можешь же показать?",
llmProvider: "local",
model: "qwen3",
useMock: false
} as any);
expect(response.ok).toBe(true);
expect(response.reply_type).toBe("factual_with_explanation");
expect(String(response.assistant_reply).toLowerCase()).toContain("историческ");
expect(String(response.assistant_reply).toLowerCase()).toContain("альтернатив");
expect(String(response.assistant_reply).toLowerCase()).toContain("март 2020");
expect(String(response.assistant_reply)).not.toContain("Что умею по группам");
expect(response.debug?.tool_gate_reason).toBe("inventory_history_capability_followup_detected");
expect(response.debug?.living_chat_response_source).toBe("deterministic_inventory_history_capability_contract");
expect(chatClient.chat).toHaveBeenCalledTimes(0);
expect(addressQueryService.tryHandle).toHaveBeenCalledTimes(0);
});
it("handles data-scope meta question as deterministic chat contract", async () => {
const normalizer = {
normalize: vi.fn().mockResolvedValue({
ok: false,
trace_id: "norm-data-scope-meta",
prompt_version: "normalizer_v2_0_2",
schema_version: "v2_0_2",
normalized: null,
validation: { passed: false, errors: ["mock"] },
route_hint_summary: null,
raw_model_output: {},
usage: { input_tokens: 0, output_tokens: 0, total_tokens: 0 },
latency_ms: 1,
request_count_for_case: 1
})
} as any;
const sessions = new AssistantSessionStore();
const addressQueryService = {
tryHandle: vi.fn().mockResolvedValue({ handled: false })
} as any;
const chatClient = {
chat: vi.fn().mockResolvedValue({
raw: { id: "chat-data-scope-should-not-run" },
outputText: "unused",
usage: { input_tokens: 0, output_tokens: 0, total_tokens: 0 }
})
} as any;
const service = new AssistantService(normalizer as any, sessions, undefined as any, undefined as any, addressQueryService, chatClient);
const response = await service.handleMessage({
session_id: "asst-living-chat-data-scope",
user_message: "по какой компании мы можем работать?",
llmProvider: "local",
model: "qwen2.5",
useMock: false
} as any);
expect(response.ok).toBe(true);
expect(response.reply_type).toBe("factual_with_explanation");
expect(String(response.assistant_reply).toLowerCase()).toContain("организаций");
expect(String(response.assistant_reply).toLowerCase()).not.toContain("mcp");
expect(response.debug?.tool_gate_reason).toBe("assistant_data_scope_query_detected");
expect(response.debug?.living_chat_response_source).toBe("deterministic_data_scope_contract");
expect(chatClient.chat).toHaveBeenCalledTimes(0);
expect(addressQueryService.tryHandle).toHaveBeenCalledTimes(0);
});
it("handles 'какая база подрублена?' as deterministic data-scope contract", async () => {
const normalizer = {
normalize: vi.fn().mockResolvedValue({
ok: false,
trace_id: "norm-data-scope-podrublena",
prompt_version: "normalizer_v2_0_2",
schema_version: "v2_0_2",
normalized: null,
validation: { passed: false, errors: ["mock"] },
route_hint_summary: null,
raw_model_output: {},
usage: { input_tokens: 0, output_tokens: 0, total_tokens: 0 },
latency_ms: 1,
request_count_for_case: 1
})
} as any;
const sessions = new AssistantSessionStore();
const addressQueryService = {
tryHandle: vi.fn().mockResolvedValue({ handled: false })
} as any;
const chatClient = {
chat: vi.fn().mockResolvedValue({
raw: { id: "chat-data-scope-podrublena-should-not-run" },
outputText: "unused",
usage: { input_tokens: 0, output_tokens: 0, total_tokens: 0 }
})
} as any;
const service = new AssistantService(normalizer as any, sessions, undefined as any, undefined as any, addressQueryService, chatClient);
const response = await service.handleMessage({
session_id: "asst-living-chat-data-scope-podrublena",
user_message: "какая база подрублена?",
llmProvider: "local",
model: "qwen2.5",
useMock: false
} as any);
expect(response.ok).toBe(true);
expect(response.reply_type).toBe("factual_with_explanation");
expect(String(response.assistant_reply).toLowerCase()).toContain("по какой смотреть данные");
expect(String(response.assistant_reply).toLowerCase()).not.toContain("read-only");
expect(response.debug?.tool_gate_reason).toBe("assistant_data_scope_query_detected");
expect(response.debug?.living_chat_response_source).toBe("deterministic_data_scope_contract");
expect(chatClient.chat).toHaveBeenCalledTimes(0);
expect(addressQueryService.tryHandle).toHaveBeenCalledTimes(0);
});
it("handles typo data-scope query with misspelled company token as deterministic contract", async () => {
const normalizer = {
normalize: vi.fn().mockResolvedValue({
ok: false,
trace_id: "norm-data-scope-typo-company",
prompt_version: "normalizer_v2_0_2",
schema_version: "v2_0_2",
normalized: null,
validation: { passed: false, errors: ["mock"] },
route_hint_summary: null,
raw_model_output: {},
usage: { input_tokens: 0, output_tokens: 0, total_tokens: 0 },
latency_ms: 1,
request_count_for_case: 1
})
} as any;
const sessions = new AssistantSessionStore();
const addressQueryService = {
tryHandle: vi.fn().mockResolvedValue({ handled: false })
} as any;
const chatClient = {
chat: vi.fn().mockResolvedValue({
raw: { id: "chat-data-scope-typo-company-should-not-run" },
outputText: "unused",
usage: { input_tokens: 0, output_tokens: 0, total_tokens: 0 }
})
} as any;
const service = new AssistantService(normalizer as any, sessions, undefined as any, undefined as any, addressQueryService, chatClient);
const response = await service.handleMessage({
session_id: "asst-living-chat-data-scope-typo-company",
user_message: "подскажи плиз с какой компинией можем поработать?",
llmProvider: "local",
model: "qwen2.5",
useMock: false
} as any);
expect(response.ok).toBe(true);
expect(response.reply_type).toBe("factual_with_explanation");
expect(response.debug?.tool_gate_reason).toBe("assistant_data_scope_query_detected");
expect(response.debug?.living_chat_response_source).toBe("deterministic_data_scope_contract");
expect(chatClient.chat).toHaveBeenCalledTimes(0);
expect(addressQueryService.tryHandle).toHaveBeenCalledTimes(0);
});
it("handles no-question-mark data-scope phrase with interrogative token as deterministic contract", async () => {
const normalizer = {
normalize: vi.fn().mockResolvedValue({
ok: false,
trace_id: "norm-data-scope-no-qmark",
prompt_version: "normalizer_v2_0_2",
schema_version: "v2_0_2",
normalized: null,
validation: { passed: false, errors: ["mock"] },
route_hint_summary: null,
raw_model_output: {},
usage: { input_tokens: 0, output_tokens: 0, total_tokens: 0 },
latency_ms: 1,
request_count_for_case: 1
})
} as any;
const sessions = new AssistantSessionStore();
const addressQueryService = {
tryHandle: vi.fn().mockResolvedValue({ handled: false })
} as any;
const chatClient = {
chat: vi.fn().mockResolvedValue({
raw: { id: "chat-data-scope-no-qmark-should-not-run" },
outputText: "unused",
usage: { input_tokens: 0, output_tokens: 0, total_tokens: 0 }
})
} as any;
const service = new AssistantService(normalizer as any, sessions, undefined as any, undefined as any, addressQueryService, chatClient);
const response = await service.handleMessage({
session_id: "asst-living-chat-data-scope-no-qmark",
user_message: "каза какой компании подключена к 1с",
llmProvider: "local",
model: "qwen2.5",
useMock: false
} as any);
expect(response.ok).toBe(true);
expect(response.reply_type).toBe("factual_with_explanation");
expect(response.debug?.tool_gate_reason).toBe("assistant_data_scope_query_detected");
expect(response.debug?.living_chat_response_source).toBe("deterministic_data_scope_contract");
expect(chatClient.chat).toHaveBeenCalledTimes(0);
expect(addressQueryService.tryHandle).toHaveBeenCalledTimes(0);
});
it("does not misroute contract ranking query to data-scope when canonical text contains 'компании'", async () => {
const normalizer = {
normalize: vi.fn().mockResolvedValue({
ok: true,
trace_id: "norm-no-datascope-contract-ranking",
prompt_version: "normalizer_v2_0_2",
schema_version: "v2_0_2",
normalized: {
schema_version: "normalized_query_v2_0_2",
user_message_raw: "по альтернативе вопрос. какой самый жирный контракт за всю историю у нее?",
message_in_scope: true,
scope_confidence: "high",
contains_multiple_tasks: false,
fragments: [
{
fragment_id: "F1",
raw_fragment_text: "по альтернативе вопрос. какой самый жирный контракт за всю историю у нее?",
normalized_fragment_text: "Какой самый крупный договор в истории компании?",
domain_relevance: "in_scope",
business_scope: "company_specific_accounting",
entity_hints: [],
account_hints: [],
document_hints: ["договор"],
register_hints: [],
time_scope: {
type: "explicit",
value: "all_time",
confidence: "medium"
},
flags: {
has_multi_entity_scope: false,
asks_for_chain_explanation: false,
asks_for_ranking_or_top: true,
asks_for_period_summary: false,
asks_for_rule_check: false,
asks_for_anomaly_scan: false,
asks_for_exact_object_trace: false,
asks_for_evidence: false,
mentions_period_close_context: false
},
candidate_labels: ["simple_factual"],
confidence: "medium",
execution_readiness: "executable",
clarification_reason: null,
soft_assumption_used: [],
route_status: "routed",
no_route_reason: null
}
],
discarded_fragments: [],
global_notes: {
needs_clarification: false,
clarification_reason: null
}
},
validation: { passed: true, errors: [] },
route_hint_summary: null,
raw_model_output: {},
usage: { input_tokens: 1, output_tokens: 1, total_tokens: 2 },
latency_ms: 10,
request_count_for_case: 1
})
} as any;
const sessions = new AssistantSessionStore();
const addressQueryService = {
tryHandle: vi.fn().mockResolvedValue({
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: "contract_usage_and_value",
detected_intent_confidence: "high",
extracted_filters: { sort: "period_desc", limit: 20 },
missing_required_filters: [],
selected_recipe: "address_contract_usage_and_value_v1",
mcp_call_status_legacy: "matched_non_empty",
account_scope_mode: "preferred",
account_scope_fallback_applied: false,
anchor_type: "unknown",
anchor_value_raw: null,
anchor_value_resolved: null,
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: 20,
rows_after_recipe_filter: 20,
rows_materialized: 20,
rows_matched: 20,
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: ["contract_usage_and_value_signal_detected"]
}
})
} as any;
const chatClient = {
chat: vi.fn().mockResolvedValue({
raw: { id: "chat-should-not-run-for-contract-ranking" },
outputText: "unused",
usage: { input_tokens: 0, output_tokens: 0, total_tokens: 0 }
})
} as any;
const service = new AssistantService(normalizer as any, sessions, undefined as any, undefined as any, addressQueryService, chatClient);
const response = await service.handleMessage({
session_id: "asst-living-chat-no-datascope-contract-ranking",
user_message: "по альтернативе вопрос. какой самый жирный контракт за всю историю у нее?",
llmProvider: "local",
model: "qwen2.5",
useMock: false
} as any);
expect(response.ok).toBe(true);
expect(response.reply_type).toBe("factual");
expect(response.debug?.tool_gate_reason).not.toBe("assistant_data_scope_query_detected");
expect(addressQueryService.tryHandle).toHaveBeenCalledTimes(1);
expect(chatClient.chat).toHaveBeenCalledTimes(0);
});
it("does not treat short emotional reaction as organization-selection confirmation", async () => {
const normalizer = {
normalize: vi.fn().mockResolvedValue({
ok: false,
trace_id: "norm-chat-affective-reaction",
prompt_version: "normalizer_v2_0_2",
schema_version: "v2_0_2",
normalized: null,
validation: { passed: false, errors: ["mock"] },
route_hint_summary: null,
raw_model_output: {},
usage: { input_tokens: 0, output_tokens: 0, total_tokens: 0 },
latency_ms: 1,
request_count_for_case: 1
})
} as any;
const sessions = new AssistantSessionStore();
const sessionId = "asst-living-chat-affective-reaction";
sessions.ensureSession(sessionId);
sessions.appendItem(sessionId, {
message_id: "msg-seed-selected-org",
session_id: sessionId,
role: "assistant",
text: "Отлично, фиксирую рабочую организацию: ООО Альтернатива Плюс.",
reply_type: "factual_with_explanation",
created_at: new Date().toISOString(),
trace_id: "chat-seed-selected-org",
debug: {
assistant_known_organizations: ["ООО Альтернатива Плюс", "ООО Лайсвуд"],
assistant_selected_organization: "ООО Альтернатива Плюс",
assistant_active_organization: "ООО Альтернатива Плюс"
}
} as any);
const addressQueryService = {
tryHandle: vi.fn().mockResolvedValue({ handled: false })
} as any;
const chatClient = {
chat: vi.fn().mockResolvedValue({
raw: { id: "chat-affective-reaction" },
outputText: "Понимаю, это выглядит неприятно. Давай разберем следующий шаг.",
usage: { input_tokens: 0, output_tokens: 0, total_tokens: 0 }
})
} as any;
const service = new AssistantService(normalizer as any, sessions, undefined as any, undefined as any, addressQueryService, chatClient);
const response = await service.handleMessage({
session_id: sessionId,
user_message: "ну ахуеть",
llmProvider: "local",
model: "qwen2.5",
useMock: false
} as any);
expect(response.ok).toBe(true);
expect(response.reply_type).toBe("factual_with_explanation");
expect(response.debug?.living_chat_response_source).not.toBe("deterministic_data_scope_selection_contract");
expect(String(response.assistant_reply)).not.toContain("фиксирую рабочую организацию");
expect(chatClient.chat).toHaveBeenCalledTimes(1);
});
});