import { detectQueryType, parseSearchQuery, queryToPredicate } from './search-parser'; import { SearchContext } from './search-parser.types'; type TestFn = () => void; interface TestResult { name: string; error?: Error; } const testResults: TestResult[] = []; const runTest = (name: string, fn: TestFn): void => { try { fn(); testResults.push({ name }); } catch (error) { testResults.push({ name, error: error instanceof Error ? error : new Error(String(error)) }); console.error(`[search-parser spec] ${name} failed`, error); } }; const assert = (condition: boolean, message: string): void => { if (!condition) { throw new Error(message); } }; const assertEqual = (actual: T, expected: T, message: string): void => { assert(actual === expected, `${message} (expected ${expected}, received ${actual})`); }; const createContext = (overrides: Partial = {}): SearchContext => ({ filePath: 'notes/example.md', fileName: 'example', fileNameWithExt: 'example.md', content: 'Hello world with #tag in section heading', tags: ['#tag'], properties: { author: 'Ada', status: 'draft' }, lines: ['Hello world with #tag', 'Another line'], blocks: ['Hello world with #tag in section heading'], sections: [{ heading: 'Section', content: 'Hello world', level: 1 }], tasks: [ { text: 'Call Bob', completed: false, line: 3 }, { text: 'Review PR', completed: true, line: 5 } ], ...overrides }); runTest('parseSearchQuery marks empty query as empty', () => { const parsed = parseSearchQuery(''); assert(parsed.isEmpty, 'Empty query should be flagged as empty'); assertEqual(parsed.ast.type, 'group', 'Empty query AST should be an empty group'); }); runTest('parseSearchQuery handles basic text queries', () => { const parsed = parseSearchQuery('hello world'); assert(!parsed.isEmpty, 'Non-empty query should not be marked empty'); assertEqual(parsed.ast.type, 'group', 'Text query AST should be a group'); }); runTest('parseSearchQuery handles operators', () => { const parsed = parseSearchQuery('path:notes/ tag:#tag file:example'); assert(!parsed.isEmpty, 'Operator query should parse successfully'); }); runTest('queryToPredicate matches simple text', () => { const predicate = queryToPredicate(parseSearchQuery('hello')); assert(predicate(createContext()), 'Predicate should match when text is present'); }); runTest('queryToPredicate respects negation', () => { const predicate = queryToPredicate(parseSearchQuery('-deprecated')); assert(predicate(createContext()), 'Predicate should pass when term is absent'); assert(!predicate(createContext({ content: 'deprecated feature' })), 'Predicate should fail when negated term exists'); }); runTest('queryToPredicate matches task scopes', () => { const todoPredicate = queryToPredicate(parseSearchQuery('task-todo:Call')); const donePredicate = queryToPredicate(parseSearchQuery('task-done:Review')); assert(todoPredicate(createContext()), 'task-todo should match incomplete tasks'); assert(donePredicate(createContext()), 'task-done should match completed tasks'); }); runTest('detectQueryType recognises path prefix', () => { const info = detectQueryType('path:notes'); assertEqual(info.type, 'path', 'Should recognise path prefix'); assertEqual(info.value, 'notes', 'Should capture path value'); }); runTest('detectQueryType recognises property bracket', () => { const info = detectQueryType('[status:'); assertEqual(info.type, 'property', 'Should recognise property prefix'); }); runTest('detectQueryType falls back to general type', () => { const info = detectQueryType('plain text'); assertEqual(info.type, 'general', 'General text should return general type'); }); export { testResults };