Skip to content

Commit 7845f96

Browse files
authored
Merge pull request #1612 from RooVetGit/cte/task-events
Add Cline events and roofactor subtasks slightly
2 parents d34b5e0 + 4b23644 commit 7845f96

File tree

19 files changed

+537
-591
lines changed

19 files changed

+537
-591
lines changed

e2e/src/suite/extension.test.ts

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,6 @@ suite("Roo Code Extension", () => {
99
})
1010

1111
test("Commands should be registered", async () => {
12-
const timeout = 10 * 1_000
13-
const interval = 1_000
14-
const startTime = Date.now()
15-
1612
const expectedCommands = [
1713
"roo-cline.plusButtonClicked",
1814
"roo-cline.mcpButtonClicked",
@@ -25,23 +21,6 @@ suite("Roo Code Extension", () => {
2521
"roo-cline.improveCode",
2622
]
2723

28-
while (Date.now() - startTime < timeout) {
29-
const commands = await vscode.commands.getCommands(true)
30-
const missingCommands = []
31-
32-
for (const cmd of expectedCommands) {
33-
if (!commands.includes(cmd)) {
34-
missingCommands.push(cmd)
35-
}
36-
}
37-
38-
if (missingCommands.length === 0) {
39-
break
40-
}
41-
42-
await new Promise((resolve) => setTimeout(resolve, interval))
43-
}
44-
4524
const commands = await vscode.commands.getCommands(true)
4625

4726
for (const cmd of expectedCommands) {

e2e/src/suite/index.ts

Lines changed: 30 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -8,55 +8,39 @@ import { RooCodeAPI } from "../../../src/exports/roo-code"
88
import { waitUntilReady } from "./utils"
99

1010
declare global {
11-
var extension: vscode.Extension<RooCodeAPI> | undefined
1211
var api: RooCodeAPI
1312
}
1413

1514
export async function run() {
16-
const mocha = new Mocha({ ui: "tdd", timeout: 300_000 })
17-
const testsRoot = path.resolve(__dirname, "..")
18-
19-
try {
20-
// Find all test files.
21-
const files = await glob("**/**.test.js", { cwd: testsRoot })
22-
files.forEach((f: string) => mocha.addFile(path.resolve(testsRoot, f)))
23-
24-
const extension = vscode.extensions.getExtension<RooCodeAPI>("RooVeterinaryInc.roo-cline")
25-
26-
if (!extension) {
27-
throw new Error("Extension not found")
28-
}
29-
30-
const api = extension.isActive ? extension.exports : await extension.activate()
31-
32-
await api.setConfiguration({
33-
apiProvider: "openrouter",
34-
openRouterApiKey: process.env.OPENROUTER_API_KEY!,
35-
openRouterModelId: "anthropic/claude-3.5-sonnet",
36-
})
37-
38-
await waitUntilReady(api)
39-
40-
globalThis.api = api
41-
globalThis.extension = extension
42-
43-
return new Promise<void>((resolve, reject) => {
44-
try {
45-
mocha.run((failures: number) => {
46-
if (failures > 0) {
47-
reject(new Error(`${failures} tests failed.`))
48-
} else {
49-
resolve()
50-
}
51-
})
52-
} catch (err) {
53-
console.error(err)
54-
reject(err)
55-
}
56-
})
57-
} catch (err) {
58-
console.error("Error while running tests:")
59-
console.error(err)
60-
throw err
15+
const extension = vscode.extensions.getExtension<RooCodeAPI>("RooVeterinaryInc.roo-cline")
16+
17+
if (!extension) {
18+
throw new Error("Extension not found")
6119
}
20+
21+
// Activate the extension if it's not already active.
22+
const api = extension.isActive ? extension.exports : await extension.activate()
23+
24+
// TODO: We might want to support a "free" model out of the box so
25+
// contributors can run the tests locally without having to pay.
26+
await api.setConfiguration({
27+
apiProvider: "openrouter",
28+
openRouterApiKey: process.env.OPENROUTER_API_KEY!,
29+
openRouterModelId: "anthropic/claude-3.5-sonnet",
30+
})
31+
32+
await waitUntilReady({ api })
33+
34+
// Expose the API to the tests.
35+
globalThis.api = api
36+
37+
// Add all the tests to the runner.
38+
const mocha = new Mocha({ ui: "tdd", timeout: 300_000 })
39+
const cwd = path.resolve(__dirname, "..")
40+
;(await glob("**/**.test.js", { cwd })).forEach((testFile) => mocha.addFile(path.resolve(cwd, testFile)))
41+
42+
// Let's go!
43+
return new Promise<void>((resolve, reject) =>
44+
mocha.run((failures) => (failures === 0 ? resolve() : reject(new Error(`${failures} tests failed.`)))),
45+
)
6246
}

e2e/src/suite/modes.test.ts

Lines changed: 16 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,63 +1,34 @@
11
import * as assert from "assert"
22

3-
import { waitForMessage } from "./utils"
3+
import { waitForMessage, getMessage } from "./utils"
44

55
suite("Roo Code Modes", () => {
66
test("Should handle switching modes correctly", async function () {
7-
const timeout = 300_000
87
const api = globalThis.api
98

10-
const testPrompt =
9+
let prompt =
1110
"For each mode (Code, Architect, Ask) respond with the mode name and what it specializes in after switching to that mode, do not start with the current mode, be sure to say 'I AM DONE' after the task is complete."
1211

1312
await api.setConfiguration({ mode: "Code", alwaysAllowModeSwitch: true, autoApprovalEnabled: true })
14-
await api.startNewTask(testPrompt)
13+
let taskId = await api.startNewTask(prompt)
14+
await waitForMessage({ api, taskId, include: "I AM DONE", exclude: "be sure to say", timeout: 300_000 })
1515

16-
await waitForMessage(api, { include: "I AM DONE", exclude: "be sure to say", timeout })
16+
// Start grading portion of test to grade the response from 1 to 10.
17+
prompt = `Given this prompt: ${prompt} grade the response from 1 to 10 in the format of "Grade: (1-10)": ${api
18+
.getMessages(taskId)
19+
.filter(({ type }) => type === "say")
20+
.map(({ text }) => text ?? "")
21+
.join("\n")}\nBe sure to say 'I AM DONE GRADING' after the task is complete.`
1722

18-
if (api.getMessages().length === 0) {
19-
assert.fail("No messages received")
20-
}
21-
22-
// Log the messages to the console.
23-
api.getMessages().forEach(({ type, text }) => {
24-
if (type === "say") {
25-
console.log(text)
26-
}
27-
})
28-
29-
// Start Grading Portion of test to grade the response from 1 to 10.
3023
await api.setConfiguration({ mode: "Ask" })
24+
taskId = await api.startNewTask(prompt)
25+
await waitForMessage({ api, taskId, include: "I AM DONE GRADING", exclude: "be sure to say" })
3126

32-
let output = api
33-
.getMessages()
34-
.map(({ type, text }) => (type === "say" ? text : ""))
35-
.join("\n")
36-
37-
await api.startNewTask(
38-
`Given this prompt: ${testPrompt} grade the response from 1 to 10 in the format of "Grade: (1-10)": ${output}\nBe sure to say 'I AM DONE GRADING' after the task is complete.`,
27+
const match = getMessage({ api, taskId, include: "Grade:", exclude: "Grade: (1-10)" })?.text?.match(
28+
/Grade: (\d+)/,
3929
)
4030

41-
await waitForMessage(api, { include: "I AM DONE GRADING", exclude: "be sure to say", timeout })
42-
43-
if (api.getMessages().length === 0) {
44-
assert.fail("No messages received")
45-
}
46-
47-
api.getMessages().forEach(({ type, text }) => {
48-
if (type === "say" && text?.includes("Grade:")) {
49-
console.log(text)
50-
}
51-
})
52-
53-
const gradeMessage = api
54-
.getMessages()
55-
.find(
56-
({ type, text }) => type === "say" && !text?.includes("Grade: (1-10)") && text?.includes("Grade:"),
57-
)?.text
58-
59-
const gradeMatch = gradeMessage?.match(/Grade: (\d+)/)
60-
const gradeNum = gradeMatch ? parseInt(gradeMatch[1]) : undefined
61-
assert.ok(gradeNum !== undefined && gradeNum >= 7 && gradeNum <= 10, "Grade must be between 7 and 10")
31+
const score = parseInt(match?.[1] ?? "0")
32+
assert.ok(score >= 7 && score <= 10, "Grade must be between 7 and 10.")
6233
})
6334
})

e2e/src/suite/subtasks.test.ts

Lines changed: 33 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,70 @@
11
import * as assert from "assert"
22

3-
import { sleep, waitForToolUse, waitForMessage } from "./utils"
3+
import { sleep, waitForMessage, waitFor, getMessage } from "./utils"
44

55
suite("Roo Code Subtasks", () => {
6-
test.skip("Should handle subtask cancellation and resumption correctly", async function () {
6+
test("Should handle subtask cancellation and resumption correctly", async function () {
77
const api = globalThis.api
88

99
await api.setConfiguration({
1010
mode: "Code",
1111
alwaysAllowModeSwitch: true,
1212
alwaysAllowSubtasks: true,
1313
autoApprovalEnabled: true,
14+
enableCheckpoints: false,
1415
})
1516

17+
const childPrompt = "You are a calculator. Respond only with numbers. What is the square root of 9?"
18+
1619
// Start a parent task that will create a subtask.
17-
await api.startNewTask(
20+
const parentTaskId = await api.startNewTask(
1821
"You are the parent task. " +
19-
"Create a subtask by using the new_task tool with the message 'You are the subtask'. " +
20-
"After creating the subtask, wait for it to complete and then respond with 'Parent task resumed'.",
22+
`Create a subtask by using the new_task tool with the message '${childPrompt}'.` +
23+
"After creating the subtask, wait for it to complete and then respond 'Parent task resumed'.",
2124
)
2225

23-
await waitForToolUse(api, "new_task")
26+
let subTaskId: string | undefined = undefined
2427

25-
// Cancel the current task (which should be the subtask).
26-
await api.cancelTask()
28+
// Wait for the subtask to be spawned and then cancel it.
29+
api.on("taskSpawned", (taskId) => (subTaskId = taskId))
30+
await waitFor(() => !!subTaskId)
31+
await sleep(2_000) // Give the task a chance to start and populate the history.
32+
await api.cancelCurrentTask()
2733

28-
// Check if the parent task is still waiting (not resumed). We need to
29-
// wait a bit to ensure any task resumption would have happened.
34+
// Wait a bit to ensure any task resumption would have happened.
3035
await sleep(5_000)
3136

3237
// The parent task should not have resumed yet, so we shouldn't see
3338
// "Parent task resumed".
3439
assert.ok(
35-
!api.getMessages().some(({ type, text }) => type === "say" && text?.includes("Parent task resumed")),
36-
"Parent task should not have resumed after subtask cancellation.",
40+
getMessage({
41+
api,
42+
taskId: parentTaskId,
43+
include: "Parent task resumed",
44+
exclude: "You are the parent task",
45+
}) === undefined,
46+
"Parent task should not have resumed after subtask cancellation",
3747
)
3848

3949
// Start a new task with the same message as the subtask.
40-
await api.startNewTask("You are the subtask")
41-
42-
// Wait for the subtask to complete.
43-
await waitForMessage(api, { include: "Task complete" })
50+
const anotherTaskId = await api.startNewTask(childPrompt)
51+
await waitForMessage({ taskId: anotherTaskId, api, include: "3" })
4452

45-
// Verify that the parent task is still not resumed. We need to wait a
46-
// bit to ensure any task resumption would have happened.
53+
// Wait a bit to ensure any task resumption would have happened.
4754
await sleep(5_000)
4855

4956
// The parent task should still not have resumed.
5057
assert.ok(
51-
!api.getMessages().some(({ type, text }) => type === "say" && text?.includes("Parent task resumed")),
52-
"Parent task should not have resumed after subtask completion.",
58+
getMessage({
59+
api,
60+
taskId: parentTaskId,
61+
include: "Parent task resumed",
62+
exclude: "You are the parent task",
63+
}) === undefined,
64+
"Parent task should not have resumed after subtask cancellation",
5365
)
5466

5567
// Clean up - cancel all tasks.
56-
await api.cancelTask()
68+
await api.cancelCurrentTask()
5769
})
5870
})

e2e/src/suite/task.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ suite("Roo Code Task", () => {
44
test("Should handle prompt and response correctly", async function () {
55
const api = globalThis.api
66
await api.setConfiguration({ mode: "Ask", alwaysAllowModeSwitch: true, autoApprovalEnabled: true })
7-
await api.startNewTask("Hello world, what is your name? Respond with 'My name is ...'")
8-
await waitForMessage(api, { include: "My name is Roo" })
7+
const taskId = await api.startNewTask("Hello world, what is your name? Respond with 'My name is ...'")
8+
await waitForMessage({ api, taskId, include: "My name is Roo" })
99
})
1010
})

e2e/src/suite/utils.ts

Lines changed: 36 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -41,36 +41,51 @@ export const waitFor = (
4141
])
4242
}
4343

44-
export const waitUntilReady = async (api: RooCodeAPI, { timeout = 10_000, interval = 250 }: WaitForOptions = {}) => {
44+
type WaitUntilReadyOptions = WaitForOptions & {
45+
api: RooCodeAPI
46+
}
47+
48+
export const waitUntilReady = async ({ api, ...options }: WaitUntilReadyOptions) => {
4549
await vscode.commands.executeCommand("roo-cline.SidebarProvider.focus")
46-
await waitFor(api.isReady, { timeout, interval })
50+
await waitFor(() => api.isReady(), options)
51+
}
52+
53+
type WaitForToolUseOptions = WaitUntilReadyOptions & {
54+
taskId: string
55+
toolName: string
4756
}
4857

49-
export const waitForToolUse = async (api: RooCodeAPI, toolName: string, options: WaitForOptions = {}) =>
58+
export const waitForToolUse = async ({ api, taskId, toolName, ...options }: WaitForToolUseOptions) =>
5059
waitFor(
5160
() =>
5261
api
53-
.getMessages()
62+
.getMessages(taskId)
5463
.some(({ type, say, text }) => type === "say" && say === "tool" && text && text.includes(toolName)),
5564
options,
5665
)
5766

58-
export const waitForMessage = async (
59-
api: RooCodeAPI,
60-
options: WaitForOptions & { include: string; exclude?: string },
61-
) =>
62-
waitFor(
63-
() =>
64-
api
65-
.getMessages()
66-
.some(
67-
({ type, text }) =>
68-
type === "say" &&
69-
text &&
70-
text.includes(options.include) &&
71-
(!options.exclude || !text.includes(options.exclude)),
72-
),
73-
options,
74-
)
67+
type WaitForMessageOptions = WaitUntilReadyOptions & {
68+
taskId: string
69+
include: string
70+
exclude?: string
71+
}
72+
73+
export const waitForMessage = async ({ api, taskId, include, exclude, ...options }: WaitForMessageOptions) =>
74+
waitFor(() => !!getMessage({ api, taskId, include, exclude }), options)
75+
76+
type GetMessageOptions = {
77+
api: RooCodeAPI
78+
taskId: string
79+
include: string
80+
exclude?: string
81+
}
82+
83+
export const getMessage = ({ api, taskId, include, exclude }: GetMessageOptions) =>
84+
api
85+
.getMessages(taskId)
86+
.find(
87+
({ type, text }) =>
88+
type === "say" && text && text.includes(include) && (!exclude || !text.includes(exclude)),
89+
)
7590

7691
export const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))

0 commit comments

Comments
 (0)