Path: blob/main/extensions/copilot/src/extension/prompts/node/panel/test/chatVariablesHelpers.spec.ts
13406 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { describe, expect, test } from 'vitest';6import { URI } from '../../../../../util/vs/base/common/uri';7import { ChatVariablesCollection, getPromptFileSlashCommandId, parseSlashCommand, PromptFileIdPrefix, type PromptVariable } from '../../../../prompt/common/chatVariablesCollection';8import { buildSlashCommandUserMessage } from '../chatVariables';910function makePromptVariable(name: string, value: PromptVariable['value']): PromptVariable {11return {12reference: { id: name, name, value },13originalName: name,14uniqueName: name,15value,16isMarkedReadonly: undefined,17};18}1920describe('getPromptFileSlashCommandId', () => {21test('prompt file uses filename without .prompt.md extension', () => {22expect(getPromptFileSlashCommandId(23makePromptVariable('prompt:yell-foo.prompt.md', URI.file('/workspace/.github/prompts/yell-foo.prompt.md'))24)).toEqual({ name: 'prompt:yell-foo.prompt.md', id: 'yell-foo' });25});2627test('skill file uses parent folder name', () => {28expect(getPromptFileSlashCommandId(29makePromptVariable('code-review', URI.file('/workspace/.github/skills/code-review/SKILL.md'))30)).toEqual({ name: 'code-review', id: 'code-review' });31});3233test('skill file is case-insensitive for SKILL.md', () => {34expect(getPromptFileSlashCommandId(35makePromptVariable('my-skill', URI.file('/workspace/.github/skills/my-skill/skill.md'))36)).toEqual({ name: 'my-skill', id: 'my-skill' });37});3839test('non-prompt, non-skill file falls back to reference name', () => {40expect(getPromptFileSlashCommandId(41makePromptVariable('some-instructions.instructions.md', URI.file('/workspace/.github/instructions/some-instructions.instructions.md'))42)).toEqual({ name: 'some-instructions.instructions.md', id: 'some-instructions.instructions.md' });43});4445test('non-URI value falls back to reference name', () => {46expect(getPromptFileSlashCommandId(47makePromptVariable('inline-ref', 'some string value')48)).toEqual({ name: 'inline-ref', id: 'inline-ref' });49});5051test('prompt file with nested path', () => {52expect(getPromptFileSlashCommandId(53makePromptVariable('prompt:deeply-nested.prompt.md', URI.file('/a/b/c/d/deeply-nested.prompt.md'))54)).toEqual({ name: 'prompt:deeply-nested.prompt.md', id: 'deeply-nested' });55});56});5758describe('buildSlashCommandUserMessage', () => {59function makeCollection(entries: { name: string; uri: ReturnType<typeof URI.file> }[]): ChatVariablesCollection {60return new ChatVariablesCollection(entries.map(e => ({61id: `${PromptFileIdPrefix}:${e.name}`,62name: e.name,63value: e.uri,64})));65}6667const chatVariables = makeCollection([68{ name: 'prompt:code-review.prompt.md', uri: URI.file('/workspace/.github/prompts/code-review.prompt.md') },69{ name: 'my-skill', uri: URI.file('/workspace/.github/skills/my-skill/SKILL.md') },70]);7172test('returns follow instruction for matching slash command without args', () => {73expect(buildSlashCommandUserMessage('/code-review', chatVariables))74.toBe('Follow instructions in #prompt:code-review.prompt.md');75});7677test('returns follow instruction with arguments when provided', () => {78expect(buildSlashCommandUserMessage('/code-review some-file.ts', chatVariables))79.toBe('Follow instructions in #prompt:code-review.prompt.md with these arguments: some-file.ts');80});8182test('passes multi-word arguments', () => {83expect(buildSlashCommandUserMessage('/code-review file1.ts file2.ts --strict', chatVariables))84.toBe('Follow instructions in #prompt:code-review.prompt.md with these arguments: file1.ts file2.ts --strict');85});8687test('matches skill slash commands', () => {88expect(buildSlashCommandUserMessage('/my-skill do something', chatVariables))89.toBe('Follow instructions in #my-skill with these arguments: do something');90});9192test('returns original query when no slash command', () => {93expect(buildSlashCommandUserMessage('just a normal question', chatVariables))94.toBe('just a normal question');95});9697test('returns original query when slash command does not match any prompt file', () => {98expect(buildSlashCommandUserMessage('/unknown-command arg1', chatVariables))99.toBe('/unknown-command arg1');100});101102test('handles leading whitespace in query', () => {103expect(buildSlashCommandUserMessage(' /code-review', chatVariables))104.toBe('Follow instructions in #prompt:code-review.prompt.md');105});106107test('trims trailing whitespace from arguments', () => {108expect(buildSlashCommandUserMessage('/code-review some-file.ts ', chatVariables))109.toBe('Follow instructions in #prompt:code-review.prompt.md with these arguments: some-file.ts');110});111112test('handles empty prompt file list', () => {113expect(buildSlashCommandUserMessage('/code-review', new ChatVariablesCollection([])))114.toBe('/code-review');115});116117test('handles multiline arguments', () => {118expect(buildSlashCommandUserMessage('/code-review line1\nline2', chatVariables))119.toBe('Follow instructions in #prompt:code-review.prompt.md with these arguments: line1\nline2');120});121});122123describe('parseSlashCommand', () => {124function makeCollection(entries: { name: string; uri: ReturnType<typeof URI.file> }[]): ChatVariablesCollection {125return new ChatVariablesCollection(entries.map(e => ({126id: `${PromptFileIdPrefix}:${e.name}`,127name: e.name,128value: e.uri,129})));130}131132const chatVariables = makeCollection([133{ name: 'prompt:code-review.prompt.md', uri: URI.file('/workspace/.github/prompts/code-review.prompt.md') },134{ name: 'my-skill', uri: URI.file('/workspace/.github/skills/my-skill/SKILL.md') },135]);136137test('returns undefined for plain text query', () => {138expect(parseSlashCommand('just a question', chatVariables)).toBeUndefined();139});140141test('returns undefined when slash command does not match any prompt file', () => {142expect(parseSlashCommand('/unknown arg', chatVariables)).toBeUndefined();143});144145test('returns undefined for empty query', () => {146expect(parseSlashCommand('', chatVariables)).toBeUndefined();147});148149test('matches prompt file and returns command and empty args', () => {150const result = parseSlashCommand('/code-review', chatVariables);151expect(result).toBeDefined();152expect(result!.command).toBe('code-review');153expect(result!.args).toBe('');154expect(result!.promptFile).toEqual({ name: 'prompt:code-review.prompt.md', id: 'code-review' });155});156157test('matches prompt file and returns parsed args', () => {158const result = parseSlashCommand('/code-review src/foo.ts --strict', chatVariables);159expect(result).toBeDefined();160expect(result!.command).toBe('code-review');161expect(result!.args).toBe('src/foo.ts --strict');162});163164test('matches skill file', () => {165const result = parseSlashCommand('/my-skill do something', chatVariables);166expect(result).toBeDefined();167expect(result!.command).toBe('my-skill');168expect(result!.promptFile).toEqual({ name: 'my-skill', id: 'my-skill' });169expect(result!.args).toBe('do something');170});171172test('returns the matched variable', () => {173const result = parseSlashCommand('/code-review', chatVariables);174expect(result).toBeDefined();175expect(URI.isUri(result!.variable.value)).toBe(true);176expect(result!.variable.reference.name).toBe('prompt:code-review.prompt.md');177});178179test('handles leading whitespace', () => {180const result = parseSlashCommand(' /code-review', chatVariables);181expect(result).toBeDefined();182expect(result!.command).toBe('code-review');183});184185test('trims trailing whitespace from args', () => {186const result = parseSlashCommand('/code-review arg ', chatVariables);187expect(result!.args).toBe('arg');188});189190test('skips non-prompt-file references', () => {191const mixed = new ChatVariablesCollection([192{ id: 'vscode.instructions.file:inst', name: 'inst', value: URI.file('/workspace/inst.md') },193{ id: `${PromptFileIdPrefix}:prompt:review.prompt.md`, name: 'prompt:review.prompt.md', value: URI.file('/workspace/review.prompt.md') },194]);195const result = parseSlashCommand('/review', mixed);196expect(result).toBeDefined();197expect(result!.command).toBe('review');198});199200test('returns undefined with empty collection', () => {201expect(parseSlashCommand('/code-review', new ChatVariablesCollection([]))).toBeUndefined();202});203});204205206