Files
qwen-code/packages/core/src/tools/exitPlanMode.test.ts

244 lines
7.3 KiB
TypeScript

/**
* @license
* Copyright 2025 Qwen
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { ExitPlanModeTool, type ExitPlanModeParams } from './exitPlanMode.js';
import { ApprovalMode, type Config } from '../config/config.js';
import { ToolConfirmationOutcome } from './tools.js';
describe('ExitPlanModeTool', () => {
let tool: ExitPlanModeTool;
let mockConfig: Config;
let approvalMode: ApprovalMode;
beforeEach(() => {
approvalMode = ApprovalMode.PLAN;
mockConfig = {
getApprovalMode: vi.fn(() => approvalMode),
setApprovalMode: vi.fn((mode: ApprovalMode) => {
approvalMode = mode;
}),
} as unknown as Config;
tool = new ExitPlanModeTool(mockConfig);
});
describe('constructor and metadata', () => {
it('should have correct tool name', () => {
expect(tool.name).toBe('exit_plan_mode');
expect(ExitPlanModeTool.Name).toBe('exit_plan_mode');
});
it('should have correct display name', () => {
expect(tool.displayName).toBe('ExitPlanMode');
});
it('should have correct kind', () => {
expect(tool.kind).toBe('think');
});
it('should have correct schema', () => {
expect(tool.schema).toEqual({
name: 'exit_plan_mode',
description: expect.stringContaining(
'Use this tool when you are in plan mode',
),
parametersJsonSchema: {
type: 'object',
properties: {
plan: {
type: 'string',
description: expect.stringContaining('The plan you came up with'),
},
},
required: ['plan'],
additionalProperties: false,
$schema: 'http://json-schema.org/draft-07/schema#',
},
});
});
});
describe('validateToolParams', () => {
it('should accept valid parameters', () => {
const params: ExitPlanModeParams = {
plan: 'This is a comprehensive plan for the implementation.',
};
const result = tool.validateToolParams(params);
expect(result).toBeNull();
});
it('should reject missing plan parameter', () => {
const params = {} as ExitPlanModeParams;
const result = tool.validateToolParams(params);
expect(result).toBe('Parameter "plan" must be a non-empty string.');
});
it('should reject empty plan parameter', () => {
const params: ExitPlanModeParams = {
plan: '',
};
const result = tool.validateToolParams(params);
expect(result).toBe('Parameter "plan" must be a non-empty string.');
});
it('should reject whitespace-only plan parameter', () => {
const params: ExitPlanModeParams = {
plan: ' \n\t ',
};
const result = tool.validateToolParams(params);
expect(result).toBe('Parameter "plan" must be a non-empty string.');
});
it('should reject non-string plan parameter', () => {
const params = {
plan: 123,
} as unknown as ExitPlanModeParams;
const result = tool.validateToolParams(params);
expect(result).toBe('Parameter "plan" must be a non-empty string.');
});
});
describe('tool execution', () => {
it('should execute successfully through tool interface after approval', async () => {
const params: ExitPlanModeParams = {
plan: 'This is my implementation plan:\n1. Step 1\n2. Step 2\n3. Step 3',
};
const signal = new AbortController().signal;
// Use the tool's public build method
const invocation = tool.build(params);
expect(invocation).toBeDefined();
expect(invocation.params).toEqual(params);
const confirmation = await invocation.shouldConfirmExecute(signal);
expect(confirmation).toMatchObject({
type: 'plan',
title: 'Would you like to proceed?',
plan: params.plan,
});
if (confirmation) {
await confirmation.onConfirm(ToolConfirmationOutcome.ProceedOnce);
}
const result = await invocation.execute(signal);
expect(result.llmContent).toContain(
'User has approved your plan. You can now start coding',
);
expect(result.returnDisplay).toEqual({
type: 'plan_summary',
message: 'User approved the plan.',
plan: params.plan,
});
expect(mockConfig.setApprovalMode).toHaveBeenCalledWith(
ApprovalMode.DEFAULT,
);
expect(approvalMode).toBe(ApprovalMode.DEFAULT);
});
it('should request confirmation with plan details', async () => {
const params: ExitPlanModeParams = {
plan: 'Simple plan',
};
const signal = new AbortController().signal;
const invocation = tool.build(params);
const confirmation = await invocation.shouldConfirmExecute(signal);
if (confirmation) {
expect(confirmation.type).toBe('plan');
if (confirmation.type === 'plan') {
expect(confirmation.plan).toBe(params.plan);
}
await confirmation.onConfirm(ToolConfirmationOutcome.ProceedAlways);
}
expect(mockConfig.setApprovalMode).toHaveBeenCalledWith(
ApprovalMode.AUTO_EDIT,
);
expect(approvalMode).toBe(ApprovalMode.AUTO_EDIT);
});
it('should remain in plan mode when confirmation is rejected', async () => {
const params: ExitPlanModeParams = {
plan: 'Remain in planning',
};
const signal = new AbortController().signal;
const invocation = tool.build(params);
const confirmation = await invocation.shouldConfirmExecute(signal);
if (confirmation) {
await confirmation.onConfirm(ToolConfirmationOutcome.Cancel);
}
const result = await invocation.execute(signal);
expect(result.llmContent).toBe(
'Plan execution was not approved. Remaining in plan mode.',
);
expect(result.returnDisplay).toBe(
'Plan execution was not approved. Remaining in plan mode.',
);
expect(mockConfig.setApprovalMode).toHaveBeenCalledWith(
ApprovalMode.PLAN,
);
expect(approvalMode).toBe(ApprovalMode.PLAN);
});
it('should have correct description', () => {
const params: ExitPlanModeParams = {
plan: 'Test plan',
};
const invocation = tool.build(params);
expect(invocation.getDescription()).toBe(
'Present implementation plan for user approval',
);
});
it('should return empty tool locations', () => {
const params: ExitPlanModeParams = {
plan: 'Test plan',
};
const invocation = tool.build(params);
expect(invocation.toolLocations()).toEqual([]);
});
});
describe('tool description', () => {
it('should contain usage guidelines', () => {
expect(tool.description).toContain(
'Only use this tool when the task requires planning',
);
expect(tool.description).toContain(
'Do not use the exit plan mode tool because you are not planning',
);
expect(tool.description).toContain(
'Use the exit plan mode tool after you have finished planning',
);
});
it('should contain examples', () => {
expect(tool.description).toContain(
'Search for and understand the implementation of vim mode',
);
expect(tool.description).toContain('Help me implement yank mode for vim');
});
});
});