import { mkdtemp, readFile, rm } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { afterEach, describe, expect, it, vi } from "vitest"; import type { Catalog, CatalogComponent, FlowNode } from "../types"; import type { RunContext } from "../engine/types"; import { getHandler } from "../engine/registry"; import "./data-extended"; const ids = [ "Collection_Create", "Collection_HasItem", "Collection_Count", "Collection_Remove", "Collection_Add", "Ini_Read", "Ini_Write", "Ini_ListSection", "Ini_ListKey", "Ini_DeleteKey", "Ini_DeleteSection", "Log_Info", "Log_Error", "ExceptionUtil_Throws", "Bivartable_CreateDf", "Bivartable_CreateDfV1", "Bivartable_DoCreate", "Bivartable_GetMaxValue", "Bivartable_GetMinValue", "Bivartable_GetSumValue", "Bivartable_GetAvgValue", "Bivartable_SummaryCalculationDf", "Bivartable_DeleteDuplicateRow", "Bivartable_MergeDf", "Bivartable_DoMerge", "Bivartable_FilterDf", "Bivartable_DoFilter" ] as const; const temporaryDirectories: string[] = []; const log = vi.fn(); const context = { log } as unknown as RunContext; const component = {} as CatalogComponent; const node = (componentId: string): FlowNode => ({ id: `data_${componentId}`, componentId, position: { x: 0, y: 0 }, values: {}, options: {} }); async function temporaryDirectory(): Promise { const directory = await mkdtemp(path.join(os.tmpdir(), "rpa-data-extended-")); temporaryDirectories.push(directory); return directory; } async function run(componentId: string, args: Record = {}): Promise { const handler = getHandler(componentId); expect(handler, `${componentId} should be registered`).toBeTruthy(); return handler!(context, node(componentId), args, component); } afterEach(async () => { log.mockReset(); await Promise.all(temporaryDirectories.splice(0).map((directory) => rm(directory, { recursive: true, force: true }))); }); describe("extended local data capabilities", () => { it("registers only real catalog component ids", async () => { const catalog = JSON.parse(await readFile("resources/catalog.json", "utf8")) as Catalog; const catalogIds = new Set(catalog.components.map((item) => item.id)); expect(ids.length).toBeGreaterThanOrEqual(15); for (const id of ids) { expect(catalogIds.has(id), `${id} should exist in catalog`).toBe(true); expect(getHandler(id), `${id} should be registered`).toBeTruthy(); } }); it("creates and mutates unique local collections", async () => { const collection = await run("Collection_Create") as unknown[]; await run("Collection_Add", { collection, value: "A" }); await run("Collection_Add", { collection, value: "A" }); await run("Collection_Add", { collection, value: "B" }); await expect(run("Collection_HasItem", { collection, item: "A" })).resolves.toBe(true); await expect(run("Collection_Count", { collection })).resolves.toBe(2); await run("Collection_Remove", { collection, value: "A" }); expect(collection).toEqual(["B"]); }); it("persists, lists, reads, and deletes INI values", async () => { const directory = await temporaryDirectory(); const file = path.join(directory, "nested", "settings.ini"); await run("Ini_Write", { file, section: "App", key: "name", value: "RPA Studio" }); await run("Ini_Write", { file, section: "App", key: "workers", value: 3 }); await run("Ini_Write", { file, section: "Network", key: "timeout", value: 30 }); await expect(run("Ini_Read", { file, section: "App", key: "name" })).resolves.toBe("RPA Studio"); await expect(run("Ini_Read", { file, section: "Missing", key: "key", defaultValue: "fallback" })).resolves.toBe("fallback"); await expect(run("Ini_ListSection", { file })).resolves.toEqual(["App", "Network"]); await expect(run("Ini_ListKey", { file, section: "App" })).resolves.toEqual(["name", "workers"]); await run("Ini_DeleteKey", { file, section: "App", key: "workers" }); await run("Ini_DeleteSection", { file, section: "Network" }); await expect(run("Ini_ListKey", { file, section: "App" })).resolves.toEqual(["name"]); await expect(run("Ini_ListSection", { file })).resolves.toEqual(["App"]); await expect(readFile(file, "utf8")).resolves.toContain("name=RPA Studio"); }); it("writes runtime logs and raises a located user exception", async () => { await run("Log_Info", { msg: "started" }); await run("Log_Error", { msg: "failed" }); expect(log).toHaveBeenNthCalledWith(1, "started", "info", "data_Log_Info"); expect(log).toHaveBeenNthCalledWith(2, "failed", "error", "data_Log_Error"); await expect(run("ExceptionUtil_Throws", { msg: "stop now" })).rejects.toMatchObject({ name: "RuntimeError", code: "user_exception", message: "stop now", nodeId: "data_ExceptionUtil_Throws" }); }); it("creates tables from arrays, records, and column dictionaries", async () => { await expect(run("Bivartable_CreateDf", { data: [[1, 2], [3, 4]], columnList: ["A", "B"] })) .resolves.toEqual([["A", "B"], [1, 2], [3, 4]]); await expect(run("Bivartable_CreateDfV1", { data: [{ name: "A", score: 2 }, { name: "B", score: 4 }] })) .resolves.toEqual([["name", "score"], ["A", 2], ["B", 4]]); await expect(run("Bivartable_DoCreate", { data: { name: ["A", "B"], score: [2, 4] } })) .resolves.toEqual([["name", "score"], ["A", 2], ["B", 4]]); }); it("calculates scalar and grouped table aggregates", async () => { const input = [["team", "score"], ["A", 2], ["A", 4], ["B", 3]]; await expect(run("Bivartable_GetMaxValue", { df: input, maxvaluecols: "score" })).resolves.toBe(4); await expect(run("Bivartable_GetMinValue", { df: input, minvaluecols: "score" })).resolves.toBe(2); await expect(run("Bivartable_GetSumValue", { df: input, sumvaluecols: "score" })).resolves.toBe(9); await expect(run("Bivartable_GetAvgValue", { df: input, avgvaluecols: "score", groupby: "team" })) .resolves.toEqual([["team", "score"], ["A", 3], ["B", 3]]); await expect(run("Bivartable_SummaryCalculationDf", { df: input, name: "score", method: "sum" })).resolves.toBe(9); }); it("deduplicates, merges, and filters local tables", async () => { const duplicated = [["id", "name"], [1, "A"], [1, "A2"], [2, "B"]]; await expect(run("Bivartable_DeleteDuplicateRow", { df: duplicated, change_type: "name", col_name: ["id"], mode: "first" })) .resolves.toEqual([["id", "name"], [1, "A"], [2, "B"]]); const left = [["id", "name"], [1, "A"], [2, "B"]]; const right = [["id", "score"], [1, 90], [3, 70]]; await expect(run("Bivartable_MergeDf", { df1: left, df2: right, on_name: "id", merge_type: "left" })) .resolves.toEqual([["id", "name", "score"], [1, "A", 90], [2, "B", null]]); await expect(run("Bivartable_DoMerge", { df1: left, df2: right, onx: "id", howx: "inner" })) .resolves.toEqual([["id", "name", "score"], [1, "A", 90]]); await expect(run("Bivartable_FilterDf", { df: left, col_name: "id", choice: ">", criteria: 1 })) .resolves.toEqual([["id", "name"], [2, "B"]]); await expect(run("Bivartable_DoFilter", { df: [["id", "score"], [1, 70], [2, 90], [3, 95]], filter: "(df['id'] > 1) & (df['score'] >= 90)" })) .resolves.toEqual([["id", "score"], [2, 90], [3, 95]]); }); });