mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-21 01:07:46 +00:00
244 lines
7.3 KiB
TypeScript
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');
|
|
});
|
|
});
|
|
});
|