basant307/AI_Governance_Project
048
1/**2 * @license3 * Copyright 2025 Google LLC4 * SPDX-License-Identifier: Apache-2.05 */6 7import { describe, it, expect, vi, beforeEach } from 'vitest';8import {9 extensionConsentString,10 requestConsentOrFail,11 requestChoicePluginNonInteractive,12} from './consent.js';13import type {14 ExtensionConfig,15 ClaudeMarketplaceConfig,16} from '@qwen-code/qwen-code-core';17import prompts from 'prompts';18 19vi.mock('../../i18n/index.js', () => ({20 t: vi.fn((str: string, params?: Record<string, string>) => {21 if (params) {22 return Object.entries(params).reduce(23 (acc, [key, value]) => acc.replace(`{{${key}}}`, value),24 str,25 );26 }27 return str;28 }),29}));30 31vi.mock('prompts');32 33describe('extensionConsentString', () => {34 it('should include extension name', () => {35 const config: ExtensionConfig = {36 name: 'test-extension',37 version: '1.0.0',38 commands: [],39 };40 41 const result = extensionConsentString(config);42 43 expect(result).toContain('Installing extension "test-extension".');44 });45 46 it('should include description when present', () => {47 const config: ExtensionConfig = {48 name: 'test-extension',49 version: '1.0.0',50 description: 'A helpful test extension',51 };52 53 const result = extensionConsentString(config);54 55 expect(result).toContain('A helpful test extension');56 });57 58 it('should strip ANSI escape codes from description', () => {59 const config: ExtensionConfig = {60 name: 'test-extension',61 version: '1.0.0',62 description: '\x1b[31mMalicious\x1b[0m description',63 };64 65 const result = extensionConsentString(config);66 67 expect(result).toContain('Malicious description');68 expect(result).not.toContain('\x1b[31m');69 });70 71 it('should handle non-string description gracefully', () => {72 const config = {73 name: 'test-extension',74 version: '1.0.0',75 description: 123,76 } as unknown as ExtensionConfig;77 78 const result = extensionConsentString(config);79 80 expect(result).not.toContain('123');81 });82 83 it('should not include description when absent', () => {84 const config: ExtensionConfig = {85 name: 'test-extension',86 version: '1.0.0',87 };88 89 const result = extensionConsentString(config);90 91 const lines = result.split('\n');92 expect(lines[0]).toContain('Installing extension "test-extension".');93 expect(lines[1]).toContain('Extensions may introduce unexpected behavior');94 });95 96 it('should include warning message', () => {97 const config: ExtensionConfig = {98 name: 'test-extension',99 version: '1.0.0',100 };101 102 const result = extensionConsentString(config);103 104 expect(result).toContain('Extensions may introduce unexpected behavior');105 });106 107 it('should include MCP servers when present', () => {108 const config: ExtensionConfig = {109 name: 'test-extension',110 version: '1.0.0',111 mcpServers: {112 'test-server': {113 command: 'node',114 args: ['server.js'],115 },116 },117 };118 119 const result = extensionConsentString(config);120 121 expect(result).toContain(122 'This extension will run the following MCP servers',123 );124 expect(result).toContain('test-server');125 expect(result).toContain('local');126 expect(result).toContain('node server.js');127 });128 129 it('should include remote MCP servers', () => {130 const config: ExtensionConfig = {131 name: 'test-extension',132 version: '1.0.0',133 mcpServers: {134 'remote-server': {135 httpUrl: 'https://example.com/mcp',136 },137 },138 };139 140 const result = extensionConsentString(config);141 142 expect(result).toContain('remote');143 expect(result).toContain('https://example.com/mcp');144 });145 146 it('should include commands when present', () => {147 const config: ExtensionConfig = {148 name: 'test-extension',149 version: '1.0.0',150 };151 152 const result = extensionConsentString(config, ['command1', 'command2']);153 154 expect(result).toContain('This extension will add the following commands');155 expect(result).toContain('command1, command2');156 });157 158 it('should include context file name when present (string)', () => {159 const config: ExtensionConfig = {160 name: 'test-extension',161 version: '1.0.0',162 contextFileName: 'CUSTOM.md',163 };164 165 const result = extensionConsentString(config);166 167 expect(result).toContain('CUSTOM.md');168 });169 170 it('should include context file name when present (array)', () => {171 const config: ExtensionConfig = {172 name: 'test-extension',173 version: '1.0.0',174 contextFileName: ['FILE1.md', 'FILE2.md'],175 };176 177 const result = extensionConsentString(config);178 179 expect(result).toContain('FILE1.md, FILE2.md');180 });181 182 it('should include skills when present', () => {183 const config: ExtensionConfig = {184 name: 'test-extension',185 version: '1.0.0',186 };187 188 const result = extensionConsentString(189 config,190 [],191 [192 {193 name: 'skill1',194 description: 'Skill 1 description',195 level: 'extension',196 filePath: '/test/skill1',197 body: 'skill body',198 },199 {200 name: 'skill2',201 description: 'Skill 2 description',202 level: 'extension',203 filePath: '/test/skill2',204 body: 'skill body',205 },206 ],207 );208 209 expect(result).toContain(210 'This extension will install the following skills',211 );212 expect(result).toContain('skill1');213 expect(result).toContain('Skill 1 description');214 });215 216 it('should include subagents when present', () => {217 const config: ExtensionConfig = {218 name: 'test-extension',219 version: '1.0.0',220 };221 222 const result = extensionConsentString(223 config,224 [],225 [],226 [227 {228 name: 'agent1',229 description: 'Agent 1 description',230 systemPrompt: 'You are agent1',231 level: 'extension',232 },233 ],234 );235 236 expect(result).toContain(237 'This extension will install the following subagents',238 );239 expect(result).toContain('agent1');240 expect(result).toContain('Agent 1 description');241 });242});243 244describe('requestConsentOrFail', () => {245 let mockRequestConsent: ReturnType<typeof vi.fn>;246 247 beforeEach(() => {248 mockRequestConsent = vi.fn();249 vi.clearAllMocks();250 });251 252 it('should do nothing when options is undefined', async () => {253 await requestConsentOrFail(mockRequestConsent, undefined);254 255 expect(mockRequestConsent).not.toHaveBeenCalled();256 });257 258 it('should request consent for new extension', async () => {259 mockRequestConsent.mockResolvedValueOnce(true);260 261 await requestConsentOrFail(mockRequestConsent, {262 extensionConfig: { name: 'test-extension', version: '1.0.0' },263 originSource: 'QwenCode',264 });265 266 expect(mockRequestConsent).toHaveBeenCalled();267 });268 269 it('should throw error when user declines consent', async () => {270 mockRequestConsent.mockResolvedValueOnce(false);271 272 await expect(273 requestConsentOrFail(mockRequestConsent, {274 extensionConfig: { name: 'test-extension', version: '1.0.0' },275 originSource: 'QwenCode',276 }),277 ).rejects.toThrow('Installation cancelled for "test-extension".');278 });279 280 it('should skip consent when consent string is unchanged', async () => {281 const extensionConfig: ExtensionConfig = {282 name: 'test-extension',283 version: '1.0.0',284 };285 286 await requestConsentOrFail(mockRequestConsent, {287 extensionConfig,288 previousExtensionConfig: extensionConfig,289 originSource: 'QwenCode',290 });291 292 expect(mockRequestConsent).not.toHaveBeenCalled();293 });294 295 it('should request consent when commands change', async () => {296 mockRequestConsent.mockResolvedValueOnce(true);297 298 await requestConsentOrFail(mockRequestConsent, {299 extensionConfig: { name: 'test-extension', version: '1.0.0' },300 commands: ['command1'],301 previousExtensionConfig: { name: 'test-extension', version: '1.0.0' },302 previousCommands: [],303 originSource: 'QwenCode',304 });305 306 expect(mockRequestConsent).toHaveBeenCalled();307 });308});309 310describe('requestChoicePluginNonInteractive', () => {311 beforeEach(() => {312 vi.clearAllMocks();313 });314 315 it('should throw error when plugins array is empty', async () => {316 const marketplace: ClaudeMarketplaceConfig = {317 name: 'test-marketplace',318 owner: { name: 'Test Owner', email: 'test@example.com' },319 plugins: [],320 };321 322 await expect(323 requestChoicePluginNonInteractive(marketplace),324 ).rejects.toThrow('No plugins available in this marketplace.');325 });326 327 it('should return selected plugin name', async () => {328 const marketplace: ClaudeMarketplaceConfig = {329 name: 'test-marketplace',330 owner: { name: 'Test Owner', email: 'test@example.com' },331 plugins: [332 {333 name: 'plugin1',334 description: 'Plugin 1',335 version: '1.0.0',336 source: 'src1',337 },338 {339 name: 'plugin2',340 description: 'Plugin 2',341 version: '1.0.0',342 source: 'src2',343 },344 ],345 };346 347 vi.mocked(prompts).mockResolvedValueOnce({ plugin: 'plugin2' });348 349 const result = await requestChoicePluginNonInteractive(marketplace);350 351 expect(result).toBe('plugin2');352 expect(prompts).toHaveBeenCalledWith(353 expect.objectContaining({354 type: 'select',355 name: 'plugin',356 choices: expect.arrayContaining([357 expect.objectContaining({ value: 'plugin1' }),358 expect.objectContaining({ value: 'plugin2' }),359 ]),360 }),361 );362 });363 364 it('should throw error when selection is cancelled', async () => {365 const marketplace: ClaudeMarketplaceConfig = {366 name: 'test-marketplace',367 owner: { name: 'Test Owner', email: 'test@example.com' },368 plugins: [{ name: 'plugin1', version: '1.0.0', source: 'src1' }],369 };370 371 vi.mocked(prompts).mockResolvedValueOnce({ plugin: undefined });372 373 await expect(374 requestChoicePluginNonInteractive(marketplace),375 ).rejects.toThrow('Plugin selection cancelled.');376 });377});378 