import { describe, expect, it } from 'vitest'; import { createBusinessCompletions, normalizeCodeEngine, resolveBusinessCompletionContext, shouldAutoAppendCallParens, shouldSkipBusinessCompletion, } from './codeCompletion'; describe('codeCompletion utils', () => { it('should normalize engine aliases', () => { expect(normalizeCodeEngine('py')).toBe('python'); expect(normalizeCodeEngine('python')).toBe('python'); expect(normalizeCodeEngine('js')).toBe('javascript'); expect(normalizeCodeEngine('javascript')).toBe('javascript'); expect(normalizeCodeEngine('')).toBe('javascript'); }); it('should create business completion list with stable priority and dedupe', () => { const completions = createBusinessCompletions('python', [ { name: 'input.text', resolved: true }, { name: 'input.text', resolved: false }, { name: 'question', resolved: false }, ]); expect(completions[0]).toMatchObject({ label: 'main', type: 'snippet', boost: 1000, }); expect(String(completions[0].apply)).toContain('def main(question):'); expect(completions.some((item) => item.label === '_result')).toBe(false); expect( completions.filter((item) => item.label === 'input.text').length, ).toBe(1); expect( completions.find((item) => item.label === 'input.text'), ).toMatchObject({ apply: '{{input.text}}', detail: '参数模板', }); expect(completions.find((item) => item.label === 'question')).toMatchObject( { apply: '{{question}}', detail: '参数模板(未映射)', }, ); expect( completions.some( (item) => item.type === 'snippet' && item.label === 'if-else', ), ).toBe(true); }); it('should prefer main return snippets for javascript output', () => { const completions = createBusinessCompletions('javascript', [ { name: 'data', resolved: true }, { name: 'question', resolved: true }, ]); const mainCompletion = completions.find((item) => item.label === 'main'); const resultCompletion = completions.find( (item) => item.label === 'result-object', ); expect(mainCompletion).toMatchObject({ type: 'snippet', detail: '代码节点入口', boost: 1000, }); expect(String(mainCompletion?.apply)).toContain( 'function main(data, question)', ); expect(String(mainCompletion?.apply)).toContain('return {'); expect(String(mainCompletion?.apply)).not.toContain('_result'); expect(String(resultCompletion?.apply)).toBe( "return { message: 'ok', data };", ); }); it('should prefer main return snippets for python output', () => { const completions = createBusinessCompletions('python', [ { name: 'data', resolved: true }, { name: 'question', resolved: true }, ]); const mainCompletion = completions.find((item) => item.label === 'main'); const resultCompletion = completions.find( (item) => item.label === 'result-object', ); expect(mainCompletion).toMatchObject({ type: 'snippet', detail: '代码节点入口', boost: 1000, }); expect(String(mainCompletion?.apply)).toContain( 'def main(data, question):', ); expect(String(mainCompletion?.apply)).toContain('return {'); expect(String(mainCompletion?.apply)).not.toContain('_result'); expect(String(resultCompletion?.apply)).toContain( 'def main(data, question):', ); expect(String(resultCompletion?.apply)).toContain( "return {'message': 'ok', 'data': data}", ); }); it('should not expose the legacy object-input contract in python snippets', () => { const completions = createBusinessCompletions('python', []); const snippets = completions.filter((item) => item.type === 'snippet'); expect(snippets).toHaveLength(4); for (const snippet of snippets) { expect(String(snippet.apply)).not.toContain('inputs'); expect(String(snippet.apply)).not.toContain('_result'); } }); it('should detect blocked nodes for comment/string/property contexts', () => { expect(shouldSkipBusinessCompletion('Comment')).toBe(true); expect(shouldSkipBusinessCompletion('LineComment')).toBe(true); expect(shouldSkipBusinessCompletion('String')).toBe(true); expect(shouldSkipBusinessCompletion('TemplateString')).toBe(true); expect(shouldSkipBusinessCompletion('PropertyName')).toBe(true); expect(shouldSkipBusinessCompletion('VariableName')).toBe(false); }); it('should resolve template context for parameter token completion', () => { const source = 'value = {{input.use'; const pos = source.length; const context = resolveBusinessCompletionContext(source, pos); expect(context).toBeTruthy(); expect(context?.from).toBe(8); expect(context?.validFor).toBeInstanceOf(RegExp); expect(context?.validFor.source).toBe('^[\\w.]*$'); }); it('should resolve word context and skip property access context', () => { const source = '_result.value'; expect(resolveBusinessCompletionContext(source, source.length)).toBeNull(); const wordSource = 'ret'; const context = resolveBusinessCompletionContext( wordSource, wordSource.length, ); expect(context).toBeTruthy(); expect(context?.from).toBe(0); expect(context?.validFor).toBeInstanceOf(RegExp); expect(context?.validFor.source).toBe('^[\\w$]*$'); }); it('should auto append call parens for function/method completions only', () => { expect( shouldAutoAppendCallParens({ label: 'print', type: 'function' }, ''), ).toBe(true); expect( shouldAutoAppendCallParens({ label: 'run', type: 'method' }, ''), ).toBe(true); expect( shouldAutoAppendCallParens({ label: 'count', type: 'variable' }, ''), ).toBe(false); expect( shouldAutoAppendCallParens({ label: 'print', type: 'function' }, '('), ).toBe(false); expect( shouldAutoAppendCallParens( { label: 'call', type: 'function', apply: 'call()' }, '', ), ).toBe(false); }); });