|
|
@@ -0,0 +1,412 @@
|
|
|
+import { describe, it, expect, vi, beforeEach } from "vitest"
|
|
|
+import { TodoItem } from "@roo-code/types"
|
|
|
+import { AttemptCompletionToolUse } from "../../../shared/tools"
|
|
|
+
|
|
|
+// Mock the formatResponse module before importing the tool
|
|
|
+vi.mock("../../prompts/responses", () => ({
|
|
|
+ formatResponse: {
|
|
|
+ toolError: vi.fn((msg: string) => `Error: ${msg}`),
|
|
|
+ },
|
|
|
+}))
|
|
|
+
|
|
|
+// Mock vscode module
|
|
|
+vi.mock("vscode", () => ({
|
|
|
+ workspace: {
|
|
|
+ getConfiguration: vi.fn(() => ({
|
|
|
+ get: vi.fn(),
|
|
|
+ })),
|
|
|
+ },
|
|
|
+}))
|
|
|
+
|
|
|
+// Mock Package module
|
|
|
+vi.mock("../../../shared/package", () => ({
|
|
|
+ Package: {
|
|
|
+ name: "roo-cline",
|
|
|
+ },
|
|
|
+}))
|
|
|
+
|
|
|
+import { attemptCompletionTool } from "../attemptCompletionTool"
|
|
|
+import { Task } from "../../task/Task"
|
|
|
+import * as vscode from "vscode"
|
|
|
+
|
|
|
+describe("attemptCompletionTool", () => {
|
|
|
+ let mockTask: Partial<Task>
|
|
|
+ let mockPushToolResult: ReturnType<typeof vi.fn>
|
|
|
+ let mockAskApproval: ReturnType<typeof vi.fn>
|
|
|
+ let mockHandleError: ReturnType<typeof vi.fn>
|
|
|
+ let mockRemoveClosingTag: ReturnType<typeof vi.fn>
|
|
|
+ let mockToolDescription: ReturnType<typeof vi.fn>
|
|
|
+ let mockAskFinishSubTaskApproval: ReturnType<typeof vi.fn>
|
|
|
+ let mockGetConfiguration: ReturnType<typeof vi.fn>
|
|
|
+
|
|
|
+ beforeEach(() => {
|
|
|
+ mockPushToolResult = vi.fn()
|
|
|
+ mockAskApproval = vi.fn()
|
|
|
+ mockHandleError = vi.fn()
|
|
|
+ mockRemoveClosingTag = vi.fn()
|
|
|
+ mockToolDescription = vi.fn()
|
|
|
+ mockAskFinishSubTaskApproval = vi.fn()
|
|
|
+ mockGetConfiguration = vi.fn(() => ({
|
|
|
+ get: vi.fn((key: string, defaultValue: any) => {
|
|
|
+ if (key === "preventCompletionWithOpenTodos") {
|
|
|
+ return defaultValue // Default to false unless overridden in test
|
|
|
+ }
|
|
|
+ return defaultValue
|
|
|
+ }),
|
|
|
+ }))
|
|
|
+
|
|
|
+ // Setup vscode mock
|
|
|
+ vi.mocked(vscode.workspace.getConfiguration).mockImplementation(mockGetConfiguration)
|
|
|
+
|
|
|
+ mockTask = {
|
|
|
+ consecutiveMistakeCount: 0,
|
|
|
+ recordToolError: vi.fn(),
|
|
|
+ todoList: undefined,
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ describe("todo list validation", () => {
|
|
|
+ it("should allow completion when there is no todo list", async () => {
|
|
|
+ const block: AttemptCompletionToolUse = {
|
|
|
+ type: "tool_use",
|
|
|
+ name: "attempt_completion",
|
|
|
+ params: { result: "Task completed successfully" },
|
|
|
+ partial: false,
|
|
|
+ }
|
|
|
+
|
|
|
+ mockTask.todoList = undefined
|
|
|
+
|
|
|
+ await attemptCompletionTool(
|
|
|
+ mockTask as Task,
|
|
|
+ block,
|
|
|
+ mockAskApproval,
|
|
|
+ mockHandleError,
|
|
|
+ mockPushToolResult,
|
|
|
+ mockRemoveClosingTag,
|
|
|
+ mockToolDescription,
|
|
|
+ mockAskFinishSubTaskApproval,
|
|
|
+ )
|
|
|
+
|
|
|
+ // Should not call pushToolResult with an error for empty todo list
|
|
|
+ expect(mockTask.consecutiveMistakeCount).toBe(0)
|
|
|
+ expect(mockTask.recordToolError).not.toHaveBeenCalled()
|
|
|
+ })
|
|
|
+
|
|
|
+ it("should allow completion when todo list is empty", async () => {
|
|
|
+ const block: AttemptCompletionToolUse = {
|
|
|
+ type: "tool_use",
|
|
|
+ name: "attempt_completion",
|
|
|
+ params: { result: "Task completed successfully" },
|
|
|
+ partial: false,
|
|
|
+ }
|
|
|
+
|
|
|
+ mockTask.todoList = []
|
|
|
+
|
|
|
+ await attemptCompletionTool(
|
|
|
+ mockTask as Task,
|
|
|
+ block,
|
|
|
+ mockAskApproval,
|
|
|
+ mockHandleError,
|
|
|
+ mockPushToolResult,
|
|
|
+ mockRemoveClosingTag,
|
|
|
+ mockToolDescription,
|
|
|
+ mockAskFinishSubTaskApproval,
|
|
|
+ )
|
|
|
+
|
|
|
+ expect(mockTask.consecutiveMistakeCount).toBe(0)
|
|
|
+ expect(mockTask.recordToolError).not.toHaveBeenCalled()
|
|
|
+ })
|
|
|
+
|
|
|
+ it("should allow completion when all todos are completed", async () => {
|
|
|
+ const block: AttemptCompletionToolUse = {
|
|
|
+ type: "tool_use",
|
|
|
+ name: "attempt_completion",
|
|
|
+ params: { result: "Task completed successfully" },
|
|
|
+ partial: false,
|
|
|
+ }
|
|
|
+
|
|
|
+ const completedTodos: TodoItem[] = [
|
|
|
+ { id: "1", content: "First task", status: "completed" },
|
|
|
+ { id: "2", content: "Second task", status: "completed" },
|
|
|
+ ]
|
|
|
+
|
|
|
+ mockTask.todoList = completedTodos
|
|
|
+
|
|
|
+ await attemptCompletionTool(
|
|
|
+ mockTask as Task,
|
|
|
+ block,
|
|
|
+ mockAskApproval,
|
|
|
+ mockHandleError,
|
|
|
+ mockPushToolResult,
|
|
|
+ mockRemoveClosingTag,
|
|
|
+ mockToolDescription,
|
|
|
+ mockAskFinishSubTaskApproval,
|
|
|
+ )
|
|
|
+
|
|
|
+ expect(mockTask.consecutiveMistakeCount).toBe(0)
|
|
|
+ expect(mockTask.recordToolError).not.toHaveBeenCalled()
|
|
|
+ })
|
|
|
+
|
|
|
+ it("should prevent completion when there are pending todos", async () => {
|
|
|
+ const block: AttemptCompletionToolUse = {
|
|
|
+ type: "tool_use",
|
|
|
+ name: "attempt_completion",
|
|
|
+ params: { result: "Task completed successfully" },
|
|
|
+ partial: false,
|
|
|
+ }
|
|
|
+
|
|
|
+ const todosWithPending: TodoItem[] = [
|
|
|
+ { id: "1", content: "First task", status: "completed" },
|
|
|
+ { id: "2", content: "Second task", status: "pending" },
|
|
|
+ ]
|
|
|
+
|
|
|
+ mockTask.todoList = todosWithPending
|
|
|
+
|
|
|
+ // Enable the setting to prevent completion with open todos
|
|
|
+ mockGetConfiguration.mockReturnValue({
|
|
|
+ get: vi.fn((key: string, defaultValue: any) => {
|
|
|
+ if (key === "preventCompletionWithOpenTodos") {
|
|
|
+ return true // Setting is enabled
|
|
|
+ }
|
|
|
+ return defaultValue
|
|
|
+ }),
|
|
|
+ })
|
|
|
+
|
|
|
+ await attemptCompletionTool(
|
|
|
+ mockTask as Task,
|
|
|
+ block,
|
|
|
+ mockAskApproval,
|
|
|
+ mockHandleError,
|
|
|
+ mockPushToolResult,
|
|
|
+ mockRemoveClosingTag,
|
|
|
+ mockToolDescription,
|
|
|
+ mockAskFinishSubTaskApproval,
|
|
|
+ )
|
|
|
+
|
|
|
+ expect(mockTask.consecutiveMistakeCount).toBe(1)
|
|
|
+ expect(mockTask.recordToolError).toHaveBeenCalledWith("attempt_completion")
|
|
|
+ expect(mockPushToolResult).toHaveBeenCalledWith(
|
|
|
+ expect.stringContaining("Cannot complete task while there are incomplete todos"),
|
|
|
+ )
|
|
|
+ })
|
|
|
+
|
|
|
+ it("should prevent completion when there are in-progress todos", async () => {
|
|
|
+ const block: AttemptCompletionToolUse = {
|
|
|
+ type: "tool_use",
|
|
|
+ name: "attempt_completion",
|
|
|
+ params: { result: "Task completed successfully" },
|
|
|
+ partial: false,
|
|
|
+ }
|
|
|
+
|
|
|
+ const todosWithInProgress: TodoItem[] = [
|
|
|
+ { id: "1", content: "First task", status: "completed" },
|
|
|
+ { id: "2", content: "Second task", status: "in_progress" },
|
|
|
+ ]
|
|
|
+
|
|
|
+ mockTask.todoList = todosWithInProgress
|
|
|
+
|
|
|
+ // Enable the setting to prevent completion with open todos
|
|
|
+ mockGetConfiguration.mockReturnValue({
|
|
|
+ get: vi.fn((key: string, defaultValue: any) => {
|
|
|
+ if (key === "preventCompletionWithOpenTodos") {
|
|
|
+ return true // Setting is enabled
|
|
|
+ }
|
|
|
+ return defaultValue
|
|
|
+ }),
|
|
|
+ })
|
|
|
+
|
|
|
+ await attemptCompletionTool(
|
|
|
+ mockTask as Task,
|
|
|
+ block,
|
|
|
+ mockAskApproval,
|
|
|
+ mockHandleError,
|
|
|
+ mockPushToolResult,
|
|
|
+ mockRemoveClosingTag,
|
|
|
+ mockToolDescription,
|
|
|
+ mockAskFinishSubTaskApproval,
|
|
|
+ )
|
|
|
+
|
|
|
+ expect(mockTask.consecutiveMistakeCount).toBe(1)
|
|
|
+ expect(mockTask.recordToolError).toHaveBeenCalledWith("attempt_completion")
|
|
|
+ expect(mockPushToolResult).toHaveBeenCalledWith(
|
|
|
+ expect.stringContaining("Cannot complete task while there are incomplete todos"),
|
|
|
+ )
|
|
|
+ })
|
|
|
+
|
|
|
+ it("should prevent completion when there are mixed incomplete todos", async () => {
|
|
|
+ const block: AttemptCompletionToolUse = {
|
|
|
+ type: "tool_use",
|
|
|
+ name: "attempt_completion",
|
|
|
+ params: { result: "Task completed successfully" },
|
|
|
+ partial: false,
|
|
|
+ }
|
|
|
+
|
|
|
+ const mixedTodos: TodoItem[] = [
|
|
|
+ { id: "1", content: "First task", status: "completed" },
|
|
|
+ { id: "2", content: "Second task", status: "pending" },
|
|
|
+ { id: "3", content: "Third task", status: "in_progress" },
|
|
|
+ ]
|
|
|
+
|
|
|
+ mockTask.todoList = mixedTodos
|
|
|
+
|
|
|
+ // Enable the setting to prevent completion with open todos
|
|
|
+ mockGetConfiguration.mockReturnValue({
|
|
|
+ get: vi.fn((key: string, defaultValue: any) => {
|
|
|
+ if (key === "preventCompletionWithOpenTodos") {
|
|
|
+ return true // Setting is enabled
|
|
|
+ }
|
|
|
+ return defaultValue
|
|
|
+ }),
|
|
|
+ })
|
|
|
+
|
|
|
+ await attemptCompletionTool(
|
|
|
+ mockTask as Task,
|
|
|
+ block,
|
|
|
+ mockAskApproval,
|
|
|
+ mockHandleError,
|
|
|
+ mockPushToolResult,
|
|
|
+ mockRemoveClosingTag,
|
|
|
+ mockToolDescription,
|
|
|
+ mockAskFinishSubTaskApproval,
|
|
|
+ )
|
|
|
+
|
|
|
+ expect(mockTask.consecutiveMistakeCount).toBe(1)
|
|
|
+ expect(mockTask.recordToolError).toHaveBeenCalledWith("attempt_completion")
|
|
|
+ expect(mockPushToolResult).toHaveBeenCalledWith(
|
|
|
+ expect.stringContaining("Cannot complete task while there are incomplete todos"),
|
|
|
+ )
|
|
|
+ })
|
|
|
+
|
|
|
+ it("should allow completion when setting is disabled even with incomplete todos", async () => {
|
|
|
+ const block: AttemptCompletionToolUse = {
|
|
|
+ type: "tool_use",
|
|
|
+ name: "attempt_completion",
|
|
|
+ params: { result: "Task completed successfully" },
|
|
|
+ partial: false,
|
|
|
+ }
|
|
|
+
|
|
|
+ const todosWithPending: TodoItem[] = [
|
|
|
+ { id: "1", content: "First task", status: "completed" },
|
|
|
+ { id: "2", content: "Second task", status: "pending" },
|
|
|
+ ]
|
|
|
+
|
|
|
+ mockTask.todoList = todosWithPending
|
|
|
+
|
|
|
+ // Ensure the setting is disabled (default behavior)
|
|
|
+ mockGetConfiguration.mockReturnValue({
|
|
|
+ get: vi.fn((key: string, defaultValue: any) => {
|
|
|
+ if (key === "preventCompletionWithOpenTodos") {
|
|
|
+ return false // Setting is disabled
|
|
|
+ }
|
|
|
+ return defaultValue
|
|
|
+ }),
|
|
|
+ })
|
|
|
+
|
|
|
+ await attemptCompletionTool(
|
|
|
+ mockTask as Task,
|
|
|
+ block,
|
|
|
+ mockAskApproval,
|
|
|
+ mockHandleError,
|
|
|
+ mockPushToolResult,
|
|
|
+ mockRemoveClosingTag,
|
|
|
+ mockToolDescription,
|
|
|
+ mockAskFinishSubTaskApproval,
|
|
|
+ )
|
|
|
+
|
|
|
+ // Should not prevent completion when setting is disabled
|
|
|
+ expect(mockTask.consecutiveMistakeCount).toBe(0)
|
|
|
+ expect(mockTask.recordToolError).not.toHaveBeenCalled()
|
|
|
+ expect(mockPushToolResult).not.toHaveBeenCalledWith(
|
|
|
+ expect.stringContaining("Cannot complete task while there are incomplete todos"),
|
|
|
+ )
|
|
|
+ })
|
|
|
+
|
|
|
+ it("should prevent completion when setting is enabled with incomplete todos", async () => {
|
|
|
+ const block: AttemptCompletionToolUse = {
|
|
|
+ type: "tool_use",
|
|
|
+ name: "attempt_completion",
|
|
|
+ params: { result: "Task completed successfully" },
|
|
|
+ partial: false,
|
|
|
+ }
|
|
|
+
|
|
|
+ const todosWithPending: TodoItem[] = [
|
|
|
+ { id: "1", content: "First task", status: "completed" },
|
|
|
+ { id: "2", content: "Second task", status: "pending" },
|
|
|
+ ]
|
|
|
+
|
|
|
+ mockTask.todoList = todosWithPending
|
|
|
+
|
|
|
+ // Enable the setting
|
|
|
+ mockGetConfiguration.mockReturnValue({
|
|
|
+ get: vi.fn((key: string, defaultValue: any) => {
|
|
|
+ if (key === "preventCompletionWithOpenTodos") {
|
|
|
+ return true // Setting is enabled
|
|
|
+ }
|
|
|
+ return defaultValue
|
|
|
+ }),
|
|
|
+ })
|
|
|
+
|
|
|
+ await attemptCompletionTool(
|
|
|
+ mockTask as Task,
|
|
|
+ block,
|
|
|
+ mockAskApproval,
|
|
|
+ mockHandleError,
|
|
|
+ mockPushToolResult,
|
|
|
+ mockRemoveClosingTag,
|
|
|
+ mockToolDescription,
|
|
|
+ mockAskFinishSubTaskApproval,
|
|
|
+ )
|
|
|
+
|
|
|
+ // Should prevent completion when setting is enabled and there are incomplete todos
|
|
|
+ expect(mockTask.consecutiveMistakeCount).toBe(1)
|
|
|
+ expect(mockTask.recordToolError).toHaveBeenCalledWith("attempt_completion")
|
|
|
+ expect(mockPushToolResult).toHaveBeenCalledWith(
|
|
|
+ expect.stringContaining("Cannot complete task while there are incomplete todos"),
|
|
|
+ )
|
|
|
+ })
|
|
|
+
|
|
|
+ it("should allow completion when setting is enabled but all todos are completed", async () => {
|
|
|
+ const block: AttemptCompletionToolUse = {
|
|
|
+ type: "tool_use",
|
|
|
+ name: "attempt_completion",
|
|
|
+ params: { result: "Task completed successfully" },
|
|
|
+ partial: false,
|
|
|
+ }
|
|
|
+
|
|
|
+ const completedTodos: TodoItem[] = [
|
|
|
+ { id: "1", content: "First task", status: "completed" },
|
|
|
+ { id: "2", content: "Second task", status: "completed" },
|
|
|
+ ]
|
|
|
+
|
|
|
+ mockTask.todoList = completedTodos
|
|
|
+
|
|
|
+ // Enable the setting
|
|
|
+ mockGetConfiguration.mockReturnValue({
|
|
|
+ get: vi.fn((key: string, defaultValue: any) => {
|
|
|
+ if (key === "preventCompletionWithOpenTodos") {
|
|
|
+ return true // Setting is enabled
|
|
|
+ }
|
|
|
+ return defaultValue
|
|
|
+ }),
|
|
|
+ })
|
|
|
+
|
|
|
+ await attemptCompletionTool(
|
|
|
+ mockTask as Task,
|
|
|
+ block,
|
|
|
+ mockAskApproval,
|
|
|
+ mockHandleError,
|
|
|
+ mockPushToolResult,
|
|
|
+ mockRemoveClosingTag,
|
|
|
+ mockToolDescription,
|
|
|
+ mockAskFinishSubTaskApproval,
|
|
|
+ )
|
|
|
+
|
|
|
+ // Should allow completion when setting is enabled but all todos are completed
|
|
|
+ expect(mockTask.consecutiveMistakeCount).toBe(0)
|
|
|
+ expect(mockTask.recordToolError).not.toHaveBeenCalled()
|
|
|
+ expect(mockPushToolResult).not.toHaveBeenCalledWith(
|
|
|
+ expect.stringContaining("Cannot complete task while there are incomplete todos"),
|
|
|
+ )
|
|
|
+ })
|
|
|
+ })
|
|
|
+})
|