|
| 1 | +import { describe, it, expect } from "vitest"; |
| 2 | +import { CalculatorNode } from "./calculator-node"; |
| 3 | +import { Node } from "@dafthunk/types"; |
| 4 | +import { NodeContext } from "../types"; |
| 5 | + |
| 6 | +describe("CalculatorNode", () => { |
| 7 | + const createNode = (): Node => ({ |
| 8 | + id: "test-calculator", |
| 9 | + type: "calculator", |
| 10 | + position: { x: 0, y: 0 }, |
| 11 | + } as unknown as Node); |
| 12 | + |
| 13 | + const createContext = (inputs: Record<string, any>) => ({ |
| 14 | + nodeId: "test-calculator", |
| 15 | + inputs, |
| 16 | + workflowId: "test-workflow", |
| 17 | + executionId: "test-execution", |
| 18 | + organizationId: "test-org", |
| 19 | + env: {}, |
| 20 | + nodeRegistry: null, |
| 21 | + toolRegistry: null, |
| 22 | + } as unknown as NodeContext); |
| 23 | + |
| 24 | + describe("nodeType", () => { |
| 25 | + it("should have correct node type definition", () => { |
| 26 | + expect(CalculatorNode.nodeType).toEqual({ |
| 27 | + id: "calculator", |
| 28 | + name: "Calculator", |
| 29 | + type: "calculator", |
| 30 | + description: |
| 31 | + "Evaluates mathematical expressions with comprehensive support for arithmetic operations, mathematical functions, trigonometric functions, constants, and complex formulas. Supports: basic arithmetic (+, -, *, /, ^, %), bitwise operators (&, |, <, >, ~), mathematical functions (sqrt, cbrt, pow, exp, log, log10, abs, floor, ceil, round, min, max, sign, trunc, hypot), trigonometric functions (sin, cos, tan, asin, acos, atan, atan2, sinh, cosh, tanh, asinh, acosh, atanh), mathematical constants (PI, E), random numbers, and complex nested expressions with proper order of operations and parentheses. All inputs are validated for security and only mathematical operations are allowed.", |
| 32 | + tags: ["Math"], |
| 33 | + icon: "calculator", |
| 34 | + inlinable: true, |
| 35 | + asTool: true, |
| 36 | + inputs: [ |
| 37 | + { |
| 38 | + name: "expression", |
| 39 | + type: "string", |
| 40 | + description: |
| 41 | + "The mathematical expression to evaluate as a string. Examples: '2 + 3 * 4', 'sqrt(16)', 'sin(PI/2)', 'pow(2, 3)', 'abs(-5)', 'floor(3.7)', 'PI * 2^2', '(10 + 5) * 2 / 4', 'log(100)', 'random * 10', '17 % 5', '15 & 7', '8 | 4', '~10'. Supports all standard mathematical operations, functions, and constants. Use ^ for exponentiation (e.g., 2^3 = 8), % for modulo (e.g., 17 % 5 = 2), & for bitwise AND, | for bitwise OR, ~ for bitwise NOT. All expressions are validated for security.", |
| 42 | + required: true, |
| 43 | + }, |
| 44 | + ], |
| 45 | + outputs: [ |
| 46 | + { |
| 47 | + name: "result", |
| 48 | + type: "number", |
| 49 | + description: |
| 50 | + "The calculated result of the mathematical expression as a number. Returns the final computed value after evaluating the expression with proper order of operations. Will be NaN or throw an error for invalid expressions.", |
| 51 | + }, |
| 52 | + ], |
| 53 | + }); |
| 54 | + }); |
| 55 | + }); |
| 56 | + |
| 57 | + describe("execute", () => { |
| 58 | + it("should evaluate simple arithmetic expressions", async () => { |
| 59 | + const node = new CalculatorNode(createNode()); |
| 60 | + const context = createContext({ expression: "2 + 3" }); |
| 61 | + |
| 62 | + const result = await node.execute(context); |
| 63 | + |
| 64 | + expect(result.status).toBe("completed"); |
| 65 | + expect(result.outputs?.result).toBe(5); |
| 66 | + }); |
| 67 | + |
| 68 | + it("should handle multiplication and division", async () => { |
| 69 | + const node = new CalculatorNode(createNode()); |
| 70 | + const context = createContext({ expression: "10 * 5 / 2" }); |
| 71 | + |
| 72 | + const result = await node.execute(context); |
| 73 | + |
| 74 | + expect(result.status).toBe("completed"); |
| 75 | + expect(result.outputs?.result).toBe(25); |
| 76 | + }); |
| 77 | + |
| 78 | + it("should handle parentheses and order of operations", async () => { |
| 79 | + const node = new CalculatorNode(createNode()); |
| 80 | + const context = createContext({ expression: "(2 + 3) * 4" }); |
| 81 | + |
| 82 | + const result = await node.execute(context); |
| 83 | + |
| 84 | + expect(result.status).toBe("completed"); |
| 85 | + expect(result.outputs?.result).toBe(20); |
| 86 | + }); |
| 87 | + |
| 88 | + it("should handle exponentiation with ^ symbol", async () => { |
| 89 | + const node = new CalculatorNode(createNode()); |
| 90 | + const context = createContext({ expression: "2^3" }); |
| 91 | + |
| 92 | + const result = await node.execute(context); |
| 93 | + |
| 94 | + expect(result.status).toBe("completed"); |
| 95 | + expect(result.outputs?.result).toBe(8); |
| 96 | + }); |
| 97 | + |
| 98 | + it("should handle mathematical functions", async () => { |
| 99 | + const node = new CalculatorNode(createNode()); |
| 100 | + const context = createContext({ expression: "sqrt(16)" }); |
| 101 | + |
| 102 | + const result = await node.execute(context); |
| 103 | + |
| 104 | + expect(result.status).toBe("completed"); |
| 105 | + expect(result.outputs?.result).toBe(4); |
| 106 | + }); |
| 107 | + |
| 108 | + it("should handle trigonometric functions", async () => { |
| 109 | + const node = new CalculatorNode(createNode()); |
| 110 | + const context = createContext({ expression: "sin(0)" }); |
| 111 | + |
| 112 | + const result = await node.execute(context); |
| 113 | + |
| 114 | + expect(result.status).toBe("completed"); |
| 115 | + expect(result.outputs?.result).toBe(0); |
| 116 | + }); |
| 117 | + |
| 118 | + it("should handle constants", async () => { |
| 119 | + const node = new CalculatorNode(createNode()); |
| 120 | + const context = createContext({ expression: "PI" }); |
| 121 | + |
| 122 | + const result = await node.execute(context); |
| 123 | + |
| 124 | + expect(result.status).toBe("completed"); |
| 125 | + expect(result.outputs?.result).toBe(Math.PI); |
| 126 | + }); |
| 127 | + |
| 128 | + it("should handle complex expressions", async () => { |
| 129 | + const node = new CalculatorNode(createNode()); |
| 130 | + const context = createContext({ expression: "sqrt(16) + sin(0) * 5" }); |
| 131 | + |
| 132 | + const result = await node.execute(context); |
| 133 | + |
| 134 | + expect(result.status).toBe("completed"); |
| 135 | + expect(result.outputs?.result).toBe(4); |
| 136 | + }); |
| 137 | + |
| 138 | + it("should return error for empty expression", async () => { |
| 139 | + const node = new CalculatorNode(createNode()); |
| 140 | + const context = createContext({ expression: "" }); |
| 141 | + |
| 142 | + const result = await node.execute(context); |
| 143 | + |
| 144 | + expect(result.status).toBe("error"); |
| 145 | + expect(result.error).toBe("Missing or empty expression."); |
| 146 | + }); |
| 147 | + |
| 148 | + it("should return error for invalid characters", async () => { |
| 149 | + const node = new CalculatorNode(createNode()); |
| 150 | + const context = createContext({ expression: "alert('hello')" }); |
| 151 | + |
| 152 | + const result = await node.execute(context); |
| 153 | + |
| 154 | + expect(result.status).toBe("error"); |
| 155 | + expect(result.error).toContain("Expression contains invalid characters"); |
| 156 | + }); |
| 157 | + |
| 158 | + it("should return error for invalid mathematical expressions", async () => { |
| 159 | + const node = new CalculatorNode(createNode()); |
| 160 | + const context = createContext({ expression: "2 +" }); |
| 161 | + |
| 162 | + const result = await node.execute(context); |
| 163 | + |
| 164 | + expect(result.status).toBe("error"); |
| 165 | + }); |
| 166 | + |
| 167 | + it("should handle decimal numbers", async () => { |
| 168 | + const node = new CalculatorNode(createNode()); |
| 169 | + const context = createContext({ expression: "3.14 * 2" }); |
| 170 | + |
| 171 | + const result = await node.execute(context); |
| 172 | + |
| 173 | + expect(result.status).toBe("completed"); |
| 174 | + expect(result.outputs?.result).toBe(6.28); |
| 175 | + }); |
| 176 | + |
| 177 | + it("should handle modulo operator", async () => { |
| 178 | + const node = new CalculatorNode(createNode()); |
| 179 | + const context = createContext({ expression: "17 % 5" }); |
| 180 | + |
| 181 | + const result = await node.execute(context); |
| 182 | + |
| 183 | + expect(result.status).toBe("completed"); |
| 184 | + expect(result.outputs?.result).toBe(2); |
| 185 | + }); |
| 186 | + |
| 187 | + it("should handle bitwise AND operator", async () => { |
| 188 | + const node = new CalculatorNode(createNode()); |
| 189 | + const context = createContext({ expression: "15 & 7" }); |
| 190 | + |
| 191 | + const result = await node.execute(context); |
| 192 | + |
| 193 | + expect(result.status).toBe("completed"); |
| 194 | + expect(result.outputs?.result).toBe(7); |
| 195 | + }); |
| 196 | + |
| 197 | + it("should handle bitwise OR operator", async () => { |
| 198 | + const node = new CalculatorNode(createNode()); |
| 199 | + const context = createContext({ expression: "8 | 4" }); |
| 200 | + |
| 201 | + const result = await node.execute(context); |
| 202 | + |
| 203 | + expect(result.status).toBe("completed"); |
| 204 | + expect(result.outputs?.result).toBe(12); |
| 205 | + }); |
| 206 | + |
| 207 | + it("should handle bitwise NOT operator", async () => { |
| 208 | + const node = new CalculatorNode(createNode()); |
| 209 | + const context = createContext({ expression: "~10" }); |
| 210 | + |
| 211 | + const result = await node.execute(context); |
| 212 | + |
| 213 | + expect(result.status).toBe("completed"); |
| 214 | + expect(result.outputs?.result).toBe(-11); |
| 215 | + }); |
| 216 | + |
| 217 | + it("should handle additional math functions", async () => { |
| 218 | + const node = new CalculatorNode(createNode()); |
| 219 | + const context = createContext({ expression: "sign(-5) + trunc(3.7) + hypot(3, 4)" }); |
| 220 | + |
| 221 | + const result = await node.execute(context); |
| 222 | + |
| 223 | + expect(result.status).toBe("completed"); |
| 224 | + expect(result.outputs?.result).toBe(-1 + 3 + 5); // -1 + 3 + 5 = 7 |
| 225 | + }); |
| 226 | + |
| 227 | + it("should handle atan2 function", async () => { |
| 228 | + const node = new CalculatorNode(createNode()); |
| 229 | + const context = createContext({ expression: "atan2(1, 1)" }); |
| 230 | + |
| 231 | + const result = await node.execute(context); |
| 232 | + |
| 233 | + expect(result.status).toBe("completed"); |
| 234 | + expect(result.outputs?.result).toBe(Math.PI / 4); |
| 235 | + }); |
| 236 | + }); |
| 237 | +}); |
0 commit comments