- Create benchmark test suites for critical operations: - Node loading performance - Database query performance - Search operations performance - Validation performance - MCP tool execution performance - Add GitHub Actions workflow for benchmark tracking: - Runs on push to main and PRs - Uses github-action-benchmark for historical tracking - Comments on PRs with performance results - Alerts on >10% performance regressions - Stores results in GitHub Pages - Create benchmark infrastructure: - Custom Vitest benchmark configuration - JSON reporter for CI results - Result formatter for github-action-benchmark - Performance threshold documentation - Add supporting utilities: - SQLiteStorageService for benchmark database setup - MCPEngine wrapper for testing MCP tools - Test factories for generating benchmark data - Enhanced NodeRepository with benchmark methods - Document benchmark system: - Comprehensive benchmark guide in docs/BENCHMARKS.md - Performance thresholds in .github/BENCHMARK_THRESHOLDS.md - README for benchmarks directory - Integration with existing test suite The benchmark system will help monitor performance over time and catch regressions before they reach production. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
399 lines
13 KiB
TypeScript
399 lines
13 KiB
TypeScript
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
import * as fs from 'fs';
|
|
import * as path from 'path';
|
|
import {
|
|
createTestDatabase,
|
|
seedTestNodes,
|
|
seedTestTemplates,
|
|
createTestNode,
|
|
createTestTemplate,
|
|
resetDatabase,
|
|
createDatabaseSnapshot,
|
|
restoreDatabaseSnapshot,
|
|
loadFixtures,
|
|
dbHelpers,
|
|
createMockDatabaseAdapter,
|
|
withTransaction,
|
|
measureDatabaseOperation,
|
|
TestDatabase
|
|
} from '../../utils/database-utils';
|
|
|
|
describe('Database Utils', () => {
|
|
let testDb: TestDatabase;
|
|
|
|
afterEach(async () => {
|
|
if (testDb) {
|
|
await testDb.cleanup();
|
|
}
|
|
});
|
|
|
|
describe('createTestDatabase', () => {
|
|
it('should create an in-memory database by default', async () => {
|
|
testDb = await createTestDatabase();
|
|
|
|
expect(testDb.adapter).toBeDefined();
|
|
expect(testDb.nodeRepository).toBeDefined();
|
|
expect(testDb.templateRepository).toBeDefined();
|
|
expect(testDb.path).toBe(':memory:');
|
|
});
|
|
|
|
it('should create a file-based database when requested', async () => {
|
|
const dbPath = path.join(__dirname, '../../temp/test-file.db');
|
|
testDb = await createTestDatabase({ inMemory: false, dbPath });
|
|
|
|
expect(testDb.path).toBe(dbPath);
|
|
expect(fs.existsSync(dbPath)).toBe(true);
|
|
});
|
|
|
|
it('should initialize schema when requested', async () => {
|
|
testDb = await createTestDatabase({ initSchema: true });
|
|
|
|
// Verify tables exist
|
|
const tables = testDb.adapter
|
|
.prepare("SELECT name FROM sqlite_master WHERE type='table'")
|
|
.all() as { name: string }[];
|
|
|
|
const tableNames = tables.map(t => t.name);
|
|
expect(tableNames).toContain('nodes');
|
|
expect(tableNames).toContain('templates');
|
|
});
|
|
|
|
it('should skip schema initialization when requested', async () => {
|
|
testDb = await createTestDatabase({ initSchema: false });
|
|
|
|
// Verify tables don't exist (SQLite has internal tables, so check for our specific tables)
|
|
const tables = testDb.adapter
|
|
.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name IN ('nodes', 'templates')")
|
|
.all() as { name: string }[];
|
|
|
|
expect(tables.length).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe('seedTestNodes', () => {
|
|
beforeEach(async () => {
|
|
testDb = await createTestDatabase();
|
|
});
|
|
|
|
it('should seed default test nodes', async () => {
|
|
const nodes = await seedTestNodes(testDb.nodeRepository);
|
|
|
|
expect(nodes).toHaveLength(3);
|
|
expect(nodes[0].nodeType).toBe('nodes-base.httpRequest');
|
|
expect(nodes[1].nodeType).toBe('nodes-base.webhook');
|
|
expect(nodes[2].nodeType).toBe('nodes-base.slack');
|
|
});
|
|
|
|
it('should seed custom nodes along with defaults', async () => {
|
|
const customNodes = [
|
|
{ nodeType: 'nodes-base.custom1', displayName: 'Custom 1' },
|
|
{ nodeType: 'nodes-base.custom2', displayName: 'Custom 2' }
|
|
];
|
|
|
|
const nodes = await seedTestNodes(testDb.nodeRepository, customNodes);
|
|
|
|
expect(nodes).toHaveLength(5); // 3 default + 2 custom
|
|
expect(nodes[3].nodeType).toBe('nodes-base.custom1');
|
|
expect(nodes[4].nodeType).toBe('nodes-base.custom2');
|
|
});
|
|
|
|
it('should save nodes to database', async () => {
|
|
await seedTestNodes(testDb.nodeRepository);
|
|
|
|
const count = dbHelpers.countRows(testDb.adapter, 'nodes');
|
|
expect(count).toBe(3);
|
|
|
|
const httpNode = testDb.nodeRepository.getNode('nodes-base.httpRequest');
|
|
expect(httpNode).toBeDefined();
|
|
expect(httpNode.displayName).toBe('HTTP Request');
|
|
});
|
|
});
|
|
|
|
describe('seedTestTemplates', () => {
|
|
beforeEach(async () => {
|
|
testDb = await createTestDatabase();
|
|
});
|
|
|
|
it('should seed default test templates', async () => {
|
|
const templates = await seedTestTemplates(testDb.templateRepository);
|
|
|
|
expect(templates).toHaveLength(2);
|
|
expect(templates[0].name).toBe('Simple HTTP Workflow');
|
|
expect(templates[1].name).toBe('Webhook to Slack');
|
|
});
|
|
|
|
it('should seed custom templates', async () => {
|
|
const customTemplates = [
|
|
{ id: 100, name: 'Custom Template' }
|
|
];
|
|
|
|
const templates = await seedTestTemplates(testDb.templateRepository, customTemplates);
|
|
|
|
expect(templates).toHaveLength(3);
|
|
expect(templates[2].id).toBe(100);
|
|
expect(templates[2].name).toBe('Custom Template');
|
|
});
|
|
});
|
|
|
|
describe('createTestNode', () => {
|
|
it('should create a node with defaults', () => {
|
|
const node = createTestNode();
|
|
|
|
expect(node.nodeType).toBe('nodes-base.test');
|
|
expect(node.displayName).toBe('Test Node');
|
|
expect(node.style).toBe('programmatic');
|
|
expect(node.isAITool).toBe(false);
|
|
});
|
|
|
|
it('should override defaults', () => {
|
|
const node = createTestNode({
|
|
nodeType: 'nodes-base.custom',
|
|
displayName: 'Custom Node',
|
|
isAITool: true
|
|
});
|
|
|
|
expect(node.nodeType).toBe('nodes-base.custom');
|
|
expect(node.displayName).toBe('Custom Node');
|
|
expect(node.isAITool).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('resetDatabase', () => {
|
|
beforeEach(async () => {
|
|
testDb = await createTestDatabase();
|
|
});
|
|
|
|
it('should clear all data and reinitialize schema', async () => {
|
|
// Add some data
|
|
await seedTestNodes(testDb.nodeRepository);
|
|
await seedTestTemplates(testDb.templateRepository);
|
|
|
|
// Verify data exists
|
|
expect(dbHelpers.countRows(testDb.adapter, 'nodes')).toBe(3);
|
|
expect(dbHelpers.countRows(testDb.adapter, 'templates')).toBe(2);
|
|
|
|
// Reset database
|
|
await resetDatabase(testDb.adapter);
|
|
|
|
// Verify data is cleared
|
|
expect(dbHelpers.countRows(testDb.adapter, 'nodes')).toBe(0);
|
|
expect(dbHelpers.countRows(testDb.adapter, 'templates')).toBe(0);
|
|
|
|
// Verify tables still exist
|
|
const tables = testDb.adapter
|
|
.prepare("SELECT name FROM sqlite_master WHERE type='table'")
|
|
.all() as { name: string }[];
|
|
|
|
const tableNames = tables.map(t => t.name);
|
|
expect(tableNames).toContain('nodes');
|
|
expect(tableNames).toContain('templates');
|
|
});
|
|
});
|
|
|
|
describe('Database Snapshots', () => {
|
|
beforeEach(async () => {
|
|
testDb = await createTestDatabase();
|
|
});
|
|
|
|
it('should create and restore database snapshot', async () => {
|
|
// Seed initial data
|
|
await seedTestNodes(testDb.nodeRepository);
|
|
await seedTestTemplates(testDb.templateRepository);
|
|
|
|
// Create snapshot
|
|
const snapshot = await createDatabaseSnapshot(testDb.adapter);
|
|
|
|
expect(snapshot.metadata.nodeCount).toBe(3);
|
|
expect(snapshot.metadata.templateCount).toBe(2);
|
|
expect(snapshot.nodes).toHaveLength(3);
|
|
expect(snapshot.templates).toHaveLength(2);
|
|
|
|
// Clear database
|
|
await resetDatabase(testDb.adapter);
|
|
expect(dbHelpers.countRows(testDb.adapter, 'nodes')).toBe(0);
|
|
|
|
// Restore from snapshot
|
|
await restoreDatabaseSnapshot(testDb.adapter, snapshot);
|
|
|
|
// Verify data is restored
|
|
expect(dbHelpers.countRows(testDb.adapter, 'nodes')).toBe(3);
|
|
expect(dbHelpers.countRows(testDb.adapter, 'templates')).toBe(2);
|
|
|
|
const httpNode = testDb.nodeRepository.getNode('nodes-base.httpRequest');
|
|
expect(httpNode).toBeDefined();
|
|
expect(httpNode.displayName).toBe('HTTP Request');
|
|
});
|
|
});
|
|
|
|
describe('loadFixtures', () => {
|
|
beforeEach(async () => {
|
|
testDb = await createTestDatabase();
|
|
});
|
|
|
|
it('should load fixtures from JSON file', async () => {
|
|
// Create a temporary fixture file
|
|
const fixturePath = path.join(__dirname, '../../temp/test-fixtures.json');
|
|
const fixtures = {
|
|
nodes: [
|
|
createTestNode({ nodeType: 'nodes-base.fixture1' }),
|
|
createTestNode({ nodeType: 'nodes-base.fixture2' })
|
|
],
|
|
templates: [
|
|
createTestTemplate({ id: 1000, name: 'Fixture Template' })
|
|
]
|
|
};
|
|
|
|
// Ensure directory exists
|
|
const dir = path.dirname(fixturePath);
|
|
if (!fs.existsSync(dir)) {
|
|
fs.mkdirSync(dir, { recursive: true });
|
|
}
|
|
|
|
fs.writeFileSync(fixturePath, JSON.stringify(fixtures, null, 2));
|
|
|
|
// Load fixtures
|
|
await loadFixtures(testDb.adapter, fixturePath);
|
|
|
|
// Verify data was loaded
|
|
expect(dbHelpers.countRows(testDb.adapter, 'nodes')).toBe(2);
|
|
expect(dbHelpers.countRows(testDb.adapter, 'templates')).toBe(1);
|
|
|
|
expect(dbHelpers.nodeExists(testDb.adapter, 'nodes-base.fixture1')).toBe(true);
|
|
expect(dbHelpers.nodeExists(testDb.adapter, 'nodes-base.fixture2')).toBe(true);
|
|
|
|
// Cleanup
|
|
fs.unlinkSync(fixturePath);
|
|
});
|
|
});
|
|
|
|
describe('dbHelpers', () => {
|
|
beforeEach(async () => {
|
|
testDb = await createTestDatabase();
|
|
await seedTestNodes(testDb.nodeRepository);
|
|
});
|
|
|
|
it('should count rows correctly', () => {
|
|
const count = dbHelpers.countRows(testDb.adapter, 'nodes');
|
|
expect(count).toBe(3);
|
|
});
|
|
|
|
it('should check if node exists', () => {
|
|
expect(dbHelpers.nodeExists(testDb.adapter, 'nodes-base.httpRequest')).toBe(true);
|
|
expect(dbHelpers.nodeExists(testDb.adapter, 'nodes-base.nonexistent')).toBe(false);
|
|
});
|
|
|
|
it('should get all node types', () => {
|
|
const nodeTypes = dbHelpers.getAllNodeTypes(testDb.adapter);
|
|
expect(nodeTypes).toHaveLength(3);
|
|
expect(nodeTypes).toContain('nodes-base.httpRequest');
|
|
expect(nodeTypes).toContain('nodes-base.webhook');
|
|
expect(nodeTypes).toContain('nodes-base.slack');
|
|
});
|
|
|
|
it('should clear table', () => {
|
|
expect(dbHelpers.countRows(testDb.adapter, 'nodes')).toBe(3);
|
|
|
|
dbHelpers.clearTable(testDb.adapter, 'nodes');
|
|
|
|
expect(dbHelpers.countRows(testDb.adapter, 'nodes')).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe('createMockDatabaseAdapter', () => {
|
|
it('should create a mock adapter with all required methods', () => {
|
|
const mockAdapter = createMockDatabaseAdapter();
|
|
|
|
expect(mockAdapter.prepare).toBeDefined();
|
|
expect(mockAdapter.exec).toBeDefined();
|
|
expect(mockAdapter.close).toBeDefined();
|
|
expect(mockAdapter.pragma).toBeDefined();
|
|
expect(mockAdapter.transaction).toBeDefined();
|
|
expect(mockAdapter.checkFTS5Support).toBeDefined();
|
|
|
|
// Test that methods are mocked
|
|
expect(vi.isMockFunction(mockAdapter.prepare)).toBe(true);
|
|
expect(vi.isMockFunction(mockAdapter.exec)).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('withTransaction', () => {
|
|
beforeEach(async () => {
|
|
testDb = await createTestDatabase();
|
|
});
|
|
|
|
it('should rollback transaction for testing', async () => {
|
|
// Insert a node
|
|
await seedTestNodes(testDb.nodeRepository, [
|
|
{ nodeType: 'nodes-base.transaction-test' }
|
|
]);
|
|
|
|
const initialCount = dbHelpers.countRows(testDb.adapter, 'nodes');
|
|
|
|
// Try to insert in a transaction that will rollback
|
|
const result = await withTransaction(testDb.adapter, async () => {
|
|
testDb.nodeRepository.saveNode(createTestNode({
|
|
nodeType: 'nodes-base.should-rollback'
|
|
}));
|
|
|
|
// Verify it was inserted within transaction
|
|
const midCount = dbHelpers.countRows(testDb.adapter, 'nodes');
|
|
expect(midCount).toBe(initialCount + 1);
|
|
|
|
return 'test-result';
|
|
});
|
|
|
|
// Transaction should have rolled back
|
|
expect(result).toBeNull();
|
|
const finalCount = dbHelpers.countRows(testDb.adapter, 'nodes');
|
|
expect(finalCount).toBe(initialCount);
|
|
});
|
|
});
|
|
|
|
describe('measureDatabaseOperation', () => {
|
|
beforeEach(async () => {
|
|
testDb = await createTestDatabase();
|
|
});
|
|
|
|
it('should measure operation duration', async () => {
|
|
const duration = await measureDatabaseOperation('test operation', async () => {
|
|
await seedTestNodes(testDb.nodeRepository);
|
|
});
|
|
|
|
expect(duration).toBeGreaterThan(0);
|
|
expect(duration).toBeLessThan(1000); // Should be fast
|
|
});
|
|
});
|
|
|
|
describe('Integration Tests', () => {
|
|
it('should handle complex database operations', async () => {
|
|
testDb = await createTestDatabase({ enableFTS5: true });
|
|
|
|
// Seed initial data
|
|
const nodes = await seedTestNodes(testDb.nodeRepository);
|
|
const templates = await seedTestTemplates(testDb.templateRepository);
|
|
|
|
// Create snapshot
|
|
const snapshot = await createDatabaseSnapshot(testDb.adapter);
|
|
|
|
// Add more data
|
|
await seedTestNodes(testDb.nodeRepository, [
|
|
{ nodeType: 'nodes-base.extra1' },
|
|
{ nodeType: 'nodes-base.extra2' }
|
|
]);
|
|
|
|
expect(dbHelpers.countRows(testDb.adapter, 'nodes')).toBe(5);
|
|
|
|
// Restore snapshot
|
|
await restoreDatabaseSnapshot(testDb.adapter, snapshot);
|
|
|
|
// Should be back to original state
|
|
expect(dbHelpers.countRows(testDb.adapter, 'nodes')).toBe(3);
|
|
|
|
// Test FTS5 if supported
|
|
if (testDb.adapter.checkFTS5Support()) {
|
|
// FTS5 operations would go here
|
|
expect(true).toBe(true);
|
|
}
|
|
});
|
|
});
|
|
}); |