CoolFace
Datasetpublic

basant307/AI_Governance_Project

sourceHugging Faceapache-2.0updated 2mo agoView on Hugging Face
0likes48downloads
SkillCommandLoader.test.ts461 linesDownload Raw Back to services
1/**2 * @license3 * Copyright 2025 Qwen4 * SPDX-License-Identifier: Apache-2.05 */6 7import { describe, it, expect, vi, beforeEach } from 'vitest';8import { SkillCommandLoader } from './SkillCommandLoader.js';9import { CommandKind, type CommandContext } from '../ui/commands/types.js';10import {11  buildSkillLlmContent,12  type Config,13  type SkillConfig,14} from '@qwen-code/qwen-code-core';15 16function makeSkill(overrides: Partial<SkillConfig> = {}): SkillConfig {17  return {18    name: 'my-skill',19    description: 'My skill description',20    level: 'user',21    filePath: '/tmp/qwen-test/skills/my-skill/SKILL.md',22    body: 'Skill body content.',23    ...overrides,24  };25}26 27function makeSkillPrompt(body: string): string {28  return buildSkillLlmContent('/tmp/qwen-test/skills/my-skill', body);29}30 31describe('SkillCommandLoader', () => {32  let mockConfig: Config;33  let mockSkillManager: { listSkills: ReturnType<typeof vi.fn> };34  let mockAddSessionAllowRule: ReturnType<typeof vi.fn>;35 36  beforeEach(() => {37    vi.clearAllMocks();38    mockSkillManager = {39      listSkills: vi.fn().mockResolvedValue([]),40    };41    mockAddSessionAllowRule = vi.fn();42    mockConfig = {43      getSkillManager: vi.fn().mockReturnValue(mockSkillManager),44      getBareMode: vi.fn().mockReturnValue(false),45      getPermissionManager: vi46        .fn()47        .mockReturnValue({ addSessionAllowRule: mockAddSessionAllowRule }),48      // SkillCommandLoader filters via this. Default to empty so existing49      // assertions about "all skills surface" stay true; per-test cases50      // override to verify the filter behavior.51      getDisabledSkillNames: vi.fn().mockReturnValue(new Set<string>()),52    } as unknown as Config;53  });54 55  const signal = new AbortController().signal;56 57  it('should return empty array when config is null', async () => {58    const loader = new SkillCommandLoader(null);59    expect(await loader.loadCommands(signal)).toEqual([]);60  });61 62  it('should return empty array when SkillManager is not available', async () => {63    const config = {64      getSkillManager: vi.fn().mockReturnValue(null),65      getBareMode: vi.fn().mockReturnValue(false),66    } as unknown as Config;67    const loader = new SkillCommandLoader(config);68    expect(await loader.loadCommands(signal)).toEqual([]);69  });70 71  it('should return empty array in bare mode', async () => {72    (mockConfig.getBareMode as ReturnType<typeof vi.fn>).mockReturnValue(true);73    const loader = new SkillCommandLoader(mockConfig);74    expect(await loader.loadCommands(signal)).toEqual([]);75    expect(mockSkillManager.listSkills).not.toHaveBeenCalled();76  });77 78  it('should propagate argumentHint from skills to slash commands', async () => {79    const skill = makeSkill({ argumentHint: '[topic]' });80    mockSkillManager.listSkills.mockImplementation(81      ({ level }: { level: string }) =>82        Promise.resolve(level === 'user' ? [skill] : []),83    );84 85    const loader = new SkillCommandLoader(mockConfig);86    const commands = await loader.loadCommands(signal);87 88    expect(commands[0]?.argumentHint).toBe('[topic]');89  });90 91  it('should default skills to user-invocable slash commands', async () => {92    const skill = makeSkill();93    mockSkillManager.listSkills.mockImplementation(94      ({ level }: { level: string }) =>95        Promise.resolve(level === 'user' ? [skill] : []),96    );97 98    const loader = new SkillCommandLoader(mockConfig);99    const commands = await loader.loadCommands(signal);100 101    expect(commands[0]?.userInvocable).toBe(true);102  });103 104  it('should propagate userInvocable from skills to slash commands', async () => {105    const skill = makeSkill({ userInvocable: false });106    mockSkillManager.listSkills.mockImplementation(107      ({ level }: { level: string }) =>108        Promise.resolve(level === 'user' ? [skill] : []),109    );110 111    const loader = new SkillCommandLoader(mockConfig);112    const commands = await loader.loadCommands(signal);113 114    expect(commands[0]?.userInvocable).toBe(false);115    expect(commands[0]?.modelInvocable).toBe(true);116  });117 118  it('should query user, project, and extension levels', async () => {119    const loader = new SkillCommandLoader(mockConfig);120    await loader.loadCommands(signal);121    expect(mockSkillManager.listSkills).toHaveBeenCalledWith({ level: 'user' });122    expect(mockSkillManager.listSkills).toHaveBeenCalledWith({123      level: 'project',124    });125    expect(mockSkillManager.listSkills).toHaveBeenCalledWith({126      level: 'extension',127    });128  });129 130  it('should load user skill as slash command with correct properties', async () => {131    const skill = makeSkill({ level: 'user' });132    mockSkillManager.listSkills.mockImplementation(133      ({ level }: { level: string }) =>134        Promise.resolve(level === 'user' ? [skill] : []),135    );136 137    const loader = new SkillCommandLoader(mockConfig);138    const commands = await loader.loadCommands(signal);139 140    expect(commands).toHaveLength(1);141    const cmd = commands[0];142    expect(cmd.name).toBe('my-skill');143    expect(cmd.description).toBe('My skill description');144    expect(cmd.kind).toBe(CommandKind.SKILL);145    expect(cmd.source).toBe('skill-dir-command');146    expect(cmd.sourceLabel).toBe('User');147    expect(cmd.sourceDetail).toBe('user');148    expect(cmd.modelInvocable).toBe(true);149  });150 151  it('does not propagate skill.priority to completionPriority', async () => {152    // Priority is scoped to the `/skills` listing only; slash-completion /153    // `/help` ordering should be independent of any skill's priority value.154    const skill = makeSkill({ level: 'user', priority: 42 });155    mockSkillManager.listSkills.mockImplementation(156      ({ level }: { level: string }) =>157        Promise.resolve(level === 'user' ? [skill] : []),158    );159 160    const loader = new SkillCommandLoader(mockConfig);161    const commands = await loader.loadCommands(signal);162 163    expect(commands[0].completionPriority).toBeUndefined();164  });165 166  it('should load project skill with sourceLabel "Project"', async () => {167    const skill = makeSkill({ level: 'project' });168    mockSkillManager.listSkills.mockImplementation(169      ({ level }: { level: string }) =>170        Promise.resolve(level === 'project' ? [skill] : []),171    );172 173    const loader = new SkillCommandLoader(mockConfig);174    const commands = await loader.loadCommands(signal);175 176    expect(commands[0].sourceLabel).toBe('Project');177    expect(commands[0].sourceDetail).toBe('project');178    expect(commands[0].source).toBe('skill-dir-command');179    expect(commands[0].modelInvocable).toBe(true);180  });181 182  it('should submit skill body as prompt', async () => {183    const skill = makeSkill();184    mockSkillManager.listSkills.mockImplementation(185      ({ level }: { level: string }) =>186        Promise.resolve(level === 'user' ? [skill] : []),187    );188 189    const loader = new SkillCommandLoader(mockConfig);190    const commands = await loader.loadCommands(signal);191    const result = await commands[0].action!(192      { invocation: { raw: '/my-skill', args: '' } } as never,193      '',194    );195 196    expect(result).toEqual({197      type: 'submit_prompt',198      content: [{ text: makeSkillPrompt('Skill body content.') }],199    });200  });201 202  it('should append raw invocation when args are provided', async () => {203    const skill = makeSkill();204    mockSkillManager.listSkills.mockImplementation(205      ({ level }: { level: string }) =>206        Promise.resolve(level === 'user' ? [skill] : []),207    );208 209    const loader = new SkillCommandLoader(mockConfig);210    const commands = await loader.loadCommands(signal);211    const result = await commands[0].action!(212      { invocation: { raw: '/my-skill foo', args: 'foo' } } as never,213      'foo',214    );215 216    expect(result).toEqual({217      type: 'submit_prompt',218      content: [219        {220          text: `${makeSkillPrompt('Skill body content.')}\n\n/my-skill foo`,221        },222      ],223    });224  });225 226  it('should return empty array when listSkills throws', async () => {227    mockSkillManager.listSkills.mockRejectedValue(new Error('load failed'));228    const loader = new SkillCommandLoader(mockConfig);229    expect(await loader.loadCommands(signal)).toEqual([]);230  });231 232  describe('extension skills', () => {233    it('should be modelInvocable when description is present', async () => {234      const skill = makeSkill({235        level: 'extension',236        extensionName: 'superpowers-lab',237        description: 'Use tmux for interactive commands',238      });239      mockSkillManager.listSkills.mockImplementation(240        ({ level }: { level: string }) =>241          Promise.resolve(level === 'extension' ? [skill] : []),242      );243 244      const loader = new SkillCommandLoader(mockConfig);245      const commands = await loader.loadCommands(signal);246 247      expect(commands[0].modelInvocable).toBe(true);248      expect(commands[0].source).toBe('plugin-command');249      expect(commands[0].sourceLabel).toBe('Extension: superpowers-lab');250      expect(commands[0].sourceDetail).toBe('extension');251    });252 253    it('should be modelInvocable when whenToUse is present', async () => {254      const skill = makeSkill({255        level: 'extension',256        extensionName: 'superpowers-lab',257        description: '',258        whenToUse: 'Use when you need tmux',259      });260      mockSkillManager.listSkills.mockImplementation(261        ({ level }: { level: string }) =>262          Promise.resolve(level === 'extension' ? [skill] : []),263      );264 265      const loader = new SkillCommandLoader(mockConfig);266      const commands = await loader.loadCommands(signal);267 268      expect(commands[0].modelInvocable).toBe(true);269    });270 271    it('should NOT be modelInvocable when description and whenToUse are absent', async () => {272      const skill = makeSkill({273        level: 'extension',274        extensionName: 'superpowers-lab',275        description: '',276        whenToUse: undefined,277      });278      mockSkillManager.listSkills.mockImplementation(279        ({ level }: { level: string }) =>280          Promise.resolve(level === 'extension' ? [skill] : []),281      );282 283      const loader = new SkillCommandLoader(mockConfig);284      const commands = await loader.loadCommands(signal);285 286      expect(commands[0].modelInvocable).toBe(false);287    });288 289    it('should NOT be modelInvocable when disableModelInvocation is true, even with description', async () => {290      const skill = makeSkill({291        level: 'extension',292        extensionName: 'superpowers-lab',293        description: 'Some description',294        disableModelInvocation: true,295      });296      mockSkillManager.listSkills.mockImplementation(297        ({ level }: { level: string }) =>298          Promise.resolve(level === 'extension' ? [skill] : []),299      );300 301      const loader = new SkillCommandLoader(mockConfig);302      const commands = await loader.loadCommands(signal);303 304      expect(commands[0].modelInvocable).toBe(false);305    });306 307    it('should use "Extension: unknown" as sourceLabel when extensionName is absent', async () => {308      const skill = makeSkill({ level: 'extension', description: 'foo' });309      mockSkillManager.listSkills.mockImplementation(310        ({ level }: { level: string }) =>311          Promise.resolve(level === 'extension' ? [skill] : []),312      );313 314      const loader = new SkillCommandLoader(mockConfig);315      const commands = await loader.loadCommands(signal);316 317      expect(commands[0].sourceLabel).toBe('Extension: unknown');318      expect(commands[0].sourceDetail).toBe('extension');319    });320  });321 322  describe('user/project skill disableModelInvocation', () => {323    it('user skill with disableModelInvocation:true should NOT be modelInvocable', async () => {324      const skill = makeSkill({ level: 'user', disableModelInvocation: true });325      mockSkillManager.listSkills.mockImplementation(326        ({ level }: { level: string }) =>327          Promise.resolve(level === 'user' ? [skill] : []),328      );329 330      const loader = new SkillCommandLoader(mockConfig);331      const commands = await loader.loadCommands(signal);332 333      expect(commands[0].modelInvocable).toBe(false);334    });335  });336 337  it('should aggregate skills from all levels', async () => {338    mockSkillManager.listSkills.mockImplementation(339      ({ level }: { level: string }) => {340        if (level === 'user')341          return Promise.resolve([342            makeSkill({ name: 'user-skill', level: 'user' }),343          ]);344        if (level === 'project')345          return Promise.resolve([346            makeSkill({ name: 'proj-skill', level: 'project' }),347          ]);348        if (level === 'extension')349          return Promise.resolve([350            makeSkill({351              name: 'ext-skill',352              level: 'extension',353              description: 'foo',354            }),355          ]);356        return Promise.resolve([]);357      },358    );359 360    const loader = new SkillCommandLoader(mockConfig);361    const commands = await loader.loadCommands(signal);362 363    expect(commands).toHaveLength(3);364    expect(commands.map((c) => c.name)).toEqual([365      'user-skill',366      'proj-skill',367      'ext-skill',368    ]);369  });370 371  describe('allowedTools grant', () => {372    it('grants allowedTools as session allow rules when the command runs', async () => {373      const skill = makeSkill({374        level: 'user',375        allowedTools: ['Bash(git *)', 'Edit'],376      });377      mockSkillManager.listSkills.mockImplementation(378        ({ level }: { level: string }) =>379          Promise.resolve(level === 'user' ? [skill] : []),380      );381 382      const loader = new SkillCommandLoader(mockConfig);383      const commands = await loader.loadCommands(signal);384      await commands[0].action?.({} as CommandContext, '');385 386      expect(mockAddSessionAllowRule).toHaveBeenCalledTimes(2);387      expect(mockAddSessionAllowRule).toHaveBeenNthCalledWith(1, 'Bash(git *)');388      expect(mockAddSessionAllowRule).toHaveBeenNthCalledWith(2, 'Edit');389    });390 391    it('does not grant when the skill declares no allowedTools', async () => {392      const skill = makeSkill({ level: 'user' }); // no allowedTools393      mockSkillManager.listSkills.mockImplementation(394        ({ level }: { level: string }) =>395          Promise.resolve(level === 'user' ? [skill] : []),396      );397 398      const loader = new SkillCommandLoader(mockConfig);399      const commands = await loader.loadCommands(signal);400      await commands[0].action?.({} as CommandContext, '');401 402      expect(mockAddSessionAllowRule).not.toHaveBeenCalled();403    });404  });405 406  describe('skills.disabled filter', () => {407    it('omits disabled skills (case-insensitive) from the command list', async () => {408      mockSkillManager.listSkills.mockImplementation(409        ({ level }: { level: string }) => {410          if (level === 'user')411            return Promise.resolve([412              makeSkill({ name: 'KeepMe', level: 'user' }),413              makeSkill({ name: 'HideMe', level: 'user' }),414            ]);415          return Promise.resolve([]);416        },417      );418      // Disabled set is lower-case (matches Config.getDisabledSkillNames419      // contract). Loader compares with `.toLowerCase()`.420      (421        mockConfig.getDisabledSkillNames as ReturnType<typeof vi.fn>422      ).mockReturnValue(new Set(['hideme']));423 424      const loader = new SkillCommandLoader(mockConfig);425      const commands = await loader.loadCommands(signal);426 427      expect(commands.map((c) => c.name)).toEqual(['KeepMe']);428    });429 430    it('reflects provider mutations on each load (live read)', async () => {431      // Regression: the provider must be called per-load, not cached, so432      // CommandService rebuilds (triggered by `reloadCommands`) pick up433      // the latest `skills.disabled`. A frozen-at-construction snapshot434      // would be a silent regression.435      mockSkillManager.listSkills.mockImplementation(436        ({ level }: { level: string }) =>437          level === 'user'438            ? Promise.resolve([makeSkill({ name: 'foo', level: 'user' })])439            : Promise.resolve([]),440      );441      let disabled = new Set<string>();442      (443        mockConfig.getDisabledSkillNames as ReturnType<typeof vi.fn>444      ).mockImplementation(() => disabled);445 446      const loader = new SkillCommandLoader(mockConfig);447 448      const first = await loader.loadCommands(signal);449      expect(first.map((c) => c.name)).toEqual(['foo']);450 451      disabled = new Set(['foo']);452      const second = await loader.loadCommands(signal);453      expect(second).toEqual([]);454 455      disabled = new Set<string>();456      const third = await loader.loadCommands(signal);457      expect(third.map((c) => c.name)).toEqual(['foo']);458    });459  });460});461 
basant307/AI_Governance_Project · CoolFace