import { getLogQLCompletionProvider } from '../completion/getCompletionProvider'; import type { ProviderOptions } from '../completion/getCompletionProvider'; import * as monaco from 'monaco-editor'; // Mock internal modules jest.mock('../parser', () => ({ parse: jest.fn(), })); jest.mock('../parser/locate', () => ({ locate: jest.fn(), })); jest.mock('../completion/completions', () => ({ buildCompletions: jest.fn(), })); jest.mock('../history', () => ({ getHistoryCompletionItems: jest.fn(), })); import { parse } from '../parser'; import { locate } from '../parser/locate'; import { buildCompletions } from '../completion/completions'; import { getHistoryCompletionItems } from '../history'; const mockParse = parse as jest.Mock; const mockLocate = locate as jest.Mock; const mockBuildCompletions = buildCompletions as jest.Mock; const mockGetHistoryCompletionItems = getHistoryCompletionItems as jest.Mock; function createMockModel(id: string, value = ''): monaco.editor.ITextModel { return ({ id, getValue: () => value, getOffsetAt: jest.fn((pos) => pos.column - 1), getPositionAt: jest.fn((offset) => ({ lineNumber: 1, column: offset + 1 })), } as unknown) as monaco.editor.ITextModel; } function createMockEditor(model: monaco.editor.ITextModel | null) { return ({ getModel: () => model, } as unknown) as monaco.editor.IStandaloneCodeEditor; } function createMockPosition(column: number, lineNumber = 1): monaco.Position { return { column, lineNumber } as monaco.Position; } beforeEach(() => { jest.clearAllMocks(); mockParse.mockReturnValue({ ast: {}, tokens: [] }); mockLocate.mockReturnValue({ kind: 'NONE', prefix: '', replaceRange: { start: 0, end: 0 } }); mockBuildCompletions.mockResolvedValue([]); mockGetHistoryCompletionItems.mockReturnValue([]); }); describe('getLogQLCompletionProvider - model ownership check', () => { it('returns empty suggestions when model ID does NOT match the editor model', async () => { const editorModel = createMockModel('editor-model-1', '{job="nginx"}'); const otherModel = createMockModel('other-model-2', 'some other content'); const editorRef = { current: createMockEditor(editorModel) }; const opts: ProviderOptions = { vendor: {} as any, getVariables: () => [], getLanguage: () => 'zh_CN', getHistoryKey: () => undefined, getCanShowHistory: () => false, getFetchFieldNames: () => undefined, getFetchFieldValues: () => undefined, editorRef, }; const provider = getLogQLCompletionProvider(opts); const result = await provider.provideCompletionItems!(otherModel, createMockPosition(2)); expect(result).toEqual({ suggestions: [] }); expect(mockParse).not.toHaveBeenCalled(); expect(mockBuildCompletions).not.toHaveBeenCalled(); }); it('proceeds with completions when model ID matches the editor model', async () => { const model = createMockModel('same-model-1', '{job="nginx"}'); const editorRef = { current: createMockEditor(model) }; const opts: ProviderOptions = { vendor: {} as any, getVariables: () => [], getLanguage: () => 'zh_CN', getHistoryKey: () => undefined, getCanShowHistory: () => false, getFetchFieldNames: () => undefined, getFetchFieldValues: () => undefined, editorRef, }; mockBuildCompletions.mockResolvedValue([{ label: 'job', insertText: 'job' }]); const provider = getLogQLCompletionProvider(opts); const result = await provider.provideCompletionItems!(model, createMockPosition(2)); expect(result).not.toEqual({ suggestions: [] }); expect(mockParse).toHaveBeenCalled(); expect(mockBuildCompletions).toHaveBeenCalled(); }); it('includes history only while typing from an empty search input', async () => { const model = createMockModel('same-model-2', 'serv'); const editorRef = { current: createMockEditor(model) }; const opts: ProviderOptions = { vendor: {} as any, getVariables: () => [], getLanguage: () => 'zh_CN', getHistoryKey: () => 'logql-history', getCanShowHistory: () => true, getFetchFieldNames: () => undefined, getFetchFieldValues: () => undefined, editorRef, }; mockLocate.mockReturnValue({ section: 'search', kind: 'FIELD_NAME', prefix: 'serv', replaceRange: { start: 0, end: 4 }, }); mockGetHistoryCompletionItems.mockReturnValue([{ query: 'service:api', usageCount: 2, lastUsedAt: 1 }]); const provider = getLogQLCompletionProvider(opts); await provider.provideCompletionItems!(model, createMockPosition(5)); expect(mockGetHistoryCompletionItems).toHaveBeenCalledWith('logql-history'); expect(mockBuildCompletions).toHaveBeenCalledWith( expect.objectContaining({ history: [{ query: 'service:api', usageCount: 2, lastUsedAt: 1 }], historyRange: { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 5 }, }), ); }); it('does not request history outside the input-start context', async () => { const model = createMockModel('same-model-3', 'service:api AND '); const editorRef = { current: createMockEditor(model) }; const opts: ProviderOptions = { vendor: {} as any, getVariables: () => [], getLanguage: () => 'zh_CN', getHistoryKey: () => 'logql-history', getCanShowHistory: () => true, getFetchFieldNames: () => undefined, getFetchFieldValues: () => undefined, editorRef, }; mockLocate.mockReturnValue({ section: 'search', kind: 'FIELD_NAME', prefix: '', replaceRange: { start: 12, end: 12 }, }); const provider = getLogQLCompletionProvider(opts); await provider.provideCompletionItems!(model, createMockPosition(18)); expect(mockGetHistoryCompletionItems).not.toHaveBeenCalled(); expect(mockBuildCompletions).toHaveBeenCalledWith(expect.objectContaining({ history: [] })); }); it('returns empty suggestions when editorRef.current is null', async () => { const model = createMockModel('some-model', 'test'); const editorRef = { current: null }; const opts: ProviderOptions = { vendor: {} as any, getVariables: () => [], getLanguage: () => 'zh_CN', getHistoryKey: () => undefined, getCanShowHistory: () => false, getFetchFieldNames: () => undefined, getFetchFieldValues: () => undefined, editorRef, }; const provider = getLogQLCompletionProvider(opts); const result = await provider.provideCompletionItems!(model, createMockPosition(1)); expect(result).toEqual({ suggestions: [] }); expect(mockParse).not.toHaveBeenCalled(); }); it('returns empty suggestions when editor has no model', async () => { const model = createMockModel('some-model', 'test'); const editorRef = { current: createMockEditor(null) }; const opts: ProviderOptions = { vendor: {} as any, getVariables: () => [], getLanguage: () => 'zh_CN', getHistoryKey: () => undefined, getCanShowHistory: () => false, getFetchFieldNames: () => undefined, getFetchFieldValues: () => undefined, editorRef, }; const provider = getLogQLCompletionProvider(opts); const result = await provider.provideCompletionItems!(model, createMockPosition(1)); expect(result).toEqual({ suggestions: [] }); expect(mockParse).not.toHaveBeenCalled(); }); it('returns empty suggestions when editor model id is undefined', async () => { const editorModel = ({ id: undefined } as unknown) as monaco.editor.ITextModel; const model = createMockModel('some-model', 'test'); const editorRef = { current: createMockEditor(editorModel) }; const opts: ProviderOptions = { vendor: {} as any, getVariables: () => [], getLanguage: () => 'zh_CN', getHistoryKey: () => undefined, getCanShowHistory: () => false, getFetchFieldNames: () => undefined, getFetchFieldValues: () => undefined, editorRef, }; const provider = getLogQLCompletionProvider(opts); const result = await provider.provideCompletionItems!(model, createMockPosition(1)); expect(result).toEqual({ suggestions: [] }); }); });