edge-cases.test.tsโข14.2 kB
/**
* Edge Case Tests
*
* Tests for edge cases and boundary conditions:
* - Very large files
* - Binary files (should reject)
* - Rapid successive operations
* - Concurrent operations
* - Invalid inputs
*/
import fs from 'fs';
import path from 'path';
import os from 'os';
import { DatabaseManager } from '../lib/database.js';
import { SessionManager } from '../lib/session-manager.js';
import { FileHandler } from '../lib/file-handler.js';
import { ClipboardManager } from '../lib/clipboard-manager.js';
import { OperationLogger } from '../lib/operation-logger.js';
import { ClipboardTools } from '../tools/clipboard-tools.js';
describe('Edge Case Tests', () => {
let dbManager: DatabaseManager;
let sessionManager: SessionManager;
let fileHandler: FileHandler;
let clipboardManager: ClipboardManager;
let operationLogger: OperationLogger;
let clipboardTools: ClipboardTools;
let sessionId: string;
let testDir: string;
beforeEach(() => {
dbManager = new DatabaseManager(':memory:');
sessionManager = new SessionManager(dbManager);
fileHandler = new FileHandler();
clipboardManager = new ClipboardManager(dbManager);
operationLogger = new OperationLogger(dbManager);
clipboardTools = new ClipboardTools(
fileHandler,
clipboardManager,
operationLogger,
sessionManager
);
sessionId = sessionManager.createSession();
testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mcp-clipboard-edge-'));
});
afterEach(() => {
if (fs.existsSync(testDir)) {
fs.rmSync(testDir, { recursive: true, force: true });
}
dbManager.close();
});
describe('Large File Handling', () => {
test('should handle copying from large files (10,000 lines)', async () => {
// Create a large file with 10,000 lines
const largeFile = path.join(testDir, 'large.txt');
const lines = Array.from({ length: 10000 }, (_, i) => `Line ${i + 1}`);
fs.writeFileSync(largeFile, lines.join('\n') + '\n');
// Copy a range from middle of large file
const startTime = Date.now();
const result = await clipboardTools.copyLines(sessionId, largeFile, 5000, 5100);
const endTime = Date.now();
expect(result.success).toBe(true);
expect(result.lines).toHaveLength(101);
expect(result.lines[0]).toBe('Line 5000');
expect(result.lines[100]).toBe('Line 5100');
// Should complete in reasonable time (< 1 second)
expect(endTime - startTime).toBeLessThan(1000);
});
test('should handle pasting to large files', async () => {
// Create source with content to paste
const sourceFile = path.join(testDir, 'source.txt');
fs.writeFileSync(sourceFile, 'Insert Line 1\nInsert Line 2\n');
// Create large target file
const largeFile = path.join(testDir, 'large-target.txt');
const lines = Array.from({ length: 5000 }, (_, i) => `Line ${i + 1}`);
fs.writeFileSync(largeFile, lines.join('\n') + '\n');
// Copy and paste
await clipboardTools.copyLines(sessionId, sourceFile, 1, 2);
const startTime = Date.now();
const result = await clipboardTools.pasteLines(sessionId, [
{ file_path: largeFile, target_line: 2500 },
]);
const endTime = Date.now();
expect(result.success).toBe(true);
// Verify paste happened at correct location
const content = fs.readFileSync(largeFile, 'utf-8');
expect(content).toContain('Insert Line 1');
expect(content).toContain('Insert Line 2');
// Should complete in reasonable time (< 2 seconds)
expect(endTime - startTime).toBeLessThan(2000);
});
test('should reject copying very large ranges that exceed size limits', async () => {
// Create a file with many large lines
const largeFile = path.join(testDir, 'huge.txt');
// Each line is 100KB, 200 lines = 20MB (exceeds 10MB limit)
const hugeLine = 'A'.repeat(100000);
const lines = Array.from({ length: 200 }, () => hugeLine);
fs.writeFileSync(largeFile, lines.join('\n') + '\n');
// Try to copy all 200 lines (20MB) - should reject
await expect(clipboardTools.copyLines(sessionId, largeFile, 1, 200)).rejects.toThrow();
});
});
describe('Binary File Handling', () => {
test('should reject binary files (PNG image)', async () => {
// Create a fake binary file (PNG header)
const binaryFile = path.join(testDir, 'image.png');
const pngHeader = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
fs.writeFileSync(binaryFile, pngHeader);
// Try to copy from binary file - should reject
await expect(clipboardTools.copyLines(sessionId, binaryFile, 1, 1)).rejects.toThrow();
});
test('should reject binary files (PDF)', async () => {
// Create a fake PDF file
const pdfFile = path.join(testDir, 'document.pdf');
const pdfHeader = Buffer.from('%PDF-1.4\n');
fs.writeFileSync(pdfFile, pdfHeader);
// Try to copy from PDF - should reject
await expect(clipboardTools.copyLines(sessionId, pdfFile, 1, 1)).rejects.toThrow();
});
test('should handle text files with unicode characters', async () => {
// Create file with various unicode characters
const unicodeFile = path.join(testDir, 'unicode.txt');
const unicodeContent = 'ไฝ ๅฅฝไธ็\nะัะธะฒะตั ะผะธั\n๐ Emoji test\nCafรฉ rรฉsumรฉ\n';
fs.writeFileSync(unicodeFile, unicodeContent, 'utf-8');
// Should successfully copy unicode content
const result = await clipboardTools.copyLines(sessionId, unicodeFile, 1, 4);
expect(result.success).toBe(true);
expect(result.content).toContain('ไฝ ๅฅฝไธ็');
expect(result.content).toContain('ะัะธะฒะตั ะผะธั');
expect(result.content).toContain('๐');
expect(result.content).toContain('Cafรฉ rรฉsumรฉ');
});
});
describe('Rapid Successive Operations', () => {
test('should handle rapid copy operations', async () => {
// Create multiple source files
const files = Array.from({ length: 10 }, (_, i) => {
const filePath = path.join(testDir, `file${i}.txt`);
fs.writeFileSync(filePath, `Content ${i}\n`);
return filePath;
});
// Rapidly copy from each file
const results = await Promise.all(
files.map((file) => clipboardTools.copyLines(sessionId, file, 1, 1))
);
// All should succeed
results.forEach((result) => {
expect(result.success).toBe(true);
});
// Clipboard should have last copied content
const clipboard = await clipboardTools.showClipboard(sessionId);
expect(clipboard.content).toBe('Content 9');
});
test('should handle rapid copy-paste-undo cycles', async () => {
// Setup files
const sourceFile = path.join(testDir, 'source.txt');
const destFile = path.join(testDir, 'dest.txt');
fs.writeFileSync(sourceFile, 'Line 1\nLine 2\n');
const originalDest = 'Original\n';
fs.writeFileSync(destFile, originalDest);
// Perform rapid copy-paste-undo cycles
for (let i = 0; i < 5; i++) {
// Copy
await clipboardTools.copyLines(sessionId, sourceFile, 1, 2);
// Paste
await clipboardTools.pasteLines(sessionId, [{ file_path: destFile, target_line: 1 }]);
// Verify paste
let content = fs.readFileSync(destFile, 'utf-8');
expect(content).toContain('Line 1');
// Undo
await clipboardTools.undoLastPaste(sessionId);
// Verify undo
content = fs.readFileSync(destFile, 'utf-8');
expect(content).toBe(originalDest);
}
// Final state should be original
const finalContent = fs.readFileSync(destFile, 'utf-8');
expect(finalContent).toBe(originalDest);
});
});
describe('Invalid Input Handling', () => {
test('should reject invalid line ranges (start > end)', async () => {
const file = path.join(testDir, 'test.txt');
fs.writeFileSync(file, 'Line 1\nLine 2\nLine 3\n');
await expect(clipboardTools.copyLines(sessionId, file, 3, 1)).rejects.toThrow();
});
test('should reject negative line numbers', async () => {
const file = path.join(testDir, 'test.txt');
fs.writeFileSync(file, 'Line 1\nLine 2\n');
await expect(clipboardTools.copyLines(sessionId, file, -1, 2)).rejects.toThrow();
});
test('should reject line numbers beyond file length', async () => {
const file = path.join(testDir, 'test.txt');
fs.writeFileSync(file, 'Line 1\nLine 2\n');
await expect(clipboardTools.copyLines(sessionId, file, 1, 100)).rejects.toThrow();
});
test('should reject invalid session ID', async () => {
const file = path.join(testDir, 'test.txt');
fs.writeFileSync(file, 'Line 1\n');
await expect(clipboardTools.copyLines('invalid-session-id', file, 1, 1)).rejects.toThrow(
'Invalid session'
);
});
test('should handle non-existent files gracefully', async () => {
const nonExistentFile = path.join(testDir, 'does-not-exist.txt');
await expect(clipboardTools.copyLines(sessionId, nonExistentFile, 1, 1)).rejects.toThrow();
});
});
describe('Edge Cases in File Operations', () => {
test('should handle empty files', async () => {
const emptyFile = path.join(testDir, 'empty.txt');
fs.writeFileSync(emptyFile, '');
await expect(clipboardTools.copyLines(sessionId, emptyFile, 1, 1)).rejects.toThrow();
});
test('should handle files with only newlines', async () => {
const newlineFile = path.join(testDir, 'newlines.txt');
fs.writeFileSync(newlineFile, '\n\n\n');
const result = await clipboardTools.copyLines(sessionId, newlineFile, 1, 3);
expect(result.success).toBe(true);
expect(result.lines).toHaveLength(3);
expect(result.lines.every((line) => line === '')).toBe(true);
});
test('should handle very long single line', async () => {
const longLineFile = path.join(testDir, 'long-line.txt');
const longLine = 'A'.repeat(50000); // 50KB single line
fs.writeFileSync(longLineFile, longLine + '\n');
const result = await clipboardTools.copyLines(sessionId, longLineFile, 1, 1);
expect(result.success).toBe(true);
expect(result.lines[0].length).toBe(50000);
});
test('should preserve different line endings (CRLF)', async () => {
const crlfFile = path.join(testDir, 'crlf.txt');
fs.writeFileSync(crlfFile, 'Line 1\r\nLine 2\r\nLine 3\r\n');
const result = await clipboardTools.copyLines(sessionId, crlfFile, 1, 3);
expect(result.success).toBe(true);
// Paste to new file
const destFile = path.join(testDir, 'dest-crlf.txt');
fs.writeFileSync(destFile, 'Original\r\n');
await clipboardTools.pasteLines(sessionId, [{ file_path: destFile, target_line: 1 }]);
// Line endings should be preserved
const destContent = fs.readFileSync(destFile, 'utf-8');
expect(destContent).toContain('\r\n');
});
test('should handle paste at line 0 (beginning of file)', async () => {
const sourceFile = path.join(testDir, 'source.txt');
const destFile = path.join(testDir, 'dest.txt');
fs.writeFileSync(sourceFile, 'Insert\n');
fs.writeFileSync(destFile, 'Line 1\nLine 2\n');
await clipboardTools.copyLines(sessionId, sourceFile, 1, 1);
// Paste at line 0 (should insert at very beginning)
const result = await clipboardTools.pasteLines(sessionId, [
{ file_path: destFile, target_line: 0 },
]);
expect(result.success).toBe(true);
const content = fs.readFileSync(destFile, 'utf-8');
expect(content.startsWith('Insert\n')).toBe(true);
});
});
describe('Clipboard Size Limits', () => {
test('should enforce 10MB clipboard limit', async () => {
const largeFile = path.join(testDir, 'large-content.txt');
// Create file with ~11MB of content (exceeds 10MB limit)
const largeLine = 'X'.repeat(100000); // 100KB per line
const lines = Array.from({ length: 110 }, () => largeLine); // 11MB total
fs.writeFileSync(largeFile, lines.join('\n') + '\n');
// Try to copy all lines - should reject due to size limit
await expect(clipboardTools.copyLines(sessionId, largeFile, 1, 110)).rejects.toThrow();
});
test('should accept content just under 10MB limit', async () => {
const largeFile = path.join(testDir, 'large-content.txt');
// Create file with ~9MB of content (under 10MB limit)
const largeLine = 'Y'.repeat(100000); // 100KB per line
const lines = Array.from({ length: 90 }, () => largeLine); // 9MB total
fs.writeFileSync(largeFile, lines.join('\n') + '\n');
// Should succeed
const result = await clipboardTools.copyLines(sessionId, largeFile, 1, 90);
expect(result.success).toBe(true);
expect(result.lines).toHaveLength(90);
});
});
describe('Session Isolation', () => {
test('should maintain separate clipboards for different sessions', async () => {
// Create second session
const sessionId2 = sessionManager.createSession();
// Create files
const file1 = path.join(testDir, 'file1.txt');
const file2 = path.join(testDir, 'file2.txt');
fs.writeFileSync(file1, 'Session 1 Content\n');
fs.writeFileSync(file2, 'Session 2 Content\n');
// Session 1 copies file1
await clipboardTools.copyLines(sessionId, file1, 1, 1);
// Session 2 copies file2
await clipboardTools.copyLines(sessionId2, file2, 1, 1);
// Verify session 1 clipboard
const clipboard1 = await clipboardTools.showClipboard(sessionId);
expect(clipboard1.content).toBe('Session 1 Content');
// Verify session 2 clipboard
const clipboard2 = await clipboardTools.showClipboard(sessionId2);
expect(clipboard2.content).toBe('Session 2 Content');
// Clipboards should be independent
expect(clipboard1.content).not.toBe(clipboard2.content);
});
});
});