Download packages/cli/src/commands/extensions/validate.test.ts from SaylorTwift/gemini-cli: direct link, hf CLI and curl.
- Browser
- Download file 6.23 kB
-
https://huggingface.co/SaylorTwift/gemini-cli/resolve/main/packages/cli/src/commands/extensions/validate.test.ts
- Command line
-
hf download hf://SaylorTwift/gemini-cli/packages/cli/src/commands/extensions/validate.test.ts
-
curl -L -o validate.test.ts https://huggingface.co/SaylorTwift/gemini-cli/resolve/main/packages/cli/src/commands/extensions/validate.test.ts
6.23 kB
| /** | |
| * @license | |
| * Copyright 2025 Google LLC | |
| * SPDX-License-Identifier: Apache-2.0 | |
| */ | |
| import * as fs from 'node:fs'; | |
| import { | |
| describe, | |
| it, | |
| expect, | |
| vi, | |
| beforeEach, | |
| afterEach, | |
| type MockInstance, | |
| } from 'vitest'; | |
| import { handleValidate, validateCommand } from './validate.js'; | |
| import yargs from 'yargs'; | |
| import { createExtension } from '../../test-utils/createExtension.js'; | |
| import path from 'node:path'; | |
| import * as os from 'node:os'; | |
| import { debugLogger } from '@google/gemini-cli-core'; | |
| vi.mock('../utils.js', () => ({ | |
| exitCli: vi.fn(), | |
| })); | |
| describe('extensions validate command', () => { | |
| it('should fail if no path is provided', () => { | |
| const validationParser = yargs([]) | |
| .locale('en') | |
| .command(validateCommand) | |
| .fail(false); | |
| expect(() => validationParser.parse('validate')).toThrow( | |
| 'Not enough non-option arguments: got 0, need at least 1', | |
| ); | |
| }); | |
| }); | |
| describe('handleValidate', () => { | |
| let debugLoggerLogSpy: MockInstance; | |
| let debugLoggerWarnSpy: MockInstance; | |
| let debugLoggerErrorSpy: MockInstance; | |
| let processSpy: MockInstance; | |
| let tempHomeDir: string; | |
| let tempWorkspaceDir: string; | |
| beforeEach(() => { | |
| debugLoggerLogSpy = vi.spyOn(debugLogger, 'log'); | |
| debugLoggerWarnSpy = vi.spyOn(debugLogger, 'warn'); | |
| debugLoggerErrorSpy = vi.spyOn(debugLogger, 'error'); | |
| processSpy = vi | |
| .spyOn(process, 'exit') | |
| .mockImplementation(() => undefined as never); | |
| tempHomeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'test-home')); | |
| tempWorkspaceDir = fs.mkdtempSync(path.join(tempHomeDir, 'test-workspace')); | |
| vi.spyOn(process, 'cwd').mockReturnValue(tempWorkspaceDir); | |
| }); | |
| afterEach(() => { | |
| vi.restoreAllMocks(); | |
| fs.rmSync(tempHomeDir, { recursive: true, force: true }); | |
| fs.rmSync(tempWorkspaceDir, { recursive: true, force: true }); | |
| }); | |
| it('should validate an extension from a local dir', async () => { | |
| createExtension({ | |
| extensionsDir: tempWorkspaceDir, | |
| name: 'local-ext-name', | |
| version: '1.0.0', | |
| }); | |
| await handleValidate({ | |
| path: 'local-ext-name', | |
| }); | |
| expect(debugLoggerLogSpy).toHaveBeenCalledWith( | |
| 'Extension local-ext-name has been successfully validated.', | |
| ); | |
| }); | |
| it('should throw an error if the extension name is invalid', async () => { | |
| createExtension({ | |
| extensionsDir: tempWorkspaceDir, | |
| name: 'INVALID_NAME', | |
| version: '1.0.0', | |
| }); | |
| await handleValidate({ | |
| path: 'INVALID_NAME', | |
| }); | |
| expect(debugLoggerErrorSpy).toHaveBeenCalledWith( | |
| expect.stringContaining( | |
| 'Invalid extension name: "INVALID_NAME". Only letters (a-z, A-Z), numbers (0-9), and dashes (-) are allowed.', | |
| ), | |
| ); | |
| expect(processSpy).toHaveBeenCalledWith(1); | |
| }); | |
| it('should warn if version is not formatted with semver', async () => { | |
| createExtension({ | |
| extensionsDir: tempWorkspaceDir, | |
| name: 'valid-name', | |
| version: '1', | |
| }); | |
| await handleValidate({ | |
| path: 'valid-name', | |
| }); | |
| expect(debugLoggerWarnSpy).toHaveBeenCalledWith( | |
| expect.stringContaining( | |
| "Version '1' does not appear to be standard semver (e.g., 1.0.0).", | |
| ), | |
| ); | |
| expect(debugLoggerLogSpy).toHaveBeenCalledWith( | |
| 'Extension valid-name has been successfully validated.', | |
| ); | |
| }); | |
| it('should throw an error if context files are missing', async () => { | |
| createExtension({ | |
| extensionsDir: tempWorkspaceDir, | |
| name: 'valid-name', | |
| version: '1.0.0', | |
| contextFileName: 'contextFile.md', | |
| }); | |
| fs.rmSync(path.join(tempWorkspaceDir, 'valid-name/contextFile.md')); | |
| await handleValidate({ | |
| path: 'valid-name', | |
| }); | |
| expect(debugLoggerErrorSpy).toHaveBeenCalledWith( | |
| expect.stringContaining( | |
| 'The following context files referenced in gemini-extension.json are missing: contextFile.md', | |
| ), | |
| ); | |
| expect(processSpy).toHaveBeenCalledWith(1); | |
| }); | |
| it('should throw an error if context file uses relative parent directory navigation', async () => { | |
| createExtension({ | |
| extensionsDir: tempWorkspaceDir, | |
| name: 'parent-nav-ext', | |
| version: '1.0.0', | |
| contextFileName: '../secret.txt', | |
| }); | |
| await handleValidate({ | |
| path: 'parent-nav-ext', | |
| }); | |
| expect(debugLoggerErrorSpy).toHaveBeenCalledWith( | |
| expect.stringContaining( | |
| 'Invalid context file path: "../secret.txt". Relative parent directory references ("..") are not allowed.', | |
| ), | |
| ); | |
| expect(processSpy).toHaveBeenCalledWith(1); | |
| }); | |
| it('should throw an error if context file uses URL-encoded relative parent directory navigation', async () => { | |
| createExtension({ | |
| extensionsDir: tempWorkspaceDir, | |
| name: 'encoded-nav-ext', | |
| version: '1.0.0', | |
| contextFileName: '%2e%2e/secret.txt', | |
| }); | |
| await handleValidate({ | |
| path: 'encoded-nav-ext', | |
| }); | |
| expect(debugLoggerErrorSpy).toHaveBeenCalledWith( | |
| expect.stringContaining( | |
| 'Invalid context file path: "%2e%2e/secret.txt". Relative parent directory references ("..") are not allowed.', | |
| ), | |
| ); | |
| expect(processSpy).toHaveBeenCalledWith(1); | |
| }); | |
| it('should throw an error if context file uses absolute path', async () => { | |
| createExtension({ | |
| extensionsDir: tempWorkspaceDir, | |
| name: 'abs-path-ext', | |
| version: '1.0.0', | |
| contextFileName: '/etc/hosts', | |
| }); | |
| await handleValidate({ | |
| path: 'abs-path-ext', | |
| }); | |
| expect(debugLoggerErrorSpy).toHaveBeenCalledWith( | |
| expect.stringContaining( | |
| 'Invalid context file path: "/etc/hosts". Absolute paths are not allowed.', | |
| ), | |
| ); | |
| expect(processSpy).toHaveBeenCalledWith(1); | |
| }); | |
| it('should successfully validate an extension with context file containing percent characters', async () => { | |
| createExtension({ | |
| extensionsDir: tempWorkspaceDir, | |
| name: 'percent-name-ext', | |
| version: '1.0.0', | |
| contextFileName: '100%_complete.md', | |
| }); | |
| await handleValidate({ | |
| path: 'percent-name-ext', | |
| }); | |
| expect(debugLoggerLogSpy).toHaveBeenCalledWith( | |
| 'Extension percent-name-ext has been successfully validated.', | |
| ); | |
| }); | |
| }); | |