CoolFace
Datasetpublic

basant307/AI_Governance_Project

sourceHugging Faceapache-2.0updated 2mo agoView on Hugging Face
0likes48downloads
install.test.ts450 linesDownload Raw Back to extensions
1/**2 * @license3 * Copyright 2025 Google LLC4 * SPDX-License-Identifier: Apache-2.05 */6 7import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';8import { handleInstall, installCommand } from './install.js';9import yargs from 'yargs';10 11const mockInstallExtension = vi.hoisted(() => vi.fn());12const mockRefreshCache = vi.hoisted(() => vi.fn());13const mockSetExtensionScope = vi.hoisted(() => vi.fn());14const mockEnableExtension = vi.hoisted(() => vi.fn());15const mockDisableExtension = vi.hoisted(() => vi.fn());16const mockParseInstallSource = vi.hoisted(() => vi.fn());17const mockRequestConsentNonInteractive = vi.hoisted(() => vi.fn());18const mockRequestConsentOrFail = vi.hoisted(() => vi.fn());19const mockIsWorkspaceTrusted = vi.hoisted(() => vi.fn());20const mockLoadSettings = vi.hoisted(() => vi.fn());21const mockWriteStdoutLine = vi.hoisted(() => vi.fn());22const mockWriteStderrLine = vi.hoisted(() => vi.fn());23 24vi.mock('@qwen-code/qwen-code-core', () => ({25  ExtensionManager: vi.fn().mockImplementation(() => ({26    installExtension: mockInstallExtension,27    refreshCache: mockRefreshCache,28    setExtensionScope: mockSetExtensionScope,29    enableExtension: mockEnableExtension,30    disableExtension: mockDisableExtension,31  })),32  parseInstallSource: mockParseInstallSource,33}));34 35vi.mock('./consent.js', () => ({36  requestConsentNonInteractive: mockRequestConsentNonInteractive,37  requestConsentOrFail: mockRequestConsentOrFail,38  requestChoicePluginNonInteractive: vi.fn(),39}));40 41vi.mock('../../config/trustedFolders.js', () => ({42  isWorkspaceTrusted: mockIsWorkspaceTrusted,43}));44 45vi.mock('../../config/settings.js', () => ({46  loadSettings: mockLoadSettings,47  SettingScope: {48    User: 'User',49    Workspace: 'Workspace',50    System: 'System',51    SystemDefaults: 'SystemDefaults',52  },53}));54 55vi.mock('../../utils/errors.js', () => ({56  getErrorMessage: vi.fn((error: Error) => error.message),57}));58 59vi.mock('../../utils/stdioHelpers.js', () => ({60  writeStdoutLine: mockWriteStdoutLine,61  writeStderrLine: mockWriteStderrLine,62  clearScreen: vi.fn(),63}));64 65describe('extensions install command', () => {66  it('should fail if no source is provided', () => {67    const validationParser = yargs([])68      .locale('en')69      .command(installCommand)70      .fail(false);71    expect(() => validationParser.parse('install')).toThrow(72      'Not enough non-option arguments: got 0, need at least 1',73    );74  });75});76 77describe('handleInstall', () => {78  beforeEach(() => {79    mockRefreshCache.mockResolvedValue(undefined);80    mockLoadSettings.mockReturnValue({ merged: {} });81    mockIsWorkspaceTrusted.mockReturnValue(true);82  });83 84  afterEach(() => {85    vi.clearAllMocks();86  });87 88  it('should install an extension from a http source', async () => {89    const processSpy = vi90      .spyOn(process, 'exit')91      .mockImplementation(() => undefined as never);92 93    mockParseInstallSource.mockResolvedValue({94      type: 'http',95      url: 'http://google.com',96    });97    mockInstallExtension.mockResolvedValue({ name: 'http-extension' });98 99    await handleInstall({100      source: 'http://google.com',101    });102 103    expect(mockWriteStdoutLine).toHaveBeenCalledWith(104      'Extension "http-extension" installed successfully and enabled.',105    );106 107    processSpy.mockRestore();108  });109 110  it('should install an extension from a https source', async () => {111    const processSpy = vi112      .spyOn(process, 'exit')113      .mockImplementation(() => undefined as never);114 115    mockParseInstallSource.mockResolvedValue({116      type: 'https',117      url: 'https://google.com',118    });119    mockInstallExtension.mockResolvedValue({ name: 'https-extension' });120 121    await handleInstall({122      source: 'https://google.com',123    });124 125    expect(mockWriteStdoutLine).toHaveBeenCalledWith(126      'Extension "https-extension" installed successfully and enabled.',127    );128 129    processSpy.mockRestore();130  });131 132  it('should install an extension from a git source', async () => {133    const processSpy = vi134      .spyOn(process, 'exit')135      .mockImplementation(() => undefined as never);136 137    mockParseInstallSource.mockResolvedValue({138      type: 'git',139      url: 'git@some-url',140    });141    mockInstallExtension.mockResolvedValue({ name: 'git-extension' });142 143    await handleInstall({144      source: 'git@some-url',145    });146 147    expect(mockWriteStdoutLine).toHaveBeenCalledWith(148      'Extension "git-extension" installed successfully and enabled.',149    );150 151    processSpy.mockRestore();152  });153 154  it('throws an error from an unknown source', async () => {155    const processSpy = vi156      .spyOn(process, 'exit')157      .mockImplementation(() => undefined as never);158 159    mockParseInstallSource.mockRejectedValue(160      new Error('Install source not found.'),161    );162    await handleInstall({163      source: 'test://google.com',164    });165 166    expect(mockWriteStderrLine).toHaveBeenCalledWith(167      'Install source not found.',168    );169    expect(processSpy).toHaveBeenCalledWith(1);170 171    processSpy.mockRestore();172  });173 174  it('should install an extension from a sso source', async () => {175    const processSpy = vi176      .spyOn(process, 'exit')177      .mockImplementation(() => undefined as never);178 179    mockParseInstallSource.mockResolvedValue({180      type: 'sso',181      url: 'sso://google.com',182    });183    mockInstallExtension.mockResolvedValue({ name: 'sso-extension' });184 185    await handleInstall({186      source: 'sso://google.com',187    });188 189    expect(mockWriteStdoutLine).toHaveBeenCalledWith(190      'Extension "sso-extension" installed successfully and enabled.',191    );192 193    processSpy.mockRestore();194  });195 196  it('should install an extension from a local path', async () => {197    const processSpy = vi198      .spyOn(process, 'exit')199      .mockImplementation(() => undefined as never);200 201    mockParseInstallSource.mockResolvedValue({202      type: 'local',203      path: '/some/path',204    });205    mockInstallExtension.mockResolvedValue({ name: 'local-extension' });206 207    await handleInstall({208      source: '/some/path',209    });210 211    expect(mockWriteStdoutLine).toHaveBeenCalledWith(212      'Extension "local-extension" installed successfully and enabled.',213    );214 215    processSpy.mockRestore();216  });217 218  it('should install an extension from an archive URL', async () => {219    const processSpy = vi220      .spyOn(process, 'exit')221      .mockImplementation(() => undefined as never);222 223    mockParseInstallSource.mockResolvedValue({224      type: 'archive-url',225      source: 'https://example.com/extension.zip',226    });227    mockInstallExtension.mockResolvedValue({ name: 'archive-extension' });228 229    await handleInstall({230      source: 'https://example.com/extension.zip',231      autoUpdate: true,232    });233 234    expect(mockInstallExtension).toHaveBeenCalledWith(235      expect.objectContaining({236        source: 'https://example.com/extension.zip',237        type: 'archive-url',238        autoUpdate: true,239      }),240      expect.any(Function),241    );242    expect(mockWriteStdoutLine).toHaveBeenCalledWith(243      'Extension "archive-extension" installed successfully and enabled.',244    );245 246    processSpy.mockRestore();247  });248 249  it('should reject --ref for archive URL extensions', async () => {250    const processSpy = vi251      .spyOn(process, 'exit')252      .mockImplementation(() => undefined as never);253 254    mockParseInstallSource.mockResolvedValue({255      type: 'archive-url',256      source: 'https://example.com/extension.zip',257    });258 259    await handleInstall({260      source: 'https://example.com/extension.zip',261      ref: 'v1.0.0',262    });263 264    expect(mockWriteStderrLine).toHaveBeenCalledWith(265      '--ref is not applicable for archive URL extensions.',266    );267    expect(mockInstallExtension).not.toHaveBeenCalled();268    expect(processSpy).toHaveBeenCalledWith(1);269 270    processSpy.mockRestore();271  });272 273  it('should throw an error if install extension fails', async () => {274    const processSpy = vi275      .spyOn(process, 'exit')276      .mockImplementation(() => undefined as never);277 278    mockParseInstallSource.mockResolvedValue({279      type: 'git',280      url: 'git@some-url',281    });282    mockInstallExtension.mockRejectedValue(283      new Error('Install extension failed'),284    );285 286    await handleInstall({ source: 'git@some-url' });287 288    expect(mockWriteStderrLine).toHaveBeenCalledWith(289      'Install extension failed',290    );291    expect(processSpy).toHaveBeenCalledWith(1);292 293    processSpy.mockRestore();294  });295 296  it('should re-scope enablement to the workspace for a project-scope install', async () => {297    mockParseInstallSource.mockResolvedValue({298      type: 'git',299      url: 'git@some-url',300    });301    mockInstallExtension.mockResolvedValue({ name: 'scoped-extension' });302 303    await handleInstall({ source: 'git@some-url', scope: 'project' });304 305    expect(mockSetExtensionScope).toHaveBeenCalledWith(306      'scoped-extension',307      'project',308    );309    expect(mockDisableExtension).toHaveBeenCalledWith(310      'scoped-extension',311      'User',312    );313    expect(mockEnableExtension).toHaveBeenCalledWith(314      'scoped-extension',315      'Workspace',316    );317    expect(mockWriteStdoutLine).toHaveBeenCalledWith(318      'Extension "scoped-extension" installed successfully and enabled for the current workspace.',319    );320  });321 322  it('rolls back the User-scope disable when the Workspace enable fails', async () => {323    const processSpy = vi324      .spyOn(process, 'exit')325      .mockImplementation(() => undefined as never);326    mockParseInstallSource.mockResolvedValue({327      type: 'git',328      url: 'git@some-url',329    });330    mockInstallExtension.mockResolvedValue({ name: 'scoped-extension' });331    // Workspace enable (first call) fails; the rollback User enable succeeds.332    mockEnableExtension.mockRejectedValueOnce(333      new Error('workspace enable failed'),334    );335    mockEnableExtension.mockResolvedValueOnce(undefined);336 337    await handleInstall({ source: 'git@some-url', scope: 'project' });338 339    expect(mockDisableExtension).toHaveBeenCalledWith(340      'scoped-extension',341      'User',342    );343    // Both the failed Workspace enable and the rollback User enable were attempted.344    expect(mockEnableExtension).toHaveBeenNthCalledWith(345      1,346      'scoped-extension',347      'Workspace',348    );349    expect(mockEnableExtension).toHaveBeenNthCalledWith(350      2,351      'scoped-extension',352      'User',353    );354    // The original failure is surfaced and the command exits non-zero.355    expect(mockWriteStderrLine).toHaveBeenCalledWith('workspace enable failed');356    expect(processSpy).toHaveBeenCalledWith(1);357    processSpy.mockRestore();358  });359 360  it('surfaces a rollback failure when the recovery enable also fails', async () => {361    const processSpy = vi362      .spyOn(process, 'exit')363      .mockImplementation(() => undefined as never);364    mockParseInstallSource.mockResolvedValue({365      type: 'git',366      url: 'git@some-url',367    });368    mockInstallExtension.mockResolvedValue({ name: 'scoped-extension' });369    // Both the Workspace enable and the rollback User enable fail.370    mockEnableExtension.mockRejectedValueOnce(371      new Error('workspace enable failed'),372    );373    mockEnableExtension.mockRejectedValueOnce(new Error('rollback failed'));374 375    await handleInstall({ source: 'git@some-url', scope: 'project' });376 377    // A warning naming the failed rollback, plus the original error, are shown.378    expect(mockWriteStderrLine).toHaveBeenCalledWith(379      expect.stringContaining('failed to roll back the scope change'),380    );381    expect(mockWriteStderrLine).toHaveBeenCalledWith('workspace enable failed');382    expect(processSpy).toHaveBeenCalledWith(1);383    processSpy.mockRestore();384  });385 386  it('should accept workspace as an alias of project scope', async () => {387    mockParseInstallSource.mockResolvedValue({388      type: 'git',389      url: 'git@some-url',390    });391    mockInstallExtension.mockResolvedValue({ name: 'scoped-extension' });392 393    await handleInstall({ source: 'git@some-url', scope: 'workspace' });394 395    expect(mockSetExtensionScope).toHaveBeenCalledWith(396      'scoped-extension',397      'project',398    );399    expect(mockEnableExtension).toHaveBeenCalledWith(400      'scoped-extension',401      'Workspace',402    );403  });404 405  it('should record user scope without re-scoping enablement', async () => {406    mockParseInstallSource.mockResolvedValue({407      type: 'git',408      url: 'git@some-url',409    });410    mockInstallExtension.mockResolvedValue({ name: 'user-extension' });411 412    await handleInstall({ source: 'git@some-url', scope: 'user' });413 414    expect(mockSetExtensionScope).toHaveBeenCalledWith(415      'user-extension',416      'user',417    );418    expect(mockDisableExtension).not.toHaveBeenCalled();419    expect(mockEnableExtension).not.toHaveBeenCalled();420    expect(mockWriteStdoutLine).toHaveBeenCalledWith(421      'Extension "user-extension" installed successfully and enabled.',422    );423  });424 425  it('should print archive validation errors from the extension manager', async () => {426    const processSpy = vi427      .spyOn(process, 'exit')428      .mockImplementation(() => undefined as never);429 430    mockParseInstallSource.mockResolvedValue({431      type: 'git',432      source: 'owner/repo',433    });434    mockInstallExtension.mockRejectedValue(435      new Error(436        'Extension archive is missing a supported extension manifest. Expected qwen-extension.json, gemini-extension.json, .claude-plugin/marketplace.json, or .claude-plugin/plugin.json at the archive root, or inside a single top-level extension directory.',437      ),438    );439 440    await handleInstall({ source: 'owner/repo' });441 442    expect(mockWriteStderrLine).toHaveBeenCalledWith(443      'Extension archive is missing a supported extension manifest. Expected qwen-extension.json, gemini-extension.json, .claude-plugin/marketplace.json, or .claude-plugin/plugin.json at the archive root, or inside a single top-level extension directory.',444    );445    expect(processSpy).toHaveBeenCalledWith(1);446 447    processSpy.mockRestore();448  });449});450 
basant307/AI_Governance_Project · CoolFace