NODEDC_1C/llm_normalizer/backend/tests/assistantFollowupStateBindi...

319 lines
13 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 request from "supertest";
import { afterEach, describe, expect, it, vi } from "vitest";
const FLAG_KEYS = [
"FEATURE_ASSISTANT_INVESTIGATION_STATE_V1",
"FEATURE_ASSISTANT_STATE_FOLLOWUP_BINDING_V1",
"FEATURE_ASSISTANT_CONTRACTS_V11",
"FEATURE_ASSISTANT_PROBLEM_UNITS_V1",
"FEATURE_ASSISTANT_PROBLEM_UNIT_CONTINUITY_V1",
"FEATURE_ASSISTANT_ANSWER_POLICY_V11",
"FEATURE_ASSISTANT_PROBLEM_CENTRIC_ANSWER_V1"
] as const;
const ORIGINAL_FLAGS: Record<string, string | undefined> = Object.fromEntries(
FLAG_KEYS.map((key) => [key, process.env[key]])
);
function restoreFlags(): void {
for (const key of FLAG_KEYS) {
const original = ORIGINAL_FLAGS[key];
if (original === undefined) {
delete process.env[key];
} else {
process.env[key] = original;
}
}
}
async function createAppWithFlags(flags: {
state: "0" | "1";
binding: "0" | "1";
contracts?: "0" | "1";
problemUnits?: "0" | "1";
continuity?: "0" | "1";
answerPolicy?: "0" | "1";
problemCentric?: "0" | "1";
}) {
process.env.FEATURE_ASSISTANT_INVESTIGATION_STATE_V1 = flags.state;
process.env.FEATURE_ASSISTANT_STATE_FOLLOWUP_BINDING_V1 = flags.binding;
process.env.FEATURE_ASSISTANT_CONTRACTS_V11 = flags.contracts ?? "1";
process.env.FEATURE_ASSISTANT_PROBLEM_UNITS_V1 = flags.problemUnits ?? "0";
process.env.FEATURE_ASSISTANT_PROBLEM_UNIT_CONTINUITY_V1 = flags.continuity ?? "0";
process.env.FEATURE_ASSISTANT_ANSWER_POLICY_V11 = flags.answerPolicy ?? "0";
process.env.FEATURE_ASSISTANT_PROBLEM_CENTRIC_ANSWER_V1 = flags.problemCentric ?? "0";
vi.resetModules();
const { createApp } = await import("../src/server");
return createApp();
}
describe.sequential("assistant follow-up state binding", () => {
afterEach(() => {
restoreFlags();
vi.resetModules();
});
it("applies investigation_state binding in follow-up flow when flags are ON", async () => {
const app = await createAppWithFlags({
state: "1",
binding: "1"
});
const sessionId = `asst-wave2-on-${Date.now()}`;
const first = await request(app).post("/api/assistant/message").send({
session_id: sessionId,
useMock: true,
promptVersion: "normalizer_v2_0_2",
user_message: "Разложи цепочку документов по контрагентам."
});
expect(first.status).toBe(200);
expect(first.body.debug?.followup_state_usage).toBeUndefined();
expect(first.body.debug?.investigation_state_snapshot?.turn_index).toBe(1);
const second = await request(app).post("/api/assistant/message").send({
session_id: sessionId,
useMock: true,
promptVersion: "normalizer_v2_0_2",
user_message: "И по периоду 2020-06."
});
expect(second.status).toBe(200);
expect(second.body.debug?.followup_state_usage?.applied).toBe(true);
expect(second.body.debug?.followup_state_usage?.context_patch?.business_context_from_state).toBe(true);
expect(second.body.debug?.followup_state_usage?.state_turn_index).toBe(1);
expect(
(second.body.debug?.routes ?? []).some((item: { route?: string }) => item.route && item.route !== "no_route")
).toBe(true);
expect(second.body.debug?.investigation_state_snapshot?.turn_index).toBe(2);
});
it("does not apply follow-up binding when binding flag is OFF", async () => {
const app = await createAppWithFlags({
state: "1",
binding: "0"
});
const sessionId = `asst-wave2-off-${Date.now()}`;
const first = await request(app).post("/api/assistant/message").send({
session_id: sessionId,
useMock: true,
promptVersion: "normalizer_v2_0_2",
user_message: "Разложи цепочку документов по контрагентам."
});
expect(first.status).toBe(200);
const second = await request(app).post("/api/assistant/message").send({
session_id: sessionId,
useMock: true,
promptVersion: "normalizer_v2_0_2",
user_message: "И по периоду 2020-06."
});
expect(second.status).toBe(200);
expect(second.body.debug?.followup_state_usage).toBeUndefined();
expect((second.body.debug?.routes ?? []).every((item: { route?: string }) => item.route === "no_route")).toBe(true);
});
it("keeps legacy-like behavior when investigation state flag is OFF", async () => {
const app = await createAppWithFlags({
state: "0",
binding: "1"
});
const response = await request(app).post("/api/assistant/message").send({
useMock: true,
promptVersion: "normalizer_v2_0_2",
user_message: "И по периоду 2020-06."
});
expect(response.status).toBe(200);
expect(response.body.debug?.investigation_state_snapshot).toBeNull();
expect(response.body.debug?.followup_state_usage).toBeUndefined();
});
it("applies problem continuity hints only when continuity flag is ON and follow-up has no strong new anchors", async () => {
const app = await createAppWithFlags({
state: "1",
binding: "1",
problemUnits: "1",
continuity: "1"
});
const sessionId = `asst-wave4-problem-continuity-${Date.now()}`;
const first = await request(app).post("/api/assistant/message").send({
session_id: sessionId,
useMock: true,
promptVersion: "normalizer_v2_0_2",
user_message: "Разложи цепочку документов и оплат по контрагентам за 2020-06, где разрыв механизма закрытия."
});
expect(first.status).toBe(200);
expect(first.body.debug?.investigation_state_snapshot?.problem_unit_state).toBeTruthy();
const second = await request(app).post("/api/assistant/message").send({
session_id: sessionId,
useMock: true,
promptVersion: "normalizer_v2_0_2",
user_message: "И по тому же разрыву добавь уточнение."
});
expect(second.status).toBe(200);
expect(second.body.debug?.followup_state_usage?.applied).toBe(true);
expect(second.body.debug?.followup_state_usage?.context_patch?.problem_continuity_available).toBe(true);
expect(second.body.debug?.followup_state_usage?.context_patch?.problem_continuity_applied).toBe(true);
expect(second.body.debug?.followup_state_usage?.context_patch?.strong_new_anchor_detected).toBe(false);
});
it("does not apply follow-up continuity when user gives strong new anchors", async () => {
const app = await createAppWithFlags({
state: "1",
binding: "1",
problemUnits: "1",
continuity: "1"
});
const sessionId = `asst-wave4-strong-anchor-${Date.now()}`;
const first = await request(app).post("/api/assistant/message").send({
session_id: sessionId,
useMock: true,
promptVersion: "normalizer_v2_0_2",
user_message: "Разбери хвосты по счету 60 за 2020-06 и покажи проблемные цепочки."
});
expect(first.status).toBe(200);
expect(first.body.debug?.investigation_state_snapshot?.turn_index).toBe(1);
const second = await request(app).post("/api/assistant/message").send({
session_id: sessionId,
useMock: true,
promptVersion: "normalizer_v2_0_2",
user_message: "И отдельно по счету 97 за 2020-07."
});
expect(second.status).toBe(200);
expect(second.body.debug?.followup_state_usage).toBeUndefined();
expect(second.body.debug?.investigation_state_snapshot?.turn_index).toBe(2);
});
it("isolates scope for independent cross-domain turn and does not carry stale period/domain", async () => {
const app = await createAppWithFlags({
state: "1",
binding: "1",
problemUnits: "1",
continuity: "1",
answerPolicy: "1",
problemCentric: "1"
});
const sessionId = `asst-wave17-scope-isolation-${Date.now()}`;
const first = await request(app).post("/api/assistant/message").send({
session_id: sessionId,
useMock: true,
promptVersion: "normalizer_v2_0_2",
user_message: "Проверь хвосты по счету 60.01 за 2020-06 и почему не закрылся долг."
});
expect(first.status).toBe(200);
expect(first.body.debug?.investigation_state_snapshot?.question_scope_id).toContain("d:settlements_60_62");
expect(first.body.debug?.investigation_state_snapshot?.scope_origin).toBe("explicit_from_message");
const second = await request(app).post("/api/assistant/message").send({
session_id: sessionId,
useMock: true,
promptVersion: "normalizer_v2_0_2",
user_message: "Проверь НДС по счету-фактуре: где разрыв в цепочке документ -> регистр -> книга?"
});
expect(second.status).toBe(200);
expect(second.body.debug?.followup_state_usage).toBeUndefined();
expect(second.body.debug?.investigation_state_snapshot?.scope_origin).toBe("explicit_from_message");
expect(String(second.body.debug?.investigation_state_snapshot?.question_scope_id ?? "")).toContain(
"d:vat_document_register_book"
);
expect(second.body.debug?.investigation_state_snapshot?.focus?.period).not.toBe("2020-06");
expect(String(second.body.debug?.investigation_state_snapshot?.focus?.domain ?? "")).not.toContain("settlements_60_62");
expect(second.body.debug?.investigation_state_snapshot?.followup_context?.question_scope_id).toBeTruthy();
expect(second.body.debug?.investigation_state_snapshot?.followup_context?.scope_origin).toBe("explicit_from_message");
});
it("rebinds follow-up domain away from settlements on fixed-asset amortization query", async () => {
const app = await createAppWithFlags({
state: "1",
binding: "1",
problemUnits: "1",
continuity: "1",
answerPolicy: "1",
problemCentric: "1"
});
const sessionId = `asst-wave16-fa-domain-${Date.now()}`;
const first = await request(app).post("/api/assistant/message").send({
session_id: sessionId,
useMock: true,
promptVersion: "normalizer_v2_0_2",
user_message: "Почему деньги ушли, а долг по 60.01/62.02 остался?"
});
expect(first.status).toBe(200);
expect(first.body.debug?.investigation_state_snapshot?.followup_context?.active_domain).toBe("settlements_60_62");
const second = await request(app).post("/api/assistant/message").send({
session_id: sessionId,
useMock: true,
promptVersion: "normalizer_v2_0_2",
user_message:
"Полно ли начислена амортизация по объектам ОС за июль? Проверь по 01/02, нет ли пропущенных объектов."
});
expect(second.status).toBe(200);
const activeDomain = String(second.body.debug?.investigation_state_snapshot?.followup_context?.active_domain ?? "");
expect(activeDomain).not.toBe("settlements_60_62");
expect(activeDomain).toMatch(/fixed_asset_amortization|month_close_costs_20_44|no_route|hybrid_store_plus_live|fixed_asset/i);
const settlementActions = second.body.debug?.investigation_state_snapshot?.followup_context?.settlement_next_actions;
expect(Array.isArray(settlementActions) ? settlementActions.length : 0).toBe(0);
});
it("keeps UTF-8 follow-up period refinement in-scope with soft continuity hints", async () => {
const app = await createAppWithFlags({
state: "1",
binding: "1",
problemUnits: "1",
continuity: "1",
answerPolicy: "1",
problemCentric: "1"
});
const first = await request(app).post("/api/assistant/message").send({
useMock: true,
promptVersion: "normalizer_v2_0_2",
user_message: "Посмотри, пожалуйста, где по поставщикам сейчас хвосты уже похожи именно на проблему, а не просто на шум."
});
expect(first.status).toBe(200);
expect(first.body.reply_type).not.toBe("out_of_scope");
const second = await request(app).post("/api/assistant/message").send({
session_id: first.body.session_id,
useMock: true,
promptVersion: "normalizer_v2_0_2",
user_message: "А если только за июнь 2020 смотреть, по кому это сильнее всего видно и что из этого реально может мешать закрытию?"
});
expect(second.status).toBe(200);
expect(second.body.reply_type).not.toBe("out_of_scope");
expect(second.body.debug?.followup_state_usage?.applied).toBe(true);
expect(typeof second.body.debug?.followup_state_usage?.context_patch?.problem_continuity_applied).toBe("boolean");
expect(second.body.debug?.followup_state_usage?.context_patch?.strong_new_anchor_detected).toBe(false);
expect(
(second.body.debug?.routes ?? []).some((item: { route?: string }) => item.route && item.route !== "no_route")
).toBe(true);
const third = await request(app).post("/api/assistant/message").send({
useMock: true,
promptVersion: "normalizer_v2_0_2",
user_message: "Проверь, пожалуйста, по 60-му счёту за июнь 2020, где есть самый явный проблемный участок по расчётам с поставщиками."
});
expect(third.status).toBe(200);
expect(third.body.reply_type).not.toBe("out_of_scope");
});
});