Skip to content

Commit c609856

Browse files
committed
Fix e2e tests
1 parent 7845f96 commit c609856

File tree

8 files changed

+143
-113
lines changed

8 files changed

+143
-113
lines changed

e2e/src/suite/modes.test.ts

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

3-
import { waitForMessage, getMessage } from "./utils"
3+
import { getCompletion, getMessage, sleep, waitForCompletion, waitUntilAborted } from "./utils"
44

55
suite("Roo Code Modes", () => {
66
test("Should handle switching modes correctly", async function () {
77
const api = globalThis.api
88

9-
let prompt =
10-
"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."
9+
/**
10+
* Switch modes.
11+
*/
12+
13+
const switchModesPrompt =
14+
"For each mode (Code, Architect, Ask) respond with the mode name and what it specializes in after switching to that mode. " +
15+
"Do not start with the current mode."
1116

1217
await api.setConfiguration({ mode: "Code", alwaysAllowModeSwitch: true, autoApprovalEnabled: true })
13-
let taskId = await api.startNewTask(prompt)
14-
await waitForMessage({ api, taskId, include: "I AM DONE", exclude: "be sure to say", timeout: 300_000 })
18+
const switchModesTaskId = await api.startNewTask(switchModesPrompt)
19+
await waitForCompletion({ api, taskId: switchModesTaskId, timeout: 60_000 })
1520

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.`
21+
/**
22+
* Grade the response.
23+
*/
2224

23-
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" })
25+
const gradePrompt =
26+
`Given this prompt: ${switchModesPrompt} grade the response from 1 to 10 in the format of "Grade: (1-10)": ` +
27+
api
28+
.getMessages(switchModesTaskId)
29+
.filter(({ type }) => type === "say")
30+
.map(({ text }) => text ?? "")
31+
.join("\n")
2632

27-
const match = getMessage({ api, taskId, include: "Grade:", exclude: "Grade: (1-10)" })?.text?.match(
28-
/Grade: (\d+)/,
29-
)
33+
await api.setConfiguration({ mode: "Ask" })
34+
const gradeTaskId = await api.startNewTask(gradePrompt)
35+
await waitForCompletion({ api, taskId: gradeTaskId, timeout: 60_000 })
3036

37+
const completion = getCompletion({ api, taskId: gradeTaskId })
38+
const match = completion?.text?.match(/Grade: (\d+)/)
3139
const score = parseInt(match?.[1] ?? "0")
32-
assert.ok(score >= 7 && score <= 10, "Grade must be between 7 and 10.")
40+
assert.ok(score >= 7 && score <= 10, `Grade must be between 7 and 10 - ${completion?.text}`)
41+
42+
await api.cancelCurrentTask()
3343
})
3444
})

e2e/src/suite/subtasks.test.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as assert from "assert"
22

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

55
suite("Roo Code Subtasks", () => {
66
test("Should handle subtask cancellation and resumption correctly", async function () {
@@ -23,16 +23,16 @@ suite("Roo Code Subtasks", () => {
2323
"After creating the subtask, wait for it to complete and then respond 'Parent task resumed'.",
2424
)
2525

26-
let subTaskId: string | undefined = undefined
26+
let spawnedTaskId: string | undefined = undefined
2727

2828
// Wait for the subtask to be spawned and then cancel it.
29-
api.on("taskSpawned", (taskId) => (subTaskId = taskId))
30-
await waitFor(() => !!subTaskId)
29+
api.on("taskSpawned", (_, childTaskId) => (spawnedTaskId = childTaskId))
30+
await waitFor(() => !!spawnedTaskId)
3131
await sleep(2_000) // Give the task a chance to start and populate the history.
3232
await api.cancelCurrentTask()
3333

3434
// Wait a bit to ensure any task resumption would have happened.
35-
await sleep(5_000)
35+
await sleep(2_000)
3636

3737
// The parent task should not have resumed yet, so we shouldn't see
3838
// "Parent task resumed".
@@ -48,10 +48,10 @@ suite("Roo Code Subtasks", () => {
4848

4949
// Start a new task with the same message as the subtask.
5050
const anotherTaskId = await api.startNewTask(childPrompt)
51-
await waitForMessage({ taskId: anotherTaskId, api, include: "3" })
51+
await waitForCompletion({ api, taskId: anotherTaskId })
5252

5353
// Wait a bit to ensure any task resumption would have happened.
54-
await sleep(5_000)
54+
await sleep(2_000)
5555

5656
// The parent task should still not have resumed.
5757
assert.ok(
@@ -65,6 +65,7 @@ suite("Roo Code Subtasks", () => {
6565
)
6666

6767
// Clean up - cancel all tasks.
68-
await api.cancelCurrentTask()
68+
await api.clearCurrentTask()
69+
await waitForCompletion({ api, taskId: parentTaskId })
6970
})
7071
})

e2e/src/suite/utils.ts

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -50,19 +50,27 @@ export const waitUntilReady = async ({ api, ...options }: WaitUntilReadyOptions)
5050
await waitFor(() => api.isReady(), options)
5151
}
5252

53-
type WaitForToolUseOptions = WaitUntilReadyOptions & {
53+
type WaitUntilAbortedOptions = WaitForOptions & {
54+
api: RooCodeAPI
5455
taskId: string
55-
toolName: string
5656
}
5757

58-
export const waitForToolUse = async ({ api, taskId, toolName, ...options }: WaitForToolUseOptions) =>
59-
waitFor(
60-
() =>
61-
api
62-
.getMessages(taskId)
63-
.some(({ type, say, text }) => type === "say" && say === "tool" && text && text.includes(toolName)),
64-
options,
65-
)
58+
export const waitUntilAborted = async ({ api, taskId, ...options }: WaitUntilAbortedOptions) => {
59+
const set = new Set<string>()
60+
api.on("taskAborted", (taskId) => set.add(taskId))
61+
await waitFor(() => set.has(taskId), options)
62+
}
63+
64+
export const waitForCompletion = async ({
65+
api,
66+
taskId,
67+
...options
68+
}: WaitUntilReadyOptions & {
69+
taskId: string
70+
}) => waitFor(() => !!getCompletion({ api, taskId }), options)
71+
72+
export const getCompletion = ({ api, taskId }: { api: RooCodeAPI; taskId: string }) =>
73+
api.getMessages(taskId).find(({ say, partial }) => say === "completion_result" && partial === false)
6674

6775
type WaitForMessageOptions = WaitUntilReadyOptions & {
6876
taskId: string

package-lock.json

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/core/Cline.ts

Lines changed: 39 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ export type ClineOptions = {
113113

114114
export class Cline extends EventEmitter<ClineEvents> {
115115
readonly taskId: string
116+
readonly instanceId: string
116117

117118
// Subtasks
118119
readonly rootTask: Cline | undefined = undefined
@@ -196,6 +197,7 @@ export class Cline extends EventEmitter<ClineEvents> {
196197
})
197198

198199
this.taskId = historyItem ? historyItem.id : crypto.randomUUID()
200+
this.instanceId = crypto.randomUUID().slice(0, 8)
199201
this.taskNumber = -1
200202
this.apiConfiguration = apiConfiguration
201203
this.api = buildApiHandler(apiConfiguration)
@@ -409,9 +411,11 @@ export class Cline extends EventEmitter<ClineEvents> {
409411
// simply removes the reference to this instance, but the instance is
410412
// still alive until this promise resolves or rejects.)
411413
if (this.abort) {
412-
throw new Error(`Task: ${this.taskNumber} Roo Code instance aborted (#1)`)
414+
throw new Error(`[Cline#ask] task ${this.taskId}.${this.instanceId} aborted`)
413415
}
416+
414417
let askTs: number
418+
415419
if (partial !== undefined) {
416420
const lastMessage = this.clineMessages.at(-1)
417421
const isUpdatingPreviousPartial =
@@ -509,7 +513,7 @@ export class Cline extends EventEmitter<ClineEvents> {
509513
progressStatus?: ToolProgressStatus,
510514
): Promise<undefined> {
511515
if (this.abort) {
512-
throw new Error(`Task: ${this.taskNumber} Roo Code instance aborted (#2)`)
516+
throw new Error(`[Cline#say] task ${this.taskId}.${this.instanceId} aborted`)
513517
}
514518

515519
if (partial !== undefined) {
@@ -584,6 +588,9 @@ export class Cline extends EventEmitter<ClineEvents> {
584588
this.isInitialized = true
585589

586590
let imageBlocks: Anthropic.ImageBlockParam[] = formatResponse.imageBlocks(images)
591+
592+
console.log(`[subtasks] task ${this.taskId}.${this.instanceId} starting`)
593+
587594
await this.initiateTaskLoop([
588595
{
589596
type: "text",
@@ -841,6 +848,9 @@ export class Cline extends EventEmitter<ClineEvents> {
841848
}
842849

843850
await this.overwriteApiConversationHistory(modifiedApiConversationHistory)
851+
852+
console.log(`[subtasks] task ${this.taskId}.${this.instanceId} resuming from history item`)
853+
844854
await this.initiateTaskLoop(newUserContent)
845855
}
846856

@@ -857,31 +867,36 @@ export class Cline extends EventEmitter<ClineEvents> {
857867
const didEndLoop = await this.recursivelyMakeClineRequests(nextUserContent, includeFileDetails)
858868
includeFileDetails = false // we only need file details the first time
859869

860-
// The way this agentic loop works is that cline will be given a task that he then calls tools to complete. unless there's an attempt_completion call, we keep responding back to him with his tool's responses until he either attempt_completion or does not use anymore tools. If he does not use anymore tools, we ask him to consider if he's completed the task and then call attempt_completion, otherwise proceed with completing the task.
861-
// There is a MAX_REQUESTS_PER_TASK limit to prevent infinite requests, but Cline is prompted to finish the task as efficiently as he can.
870+
// The way this agentic loop works is that cline will be given a
871+
// task that he then calls tools to complete. Unless there's an
872+
// attempt_completion call, we keep responding back to him with his
873+
// tool's responses until he either attempt_completion or does not
874+
// use anymore tools. If he does not use anymore tools, we ask him
875+
// to consider if he's completed the task and then call
876+
// attempt_completion, otherwise proceed with completing the task.
877+
// There is a MAX_REQUESTS_PER_TASK limit to prevent infinite
878+
// requests, but Cline is prompted to finish the task as efficiently
879+
// as he can.
862880

863-
//const totalCost = this.calculateApiCostAnthropic(totalInputTokens, totalOutputTokens)
864881
if (didEndLoop) {
865-
// For now a task never 'completes'. This will only happen if the user hits max requests and denies resetting the count.
866-
//this.say("task_completed", `Task completed. Total API usage cost: ${totalCost}`)
882+
// For now a task never 'completes'. This will only happen if
883+
// the user hits max requests and denies resetting the count.
867884
break
868885
} else {
869-
// this.say(
870-
// "tool",
871-
// "Cline responded with only text blocks but has not called attempt_completion yet. Forcing him to continue with task..."
872-
// )
873-
nextUserContent = [
874-
{
875-
type: "text",
876-
text: formatResponse.noToolsUsed(),
877-
},
878-
]
886+
nextUserContent = [{ type: "text", text: formatResponse.noToolsUsed() }]
879887
this.consecutiveMistakeCount++
880888
}
881889
}
882890
}
883891

884892
async abortTask(isAbandoned = false) {
893+
// if (this.abort) {
894+
// console.log(`[subtasks] already aborted task ${this.taskId}.${this.instanceId}`)
895+
// return
896+
// }
897+
898+
console.log(`[subtasks] aborting task ${this.taskId}.${this.instanceId}`)
899+
885900
// Will stop any autonomously running promises.
886901
if (isAbandoned) {
887902
this.abandoned = true
@@ -1237,7 +1252,7 @@ export class Cline extends EventEmitter<ClineEvents> {
12371252

12381253
async presentAssistantMessage() {
12391254
if (this.abort) {
1240-
throw new Error(`Task: ${this.taskNumber} Roo Code instance aborted (#3)`)
1255+
throw new Error(`[Cline#presentAssistantMessage] task ${this.taskId}.${this.instanceId} aborted`)
12411256
}
12421257

12431258
if (this.presentAssistantMessageLocked) {
@@ -3113,7 +3128,7 @@ export class Cline extends EventEmitter<ClineEvents> {
31133128
includeFileDetails: boolean = false,
31143129
): Promise<boolean> {
31153130
if (this.abort) {
3116-
throw new Error(`Task: ${this.taskNumber} Roo Code instance aborted (#4)`)
3131+
throw new Error(`[Cline#recursivelyMakeClineRequests] task ${this.taskId}.${this.instanceId} aborted`)
31173132
}
31183133

31193134
if (this.consecutiveMistakeCount >= 3) {
@@ -3146,9 +3161,9 @@ export class Cline extends EventEmitter<ClineEvents> {
31463161
const provider = this.providerRef.deref()
31473162

31483163
if (this.isPaused && provider) {
3149-
provider.log(`[subtasks] paused ${this.taskId}`)
3164+
provider.log(`[subtasks] paused ${this.taskId}.${this.instanceId}`)
31503165
await this.waitForResume()
3151-
provider.log(`[subtasks] resumed ${this.taskId}`)
3166+
provider.log(`[subtasks] resumed ${this.taskId}.${this.instanceId}`)
31523167
const currentMode = (await provider.getState())?.mode ?? defaultModeSlug
31533168

31543169
if (currentMode !== this.pausedModeSlug) {
@@ -3159,7 +3174,7 @@ export class Cline extends EventEmitter<ClineEvents> {
31593174
await delay(500)
31603175

31613176
provider.log(
3162-
`[subtasks] task ${this.taskId} has switched back to '${this.pausedModeSlug}' from '${currentMode}'`,
3177+
`[subtasks] task ${this.taskId}.${this.instanceId} has switched back to '${this.pausedModeSlug}' from '${currentMode}'`,
31633178
)
31643179
}
31653180
}
@@ -3279,6 +3294,7 @@ export class Cline extends EventEmitter<ClineEvents> {
32793294
let assistantMessage = ""
32803295
let reasoningMessage = ""
32813296
this.isStreaming = true
3297+
32823298
try {
32833299
for await (const chunk of stream) {
32843300
if (!chunk) {
@@ -3356,7 +3372,7 @@ export class Cline extends EventEmitter<ClineEvents> {
33563372

33573373
// need to call here in case the stream was aborted
33583374
if (this.abort || this.abandoned) {
3359-
throw new Error(`Task: ${this.taskNumber} Roo Code instance aborted (#5)`)
3375+
throw new Error(`[Cline#recursivelyMakeClineRequests] task ${this.taskId}.${this.instanceId} aborted`)
33603376
}
33613377

33623378
this.didCompleteReadingStream = true

0 commit comments

Comments
 (0)