Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/extension/chatSessions/copilotcli/vscode-node/test/askUserQuestionHandler.spec.ts
13406 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { describe, expect, it, vi } from 'vitest';
7
import { LanguageModelToolInvocationOptions } from 'vscode';
8
import { mock } from '../../../../../util/common/test/simpleMock';
9
import { CancellationToken } from '../../../../../util/vs/base/common/cancellation';
10
import { LanguageModelTextPart, LanguageModelToolResult, LanguageModelToolResult2 } from '../../../../../vscodeTypes';
11
import { ICopilotTool } from '../../../../tools/common/toolsRegistry';
12
import { IToolsService } from '../../../../tools/common/toolsService';
13
import { IQuestion } from '../../../copilotcli/node/userInputHelpers';
14
import { IAnswerResult, UserQuestionHandler } from '../askUserQuestionHandler';
15
16
function makeAskQuestionsTool(invokeResult: LanguageModelToolResult | undefined, resolveInput?: unknown): ICopilotTool<unknown> {
17
return {
18
invoke: vi.fn(async () => invokeResult),
19
resolveInput,
20
} as unknown as ICopilotTool<unknown>;
21
}
22
23
function makeToolsService(tool: ICopilotTool<unknown> | undefined): IToolsService {
24
return new class extends mock<IToolsService>() {
25
override invokeTool(name: string, options: LanguageModelToolInvocationOptions<unknown>, token: CancellationToken): Thenable<LanguageModelToolResult2> {
26
return (tool as any).invoke(options, token) as Thenable<LanguageModelToolResult2>;
27
}
28
}();
29
}
30
31
const logService = new class extends mock<import('../../../../../platform/log/common/logService').ILogService>() {
32
override trace = vi.fn();
33
override warn = vi.fn();
34
}();
35
36
function makeHandler(tool: ICopilotTool<unknown> | undefined) {
37
return new UserQuestionHandler(logService, makeToolsService(tool));
38
}
39
40
const toolInvocationToken = {} as import('vscode').ChatParticipantToolToken;
41
42
const question: IQuestion = {
43
header: 'What color?',
44
question: 'What color?',
45
options: [{ label: 'Red' }, { label: 'Blue' }],
46
allowFreeformInput: true,
47
};
48
49
describe('UserQuestionHandler', () => {
50
describe('askUserQuestion', () => {
51
it('returns undefined when tool returns no content', async () => {
52
const tool = makeAskQuestionsTool(new LanguageModelToolResult([]));
53
const handler = makeHandler(tool);
54
const result = await handler.askUserQuestion(question, toolInvocationToken, CancellationToken.None);
55
expect(result).toBeUndefined();
56
});
57
58
it('returns undefined when result part is not a LanguageModelTextPart', async () => {
59
const tool = makeAskQuestionsTool(new LanguageModelToolResult([{ value: 'not-a-text-part' } as unknown as LanguageModelTextPart]));
60
const handler = makeHandler(tool);
61
const result = await handler.askUserQuestion(question, toolInvocationToken, CancellationToken.None);
62
expect(result).toBeUndefined();
63
});
64
65
it('returns undefined when the answer key is missing', async () => {
66
const answers: IAnswerResult = { answers: {} };
67
const tool = makeAskQuestionsTool(new LanguageModelToolResult([new LanguageModelTextPart(JSON.stringify(answers))]));
68
const handler = makeHandler(tool);
69
const result = await handler.askUserQuestion(question, toolInvocationToken, CancellationToken.None);
70
expect(result).toBeUndefined();
71
});
72
73
it('returns freeText answer when freeText is present', async () => {
74
const answers: IAnswerResult = {
75
answers: {
76
'What color?': { selected: [], freeText: 'Purple', skipped: false }
77
}
78
};
79
const tool = makeAskQuestionsTool(new LanguageModelToolResult([new LanguageModelTextPart(JSON.stringify(answers))]));
80
const handler = makeHandler(tool);
81
const result = await handler.askUserQuestion(question, toolInvocationToken, CancellationToken.None);
82
expect(result).toEqual({ selected: [], freeText: 'Purple', skipped: false });
83
});
84
85
it('returns selections when choices are selected', async () => {
86
const answers: IAnswerResult = {
87
answers: {
88
'What color?': { selected: ['Red', 'Blue'], freeText: null, skipped: false }
89
}
90
};
91
const tool = makeAskQuestionsTool(new LanguageModelToolResult([new LanguageModelTextPart(JSON.stringify(answers))]));
92
const handler = makeHandler(tool);
93
const result = await handler.askUserQuestion(question, toolInvocationToken, CancellationToken.None);
94
expect(result).toEqual({ selected: ['Red', 'Blue'], freeText: null, skipped: false });
95
});
96
97
it('returns undefined when answer has no freeText and no selections', async () => {
98
const answers: IAnswerResult = {
99
answers: {
100
'What color?': { selected: [], freeText: null, skipped: true }
101
}
102
};
103
const tool = makeAskQuestionsTool(new LanguageModelToolResult([new LanguageModelTextPart(JSON.stringify(answers))]));
104
const handler = makeHandler(tool);
105
const result = await handler.askUserQuestion(question, toolInvocationToken, CancellationToken.None);
106
expect(result).toBeUndefined();
107
});
108
109
it('passes question text, choices, and freeform flag to the tool', async () => {
110
const answers: IAnswerResult = {
111
answers: { 'What color?': { selected: ['Red'], freeText: null, skipped: false } }
112
};
113
const tool = makeAskQuestionsTool(new LanguageModelToolResult([new LanguageModelTextPart(JSON.stringify(answers))]));
114
const handler = makeHandler(tool);
115
await handler.askUserQuestion(question, toolInvocationToken, CancellationToken.None);
116
const invokeArg = (tool.invoke as ReturnType<typeof vi.fn>).mock.calls[0][0];
117
expect(invokeArg.input.questions[0]).toMatchObject({
118
header: 'What color?',
119
question: 'What color?',
120
allowFreeformInput: true,
121
options: [{ label: 'Red' }, { label: 'Blue' }],
122
});
123
});
124
});
125
});
126
127