basant307/AI_Governance_Project
048
1/**2 * @license3 * Copyright 2025 Google LLC4 * SPDX-License-Identifier: Apache-2.05 */6 7import type { Mock } from 'vitest';8import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';9import { getInstallationInfo, PackageManager } from './installationInfo.js';10import { updateEventEmitter } from './updateEventEmitter.js';11import type { UpdateObject } from '../ui/utils/updateCheck.js';12import type { LoadedSettings } from '../config/settings.js';13import EventEmitter from 'node:events';14import { handleAutoUpdate, setUpdateHandler } from './handleAutoUpdate.js';15import { performStandaloneUpdate } from './standalone-update.js';16import { MessageType } from '../ui/types.js';17 18vi.mock('./installationInfo.js', async () => {19 const actual = await vi.importActual('./installationInfo.js');20 return {21 ...actual,22 getInstallationInfo: vi.fn(),23 };24});25 26vi.mock('./standalone-update.js', () => ({27 performStandaloneUpdate: vi.fn(),28}));29 30vi.mock('./updateEventEmitter.js', async () => {31 const { EventEmitter } = await import('node:events');32 return {33 updateEventEmitter: new EventEmitter(),34 };35});36 37interface MockChildProcess extends EventEmitter {38 stdin: EventEmitter & {39 write: Mock;40 end: Mock;41 };42 stderr: EventEmitter;43}44 45const mockGetInstallationInfo = vi.mocked(getInstallationInfo);46const mockPerformStandaloneUpdate = vi.mocked(performStandaloneUpdate);47 48describe('handleAutoUpdate', () => {49 let mockSpawn: Mock;50 let mockUpdateInfo: UpdateObject;51 let mockSettings: LoadedSettings;52 let mockChildProcess: MockChildProcess;53 let emitSpy: ReturnType<typeof vi.spyOn>;54 55 beforeEach(() => {56 mockSpawn = vi.fn();57 vi.clearAllMocks();58 emitSpy = vi.spyOn(updateEventEmitter, 'emit');59 mockUpdateInfo = {60 update: {61 latest: '2.0.0',62 current: '1.0.0',63 type: 'major',64 name: '@qwen-code/qwen-code',65 },66 message: 'An update is available!',67 };68 69 mockSettings = {70 merged: {71 general: {72 enableAutoUpdate: true,73 },74 },75 } as LoadedSettings;76 77 mockChildProcess = Object.assign(new EventEmitter(), {78 stdin: Object.assign(new EventEmitter(), {79 write: vi.fn(),80 end: vi.fn(),81 }),82 stderr: new EventEmitter(),83 }) as MockChildProcess;84 85 mockSpawn.mockReturnValue(86 mockChildProcess as unknown as ReturnType<typeof mockSpawn>,87 );88 });89 90 afterEach(() => {91 vi.restoreAllMocks();92 });93 94 it('should do nothing if update info is null', () => {95 handleAutoUpdate(null, mockSettings, '/root', mockSpawn);96 expect(mockGetInstallationInfo).not.toHaveBeenCalled();97 expect(emitSpy).not.toHaveBeenCalled();98 expect(mockSpawn).not.toHaveBeenCalled();99 });100 101 it('should show manual update message when enableAutoUpdate is false', () => {102 // When enableAutoUpdate is false, gemini.tsx won't call checkForUpdates(),103 // but if handleAutoUpdate is still called, it should show a manual update message.104 mockSettings.merged.general!.enableAutoUpdate = false;105 mockGetInstallationInfo.mockReturnValue({106 updateCommand: 'npm i -g @qwen-code/qwen-code@latest',107 updateMessage:108 'Please run npm i -g @qwen-code/qwen-code@latest to update',109 isGlobal: true,110 packageManager: PackageManager.NPM,111 });112 113 handleAutoUpdate(mockUpdateInfo, mockSettings, '/root', mockSpawn);114 115 // Should still emit update-received with manual update message116 expect(emitSpy).toHaveBeenCalledWith('update-received', {117 message:118 'An update is available!\nPlease run npm i -g @qwen-code/qwen-code@latest to update',119 });120 // Should NOT spawn update when enableAutoUpdate is false121 expect(mockSpawn).not.toHaveBeenCalled();122 });123 124 it('should emit "update-received" but not update if no update command is found', () => {125 mockGetInstallationInfo.mockReturnValue({126 updateCommand: undefined,127 updateMessage: 'Cannot determine update command.',128 isGlobal: false,129 packageManager: PackageManager.NPM,130 });131 132 handleAutoUpdate(mockUpdateInfo, mockSettings, '/root', mockSpawn);133 134 expect(emitSpy).toHaveBeenCalledTimes(1);135 expect(emitSpy).toHaveBeenCalledWith('update-received', {136 message: 'An update is available!\nCannot determine update command.',137 });138 expect(mockSpawn).not.toHaveBeenCalled();139 });140 141 it('should combine update messages correctly', () => {142 mockGetInstallationInfo.mockReturnValue({143 updateCommand: undefined, // No command to prevent spawn144 updateMessage: 'This is an additional message.',145 isGlobal: false,146 packageManager: PackageManager.NPM,147 });148 149 handleAutoUpdate(mockUpdateInfo, mockSettings, '/root', mockSpawn);150 151 expect(emitSpy).toHaveBeenCalledTimes(1);152 expect(emitSpy).toHaveBeenCalledWith('update-received', {153 message: 'An update is available!\nThis is an additional message.',154 });155 });156 157 it('should attempt to perform an update when conditions are met', async () => {158 mockGetInstallationInfo.mockReturnValue({159 updateCommand: 'npm i -g @qwen-code/qwen-code@latest',160 updateMessage: 'This is an additional message.',161 isGlobal: false,162 packageManager: PackageManager.NPM,163 });164 165 // Simulate successful execution166 setTimeout(() => {167 mockChildProcess.emit('close', 0);168 }, 0);169 170 handleAutoUpdate(mockUpdateInfo, mockSettings, '/root', mockSpawn);171 172 expect(mockSpawn).toHaveBeenCalledOnce();173 });174 175 it('should emit "update-failed" when the update process fails', async () => {176 await new Promise<void>((resolve) => {177 mockGetInstallationInfo.mockReturnValue({178 updateCommand: 'npm i -g @qwen-code/qwen-code@latest',179 updateMessage: 'This is an additional message.',180 isGlobal: false,181 packageManager: PackageManager.NPM,182 });183 184 // Simulate failed execution185 setTimeout(() => {186 mockChildProcess.stderr.emit('data', 'An error occurred');187 mockChildProcess.emit('close', 1);188 resolve();189 }, 0);190 191 handleAutoUpdate(mockUpdateInfo, mockSettings, '/root', mockSpawn);192 });193 194 expect(emitSpy).toHaveBeenCalledWith('update-failed', {195 message:196 'Automatic update failed. Please try updating manually. (command: npm i -g @qwen-code/qwen-code@2.0.0, stderr: An error occurred)',197 });198 });199 200 it('should emit "update-failed" when the spawn function throws an error', async () => {201 await new Promise<void>((resolve) => {202 mockGetInstallationInfo.mockReturnValue({203 updateCommand: 'npm i -g @qwen-code/qwen-code@latest',204 updateMessage: 'This is an additional message.',205 isGlobal: false,206 packageManager: PackageManager.NPM,207 });208 209 // Simulate an error event210 setTimeout(() => {211 mockChildProcess.emit('error', new Error('Spawn error'));212 resolve();213 }, 0);214 215 handleAutoUpdate(mockUpdateInfo, mockSettings, '/root', mockSpawn);216 });217 218 expect(emitSpy).toHaveBeenCalledWith('update-failed', {219 message:220 'Automatic update failed. Please try updating manually. (error: Spawn error)',221 });222 });223 224 it('should use the "@nightly" tag for nightly updates', async () => {225 mockUpdateInfo.update.latest = '2.0.0-nightly';226 mockGetInstallationInfo.mockReturnValue({227 updateCommand: 'npm i -g @qwen-code/qwen-code@latest',228 updateMessage: 'This is an additional message.',229 isGlobal: false,230 packageManager: PackageManager.NPM,231 });232 233 handleAutoUpdate(mockUpdateInfo, mockSettings, '/root', mockSpawn);234 235 expect(mockSpawn).toHaveBeenCalledWith(236 expect.stringMatching(/^(bash|cmd\.exe)$/),237 expect.arrayContaining([238 expect.stringMatching(/^(-c|\/c)$/),239 'npm i -g @qwen-code/qwen-code@nightly',240 ]),241 {242 stdio: 'pipe',243 },244 );245 });246 247 it('should emit "update-success" when the update process succeeds', async () => {248 await new Promise<void>((resolve) => {249 mockGetInstallationInfo.mockReturnValue({250 updateCommand: 'npm i -g @qwen-code/qwen-code@latest',251 updateMessage: 'This is an additional message.',252 isGlobal: false,253 packageManager: PackageManager.NPM,254 });255 256 // Simulate successful execution257 setTimeout(() => {258 mockChildProcess.emit('close', 0);259 resolve();260 }, 0);261 262 handleAutoUpdate(mockUpdateInfo, mockSettings, '/root', mockSpawn);263 });264 265 expect(emitSpy).toHaveBeenCalledWith('update-success', {266 message:267 'Update successful! Please restart Qwen Code to use the new version. ' +268 'Switching model providers before restarting may not work correctly.',269 });270 });271});272 273describe('handleAutoUpdate — standalone path', () => {274 let mockSpawn: Mock;275 let mockUpdateInfo: UpdateObject;276 let mockSettings: LoadedSettings;277 let emitSpy: ReturnType<typeof vi.spyOn>;278 279 beforeEach(() => {280 mockSpawn = vi.fn();281 vi.clearAllMocks();282 emitSpy = vi.spyOn(updateEventEmitter, 'emit');283 mockUpdateInfo = {284 update: {285 latest: '2.0.0',286 current: '1.0.0',287 type: 'major',288 name: '@qwen-code/qwen-code',289 },290 message: 'An update is available!',291 };292 mockSettings = {293 merged: { general: { enableAutoUpdate: true } },294 } as LoadedSettings;295 });296 297 afterEach(() => {298 vi.restoreAllMocks();299 });300 301 it('calls performStandaloneUpdate and does NOT spawn npm', async () => {302 mockGetInstallationInfo.mockReturnValue({303 updateCommand: 'npm i -g @qwen-code/qwen-code@latest',304 updateMessage: '',305 isGlobal: false,306 isStandalone: true,307 standaloneDir: '/home/user/.local/lib/qwen-code',308 packageManager: PackageManager.NPM,309 });310 mockPerformStandaloneUpdate.mockResolvedValue('done');311 312 handleAutoUpdate(mockUpdateInfo, mockSettings, '/root', mockSpawn);313 await vi.waitFor(() =>314 expect(emitSpy).toHaveBeenCalledWith('update-success', expect.anything()),315 );316 317 expect(mockPerformStandaloneUpdate).toHaveBeenCalledWith(318 '/home/user/.local/lib/qwen-code',319 '2.0.0',320 );321 expect(mockSpawn).not.toHaveBeenCalled();322 });323 324 it('emits deferred message when result is "deferred"', async () => {325 mockGetInstallationInfo.mockReturnValue({326 updateCommand: undefined,327 updateMessage: '',328 isGlobal: false,329 isStandalone: true,330 standaloneDir: '/home/user/.local/lib/qwen-code',331 packageManager: PackageManager.NPM,332 });333 mockPerformStandaloneUpdate.mockResolvedValue('deferred');334 335 handleAutoUpdate(mockUpdateInfo, mockSettings, '/root', mockSpawn);336 await vi.waitFor(() =>337 expect(emitSpy).toHaveBeenCalledWith('update-success', expect.anything()),338 );339 340 expect(emitSpy).toHaveBeenCalledWith('update-success', {341 message:342 'Update downloaded. It will be applied after you exit this session.',343 });344 });345 346 it('emits "done" success message when result is "done"', async () => {347 mockGetInstallationInfo.mockReturnValue({348 updateCommand: undefined,349 updateMessage: '',350 isGlobal: false,351 isStandalone: true,352 standaloneDir: '/home/user/.local/lib/qwen-code',353 packageManager: PackageManager.NPM,354 });355 mockPerformStandaloneUpdate.mockResolvedValue('done');356 357 handleAutoUpdate(mockUpdateInfo, mockSettings, '/root', mockSpawn);358 await vi.waitFor(() =>359 expect(emitSpy).toHaveBeenCalledWith('update-success', expect.anything()),360 );361 362 expect(emitSpy).toHaveBeenCalledWith('update-success', {363 message:364 'Update successful! The new version will be used on your next run.',365 });366 });367 368 it('emits update-failed on rejection', async () => {369 mockGetInstallationInfo.mockReturnValue({370 updateCommand: undefined,371 updateMessage: '',372 isGlobal: false,373 isStandalone: true,374 standaloneDir: '/home/user/.local/lib/qwen-code',375 packageManager: PackageManager.NPM,376 });377 mockPerformStandaloneUpdate.mockRejectedValue(new Error('Download failed'));378 379 handleAutoUpdate(mockUpdateInfo, mockSettings, '/root', mockSpawn);380 await vi.waitFor(() =>381 expect(emitSpy).toHaveBeenCalledWith('update-failed', expect.anything()),382 );383 384 expect(emitSpy).toHaveBeenCalledWith('update-failed', {385 message:386 'Automatic update failed: Download failed. Re-run the installer to update manually.',387 });388 expect(mockSpawn).not.toHaveBeenCalled();389 });390});391 392describe('setUpdateHandler', () => {393 let addItem: Mock;394 let setUpdateInfo: Mock;395 396 beforeEach(() => {397 addItem = vi.fn();398 setUpdateInfo = vi.fn();399 updateEventEmitter.removeAllListeners();400 });401 402 afterEach(() => {403 updateEventEmitter.removeAllListeners();404 });405 406 it('should call addItem immediately when idle', () => {407 const isIdleRef = { current: true };408 const { cleanup } = setUpdateHandler(addItem, setUpdateInfo, isIdleRef);409 410 updateEventEmitter.emit('update-success', {411 message: 'Update successful!',412 });413 414 expect(addItem).toHaveBeenCalledWith(415 {416 type: MessageType.INFO,417 text: 'Update successful!',418 },419 expect.any(Number),420 );421 422 cleanup();423 });424 425 it('should use default success message when update-success has no message', () => {426 const isIdleRef = { current: true };427 const { cleanup } = setUpdateHandler(addItem, setUpdateInfo, isIdleRef);428 429 updateEventEmitter.emit('update-success', {});430 431 expect(addItem).toHaveBeenCalledWith(432 {433 type: MessageType.INFO,434 text:435 'Update successful! Please restart Qwen Code to use the new version. ' +436 'Switching model providers before restarting may not work correctly.',437 },438 expect.any(Number),439 );440 441 cleanup();442 });443 444 it('should use default failure message when update-failed has no message', () => {445 const isIdleRef = { current: true };446 const { cleanup } = setUpdateHandler(addItem, setUpdateInfo, isIdleRef);447 448 updateEventEmitter.emit('update-failed', {});449 450 expect(addItem).toHaveBeenCalledWith(451 {452 type: MessageType.ERROR,453 text: 'Automatic update failed. Please try updating manually.',454 },455 expect.any(Number),456 );457 458 cleanup();459 });460 461 it('should defer addItem when not idle (update-success)', () => {462 const isIdleRef = { current: false };463 const { cleanup } = setUpdateHandler(addItem, setUpdateInfo, isIdleRef);464 465 updateEventEmitter.emit('update-success', {466 message: 'Update successful!',467 });468 469 expect(addItem).not.toHaveBeenCalled();470 471 cleanup();472 });473 474 it('should defer addItem when not idle (update-failed)', () => {475 const isIdleRef = { current: false };476 const { cleanup } = setUpdateHandler(addItem, setUpdateInfo, isIdleRef);477 478 updateEventEmitter.emit('update-failed', {479 message: 'Update failed',480 });481 482 expect(addItem).not.toHaveBeenCalled();483 484 cleanup();485 });486 487 it('should flush deferred notifications when flush is called', () => {488 const isIdleRef = { current: false };489 const { cleanup, flush } = setUpdateHandler(490 addItem,491 setUpdateInfo,492 isIdleRef,493 );494 495 updateEventEmitter.emit('update-success', {496 message: 'Update successful!',497 });498 499 expect(addItem).not.toHaveBeenCalled();500 501 isIdleRef.current = true;502 flush();503 504 expect(addItem).toHaveBeenCalledWith(505 {506 type: MessageType.INFO,507 text: 'Update successful!',508 },509 expect.any(Number),510 );511 512 cleanup();513 });514 515 it('should flush update-failed notifications correctly', () => {516 const isIdleRef = { current: false };517 const { cleanup, flush } = setUpdateHandler(518 addItem,519 setUpdateInfo,520 isIdleRef,521 );522 523 updateEventEmitter.emit('update-failed', {524 message: 'Update failed',525 });526 527 expect(addItem).not.toHaveBeenCalled();528 529 flush();530 531 expect(addItem).toHaveBeenCalledWith(532 {533 type: MessageType.ERROR,534 text: 'Update failed',535 },536 expect.any(Number),537 );538 539 cleanup();540 });541 542 it('should flush multiple deferred notifications in order', () => {543 const isIdleRef = { current: false };544 const { cleanup, flush } = setUpdateHandler(545 addItem,546 setUpdateInfo,547 isIdleRef,548 );549 550 updateEventEmitter.emit('update-info', { message: 'Info message' });551 updateEventEmitter.emit('update-success', { message: 'Success!' });552 553 expect(addItem).not.toHaveBeenCalled();554 555 flush();556 557 expect(addItem).toHaveBeenCalledTimes(2);558 expect(addItem).toHaveBeenNthCalledWith(559 1,560 { type: MessageType.INFO, text: 'Info message' },561 expect.any(Number),562 );563 expect(addItem).toHaveBeenNthCalledWith(564 2,565 {566 type: MessageType.INFO,567 text: 'Success!',568 },569 expect.any(Number),570 );571 572 cleanup();573 });574 575 it('should clear pending notifications on cleanup', () => {576 const isIdleRef = { current: false };577 const { cleanup, flush } = setUpdateHandler(578 addItem,579 setUpdateInfo,580 isIdleRef,581 );582 583 updateEventEmitter.emit('update-success', { message: 'Success!' });584 expect(addItem).not.toHaveBeenCalled();585 586 cleanup();587 flush();588 589 // Pending queue was cleared by cleanup, so addItem should not be called590 expect(addItem).not.toHaveBeenCalled();591 });592 593 it('should be a no-op when flushing an empty queue', () => {594 const isIdleRef = { current: true };595 const { cleanup, flush } = setUpdateHandler(596 addItem,597 setUpdateInfo,598 isIdleRef,599 );600 601 flush();602 603 expect(addItem).not.toHaveBeenCalled();604 605 cleanup();606 });607 608 it('should deliver immediately after transitioning from busy to idle', () => {609 const isIdleRef = { current: false };610 const { cleanup, flush } = setUpdateHandler(611 addItem,612 setUpdateInfo,613 isIdleRef,614 );615 616 // First event while busy — deferred617 updateEventEmitter.emit('update-info', { message: 'Deferred msg' });618 expect(addItem).not.toHaveBeenCalled();619 620 // Transition to idle621 isIdleRef.current = true;622 623 // Next event while idle — delivered immediately624 updateEventEmitter.emit('update-info', { message: 'Immediate msg' });625 expect(addItem).toHaveBeenCalledTimes(1);626 expect(addItem).toHaveBeenCalledWith(627 { type: MessageType.INFO, text: 'Immediate msg' },628 expect.any(Number),629 );630 631 // The earlier deferred message should still be in the queue632 flush();633 expect(addItem).toHaveBeenCalledTimes(2);634 expect(addItem).toHaveBeenNthCalledWith(635 2,636 { type: MessageType.INFO, text: 'Deferred msg' },637 expect.any(Number),638 );639 640 cleanup();641 });642});643 