import { tokenize } from '../parser/lexer'; import { TokenKind } from '../parser/types'; function kinds(input: string): TokenKind[] { return tokenize(input) .filter((t) => t.kind !== TokenKind.Whitespace && t.kind !== TokenKind.EOF) .map((t) => t.kind); } function values(input: string): string[] { return tokenize(input) .filter((t) => t.kind !== TokenKind.Whitespace && t.kind !== TokenKind.EOF) .map((t) => t.value); } describe('logql lexer', () => { it('emits a single EOF token for empty input', () => { const tokens = tokenize(''); expect(tokens).toHaveLength(1); expect(tokens[0].kind).toBe(TokenKind.EOF); }); it('recognises identifiers and the field-assign colon', () => { expect(kinds('level:ERROR')).toEqual([TokenKind.Ident, TokenKind.Operator, TokenKind.Ident]); expect(values('level:ERROR')).toEqual(['level', ':', 'ERROR']); }); it('normalises search keywords to upper case regardless of input casing', () => { const tokens = tokenize('a and b OR c Not d'); const keywords = tokens.filter((t) => t.kind === TokenKind.Keyword).map((t) => t.value); expect(keywords).toEqual(['AND', 'OR', 'NOT']); }); it('emits Pipe only when not nested inside parens / brackets / strings', () => { const tokens = tokenize('a | b'); const pipeIdx = tokens.findIndex((t) => t.kind === TokenKind.Pipe); expect(pipeIdx).toBeGreaterThan(-1); const nested = tokenize('a:(b|c) | d'); const pipes = nested.filter((t) => t.kind === TokenKind.Pipe); expect(pipes).toHaveLength(1); const inString = tokenize('a:"x|y" | sql'); const pipesInString = inString.filter((t) => t.kind === TokenKind.Pipe); expect(pipesInString).toHaveLength(1); }); it('treats `||` as the SQL boolean OR operator, not a top-level pipe', () => { const tokens = tokenize('a || b'); expect(tokens.filter((t) => t.kind === TokenKind.Pipe)).toHaveLength(0); const ops = tokens.filter((t) => t.kind === TokenKind.Operator).map((t) => t.value); expect(ops).toContain('||'); }); it('parses a closed `${name}` as a single Variable token with name extracted', () => { const tokens = tokenize('host:${env}'); const v = tokens.find((t) => t.kind === TokenKind.Variable); expect(v).toBeDefined(); expect(v!.name).toBe('env'); expect(v!.unterminated).toBeFalsy(); }); it('marks unclosed `${...` as an unterminated Variable spanning the partial name', () => { const tokens = tokenize('host:${ho'); const v = tokens.find((t) => t.kind === TokenKind.Variable); expect(v).toBeDefined(); expect(v!.unterminated).toBe(true); expect(v!.name).toBe('ho'); expect(v!.end).toBe(9); }); it('emits a lone `$` as Dollar so completion can pre-empt typing', () => { const tokens = tokenize('a:$'); const last = tokens[tokens.length - 2]; expect(last.kind).toBe(TokenKind.Dollar); }); it('captures unterminated strings with the unterminated flag', () => { const tokens = tokenize('"abc'); const s = tokens.find((t) => t.kind === TokenKind.String); expect(s).toBeDefined(); expect(s!.unterminated).toBe(true); expect(s!.quote).toBe('"'); }); it('reads multi-character comparison operators as a single token', () => { const tokens = tokenize('a >= 1 AND b <= 2 AND c != 3 AND d <> 4'); const ops = tokens.filter((t) => t.kind === TokenKind.Operator).map((t) => t.value); expect(ops).toEqual(['>=', '<=', '!=', '<>']); }); it('records accurate start/end offsets for every token', () => { const input = 'level:ERROR'; const tokens = tokenize(input).filter((t) => t.kind !== TokenKind.EOF); for (const t of tokens) { expect(input.slice(t.start, t.end)).toBe(t.value); } }); });