Governed substrate for autonomous agents: scoped identity (passports), audited actions, MCP workspace. Infra IPs and secrets redacted for public release.
215 lines
6.6 KiB
JavaScript
215 lines
6.6 KiB
JavaScript
/**
|
|
* ADR-026: Tables Summary Variable API Tests
|
|
* POST /api/v3/tables/:tableId/columns/:columnId/summary-variable
|
|
*/
|
|
|
|
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
|
|
// Mock database functions
|
|
const mockDbGet = vi.fn();
|
|
const mockDbRun = vi.fn();
|
|
const mockDbAll = vi.fn();
|
|
|
|
vi.mock('../../database/connection', () => ({
|
|
dbGet: (...args) => mockDbGet(...args),
|
|
dbRun: (...args) => mockDbRun(...args),
|
|
dbAll: (...args) => mockDbAll(...args),
|
|
}));
|
|
|
|
// Import after mocks
|
|
import { createSummaryVariable, AGGREGATION_FORMULAS } from '../SummaryVariableService.js';
|
|
|
|
describe('SummaryVariableService', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
describe('createSummaryVariable', () => {
|
|
const mockTable = {
|
|
id: 42,
|
|
name: 'orders',
|
|
project_id: 10,
|
|
};
|
|
|
|
const mockColumn = {
|
|
id: 'col_revenue',
|
|
name: 'revenue',
|
|
type: 'number',
|
|
table_id: 42,
|
|
};
|
|
|
|
const mockSpace = {
|
|
id: 1,
|
|
name: 'Test Space',
|
|
};
|
|
|
|
beforeEach(() => {
|
|
// Setup default mocks
|
|
mockDbGet.mockImplementation((query) => {
|
|
if (query.includes('universal_tables')) return Promise.resolve(mockTable);
|
|
if (query.includes('table_columns')) return Promise.resolve(mockColumn);
|
|
if (query.includes('projects') && query.includes('spaces')) return Promise.resolve({ space_id: 1 });
|
|
if (query.includes('spaces')) return Promise.resolve(mockSpace);
|
|
return Promise.resolve(null);
|
|
});
|
|
|
|
mockDbAll.mockResolvedValue([]);
|
|
mockDbRun.mockResolvedValue({ lastID: 123, changes: 1 });
|
|
});
|
|
|
|
it('creates variable with correct formula for SUM', async () => {
|
|
const result = await createSummaryVariable({
|
|
tableId: 42,
|
|
columnId: 'col_revenue',
|
|
aggregation: 'sum',
|
|
userId: 1,
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.variable.name).toBe('$revenue_sum');
|
|
expect(result.variable.formula).toBe('SUM({{revenue}})');
|
|
expect(result.variable.scope).toBe('table');
|
|
expect(result.variable.scopeRef).toBe(42);
|
|
});
|
|
|
|
it('creates variable with correct formula for AVG', async () => {
|
|
const result = await createSummaryVariable({
|
|
tableId: 42,
|
|
columnId: 'col_revenue',
|
|
aggregation: 'avg',
|
|
userId: 1,
|
|
});
|
|
|
|
expect(result.variable.formula).toBe('AVG({{revenue}})');
|
|
expect(result.variable.name).toBe('$revenue_avg');
|
|
});
|
|
|
|
it('creates variable with correct formula for MIN', async () => {
|
|
const result = await createSummaryVariable({
|
|
tableId: 42,
|
|
columnId: 'col_revenue',
|
|
aggregation: 'min',
|
|
userId: 1,
|
|
});
|
|
|
|
expect(result.variable.formula).toBe('MIN({{revenue}})');
|
|
expect(result.variable.name).toBe('$revenue_min');
|
|
});
|
|
|
|
it('creates variable with correct formula for MAX', async () => {
|
|
const result = await createSummaryVariable({
|
|
tableId: 42,
|
|
columnId: 'col_revenue',
|
|
aggregation: 'max',
|
|
userId: 1,
|
|
});
|
|
|
|
expect(result.variable.formula).toBe('MAX({{revenue}})');
|
|
expect(result.variable.name).toBe('$revenue_max');
|
|
});
|
|
|
|
it('creates variable with correct formula for COUNT', async () => {
|
|
const result = await createSummaryVariable({
|
|
tableId: 42,
|
|
columnId: 'col_revenue',
|
|
aggregation: 'count',
|
|
userId: 1,
|
|
});
|
|
|
|
expect(result.variable.formula).toBe('COUNT({{revenue}})');
|
|
expect(result.variable.name).toBe('$revenue_count');
|
|
});
|
|
|
|
it('throws error for invalid aggregation type', async () => {
|
|
await expect(
|
|
createSummaryVariable({
|
|
tableId: 42,
|
|
columnId: 'col_revenue',
|
|
aggregation: 'invalid',
|
|
userId: 1,
|
|
})
|
|
).rejects.toThrow('Invalid aggregation type');
|
|
});
|
|
|
|
it('throws error if table not found', async () => {
|
|
mockDbGet.mockImplementation((query) => {
|
|
if (query.includes('universal_tables')) return Promise.resolve(null);
|
|
return Promise.resolve(null);
|
|
});
|
|
|
|
await expect(
|
|
createSummaryVariable({
|
|
tableId: 999,
|
|
columnId: 'col_revenue',
|
|
aggregation: 'sum',
|
|
userId: 1,
|
|
})
|
|
).rejects.toThrow('Table not found');
|
|
});
|
|
|
|
it('throws error if column not found', async () => {
|
|
mockDbGet.mockImplementation((query) => {
|
|
if (query.includes('universal_tables')) return Promise.resolve(mockTable);
|
|
if (query.includes('table_columns')) return Promise.resolve(null);
|
|
return Promise.resolve(null);
|
|
});
|
|
|
|
await expect(
|
|
createSummaryVariable({
|
|
tableId: 42,
|
|
columnId: 'nonexistent',
|
|
aggregation: 'sum',
|
|
userId: 1,
|
|
})
|
|
).rejects.toThrow('Column not found');
|
|
});
|
|
|
|
it('generates unique name if variable already exists', async () => {
|
|
// First call: variable exists
|
|
mockDbAll.mockResolvedValueOnce([{ name: '$revenue_sum' }]);
|
|
|
|
const result = await createSummaryVariable({
|
|
tableId: 42,
|
|
columnId: 'col_revenue',
|
|
aggregation: 'sum',
|
|
userId: 1,
|
|
});
|
|
|
|
// Should have suffix to make unique
|
|
expect(result.variable.name).toMatch(/\$revenue_sum_\d+/);
|
|
});
|
|
|
|
it('uses custom variable name if provided', async () => {
|
|
const result = await createSummaryVariable({
|
|
tableId: 42,
|
|
columnId: 'col_revenue',
|
|
aggregation: 'sum',
|
|
userId: 1,
|
|
variableName: '$my_custom_sum',
|
|
});
|
|
|
|
expect(result.variable.name).toBe('$my_custom_sum');
|
|
});
|
|
});
|
|
|
|
describe('AGGREGATION_FORMULAS', () => {
|
|
it('has all required aggregation types', () => {
|
|
expect(AGGREGATION_FORMULAS).toHaveProperty('sum');
|
|
expect(AGGREGATION_FORMULAS).toHaveProperty('avg');
|
|
expect(AGGREGATION_FORMULAS).toHaveProperty('min');
|
|
expect(AGGREGATION_FORMULAS).toHaveProperty('max');
|
|
expect(AGGREGATION_FORMULAS).toHaveProperty('count');
|
|
expect(AGGREGATION_FORMULAS).toHaveProperty('countUnique');
|
|
expect(AGGREGATION_FORMULAS).toHaveProperty('countEmpty');
|
|
expect(AGGREGATION_FORMULAS).toHaveProperty('countFilled');
|
|
});
|
|
|
|
it('generates correct formula templates', () => {
|
|
expect(AGGREGATION_FORMULAS.sum('amount')).toBe('SUM({{amount}})');
|
|
expect(AGGREGATION_FORMULAS.avg('price')).toBe('AVG({{price}})');
|
|
expect(AGGREGATION_FORMULAS.min('date')).toBe('MIN({{date}})');
|
|
expect(AGGREGATION_FORMULAS.max('score')).toBe('MAX({{score}})');
|
|
expect(AGGREGATION_FORMULAS.count('items')).toBe('COUNT({{items}})');
|
|
});
|
|
});
|
|
});
|