@@ -23,6 +23,7 @@ import {
2323 type LoadedSettings ,
2424} from '../../config/settings.js' ;
2525import { getErrorMessage } from '@google/gemini-cli-core' ;
26+ import { exitCli } from '../utils.js' ;
2627
2728// Mock dependencies
2829const emitConsoleLog = vi . hoisted ( ( ) => vi . fn ( ) ) ;
@@ -64,6 +65,7 @@ describe('extensions disable command', () => {
6465 const mockLoadSettings = vi . mocked ( loadSettings ) ;
6566 const mockGetErrorMessage = vi . mocked ( getErrorMessage ) ;
6667 const mockExtensionManager = vi . mocked ( ExtensionManager ) ;
68+ const mockExitCli = vi . mocked ( exitCli ) ;
6769
6870 beforeEach ( async ( ) => {
6971 vi . clearAllMocks ( ) ;
@@ -76,6 +78,7 @@ describe('extensions disable command', () => {
7678 mockExtensionManager . prototype . disableExtension = vi
7779 . fn ( )
7880 . mockResolvedValue ( undefined ) ;
81+ mockExtensionManager . prototype . getExtensions = vi . fn ( ) . mockReturnValue ( [ ] ) ;
7982 } ) ;
8083
8184 afterEach ( ( ) => {
@@ -85,31 +88,28 @@ describe('extensions disable command', () => {
8588 describe ( 'handleDisable' , ( ) => {
8689 it . each ( [
8790 {
88- name : 'my-extension' ,
8991 scope : undefined ,
9092 expectedScope : SettingScope . User ,
9193 expectedLog :
9294 'Extension "my-extension" successfully disabled for scope "undefined".' ,
9395 } ,
9496 {
95- name : 'my-extension' ,
9697 scope : 'user' ,
9798 expectedScope : SettingScope . User ,
9899 expectedLog :
99100 'Extension "my-extension" successfully disabled for scope "user".' ,
100101 } ,
101102 {
102- name : 'my-extension' ,
103103 scope : 'workspace' ,
104104 expectedScope : SettingScope . Workspace ,
105105 expectedLog :
106106 'Extension "my-extension" successfully disabled for scope "workspace".' ,
107107 } ,
108108 ] ) (
109109 'should disable an extension in the $expectedScope scope when scope is $scope' ,
110- async ( { name , scope, expectedScope, expectedLog } ) => {
110+ async ( { scope, expectedScope, expectedLog } ) => {
111111 const mockCwd = vi . spyOn ( process , 'cwd' ) . mockReturnValue ( '/test/dir' ) ;
112- await handleDisable ( { name , scope } ) ;
112+ await handleDisable ( { names : [ 'my-extension' ] , scope } ) ;
113113 expect ( mockExtensionManager ) . toHaveBeenCalledWith (
114114 expect . objectContaining ( {
115115 workspaceDir : '/test/dir' ,
@@ -120,39 +120,95 @@ describe('extensions disable command', () => {
120120 ) . toHaveBeenCalled ( ) ;
121121 expect (
122122 mockExtensionManager . prototype . disableExtension ,
123- ) . toHaveBeenCalledWith ( name , expectedScope ) ;
123+ ) . toHaveBeenCalledWith ( 'my-extension' , expectedScope ) ;
124124 expect ( emitConsoleLog ) . toHaveBeenCalledWith ( 'log' , expectedLog ) ;
125125 mockCwd . mockRestore ( ) ;
126126 } ,
127127 ) ;
128128
129- it ( 'should log an error message and exit with code 1 when extension disabling fails' , async ( ) => {
130- const mockProcessExit = vi
131- . spyOn ( process , 'exit' )
132- . mockImplementation ( ( ( ) => { } ) as (
133- code ?: string | number | null | undefined ,
134- ) => never ) ;
129+ it ( 'should disable multiple extensions when given a list of names' , async ( ) => {
130+ const mockCwd = vi . spyOn ( process , 'cwd' ) . mockReturnValue ( '/test/dir' ) ;
131+ await handleDisable ( { names : [ 'ext-a' , 'ext-b' ] } ) ;
132+
133+ expect (
134+ mockExtensionManager . prototype . disableExtension ,
135+ ) . toHaveBeenCalledWith ( 'ext-a' , SettingScope . User ) ;
136+ expect (
137+ mockExtensionManager . prototype . disableExtension ,
138+ ) . toHaveBeenCalledWith ( 'ext-b' , SettingScope . User ) ;
139+ mockCwd . mockRestore ( ) ;
140+ } ) ;
141+
142+ it ( 'should dedupe duplicate names' , async ( ) => {
143+ const mockCwd = vi . spyOn ( process , 'cwd' ) . mockReturnValue ( '/test/dir' ) ;
144+ await handleDisable ( { names : [ 'ext-a' , 'ext-a' ] } ) ;
145+
146+ expect (
147+ mockExtensionManager . prototype . disableExtension ,
148+ ) . toHaveBeenCalledTimes ( 1 ) ;
149+ mockCwd . mockRestore ( ) ;
150+ } ) ;
151+
152+ it ( 'should disable every installed extension when --all is set' , async ( ) => {
153+ const mockCwd = vi . spyOn ( process , 'cwd' ) . mockReturnValue ( '/test/dir' ) ;
154+ mockExtensionManager . prototype . getExtensions = vi
155+ . fn ( )
156+ . mockReturnValue ( [ { name : 'ext-a' } , { name : 'ext-b' } ] ) ;
157+
158+ await handleDisable ( { all : true } ) ;
159+
160+ expect (
161+ mockExtensionManager . prototype . disableExtension ,
162+ ) . toHaveBeenCalledWith ( 'ext-a' , SettingScope . User ) ;
163+ expect (
164+ mockExtensionManager . prototype . disableExtension ,
165+ ) . toHaveBeenCalledWith ( 'ext-b' , SettingScope . User ) ;
166+ mockCwd . mockRestore ( ) ;
167+ } ) ;
168+
169+ it ( 'should log a message and return when --all is set with no installed extensions' , async ( ) => {
170+ const mockCwd = vi . spyOn ( process , 'cwd' ) . mockReturnValue ( '/test/dir' ) ;
171+ mockExtensionManager . prototype . getExtensions = vi
172+ . fn ( )
173+ . mockReturnValue ( [ ] ) ;
174+
175+ await handleDisable ( { all : true } ) ;
176+
177+ expect ( emitConsoleLog ) . toHaveBeenCalledWith (
178+ 'log' ,
179+ 'No extensions currently installed.' ,
180+ ) ;
181+ expect (
182+ mockExtensionManager . prototype . disableExtension ,
183+ ) . not . toHaveBeenCalled ( ) ;
184+ mockCwd . mockRestore ( ) ;
185+ } ) ;
186+
187+ it ( 'should log each error and call exitCli(1) when disabling fails' , async ( ) => {
188+ const mockCwd = vi . spyOn ( process , 'cwd' ) . mockReturnValue ( '/test/dir' ) ;
135189 const error = new Error ( 'Disable failed' ) ;
136190 (
137191 mockExtensionManager . prototype . disableExtension as Mock
138192 ) . mockRejectedValue ( error ) ;
139193 mockGetErrorMessage . mockReturnValue ( 'Disable failed message' ) ;
140- await handleDisable ( { name : 'my-extension' } ) ;
194+
195+ await handleDisable ( { names : [ 'my-extension' ] } ) ;
196+
141197 expect ( emitConsoleLog ) . toHaveBeenCalledWith (
142198 'error' ,
143- 'Disable failed message' ,
199+ 'Failed to disable "my-extension": Disable failed message' ,
144200 ) ;
145- expect ( mockProcessExit ) . toHaveBeenCalledWith ( 1 ) ;
146- mockProcessExit . mockRestore ( ) ;
201+ expect ( mockExitCli ) . toHaveBeenCalledWith ( 1 ) ;
202+ mockCwd . mockRestore ( ) ;
147203 } ) ;
148204 } ) ;
149205
150206 describe ( 'disableCommand' , ( ) => {
151207 const command = disableCommand ;
152208
153209 it ( 'should have correct command and describe' , ( ) => {
154- expect ( command . command ) . toBe ( 'disable [--scope] <name> ' ) ;
155- expect ( command . describe ) . toBe ( 'Disables an extension .' ) ;
210+ expect ( command . command ) . toBe ( 'disable [names..] ' ) ;
211+ expect ( command . describe ) . toBe ( 'Disables one or more extensions .' ) ;
156212 } ) ;
157213
158214 describe ( 'builder' , ( ) => {
@@ -176,10 +232,17 @@ describe('extensions disable command', () => {
176232 ( command . builder as ( yargs : Argv ) => Argv ) (
177233 yargsMock as unknown as Argv ,
178234 ) ;
179- expect ( yargsMock . positional ) . toHaveBeenCalledWith ( 'name' , {
180- describe : 'The name of the extension to disable.' ,
181- type : 'string' ,
182- } ) ;
235+ expect ( yargsMock . positional ) . toHaveBeenCalledWith (
236+ 'names' ,
237+ expect . objectContaining ( {
238+ type : 'string' ,
239+ array : true ,
240+ } ) ,
241+ ) ;
242+ expect ( yargsMock . option ) . toHaveBeenCalledWith (
243+ 'all' ,
244+ expect . objectContaining ( { type : 'boolean' } ) ,
245+ ) ;
183246 expect ( yargsMock . option ) . toHaveBeenCalledWith ( 'scope' , {
184247 describe : 'The scope to disable the extension in.' ,
185248 type : 'string' ,
@@ -188,6 +251,16 @@ describe('extensions disable command', () => {
188251 expect ( yargsMock . check ) . toHaveBeenCalled ( ) ;
189252 } ) ;
190253
254+ it ( 'check function should throw when neither names nor --all is provided' , ( ) => {
255+ ( command . builder as ( yargs : Argv ) => Argv ) (
256+ yargsMock as unknown as Argv ,
257+ ) ;
258+ const checkCallback = yargsMock . check . mock . calls [ 0 ] [ 0 ] ;
259+ expect ( ( ) => checkCallback ( { } ) ) . toThrow (
260+ / a t l e a s t o n e e x t e n s i o n n a m e t o d i s a b l e / ,
261+ ) ;
262+ } ) ;
263+
191264 it ( 'check function should throw for invalid scope' , ( ) => {
192265 ( command . builder as ( yargs : Argv ) => Argv ) (
193266 yargsMock as unknown as Argv ,
@@ -198,7 +271,7 @@ describe('extensions disable command', () => {
198271 )
199272 . map ( ( s ) => s . toLowerCase ( ) )
200273 . join ( ', ' ) } .`;
201- expect ( ( ) => checkCallback ( { scope : 'invalid' } ) ) . toThrow (
274+ expect ( ( ) => checkCallback ( { all : true , scope : 'invalid' } ) ) . toThrow (
202275 expectedError ,
203276 ) ;
204277 } ) ;
@@ -210,20 +283,22 @@ describe('extensions disable command', () => {
210283 yargsMock as unknown as Argv ,
211284 ) ;
212285 const checkCallback = yargsMock . check . mock . calls [ 0 ] [ 0 ] ;
213- expect ( checkCallback ( { scope } ) ) . toBe ( true ) ;
286+ expect ( checkCallback ( { all : true , scope } ) ) . toBe ( true ) ;
214287 } ,
215288 ) ;
216289 } ) ;
217290
218291 it ( 'handler should trigger extension disabling' , async ( ) => {
219292 const mockCwd = vi . spyOn ( process , 'cwd' ) . mockReturnValue ( '/test/dir' ) ;
220293 interface TestArgv {
221- name : string ;
294+ names : string [ ] ;
295+ all : boolean ;
222296 scope : string ;
223297 [ key : string ] : unknown ;
224298 }
225299 const argv : TestArgv = {
226- name : 'test-ext' ,
300+ names : [ 'test-ext' ] ,
301+ all : false ,
227302 scope : 'workspace' ,
228303 _ : [ ] ,
229304 $0 : '' ,
@@ -246,5 +321,36 @@ describe('extensions disable command', () => {
246321 ) ;
247322 mockCwd . mockRestore ( ) ;
248323 } ) ;
324+
325+ it ( 'handler should normalize a scalar string positional to a single-element array' , async ( ) => {
326+ // Regression: yargs may pass `names` as a bare string when only one
327+ // positional is provided. The handler must wrap it in an array, not let
328+ // it be iterated character-by-character.
329+ const mockCwd = vi . spyOn ( process , 'cwd' ) . mockReturnValue ( '/test/dir' ) ;
330+ interface TestArgv {
331+ names : string ;
332+ all : boolean ;
333+ scope : string ;
334+ [ key : string ] : unknown ;
335+ }
336+ const argv : TestArgv = {
337+ names : 'conductor' ,
338+ all : false ,
339+ scope : 'user' ,
340+ _ : [ ] ,
341+ $0 : '' ,
342+ } ;
343+ await ( command . handler as unknown as ( args : TestArgv ) => Promise < void > ) (
344+ argv ,
345+ ) ;
346+
347+ expect (
348+ mockExtensionManager . prototype . disableExtension ,
349+ ) . toHaveBeenCalledTimes ( 1 ) ;
350+ expect (
351+ mockExtensionManager . prototype . disableExtension ,
352+ ) . toHaveBeenCalledWith ( 'conductor' , SettingScope . User ) ;
353+ mockCwd . mockRestore ( ) ;
354+ } ) ;
249355 } ) ;
250356} ) ;
0 commit comments