cut-paste-undo.test.tsā¢9.39 kB
/**
* Cut-Paste-Undo Integration Tests
*
* Tests for ensuring that undoing a paste operation that came from a cut
* properly restores both the paste target AND the cut source.
*/
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('Cut-Paste-Undo Workflow', () => {
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();
});
test('should restore both target and source when undoing a paste from cut', async () => {
// Setup: Create source file with 5 lines
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\n';
fs.writeFileSync(destFile, destContent);
// Step 1: Cut lines 2-4 from source
const cutResult = await clipboardTools.cutLines(sessionId, sourceFile, 2, 4);
expect(cutResult.success).toBe(true);
expect(cutResult.lines).toEqual(['Line 2', 'Line 3', 'Line 4']);
// Verify source file has lines removed
const sourceAfterCut = fs.readFileSync(sourceFile, 'utf-8');
expect(sourceAfterCut).toBe('Line 1\nLine 5\n');
// Step 2: 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');
expect(destAfterPaste).toBe('Dest Line 1\nLine 2\nLine 3\nLine 4\nDest Line 2\n');
// Verify source still has lines removed
const sourceAfterPaste = fs.readFileSync(sourceFile, 'utf-8');
expect(sourceAfterPaste).toBe('Line 1\nLine 5\n');
// Step 3: Undo the paste
const undoResult = await clipboardTools.undoLastPaste(sessionId);
expect(undoResult.success).toBe(true);
expect(undoResult.restoredFiles.length).toBeGreaterThanOrEqual(1);
// CRITICAL ASSERTIONS:
// 1. Destination should be restored to original state
const destAfterUndo = fs.readFileSync(destFile, 'utf-8');
expect(destAfterUndo).toBe(destContent);
// 2. Source should be restored to original state (lines 2-4 restored)
const sourceAfterUndo = fs.readFileSync(sourceFile, 'utf-8');
expect(sourceAfterUndo).toBe(sourceContent);
});
test('should NOT restore source when undoing a paste from copy', 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 destination file
const destFile = path.join(testDir, 'destination.txt');
const destContent = 'Dest Line 1\nDest Line 2\n';
fs.writeFileSync(destFile, destContent);
// Step 1: COPY (not cut) lines from source
await clipboardTools.copyLines(sessionId, sourceFile, 2, 3);
// Verify source unchanged
const sourceAfterCopy = fs.readFileSync(sourceFile, 'utf-8');
expect(sourceAfterCopy).toBe(sourceContent);
// Step 2: Paste to destination
await clipboardTools.pasteLines(sessionId, [{ file_path: destFile, target_line: 2 }]);
// Step 3: Undo the paste
await clipboardTools.undoLastPaste(sessionId);
// Destination should be restored
const destAfterUndo = fs.readFileSync(destFile, 'utf-8');
expect(destAfterUndo).toBe(destContent);
// Source should STILL be unchanged (since it was a copy, not a cut)
const sourceAfterUndo = fs.readFileSync(sourceFile, 'utf-8');
expect(sourceAfterUndo).toBe(sourceContent);
});
test('should restore source file when undoing multi-target paste from cut', 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');
fs.writeFileSync(dest1, 'Dest1\n');
fs.writeFileSync(dest2, 'Dest2\n');
const dest1Content = fs.readFileSync(dest1, 'utf-8');
const dest2Content = fs.readFileSync(dest2, 'utf-8');
// Step 1: Cut line 2 from source
await clipboardTools.cutLines(sessionId, sourceFile, 2, 2);
// Verify source has line removed
expect(fs.readFileSync(sourceFile, 'utf-8')).toBe('Line 1\nLine 3\n');
// Step 2: Paste to multiple destinations
await clipboardTools.pasteLines(sessionId, [
{ file_path: dest1, target_line: 1 },
{ file_path: dest2, target_line: 1 },
]);
// Verify both destinations have pasted content
expect(fs.readFileSync(dest1, 'utf-8')).toContain('Line 2');
expect(fs.readFileSync(dest2, 'utf-8')).toContain('Line 2');
// Step 3: Undo
await clipboardTools.undoLastPaste(sessionId);
// All three files should be restored
expect(fs.readFileSync(sourceFile, 'utf-8')).toBe(sourceContent);
expect(fs.readFileSync(dest1, 'utf-8')).toBe(dest1Content);
expect(fs.readFileSync(dest2, 'utf-8')).toBe(dest2Content);
});
test('should handle sequential cut-paste-undo-paste operations correctly', async () => {
// Setup files
const sourceFile = path.join(testDir, 'source.txt');
const destFile = path.join(testDir, 'dest.txt');
const sourceContent = 'Line 1\nLine 2\nLine 3\n';
const destContent = 'Dest\n';
fs.writeFileSync(sourceFile, sourceContent);
fs.writeFileSync(destFile, destContent);
// Operation 1: Cut and paste
await clipboardTools.cutLines(sessionId, sourceFile, 2, 2);
expect(fs.readFileSync(sourceFile, 'utf-8')).toBe('Line 1\nLine 3\n');
await clipboardTools.pasteLines(sessionId, [{ file_path: destFile, target_line: 1 }]);
expect(fs.readFileSync(destFile, 'utf-8')).toContain('Line 2');
// Operation 2: Undo
await clipboardTools.undoLastPaste(sessionId);
expect(fs.readFileSync(sourceFile, 'utf-8')).toBe(sourceContent); // Source restored
expect(fs.readFileSync(destFile, 'utf-8')).toBe(destContent); // Dest restored
// Operation 3: Paste again (clipboard should still have Line 2)
await clipboardTools.pasteLines(sessionId, [{ file_path: destFile, target_line: 1 }]);
// Now we should have the content pasted again
expect(fs.readFileSync(destFile, 'utf-8')).toContain('Line 2');
// But source should still be original (because we're pasting from clipboard, not cutting again)
// NOTE: This is a design decision - after undo restores source, clipboard still has content
// but paste doesn't re-cut from source
});
test('should track cut operation ID when logging paste', async () => {
// This test verifies the data model links paste to source cut operation
const sourceFile = path.join(testDir, 'source.txt');
const destFile = path.join(testDir, 'dest.txt');
fs.writeFileSync(sourceFile, 'Line 1\nLine 2\n');
fs.writeFileSync(destFile, 'Dest\n');
// Cut from source
await clipboardTools.cutLines(sessionId, sourceFile, 1, 1);
// Paste to dest
await clipboardTools.pasteLines(sessionId, [{ file_path: destFile, target_line: 1 }]);
// Get operation history
const history = await clipboardTools.getOperationHistory(sessionId, 10);
// Should have cut and paste operations
const opTypes = history.operations.map((op) => op.operationType);
expect(opTypes).toContain('cut');
expect(opTypes).toContain('paste');
// The cut operation should have source file metadata
const cutOp = history.operations.find((op) => op.operationType === 'cut');
expect(cutOp).toBeDefined();
expect(cutOp?.details.sourceFile).toBe(sourceFile);
expect(cutOp?.details.startLine).toBe(1);
expect(cutOp?.details.endLine).toBe(1);
});
});