import { parse } from '../parser/parser'; import type { KQLNode, IsNode, RangeNode, AndNode, OrNode, NotNode, NestedNode, ValueListNode } from '../parser/types'; function as(node: KQLNode | null, type: T['type']): T { expect(node).not.toBeNull(); expect((node as KQLNode).type).toBe(type); return node as T; } describe('KQL parser', () => { // ── Empty / trivial ── it('parses empty input without errors', () => { const result = parse(''); expect(result.root).toBeNull(); expect(result.errors).toHaveLength(0); }); // ── Field:value ── it('parses field:value without spaces', () => { const root = as(parse('response:200').root, 'is'); expect(root.field?.value).toBe('response'); expect(root.value && root.value.type === 'literal' ? root.value.value : null).toBe('200'); expect(root.start).toBe(0); expect(root.end).toBe(12); }); it('parses field: value with spaces and trims the literal', () => { const root = as(parse('response: 200').root, 'is'); expect(root.value && root.value.type === 'literal' ? root.value.value : null).toBe('200'); }); it('parses quoted values and strips quotes/escapes', () => { const root = as(parse('machine.os: "win 10"').root, 'is'); const value = root.value; expect(value && value.type === 'literal' ? value.isQuoted : null).toBe(true); expect(value && value.type === 'literal' ? value.value : null).toBe('win 10'); }); it('parses bare values with spaces as one literal', () => { const root = as(parse('machine.os.keyword:Windows 10').root, 'is'); expect(root.field?.value).toBe('machine.os.keyword'); expect(root.value && root.value.type === 'literal' ? root.value.value : null).toBe('Windows 10'); }); it('marks wildcard literals', () => { const root = as(parse('message:*').root, 'is'); expect(root.value && root.value.type === 'literal' ? root.value.isWildcard : null).toBe(true); }); // ── Ranges ── it('parses range expressions', () => { const gte = as(parse('response >= 400').root, 'range'); expect(gte.operator).toBe('gte'); expect(gte.field.value).toBe('response'); expect(gte.value?.type === 'literal' ? gte.value.value : null).toBe('400'); const lte = as(parse('response <= 200').root, 'range'); expect(lte.operator).toBe('lte'); const lt = as(parse('response < 500').root, 'range'); expect(lt.operator).toBe('lt'); const gt = as(parse('response > 100').root, 'range'); expect(gt.operator).toBe('gt'); }); // ── Conjunctions ── it('parses and/or chains (case insensitive)', () => { const orNode = as(parse('response:200 OR response:500 or response:404').root, 'or'); expect(orNode.children).toHaveLength(3); expect(orNode.start).toBe(0); const andNode = as(parse('response:200 and extension:php AND tags:web').root, 'and'); expect(andNode.children).toHaveLength(3); }); it('parses not (case insensitive)', () => { const node = as(parse('NOT response:200').root, 'not'); expect(node.child?.type).toBe('is'); }); it('parses mixed precedence: or splits, and binds tighter', () => { const orNode = as(parse('a:1 and b:2 or c:3').root, 'or'); expect(orNode.children).toHaveLength(2); expect(orNode.children[0].type).toBe('and'); expect(orNode.children[1].type).toBe('is'); }); // ── Value lists ── it('parses field:(a or b)', () => { const root = as(parse('machine.os: (osx or ios or "win 10")').root, 'is'); const list = as(root.value, 'valueList'); const orNode = as(list.values[0], 'or'); expect(orNode.children).toHaveLength(3); }); it('parses field:(success and info and "ssh login")', () => { const root = as(parse('tags:(success and info and "ssh login")').root, 'is'); const list = as(root.value, 'valueList'); const andNode = as(list.values[0], 'and'); expect(andNode.children).toHaveLength(3); }); it('parses negations inside value lists', () => { const root = as(parse('tags:(success and not info)').root, 'is'); const list = as(root.value, 'valueList'); const andNode = as(list.values[0], 'and'); expect(andNode.children[1].type).toBe('not'); }); // ── Groups ── it('parses grouped sub queries', () => { const node = as(parse('not (response:500 or response:503)').root, 'not'); expect(node.child?.type).toBe('or'); }); // ── Nested queries ── it('parses nested queries with path:{ }', () => { const node = as(parse('user: { name: admin }').root, 'nested'); expect(node.field.value).toBe('user'); expect(node.query?.type).toBe('is'); expect(node.start).toBe(0); }); it('parses conjunctions inside nested queries', () => { const node = as(parse('user: { name: admin and tags: (admin or guest) }').root, 'nested'); expect(node.query?.type).toBe('and'); }); // ── Escapes & literals ── it('parses escaped special characters in literals', () => { const root = as(parse('field: a\\:b').root, 'is'); expect(root.value && root.value.type === 'literal' ? root.value.value : null).toBe('a:b'); }); it('parses escaped keywords as literals', () => { const root = as(parse('field: foo \\and bar').root, 'is'); expect(root.value && root.value.type === 'literal' ? root.value.value : null).toBe('foo and bar'); }); it('treats and/or without trailing whitespace as literal values (grammar quirk)', () => { const result = parse('a and'); expect(result.errors).toHaveLength(0); const root = as(result.root, 'is'); expect(root.field).toBeNull(); expect(root.value && root.value.type === 'literal' ? root.value.value : null).toBe('a and'); }); // ── Tolerance ── it('records an error and keeps a partial node for "foo:"', () => { const result = parse('foo:'); const root = as(result.root, 'is'); expect(root.field?.value).toBe('foo'); expect(root.value).toBeNull(); expect(result.errors).toHaveLength(1); expect(result.errors[0].start).toBeGreaterThanOrEqual(0); }); it('records an error for a dangling and', () => { const result = parse('a:1 and '); expect(result.errors).toHaveLength(1); expect(result.root?.type).toBe('is'); }); it('records an error for a dangling or', () => { // without the trailing space, "or" stays part of the literal (grammar-faithful) const result = parse('a:1 or '); expect(result.errors).toHaveLength(1); }); it('records an error for missing closing paren', () => { const result = parse('(response:200'); expect(result.errors).toHaveLength(1); expect(result.root?.type).toBe('is'); }); it('records an error for missing closing brace in nested query', () => { const result = parse('user: { name: admin'); expect(result.errors).toHaveLength(1); expect(result.root?.type).toBe('nested'); }); it('records an error for missing closing paren in value list', () => { const result = parse('machine.os: (osx or ios'); expect(result.errors).toHaveLength(1); expect(result.root?.type).toBe('is'); }); it('does not throw on garbage input', () => { expect(() => parse('((( : : "unclosed')).not.toThrow(); }); });