@@ -26,6 +26,24 @@ function parseOutput (output: string): { candidates: string[]; directive: number
2626 }
2727}
2828
29+
30+ async function withConfig ( yamlLines : string [ ] , fn : ( ) => Promise < void > ) {
31+ const { mkdtemp, writeFile, rm } = await import ( 'node:fs/promises' )
32+ const { tmpdir } = await import ( 'node:os' )
33+ const { join } = await import ( 'node:path' )
34+ const dir = await mkdtemp ( join ( tmpdir ( ) , 'elastic-cli-test-' ) )
35+ const path = join ( dir , 'config.yml' )
36+ await writeFile ( path , yamlLines . join ( '\n' ) )
37+ const originalEnv = process . env [ 'ELASTIC_CLI_CONFIG_FILE' ]
38+ process . env [ 'ELASTIC_CLI_CONFIG_FILE' ] = path
39+ try {
40+ await fn ( )
41+ } finally {
42+ if ( originalEnv != null ) process . env [ 'ELASTIC_CLI_CONFIG_FILE' ] = originalEnv
43+ else delete process . env [ 'ELASTIC_CLI_CONFIG_FILE' ]
44+ await rm ( dir , { recursive : true } )
45+ }
46+ }
2947describe ( 'buildCompletionTree -- top-level commands' , ( ) => {
3048 it ( 'registers version + every visible top-level group as stubs by default' , async ( ) => {
3149 const root = await buildCompletionTree ( [ ] )
@@ -101,20 +119,9 @@ describe('buildCompletionTree -- lazy loading', () => {
101119} )
102120
103121describe ( 'handleComplete -- policy enforcement' , ( ) => {
104- const ORIGINAL_ENV = process . env [ 'ELASTIC_CLI_CONFIG_FILE' ]
105- beforeEach ( ( ) => { delete process . env [ 'ELASTIC_CLI_CONFIG_FILE' ] } )
106- afterEach ( ( ) => {
107- if ( ORIGINAL_ENV != null ) process . env [ 'ELASTIC_CLI_CONFIG_FILE' ] = ORIGINAL_ENV
108- else delete process . env [ 'ELASTIC_CLI_CONFIG_FILE' ]
109- } )
110122
111123 it ( 'hides top-level groups blocked by commands.blocked' , async ( ) => {
112- const { mkdtemp, writeFile, rm } = await import ( 'node:fs/promises' )
113- const { tmpdir } = await import ( 'node:os' )
114- const { join } = await import ( 'node:path' )
115- const dir = await mkdtemp ( join ( tmpdir ( ) , 'elastic-cli-policy-' ) )
116- const path = join ( dir , 'config.yml' )
117- await writeFile ( path , [
124+ await withConfig ( [
118125 'current_context: local' ,
119126 'commands:' ,
120127 ' blocked:' ,
@@ -125,10 +132,7 @@ describe('handleComplete -- policy enforcement', () => {
125132 ' elasticsearch:' ,
126133 ' url: http://localhost:9200' ,
127134 '' ,
128- ] . join ( '\n' ) )
129- process . env [ 'ELASTIC_CLI_CONFIG_FILE' ] = path
130-
131- try {
135+ ] , async ( ) => {
132136 const buf = bufferedWriter ( )
133137 await handleComplete ( [ '' ] , buf . write )
134138
@@ -139,18 +143,11 @@ describe('handleComplete -- policy enforcement', () => {
139143 // Groups not blocked should still appear.
140144 assert . ok ( out . candidates . includes ( 'stack' ) )
141145 assert . ok ( out . candidates . includes ( 'version' ) )
142- } finally {
143- await rm ( dir , { recursive : true } )
144- }
146+ } )
145147 } )
146148
147149 it ( 'applies blocked commands without resolving active-context expressions' , async ( ) => {
148- const { mkdtemp, writeFile, rm } = await import ( 'node:fs/promises' )
149- const { tmpdir } = await import ( 'node:os' )
150- const { join } = await import ( 'node:path' )
151- const dir = await mkdtemp ( join ( tmpdir ( ) , 'elastic-cli-policy-' ) )
152- const path = join ( dir , 'config.yml' )
153- await writeFile ( path , [
150+ await withConfig ( [
154151 'current_context: local' ,
155152 'commands:' ,
156153 ' blocked:' ,
@@ -160,21 +157,17 @@ describe('handleComplete -- policy enforcement', () => {
160157 ' elasticsearch:' ,
161158 ' url: $(env:ELASTIC_COMPLETION_MISSING_URL)' ,
162159 '' ,
163- ] . join ( '\n' ) )
164- process . env [ 'ELASTIC_CLI_CONFIG_FILE' ] = path
165- delete process . env [ 'ELASTIC_COMPLETION_MISSING_URL' ]
160+ ] , async ( ) => {
161+ delete process . env [ 'ELASTIC_COMPLETION_MISSING_URL' ]
166162
167- try {
168163 const buf = bufferedWriter ( )
169164 await handleComplete ( [ '' ] , buf . write )
170165
171166 const out = parseOutput ( buf . chunks . join ( '' ) )
172167 assert . ok ( ! out . candidates . includes ( 'sanitize' ) ,
173168 `sanitize should be hidden by policy even with unresolved expressions; got: ${ out . candidates . join ( ',' ) } ` )
174169 assert . ok ( out . candidates . includes ( 'stack' ) )
175- } finally {
176- await rm ( dir , { recursive : true } )
177- }
170+ } )
178171 } )
179172} )
180173
@@ -222,20 +215,9 @@ describe('handleComplete -- stdout protocol', () => {
222215} )
223216
224217describe ( 'handleComplete -- dynamic context name completion' , ( ) => {
225- const ORIGINAL_ENV = process . env [ 'ELASTIC_CLI_CONFIG_FILE' ]
226- beforeEach ( ( ) => { delete process . env [ 'ELASTIC_CLI_CONFIG_FILE' ] } )
227- afterEach ( ( ) => {
228- if ( ORIGINAL_ENV != null ) process . env [ 'ELASTIC_CLI_CONFIG_FILE' ] = ORIGINAL_ENV
229- else delete process . env [ 'ELASTIC_CLI_CONFIG_FILE' ]
230- } )
231218
232219 it ( 'emits context names from the configured file' , async ( ) => {
233- const { mkdtemp, writeFile, rm } = await import ( 'node:fs/promises' )
234- const { tmpdir } = await import ( 'node:os' )
235- const { join } = await import ( 'node:path' )
236- const dir = await mkdtemp ( join ( tmpdir ( ) , 'elastic-cli-cnames-' ) )
237- const path = join ( dir , 'config.yml' )
238- await writeFile ( path , [
220+ await withConfig ( [
239221 'current_context: local' ,
240222 'contexts:' ,
241223 ' local:' ,
@@ -245,15 +227,13 @@ describe('handleComplete -- dynamic context name completion', () => {
245227 ' elasticsearch:' ,
246228 ' url: http://localhost:9200' ,
247229 '' ,
248- ] . join ( '\n' ) )
249- process . env [ 'ELASTIC_CLI_CONFIG_FILE' ] = path
250-
251- const buf = bufferedWriter ( )
252- await handleComplete ( [ '--use-context' , '' ] , buf . write )
253- await rm ( dir , { recursive : true } )
230+ ] , async ( ) => {
231+ const buf = bufferedWriter ( )
232+ await handleComplete ( [ '--use-context' , '' ] , buf . write )
254233
255- const out = parseOutput ( buf . chunks . join ( '' ) )
256- assert . deepEqual ( out . candidates . sort ( ) , [ 'local' , 'staging' ] )
234+ const out = parseOutput ( buf . chunks . join ( '' ) )
235+ assert . deepEqual ( out . candidates . sort ( ) , [ 'local' , 'staging' ] )
236+ } )
257237 } )
258238} )
259239
@@ -308,3 +288,116 @@ describe('buildCompleteCommand', () => {
308288 assert . match ( captured . join ( '' ) , / : \d + / )
309289 } )
310290} )
291+
292+ describe ( 'buildCompletionTree -- kb lazy loading' , ( ) => {
293+ // kb lazy loading exercises the KB_ALIASES branch without importing kb modules that
294+ // would bring uncovered kb/*.ts functions into the coverage scope.
295+ it ( 'shows kb as a stub when secondWord does not match kb aliases' , async ( ) => {
296+ const root = await buildCompletionTree ( [ 'stack' , 'something-else' ] )
297+ const stack = root . commands . find ( ( c ) => c . name ( ) === 'stack' ) !
298+ const kb = stack . commands . find ( ( c ) => c . name ( ) === 'kb' ) !
299+ assert . ok ( kb != null , 'kb should still be present as a stub' )
300+ } )
301+ } )
302+
303+ describe ( 'handleComplete -- loadCompletionCommandPolicy edge cases' , ( ) => {
304+
305+ it ( 'returns all top-level groups when current_context is missing from contexts' , async ( ) => {
306+ await withConfig ( [
307+ 'current_context: nonexistent' ,
308+ 'contexts:' ,
309+ ' local:' ,
310+ ' elasticsearch:' ,
311+ ' url: http://localhost:9200' ,
312+ '' ,
313+ ] , async ( ) => {
314+ const buf = bufferedWriter ( )
315+ await handleComplete ( [ '' ] , buf . write )
316+ const out = parseOutput ( buf . chunks . join ( '' ) )
317+ // policy returns undefined → all commands visible
318+ assert . ok ( out . candidates . includes ( 'stack' ) )
319+ } )
320+ } )
321+
322+ it ( 'returns all top-level groups when default_profile is invalid' , async ( ) => {
323+ await withConfig ( [
324+ 'current_context: local' ,
325+ 'default_profile: 123' ,
326+ 'contexts:' ,
327+ ' local:' ,
328+ ' elasticsearch:' ,
329+ ' url: http://localhost:9200' ,
330+ '' ,
331+ ] , async ( ) => {
332+ const buf = bufferedWriter ( )
333+ await handleComplete ( [ '' ] , buf . write )
334+ const out = parseOutput ( buf . chunks . join ( '' ) )
335+ assert . ok ( out . candidates . includes ( 'stack' ) )
336+ } )
337+ } )
338+
339+ it ( 'returns all top-level groups when root commands block field is invalid' , async ( ) => {
340+ await withConfig ( [
341+ 'current_context: local' ,
342+ 'commands:' ,
343+ ' blocked: not-an-array' ,
344+ 'contexts:' ,
345+ ' local:' ,
346+ ' elasticsearch:' ,
347+ ' url: http://localhost:9200' ,
348+ '' ,
349+ ] , async ( ) => {
350+ const buf = bufferedWriter ( )
351+ await handleComplete ( [ '' ] , buf . write )
352+ const out = parseOutput ( buf . chunks . join ( '' ) )
353+ assert . ok ( out . candidates . includes ( 'stack' ) )
354+ } )
355+ } )
356+
357+ it ( 'returns all top-level groups when context commands block field is invalid' , async ( ) => {
358+ await withConfig ( [
359+ 'current_context: local' ,
360+ 'contexts:' ,
361+ ' local:' ,
362+ ' commands:' ,
363+ ' blocked: not-an-array' ,
364+ ' elasticsearch:' ,
365+ ' url: http://localhost:9200' ,
366+ '' ,
367+ ] , async ( ) => {
368+ const buf = bufferedWriter ( )
369+ await handleComplete ( [ '' ] , buf . write )
370+ const out = parseOutput ( buf . chunks . join ( '' ) )
371+ assert . ok ( out . candidates . includes ( 'stack' ) )
372+ } )
373+ } )
374+ } )
375+
376+ describe ( 'buildCompletionTree -- docs and config subtrees' , ( ) => {
377+ it ( 'registers docs as a stub by default' , async ( ) => {
378+ const root = await buildCompletionTree ( [ 'stack' ] )
379+ const docs = root . commands . find ( ( c ) => c . name ( ) === 'docs' )
380+ assert . ok ( docs != null , 'docs group should be present' )
381+ } )
382+
383+ it ( 'registers config as a stub by default' , async ( ) => {
384+ const root = await buildCompletionTree ( [ 'stack' ] )
385+ const config = root . commands . find ( ( c ) => c . name ( ) === 'config' )
386+ assert . ok ( config != null , 'config group should be present' )
387+ } )
388+
389+ it ( 'deep-loads docs when first word is "docs"' , async ( ) => {
390+ const root = await buildCompletionTree ( [ 'docs' ] )
391+ const docs = root . commands . find ( ( c ) => c . name ( ) === 'docs' )
392+ assert . ok ( docs != null , 'docs should be present' )
393+ // deep-loaded docs should have child commands
394+ assert . ok ( docs . commands . length > 0 , 'docs should have children when deep-loaded' )
395+ } )
396+
397+ it ( 'deep-loads config when first word is "config"' , async ( ) => {
398+ const root = await buildCompletionTree ( [ 'config' ] )
399+ const config = root . commands . find ( ( c ) => c . name ( ) === 'config' )
400+ assert . ok ( config != null , 'config should be present' )
401+ assert . ok ( config . commands . length > 0 , 'config should have children when deep-loaded' )
402+ } )
403+ } )
0 commit comments