Files
n8n-mcp/tests/unit/mcp-engine/session-persistence.test.ts
Romuald Członkowski de2abaf89d refactor: streamline test suite — 33 fewer files, 11.9x faster (#670)
* refactor: streamline test suite - cut 33 files, enable parallel execution (11.9x speedup)

Remove duplicate, low-value, and fragmented test files while preserving
all meaningful coverage. Enable parallel test execution and remove
the entire benchmark infrastructure.

Key changes:
- Consolidate workflow-validator tests (13 files -> 3)
- Consolidate config-validator tests (9 files -> 3)
- Consolidate telemetry tests (11 files -> 6)
- Merge AI validator tests (2 files -> 1)
- Remove example/demo test files, mock-testing files, and already-skipped tests
- Remove benchmark infrastructure (10 files, CI workflow, 4 npm scripts)
- Enable parallel test execution (remove singleThread: true)
- Remove retry:2 that was masking flaky tests
- Slim CI publish-results job

Results: 224 -> 191 test files, 4690 -> 4303 tests, 121K -> 106K lines
Local runtime: 319s -> 27s (11.9x speedup)

Conceived by Romuald Członkowski - www.aiadvisors.pl/en

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor: absorb config-validator satellite tests into consolidated file

The previous commit deleted 4 config-validator satellite files. This
properly merges their unique tests into the consolidated config-validator.test.ts,
recovering 89 tests that were dropped during the bulk deletion.

Deduplicates 5 tests that existed in both the satellite files and the
security test file.

Conceived by Romuald Członkowski - www.aiadvisors.pl/en

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: delete missed benchmark-pr.yml workflow, fix flaky session test

- Remove benchmark-pr.yml that referenced deleted benchmark:ci script
- Fix session-persistence round-trip test using timestamps closer to
  now to avoid edge cases exposed by removing retry:2

Conceived by Romuald Członkowski - www.aiadvisors.pl/en

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: rebuild FTS5 index after database rebuild to prevent stale rowid refs

The FTS5 content-synced index could retain phantom rowid references from
previous rebuild cycles, causing 'missing row N from content table'
errors on MATCH queries.

- Add explicit FTS5 rebuild command in rebuild script after all nodes saved
- Add FTS5 rebuild in test beforeAll as defense-in-depth
- Rebuild nodes.db with consistent FTS5 index

Conceived by Romuald Członkowski - www.aiadvisors.pl/en

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: use recent timestamps in all session persistence tests

Session round-trip tests used timestamps 5-10 minutes in the past which
could fail under CI load when combined with session timeout validation.
Use timestamps 30 seconds in the past for all valid-session test data.

Conceived by Romuald Członkowski - www.aiadvisors.pl/en

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 14:22:22 +01:00

256 lines
7.8 KiB
TypeScript

/**
* Unit tests for N8NMCPEngine session persistence wrapper methods
*/
import { describe, it, expect, beforeEach } from 'vitest';
import { N8NMCPEngine } from '../../../src/mcp-engine';
import { SessionState } from '../../../src/types/session-state';
describe('N8NMCPEngine - Session Persistence', () => {
let engine: N8NMCPEngine;
beforeEach(() => {
engine = new N8NMCPEngine({
sessionTimeout: 30 * 60 * 1000,
logLevel: 'error' // Quiet during tests
});
});
describe('exportSessionState()', () => {
it('should return empty array when no sessions exist', () => {
const exported = engine.exportSessionState();
expect(exported).toEqual([]);
});
it('should delegate to underlying server', () => {
// Access private server to create test sessions
const engineAny = engine as any;
const server = engineAny.server;
const serverAny = server as any;
// Create a mock session
serverAny.sessionMetadata['test-session'] = {
createdAt: new Date(),
lastAccess: new Date()
};
serverAny.sessionContexts['test-session'] = {
n8nApiUrl: 'https://test.example.com',
n8nApiKey: 'test-key',
instanceId: 'test-instance'
};
const exported = engine.exportSessionState();
expect(exported).toHaveLength(1);
expect(exported[0].sessionId).toBe('test-session');
expect(exported[0].context.n8nApiUrl).toBe('https://test.example.com');
});
it('should handle server not initialized', () => {
// Create engine without server
const engineAny = {} as N8NMCPEngine;
const exportMethod = N8NMCPEngine.prototype.exportSessionState.bind(engineAny);
// Should not throw, should return empty array
expect(() => exportMethod()).not.toThrow();
const result = exportMethod();
expect(result).toEqual([]);
});
});
describe('restoreSessionState()', () => {
it('should restore sessions via underlying server', () => {
const sessions: SessionState[] = [
{
sessionId: 'restored-session',
metadata: {
createdAt: new Date().toISOString(),
lastAccess: new Date().toISOString()
},
context: {
n8nApiUrl: 'https://restored.example.com',
n8nApiKey: 'restored-key',
instanceId: 'restored-instance'
}
}
];
const count = engine.restoreSessionState(sessions);
expect(count).toBe(1);
// Verify session was restored
const engineAny = engine as any;
const server = engineAny.server;
const serverAny = server as any;
expect(serverAny.sessionMetadata['restored-session']).toBeDefined();
expect(serverAny.sessionContexts['restored-session']).toMatchObject({
n8nApiUrl: 'https://restored.example.com',
n8nApiKey: 'restored-key',
instanceId: 'restored-instance'
});
});
it('should return 0 when restoring empty array', () => {
const count = engine.restoreSessionState([]);
expect(count).toBe(0);
});
it('should handle server not initialized', () => {
const engineAny = {} as N8NMCPEngine;
const restoreMethod = N8NMCPEngine.prototype.restoreSessionState.bind(engineAny);
const sessions: SessionState[] = [
{
sessionId: 'test',
metadata: {
createdAt: new Date().toISOString(),
lastAccess: new Date().toISOString()
},
context: {
n8nApiUrl: 'https://test.example.com',
n8nApiKey: 'test-key',
instanceId: 'test-instance'
}
}
];
// Should not throw, should return 0
expect(() => restoreMethod(sessions)).not.toThrow();
const result = restoreMethod(sessions);
expect(result).toBe(0);
});
it('should return count of successfully restored sessions', () => {
const now = Date.now();
const sessions: SessionState[] = [
{
sessionId: 'valid-1',
metadata: {
createdAt: new Date(now - 2 * 60 * 1000).toISOString(),
lastAccess: new Date(now - 30 * 1000).toISOString()
},
context: {
n8nApiUrl: 'https://valid1.example.com',
n8nApiKey: 'key1',
instanceId: 'instance1'
}
},
{
sessionId: 'valid-2',
metadata: {
createdAt: new Date(now - 2 * 60 * 1000).toISOString(),
lastAccess: new Date(now - 30 * 1000).toISOString()
},
context: {
n8nApiUrl: 'https://valid2.example.com',
n8nApiKey: 'key2',
instanceId: 'instance2'
}
},
{
sessionId: 'expired',
metadata: {
createdAt: new Date(now - 60 * 60 * 1000).toISOString(),
lastAccess: new Date(now - 45 * 60 * 1000).toISOString() // Expired
},
context: {
n8nApiUrl: 'https://expired.example.com',
n8nApiKey: 'expired-key',
instanceId: 'expired-instance'
}
}
];
const count = engine.restoreSessionState(sessions);
expect(count).toBe(2); // Only 2 valid sessions
});
});
describe('Round-trip through engine', () => {
it('should preserve sessions through export → restore cycle', () => {
// Create mock sessions with current timestamps
const engineAny = engine as any;
const server = engineAny.server;
const serverAny = server as any;
const now = new Date();
const createdAt = new Date(now.getTime() - 60 * 1000); // 1 minute ago
const lastAccess = new Date(now.getTime() - 30 * 1000); // 30 seconds ago
serverAny.sessionMetadata['engine-session'] = {
createdAt,
lastAccess
};
serverAny.sessionContexts['engine-session'] = {
n8nApiUrl: 'https://engine-test.example.com',
n8nApiKey: 'engine-key',
instanceId: 'engine-instance',
metadata: { env: 'production' }
};
// Export via engine
const exported = engine.exportSessionState();
expect(exported).toHaveLength(1);
// Clear sessions
delete serverAny.sessionMetadata['engine-session'];
delete serverAny.sessionContexts['engine-session'];
// Restore via engine
const count = engine.restoreSessionState(exported);
expect(count).toBe(1);
// Verify data
expect(serverAny.sessionMetadata['engine-session']).toBeDefined();
expect(serverAny.sessionContexts['engine-session']).toMatchObject({
n8nApiUrl: 'https://engine-test.example.com',
n8nApiKey: 'engine-key',
instanceId: 'engine-instance',
metadata: { env: 'production' }
});
});
});
describe('Integration with getSessionInfo()', () => {
it('should reflect restored sessions in session info', () => {
const sessions: SessionState[] = [
{
sessionId: 'info-session-1',
metadata: {
createdAt: new Date().toISOString(),
lastAccess: new Date().toISOString()
},
context: {
n8nApiUrl: 'https://info1.example.com',
n8nApiKey: 'info-key-1',
instanceId: 'info-instance-1'
}
},
{
sessionId: 'info-session-2',
metadata: {
createdAt: new Date().toISOString(),
lastAccess: new Date().toISOString()
},
context: {
n8nApiUrl: 'https://info2.example.com',
n8nApiKey: 'info-key-2',
instanceId: 'info-instance-2'
}
}
];
engine.restoreSessionState(sessions);
const info = engine.getSessionInfo();
// Note: getSessionInfo() reflects metadata, not transports
// Restored sessions won't have transports until first request
expect(info).toBeDefined();
});
});
});