regex-search.test.ts•18.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');
});
});
});