fix(plan): make question type required in AskUser tool (#18959)

This commit is contained in:
Adib234
2026-02-13 10:03:52 -05:00
committed by GitHub
parent b61a123da8
commit d5dfae6bbf
5 changed files with 77 additions and 29 deletions

View File

@@ -147,9 +147,9 @@ export enum QuestionType {
export interface Question {
question: string;
header: string;
/** Question type: 'choice' renders selectable options, 'text' renders free-form input, 'yesno' renders a binary Yes/No choice. Defaults to 'choice'. */
type?: QuestionType;
/** Selectable choices. REQUIRED when type='choice' or omitted. IGNORED for 'text' and 'yesno'. */
/** Question type: 'choice' renders selectable options, 'text' renders free-form input, 'yesno' renders a binary Yes/No choice. */
type: QuestionType;
/** Selectable choices. REQUIRED when type='choice'. IGNORED for 'text' and 'yesno'. */
options?: QuestionOption[];
/** Allow multiple selections. Only applies when type='choice'. */
multiSelect?: boolean;

View File

@@ -131,6 +131,7 @@ describe('AskUserTool', () => {
const questions = Array(5).fill({
question: 'Test?',
header: 'Test',
type: QuestionType.CHOICE,
options: [
{ label: 'A', description: 'A' },
{ label: 'B', description: 'B' },
@@ -156,7 +157,13 @@ describe('AskUserTool', () => {
it('should return error if header exceeds max length', () => {
const result = tool.validateToolParams({
questions: [{ question: 'Test?', header: 'This is way too long' }],
questions: [
{
question: 'Test?',
header: 'This is way too long',
type: QuestionType.CHOICE,
},
],
});
expect(result).toContain('must NOT have more than 16 characters');
});
@@ -167,6 +174,7 @@ describe('AskUserTool', () => {
{
question: 'Test?',
header: 'Test',
type: QuestionType.CHOICE,
options: [{ label: 'A', description: 'A' }],
},
],
@@ -182,6 +190,7 @@ describe('AskUserTool', () => {
{
question: 'Test?',
header: 'Test',
type: QuestionType.CHOICE,
options: [
{ label: 'A', description: 'A' },
{ label: 'B', description: 'B' },
@@ -201,6 +210,7 @@ describe('AskUserTool', () => {
{
question: 'Which approach?',
header: 'Approach',
type: QuestionType.CHOICE,
options: [
{ label: 'A', description: 'Option A' },
{ label: 'B', description: 'Option B' },
@@ -224,18 +234,16 @@ describe('AskUserTool', () => {
expect(result).toContain("type='choice' requires 'options'");
});
it('should return error if type is omitted and options missing (defaults to choice)', () => {
it('should return error if type is missing', () => {
const result = tool.validateToolParams({
questions: [
{
question: 'Pick one?',
header: 'Choice',
// type omitted, defaults to 'choice'
// options missing
},
} as unknown as Question,
],
});
expect(result).toContain("type='choice' requires 'options'");
expect(result).toContain("must have required property 'type'");
});
it('should accept text type without options', () => {
@@ -288,6 +296,7 @@ describe('AskUserTool', () => {
{
question: 'Pick one?',
header: 'Choice',
type: QuestionType.CHOICE,
options: [
{ label: '', description: 'Empty label' },
{ label: 'B', description: 'Option B' },
@@ -304,6 +313,7 @@ describe('AskUserTool', () => {
{
question: 'Pick one?',
header: 'Choice',
type: QuestionType.CHOICE,
options: [
{ label: 'A' } as { label: string; description: string },
{ label: 'B', description: 'Option B' },
@@ -318,7 +328,13 @@ describe('AskUserTool', () => {
describe('validateBuildAndExecute', () => {
it('should hide validation errors from returnDisplay', async () => {
const params = {
questions: [{ question: 'Test?', header: 'This is way too long' }],
questions: [
{
question: 'Test?',
header: 'This is way too long',
type: QuestionType.TEXT,
},
],
};
const result = await tool.validateBuildAndExecute(
@@ -337,7 +353,9 @@ describe('AskUserTool', () => {
.mockReturnValue(null);
const params = {
questions: [{ question: 'Valid?', header: 'Valid' }],
questions: [
{ question: 'Valid?', header: 'Valid', type: QuestionType.TEXT },
],
};
const mockInvocation = {
@@ -366,10 +384,11 @@ describe('AskUserTool', () => {
describe('shouldConfirmExecute', () => {
it('should return confirmation details with normalized questions', async () => {
const questions = [
const questions: Question[] = [
{
question: 'How should we proceed with this task?',
header: 'Approach',
type: QuestionType.CHOICE,
options: [
{
label: 'Quick fix (Recommended)',
@@ -394,12 +413,7 @@ describe('AskUserTool', () => {
expect(details).not.toBe(false);
if (details && details.type === 'ask_user') {
expect(details.title).toBe('Ask User');
expect(details.questions).toEqual(
questions.map((q) => ({
...q,
type: QuestionType.CHOICE,
})),
);
expect(details.questions).toEqual(questions);
expect(typeof details.onConfirm).toBe('function');
} else {
// Type guard for TypeScript
@@ -407,11 +421,12 @@ describe('AskUserTool', () => {
}
});
it('should normalize question type to CHOICE when omitted', async () => {
const questions = [
it('should use provided question type', async () => {
const questions: Question[] = [
{
question: 'Which approach?',
header: 'Approach',
type: QuestionType.CHOICE,
options: [
{ label: 'Option A', description: 'First option' },
{ label: 'Option B', description: 'Second option' },
@@ -432,10 +447,11 @@ describe('AskUserTool', () => {
describe('execute', () => {
it('should return user answers after confirmation', async () => {
const questions = [
const questions: Question[] = [
{
question: 'How should we proceed with this task?',
header: 'Approach',
type: QuestionType.CHOICE,
options: [
{
label: 'Quick fix (Recommended)',
@@ -484,10 +500,11 @@ describe('AskUserTool', () => {
});
it('should display message when user submits without answering', async () => {
const questions = [
const questions: Question[] = [
{
question: 'Which approach?',
header: 'Approach',
type: QuestionType.CHOICE,
options: [
{ label: 'Option A', description: 'First option' },
{ label: 'Option B', description: 'Second option' },
@@ -528,6 +545,7 @@ describe('AskUserTool', () => {
{
question: 'Which sections of the documentation should be updated?',
header: 'Docs',
type: QuestionType.CHOICE,
options: [
{
label: 'User Guide',

View File

@@ -42,7 +42,7 @@ export class AskUserTool extends BaseDeclarativeTool<
maxItems: 4,
items: {
type: 'object',
required: ['question', 'header'],
required: ['question', 'header', 'type'],
properties: {
question: {
type: 'string',
@@ -111,7 +111,7 @@ export class AskUserTool extends BaseDeclarativeTool<
for (let i = 0; i < params.questions.length; i++) {
const q = params.questions[i];
const questionType = q.type ?? QuestionType.CHOICE;
const questionType = q.type;
// Validate that 'choice' type has options
if (questionType === QuestionType.CHOICE) {
@@ -186,7 +186,7 @@ export class AskUserInvocation extends BaseToolInvocation<
): Promise<ToolAskUserConfirmationDetails | false> {
const normalizedQuestions = this.params.questions.map((q) => ({
...q,
type: q.type ?? QuestionType.CHOICE,
type: q.type,
}));
return {
@@ -210,9 +210,7 @@ export class AskUserInvocation extends BaseToolInvocation<
}
async execute(_signal: AbortSignal): Promise<ToolResult> {
const questionTypes = this.params.questions.map(
(q) => q.type ?? QuestionType.CHOICE,
);
const questionTypes = this.params.questions.map((q) => q.type);
if (this.confirmationOutcome === ToolConfirmationOutcome.Cancel) {
return {