import { describe, it, expect, beforeEach, afterEach } from '@jest/globals';
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import { DatabaseManager } from '../../utils/database.js';
import { TestDatabaseHelper } from '../../test-helpers/database-helper.js';
describe('Concurrent Access Tests', () => {
let tempDir: string;
let tempDbPath: string;
let dbManager: DatabaseManager;
beforeEach(() => {
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mcp-test-'));
tempDbPath = path.join(tempDir, 'test.db');
dbManager = TestDatabaseHelper.createTestDatabase();
});
afterEach(async () => {
// Clean up all tracked databases
await TestDatabaseHelper.cleanupAll();
// Clean up temp directory
try {
fs.rmSync(tempDir, { recursive: true, force: true });
} catch (_error) {
console.warn('Error cleaning up temp directory:', _error);
}
// Clean up any WAL/SHM files
TestDatabaseHelper.cleanupDbFiles(tempDbPath);
});
it('should handle concurrent session writes without corruption', async () => {
const db = dbManager.getDatabase();
const sessionId = 'test-session';
// Create session
db.prepare(
`
INSERT INTO sessions (id, name, description, branch, working_directory)
VALUES (?, ?, ?, ?, ?)
`
).run(sessionId, 'Test Session', 'Test', null, null);
// Simulate concurrent writes to same session
const promises = [];
for (let i = 0; i < 10; i++) {
promises.push(
new Promise<void>(resolve => {
setTimeout(() => {
try {
db.prepare(
`
INSERT OR REPLACE INTO context_items (id, session_id, key, value, category, priority)
VALUES (?, ?, ?, ?, ?, ?)
`
).run(`item-${i}`, sessionId, `key-${i}`, `value-${i}`, 'test', 'normal');
resolve();
} catch (_error) {
resolve(); // Still resolve to continue test
}
}, Math.random() * 10);
})
);
}
await Promise.all(promises);
// Verify all items were written
const items = db
.prepare('SELECT COUNT(*) as count FROM context_items WHERE session_id = ?')
.get(sessionId) as any;
expect(items.count).toBe(10);
});
it('should handle concurrent updates to same key', async () => {
const db = dbManager.getDatabase();
const sessionId = 'test-session';
const key = 'shared-key';
// Create session
db.prepare(
`
INSERT INTO sessions (id, name, description, branch, working_directory)
VALUES (?, ?, ?, ?, ?)
`
).run(sessionId, 'Test Session', 'Test', null, null);
// Initial value
db.prepare(
`
INSERT INTO context_items (id, session_id, key, value, category, priority)
VALUES (?, ?, ?, ?, ?, ?)
`
).run('initial', sessionId, key, 'initial-value', 'test', 'normal');
// Concurrent updates
const updates = [];
for (let i = 0; i < 20; i++) {
updates.push(
new Promise<void>(resolve => {
setTimeout(() => {
try {
db.prepare(
`
UPDATE context_items
SET value = ?, updated_at = CURRENT_TIMESTAMP
WHERE session_id = ? AND key = ?
`
).run(`value-${i}`, sessionId, key);
resolve();
} catch (_error) {
resolve();
}
}, Math.random() * 10);
})
);
}
await Promise.all(updates);
// Should have exactly one item with the key
const items = db
.prepare('SELECT COUNT(*) as count FROM context_items WHERE session_id = ? AND key = ?')
.get(sessionId, key) as any;
expect(items.count).toBe(1);
});
it('should handle database locks gracefully', () => {
const db = dbManager.getDatabase();
// In WAL mode, readers don't block writers and vice versa
// So we need to test a different scenario - two writers
// Start a write transaction
db.prepare('BEGIN IMMEDIATE').run();
// Try to start another write transaction (should fail)
let errorThrown = false;
try {
// This should fail because we already have a write transaction
db.prepare('BEGIN IMMEDIATE').run();
} catch (error: any) {
errorThrown = true;
// The exact error code might vary
expect(error.message).toMatch(/database is locked|cannot start a transaction/i);
}
db.prepare('ROLLBACK').run();
// If no error was thrown, it might be because SQLite is configured differently
// In that case, just verify we can at least do transactions
if (!errorThrown) {
// Verify basic transaction works
db.prepare('BEGIN').run();
db.prepare('INSERT INTO sessions (id, name) VALUES (?, ?)').run('test', 'Test');
db.prepare('COMMIT').run();
const result = db.prepare('SELECT * FROM sessions WHERE id = ?').get('test') as any;
expect(result.name).toBe('Test');
}
});
it('should handle WAL mode concurrent reads during write', () => {
const db = dbManager.getDatabase();
// Verify WAL mode is enabled
const journalMode = db.pragma('journal_mode') as any[];
expect(journalMode[0].journal_mode).toBe('wal');
// Create a session
const sessionId = 'wal-test';
db.prepare(
`
INSERT INTO sessions (id, name, description, branch, working_directory)
VALUES (?, ?, ?, ?, ?)
`
).run(sessionId, 'WAL Test', 'Test', null, null);
// Start a write transaction
const writeStmt = db.prepare('BEGIN IMMEDIATE');
writeStmt.run();
// Insert item in transaction
db.prepare(
`
INSERT INTO context_items (id, session_id, key, value, category, priority)
VALUES (?, ?, ?, ?, ?, ?)
`
).run('item-1', sessionId, 'key-1', 'value-1', 'test', 'normal');
// Read should work even with ongoing write transaction (WAL mode benefit)
const readResult = db.prepare('SELECT COUNT(*) as count FROM sessions').get() as any;
expect(readResult.count).toBeGreaterThan(0);
// Commit the transaction
db.prepare('COMMIT').run();
});
});