import { buildCompletions } from '../completion/completions'; import { getVendorConfig } from '../vendors'; import type { Situation } from '../parser/types'; const RANGE = { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 1 }; const SLS = getVendorConfig('sls'); function makeSituation(partial: Partial): Situation { return { section: 'search', kind: 'FIELD_NAME', prefix: '', replaceRange: { start: 0, end: 0 }, ...partial, }; } describe('buildCompletions', () => { it('returns nothing for the NONE situation', async () => { const items = await buildCompletions({ vendor: SLS, situation: makeSituation({ kind: 'NONE' }), range: RANGE, variables: ['host'], }); expect(items).toEqual([]); }); it('returns ONLY variable items when the cursor is inside a string', async () => { const items = await buildCompletions({ vendor: SLS, situation: makeSituation({ kind: 'IN_STRING' }), range: RANGE, variables: ['host', 'env'], }); expect(items).toHaveLength(2); expect(items.map((i) => i.label)).toEqual(['${host}', '${env}']); expect(items.every((i) => i.insertText.startsWith('${'))).toBe(true); }); it('returns ONLY variable items when in VARIABLE situation', async () => { const items = await buildCompletions({ vendor: SLS, situation: makeSituation({ kind: 'VARIABLE' }), range: RANGE, variables: ['host'], }); expect(items).toHaveLength(1); expect(items[0].label).toBe('${host}'); expect(items[0].insertText).toBe('${host}'); }); it('returns keywords, builtin fields, variables AND fetched field names for FIELD_NAME', async () => { const fetchFieldNames = jest.fn().mockResolvedValue(['level', 'service']); const items = await buildCompletions({ vendor: SLS, situation: makeSituation({ kind: 'FIELD_NAME', prefix: 'le' }), range: RANGE, variables: ['host'], fetchFieldNames, }); const labels = items.map((i) => i.label); expect(labels).toContain('AND'); // shared keyword expect(labels).toContain('OR'); expect(labels).toContain('NOT'); expect(labels).toContain('${host}'); // variable expect(labels).toContain('__topic__'); // SLS builtin expect(labels).toContain('level'); // fetched field expect(labels).toContain('service'); // fetched field expect(fetchFieldNames).toHaveBeenCalledTimes(1); expect(fetchFieldNames).toHaveBeenCalledWith( expect.objectContaining({ vendor: 'sls', section: 'search', prefix: 'le' }), ); }); it('places history completions ahead of all other suggestions', async () => { const items = await buildCompletions({ vendor: SLS, situation: makeSituation({ kind: 'FIELD_NAME' }), range: RANGE, historyRange: { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 4 }, history: [ { query: 'service:api', usageCount: 5, lastUsedAt: 2 }, { query: 'level:ERROR', usageCount: 2, lastUsedAt: 1 }, ], variables: ['env'], }); expect(items.slice(0, 2).map((item) => item.label)).toEqual([ { label: 'service:api', description: '历史记录' }, { label: 'level:ERROR', description: '历史记录' }, ]); expect(items[0].range).toEqual({ startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 4 }); }); it('localizes the history completion label', async () => { const items = await buildCompletions({ vendor: SLS, situation: makeSituation({ kind: 'FIELD_NAME' }), range: RANGE, variables: [], language: 'en_US', history: [{ query: 'service:api', usageCount: 1, lastUsedAt: 1 }], }); expect(items[0].label).toEqual({ label: 'service:api', description: 'History' }); }); it('uses the vendor field-assign operator when constructing field-name insert text', async () => { const slsItems = await buildCompletions({ vendor: SLS, situation: makeSituation({ kind: 'FIELD_NAME' }), range: RANGE, variables: [], fetchFieldNames: async () => ['service'], }); expect(slsItems.find((i) => i.label === 'service')!.insertText).toBe('service:'); // LTS allows `=` and lists it first in fieldAssignOperators const LTS = getVendorConfig('lts'); const ltsItems = await buildCompletions({ vendor: LTS, situation: makeSituation({ kind: 'FIELD_NAME' }), range: RANGE, variables: [], fetchFieldNames: async () => ['service'], }); expect(ltsItems.find((i) => i.label === 'service')!.insertText).toBe('service:'); }); it('passes the field name through to fetchFieldValues for FIELD_VALUE', async () => { const fetchFieldValues = jest.fn().mockResolvedValue(['ERROR', 'WARN']); const items = await buildCompletions({ vendor: SLS, situation: makeSituation({ kind: 'FIELD_VALUE', fieldName: 'level', prefix: 'E' }), range: RANGE, variables: [], fetchFieldValues, }); expect(fetchFieldValues).toHaveBeenCalledWith( 'level', expect.objectContaining({ vendor: 'sls', section: 'search', fieldName: 'level', prefix: 'E' }), ); expect(items.map((i) => i.label)).toEqual(expect.arrayContaining(['ERROR', 'WARN'])); }); it('quotes field values that contain whitespace or special characters', async () => { const items = await buildCompletions({ vendor: SLS, situation: makeSituation({ kind: 'FIELD_VALUE', fieldName: 'msg' }), range: RANGE, variables: [], fetchFieldValues: async () => ['simple', 'has space', 'with"quote'], }); const simple = items.find((i) => i.label === 'simple')!; const spaced = items.find((i) => i.label === 'has space')!; const quoted = items.find((i) => i.label === 'with"quote')!; expect(simple.insertText).toBe('simple'); expect(spaced.insertText).toBe('"has space"'); expect(quoted.insertText).toBe('"with\\"quote"'); }); it('includes SQL keywords and SQL functions for ANALYZE_KEYWORD', async () => { const items = await buildCompletions({ vendor: SLS, situation: makeSituation({ section: 'analyze', kind: 'ANALYZE_KEYWORD' }), range: RANGE, variables: [], }); const labels = items.map((i) => i.label); expect(labels).toContain('SELECT'); expect(labels).toContain('FROM'); expect(labels).toContain('GROUP'); expect(labels).toContain('COUNT'); // function expect(labels).toContain('APPROX_DISTINCT'); // SLS-specific function }); it('always surfaces variables across non-NONE situations', async () => { const kinds: Array = [ 'FIELD_NAME', 'FIELD_VALUE', 'IN_STRING', 'VARIABLE', 'ANALYZE_KEYWORD', 'ANALYZE_FIELD', 'ANALYZE_FUNCTION', ]; for (const kind of kinds) { const items = await buildCompletions({ vendor: SLS, situation: makeSituation({ kind, section: kind.startsWith('ANALYZE') ? 'analyze' : 'search', fieldName: 'foo' }), range: RANGE, variables: ['env'], fetchFieldNames: async () => [], fetchFieldValues: async () => [], }); expect(items.find((i) => i.label === '${env}')).toBeDefined(); } }); it('swallows errors thrown by fetch hooks and still returns other items', async () => { const items = await buildCompletions({ vendor: SLS, situation: makeSituation({ kind: 'FIELD_NAME', prefix: 'x' }), range: RANGE, variables: ['env'], fetchFieldNames: async () => { throw new Error('boom'); }, }); expect(items.find((i) => i.label === '${env}')).toBeDefined(); expect(items.find((i) => i.label === 'AND')).toBeDefined(); }); });