CoolFace
Datasetpublic

basant307/AI_Governance_Project

sourceHugging Faceapache-2.0updated 2mo agoView on Hugging Face
0likes48downloads
handleAutoUpdate.test.ts643 linesDownload Raw Back to utils
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 
basant307/AI_Governance_Project · CoolFace