diff --git a/tests/integration/community/community-nodes-integration.test.ts b/tests/integration/community/community-nodes-integration.test.ts index 90c8262..2ac1b85 100644 --- a/tests/integration/community/community-nodes-integration.test.ts +++ b/tests/integration/community/community-nodes-integration.test.ts @@ -87,7 +87,7 @@ class InMemoryDatabaseAdapter implements DatabaseAdapter { class InMemoryPreparedStatement implements PreparedStatement { run = vi.fn((...params: any[]): RunResult => { - if (this.sql.includes('INSERT OR REPLACE INTO nodes')) { + if (this.sql.includes('INSERT INTO nodes')) { const node = this.paramsToNode(params); this.adapter.saveNode(node); return { changes: 1, lastInsertRowid: 1 }; diff --git a/tests/integration/n8n-api/scripts/cleanup-non-test-workflows.ts b/tests/integration/n8n-api/scripts/cleanup-non-test-workflows.ts new file mode 100644 index 0000000..149c2aa --- /dev/null +++ b/tests/integration/n8n-api/scripts/cleanup-non-test-workflows.ts @@ -0,0 +1,115 @@ +#!/usr/bin/env tsx +/** + * Cleanup Non-Test Workflows + * + * Deletes all workflows from the n8n test instance EXCEPT those + * with "[TEST]" in the name. This helps keep the test instance + * clean and prevents list endpoint pagination issues. + * + * Usage: + * npx tsx tests/integration/n8n-api/scripts/cleanup-non-test-workflows.ts + * npx tsx tests/integration/n8n-api/scripts/cleanup-non-test-workflows.ts --dry-run + */ + +import { getN8nCredentials, validateCredentials } from '../utils/credentials'; + +const DRY_RUN = process.argv.includes('--dry-run'); + +interface Workflow { + id: string; + name: string; + active: boolean; +} + +async function fetchAllWorkflows(baseUrl: string, apiKey: string): Promise { + const all: Workflow[] = []; + let cursor: string | undefined; + + while (true) { + const url = new URL('/api/v1/workflows', baseUrl); + url.searchParams.set('limit', '100'); + if (cursor) url.searchParams.set('cursor', cursor); + + const res = await fetch(url.toString(), { + headers: { 'X-N8N-API-KEY': apiKey } + }); + + if (!res.ok) { + throw new Error(`Failed to list workflows: ${res.status} ${res.statusText}`); + } + + const body = await res.json() as { data: Workflow[]; nextCursor?: string }; + all.push(...body.data); + + if (!body.nextCursor) break; + cursor = body.nextCursor; + } + + return all; +} + +async function deleteWorkflow(baseUrl: string, apiKey: string, id: string): Promise { + const res = await fetch(`${baseUrl}/api/v1/workflows/${id}`, { + method: 'DELETE', + headers: { 'X-N8N-API-KEY': apiKey } + }); + + if (!res.ok) { + throw new Error(`Failed to delete workflow ${id}: ${res.status} ${res.statusText}`); + } +} + +async function main() { + const creds = getN8nCredentials(); + validateCredentials(creds); + + console.log(`n8n Instance: ${creds.url}`); + console.log(`Mode: ${DRY_RUN ? 'DRY RUN' : 'LIVE DELETE'}\n`); + + const workflows = await fetchAllWorkflows(creds.url, creds.apiKey); + console.log(`Total workflows found: ${workflows.length}\n`); + + const toKeep = workflows.filter(w => w.name.includes('[TEST]')); + const toDelete = workflows.filter(w => !w.name.includes('[TEST]')); + + console.log(`Keeping (${toKeep.length}):`); + for (const w of toKeep) { + console.log(` ✅ ${w.id} - ${w.name}`); + } + + console.log(`\nDeleting (${toDelete.length}):`); + for (const w of toDelete) { + console.log(` 🗑️ ${w.id} - ${w.name}${w.active ? ' (ACTIVE)' : ''}`); + } + + if (DRY_RUN) { + console.log('\nDry run complete. No workflows were deleted.'); + return; + } + + if (toDelete.length === 0) { + console.log('\nNothing to delete.'); + return; + } + + console.log(`\nDeleting ${toDelete.length} workflows...`); + let deleted = 0; + let failed = 0; + + for (const w of toDelete) { + try { + await deleteWorkflow(creds.url, creds.apiKey, w.id); + deleted++; + } catch (err) { + console.error(` Failed to delete ${w.id} (${w.name}): ${err}`); + failed++; + } + } + + console.log(`\nDone! Deleted: ${deleted}, Failed: ${failed}, Kept: ${toKeep.length}`); +} + +main().catch(err => { + console.error('Fatal error:', err); + process.exit(1); +}); diff --git a/tests/unit/database/node-repository-community.test.ts b/tests/unit/database/node-repository-community.test.ts index 5afd132..624cbd3 100644 --- a/tests/unit/database/node-repository-community.test.ts +++ b/tests/unit/database/node-repository-community.test.ts @@ -120,7 +120,7 @@ class MockPreparedStatement implements PreparedStatement { } // saveNode - INSERT OR REPLACE - if (this.sql.includes('INSERT OR REPLACE INTO nodes')) { + if (this.sql.includes('INSERT INTO nodes')) { this.run = vi.fn((...params: any[]): RunResult => { const nodes = this.mockData.get('community_nodes') || []; const nodeType = params[0]; diff --git a/tests/unit/database/node-repository-core.test.ts b/tests/unit/database/node-repository-core.test.ts index 9359a4f..ce8a311 100644 --- a/tests/unit/database/node-repository-core.test.ts +++ b/tests/unit/database/node-repository-core.test.ts @@ -91,7 +91,7 @@ describe('NodeRepository - Core Functionality', () => { repository.saveNode(parsedNode); // Verify prepare was called with correct SQL - expect(mockAdapter.prepare).toHaveBeenCalledWith(expect.stringContaining('INSERT OR REPLACE INTO nodes')); + expect(mockAdapter.prepare).toHaveBeenCalledWith(expect.stringContaining('INSERT INTO nodes')); // Get the prepared statement and verify run was called const stmt = mockAdapter._getStatement(mockAdapter.prepare.mock.lastCall?.[0] || ''); diff --git a/tests/unit/database/node-repository-outputs.test.ts b/tests/unit/database/node-repository-outputs.test.ts index d609904..2d96364 100644 --- a/tests/unit/database/node-repository-outputs.test.ts +++ b/tests/unit/database/node-repository-outputs.test.ts @@ -56,7 +56,7 @@ describe('NodeRepository - Outputs Handling', () => { repository.saveNode(node); expect(mockDb.prepare).toHaveBeenCalledWith(` - INSERT OR REPLACE INTO nodes ( + INSERT INTO nodes ( node_type, package_name, display_name, description, category, development_style, is_ai_tool, is_trigger, is_webhook, is_versioned, is_tool_variant, tool_variant_of,