import fs from "fs"; import os from "os"; import path from "path"; import { afterEach, describe, expect, it, vi } from "vitest"; const GRAPH_RUNTIME_FLAG = "FEATURE_ASSISTANT_GRAPH_RUNTIME_V1"; const ORIGINAL_GRAPH_RUNTIME_FLAG = process.env[GRAPH_RUNTIME_FLAG]; const TEMP_DIRS: string[] = []; function restoreGraphFlag(): void { if (ORIGINAL_GRAPH_RUNTIME_FLAG === undefined) { delete process.env[GRAPH_RUNTIME_FLAG]; return; } process.env[GRAPH_RUNTIME_FLAG] = ORIGINAL_GRAPH_RUNTIME_FLAG; } function cleanupTempDirs(): void { for (const dir of TEMP_DIRS.splice(0)) { fs.rmSync(dir, { recursive: true, force: true }); } } function createSnapshotRoot(records: Array>): string { const root = fs.mkdtempSync(path.join(os.tmpdir(), "assistant-graph-critical-")); TEMP_DIRS.push(root); fs.writeFileSync( path.resolve(root, "09_samples_key_fields_Recorder_Ref_Supplier_Buyer_Responsible.json"), JSON.stringify({ records }, null, 2), "utf-8" ); return root; } function buildRecord(input: { id: string; counterparty: string; description: string; account?: string; period?: string; unknownLinks?: number; withDocumentLink?: boolean; recorder?: string | null; }): Record { return { source_entity: "Document", source_id: input.id, display_name: input.id, unknown_link_count: input.unknownLinks ?? 0, attributes: { Recorder: input.recorder === null ? "" : (input.recorder ?? `${input.id}-REC`), Period: input.period ?? "2020-06-15T00:00:00", Description: input.description, Account: input.account ?? "60" }, links: [ { relation: "document_has_counterparty", target_entity: "Counterparty", target_id: input.counterparty, source_field: "Counterparty" }, ...(input.withDocumentLink === false ? [] : [ { relation: "document_refers_to_document", target_entity: "Document", target_id: `${input.id}-LINK`, source_field: "Recorder" } ]) ] }; } async function executeHybrid(input: { flag: "0" | "1"; query: string; records: Array>; }) { process.env[GRAPH_RUNTIME_FLAG] = input.flag; vi.resetModules(); const { AssistantDataLayer } = await import("../src/services/assistantDataLayer"); const dataLayer = new AssistantDataLayer(createSnapshotRoot(input.records)); return dataLayer.executeRoute("hybrid_store_plus_live", input.query); } function summaryObject(result: { summary: Record }): Record { return result.summary as Record; } function graphTraversal(result: { summary: Record }): Record { const summary = summaryObject(result); return (summary.graph_traversal as Record) ?? {}; } describe.sequential("stage4 graph critical supplemental coverage", () => { afterEach(() => { cleanupTempDirs(); restoreGraphFlag(); vi.resetModules(); }); it("captures neighbor branch lifting when linked branch is outside primary 97 scope", async () => { const result = await executeHybrid({ flag: "1", query: "Покажи по 97-му, где видно, что движение началось, но до ожидаемого закрытия не дошло.", records: [ buildRecord({ id: "NBR-1", counterparty: "CP-NBR", account: "97", description: "deferred expense writeoff vat invoice linked branch", period: "2020-06-21T00:00:00" }) ] }); const traversal = graphTraversal(result); expect(Number(traversal.neighbor_branch_lifted_candidates ?? 0)).toBeGreaterThan(0); expect((traversal.ranking_shift_signals as string[]).includes("neighbor_branch_lifting")).toBe(true); }); it("surfaces cross-branch inconsistency as graph-critical conflict signal", async () => { const result = await executeHybrid({ flag: "1", query: "Проверь по НДС, где документы и регистры показывают разную картину по одной и той же операции.", records: [ buildRecord({ id: "CBR-1", counterparty: "CP-CBR", account: "68", description: "bank payment vat invoice register conflict operation", period: "2020-06-22T00:00:00" }) ] }); const traversal = graphTraversal(result); const signalCounts = (traversal.signal_counts as Record) ?? {}; expect(Number(signalCounts.conflicting_transition ?? 0)).toBeGreaterThan(0); expect(Number(traversal.cross_branch_conflict_candidates ?? 0)).toBeGreaterThan(0); }); it("keeps terminal gap explicit instead of collapsing it into generic anomaly", async () => { const result = await executeHybrid({ flag: "1", query: "Что сейчас сильнее всего мешает закрытию периода не по отдельному документу, а по связанной цепочке операций?", records: [ buildRecord({ id: "TRM-1", counterparty: "CP-TRM", account: "97", description: "period close deferred expense chain almost completed", period: "2020-06-30T23:59:59", unknownLinks: 1 }) ] }); const traversal = graphTraversal(result); const signalCounts = (traversal.signal_counts as Record) ?? {}; expect(Number(signalCounts.terminal_state_gap ?? 0)).toBeGreaterThan(0); expect(Number(traversal.terminal_gap_candidates ?? 0)).toBeGreaterThan(0); }); it("shows ranking shift between graph OFF and graph ON for graph-critical contour", async () => { const records = [ buildRecord({ id: "RS-A", counterparty: "CP-A", account: "60", description: "supplier payment contract settlement contour", period: "2020-06-10T00:00:00" }), buildRecord({ id: "RS-B", counterparty: "CP-B", account: "60", description: "supplier payment contract lifecycle transition", period: "2020-06-30T23:59:59" }) ]; const query = "Покажи, где по 60-му счёту хвост выглядит не случайным, а похож на реально незавершённый контур."; const off = await executeHybrid({ flag: "0", query, records }); const on = await executeHybrid({ flag: "1", query, records }); const offItems = off.items as Array>; const onItems = on.items as Array>; expect(offItems.length).toBeGreaterThan(1); expect(onItems.length).toBeGreaterThan(1); expect(String(offItems[0]?.counterparty_id)).toBe("CP-A"); expect(String(onItems[0]?.counterparty_id)).toBe("CP-B"); const offSummary = summaryObject(off); const onSummary = summaryObject(on); expect(offSummary.graph_traversal_applied).toBe(false); expect(onSummary.graph_traversal_applied).toBe(true); }); it("confirms multi-hop traversal is used for chain reasoning", async () => { const result = await executeHybrid({ flag: "1", query: "Где лучше всего видно, что проблема сидит не в одном документе, а в разрыве между связанными объектами?", records: [ buildRecord({ id: "MHP-1", counterparty: "CP-MHP", account: "60", description: "supplier payment contract bank statement settlement contour", period: "2020-06-25T00:00:00" }) ] }); const traversal = graphTraversal(result); expect(Number(traversal.multi_hop_candidates ?? 0)).toBeGreaterThan(0); expect(Number(traversal.max_relation_hops ?? 0)).toBeGreaterThanOrEqual(2); }); it("keeps domain separation across deferred, fixed asset, vat, period close, bank and customer settlement", async () => { const genericRecords = [ buildRecord({ id: "DOM-1", counterparty: "CP-DOM", description: "generic accounting operation", account: "60" }) ]; const cases: Array<{ query: string; expectedDomain: string }> = [ { query: "Посмотри, пожалуйста, по поставщикам, где оплата прошла, а расчёт нормально не закрылся.", expectedDomain: "bank_settlement" }, { query: "Проверь по 97-му счёту, где расходы будущих периодов зависли и не дошли до нормального списания.", expectedDomain: "deferred_expense" }, { query: "Покажи по основным средствам, где карточка, документы и начисления между собой не бьются.", expectedDomain: "fixed_asset" }, { query: "Проверь по НДС, где документы и регистры показывают разную картину по одной и той же операции.", expectedDomain: "vat_flow" }, { query: "Что сейчас сильнее всего мешает закрытию периода не по отдельному документу, а по связанной цепочке операций?", expectedDomain: "period_close" }, { query: "Show customer payments where settlement did not close.", expectedDomain: "customer_settlement" } ]; const observedDomains: string[] = []; for (const testCase of cases) { const result = await executeHybrid({ flag: "1", query: testCase.query, records: genericRecords }); const traversal = graphTraversal(result); const targetDomains = Array.isArray(traversal.target_domains) ? (traversal.target_domains as string[]) : []; expect(targetDomains.includes(testCase.expectedDomain)).toBe(true); observedDomains.push(...targetDomains); const summary = summaryObject(result); expect(summary.graph_eligible).toBe(true); } const uniqueObserved = Array.from(new Set(observedDomains)); expect(uniqueObserved.length).toBeGreaterThanOrEqual(5); }); });