godcrm/backend/utils/__tests__/formulaParser.test.js
GOD CRM Release f89e074dd1
Some checks failed
CI / Lint / Typecheck / Test / Build (push) Has been cancelled
CI / PostgreSQL Integration Tests (push) Has been cancelled
GOD CRM — public scrubbed snapshot
Governed substrate for autonomous agents: scoped identity (passports),
audited actions, MCP workspace. Infra IPs and secrets redacted for public release.
2026-08-10 04:01:45 +03:00

253 lines
8.5 KiB
JavaScript

/**
* Formula Parser Tests - Sprint 1 (ADR-026)
* Testing formula dependency extraction and parsing
*
* 🔴 RED → 🟢 GREEN → 🔵 REFACTOR
*/
import { describe, test, expect } from 'vitest';
import {
parseFormulaDependencies,
extractColumns,
extractVariables,
extractFunctions,
FORMULA_FUNCTIONS
} from '../formulaParser.js';
// ============================================================
// TESTS
// ============================================================
describe('Formula Parser (ADR-026)', () => {
// ============================================================
// 🔴 RED PHASE: extractColumns
// ============================================================
describe('extractColumns()', () => {
/**
* BEHAVIOR: When parsing a formula
* GIVEN a formula with {{column_name}} references
* THEN return array of column names
*/
test('should extract single column reference', () => {
const result = extractColumns('SUM({{amount}})');
expect(result).toEqual(['amount']);
});
test('should extract multiple column references', () => {
const result = extractColumns('{{price}} * {{quantity}}');
expect(result).toEqual(['price', 'quantity']);
});
test('should handle formula without columns', () => {
const result = extractColumns('100 + 50');
expect(result).toEqual([]);
});
test('should handle complex formulas with nested columns', () => {
const result = extractColumns('IF({{status}} == "active", SUM({{revenue}}) - SUM({{costs}}), 0)');
expect(result).toEqual(['status', 'revenue', 'costs']);
});
test('should not duplicate column names', () => {
const result = extractColumns('{{amount}} + {{amount}}');
expect(result).toEqual(['amount']);
});
});
// ============================================================
// 🔴 RED PHASE: extractVariables
// ============================================================
describe('extractVariables()', () => {
/**
* BEHAVIOR: When parsing a formula
* GIVEN a formula with $variable_name references
* THEN return array of variable names
*/
test('should extract single variable reference', () => {
const result = extractVariables('$tax_rate * 100');
expect(result).toEqual(['$tax_rate']);
});
test('should extract multiple variable references', () => {
const result = extractVariables('$total_revenue - $total_costs');
expect(result).toEqual(['$total_revenue', '$total_costs']);
});
test('should handle formula without variables', () => {
const result = extractVariables('SUM({{amount}})');
expect(result).toEqual([]);
});
test('should handle complex variable names with underscores', () => {
const result = extractVariables('$margin_percent_ytd + $growth_rate_q1');
expect(result).toEqual(['$margin_percent_ytd', '$growth_rate_q1']);
});
test('should not duplicate variable names', () => {
const result = extractVariables('$tax * 2 + $tax * 3');
expect(result).toEqual(['$tax']);
});
test('should not confuse $variables with other $ patterns', () => {
// Price like $100 should not be captured
const result = extractVariables('$total + 100');
expect(result).toEqual(['$total']);
});
});
// ============================================================
// 🔴 RED PHASE: extractFunctions
// ============================================================
describe('extractFunctions()', () => {
/**
* BEHAVIOR: When parsing a formula
* GIVEN a formula with function calls
* THEN return array of function names (uppercase)
*/
test('should extract single function', () => {
const result = extractFunctions('SUM({{amount}})');
expect(result).toEqual(['SUM']);
});
test('should extract multiple functions', () => {
const result = extractFunctions('SUM({{revenue}}) / COUNT({{orders}})');
expect(result).toEqual(['SUM', 'COUNT']);
});
test('should extract nested functions', () => {
const result = extractFunctions('ROUND(AVG({{price}}), 2)');
expect(result).toContain('ROUND');
expect(result).toContain('AVG');
});
test('should handle formula without functions', () => {
const result = extractFunctions('{{price}} * {{quantity}}');
expect(result).toEqual([]);
});
test('should handle IF function', () => {
const result = extractFunctions('IF({{status}} == "active", 1, 0)');
expect(result).toEqual(['IF']);
});
});
// ============================================================
// 🔴 RED PHASE: parseFormulaDependencies
// ============================================================
describe('parseFormulaDependencies()', () => {
/**
* BEHAVIOR: Full dependency parsing
* GIVEN a complex formula
* THEN return columns, variables, and functions arrays
*/
test('should parse complex formula with all types of dependencies', () => {
const formula = 'SUM({{revenue}}) * $tax_rate + AVG({{costs}})';
const result = parseFormulaDependencies(formula);
expect(result.columns).toEqual(['revenue', 'costs']);
expect(result.variables).toEqual(['$tax_rate']);
expect(result.functions).toContain('SUM');
expect(result.functions).toContain('AVG');
});
test('should handle empty formula', () => {
const result = parseFormulaDependencies('');
expect(result.columns).toEqual([]);
expect(result.variables).toEqual([]);
expect(result.functions).toEqual([]);
});
test('should handle null formula', () => {
const result = parseFormulaDependencies(null);
expect(result.columns).toEqual([]);
expect(result.variables).toEqual([]);
expect(result.functions).toEqual([]);
});
test('should handle simple constant formula', () => {
const result = parseFormulaDependencies('0.20');
expect(result.columns).toEqual([]);
expect(result.variables).toEqual([]);
expect(result.functions).toEqual([]);
});
test('should parse chained variable formula', () => {
const formula = '$total_revenue * 0.3';
const result = parseFormulaDependencies(formula);
expect(result.columns).toEqual([]);
expect(result.variables).toEqual(['$total_revenue']);
});
test('should parse formula with COUNTIF', () => {
const formula = 'COUNTIF({{status}}, "active")';
const result = parseFormulaDependencies(formula);
expect(result.columns).toEqual(['status']);
expect(result.functions).toEqual(['COUNTIF']);
});
});
// ============================================================
// 🔴 RED PHASE: FORMULA_FUNCTIONS registry
// ============================================================
describe('FORMULA_FUNCTIONS', () => {
test('should have SUM function', () => {
expect(FORMULA_FUNCTIONS.SUM).toBeDefined();
expect(FORMULA_FUNCTIONS.SUM([1, 2, 3])).toBe(6);
});
test('should have AVG function', () => {
expect(FORMULA_FUNCTIONS.AVG).toBeDefined();
expect(FORMULA_FUNCTIONS.AVG([2, 4, 6])).toBe(4);
});
test('should have MIN function', () => {
expect(FORMULA_FUNCTIONS.MIN).toBeDefined();
expect(FORMULA_FUNCTIONS.MIN([3, 1, 2])).toBe(1);
});
test('should have MAX function', () => {
expect(FORMULA_FUNCTIONS.MAX).toBeDefined();
expect(FORMULA_FUNCTIONS.MAX([3, 1, 2])).toBe(3);
});
test('should have COUNT function', () => {
expect(FORMULA_FUNCTIONS.COUNT).toBeDefined();
expect(FORMULA_FUNCTIONS.COUNT([1, 2, 3, 4, 5])).toBe(5);
});
test('should have ROUND function', () => {
expect(FORMULA_FUNCTIONS.ROUND).toBeDefined();
expect(FORMULA_FUNCTIONS.ROUND(3.14159, 2)).toBe(3.14);
});
test('should have IF function', () => {
expect(FORMULA_FUNCTIONS.IF).toBeDefined();
expect(FORMULA_FUNCTIONS.IF(true, 'yes', 'no')).toBe('yes');
expect(FORMULA_FUNCTIONS.IF(false, 'yes', 'no')).toBe('no');
});
test('should have NOW function', () => {
expect(FORMULA_FUNCTIONS.NOW).toBeDefined();
const now = FORMULA_FUNCTIONS.NOW();
expect(now instanceof Date).toBe(true);
});
test('should have CONCAT function', () => {
expect(FORMULA_FUNCTIONS.CONCAT).toBeDefined();
expect(FORMULA_FUNCTIONS.CONCAT('Hello', ' ', 'World')).toBe('Hello World');
});
test('should have ABS function', () => {
expect(FORMULA_FUNCTIONS.ABS).toBeDefined();
expect(FORMULA_FUNCTIONS.ABS(-5)).toBe(5);
});
});
});