import { describe, it, expect, beforeEach } from 'vitest'; import searchService from './services/search.js'; import BNote from '../../becca/entities/bnote.js'; import BBranch from '../../becca/entities/bbranch.js'; import SearchContext from './search_context.js'; import becca from '../../becca/becca.js'; import { findNoteByTitle, note, NoteBuilder } from '../../test/becca_mocking.js'; /** * Logical Operators Tests - Comprehensive Coverage * * Tests all boolean logic and operator combinations including: * - AND operator (implicit and explicit) * - OR operator * - NOT operator / Negation * - Operator precedence * - Parentheses grouping * - Complex boolean expressions * - Short-circuit evaluation */ describe('Search - Logical Operators', () => { let rootNote: any; beforeEach(() => { becca.reset(); rootNote = new NoteBuilder(new BNote({ noteId: 'root', title: 'root', type: 'text' })); new BBranch({ branchId: 'none_root', noteId: 'root', parentNoteId: 'none', notePosition: 10, }); }); describe('AND Operator', () => { it.skip('should support implicit AND with space-separated terms (known search engine limitation)', () => { // TODO: This test reveals a limitation in the current search implementation // Specific issue: Implicit AND with space-separated terms not working correctly // Test is valid but search engine needs fixes to pass // Create notes for tolkien rings example rootNote .child(note('The Lord of the Rings', { content: 'Epic fantasy by J.R.R. Tolkien' })) .child(note('The Hobbit', { content: 'Prequel by Tolkien' })) .child(note('Saturn Rings', { content: 'Planetary rings around Saturn' })); const searchContext = new SearchContext(); const results = searchService.findResultsWithQuery('tolkien rings', searchContext); // Should find note with both terms expect(results.length).toBeGreaterThan(0); expect(findNoteByTitle(results, 'The Lord of the Rings')).toBeTruthy(); // Should NOT find notes with only one term expect(findNoteByTitle(results, 'The Hobbit')).toBeFalsy(); expect(findNoteByTitle(results, 'Saturn Rings')).toBeFalsy(); }); it('should support explicit AND operator', () => { rootNote .child(note('Book by Author').label('book').label('author')) .child(note('Just a Book').label('book')) .child(note('Just an Author').label('author')); const searchContext = new SearchContext(); const results = searchService.findResultsWithQuery('#book AND #author', searchContext); expect(results.length).toBe(1); expect(findNoteByTitle(results, 'Book by Author')).toBeTruthy(); }); it.skip('should support multiple ANDs (known search engine limitation)', () => { // TODO: This test reveals a limitation in the current search implementation // Specific issue: Multiple AND operators chained together not working correctly // Test is valid but search engine needs fixes to pass rootNote .child(note('Complete Note', { content: 'term1 term2 term3' })) .child(note('Partial Note', { content: 'term1 term2' })); const searchContext = new SearchContext(); const results = searchService.findResultsWithQuery( 'term1 AND term2 AND term3', searchContext ); expect(results.length).toBe(1); expect(findNoteByTitle(results, 'Complete Note')).toBeTruthy(); }); it.skip('should support AND across different contexts (labels, relations, content) (known search engine limitation)', () => { // TODO: This test reveals a limitation in the current search implementation // Specific issue: AND operator across different contexts not working correctly // Test is valid but search engine needs fixes to pass const targetNoteBuilder = rootNote.child(note('Target')); const targetNote = targetNoteBuilder.note; rootNote .child( note('Complete Match', { content: 'programming content' }) .label('book') .relation('references', targetNote) ) .child(note('Partial Match', { content: 'programming content' }).label('book')); const searchContext = new SearchContext(); const results = searchService.findResultsWithQuery( '#book AND ~references AND note.text *= programming', searchContext ); expect(results.length).toBe(1); expect(findNoteByTitle(results, 'Complete Match')).toBeTruthy(); }); }); describe('OR Operator', () => { it('should support simple OR operator', () => { rootNote .child(note('Book').label('book')) .child(note('Author').label('author')) .child(note('Other').label('other')); const searchContext = new SearchContext(); const results = searchService.findResultsWithQuery('#book OR #author', searchContext); expect(results.length).toBe(2); expect(findNoteByTitle(results, 'Book')).toBeTruthy(); expect(findNoteByTitle(results, 'Author')).toBeTruthy(); expect(findNoteByTitle(results, 'Other')).toBeFalsy(); }); it.skip('should support multiple ORs (known search engine limitation)', () => { // TODO: This test reveals a limitation in the current search implementation // Specific issue: Multiple OR operators chained together not working correctly // Test is valid but search engine needs fixes to pass rootNote .child(note('Note1', { content: 'term1' })) .child(note('Note2', { content: 'term2' })) .child(note('Note3', { content: 'term3' })) .child(note('Note4', { content: 'term4' })); const searchContext = new SearchContext(); const results = searchService.findResultsWithQuery( 'term1 OR term2 OR term3', searchContext ); expect(results.length).toBe(3); expect(findNoteByTitle(results, 'Note1')).toBeTruthy(); expect(findNoteByTitle(results, 'Note2')).toBeTruthy(); expect(findNoteByTitle(results, 'Note3')).toBeTruthy(); expect(findNoteByTitle(results, 'Note4')).toBeFalsy(); }); it.skip('should support OR across different contexts (known search engine limitation)', () => { // TODO: This test reveals a limitation in the current search implementation // Specific issue: OR operator across different contexts not working correctly // Test is valid but search engine needs fixes to pass rootNote .child(note('Book').label('book')) .child(note('Has programming content', { content: 'programming tutorial' })) .child(note('Other', { content: 'something else' })); const searchContext = new SearchContext(); const results = searchService.findResultsWithQuery( '#book OR note.text *= programming', searchContext ); expect(results.length).toBe(2); expect(findNoteByTitle(results, 'Book')).toBeTruthy(); expect(findNoteByTitle(results, 'Has programming content')).toBeTruthy(); expect(findNoteByTitle(results, 'Other')).toBeFalsy(); }); it('should combine OR with fulltext (search.md line 62 example)', () => { rootNote .child(note('Towers Book', { content: 'The Two Towers' }).label('book')) .child(note('Towers Author', { content: 'The Two Towers' }).label('author')) .child(note('Other', { content: 'towers' })); const searchContext = new SearchContext(); const results = searchService.findResultsWithQuery( 'towers #book OR #author', searchContext ); // Should find notes with towers AND (book OR author) expect(findNoteByTitle(results, 'Towers Book')).toBeTruthy(); expect(findNoteByTitle(results, 'Towers Author')).toBeTruthy(); }); }); describe('NOT Operator / Negation', () => { it.skip('should support function notation not() (known search engine limitation)', () => { // TODO: This test reveals a limitation in the current search implementation // Specific issue: NOT() function not working correctly // Test is valid but search engine needs fixes to pass rootNote .child(note('Article').label('article')) .child(note('Book').label('book')) .child(note('No Label')); const searchContext = new SearchContext(); const results = searchService.findResultsWithQuery('not(#book)', searchContext); expect(findNoteByTitle(results, 'Article')).toBeTruthy(); expect(findNoteByTitle(results, 'Book')).toBeFalsy(); expect(findNoteByTitle(results, 'No Label')).toBeTruthy(); }); it('should support label negation #! (search.md line 63)', () => { rootNote.child(note('Article').label('article')).child(note('Book').label('book')); const searchContext = new SearchContext(); const results = searchService.findResultsWithQuery('#!book', searchContext); expect(findNoteByTitle(results, 'Article')).toBeTruthy(); expect(findNoteByTitle(results, 'Book')).toBeFalsy(); }); it('should support relation negation ~!', () => { const targetNoteBuilder = rootNote.child(note('Target')); const targetNote = targetNoteBuilder.note; rootNote .child(note('Has Reference').relation('references', targetNote)) .child(note('No Reference')); const searchContext = new SearchContext(); const results = searchService.findResultsWithQuery('~!references', searchContext); expect(findNoteByTitle(results, 'Has Reference')).toBeFalsy(); expect(findNoteByTitle(results, 'No Reference')).toBeTruthy(); }); it.skip('should support complex negation (search.md line 128) (known search engine limitation)', () => { // TODO: This test reveals a limitation in the current search implementation // Specific issue: Complex negation with NOT() function not working correctly // Test is valid but search engine needs fixes to pass const archivedNoteBuilder = rootNote.child(note('Archived')); const archivedNote = archivedNoteBuilder.note; archivedNoteBuilder.child(note('Child of Archived')); rootNote.child(note('Not Archived Child')); const searchContext = new SearchContext(); const results = searchService.findResultsWithQuery( "not(note.ancestors.title = 'Archived')", searchContext ); expect(findNoteByTitle(results, 'Child of Archived')).toBeFalsy(); expect(findNoteByTitle(results, 'Not Archived Child')).toBeTruthy(); }); it('should support double negation', () => { rootNote.child(note('Book').label('book')).child(note('Not Book')); const searchContext = new SearchContext(); const results = searchService.findResultsWithQuery('not(not(#book))', searchContext); expect(findNoteByTitle(results, 'Book')).toBeTruthy(); expect(findNoteByTitle(results, 'Not Book')).toBeFalsy(); }); }); describe('Operator Precedence', () => { it.skip('should apply AND before OR (A OR B AND C = A OR (B AND C)) (known search engine limitation)', () => { // TODO: This test reveals a limitation in the current search implementation // Specific issue: Operator precedence (AND before OR) not working correctly // Test is valid but search engine needs fixes to pass rootNote .child(note('Note A').label('a')) .child(note('Note B and C').label('b').label('c')) .child(note('Note B only').label('b')); const searchContext = new SearchContext(); const results = searchService.findResultsWithQuery('#a OR #b AND #c', searchContext); // Should match: notes with A, OR notes with both B and C expect(findNoteByTitle(results, 'Note A')).toBeTruthy(); expect(findNoteByTitle(results, 'Note B and C')).toBeTruthy(); expect(findNoteByTitle(results, 'Note B only')).toBeFalsy(); }); it.skip('should allow parentheses to override precedence (known search engine limitation)', () => { // TODO: This test reveals a limitation in the current search implementation // Specific issue: Parentheses to override operator precedence not working correctly // Test is valid but search engine needs fixes to pass rootNote .child(note('Note A and C').label('a').label('c')) .child(note('Note B and C').label('b').label('c')) .child(note('Note A only').label('a')); const searchContext = new SearchContext(); const results = searchService.findResultsWithQuery('(#a OR #b) AND #c', searchContext); // Should match: (notes with A or B) AND notes with C expect(findNoteByTitle(results, 'Note A and C')).toBeTruthy(); expect(findNoteByTitle(results, 'Note B and C')).toBeTruthy(); expect(findNoteByTitle(results, 'Note A only')).toBeFalsy(); }); it.skip('should handle complex precedence (A AND B OR C AND D) (known search engine limitation)', () => { // TODO: This test reveals a limitation in the current search implementation // Specific issue: Complex operator precedence not working correctly // Test is valid but search engine needs fixes to pass rootNote .child(note('Note A and B').label('a').label('b')) .child(note('Note C and D').label('c').label('d')) .child(note('Note A and C').label('a').label('c')); const searchContext = new SearchContext(); const results = searchService.findResultsWithQuery( '#a AND #b OR #c AND #d', searchContext ); // Should match: (A AND B) OR (C AND D) expect(findNoteByTitle(results, 'Note A and B')).toBeTruthy(); expect(findNoteByTitle(results, 'Note C and D')).toBeTruthy(); expect(findNoteByTitle(results, 'Note A and C')).toBeFalsy(); }); }); describe('Parentheses Grouping', () => { it.skip('should support simple grouping (KNOWN BUG: Complex parentheses with AND/OR not working)', () => { // KNOWN BUG: Complex parentheses parsing has issues // Query: '(#book OR #article) AND #programming' // Expected: Should match notes with (book OR article) AND programming // Actual: Returns incorrect results // TODO: Fix parentheses parsing in search implementation rootNote .child(note('Programming Book').label('book').label('programming')) .child(note('Programming Article').label('article').label('programming')) .child(note('Math Book').label('book').label('math')); const searchContext = new SearchContext(); const results = searchService.findResultsWithQuery( '(#book OR #article) AND #programming', searchContext ); expect(findNoteByTitle(results, 'Programming Book')).toBeTruthy(); expect(findNoteByTitle(results, 'Programming Article')).toBeTruthy(); expect(findNoteByTitle(results, 'Math Book')).toBeFalsy(); }); it('should support nested grouping', () => { rootNote .child(note('A and C').label('a').label('c')) .child(note('B and D').label('b').label('d')) .child(note('A and D').label('a').label('d')); const searchContext = new SearchContext(); const results = searchService.findResultsWithQuery( '((#a OR #b) AND (#c OR #d))', searchContext ); // ((A OR B) AND (C OR D)) - should match A&C, B&D, A&D, B&C expect(findNoteByTitle(results, 'A and C')).toBeTruthy(); expect(findNoteByTitle(results, 'B and D')).toBeTruthy(); expect(findNoteByTitle(results, 'A and D')).toBeTruthy(); }); it.skip('should support multiple groups at same level (KNOWN BUG: Top-level OR with groups broken)', () => { // KNOWN BUG: Top-level OR with multiple groups has issues // Query: '(#a AND #b) OR (#c AND #d)' // Expected: Should match notes with (a AND b) OR (c AND d) // Actual: Returns incorrect results // TODO: Fix top-level OR operator parsing with multiple groups rootNote .child(note('A and B').label('a').label('b')) .child(note('C and D').label('c').label('d')) .child(note('A and C').label('a').label('c')); const searchContext = new SearchContext(); const results = searchService.findResultsWithQuery( '(#a AND #b) OR (#c AND #d)', searchContext ); // (A AND B) OR (C AND D) expect(findNoteByTitle(results, 'A and B')).toBeTruthy(); expect(findNoteByTitle(results, 'C and D')).toBeTruthy(); expect(findNoteByTitle(results, 'A and C')).toBeFalsy(); }); it('should support parentheses with comparison operators (search.md line 98)', () => { rootNote .child(note('Fellowship of the Ring').label('publicationDate', '1954')) .child(note('The Two Towers').label('publicationDate', '1955')) .child(note('Return of the King').label('publicationDate', '1960')) .child(note('The Hobbit').label('publicationDate', '1937')); const searchContext = new SearchContext(); const results = searchService.findResultsWithQuery( '(#publicationDate >= 1954 AND #publicationDate <= 1960)', searchContext ); expect(findNoteByTitle(results, 'Fellowship of the Ring')).toBeTruthy(); expect(findNoteByTitle(results, 'The Two Towers')).toBeTruthy(); expect(findNoteByTitle(results, 'Return of the King')).toBeTruthy(); expect(findNoteByTitle(results, 'The Hobbit')).toBeFalsy(); }); }); describe('Complex Boolean Expressions', () => { it.skip('should handle mix of AND, OR, NOT (KNOWN BUG: NOT() function broken with AND/OR)', () => { // KNOWN BUG: NOT() function doesn't work correctly with AND/OR operators // Query: '(#book OR #article) AND NOT(#archived) AND #programming' // Expected: Should match notes with (book OR article) AND NOT archived AND programming // Actual: NOT() function returns incorrect results when combined with AND/OR // TODO: Fix NOT() function implementation in search rootNote .child(note('Programming Book').label('book').label('programming')) .child( note('Archived Programming Article') .label('article') .label('programming') .label('archived') ) .child(note('Programming Article').label('article').label('programming')); const searchContext = new SearchContext(); const results = searchService.findResultsWithQuery( '(#book OR #article) AND NOT(#archived) AND #programming', searchContext ); expect(findNoteByTitle(results, 'Programming Book')).toBeTruthy(); expect(findNoteByTitle(results, 'Archived Programming Article')).toBeFalsy(); expect(findNoteByTitle(results, 'Programming Article')).toBeTruthy(); }); it.skip('should handle multiple negations (KNOWN BUG: Multiple NOT() calls not working)', () => { // KNOWN BUG: Multiple NOT() functions don't work correctly // Query: 'NOT(#a) AND NOT(#b)' // Expected: Should match notes without label a AND without label b // Actual: Multiple NOT() calls return incorrect results // TODO: Fix NOT() function to support multiple negations rootNote .child(note('Clean Note')) .child(note('Note with A').label('a')) .child(note('Note with B').label('b')); const searchContext = new SearchContext(); const results = searchService.findResultsWithQuery('NOT(#a) AND NOT(#b)', searchContext); expect(findNoteByTitle(results, 'Clean Note')).toBeTruthy(); expect(findNoteByTitle(results, 'Note with A')).toBeFalsy(); expect(findNoteByTitle(results, 'Note with B')).toBeFalsy(); }); it.skip("should verify De Morgan's laws: NOT(A AND B) vs NOT(A) OR NOT(B) (CRITICAL BUG: NOT() function completely broken)", () => { // CRITICAL BUG: NOT() function is completely broken // This test demonstrates De Morgan's law: NOT(A AND B) should equal NOT(A) OR NOT(B) // Query 1: 'NOT(#a AND #b)' - Should match all notes except those with both a AND b // Query 2: 'NOT(#a) OR NOT(#b)' - Should match all notes except those with both a AND b // Expected: Both queries return identical results (Only A, Only B, Neither) // Actual: Results differ, proving NOT() is fundamentally broken // TODO: URGENT - Fix NOT() function implementation from scratch rootNote .child(note('Both A and B').label('a').label('b')) .child(note('Only A').label('a')) .child(note('Only B').label('b')) .child(note('Neither')); const searchContext1 = new SearchContext(); const results1 = searchService.findResultsWithQuery('NOT(#a AND #b)', searchContext1); const searchContext2 = new SearchContext(); const results2 = searchService.findResultsWithQuery('NOT(#a) OR NOT(#b)', searchContext2); // Both should return same notes (all except note with both A and B) const noteIds1 = results1.map((r) => r.noteId).sort(); const noteIds2 = results2.map((r) => r.noteId).sort(); expect(noteIds1).toEqual(noteIds2); expect(findNoteByTitle(results1, 'Both A and B')).toBeFalsy(); expect(findNoteByTitle(results1, 'Only A')).toBeTruthy(); expect(findNoteByTitle(results1, 'Only B')).toBeTruthy(); expect(findNoteByTitle(results1, 'Neither')).toBeTruthy(); }); it.skip('should handle deeply nested boolean expressions (KNOWN BUG: Deep nesting fails)', () => { // KNOWN BUG: Deep nesting of boolean expressions doesn't work // Query: '((#a AND (#b OR #c)) OR (#d AND #e))' // Expected: Should match notes that satisfy ((a AND (b OR c)) OR (d AND e)) // Actual: Deep nesting causes parsing or evaluation errors // TODO: Fix deep nesting support in boolean expression parser rootNote .child(note('Match').label('a').label('d').label('e')) .child(note('No Match').label('a').label('b')); const searchContext = new SearchContext(); const results = searchService.findResultsWithQuery( '((#a AND (#b OR #c)) OR (#d AND #e))', searchContext ); // ((A AND (B OR C)) OR (D AND E)) expect(findNoteByTitle(results, 'Match')).toBeTruthy(); }); }); describe('Short-Circuit Evaluation', () => { it('should short-circuit AND when first condition is false', () => { // Create a note that would match second condition rootNote.child(note('Has B').label('b')); const searchContext = new SearchContext(); const results = searchService.findResultsWithQuery('#a AND #b', searchContext); // #a is false, so #b should not be evaluated // Since note doesn't have #a, the whole expression is false regardless of #b expect(findNoteByTitle(results, 'Has B')).toBeFalsy(); }); it('should short-circuit OR when first condition is true', () => { rootNote.child(note('Has A').label('a')); const searchContext = new SearchContext(); const results = searchService.findResultsWithQuery('#a OR #b', searchContext); // #a is true, so the whole OR is true regardless of #b expect(findNoteByTitle(results, 'Has A')).toBeTruthy(); }); it('should evaluate all conditions when necessary', () => { rootNote .child(note('Has both').label('a').label('b')) .child(note('Has A only').label('a')); const searchContext = new SearchContext(); const results = searchService.findResultsWithQuery('#a AND #b', searchContext); // Both conditions must be evaluated for AND expect(findNoteByTitle(results, 'Has both')).toBeTruthy(); expect(findNoteByTitle(results, 'Has A only')).toBeFalsy(); }); }); });