import { parse } from '../parser/parser'; import type { BinaryNode, FilterNode, GroupNode, SearchExpr, UnaryNode } from '../parser/types'; function getSearch(input: string): SearchExpr | undefined { return parse(input).ast.search; } describe('logql parser - search segment', () => { it('returns an empty AST for empty input', () => { const { ast } = parse(''); expect(ast.search).toBeUndefined(); expect(ast.analyze).toBeUndefined(); expect(ast.pipe).toBeUndefined(); }); it('parses a single key:value pair as a Filter node', () => { const node = getSearch('level:ERROR') as FilterNode; expect(node.type).toBe('Filter'); expect(node.field?.value).toBe('level'); expect(node.op?.value).toBe(':'); expect(node.value).toBeDefined(); expect(node.value!.type).toBe('Literal'); }); it('builds a binary AND from explicit AND keyword', () => { const node = getSearch('a:1 AND b:2') as BinaryNode; expect(node.type).toBe('Binary'); expect(node.op).toBe('AND'); expect((node.left as FilterNode).field?.value).toBe('a'); expect((node.right as FilterNode).field?.value).toBe('b'); }); it('treats adjacent filters as implicit AND', () => { const node = getSearch('a:1 b:2') as BinaryNode; expect(node.type).toBe('Binary'); expect(node.op).toBe('AND'); }); it('parses OR with lower precedence than AND', () => { // a:1 OR b:2 AND c:3 ==> a:1 OR (b:2 AND c:3) const node = getSearch('a:1 OR b:2 AND c:3') as BinaryNode; expect(node.type).toBe('Binary'); expect(node.op).toBe('OR'); const right = node.right as BinaryNode; expect(right.type).toBe('Binary'); expect(right.op).toBe('AND'); }); it('parses NOT as a unary prefix', () => { const node = getSearch('NOT level:ERROR') as UnaryNode; expect(node.type).toBe('Unary'); expect(node.op).toBe('NOT'); expect((node.expr as FilterNode).type).toBe('Filter'); }); it('parses parenthesised groups', () => { const node = getSearch('(a:1 OR b:2) AND c:3') as BinaryNode; expect(node.type).toBe('Binary'); expect(node.op).toBe('AND'); const group = node.left as GroupNode; expect(group.type).toBe('Group'); expect(group.expr).toBeDefined(); expect((group.expr as BinaryNode).op).toBe('OR'); }); it('captures a Variable reference inside a filter value', () => { const node = getSearch('host:${env}') as FilterNode; expect(node.value?.type).toBe('Variable'); if (node.value && node.value.type === 'Variable') { expect(node.value.name).toBe('env'); } }); it('tolerates a missing value after the colon without throwing', () => { const node = getSearch('level:') as FilterNode; expect(node.type).toBe('Filter'); expect(node.field?.value).toBe('level'); expect(node.op?.value).toBe(':'); expect(node.value).toBeUndefined(); }); it('tolerates an unclosed parenthesis', () => { const node = getSearch('(a:1 AND b:2') as GroupNode; expect(node.type).toBe('Group'); expect(node.expr).toBeDefined(); }); }); describe('logql parser - analyze segment', () => { it('splits at the first top-level pipe', () => { const { ast } = parse('level:ERROR | SELECT count(*) FROM log'); expect(ast.pipe).toBeDefined(); expect(ast.search).toBeDefined(); expect(ast.analyze).toBeDefined(); expect(ast.analyze!.tokens.length).toBeGreaterThan(0); }); it('keeps a pipe inside parens with the search segment', () => { const { ast } = parse('a:(b|c)'); expect(ast.pipe).toBeUndefined(); expect(ast.analyze).toBeUndefined(); }); it('allows analyze-only queries (cursor before any text on the search side)', () => { const { ast } = parse('| SELECT 1'); expect(ast.pipe).toBeDefined(); expect(ast.search).toBeUndefined(); expect(ast.analyze).toBeDefined(); }); });