Skip to content
Open
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
4 changes: 3 additions & 1 deletion apps/vscode-e2e/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"lint": "eslint src --ext=ts --max-warnings=0",
"check-types": "tsc -p tsconfig.esm.json --noEmit",
"format": "prettier --write src",
"test:unit": "vitest run --config vitest.config.ts",
"test:ci": "pnpm -w bundle && pnpm --filter @roo-code/vscode-webview build && pnpm test:run",
"test:ci:mock": "pnpm -w bundle && pnpm --filter @roo-code/vscode-webview build && USE_MOCK=true pnpm test:run",
"test:record": "AIMOCK_RECORD=true pnpm test:ci",
Expand All @@ -23,6 +24,7 @@
"dotenv-cli": "11.0.0",
"glob": "11.1.0",
"mocha": "11.2.2",
"rimraf": "6.0.1"
"rimraf": "6.0.1",
"vitest": "4.1.9"
}
}
312 changes: 312 additions & 0 deletions apps/vscode-e2e/src/fixtures/orchestrator-plan.ts

Large diffs are not rendered by default.

280 changes: 280 additions & 0 deletions apps/vscode-e2e/src/fixtures/orchestrator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,280 @@
import { LLMock } from "@copilotkit/aimock"
import type { ChatCompletionRequest } from "@copilotkit/aimock"

export * from "./orchestrator-plan"

import {
ORCHESTRATOR_CANCELLATION_RECOVERY_CHILD_STEP,
ORCHESTRATOR_CANCELLATION_RECOVERY_FINAL_RESULT,
ORCHESTRATOR_CANCELLATION_RECOVERY_FOLLOWUP_TOOL_CALL_ID,
ORCHESTRATOR_CANCELLATION_RECOVERY_MARKER,
ORCHESTRATOR_FAN_OUT_CHILD_STEPS,
ORCHESTRATOR_FAN_OUT_FINAL_RESULT,
ORCHESTRATOR_FAN_OUT_MARKER,
ORCHESTRATOR_NESTED_DELEGATION_CHILD_FINAL_RESULT,
ORCHESTRATOR_NESTED_DELEGATION_CHILD_ORCHESTRATOR_STEP,
ORCHESTRATOR_NESTED_DELEGATION_FINAL_RESULT,
ORCHESTRATOR_NESTED_DELEGATION_GRANDCHILD_STEPS,
ORCHESTRATOR_NESTED_DELEGATION_MARKER,
ORCHESTRATOR_REPEATED_DELEGATION_CHILD_STEPS,
ORCHESTRATOR_REPEATED_DELEGATION_FINAL_RESULT,
ORCHESTRATOR_REPEATED_DELEGATION_MARKER,
buildOrchestratorCancellationRecoveryResumeExpectations,
buildOrchestratorNestedChildResumeExpectations,
buildOrchestratorNestedParentResumeExpectations,
buildOrchestratorRepeatedResumeExpectations,
buildOrchestratorResumeExpectations,
shouldMatchOrchestratorCancellationChildCompletionRequest,
shouldMatchOrchestratorCancellationChildRequest,
shouldMatchOrchestratorCancellationRecoveryResumeRequest,
shouldMatchOrchestratorChildRequest,
shouldMatchOrchestratorNestedChildResumeRequest,
shouldMatchOrchestratorNestedParentResumeRequest,
shouldMatchOrchestratorRepeatedResumeRequest,
shouldMatchOrchestratorResumeRequest,
} from "./orchestrator-plan"

const requestText = (req: ChatCompletionRequest) => JSON.stringify(req)

function addChildCompletionFixtures(
mock: InstanceType<typeof LLMock>,
steps: readonly { marker: string; summary: string; completionToolCallId: string }[],
) {
for (const step of steps) {
mock.addFixture({
match: {
predicate: (req: ChatCompletionRequest) =>
shouldMatchOrchestratorChildRequest(requestText(req), step.marker),
},
response: {
toolCalls: [
{
name: "attempt_completion",
arguments: JSON.stringify({ result: step.summary }),
id: step.completionToolCallId,
},
],
},
})
}
}

function addResumeFixtures(
mock: InstanceType<typeof LLMock>,
config: {
expectations: readonly { stepIndex: number; requiredSummaries: readonly string[] }[]
steps: readonly { mode: string; prompt: string; newTaskToolCallId: string }[]
matches: (rawRequest: string, requiredSummaries: readonly string[]) => boolean
finalResult: string
finalToolCallId: string
},
) {
// Register most-cumulative expectations first so less-specific predicates cannot shadow later rounds.
for (const expectation of [...config.expectations].reverse()) {
const nextStep = config.steps[expectation.stepIndex]

mock.addFixture({
match: {
predicate: (req: ChatCompletionRequest) =>
config.matches(requestText(req), expectation.requiredSummaries),
},
response: {
toolCalls: nextStep
? [
{
name: "new_task",
arguments: JSON.stringify({
mode: nextStep.mode,
message: nextStep.prompt,
}),
id: nextStep.newTaskToolCallId,
},
]
: [
{
name: "attempt_completion",
arguments: JSON.stringify({ result: config.finalResult }),
id: config.finalToolCallId,
},
],
},
})
}
}

export function addOrchestratorFixtures(mock: InstanceType<typeof LLMock>) {
const firstFanOutStep = ORCHESTRATOR_FAN_OUT_CHILD_STEPS[0]!
const firstRepeatedStep = ORCHESTRATOR_REPEATED_DELEGATION_CHILD_STEPS[0]!
const firstNestedGrandchildStep = ORCHESTRATOR_NESTED_DELEGATION_GRANDCHILD_STEPS[0]!

mock.addFixture({
match: {
userMessage: new RegExp(ORCHESTRATOR_FAN_OUT_MARKER),
sequenceIndex: 0,
},
response: {
toolCalls: [
{
name: "new_task",
arguments: JSON.stringify({
mode: firstFanOutStep.mode,
message: firstFanOutStep.prompt,
}),
id: firstFanOutStep.newTaskToolCallId,
},
],
},
})

addChildCompletionFixtures(mock, ORCHESTRATOR_FAN_OUT_CHILD_STEPS)
addResumeFixtures(mock, {
expectations: buildOrchestratorResumeExpectations(),
steps: ORCHESTRATOR_FAN_OUT_CHILD_STEPS,
matches: shouldMatchOrchestratorResumeRequest,
finalResult: ORCHESTRATOR_FAN_OUT_FINAL_RESULT,
finalToolCallId: "call_orchestrator_fan_out_parent_completion_004",
})
mock.addFixture({
match: {
userMessage: new RegExp(ORCHESTRATOR_REPEATED_DELEGATION_MARKER),
sequenceIndex: 0,
},
response: {
toolCalls: [
{
name: "new_task",
arguments: JSON.stringify({
mode: firstRepeatedStep.mode,
message: firstRepeatedStep.prompt,
}),
id: firstRepeatedStep.newTaskToolCallId,
},
],
},
})

addChildCompletionFixtures(mock, ORCHESTRATOR_REPEATED_DELEGATION_CHILD_STEPS)
addResumeFixtures(mock, {
expectations: buildOrchestratorRepeatedResumeExpectations(),
steps: ORCHESTRATOR_REPEATED_DELEGATION_CHILD_STEPS,
matches: shouldMatchOrchestratorRepeatedResumeRequest,
finalResult: ORCHESTRATOR_REPEATED_DELEGATION_FINAL_RESULT,
finalToolCallId: "call_orchestrator_repeated_parent_completion_010",
})

mock.addFixture({
match: {
userMessage: new RegExp(ORCHESTRATOR_NESTED_DELEGATION_MARKER),
sequenceIndex: 0,
},
response: {
toolCalls: [
{
name: "new_task",
arguments: JSON.stringify({
mode: ORCHESTRATOR_NESTED_DELEGATION_CHILD_ORCHESTRATOR_STEP.mode,
message: ORCHESTRATOR_NESTED_DELEGATION_CHILD_ORCHESTRATOR_STEP.prompt,
}),
id: ORCHESTRATOR_NESTED_DELEGATION_CHILD_ORCHESTRATOR_STEP.newTaskToolCallId,
},
],
},
})

mock.addFixture({
match: {
userMessage: new RegExp(ORCHESTRATOR_NESTED_DELEGATION_CHILD_ORCHESTRATOR_STEP.marker),
sequenceIndex: 0,
},
response: {
toolCalls: [
{
name: "new_task",
arguments: JSON.stringify({
mode: firstNestedGrandchildStep.mode,
message: firstNestedGrandchildStep.prompt,
}),
id: firstNestedGrandchildStep.newTaskToolCallId,
},
],
},
})

addChildCompletionFixtures(mock, ORCHESTRATOR_NESTED_DELEGATION_GRANDCHILD_STEPS)
addResumeFixtures(mock, {
expectations: buildOrchestratorNestedChildResumeExpectations(),
steps: ORCHESTRATOR_NESTED_DELEGATION_GRANDCHILD_STEPS,
matches: shouldMatchOrchestratorNestedChildResumeRequest,
finalResult: ORCHESTRATOR_NESTED_DELEGATION_CHILD_FINAL_RESULT,
finalToolCallId: ORCHESTRATOR_NESTED_DELEGATION_CHILD_ORCHESTRATOR_STEP.completionToolCallId,
})
addResumeFixtures(mock, {
expectations: buildOrchestratorNestedParentResumeExpectations(),
steps: [],
matches: shouldMatchOrchestratorNestedParentResumeRequest,
finalResult: ORCHESTRATOR_NESTED_DELEGATION_FINAL_RESULT,
finalToolCallId: "call_orchestrator_nested_parent_completion_002",
})

mock.addFixture({
match: {
userMessage: new RegExp(ORCHESTRATOR_CANCELLATION_RECOVERY_MARKER),
sequenceIndex: 0,
},
response: {
toolCalls: [
{
name: "new_task",
arguments: JSON.stringify({
mode: ORCHESTRATOR_CANCELLATION_RECOVERY_CHILD_STEP.mode,
message: ORCHESTRATOR_CANCELLATION_RECOVERY_CHILD_STEP.prompt,
}),
id: ORCHESTRATOR_CANCELLATION_RECOVERY_CHILD_STEP.newTaskToolCallId,
},
],
},
})

mock.addFixture({
match: {
predicate: (req: ChatCompletionRequest) =>
shouldMatchOrchestratorCancellationChildRequest(requestText(req)),
},
response: {
toolCalls: [
{
name: "ask_followup_question",
arguments: JSON.stringify({
question: `Type "${ORCHESTRATOR_CANCELLATION_RECOVERY_CHILD_STEP.followupAnswer}" to recover the cancelled child.`,
follow_up: [{ text: ORCHESTRATOR_CANCELLATION_RECOVERY_CHILD_STEP.followupAnswer }],
}),
id: ORCHESTRATOR_CANCELLATION_RECOVERY_FOLLOWUP_TOOL_CALL_ID,
},
],
},
})

mock.addFixture({
match: {
predicate: (req: ChatCompletionRequest) =>
shouldMatchOrchestratorCancellationChildCompletionRequest(requestText(req)),
},
response: {
toolCalls: [
{
name: "attempt_completion",
arguments: JSON.stringify({ result: ORCHESTRATOR_CANCELLATION_RECOVERY_CHILD_STEP.summary }),
id: ORCHESTRATOR_CANCELLATION_RECOVERY_CHILD_STEP.completionToolCallId,
},
],
},
})

addResumeFixtures(mock, {
expectations: buildOrchestratorCancellationRecoveryResumeExpectations(),
steps: [],
matches: shouldMatchOrchestratorCancellationRecoveryResumeRequest,
finalResult: ORCHESTRATOR_CANCELLATION_RECOVERY_FINAL_RESULT,
finalToolCallId: "call_orchestrator_cancellation_parent_completion_002",
})
}
85 changes: 85 additions & 0 deletions apps/vscode-e2e/src/fixtures/resource-diagnostics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import type { RooCodeResourceDiagnosticEventName, RooCodeResourceDiagnostics } from "@roo-code/types"

export type ResourceDiagnosticsConvergenceOptions = {
baseline: RooCodeResourceDiagnostics
final: RooCodeResourceDiagnostics
observedChildTaskIds?: string[]
}

type DiagnosticIssue = {
name: string
baseline: number
final: number
}

const formatIssue = ({ name, baseline, final }: DiagnosticIssue) => `${name}: baseline=${baseline}, final=${final}`

const listenerCountIssues = (
baseline: RooCodeResourceDiagnostics,
final: RooCodeResourceDiagnostics,
): DiagnosticIssue[] => {
const listenerNames = new Set<RooCodeResourceDiagnosticEventName>([
...(Object.keys(baseline.listenerCounts) as RooCodeResourceDiagnosticEventName[]),
...(Object.keys(final.listenerCounts) as RooCodeResourceDiagnosticEventName[]),
])

return [...listenerNames].sort().flatMap((listenerName) => {
const baselineCount = baseline.listenerCounts[listenerName] ?? 0
const finalCount = final.listenerCounts[listenerName] ?? 0

return finalCount === baselineCount
? []
: [
{
name: `listenerCounts.${listenerName}`,
baseline: baselineCount,
final: finalCount,
},
]
})
}

export const getResourceDiagnosticsConvergenceIssues = ({
baseline,
final,
}: ResourceDiagnosticsConvergenceOptions): DiagnosticIssue[] => {
const issues: DiagnosticIssue[] = []

if (final.registeredTaskCount !== baseline.registeredTaskCount) {
issues.push({
name: "registeredTaskCount",
baseline: baseline.registeredTaskCount,
final: final.registeredTaskCount,
})
}

// A final zero intentionally treats a shrinking task stack as converged, unlike registeredTaskCount.
if (final.currentTaskStackLength !== baseline.currentTaskStackLength && final.currentTaskStackLength !== 0) {
issues.push({
name: "currentTaskStackLength",
baseline: baseline.currentTaskStackLength,
final: final.currentTaskStackLength,
})
}

issues.push(...listenerCountIssues(baseline, final))

return issues
}

export const assertResourceDiagnosticsConverged = (options: ResourceDiagnosticsConvergenceOptions) => {
const issues = getResourceDiagnosticsConvergenceIssues(options)

if (issues.length === 0) {
return
}

const staleChildHint =
options.observedChildTaskIds && options.observedChildTaskIds.length > 0
? ` Observed child task ids: ${options.observedChildTaskIds.join(", ")}.`
: ""

throw new Error(
`Resource diagnostics did not converge after orchestrator cleanup: ${issues.map(formatIssue).join("; ")}.${staleChildHint}`,
)
}
2 changes: 2 additions & 0 deletions apps/vscode-e2e/src/runTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { addListFilesResultFixtures } from "./fixtures/list-files"
import { addReadFileResultFixtures } from "./fixtures/read-file"
import { addSearchFilesResultFixtures } from "./fixtures/search-files"
import { addSubtaskFixtures } from "./fixtures/subtasks"
import { addOrchestratorFixtures } from "./fixtures/orchestrator"
import { addUseMcpToolResultFixtures } from "./fixtures/use-mcp-tool"
import { addWriteToFileResultFixtures } from "./fixtures/write-to-file"

Expand Down Expand Up @@ -126,6 +127,7 @@ async function main() {
addReadFileResultFixtures(mock)
addSearchFilesResultFixtures(mock)
addSubtaskFixtures(mock)
addOrchestratorFixtures(mock)
addUseMcpToolResultFixtures(mock)
addWriteToFileResultFixtures(mock)
addDeepSeekV4Fixtures(mock)
Expand Down
Loading
Loading