Path: blob/main/extensions/copilot/src/extension/chatSessions/copilotcli/vscode-node/test/askUserQuestionHandler.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, it, vi } from 'vitest';6import { LanguageModelToolInvocationOptions } from 'vscode';7import { mock } from '../../../../../util/common/test/simpleMock';8import { CancellationToken } from '../../../../../util/vs/base/common/cancellation';9import { LanguageModelTextPart, LanguageModelToolResult, LanguageModelToolResult2 } from '../../../../../vscodeTypes';10import { ICopilotTool } from '../../../../tools/common/toolsRegistry';11import { IToolsService } from '../../../../tools/common/toolsService';12import { IQuestion } from '../../../copilotcli/node/userInputHelpers';13import { IAnswerResult, UserQuestionHandler } from '../askUserQuestionHandler';1415function makeAskQuestionsTool(invokeResult: LanguageModelToolResult | undefined, resolveInput?: unknown): ICopilotTool<unknown> {16return {17invoke: vi.fn(async () => invokeResult),18resolveInput,19} as unknown as ICopilotTool<unknown>;20}2122function makeToolsService(tool: ICopilotTool<unknown> | undefined): IToolsService {23return new class extends mock<IToolsService>() {24override invokeTool(name: string, options: LanguageModelToolInvocationOptions<unknown>, token: CancellationToken): Thenable<LanguageModelToolResult2> {25return (tool as any).invoke(options, token) as Thenable<LanguageModelToolResult2>;26}27}();28}2930const logService = new class extends mock<import('../../../../../platform/log/common/logService').ILogService>() {31override trace = vi.fn();32override warn = vi.fn();33}();3435function makeHandler(tool: ICopilotTool<unknown> | undefined) {36return new UserQuestionHandler(logService, makeToolsService(tool));37}3839const toolInvocationToken = {} as import('vscode').ChatParticipantToolToken;4041const question: IQuestion = {42header: 'What color?',43question: 'What color?',44options: [{ label: 'Red' }, { label: 'Blue' }],45allowFreeformInput: true,46};4748describe('UserQuestionHandler', () => {49describe('askUserQuestion', () => {50it('returns undefined when tool returns no content', async () => {51const tool = makeAskQuestionsTool(new LanguageModelToolResult([]));52const handler = makeHandler(tool);53const result = await handler.askUserQuestion(question, toolInvocationToken, CancellationToken.None);54expect(result).toBeUndefined();55});5657it('returns undefined when result part is not a LanguageModelTextPart', async () => {58const tool = makeAskQuestionsTool(new LanguageModelToolResult([{ value: 'not-a-text-part' } as unknown as LanguageModelTextPart]));59const handler = makeHandler(tool);60const result = await handler.askUserQuestion(question, toolInvocationToken, CancellationToken.None);61expect(result).toBeUndefined();62});6364it('returns undefined when the answer key is missing', async () => {65const answers: IAnswerResult = { answers: {} };66const tool = makeAskQuestionsTool(new LanguageModelToolResult([new LanguageModelTextPart(JSON.stringify(answers))]));67const handler = makeHandler(tool);68const result = await handler.askUserQuestion(question, toolInvocationToken, CancellationToken.None);69expect(result).toBeUndefined();70});7172it('returns freeText answer when freeText is present', async () => {73const answers: IAnswerResult = {74answers: {75'What color?': { selected: [], freeText: 'Purple', skipped: false }76}77};78const tool = makeAskQuestionsTool(new LanguageModelToolResult([new LanguageModelTextPart(JSON.stringify(answers))]));79const handler = makeHandler(tool);80const result = await handler.askUserQuestion(question, toolInvocationToken, CancellationToken.None);81expect(result).toEqual({ selected: [], freeText: 'Purple', skipped: false });82});8384it('returns selections when choices are selected', async () => {85const answers: IAnswerResult = {86answers: {87'What color?': { selected: ['Red', 'Blue'], freeText: null, skipped: false }88}89};90const tool = makeAskQuestionsTool(new LanguageModelToolResult([new LanguageModelTextPart(JSON.stringify(answers))]));91const handler = makeHandler(tool);92const result = await handler.askUserQuestion(question, toolInvocationToken, CancellationToken.None);93expect(result).toEqual({ selected: ['Red', 'Blue'], freeText: null, skipped: false });94});9596it('returns undefined when answer has no freeText and no selections', async () => {97const answers: IAnswerResult = {98answers: {99'What color?': { selected: [], freeText: null, skipped: true }100}101};102const tool = makeAskQuestionsTool(new LanguageModelToolResult([new LanguageModelTextPart(JSON.stringify(answers))]));103const handler = makeHandler(tool);104const result = await handler.askUserQuestion(question, toolInvocationToken, CancellationToken.None);105expect(result).toBeUndefined();106});107108it('passes question text, choices, and freeform flag to the tool', async () => {109const answers: IAnswerResult = {110answers: { 'What color?': { selected: ['Red'], freeText: null, skipped: false } }111};112const tool = makeAskQuestionsTool(new LanguageModelToolResult([new LanguageModelTextPart(JSON.stringify(answers))]));113const handler = makeHandler(tool);114await handler.askUserQuestion(question, toolInvocationToken, CancellationToken.None);115const invokeArg = (tool.invoke as ReturnType<typeof vi.fn>).mock.calls[0][0];116expect(invokeArg.input.questions[0]).toMatchObject({117header: 'What color?',118question: 'What color?',119allowFreeformInput: true,120options: [{ label: 'Red' }, { label: 'Blue' }],121});122});123});124});125126127