Governed substrate for autonomous agents: scoped identity (passports), audited actions, MCP workspace. Infra IPs and secrets redacted for public release.
253 lines
8.5 KiB
JavaScript
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);
|
|
});
|
|
});
|
|
});
|