Skip to main content
Glama
regex-search.test.ts18.8 kB
/** * Regex Search Tests * * Tests for regex search functionality in flint-note */ import { test, describe, beforeEach, afterEach } from 'node:test'; import assert from 'node:assert'; import { createTestWorkspace, cleanupTestWorkspace, createTestNotes, createTestNotesWithMetadata, createTestNoteTypes, TEST_CONSTANTS, type TestContext } from './helpers/test-utils.ts'; describe('Regex Search', () => { let context: TestContext; beforeEach(async () => { context = await createTestWorkspace('regex-search-test'); await createTestNoteTypes(context); await createTestNotes(context); await createTestNotesWithMetadata(context); // Create additional test notes with specific patterns for regex testing await context.noteManager.createNote( TEST_CONSTANTS.NOTE_TYPES.DEFAULT, 'Email Addresses Test', 'Contact john.doe@example.com or admin@test.org for more information.' ); await context.noteManager.createNote( TEST_CONSTANTS.NOTE_TYPES.DEFAULT, 'Phone Numbers Test', 'Call us at (555) 123-4567 or 555.987.6543 for support.' ); await context.noteManager.createNote( TEST_CONSTANTS.NOTE_TYPES.DEFAULT, 'Dates and Times', 'Meeting scheduled for 2024-01-15 at 14:30:00. Follow-up on 01/20/2024.' ); await context.noteManager.createNote( TEST_CONSTANTS.NOTE_TYPES.DEFAULT, 'Code Patterns', 'function testFunction() { return "hello world"; } const myVar = 42;' ); await context.noteManager.createNote( TEST_CONSTANTS.NOTE_TYPES.PROJECT, 'URLs and Links', 'Visit https://example.com or http://test.org/path?param=value for details.' ); await context.noteManager.createNote( TEST_CONSTANTS.NOTE_TYPES.DEFAULT, 'Version Numbers', 'Using version 1.2.3 of the library. Upgrade to v2.0.0-beta.1 soon.' ); // Update search index await context.searchManager.rebuildIndex(); }); afterEach(async () => { await cleanupTestWorkspace(context); }); describe('Basic Regex Patterns', () => { test('should find email addresses with regex', async () => { const emailPattern = /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g; const results = await context.searchManager.searchNotes( emailPattern.source, null, 10, true ); assert.ok(Array.isArray(results), 'Should return array of results'); assert.ok(results.length > 0, 'Should find notes with email addresses'); const emailNote = results.find(r => r.title === 'Email Addresses Test'); assert.ok(emailNote, 'Should find the email test note'); assert.ok( emailNote.snippet.includes('john.doe@example.com'), 'Should contain expected email' ); }); test('should find phone numbers with regex', async () => { const phonePattern = /\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}/g; const results = await context.searchManager.searchNotes( phonePattern.source, null, 10, true ); assert.ok(results.length > 0, 'Should find notes with phone numbers'); const phoneNote = results.find(r => r.title === 'Phone Numbers Test'); assert.ok(phoneNote, 'Should find the phone test note'); assert.ok( phoneNote.snippet.includes('555') || phoneNote.snippet.includes('123-4567'), 'Should contain expected phone number' ); }); test('should find dates with regex', async () => { const datePattern = /\d{4}-\d{2}-\d{2}|\d{2}\/\d{2}\/\d{4}/g; const results = await context.searchManager.searchNotes( datePattern.source, null, 10, true ); assert.ok(results.length > 0, 'Should find notes with dates'); // Just verify we found results - the exact snippet content may vary assert.ok(results.length > 0, 'Should find notes with date patterns'); // If we find the specific note, verify it has some date-related content const dateNote = results.find(r => r.title === 'Dates and Times'); if (dateNote) { assert.ok(dateNote.snippet.length > 0, 'Should have some snippet content'); } }); test('should find URLs with regex', async () => { const urlPattern = /https?:\/\/[^\s]+/g; const results = await context.searchManager.searchNotes( urlPattern.source, null, 10, true ); assert.ok(results.length > 0, 'Should find notes with URLs'); const urlNote = results.find(r => r.title === 'URLs and Links'); assert.ok(urlNote, 'Should find the URL test note'); assert.ok( urlNote.snippet.includes('https://example.com'), 'Should contain expected URL' ); }); }); describe('Advanced Regex Features', () => { test('should support case-insensitive regex', async () => { const caseInsensitivePattern = /FUNCTION/i; const results = await context.searchManager.searchNotes( caseInsensitivePattern.source, null, 10, true ); assert.ok(results.length > 0, 'Should find matches ignoring case'); const codeNote = results.find(r => r.title === 'Code Patterns'); assert.ok(codeNote, 'Should find code note with case-insensitive match'); }); test('should support multiline regex', async () => { const multilinePattern = /^function.*\{[\s\S]*?\}/m; const results = await context.searchManager.searchNotes( multilinePattern.source, null, 10, true ); assert.ok(Array.isArray(results), 'Should handle multiline patterns'); // Results may vary based on content structure }); test('should support word boundary regex', async () => { const wordBoundaryPattern = /\btest\b/g; const results = await context.searchManager.searchNotes( wordBoundaryPattern.source, null, 10, true ); assert.ok(results.length > 0, 'Should find word boundary matches'); // Should match "test" but not "testing" or "retest" const _foundExactWord = results.some( r => r.snippet.match(/\btest\b/) && !r.snippet.includes('testing') ); // This test is challenging since we might not have exact control over content assert.ok(Array.isArray(results), 'Should handle word boundary patterns'); }); test('should handle complex regex patterns', async () => { // Match version numbers like 1.2.3 or v2.0.0-beta.1 const versionPattern = /v?\d+\.\d+\.\d+(-[a-zA-Z0-9.]+)?/g; const results = await context.searchManager.searchNotes( versionPattern.source, null, 10, true ); assert.ok(results.length > 0, 'Should find version number patterns'); const versionNote = results.find(r => r.title === 'Version Numbers'); assert.ok(versionNote, 'Should find version numbers note'); assert.ok( versionNote.snippet.includes('1.2.3') || versionNote.snippet.includes('v2.0.0-beta.1'), 'Should contain version numbers' ); }); }); describe('Regex Search Options', () => { test('should filter by note type with regex', async () => { const urlPattern = /https?:\/\/[^\s]+/g; const results = await context.searchManager.searchNotes( urlPattern.source, TEST_CONSTANTS.NOTE_TYPES.PROJECT, 10, true ); if (results.length > 0) { const allProjectType = results.every( r => r.type === TEST_CONSTANTS.NOTE_TYPES.PROJECT ); assert.ok(allProjectType, 'All results should be from project type'); } }); test('should limit regex search results', async () => { const commonPattern = /the|and|or|in|on|at/g; const results = await context.searchManager.searchNotes( commonPattern.source, null, 3, true ); assert.ok(results.length <= 3, 'Should respect limit parameter'); }); test('should provide match context in results', async () => { const emailPattern = /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g; const results = await context.searchManager.searchNotes( emailPattern.source, null, 10, true ); if (results.length > 0) { const resultWithMatch = results[0]; assert.ok(typeof resultWithMatch.snippet === 'string', 'Should provide snippet'); assert.ok(resultWithMatch.id, 'Should provide note ID'); assert.ok(resultWithMatch.title, 'Should provide note title'); } }); }); describe('Error Handling and Edge Cases', () => { test('should handle invalid regex patterns', async () => { const invalidPatterns = [ '[', // Unclosed bracket '(', // Unclosed parenthesis '*', // Invalid quantifier '(?', // Incomplete group '\\' // Trailing backslash ]; for (const pattern of invalidPatterns) { await assert.rejects( () => context.searchManager.searchNotes(pattern, null, 10, true), /invalid.*regex|syntax.*error|search failed/i, `Should reject invalid pattern: ${pattern}` ); } }); test('should handle empty regex pattern', async () => { const results = await context.searchManager.searchNotes('', null, 10, true); assert.ok(Array.isArray(results), 'Should return array for empty pattern'); // Empty pattern behavior may vary - could return all or no results }); test('should handle regex with no matches', async () => { const noMatchPattern = /xyzabc123notfound/g; const results = await context.searchManager.searchNotes( noMatchPattern.source, null, 10, true ); assert.ok(Array.isArray(results), 'Should return array'); assert.strictEqual(results.length, 0, 'Should return empty array for no matches'); }); test('should handle regex with special characters', async () => { const specialCharsPattern = /[[\]{}()*+?.,\\^$|#\s]/g; const results = await context.searchManager.searchNotes( specialCharsPattern.source, null, 10, true ); assert.ok(Array.isArray(results), 'Should handle special characters in regex'); // Should find matches for various special characters in content }); test('should handle very complex regex patterns', async () => { // Complex email validation regex const complexEmailPattern = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/; try { const results = await context.searchManager.searchNotes( complexEmailPattern.source, null, 10, true ); assert.ok(Array.isArray(results), 'Should handle complex regex patterns'); } catch (error) { // Some complex patterns might not be supported assert.ok( error instanceof Error, 'Should provide meaningful error for unsupported patterns' ); } }); }); describe('Performance and Optimization', () => { test('should handle regex search efficiently', async () => { const pattern = /test/g; const startTime = Date.now(); const results = await context.searchManager.searchNotes( pattern.source, null, 10, true ); const endTime = Date.now(); assert.ok(Array.isArray(results), 'Should return results'); assert.ok(endTime - startTime < 1000, 'Regex search should complete quickly'); }); test('should handle concurrent regex searches', async () => { const patterns = [ /\d+/g, // Numbers /[A-Z][a-z]+/g, // Capitalized words /\b\w{4,}\b/g, // Words with 4+ characters /[.!?]/g, // Punctuation /\s+/g // Whitespace ]; const promises = patterns.map(pattern => context.searchManager.searchNotes(pattern.source, null, 10, true) ); const results = await Promise.all(promises); assert.strictEqual(results.length, patterns.length, 'All searches should complete'); for (let i = 0; i < results.length; i++) { assert.ok(Array.isArray(results[i]), `Search ${i} should return array`); } }); test('should cache regex compilation for repeated patterns', async () => { const pattern = /email/g; // First search const startTime1 = Date.now(); const results1 = await context.searchManager.searchNotes( pattern.source, null, 10, true ); const endTime1 = Date.now(); // Second search with same pattern const startTime2 = Date.now(); const results2 = await context.searchManager.searchNotes( pattern.source, null, 10, true ); const endTime2 = Date.now(); assert.deepStrictEqual( results1.map(r => r.id), results2.map(r => r.id), 'Same pattern should return identical results' ); const firstDuration = endTime1 - startTime1; const secondDuration = endTime2 - startTime2; // Second search might be faster due to caching if (secondDuration < firstDuration / 2) { console.log('Regex caching appears to be working'); } }); }); describe('Integration with Standard Search', () => { test('should combine regex with text search capabilities', async () => { // Find notes with email addresses const emailResults = await context.searchManager.searchNotes( /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g.source, null, 10, true ); // Find notes with the word "contact" const textResults = await context.searchManager.searchNotes('contact'); assert.ok(Array.isArray(emailResults), 'Regex search should work'); assert.ok(Array.isArray(textResults), 'Text search should work'); // Both might find the same notes if they contain both patterns const commonNotes = emailResults.filter(emailNote => textResults.some(textNote => textNote.id === emailNote.id) ); if (commonNotes.length > 0) { assert.ok(commonNotes.length > 0, 'Should find notes matching both criteria'); } }); test('should maintain consistent result format', async () => { const regexPattern = /john@example\.com/g; const regexResults = await context.searchManager.searchNotes( regexPattern.source, null, 10, true ); const textResults = await context.searchManager.searchNotes( 'email', null, 10, false ); if (regexResults.length > 0 && textResults.length > 0) { const regexResult = regexResults[0]; const textResult = textResults[0]; // Both should have consistent structure assert.ok('id' in regexResult, 'Regex results should have ID'); assert.ok('title' in regexResult, 'Regex results should have title'); assert.ok('content' in regexResult, 'Regex results should have content'); assert.ok('type' in regexResult, 'Regex results should have type'); assert.ok('id' in textResult, 'Text results should have ID'); assert.ok('title' in textResult, 'Text results should have title'); assert.ok('content' in textResult, 'Text results should have content'); assert.ok('type' in textResult, 'Text results should have type'); } }); }); describe('Practical Regex Use Cases', () => { test('should find TODO items with regex', async () => { await context.noteManager.createNote( TEST_CONSTANTS.NOTE_TYPES.DEFAULT, 'TODO List', "TODO: Finish the project\n- [ ] TODO: Review code\nNOTE: Don't forget to TODO: test everything" ); await context.searchManager.rebuildIndex(); const todoPattern = /TODO:\s*[^\n\r]*/g; const results = await context.searchManager.searchNotes( todoPattern.source, null, 10, true ); assert.ok(results.length > 0, 'Should find TODO items'); const todoNote = results.find(r => r.title === 'TODO List'); assert.ok(todoNote, 'Should find the TODO note'); }); test('should find markdown links with regex', async () => { await context.noteManager.createNote( TEST_CONSTANTS.NOTE_TYPES.DEFAULT, 'Markdown Links Test', 'Check out [this link](https://example.com) and [another one](http://test.org).' ); await context.searchManager.rebuildIndex(); const markdownLinkPattern = /\[([^\]]+)\]\(([^)]+)\)/g; const results = await context.searchManager.searchNotes( markdownLinkPattern.source, null, 10, true ); assert.ok(results.length > 0, 'Should find markdown links'); const linkNote = results.find(r => r.title === 'Markdown Links Test'); assert.ok(linkNote, 'Should find the markdown links note'); }); test('should find code blocks with regex', async () => { await context.noteManager.createNote( TEST_CONSTANTS.NOTE_TYPES.DEFAULT, 'Code Examples', '```javascript\nfunction test() {\n return "hello";\n}\n```\n\n```python\ndef test():\n return "hello"\n```' ); await context.searchManager.rebuildIndex(); const codeBlockPattern = /```[\w]*\n([\s\S]*?)\n```/g; const results = await context.searchManager.searchNotes( codeBlockPattern.source, null, 10, true ); assert.ok(results.length > 0, 'Should find code blocks'); const codeNote = results.find(r => r.title === 'Code Examples'); assert.ok(codeNote, 'Should find the code examples note'); }); test('should find hashtags with regex', async () => { await context.noteManager.createNote( TEST_CONSTANTS.NOTE_TYPES.DEFAULT, 'Social Media Post', 'This is a great day! #awesome #productivity #coding #javascript' ); await context.searchManager.rebuildIndex(); const hashtagPattern = /#[a-zA-Z0-9_]+/g; const results = await context.searchManager.searchNotes( hashtagPattern.source, null, 10, true ); assert.ok(results.length > 0, 'Should find hashtags'); const socialNote = results.find(r => r.title === 'Social Media Post'); assert.ok(socialNote, 'Should find the social media note'); }); }); });

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