import { tokenize, nonWhitespace } from '../parser/lexer'; import { parse } from '../parser/parser'; import { locate } from '../parser/locate'; import type { Situation } from '../parser/types'; // ─── Helper ─────────────────────────────────────────────────────── function situationAt(input: string, offset: number): Situation { const rawTokens = tokenize(input); const tokens = nonWhitespace(rawTokens); const ast = parse(input); return locate(offset, tokens); } describe('Loki locate', () => { // ── IN_SELECTOR_LABEL ── it('returns IN_SELECTOR_LABEL for empty {}', () => { const s = situationAt('{}', 1); expect(s.kind).toBe('IN_SELECTOR_LABEL'); }); it('returns IN_SELECTOR_LABEL after opening brace', () => { const s = situationAt('{', 1); expect(s.kind).toBe('IN_SELECTOR_LABEL'); }); it('returns IN_SELECTOR_LABEL after comma in multi-matcher', () => { const s = situationAt('{job="nginx", }', 13); expect(s.kind).toBe('IN_SELECTOR_LABEL'); }); it('returns IN_SELECTOR_LABEL when typing label name', () => { const s = situationAt('{j', 2); expect(s.kind).toBe('IN_SELECTOR_LABEL'); }); // ── IN_SELECTOR_VALUE ── it('returns IN_SELECTOR_VALUE after operator', () => { const s = situationAt('{job=', 5); expect(s.kind).toBe('IN_SELECTOR_VALUE'); expect(s.labelName).toBe('job'); }); it('returns IN_SELECTOR_VALUE after != operator', () => { const s = situationAt('{job!=', 6); expect(s.kind).toBe('IN_SELECTOR_VALUE'); expect(s.labelName).toBe('job'); }); it('returns IN_SELECTOR_VALUE inside string value', () => { const s = situationAt('{job="', 6); expect(s.kind).toBe('IN_SELECTOR_VALUE'); expect(s.labelName).toBe('job'); expect(s.insideString).toBe(true); }); it('returns IN_SELECTOR_VALUE between quotes', () => { const s = situationAt('{job=""}', 6); expect(s.kind).toBe('IN_SELECTOR_VALUE'); expect(s.labelName).toBe('job'); expect(s.insideString).toBe(true); }); it('returns IN_SELECTOR_VALUE for =~ operator', () => { const s = situationAt('{job=~', 6); expect(s.kind).toBe('IN_SELECTOR_VALUE'); expect(s.labelName).toBe('job'); }); // ── AFTER_PIPE ── it('returns AFTER_PIPE after pipe', () => { const s = situationAt('{job="nginx"} | ', 16); expect(s.kind).toBe('AFTER_PIPE'); }); it('returns IN_SELECTOR_LABEL inside {}', () => { const s = situationAt('{}', 1); expect(s.kind).toBe('IN_SELECTOR_LABEL'); }); // ── IN_FUNCTION_PARENS ── it('returns IN_FUNCTION_PARENS inside rate()', () => { const s = situationAt('rate(', 5); expect(s.kind).toBe('IN_FUNCTION_PARENS'); }); it('returns IN_FUNCTION_PARENS inside count_over_time()', () => { const s = situationAt('count_over_time(', 16); expect(s.kind).toBe('IN_FUNCTION_PARENS'); }); it('returns IN_FUNCTION_PARENS at root level', () => { const s = situationAt('', 0); expect(s.kind).toBe('IN_FUNCTION_PARENS'); }); // ── NONE inside range brackets ── it('returns NONE inside [5m]', () => { const s = situationAt('{job="nginx"}[5m]', 15); expect(s.kind).not.toBe('IN_FUNCTION_PARENS'); }); it('returns NONE inside []', () => { const s = situationAt('rate({job="nginx"}[', 19); expect(s.kind).not.toBe('IN_FUNCTION_PARENS'); }); it('returns NONE typing range inside brackets', () => { const s = situationAt('rate({job="nginx"}[1m', 21); expect(s.kind).not.toBe('IN_FUNCTION_PARENS'); }); // ── matcherIndex ── it('sets matcherIndex for first matcher', () => { const s = situationAt('{job=', 5); expect(s.matcherIndex).toBe(0); }); it('sets matcherIndex for second matcher', () => { const s = situationAt('{job="nginx", app=', 18); expect(s.matcherIndex).toBe(1); }); it('sets matcherIndex inside string of second matcher', () => { const s = situationAt('{job="nginx", app="', 19); expect(s.matcherIndex).toBe(1); }); // ── Prefix ── it('uses full identifier as prefix at end of word', () => { const s = situationAt('count', 5); expect(s.prefix).toBe('count'); }); it('uses partial identifier as prefix', () => { const s = situationAt('cou', 3); expect(s.prefix).toBe('cou'); }); // ── RCurly boundary (cursor at `}`) ── it('returns IN_SELECTOR_LABEL at RCurly in empty selector { }', () => { // { } — cursor is at `}` but no matcher exists yet const s = situationAt('{ }', 2); expect(s.kind).toBe('IN_SELECTOR_LABEL'); }); it('returns IN_SELECTOR_LABEL at RCurly when label has no operator {job}', () => { // {job} — cursor is at `}`, label name without operator → still building selector const s = situationAt('{job}', 4); expect(s.kind).toBe('IN_SELECTOR_LABEL'); }); it('returns AFTER_PIPE at RCurly with complete matcher {job!="" }', () => { // {job!="" } — space before `}` so RCurly doesn't share String's boundary const s = situationAt('{job!="" }', 9); expect(s.kind).toBe('AFTER_PIPE'); }); it('returns IN_SELECTOR_LABEL at RCurly with incomplete second matcher', () => { // {job="nginx", app} — second matcher has no operator yet const s = situationAt('{job="nginx", app}', 17); expect(s.kind).toBe('IN_SELECTOR_LABEL'); }); it('returns AFTER_PIPE at RCurly with operator present {job!="nginx" }', () => { // {job!="nginx" } — space before `}` avoids boundary conflict; has operator → AFTER_PIPE const s = situationAt('{job!="nginx" }', 14); expect(s.kind).toBe('AFTER_PIPE'); }); it('returns IN_SELECTOR_LABEL at RCurly with only comma separators', () => { // {job, app} — only idents and commas, no operator → still building selector const s = situationAt('{job, app}', 9); expect(s.kind).toBe('IN_SELECTOR_LABEL'); }); // ── Label filter ── it('returns IN_LABEL_FILTER after label value comparison', () => { const s = situationAt('{job="nginx"} | status >', 24); expect(s.kind).toBe('IN_LABEL_FILTER'); }); });