mirror of
https://github.com/czlonkowski/n8n-mcp.git
synced 2026-03-28 13:13:08 +00:00
* refactor: streamline test suite - cut 33 files, enable parallel execution (11.9x speedup) Remove duplicate, low-value, and fragmented test files while preserving all meaningful coverage. Enable parallel test execution and remove the entire benchmark infrastructure. Key changes: - Consolidate workflow-validator tests (13 files -> 3) - Consolidate config-validator tests (9 files -> 3) - Consolidate telemetry tests (11 files -> 6) - Merge AI validator tests (2 files -> 1) - Remove example/demo test files, mock-testing files, and already-skipped tests - Remove benchmark infrastructure (10 files, CI workflow, 4 npm scripts) - Enable parallel test execution (remove singleThread: true) - Remove retry:2 that was masking flaky tests - Slim CI publish-results job Results: 224 -> 191 test files, 4690 -> 4303 tests, 121K -> 106K lines Local runtime: 319s -> 27s (11.9x speedup) Conceived by Romuald Członkowski - www.aiadvisors.pl/en Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: absorb config-validator satellite tests into consolidated file The previous commit deleted 4 config-validator satellite files. This properly merges their unique tests into the consolidated config-validator.test.ts, recovering 89 tests that were dropped during the bulk deletion. Deduplicates 5 tests that existed in both the satellite files and the security test file. Conceived by Romuald Członkowski - www.aiadvisors.pl/en Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: delete missed benchmark-pr.yml workflow, fix flaky session test - Remove benchmark-pr.yml that referenced deleted benchmark:ci script - Fix session-persistence round-trip test using timestamps closer to now to avoid edge cases exposed by removing retry:2 Conceived by Romuald Członkowski - www.aiadvisors.pl/en Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: rebuild FTS5 index after database rebuild to prevent stale rowid refs The FTS5 content-synced index could retain phantom rowid references from previous rebuild cycles, causing 'missing row N from content table' errors on MATCH queries. - Add explicit FTS5 rebuild command in rebuild script after all nodes saved - Add FTS5 rebuild in test beforeAll as defense-in-depth - Rebuild nodes.db with consistent FTS5 index Conceived by Romuald Członkowski - www.aiadvisors.pl/en Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: use recent timestamps in all session persistence tests Session round-trip tests used timestamps 5-10 minutes in the past which could fail under CI load when combined with session timeout validation. Use timestamps 30 seconds in the past for all valid-session test data. Conceived by Romuald Członkowski - www.aiadvisors.pl/en Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
236 lines
8.0 KiB
TypeScript
236 lines
8.0 KiB
TypeScript
/**
|
|
* Integration tests for node FTS5 search functionality
|
|
* Ensures the production search failures (Issue #296) are prevented
|
|
*/
|
|
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
import { createDatabaseAdapter } from '../../../src/database/database-adapter';
|
|
import { NodeRepository } from '../../../src/database/node-repository';
|
|
import * as fs from 'fs';
|
|
import * as path from 'path';
|
|
|
|
describe('Node FTS5 Search Integration Tests', () => {
|
|
let db: any;
|
|
let repository: NodeRepository;
|
|
|
|
beforeAll(async () => {
|
|
// Use test database
|
|
const testDbPath = './data/nodes.db';
|
|
db = await createDatabaseAdapter(testDbPath);
|
|
repository = new NodeRepository(db);
|
|
|
|
// Rebuild FTS5 index to ensure it is in sync with the nodes table.
|
|
// The content-synced FTS5 index (content=nodes) can become stale if the
|
|
// database was rebuilt without an explicit FTS5 rebuild command, leaving
|
|
// phantom rowid references that cause "missing row" errors on MATCH queries.
|
|
db.prepare("INSERT INTO nodes_fts(nodes_fts) VALUES('rebuild')").run();
|
|
});
|
|
|
|
afterAll(() => {
|
|
if (db) {
|
|
db.close();
|
|
}
|
|
});
|
|
|
|
describe('FTS5 Table Existence', () => {
|
|
it('should have nodes_fts table in schema', () => {
|
|
const schemaPath = path.join(__dirname, '../../../src/database/schema.sql');
|
|
const schema = fs.readFileSync(schemaPath, 'utf-8');
|
|
|
|
expect(schema).toContain('CREATE VIRTUAL TABLE IF NOT EXISTS nodes_fts USING fts5');
|
|
expect(schema).toContain('CREATE TRIGGER IF NOT EXISTS nodes_fts_insert');
|
|
expect(schema).toContain('CREATE TRIGGER IF NOT EXISTS nodes_fts_update');
|
|
expect(schema).toContain('CREATE TRIGGER IF NOT EXISTS nodes_fts_delete');
|
|
});
|
|
|
|
it('should have nodes_fts table in database', () => {
|
|
const result = db.prepare(`
|
|
SELECT name FROM sqlite_master
|
|
WHERE type='table' AND name='nodes_fts'
|
|
`).get();
|
|
|
|
expect(result).toBeDefined();
|
|
expect(result.name).toBe('nodes_fts');
|
|
});
|
|
|
|
it('should have FTS5 triggers in database', () => {
|
|
const triggers = db.prepare(`
|
|
SELECT name FROM sqlite_master
|
|
WHERE type='trigger' AND name LIKE 'nodes_fts_%'
|
|
`).all();
|
|
|
|
expect(triggers).toHaveLength(3);
|
|
const triggerNames = triggers.map((t: any) => t.name);
|
|
expect(triggerNames).toContain('nodes_fts_insert');
|
|
expect(triggerNames).toContain('nodes_fts_update');
|
|
expect(triggerNames).toContain('nodes_fts_delete');
|
|
});
|
|
});
|
|
|
|
describe('FTS5 Index Population', () => {
|
|
it('should have nodes_fts count matching nodes count', () => {
|
|
const nodesCount = db.prepare('SELECT COUNT(*) as count FROM nodes').get();
|
|
const ftsCount = db.prepare('SELECT COUNT(*) as count FROM nodes_fts').get();
|
|
|
|
expect(nodesCount.count).toBeGreaterThan(500); // Should have both packages
|
|
expect(ftsCount.count).toBe(nodesCount.count);
|
|
});
|
|
|
|
it('should not have empty FTS5 index', () => {
|
|
const ftsCount = db.prepare('SELECT COUNT(*) as count FROM nodes_fts').get();
|
|
|
|
expect(ftsCount.count).toBeGreaterThan(0);
|
|
});
|
|
});
|
|
|
|
describe('Critical Node Searches (Production Failure Cases)', () => {
|
|
it('should find webhook node via FTS5', () => {
|
|
const results = db.prepare(`
|
|
SELECT node_type FROM nodes_fts
|
|
WHERE nodes_fts MATCH 'webhook'
|
|
`).all();
|
|
|
|
expect(results.length).toBeGreaterThan(0);
|
|
const nodeTypes = results.map((r: any) => r.node_type);
|
|
expect(nodeTypes).toContain('nodes-base.webhook');
|
|
});
|
|
|
|
it('should find merge node via FTS5', () => {
|
|
const results = db.prepare(`
|
|
SELECT node_type FROM nodes_fts
|
|
WHERE nodes_fts MATCH 'merge'
|
|
`).all();
|
|
|
|
expect(results.length).toBeGreaterThan(0);
|
|
const nodeTypes = results.map((r: any) => r.node_type);
|
|
expect(nodeTypes).toContain('nodes-base.merge');
|
|
});
|
|
|
|
it('should find split batch node via FTS5', () => {
|
|
const results = db.prepare(`
|
|
SELECT node_type FROM nodes_fts
|
|
WHERE nodes_fts MATCH 'split OR batch'
|
|
`).all();
|
|
|
|
expect(results.length).toBeGreaterThan(0);
|
|
const nodeTypes = results.map((r: any) => r.node_type);
|
|
expect(nodeTypes).toContain('nodes-base.splitInBatches');
|
|
});
|
|
|
|
it('should find code node via FTS5', () => {
|
|
const results = db.prepare(`
|
|
SELECT node_type FROM nodes_fts
|
|
WHERE nodes_fts MATCH 'code'
|
|
`).all();
|
|
|
|
expect(results.length).toBeGreaterThan(0);
|
|
const nodeTypes = results.map((r: any) => r.node_type);
|
|
expect(nodeTypes).toContain('nodes-base.code');
|
|
});
|
|
|
|
it('should find http request node via FTS5', () => {
|
|
const results = db.prepare(`
|
|
SELECT node_type FROM nodes_fts
|
|
WHERE nodes_fts MATCH 'http OR request'
|
|
`).all();
|
|
|
|
expect(results.length).toBeGreaterThan(0);
|
|
const nodeTypes = results.map((r: any) => r.node_type);
|
|
expect(nodeTypes).toContain('nodes-base.httpRequest');
|
|
});
|
|
});
|
|
|
|
describe('FTS5 Search Quality', () => {
|
|
it('should rank exact matches higher', () => {
|
|
const results = db.prepare(`
|
|
SELECT
|
|
n.node_type,
|
|
rank
|
|
FROM nodes n
|
|
JOIN nodes_fts ON n.rowid = nodes_fts.rowid
|
|
WHERE nodes_fts MATCH 'webhook'
|
|
ORDER BY
|
|
CASE
|
|
WHEN LOWER(n.display_name) = LOWER('webhook') THEN 0
|
|
WHEN LOWER(n.display_name) LIKE LOWER('%webhook%') THEN 1
|
|
WHEN LOWER(n.node_type) LIKE LOWER('%webhook%') THEN 2
|
|
ELSE 3
|
|
END,
|
|
rank
|
|
LIMIT 10
|
|
`).all();
|
|
|
|
expect(results.length).toBeGreaterThan(0);
|
|
// Exact match should be in top results (using production boosting logic with CASE-first ordering)
|
|
const topResults = results.slice(0, 3).map((r: any) => r.node_type);
|
|
expect(topResults).toContain('nodes-base.webhook');
|
|
});
|
|
|
|
it('should support phrase searches', () => {
|
|
const results = db.prepare(`
|
|
SELECT node_type FROM nodes_fts
|
|
WHERE nodes_fts MATCH '"http request"'
|
|
`).all();
|
|
|
|
expect(results.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('should support boolean operators', () => {
|
|
const andResults = db.prepare(`
|
|
SELECT node_type FROM nodes_fts
|
|
WHERE nodes_fts MATCH 'google AND sheets'
|
|
`).all();
|
|
|
|
const orResults = db.prepare(`
|
|
SELECT node_type FROM nodes_fts
|
|
WHERE nodes_fts MATCH 'google OR sheets'
|
|
`).all();
|
|
|
|
expect(andResults.length).toBeGreaterThan(0);
|
|
expect(orResults.length).toBeGreaterThanOrEqual(andResults.length);
|
|
});
|
|
});
|
|
|
|
describe('FTS5 Index Synchronization', () => {
|
|
it('should keep FTS5 in sync after node updates', () => {
|
|
// This test ensures triggers work properly
|
|
const beforeCount = db.prepare('SELECT COUNT(*) as count FROM nodes_fts').get();
|
|
|
|
// Insert a test node
|
|
db.prepare(`
|
|
INSERT INTO nodes (
|
|
node_type, package_name, display_name, description,
|
|
category, development_style, is_ai_tool, is_trigger,
|
|
is_webhook, is_versioned, version, properties_schema,
|
|
operations, credentials_required
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
`).run(
|
|
'test.node',
|
|
'test-package',
|
|
'Test Node',
|
|
'A test node for FTS5 synchronization',
|
|
'Test',
|
|
'programmatic',
|
|
0, 0, 0, 0,
|
|
'1.0',
|
|
'[]', '[]', '[]'
|
|
);
|
|
|
|
const afterInsert = db.prepare('SELECT COUNT(*) as count FROM nodes_fts').get();
|
|
expect(afterInsert.count).toBe(beforeCount.count + 1);
|
|
|
|
// Verify the new node is searchable
|
|
const searchResults = db.prepare(`
|
|
SELECT node_type FROM nodes_fts
|
|
WHERE nodes_fts MATCH 'test synchronization'
|
|
`).all();
|
|
expect(searchResults.length).toBeGreaterThan(0);
|
|
|
|
// Clean up
|
|
db.prepare('DELETE FROM nodes WHERE node_type = ?').run('test.node');
|
|
|
|
const afterDelete = db.prepare('SELECT COUNT(*) as count FROM nodes_fts').get();
|
|
expect(afterDelete.count).toBe(beforeCount.count);
|
|
});
|
|
});
|
|
});
|