Governed substrate for autonomous agents: scoped identity (passports), audited actions, MCP workspace. Infra IPs and secrets redacted for public release.
216 lines
7 KiB
JavaScript
216 lines
7 KiB
JavaScript
/**
|
|
* Space Variables API Tests (ADR-026)
|
|
* Testing REST API endpoints for space variables
|
|
*
|
|
* 🔴 RED → 🟢 GREEN → 🔵 REFACTOR
|
|
*/
|
|
|
|
import { describe, test, expect, beforeEach, afterEach } from 'vitest';
|
|
import request from 'supertest';
|
|
import express from 'express';
|
|
import spacesRoutes from '../spaces.js';
|
|
import { dbAll, dbGet, dbRun, destroyAdapter, resetAdapter } from '../../../database/connection.js';
|
|
import { ensureCoreSystemTablesForSpace } from '../../../services/SystemTablesCreator.js';
|
|
|
|
// Create test app
|
|
const app = express();
|
|
app.use(express.json());
|
|
|
|
// Mock authenticate middleware
|
|
app.use((req, res, next) => {
|
|
req.user = { id: 1, role: 'owner' };
|
|
next();
|
|
});
|
|
|
|
app.use('/api/v3/spaces', spacesRoutes);
|
|
|
|
// ============================================================
|
|
// Helper Functions
|
|
// ============================================================
|
|
|
|
async function createTestUser() {
|
|
const uniqueEmail = `test-spacevar-${Date.now()}@hltrn.cc`;
|
|
const result = await dbRun(
|
|
'INSERT INTO users (email, password_hash, name, encryption_key_encrypted, email_verified) VALUES (?, ?, ?, ?, ?)',
|
|
[uniqueEmail, 'hash', 'Test User', 'encrypted_key', 1]
|
|
);
|
|
return result.lastInsertRowid;
|
|
}
|
|
|
|
async function createTestSpace(ownerId) {
|
|
const result = await dbRun(
|
|
'INSERT INTO spaces (owner_id, name, type) VALUES (?, ?, ?)',
|
|
[ownerId, 'Test Space', 'business']
|
|
);
|
|
return result.lastInsertRowid;
|
|
}
|
|
|
|
async function addVariableToTable(tableId, variableData) {
|
|
const data = JSON.stringify({
|
|
name: variableData.name,
|
|
scope_type: variableData.scope_type || 'space',
|
|
scope_ref: variableData.scope_ref || null,
|
|
formula: variableData.formula || '',
|
|
description: variableData.description || '',
|
|
stream_id: variableData.stream_id || 1,
|
|
order_index: variableData.order_index || 0,
|
|
cached_value: variableData.cached_value || null,
|
|
cached_at: null,
|
|
dependencies: JSON.stringify([])
|
|
});
|
|
|
|
// Generate unique base_id (required field)
|
|
const baseId = `var_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
|
|
const result = await dbRun(
|
|
'INSERT INTO table_rows (table_id, base_id, data) VALUES (?, ?, ?)',
|
|
[tableId, baseId, data]
|
|
);
|
|
return result.lastInsertRowid;
|
|
}
|
|
|
|
// ============================================================
|
|
// TESTS
|
|
// ============================================================
|
|
|
|
describe('Space Variables API (ADR-026)', () => {
|
|
let userId;
|
|
let spaceId;
|
|
let variablesTableId;
|
|
|
|
beforeEach(async () => {
|
|
process.env.TEST_MODE = 'true';
|
|
process.env.SKIP_DEV_USER = 'true';
|
|
await resetAdapter();
|
|
|
|
// Create test data
|
|
userId = await createTestUser();
|
|
spaceId = await createTestSpace(userId);
|
|
|
|
// Ensure system tables including Variables
|
|
const systemTables = await ensureCoreSystemTablesForSpace(spaceId);
|
|
variablesTableId = systemTables.variablesTableId;
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await destroyAdapter();
|
|
});
|
|
|
|
// ============================================================
|
|
// 🔴 RED PHASE: GET /api/v3/spaces/:spaceId/variables
|
|
// ============================================================
|
|
|
|
describe('GET /api/v3/spaces/:spaceId/variables', () => {
|
|
/**
|
|
* BEHAVIOR: When I request variables for a space
|
|
* GIVEN a space with Variables table
|
|
* THEN I should get the tableId and list of variables
|
|
*/
|
|
test('should return variables table info and empty list for new space', async () => {
|
|
const response = await request(app)
|
|
.get(`/api/v3/spaces/${spaceId}/variables`)
|
|
.expect(200);
|
|
|
|
expect(response.body.success).toBe(true);
|
|
expect(response.body.data).toBeDefined();
|
|
expect(response.body.data.tableId).toBe(variablesTableId);
|
|
expect(response.body.data.variables).toEqual([]);
|
|
});
|
|
|
|
test('should return all variables with their values', async () => {
|
|
// Add some test variables
|
|
await addVariableToTable(variablesTableId, {
|
|
name: '$tax_rate',
|
|
scope_type: 'space',
|
|
formula: '0.20',
|
|
cached_value: '0.20'
|
|
});
|
|
await addVariableToTable(variablesTableId, {
|
|
name: '$total_revenue',
|
|
scope_type: 'table',
|
|
scope_ref: 15,
|
|
formula: 'SUM({{amount}})',
|
|
cached_value: '150000'
|
|
});
|
|
|
|
const response = await request(app)
|
|
.get(`/api/v3/spaces/${spaceId}/variables`)
|
|
.expect(200);
|
|
|
|
expect(response.body.success).toBe(true);
|
|
expect(response.body.data.variables).toHaveLength(2);
|
|
|
|
const taxVar = response.body.data.variables.find(v => v.name === '$tax_rate');
|
|
expect(taxVar).toBeDefined();
|
|
expect(taxVar.scope).toBe('space');
|
|
expect(taxVar.value).toBe('0.20');
|
|
|
|
const revenueVar = response.body.data.variables.find(v => v.name === '$total_revenue');
|
|
expect(revenueVar).toBeDefined();
|
|
expect(revenueVar.scope).toBe('table');
|
|
expect(revenueVar.scopeRef).toBe(15);
|
|
});
|
|
|
|
test('should return 404 for non-existent space', async () => {
|
|
const response = await request(app)
|
|
.get('/api/v3/spaces/999999/variables')
|
|
.expect(404);
|
|
|
|
expect(response.body.success).toBe(false);
|
|
expect(response.body.error.code).toBe('NOT_FOUND');
|
|
});
|
|
});
|
|
|
|
// ============================================================
|
|
// 🔴 RED PHASE: POST /api/v3/spaces/:spaceId/variables/recalculate
|
|
// ============================================================
|
|
|
|
describe('POST /api/v3/spaces/:spaceId/variables/recalculate', () => {
|
|
/**
|
|
* BEHAVIOR: When I trigger recalculation of all variables
|
|
* GIVEN a space with variables
|
|
* THEN all variable values should be recalculated
|
|
*/
|
|
test('should recalculate all variables and return status', async () => {
|
|
// Add test variable
|
|
await addVariableToTable(variablesTableId, {
|
|
name: '$simple_const',
|
|
scope_type: 'space',
|
|
formula: '42',
|
|
cached_value: null
|
|
});
|
|
|
|
const response = await request(app)
|
|
.post(`/api/v3/spaces/${spaceId}/variables/recalculate`)
|
|
.expect(200);
|
|
|
|
expect(response.body.success).toBe(true);
|
|
expect(response.body.data).toBeDefined();
|
|
expect(typeof response.body.data.calculated).toBe('number');
|
|
expect(typeof response.body.data.cached).toBe('number');
|
|
expect(Array.isArray(response.body.data.errors)).toBe(true);
|
|
});
|
|
|
|
test('should update cached_value after recalculation', async () => {
|
|
// Add test variable with simple numeric formula
|
|
const rowId = await addVariableToTable(variablesTableId, {
|
|
name: '$pi',
|
|
scope_type: 'space',
|
|
formula: '3.14159',
|
|
cached_value: null
|
|
});
|
|
|
|
await request(app)
|
|
.post(`/api/v3/spaces/${spaceId}/variables/recalculate`)
|
|
.expect(200);
|
|
|
|
// Check that cached_value was updated
|
|
const row = await dbGet(
|
|
'SELECT data FROM table_rows WHERE id = ?',
|
|
[rowId]
|
|
);
|
|
const data = typeof row.data === 'string' ? JSON.parse(row.data) : row.data;
|
|
expect(data.cached_value).toBe('3.14159');
|
|
});
|
|
});
|
|
});
|