integration.test.tsā¢16.4 kB
/**
* Integration Tests - End-to-End Workflows
*
* These tests verify complete workflows across all components:
* - Copy ā Paste ā Undo
* - Cut ā Paste ā Verify source removed
* - Multi-file paste operations
*/
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('Integration Tests - End-to-End Workflows', () => {
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(() => {
// Use in-memory database for testing
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
);
// Create session
sessionId = sessionManager.createSession();
// Create temporary test directory
testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mcp-clipboard-test-'));
});
afterEach(() => {
// Clean up test directory
if (fs.existsSync(testDir)) {
fs.rmSync(testDir, { recursive: true, force: true });
}
// Close database
dbManager.close();
});
describe('Copy ā Paste ā Undo Workflow', () => {
test('should complete full copy-paste-undo workflow', async () => {
// Setup: Create source file
const sourceFile = path.join(testDir, 'source.txt');
const sourceContent = 'Line 1\nLine 2\nLine 3\nLine 4\nLine 5\n';
fs.writeFileSync(sourceFile, sourceContent);
// Setup: Create destination file
const destFile = path.join(testDir, 'destination.txt');
const destContent = 'Dest Line 1\nDest Line 2\nDest Line 3\n';
fs.writeFileSync(destFile, destContent);
// Step 1: Copy lines 2-3 from source
const copyResult = await clipboardTools.copyLines(sessionId, sourceFile, 2, 3);
expect(copyResult.success).toBe(true);
expect(copyResult.lines).toEqual(['Line 2', 'Line 3']);
// Verify source file unchanged
const sourceAfterCopy = fs.readFileSync(sourceFile, 'utf-8');
expect(sourceAfterCopy).toBe(sourceContent);
// Step 2: Show clipboard to verify
const clipboardResult = await clipboardTools.showClipboard(sessionId);
expect(clipboardResult.hasContent).toBe(true);
expect(clipboardResult.content).toBe('Line 2\nLine 3');
expect(clipboardResult.sourceFile).toBe(sourceFile);
expect(clipboardResult.operationType).toBe('copy');
// Step 3: Paste to destination at line 2
const pasteResult = await clipboardTools.pasteLines(sessionId, [
{ file_path: destFile, target_line: 2 },
]);
expect(pasteResult.success).toBe(true);
expect(pasteResult.pastedTo).toHaveLength(1);
expect(pasteResult.pastedTo[0].file).toBe(destFile);
// Verify destination file has pasted content
const destAfterPaste = fs.readFileSync(destFile, 'utf-8');
const expectedAfterPaste = 'Dest Line 1\nLine 2\nLine 3\nDest Line 2\nDest Line 3\n';
expect(destAfterPaste).toBe(expectedAfterPaste);
// Step 4: Undo the paste
const undoResult = await clipboardTools.undoLastPaste(sessionId);
expect(undoResult.success).toBe(true);
expect(undoResult.restoredFiles).toHaveLength(1);
expect(undoResult.restoredFiles[0].file).toBe(destFile);
// Verify destination file restored to original
const destAfterUndo = fs.readFileSync(destFile, 'utf-8');
expect(destAfterUndo).toBe(destContent);
// Step 5: Verify operation history
const historyResult = await clipboardTools.getOperationHistory(sessionId, 10);
// Operations should be logged: copy, paste, undo
expect(historyResult.operations.length).toBeGreaterThanOrEqual(3);
// Operations are returned in reverse chronological order (most recent first)
// Verify all operation types are present
const opTypes = historyResult.operations.map((op) => op.operationType);
expect(opTypes).toContain('copy');
expect(opTypes).toContain('paste');
expect(opTypes).toContain('undo');
// Verify the operations occurred in the correct sequence
// Note: Due to timestamp precision, we just verify all three types are present
// The ORDER is validated by the undo operation working correctly above
});
test('should handle copy-paste to multiple files', async () => {
// Setup: Create source file
const sourceFile = path.join(testDir, 'source.txt');
const sourceContent = 'Line 1\nLine 2\nLine 3\n';
fs.writeFileSync(sourceFile, sourceContent);
// Setup: Create multiple destination files
const dest1 = path.join(testDir, 'dest1.txt');
const dest2 = path.join(testDir, 'dest2.txt');
const dest3 = path.join(testDir, 'dest3.txt');
fs.writeFileSync(dest1, 'Dest1 Line 1\nDest1 Line 2\n');
fs.writeFileSync(dest2, 'Dest2 Line 1\nDest2 Line 2\n');
fs.writeFileSync(dest3, 'Dest3 Line 1\nDest3 Line 2\n');
// Step 1: Copy lines from source
const copyResult = await clipboardTools.copyLines(sessionId, sourceFile, 1, 2);
expect(copyResult.success).toBe(true);
// Step 2: Paste to multiple destinations
const pasteResult = await clipboardTools.pasteLines(sessionId, [
{ file_path: dest1, target_line: 1 },
{ file_path: dest2, target_line: 2 },
{ file_path: dest3, target_line: 1 },
]);
expect(pasteResult.success).toBe(true);
expect(pasteResult.pastedTo).toHaveLength(3);
// Verify all destination files have pasted content
const dest1Content = fs.readFileSync(dest1, 'utf-8');
expect(dest1Content).toContain('Line 1');
expect(dest1Content).toContain('Line 2');
const dest2Content = fs.readFileSync(dest2, 'utf-8');
expect(dest2Content).toContain('Line 1');
expect(dest2Content).toContain('Line 2');
const dest3Content = fs.readFileSync(dest3, 'utf-8');
expect(dest3Content).toContain('Line 1');
expect(dest3Content).toContain('Line 2');
// Step 3: Undo should restore all files
const undoResult = await clipboardTools.undoLastPaste(sessionId);
expect(undoResult.success).toBe(true);
expect(undoResult.restoredFiles).toHaveLength(3);
// Verify all files restored
expect(fs.readFileSync(dest1, 'utf-8')).toBe('Dest1 Line 1\nDest1 Line 2\n');
expect(fs.readFileSync(dest2, 'utf-8')).toBe('Dest2 Line 1\nDest2 Line 2\n');
expect(fs.readFileSync(dest3, 'utf-8')).toBe('Dest3 Line 1\nDest3 Line 2\n');
});
});
describe('Cut ā Paste ā Verify Workflow', () => {
test('should complete full cut-paste workflow', async () => {
// Setup: Create source file
const sourceFile = path.join(testDir, 'source.txt');
const sourceContent = 'Line 1\nLine 2\nLine 3\nLine 4\nLine 5\nLine 6\n';
fs.writeFileSync(sourceFile, sourceContent);
// Setup: Create destination file
const destFile = path.join(testDir, 'destination.txt');
const destContent = 'Dest Line 1\nDest Line 2\n';
fs.writeFileSync(destFile, destContent);
// Step 1: Cut lines 3-5 from source
const cutResult = await clipboardTools.cutLines(sessionId, sourceFile, 3, 5);
expect(cutResult.success).toBe(true);
expect(cutResult.lines).toEqual(['Line 3', 'Line 4', 'Line 5']);
// Verify source file has lines removed
const sourceAfterCut = fs.readFileSync(sourceFile, 'utf-8');
expect(sourceAfterCut).toBe('Line 1\nLine 2\nLine 6\n');
expect(sourceAfterCut).not.toContain('Line 3');
expect(sourceAfterCut).not.toContain('Line 4');
expect(sourceAfterCut).not.toContain('Line 5');
// Step 2: Verify clipboard has cut content
const clipboardResult = await clipboardTools.showClipboard(sessionId);
expect(clipboardResult.hasContent).toBe(true);
expect(clipboardResult.content).toBe('Line 3\nLine 4\nLine 5');
expect(clipboardResult.operationType).toBe('cut');
// Step 3: Paste to destination
const pasteResult = await clipboardTools.pasteLines(sessionId, [
{ file_path: destFile, target_line: 2 },
]);
expect(pasteResult.success).toBe(true);
// Verify destination has pasted content
const destAfterPaste = fs.readFileSync(destFile, 'utf-8');
const expectedDest = 'Dest Line 1\nLine 3\nLine 4\nLine 5\nDest Line 2\n';
expect(destAfterPaste).toBe(expectedDest);
// Verify source still has lines removed
const sourceFinal = fs.readFileSync(sourceFile, 'utf-8');
expect(sourceFinal).toBe('Line 1\nLine 2\nLine 6\n');
});
test('should move code between files using cut-paste', async () => {
// Setup: Create source file with function
const sourceFile = path.join(testDir, 'old-module.js');
const sourceContent = `export function oldFunction() {\n return 'old';\n}\n\nexport function keepThis() {\n return 'keep';\n}\n`;
fs.writeFileSync(sourceFile, sourceContent);
// Setup: Create destination file
const destFile = path.join(testDir, 'new-module.js');
const destContent = `// New module\n\n`;
fs.writeFileSync(destFile, destContent);
// Step 1: Cut the old function (lines 1-3)
const cutResult = await clipboardTools.cutLines(sessionId, sourceFile, 1, 3);
expect(cutResult.success).toBe(true);
expect(cutResult.content).toContain('oldFunction');
// Step 2: Paste to new module
const pasteResult = await clipboardTools.pasteLines(sessionId, [
{ file_path: destFile, target_line: 2 },
]);
expect(pasteResult.success).toBe(true);
// Verify source has function removed but kept other function
const sourceFinal = fs.readFileSync(sourceFile, 'utf-8');
expect(sourceFinal).not.toContain('oldFunction');
expect(sourceFinal).toContain('keepThis');
// Verify destination has new function
const destFinal = fs.readFileSync(destFile, 'utf-8');
expect(destFinal).toContain('oldFunction');
expect(destFinal).toContain('// New module');
});
});
describe('Complex Multi-Operation Workflows', () => {
test('should handle successive copy operations overwriting clipboard', async () => {
// Setup files
const file1 = path.join(testDir, 'file1.txt');
const file2 = path.join(testDir, 'file2.txt');
const file3 = path.join(testDir, 'file3.txt');
fs.writeFileSync(file1, 'File1 Line 1\nFile1 Line 2\n');
fs.writeFileSync(file2, 'File2 Line 1\nFile2 Line 2\n');
fs.writeFileSync(file3, 'File3 Line 1\n');
// Copy from file1
await clipboardTools.copyLines(sessionId, file1, 1, 1);
// Verify clipboard has file1 content
let clipboard = await clipboardTools.showClipboard(sessionId);
expect(clipboard.content).toBe('File1 Line 1');
// Copy from file2 (should overwrite clipboard)
await clipboardTools.copyLines(sessionId, file2, 1, 1);
// Verify clipboard now has file2 content
clipboard = await clipboardTools.showClipboard(sessionId);
expect(clipboard.content).toBe('File2 Line 1');
expect(clipboard.sourceFile).toBe(file2);
// Paste should use most recent clipboard
const pasteResult = await clipboardTools.pasteLines(sessionId, [
{ file_path: file3, target_line: 1 },
]);
expect(pasteResult.success).toBe(true);
const file3Content = fs.readFileSync(file3, 'utf-8');
expect(file3Content).toContain('File2 Line 1');
expect(file3Content).not.toContain('File1 Line 1');
});
test('should handle paste to same file multiple times with adjusted line numbers', async () => {
// Setup: Create source and target
const sourceFile = path.join(testDir, 'source.txt');
const targetFile = path.join(testDir, 'target.txt');
fs.writeFileSync(sourceFile, 'Insert Line\n');
fs.writeFileSync(targetFile, 'Line 1\nLine 2\nLine 3\nLine 4\nLine 5\n');
// Copy the insert line
await clipboardTools.copyLines(sessionId, sourceFile, 1, 1);
// Paste to same file at multiple locations
const pasteResult = await clipboardTools.pasteLines(sessionId, [
{ file_path: targetFile, target_line: 2 }, // After Line 1
{ file_path: targetFile, target_line: 4 }, // After Line 3 (but adjusted for previous insert)
{ file_path: targetFile, target_line: 6 }, // After Line 5 (adjusted)
]);
expect(pasteResult.success).toBe(true);
expect(pasteResult.pastedTo).toHaveLength(3);
// Verify file has inserts at correct locations
const finalContent = fs.readFileSync(targetFile, 'utf-8');
const lines = finalContent.split('\n').filter((l) => l.length > 0);
expect(lines.length).toBe(8); // 5 original + 3 inserts
expect(lines).toContain('Insert Line');
});
test('should maintain operation history across multiple operations', async () => {
// Setup files
const file1 = path.join(testDir, 'file1.txt');
const file2 = path.join(testDir, 'file2.txt');
fs.writeFileSync(file1, 'Line 1\nLine 2\nLine 3\n');
fs.writeFileSync(file2, 'Target Line 1\n');
// Perform multiple operations
await clipboardTools.copyLines(sessionId, file1, 1, 1); // Op 1: copy
await clipboardTools.pasteLines(sessionId, [{ file_path: file2, target_line: 1 }]); // Op 2: paste
await clipboardTools.undoLastPaste(sessionId); // Op 3: undo
await clipboardTools.cutLines(sessionId, file1, 2, 2); // Op 4: cut
await clipboardTools.pasteLines(sessionId, [{ file_path: file2, target_line: 1 }]); // Op 5: paste
// Get operation history
const history = await clipboardTools.getOperationHistory(sessionId, 20);
expect(history.operations.length).toBeGreaterThanOrEqual(5);
// Verify operation types in history
const opTypes = history.operations.map((op) => op.operationType).reverse();
expect(opTypes).toContain('copy');
expect(opTypes).toContain('paste');
expect(opTypes).toContain('undo');
expect(opTypes).toContain('cut');
});
});
describe('Error Recovery in Workflows', () => {
test('should handle paste failure and maintain clipboard state', async () => {
// Setup source file
const sourceFile = path.join(testDir, 'source.txt');
fs.writeFileSync(sourceFile, 'Line 1\nLine 2\n');
// Copy content
await clipboardTools.copyLines(sessionId, sourceFile, 1, 2);
// Try to paste to non-existent file (should fail)
const nonExistentFile = path.join(testDir, 'does-not-exist.txt');
await expect(
clipboardTools.pasteLines(sessionId, [{ file_path: nonExistentFile, target_line: 1 }])
).rejects.toThrow();
// Verify clipboard still has content after failed paste
const clipboard = await clipboardTools.showClipboard(sessionId);
expect(clipboard.hasContent).toBe(true);
expect(clipboard.content).toBe('Line 1\nLine 2');
});
test('should handle undo when no paste exists', async () => {
// Try to undo without any paste operation
await expect(clipboardTools.undoLastPaste(sessionId)).rejects.toThrow(
'No paste operation to undo'
);
});
test('should handle paste with empty clipboard', async () => {
// Setup destination file
const destFile = path.join(testDir, 'dest.txt');
fs.writeFileSync(destFile, 'Line 1\n');
// Try to paste without copying anything first
await expect(
clipboardTools.pasteLines(sessionId, [{ file_path: destFile, target_line: 1 }])
).rejects.toThrow('Clipboard is empty');
});
});
});