godcrm/backend/utils/__tests__/dependencyGraph.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

304 lines
9.6 KiB
JavaScript

/**
* Dependency Graph Tests - Sprint 1 (ADR-026)
* Testing cycle detection and topological sorting for formula variables
*
* 🔴 RED → 🟢 GREEN → 🔵 REFACTOR
*/
import { describe, test, expect, beforeEach } from 'vitest';
import { DependencyGraph } from '../dependencyGraph.js';
// ============================================================
// TESTS
// ============================================================
describe('DependencyGraph (ADR-026)', () => {
let graph;
beforeEach(() => {
graph = new DependencyGraph();
});
// ============================================================
// 🔴 RED PHASE: addDependency
// ============================================================
describe('addDependency()', () => {
/**
* BEHAVIOR: When adding a dependency
* GIVEN two variable names
* THEN the dependency is recorded
*/
test('should add single dependency', () => {
graph.addDependency('$margin', '$revenue');
expect(graph.getDependencies('$margin')).toContain('$revenue');
});
test('should add multiple dependencies for same variable', () => {
graph.addDependency('$profit', '$revenue');
graph.addDependency('$profit', '$costs');
const deps = graph.getDependencies('$profit');
expect(deps).toContain('$revenue');
expect(deps).toContain('$costs');
});
test('should handle variables with no dependencies', () => {
graph.addNode('$constant');
expect(graph.getDependencies('$constant')).toEqual([]);
});
});
// ============================================================
// 🔴 RED PHASE: hasCycle
// ============================================================
describe('hasCycle()', () => {
/**
* BEHAVIOR: Detect circular dependencies
* GIVEN a graph with cycles
* THEN hasCycle returns true
*/
test('should return false for empty graph', () => {
expect(graph.hasCycle()).toBe(false);
});
test('should return false for graph without cycles', () => {
// $margin depends on $revenue and $costs
graph.addDependency('$margin', '$revenue');
graph.addDependency('$margin', '$costs');
// $profit depends on $margin
graph.addDependency('$profit', '$margin');
expect(graph.hasCycle()).toBe(false);
});
test('should detect direct cycle (A → B → A)', () => {
graph.addDependency('$a', '$b');
graph.addDependency('$b', '$a');
expect(graph.hasCycle()).toBe(true);
});
test('should detect indirect cycle (A → B → C → A)', () => {
graph.addDependency('$a', '$b');
graph.addDependency('$b', '$c');
graph.addDependency('$c', '$a');
expect(graph.hasCycle()).toBe(true);
});
test('should detect self-referencing cycle (A → A)', () => {
graph.addDependency('$recursive', '$recursive');
expect(graph.hasCycle()).toBe(true);
});
});
// ============================================================
// 🔴 RED PHASE: findCycle
// ============================================================
describe('findCycle()', () => {
/**
* BEHAVIOR: Find the cycle path
* GIVEN a graph with cycles
* THEN findCycle returns the cycle path
*/
test('should return null for graph without cycles', () => {
graph.addDependency('$a', '$b');
graph.addDependency('$b', '$c');
expect(graph.findCycle()).toBeNull();
});
test('should return cycle path', () => {
graph.addDependency('$a', '$b');
graph.addDependency('$b', '$a');
const cycle = graph.findCycle();
expect(cycle).not.toBeNull();
expect(Array.isArray(cycle)).toBe(true);
expect(cycle.length).toBeGreaterThan(1);
});
test('should return self-reference cycle', () => {
graph.addDependency('$x', '$x');
const cycle = graph.findCycle();
expect(cycle).not.toBeNull();
expect(cycle).toContain('$x');
});
});
// ============================================================
// 🔴 RED PHASE: topologicalSort
// ============================================================
describe('topologicalSort()', () => {
/**
* BEHAVIOR: Sort variables by dependency order
* GIVEN a DAG (directed acyclic graph)
* THEN return variables in calculation order
*/
test('should return empty array for empty graph', () => {
const sorted = graph.topologicalSort();
expect(sorted).toEqual([]);
});
test('should sort single variable', () => {
graph.addNode('$constant');
const sorted = graph.topologicalSort();
expect(sorted).toEqual(['$constant']);
});
test('should sort linear dependencies correctly', () => {
// $c depends on $b, $b depends on $a
graph.addDependency('$c', '$b');
graph.addDependency('$b', '$a');
graph.addNode('$a'); // No dependencies
const sorted = graph.topologicalSort();
// $a should come before $b, $b before $c
const indexA = sorted.indexOf('$a');
const indexB = sorted.indexOf('$b');
const indexC = sorted.indexOf('$c');
expect(indexA).toBeLessThan(indexB);
expect(indexB).toBeLessThan(indexC);
});
test('should sort complex dependencies correctly', () => {
// $total = $revenue - $costs
// $margin = $total * 0.3
// $tax = $margin * $tax_rate
graph.addDependency('$total', '$revenue');
graph.addDependency('$total', '$costs');
graph.addDependency('$margin', '$total');
graph.addDependency('$tax', '$margin');
graph.addDependency('$tax', '$tax_rate');
graph.addNode('$revenue');
graph.addNode('$costs');
graph.addNode('$tax_rate');
const sorted = graph.topologicalSort();
// Check order constraints
expect(sorted.indexOf('$revenue')).toBeLessThan(sorted.indexOf('$total'));
expect(sorted.indexOf('$costs')).toBeLessThan(sorted.indexOf('$total'));
expect(sorted.indexOf('$total')).toBeLessThan(sorted.indexOf('$margin'));
expect(sorted.indexOf('$margin')).toBeLessThan(sorted.indexOf('$tax'));
});
test('should return null for cyclic graph', () => {
graph.addDependency('$a', '$b');
graph.addDependency('$b', '$a');
const sorted = graph.topologicalSort();
expect(sorted).toBeNull();
});
});
// ============================================================
// 🔴 RED PHASE: getCalculationStreams
// ============================================================
describe('getCalculationStreams()', () => {
/**
* BEHAVIOR: Group variables into parallel calculation streams
* GIVEN a DAG
* THEN return map of stream number to variable arrays
*/
test('should return empty map for empty graph', () => {
const streams = graph.getCalculationStreams();
expect(streams.size).toBe(0);
});
test('should put independent variables in stream 1', () => {
graph.addNode('$a');
graph.addNode('$b');
graph.addNode('$c');
const streams = graph.getCalculationStreams();
expect(streams.get(1)).toContain('$a');
expect(streams.get(1)).toContain('$b');
expect(streams.get(1)).toContain('$c');
});
test('should put dependent variables in later streams', () => {
// Stream 1: $revenue, $costs (no deps)
// Stream 2: $total (depends on stream 1)
// Stream 3: $margin (depends on stream 2)
graph.addNode('$revenue');
graph.addNode('$costs');
graph.addDependency('$total', '$revenue');
graph.addDependency('$total', '$costs');
graph.addDependency('$margin', '$total');
const streams = graph.getCalculationStreams();
expect(streams.get(1)).toContain('$revenue');
expect(streams.get(1)).toContain('$costs');
expect(streams.get(2)).toContain('$total');
expect(streams.get(3)).toContain('$margin');
});
test('should return null for cyclic graph', () => {
graph.addDependency('$a', '$b');
graph.addDependency('$b', '$a');
const streams = graph.getCalculationStreams();
expect(streams).toBeNull();
});
});
// ============================================================
// 🔴 RED PHASE: getAllNodes
// ============================================================
describe('getAllNodes()', () => {
test('should return all nodes in graph', () => {
graph.addNode('$a');
graph.addNode('$b');
graph.addDependency('$c', '$a');
const nodes = graph.getAllNodes();
expect(nodes).toContain('$a');
expect(nodes).toContain('$b');
expect(nodes).toContain('$c');
});
});
// ============================================================
// 🔴 RED PHASE: getDependents
// ============================================================
describe('getDependents()', () => {
/**
* BEHAVIOR: Find variables that depend on this one
* GIVEN a variable name
* THEN return variables that depend on it (reverse deps)
*/
test('should return empty array for leaf node', () => {
graph.addDependency('$a', '$b');
const dependents = graph.getDependents('$a');
expect(dependents).toEqual([]);
});
test('should return dependents', () => {
graph.addDependency('$margin', '$revenue');
graph.addDependency('$profit', '$revenue');
const dependents = graph.getDependents('$revenue');
expect(dependents).toContain('$margin');
expect(dependents).toContain('$profit');
});
});
});