import request from "supertest"; import { afterEach, describe, expect, it, vi } from "vitest"; const FLAG_KEYS = [ "FEATURE_ASSISTANT_PROBLEM_UNITS_V1", "FEATURE_ASSISTANT_ANSWER_POLICY_V11", "FEATURE_ASSISTANT_BROAD_GUARD_V1", "FEATURE_ASSISTANT_MIN_EVIDENCE_GATE_V1", "FEATURE_ASSISTANT_ANTI_GENERIC_RANKING_GUARD_V1" ] as const; const ORIGINAL_FLAGS: Record = 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 createAppWithProblemUnitsFlag(flagValue: "0" | "1") { process.env.FEATURE_ASSISTANT_PROBLEM_UNITS_V1 = flagValue; process.env.FEATURE_ASSISTANT_ANSWER_POLICY_V11 = "1"; process.env.FEATURE_ASSISTANT_BROAD_GUARD_V1 = "1"; process.env.FEATURE_ASSISTANT_MIN_EVIDENCE_GATE_V1 = "1"; process.env.FEATURE_ASSISTANT_ANTI_GENERIC_RANKING_GUARD_V1 = "1"; vi.resetModules(); const { createApp } = await import("../src/server"); return createApp(); } function routedRetrievalResults(body: Record): Record[] { const results = Array.isArray((body.debug as { retrieval_results?: unknown[] } | undefined)?.retrieval_results) ? ((body.debug as { retrieval_results?: unknown[] }).retrieval_results as Record[]) : []; return results.filter((item) => String(item.route ?? "") !== "no_route"); } describe.sequential("assistant problem-unit runtime rollout", () => { afterEach(() => { restoreFlags(); vi.resetModules(); }); it("emits problem-unit layer on problem-heavy scenarios when flag is ON", async () => { const app = await createAppWithProblemUnitsFlag("1"); const cases = [ { tag: "chain", user_message: "Разложи цепочку документов и оплат по контрагентам за 2020-06, где разрыв механизма закрытия." }, { tag: "anomaly", user_message: "Разложи lifecycle по счету 97 за 2020-06 и покажи аномалии списания по последовательности." }, { tag: "contradiction", user_message: "Проверь НДС за 2020-06: где противоречия между документами, проводками и регистрами." }, { tag: "period_risk", user_message: "Разложи по счетам 51 и 60 за 2020-06, что создаёт риск закрытия периода и где разрывы цепочки." } ]; const observedTypes = new Set(); let scenariosWithProblemUnits = 0; for (const scenario of cases) { const response = await request(app).post("/api/assistant/message").send({ useMock: true, promptVersion: "normalizer_v2_0_2", user_message: scenario.user_message }); expect(response.status).toBe(200); const routed = routedRetrievalResults(response.body as Record); expect(routed.length).toBeGreaterThan(0); const withProblemUnits = routed.filter((item) => Array.isArray(item.problem_units) && item.problem_units.length > 0); if (withProblemUnits.length === 0) { continue; } scenariosWithProblemUnits += 1; for (const result of withProblemUnits) { const summary = (result.summary as Record) ?? {}; const candidateEvidence = result.candidate_evidence as Array>; const problemUnits = result.problem_units as Array>; const problemSummary = (result.problem_unit_summary as Record) ?? {}; expect(summary.problem_units_enabled).toBe(true); expect(summary.candidate_evidence_count).toBe(candidateEvidence.length); expect(summary.problem_units_count).toBe(problemUnits.length); expect(Array.isArray(summary.problem_unit_types)).toBe(true); expect(typeof summary.problem_unit_duplicate_collapses).toBe("number"); expect(problemSummary.units_total).toBe(problemUnits.length); for (const unit of problemUnits) { expect(typeof unit.problem_unit_id).toBe("string"); expect(typeof unit.problem_unit_type).toBe("string"); expect(typeof unit.mechanism_summary).toBe("string"); expect(typeof unit.severity?.score).toBe("number"); expect(typeof unit.confidence?.score).toBe("number"); observedTypes.add(String(unit.problem_unit_type)); } } } expect(scenariosWithProblemUnits).toBeGreaterThan(0); expect(observedTypes.size).toBeGreaterThan(0); expect(Array.from(observedTypes).every((item) => [ "document_conflict", "broken_chain_segment", "lifecycle_anomaly_node", "unresolved_settlement_cluster", "period_risk_cluster", "cross_branch_inconsistency_cluster" ].includes(item) )).toBe(true); }); it("does not emit problem-unit layer when flag is OFF", async () => { const app = await createAppWithProblemUnitsFlag("0"); 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); const routed = routedRetrievalResults(response.body as Record); expect(routed.length).toBeGreaterThan(0); for (const result of routed) { expect(result.raw_entities).toBeUndefined(); expect(result.candidate_evidence).toBeUndefined(); expect(result.problem_units).toBeUndefined(); expect(result.problem_unit_summary).toBeUndefined(); expect((result.summary as Record).problem_units_enabled).toBeUndefined(); } }); });