Skip to main content
Glama
deletion-integration.test.ts19.9 kB
/** * Integration tests for note and note type deletion functionality * * Tests the full end-to-end deletion workflows through the MCP server, * including server communication, file system operations, and cross-system integration. */ import { test, describe, beforeEach, afterEach } from 'node:test'; import { strict as assert } from 'node:assert'; import { promises as fs } from 'node:fs'; import { join } from 'node:path'; import { type ChildProcess } from 'node:child_process'; import { createIntegrationWorkspace, cleanupIntegrationWorkspace, startServer, stopServer, createIntegrationTestNotes, createTestNoteType, type IntegrationTestContext, INTEGRATION_CONSTANTS } from './helpers/integration-utils.js'; /** * Simple MCP client for testing server communication */ class MCPClient { #serverProcess: ChildProcess; constructor(serverProcess: ChildProcess) { this.#serverProcess = serverProcess; } async sendRequest(method: string, params: Record<string, unknown>): Promise<unknown> { return new Promise((resolve, reject) => { const request = { jsonrpc: '2.0', id: Math.floor(Math.random() * 10000), method, params }; const requestStr = JSON.stringify(request) + '\n'; let responseBuffer = ''; let hasResponded = false; const timeout = setTimeout(() => { if (!hasResponded) { hasResponded = true; reject(new Error(`Request timeout for ${method}`)); } }, INTEGRATION_CONSTANTS.DEFAULT_TIMEOUT); const onData = (data: Buffer) => { responseBuffer += data.toString(); // Look for complete JSON response const lines = responseBuffer.split('\n'); for (const line of lines) { if (line.trim() && !hasResponded) { try { const response = JSON.parse(line); if (response.id === request.id) { hasResponded = true; clearTimeout(timeout); this.#serverProcess.stdout?.off('data', onData); if (response.error) { reject(new Error(response.error.message || 'Server error')); } else { resolve(response.result); } return; } } catch { // Not a complete JSON line yet, continue } } } }; this.#serverProcess.stdout?.on('data', onData); this.#serverProcess.stdin?.write(requestStr); }); } async callTool(name: string, args: Record<string, unknown>): Promise<unknown> { return await this.sendRequest('tools/call', { name, arguments: args }); } } describe('Deletion Integration Tests', () => { let context: IntegrationTestContext; let serverProcess: ChildProcess; let client: MCPClient; beforeEach(async () => { // Create workspace and test data context = await createIntegrationWorkspace('deletion-test'); await createIntegrationTestNotes(context.tempDir); // Create additional test note types for deletion testing await createTestNoteType( context.tempDir, 'temporary', 'Temporary notes for deletion testing' ); await createTestNoteType( context.tempDir, 'archive', 'Archive notes for migration testing' ); // Start server serverProcess = await startServer({ workspacePath: context.tempDir, timeout: INTEGRATION_CONSTANTS.SERVER_STARTUP_TIMEOUT }); context.serverProcess = serverProcess; client = new MCPClient(serverProcess); }); afterEach(async () => { if (serverProcess && !serverProcess.killed) { await stopServer(serverProcess, INTEGRATION_CONSTANTS.SERVER_SHUTDOWN_TIMEOUT); } await cleanupIntegrationWorkspace(context); }); describe('Single Note Deletion', () => { test('should delete a note through MCP server', async () => { // Create a note to delete const createResult = await client.callTool('create_note', { type: 'general', title: 'Delete Me', content: '# Delete Me\n\nThis note should be deleted.' }); assert.ok(createResult, 'Note creation should succeed'); const noteData = JSON.parse((createResult as any).content[0].text); const noteId = noteData.id; // Verify note exists in file system const notePath = join(context.tempDir, noteId + '.md'); await fs.access(notePath); // Should not throw // Delete the note const deleteResult = await client.callTool('delete_note', { identifier: noteId, confirm: true }); assert.ok(deleteResult, 'Delete should return result'); const deleteData = JSON.parse((deleteResult as any).content[0].text); assert.ok(deleteData.success, 'Delete should be successful'); assert.ok(deleteData.result.deleted, 'Result should indicate deletion'); // Verify note is removed from file system try { await fs.access(notePath); assert.fail('Note file should be deleted'); } catch { // Expected - file should not exist } }); test('should require confirmation for note deletion', async () => { // Create a note const createResult = await client.callTool('create_note', { type: 'general', title: 'Needs Confirmation', content: '# Needs Confirmation\n\nThis deletion needs confirmation.' }); const noteData = JSON.parse((createResult as any).content[0].text); const noteId = noteData.id; // Try to delete without confirmation const deleteResult = await client.callTool('delete_note', { identifier: noteId, confirm: false }); const deleteData = JSON.parse((deleteResult as any).content[0].text); assert.ok(!deleteData.success, 'Delete without confirmation should fail'); assert.ok( deleteData.error.includes('confirmation') || deleteData.error.includes('confirm'), 'Error should mention confirmation requirement' ); // Verify note still exists const notePath = join(context.tempDir, noteId + '.md'); await fs.access(notePath); // Should not throw }); test('should handle deletion of non-existent note', async () => { const deleteResult = await client.callTool('delete_note', { identifier: 'general/non-existent.md', confirm: true }); const deleteData = JSON.parse((deleteResult as any).content[0].text); assert.ok(!deleteData.success, 'Delete of non-existent note should fail'); assert.ok(deleteData.error, 'Should include error message'); }); }); describe('Note Type Deletion', () => { test('should delete empty note type with error strategy', async () => { // Delete the temporary note type (should be empty) const deleteResult = await client.callTool('delete_note_type', { type_name: 'temporary', action: 'error', confirm: true }); assert.ok(deleteResult, 'Delete should return result'); const deleteData = JSON.parse((deleteResult as any).content[0].text); assert.ok(deleteData.success, 'Delete should be successful'); // Verify directory is removed const typeDir = join(context.tempDir, 'temporary'); try { await fs.access(typeDir); assert.fail('Note type directory should be deleted'); } catch { // Expected - directory should not exist } }); test('should prevent deletion of non-empty note type with error strategy', async () => { // Create a note in the temporary type await client.callTool('create_note', { type: 'temporary', title: 'Blocking Note', content: '# Blocking Note\n\nThis note prevents type deletion.' }); // Try to delete with error strategy const deleteResult = await client.callTool('delete_note_type', { type_name: 'temporary', action: 'error', confirm: true }); const deleteData = JSON.parse((deleteResult as any).content[0].text); assert.ok(!deleteData.success, 'Delete should fail with existing notes'); assert.ok( deleteData.error.includes('contains') && deleteData.error.includes('notes'), 'Error should mention existing notes' ); // Verify type directory still exists const typeDir = join(context.tempDir, 'temporary'); await fs.access(typeDir); // Should not throw }); test('should migrate notes to target type', async () => { // Create notes in temporary type await client.callTool('create_note', { type: 'temporary', title: 'Migrate Me 1', content: '# Migrate Me 1\n\nThis note should be migrated.' }); await client.callTool('create_note', { type: 'temporary', title: 'Migrate Me 2', content: '# Migrate Me 2\n\nThis note should also be migrated.' }); // Delete note type with migration const deleteResult = await client.callTool('delete_note_type', { type_name: 'temporary', action: 'migrate', target_type: 'archive', confirm: true }); const deleteData = JSON.parse((deleteResult as any).content[0].text); assert.ok(deleteData.success, 'Migration should be successful'); // Verify notes are now in archive type const archiveDir = join(context.tempDir, 'archive'); const archiveFiles = await fs.readdir(archiveDir); const migratedFiles = archiveFiles.filter( file => file.includes('migrate-me-1') || file.includes('migrate-me-2') ); assert.strictEqual(migratedFiles.length, 2, 'Both notes should be migrated'); // Verify temporary directory is removed const tempDir = join(context.tempDir, 'temporary'); try { await fs.access(tempDir); assert.fail('Temporary note type directory should be deleted'); } catch { // Expected - directory should not exist } }); test('should delete note type and all its notes', async () => { // Create notes in temporary type await client.callTool('create_note', { type: 'temporary', title: 'Delete Me 1', content: '# Delete Me 1\n\nThis note should be deleted.' }); await client.callTool('create_note', { type: 'temporary', title: 'Delete Me 2', content: '# Delete Me 2\n\nThis note should also be deleted.' }); // Delete note type with delete strategy const deleteResult = await client.callTool('delete_note_type', { type_name: 'temporary', action: 'delete', confirm: true }); const deleteData = JSON.parse((deleteResult as any).content[0].text); assert.ok(deleteData.success, 'Deletion should be successful'); // Verify directory is completely removed const tempDir = join(context.tempDir, 'temporary'); try { await fs.access(tempDir); assert.fail('Note type directory should be deleted'); } catch { // Expected - directory should not exist } }); test('should require confirmation for note type deletion', async () => { const deleteResult = await client.callTool('delete_note_type', { type_name: 'temporary', action: 'error', confirm: false }); const deleteData = JSON.parse((deleteResult as any).content[0].text); assert.ok(!deleteData.success, 'Delete without confirmation should fail'); assert.ok( deleteData.error.includes('confirmation') || deleteData.error.includes('confirm'), 'Error should mention confirmation requirement' ); }); }); describe('Bulk Note Deletion', () => { test('should delete notes by type', async () => { // Create notes in temporary type await client.callTool('create_note', { type: 'temporary', title: 'Bulk Delete 1', content: '# Bulk Delete 1\n\nFirst note to bulk delete.' }); await client.callTool('create_note', { type: 'temporary', title: 'Bulk Delete 2', content: '# Bulk Delete 2\n\nSecond note to bulk delete.' }); // Bulk delete by type const deleteResult = await client.callTool('bulk_delete_notes', { type: 'temporary', confirm: true }); const deleteData = JSON.parse((deleteResult as any).content[0].text); assert.ok(deleteData.success, 'Bulk delete should be successful'); // Verify temporary directory is empty or removed const tempDir = join(context.tempDir, 'temporary'); try { const files = await fs.readdir(tempDir); const markdownFiles = files.filter(f => f.endsWith('.md')); assert.strictEqual(markdownFiles.length, 0, 'No markdown files should remain'); } catch { // Directory might be completely removed, which is also acceptable } }); test('should delete notes by pattern', async () => { // Create notes with specific pattern await client.callTool('create_note', { type: 'general', title: 'temp-file-1', content: '# Temporary File 1\n\nTemporary content.' }); await client.callTool('create_note', { type: 'general', title: 'temp-file-2', content: '# Temporary File 2\n\nMore temporary content.' }); await client.callTool('create_note', { type: 'general', title: 'important-file', content: '# Important File\n\nImportant content.' }); // Bulk delete by pattern const deleteResult = await client.callTool('bulk_delete_notes', { pattern: 'temp-.*', confirm: true }); const deleteData = JSON.parse((deleteResult as any).content[0].text); assert.ok(deleteData.success, 'Pattern-based bulk delete should be successful'); // Verify only temp files are deleted, important file remains const generalDir = join(context.tempDir, 'general'); const files = await fs.readdir(generalDir); const importantFile = files.find(f => f.includes('important-file')); assert.ok(importantFile, 'Important file should remain'); const tempFiles = files.filter(f => f.includes('temp-file')); assert.strictEqual(tempFiles.length, 0, 'Temp files should be deleted'); }); test('should require confirmation for bulk deletion', async () => { // Create a note for bulk deletion await client.callTool('create_note', { type: 'temporary', title: 'Bulk Test', content: '# Bulk Test\n\nTest content.' }); // Try bulk delete without confirmation const deleteResult = await client.callTool('bulk_delete_notes', { type: 'temporary', confirm: false }); const deleteData = JSON.parse((deleteResult as any).content[0].text); assert.ok(!deleteData.success, 'Bulk delete without confirmation should fail'); assert.ok( deleteData.error.includes('confirmation') || deleteData.error.includes('confirm'), 'Error should mention confirmation requirement' ); }); }); describe('Error Handling and Edge Cases', () => { test('should handle invalid note identifiers gracefully', async () => { const invalidIdentifiers = [ 'invalid-format', '/absolute/path.md', '../relative/path.md', '' ]; for (const identifier of invalidIdentifiers) { const deleteResult = await client.callTool('delete_note', { identifier, confirm: true }); const deleteData = JSON.parse((deleteResult as any).content[0].text); assert.ok(!deleteData.success, `Invalid identifier should fail: ${identifier}`); assert.ok(deleteData.error, 'Should include error message'); } }); test('should handle deletion of non-existent note type', async () => { const deleteResult = await client.callTool('delete_note_type', { type_name: 'non-existent-type', action: 'error', confirm: true }); const deleteData = JSON.parse((deleteResult as any).content[0].text); assert.ok(!deleteData.success, 'Delete of non-existent type should fail'); assert.ok(deleteData.error, 'Should include error message'); }); test('should handle migration to non-existent target type', async () => { // Create a note in temporary type await client.callTool('create_note', { type: 'temporary', title: 'Migration Test', content: '# Migration Test\n\nTest content.' }); const deleteResult = await client.callTool('delete_note_type', { type_name: 'temporary', action: 'migrate', target_type: 'non-existent-target', confirm: true }); const deleteData = JSON.parse((deleteResult as any).content[0].text); assert.ok(!deleteData.success, 'Migration to non-existent type should fail'); assert.ok(deleteData.error, 'Should include error message'); }); }); describe('File System Integration', () => { test('should maintain file system consistency during deletion', async () => { // Create a note const createResult = await client.callTool('create_note', { type: 'general', title: 'Consistency Test', content: '# Consistency Test\n\nTest content for consistency.' }); const noteData = JSON.parse((createResult as any).content[0].text); const noteId = noteData.id; const notePath = join(context.tempDir, noteId + '.md'); // Verify file exists await fs.access(notePath); const beforeContent = await fs.readFile(notePath, 'utf8'); assert.ok( beforeContent.includes('Consistency Test'), 'File should contain expected content' ); // Delete the note await client.callTool('delete_note', { identifier: noteId, confirm: true }); // Verify file is completely removed try { await fs.access(notePath); assert.fail('File should be completely removed from file system'); } catch { // Expected - file should not exist } }); test('should handle concurrent operations during deletion', async () => { // Create multiple notes const notePromises: Promise<unknown>[] = []; for (let i = 0; i < 3; i++) { notePromises.push( client.callTool('create_note', { type: 'general', title: `Concurrent Test ${i}`, content: `# Concurrent Test ${i}\n\nContent for concurrent testing.` }) ); } const createResults = await Promise.all(notePromises); const noteIds = createResults.map(result => { const data = JSON.parse((result as any).content[0].text); return data.id; }); // Attempt concurrent deletions const deletePromises = noteIds.map(id => client.callTool('delete_note', { identifier: id, confirm: true }) ); const deleteResults = await Promise.all(deletePromises); // All deletions should succeed for (const result of deleteResults) { const deleteData = JSON.parse((result as any).content[0].text); assert.ok(deleteData.success, 'Concurrent deletion should succeed'); } // Verify all files are removed for (const noteId of noteIds) { const notePath = join(context.tempDir, noteId + '.md'); try { await fs.access(notePath); assert.fail(`File should be deleted: ${noteId}`); } catch { // Expected - file should not exist } } }); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/disnet/flint-note'

If you have feedback or need assistance with the MCP directory API, please join our Discord server