Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 110 additions & 0 deletions e2e/src/suite/task.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as assert from "assert"
import * as vscode from "vscode"

suite("Roo Code Task", () => {
test("Should handle prompt and response correctly", async function () {
Expand Down Expand Up @@ -48,4 +49,113 @@ suite("Roo Code Task", () => {
"Did not receive expected response containing 'My name is Roo'",
)
})

test("Should handle subtask cancellation and resumption correctly", async function () {
this.timeout(60000) // Increase timeout for this test
const interval = 1000

if (!globalThis.extension) {
assert.fail("Extension not found")
}

// Ensure the webview is launched
await ensureWebviewLaunched(30000, interval)

// Set up required global state
await globalThis.provider.updateGlobalState("mode", "Code")
await globalThis.provider.updateGlobalState("alwaysAllowModeSwitch", true)
await globalThis.provider.updateGlobalState("alwaysAllowSubtasks", true)
await globalThis.provider.updateGlobalState("autoApprovalEnabled", true)

// 1. Start a parent task that will create a subtask
await globalThis.api.startNewTask(
"You are the parent task. Create a subtask by using the new_task tool with the message 'You are the subtask'. " +
"After creating the subtask, wait for it to complete and then respond with 'Parent task resumed'.",
)

// Wait for the parent task to use the new_task tool
await waitForToolUse("new_task", 30000, interval)

// Wait for the subtask to be created and start responding
await waitForMessage("You are the subtask", 10000, interval)

// 3. Cancel the current task (which should be the subtask)
await globalThis.provider.cancelTask()

// 4. Check if the parent task is still waiting (not resumed)
// We need to wait a bit to ensure any task resumption would have happened
await new Promise((resolve) => setTimeout(resolve, 5000))

// The parent task should not have resumed yet, so we shouldn't see "Parent task resumed"
assert.ok(
!globalThis.provider.messages.some(
({ type, text }) => type === "say" && text?.includes("Parent task resumed"),
),
"Parent task should not have resumed after subtask cancellation",
)

// 5. Start a new task with the same message as the subtask
await globalThis.api.startNewTask("You are the subtask")

// Wait for the subtask to complete
await waitForMessage("Task complete", 20000, interval)

// 6. Verify that the parent task is still not resumed
// We need to wait a bit to ensure any task resumption would have happened
await new Promise((resolve) => setTimeout(resolve, 5000))

// The parent task should still not have resumed
assert.ok(
!globalThis.provider.messages.some(
({ type, text }) => type === "say" && text?.includes("Parent task resumed"),
),
"Parent task should not have resumed after subtask completion",
)

// Clean up - cancel all tasks
await globalThis.provider.cancelTask()
})
})

// Helper functions
async function ensureWebviewLaunched(timeout: number, interval: number): Promise<void> {
const startTime = Date.now()
while (Date.now() - startTime < timeout) {
if (globalThis.provider.viewLaunched) {
return
}
await new Promise((resolve) => setTimeout(resolve, interval))
}
throw new Error("Webview failed to launch within timeout")
}

async function waitForToolUse(toolName: string, timeout: number, interval: number): Promise<void> {
const startTime = Date.now()
while (Date.now() - startTime < timeout) {
const messages = globalThis.provider.messages
if (
messages.some(
(message) =>
message.type === "say" && message.say === "tool" && message.text && message.text.includes(toolName),
)
) {
return
}
await new Promise((resolve) => setTimeout(resolve, interval))
}
throw new Error(`Tool ${toolName} was not used within timeout`)
}

async function waitForMessage(messageContent: string, timeout: number, interval: number): Promise<void> {
const startTime = Date.now()
while (Date.now() - startTime < timeout) {
const messages = globalThis.provider.messages
if (
messages.some((message) => message.type === "say" && message.text && message.text.includes(messageContent))
) {
return
}
await new Promise((resolve) => setTimeout(resolve, interval))
}
throw new Error(`Message containing "${messageContent}" not found within timeout`)
}
24 changes: 22 additions & 2 deletions src/core/webview/ClineProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1933,8 +1933,16 @@ export class ClineProvider implements vscode.WebviewViewProvider {

async cancelTask() {
if (this.getCurrentCline()) {
const { historyItem } = await this.getTaskWithId(this.getCurrentCline()!.taskId)
this.getCurrentCline()!.abortTask()
const currentCline = this.getCurrentCline()!
const { historyItem } = await this.getTaskWithId(currentCline.taskId)

// Store parent task information if this is a subtask
// Check if this is a subtask by seeing if it has a parent task
const parentTask = currentCline.getParentTask()
const isSubTask = parentTask !== undefined
const rootTask = isSubTask ? currentCline.getRootTask() : undefined

currentCline.abortTask()

await pWaitFor(
() =>
Expand All @@ -1961,6 +1969,18 @@ export class ClineProvider implements vscode.WebviewViewProvider {

// Clears task again, so we need to abortTask manually above.
await this.initClineWithHistoryItem(historyItem)

// Restore parent-child relationship if this was a subtask
if (isSubTask && this.getCurrentCline() && parentTask) {
this.getCurrentCline()!.setSubTask()
this.getCurrentCline()!.setParentTask(parentTask)
if (rootTask) {
this.getCurrentCline()!.setRootTask(rootTask)
}
this.log(
`[subtasks] Restored parent-child relationship for task: ${this.getCurrentCline()!.getTaskNumber()}`,
)
}
}
}

Expand Down
Loading