mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-08 22:21:23 +00:00
refactor and fixed copy
This commit is contained in:
parent
21360b0d4e
commit
adc1573b09
16 changed files with 997 additions and 874 deletions
|
|
@ -231,7 +231,7 @@ export const EVALS_SETTINGS: RooCodeSettings = {
|
|||
enableCheckpoints: false,
|
||||
|
||||
// Timeout settings
|
||||
toolExecutionTimeoutMs: 300000, // 5 minutes default
|
||||
toolExecutionTimeoutMs: 60000, // 1 minute default
|
||||
timeoutFallbackEnabled: false,
|
||||
|
||||
rateLimitSeconds: 0,
|
||||
|
|
|
|||
|
|
@ -32,10 +32,10 @@ export class TimeoutManager extends EventEmitter {
|
|||
private static instance: TimeoutManager | undefined
|
||||
private activeOperations = new Map<string, AbortController>()
|
||||
/**
|
||||
* Multiple timeout events can be run at once
|
||||
* eg. running a command while reading a file
|
||||
* Assumes there is only on active tool-- does not timeout edge cases
|
||||
* like "Proceed While Running"
|
||||
*/
|
||||
private timeoutEvents: TimeoutEvent[] = []
|
||||
private lastTimeoutEvent: TimeoutEvent | null = null
|
||||
|
||||
private constructor() {
|
||||
super()
|
||||
|
|
@ -93,7 +93,7 @@ export class TimeoutManager extends EventEmitter {
|
|||
const timedOut = controller.signal.aborted
|
||||
|
||||
if (timedOut) {
|
||||
// Log timeout event
|
||||
// Store the last timeout event
|
||||
const timeoutEvent: TimeoutEvent = {
|
||||
toolName: config.toolName,
|
||||
timeoutMs: config.timeoutMs,
|
||||
|
|
@ -102,7 +102,7 @@ export class TimeoutManager extends EventEmitter {
|
|||
timestamp: Date.now(),
|
||||
}
|
||||
|
||||
this.timeoutEvents.push(timeoutEvent)
|
||||
this.lastTimeoutEvent = timeoutEvent
|
||||
this.emit("timeout", timeoutEvent)
|
||||
|
||||
return {
|
||||
|
|
@ -154,17 +154,17 @@ export class TimeoutManager extends EventEmitter {
|
|||
}
|
||||
|
||||
/**
|
||||
* Get timeout events for debugging/monitoring
|
||||
* Get the last timeout event
|
||||
*/
|
||||
public getTimeoutEvents(limit = 100): TimeoutEvent[] {
|
||||
return this.timeoutEvents.slice(-limit)
|
||||
public getLastTimeoutEvent(): TimeoutEvent | null {
|
||||
return this.lastTimeoutEvent
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear timeout event history
|
||||
* Clear the last timeout event
|
||||
*/
|
||||
public clearTimeoutEvents(): void {
|
||||
this.timeoutEvents = []
|
||||
public clearLastTimeoutEvent(): void {
|
||||
this.lastTimeoutEvent = null
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -183,7 +183,7 @@ export class TimeoutManager extends EventEmitter {
|
|||
}
|
||||
|
||||
private generateOperationId(toolName: ToolName, taskId?: string): string {
|
||||
return `${toolName}:${taskId || "default"}:${Date.now()}`
|
||||
return `${toolName}:${taskId || "default"}`
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -192,7 +192,7 @@ export class TimeoutManager extends EventEmitter {
|
|||
public dispose(): void {
|
||||
this.cancelAllOperations()
|
||||
this.removeAllListeners()
|
||||
this.timeoutEvents = []
|
||||
this.lastTimeoutEvent = null
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ export class ToolExecutionWrapper {
|
|||
public static async execute<T>(
|
||||
operation: (signal: AbortSignal) => Promise<T>,
|
||||
options: ToolExecutionOptions,
|
||||
defaultTimeoutMs = 300000, // 5 minutes default
|
||||
defaultTimeoutMs = 60000, // 1 minute default
|
||||
): Promise<TimeoutResult<T>> {
|
||||
const config: TimeoutConfig = {
|
||||
toolName: options.toolName,
|
||||
|
|
@ -29,135 +29,4 @@ export class ToolExecutionWrapper {
|
|||
|
||||
return timeoutManager.executeWithTimeout(operation, config)
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap a promise-based operation to support AbortSignal
|
||||
*/
|
||||
public static wrapPromise<T>(promiseFactory: () => Promise<T>, signal: AbortSignal): Promise<T> {
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
// Check if already aborted
|
||||
if (signal.aborted) {
|
||||
reject(new Error("Operation was aborted before starting"))
|
||||
return
|
||||
}
|
||||
|
||||
// Set up abort listener
|
||||
const abortListener = () => {
|
||||
reject(new Error("Operation was aborted"))
|
||||
}
|
||||
|
||||
signal.addEventListener("abort", abortListener)
|
||||
|
||||
// Execute the operation
|
||||
promiseFactory()
|
||||
.then((result) => {
|
||||
signal.removeEventListener("abort", abortListener)
|
||||
resolve(result)
|
||||
})
|
||||
.catch((error) => {
|
||||
signal.removeEventListener("abort", abortListener)
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap a callback-based operation to support AbortSignal
|
||||
*/
|
||||
public static wrapCallback<T>(
|
||||
operation: (callback: (error: Error | null, result?: T) => void, signal: AbortSignal) => void,
|
||||
signal: AbortSignal,
|
||||
): Promise<T> {
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
// Check if already aborted
|
||||
if (signal.aborted) {
|
||||
reject(new Error("Operation was aborted before starting"))
|
||||
return
|
||||
}
|
||||
|
||||
// Set up abort listener
|
||||
const abortListener = () => {
|
||||
reject(new Error("Operation was aborted"))
|
||||
}
|
||||
|
||||
signal.addEventListener("abort", abortListener)
|
||||
|
||||
// Execute the operation
|
||||
operation((error, result) => {
|
||||
signal.removeEventListener("abort", abortListener)
|
||||
|
||||
if (error) {
|
||||
reject(error)
|
||||
} else {
|
||||
resolve(result!)
|
||||
}
|
||||
}, signal)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an abortable delay
|
||||
*/
|
||||
public static delay(ms: number, signal: AbortSignal): Promise<void> {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
if (signal.aborted) {
|
||||
reject(new Error("Delay was aborted before starting"))
|
||||
return
|
||||
}
|
||||
|
||||
const timeoutId = setTimeout(() => {
|
||||
signal.removeEventListener("abort", abortListener)
|
||||
resolve()
|
||||
}, ms)
|
||||
|
||||
const abortListener = () => {
|
||||
clearTimeout(timeoutId)
|
||||
reject(new Error("Delay was aborted"))
|
||||
}
|
||||
|
||||
signal.addEventListener("abort", abortListener)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute multiple operations in parallel with timeout protection
|
||||
*/
|
||||
public static async executeParallel<T>(
|
||||
operations: Array<{
|
||||
operation: (signal: AbortSignal) => Promise<T>
|
||||
options: ToolExecutionOptions
|
||||
}>,
|
||||
defaultTimeoutMs = 300000,
|
||||
): Promise<TimeoutResult<T>[]> {
|
||||
const promises = operations.map(({ operation, options }) =>
|
||||
ToolExecutionWrapper.execute(operation, options, defaultTimeoutMs),
|
||||
)
|
||||
|
||||
return Promise.all(promises)
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute operations in sequence with timeout protection
|
||||
*/
|
||||
public static async executeSequential<T>(
|
||||
operations: Array<{
|
||||
operation: (signal: AbortSignal) => Promise<T>
|
||||
options: ToolExecutionOptions
|
||||
}>,
|
||||
defaultTimeoutMs = 300000,
|
||||
): Promise<TimeoutResult<T>[]> {
|
||||
const results: TimeoutResult<T>[] = []
|
||||
|
||||
for (const { operation, options } of operations) {
|
||||
const result = await ToolExecutionWrapper.execute(operation, options, defaultTimeoutMs)
|
||||
results.push(result)
|
||||
|
||||
// Stop execution if any operation fails or times out
|
||||
if (!result.success) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,234 +0,0 @@
|
|||
// npx vitest run src/core/timeout/__tests__/ai-fallback-real.spec.ts
|
||||
|
||||
import { describe, test, expect, beforeEach, vitest } from "vitest"
|
||||
import { TimeoutFallbackHandler } from "../TimeoutFallbackHandler"
|
||||
import type { ApiHandler, SingleCompletionHandler } from "../../../api"
|
||||
import type { Task } from "../../task/Task"
|
||||
|
||||
// Create a mock API handler that extends ApiHandler and includes completePrompt
|
||||
interface MockApiHandler extends ApiHandler, SingleCompletionHandler {}
|
||||
|
||||
describe("TimeoutFallbackHandler - Real AI Implementation", () => {
|
||||
let mockApiHandler: MockApiHandler
|
||||
let mockTask: Partial<Task>
|
||||
|
||||
beforeEach(() => {
|
||||
vitest.clearAllMocks()
|
||||
|
||||
// Mock API handler that simulates real AI responses
|
||||
mockApiHandler = {
|
||||
createMessage: vitest.fn(),
|
||||
getModel: vitest.fn().mockReturnValue({ id: "test-model", info: { maxTokens: 4096 } }),
|
||||
countTokens: vitest.fn().mockResolvedValue(100),
|
||||
completePrompt: vitest.fn(),
|
||||
}
|
||||
|
||||
// Mock task with API handler
|
||||
mockTask = {
|
||||
api: mockApiHandler,
|
||||
}
|
||||
})
|
||||
|
||||
test("should use AI to generate contextual suggestions when available", async () => {
|
||||
// Mock AI response with numbered suggestions
|
||||
const mockAiResponse = `Here are some suggestions for the timeout:
|
||||
|
||||
1. Break the npm install command into smaller package installations
|
||||
2. Clear npm cache and try again with npm cache clean --force
|
||||
3. Use npm install --no-optional to skip optional dependencies
|
||||
4. Check network connectivity and try with different registry`
|
||||
|
||||
;(mockApiHandler.completePrompt as any).mockResolvedValueOnce(mockAiResponse)
|
||||
|
||||
const context = {
|
||||
toolName: "execute_command" as const,
|
||||
timeoutMs: 30000,
|
||||
executionTimeMs: 35000,
|
||||
toolParams: { command: "npm install" },
|
||||
}
|
||||
|
||||
const result = await TimeoutFallbackHandler.generateAiFallback(context, mockTask as Task)
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.toolCall).toBeDefined()
|
||||
expect(result.toolCall?.name).toBe("ask_followup_question")
|
||||
expect(result.toolCall?.params.question).toContain("execute_command")
|
||||
expect(result.toolCall?.params.question).toContain("30 seconds")
|
||||
|
||||
// Check that AI-generated suggestions are included
|
||||
const followUp = result.toolCall?.params.follow_up || ""
|
||||
expect(followUp).toContain("Break the npm install command into smaller package installations")
|
||||
expect(followUp).toContain("Clear npm cache and try again")
|
||||
expect(followUp).toContain("Use npm install --no-optional")
|
||||
expect(followUp).toContain("Check network connectivity")
|
||||
|
||||
// Verify AI was called with proper prompt
|
||||
expect(mockApiHandler.completePrompt).toHaveBeenCalledWith(
|
||||
expect.stringContaining("execute_command operation has timed out"),
|
||||
)
|
||||
expect(mockApiHandler.completePrompt).toHaveBeenCalledWith(expect.stringContaining("npm install"))
|
||||
})
|
||||
|
||||
test("should fallback to static suggestions when AI fails", async () => {
|
||||
// Mock AI failure
|
||||
;(mockApiHandler.completePrompt as any).mockRejectedValueOnce(new Error("API Error"))
|
||||
|
||||
const context = {
|
||||
toolName: "execute_command" as const,
|
||||
timeoutMs: 30000,
|
||||
executionTimeMs: 35000,
|
||||
toolParams: { command: "npm test" },
|
||||
}
|
||||
|
||||
const result = await TimeoutFallbackHandler.generateAiFallback(context, mockTask as Task)
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.toolCall).toBeDefined()
|
||||
expect(result.toolCall?.name).toBe("ask_followup_question")
|
||||
|
||||
// Should contain static fallback suggestions
|
||||
const followUp = result.toolCall?.params.follow_up || ""
|
||||
expect(followUp).toContain('Break "npm test" into smaller')
|
||||
expect(followUp).toContain("background using")
|
||||
expect(followUp).toContain("alternative approach")
|
||||
expect(followUp).toContain("Increase the timeout")
|
||||
})
|
||||
|
||||
test("should fallback to static suggestions when API handler is unavailable", async () => {
|
||||
// Task without API handler
|
||||
const taskWithoutApi = {}
|
||||
|
||||
const context = {
|
||||
toolName: "read_file" as const,
|
||||
timeoutMs: 5000,
|
||||
executionTimeMs: 6000,
|
||||
toolParams: { path: "/large/file.txt" },
|
||||
}
|
||||
|
||||
const result = await TimeoutFallbackHandler.generateAiFallback(context, taskWithoutApi as Task)
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.toolCall).toBeDefined()
|
||||
|
||||
// Should contain static fallback suggestions for read_file
|
||||
const followUp = result.toolCall?.params.follow_up || ""
|
||||
expect(followUp).toContain('Read "/large/file.txt" in smaller chunks')
|
||||
expect(followUp).toContain("accessible and not locked")
|
||||
})
|
||||
|
||||
test("should parse AI response with different numbering formats", async () => {
|
||||
// Test different numbering formats
|
||||
const mockAiResponse = `Here are the suggestions:
|
||||
|
||||
1) Try breaking the command into parts
|
||||
2. Use a different approach
|
||||
3) Check system resources
|
||||
4. Increase timeout duration`
|
||||
|
||||
;(mockApiHandler.completePrompt as any).mockResolvedValueOnce(mockAiResponse)
|
||||
|
||||
const context = {
|
||||
toolName: "execute_command" as const,
|
||||
timeoutMs: 10000,
|
||||
executionTimeMs: 12000,
|
||||
toolParams: { command: "build script" },
|
||||
}
|
||||
|
||||
const result = await TimeoutFallbackHandler.generateAiFallback(context, mockTask as Task)
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
const followUp = result.toolCall?.params.follow_up || ""
|
||||
expect(followUp).toContain("Try breaking the command into parts")
|
||||
expect(followUp).toContain("Use a different approach")
|
||||
expect(followUp).toContain("Check system resources")
|
||||
expect(followUp).toContain("Increase timeout duration")
|
||||
})
|
||||
|
||||
test("should handle AI response without numbered list", async () => {
|
||||
// AI response without clear numbering
|
||||
const mockAiResponse = `You could try splitting the operation. Another option is to check the network. Maybe increase the timeout. Consider using a different tool.`
|
||||
|
||||
;(mockApiHandler.completePrompt as any).mockResolvedValueOnce(mockAiResponse)
|
||||
|
||||
const context = {
|
||||
toolName: "browser_action" as const,
|
||||
timeoutMs: 15000,
|
||||
executionTimeMs: 16000,
|
||||
toolParams: { action: "click" },
|
||||
}
|
||||
|
||||
const result = await TimeoutFallbackHandler.generateAiFallback(context, mockTask as Task)
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
const followUp = result.toolCall?.params.follow_up || ""
|
||||
|
||||
// Should extract sentences as suggestions
|
||||
expect(followUp).toContain("You could try splitting the operation")
|
||||
expect(followUp).toContain("Another option is to check the network")
|
||||
})
|
||||
|
||||
test("should include task context in AI prompt when available", async () => {
|
||||
const mockAiResponse = `1. Try a different approach\n2. Check the working directory\n3. Break into steps`
|
||||
;(mockApiHandler.completePrompt as any).mockResolvedValueOnce(mockAiResponse)
|
||||
|
||||
const context = {
|
||||
toolName: "search_files" as const,
|
||||
timeoutMs: 20000,
|
||||
executionTimeMs: 22000,
|
||||
toolParams: { path: "/project", regex: ".*\\.ts$" },
|
||||
taskContext: {
|
||||
currentStep: "Finding TypeScript files",
|
||||
workingDirectory: "/project/src",
|
||||
previousActions: ["read package.json", "list files"],
|
||||
},
|
||||
}
|
||||
|
||||
const result = await TimeoutFallbackHandler.generateAiFallback(context, mockTask as Task)
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
|
||||
// Verify the prompt included task context
|
||||
const calledPrompt = (mockApiHandler.completePrompt as any).mock.calls[0][0]
|
||||
expect(calledPrompt).toContain("Current step: Finding TypeScript files")
|
||||
expect(calledPrompt).toContain("Working directory: /project/src")
|
||||
expect(calledPrompt).toContain("search_files")
|
||||
expect(calledPrompt).toContain("ts$") // Just check for the pattern ending
|
||||
})
|
||||
|
||||
test("should limit suggestions to maximum of 4", async () => {
|
||||
// AI response with many suggestions
|
||||
const mockAiResponse = `Here are many suggestions:
|
||||
|
||||
1. First suggestion
|
||||
2. Second suggestion
|
||||
3. Third suggestion
|
||||
4. Fourth suggestion
|
||||
5. Fifth suggestion
|
||||
6. Sixth suggestion
|
||||
7. Seventh suggestion`
|
||||
|
||||
;(mockApiHandler.completePrompt as any).mockResolvedValueOnce(mockAiResponse)
|
||||
|
||||
const context = {
|
||||
toolName: "write_to_file" as const,
|
||||
timeoutMs: 8000,
|
||||
executionTimeMs: 9000,
|
||||
toolParams: { path: "/output.txt" },
|
||||
}
|
||||
|
||||
const result = await TimeoutFallbackHandler.generateAiFallback(context, mockTask as Task)
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
const followUp = result.toolCall?.params.follow_up || ""
|
||||
|
||||
// Count the number of <suggest> tags
|
||||
const suggestCount = (followUp.match(/<suggest>/g) || []).length
|
||||
expect(suggestCount).toBeLessThanOrEqual(4)
|
||||
|
||||
// Should include first 4 suggestions
|
||||
expect(followUp).toContain("First suggestion")
|
||||
expect(followUp).toContain("Fourth suggestion")
|
||||
// Should not include 5th and beyond
|
||||
expect(followUp).not.toContain("Fifth suggestion")
|
||||
})
|
||||
})
|
||||
|
|
@ -1,179 +0,0 @@
|
|||
// End-to-end test to verify AI fallback generation works
|
||||
// npx vitest run src/core/timeout/__tests__/e2e-ai-test.spec.ts
|
||||
|
||||
import { describe, test, expect, beforeEach, vitest } from "vitest"
|
||||
import { TimeoutFallbackHandler } from "../TimeoutFallbackHandler"
|
||||
import type { ApiHandler, SingleCompletionHandler } from "../../../api"
|
||||
import type { Task } from "../../task/Task"
|
||||
|
||||
// Mock API handler that simulates a real AI provider
|
||||
interface TestApiHandler extends ApiHandler, SingleCompletionHandler {}
|
||||
|
||||
describe("TimeoutFallbackHandler - End-to-End AI Test", () => {
|
||||
test("should generate realistic AI suggestions for execute_command timeout", async () => {
|
||||
// Create a realistic mock API handler
|
||||
const mockApiHandler: TestApiHandler = {
|
||||
createMessage: vitest.fn(),
|
||||
getModel: vitest.fn().mockReturnValue({ id: "claude-3-sonnet", info: { maxTokens: 4096 } }),
|
||||
countTokens: vitest.fn().mockResolvedValue(150),
|
||||
completePrompt: vitest.fn().mockResolvedValue(
|
||||
`
|
||||
Here are some suggestions for the npm install timeout:
|
||||
|
||||
1. Clear npm cache with "npm cache clean --force" and retry
|
||||
2. Break installation into smaller chunks by installing packages individually
|
||||
3. Use "npm install --no-optional" to skip optional dependencies
|
||||
4. Check network connectivity and try with a different registry
|
||||
`.trim(),
|
||||
),
|
||||
}
|
||||
|
||||
const mockTask: Partial<Task> = {
|
||||
api: mockApiHandler,
|
||||
}
|
||||
|
||||
const context = {
|
||||
toolName: "execute_command" as const,
|
||||
timeoutMs: 60000,
|
||||
executionTimeMs: 65000,
|
||||
toolParams: {
|
||||
command: "npm install",
|
||||
cwd: "/project",
|
||||
},
|
||||
}
|
||||
|
||||
const result = await TimeoutFallbackHandler.generateAiFallback(context, mockTask as Task)
|
||||
|
||||
// Verify the result structure
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.toolCall).toBeDefined()
|
||||
expect(result.toolCall?.name).toBe("ask_followup_question")
|
||||
expect(result.toolCall?.params.question).toContain("execute_command")
|
||||
expect(result.toolCall?.params.question).toContain("60 seconds")
|
||||
|
||||
// Verify AI-generated suggestions are included
|
||||
const followUp = result.toolCall?.params.follow_up || ""
|
||||
expect(followUp).toContain("Clear npm cache")
|
||||
expect(followUp).toContain("Break installation into smaller chunks")
|
||||
expect(followUp).toContain("no-optional")
|
||||
expect(followUp).toContain("network connectivity")
|
||||
|
||||
// Verify the AI was called with a proper prompt
|
||||
expect(mockApiHandler.completePrompt).toHaveBeenCalledWith(
|
||||
expect.stringContaining("execute_command operation has timed out"),
|
||||
)
|
||||
expect(mockApiHandler.completePrompt).toHaveBeenCalledWith(expect.stringContaining("npm install"))
|
||||
expect(mockApiHandler.completePrompt).toHaveBeenCalledWith(expect.stringContaining("60 seconds"))
|
||||
})
|
||||
|
||||
test("should handle AI response with different formatting", async () => {
|
||||
// Mock AI response with different numbering style
|
||||
const mockApiHandler: TestApiHandler = {
|
||||
createMessage: vitest.fn(),
|
||||
getModel: vitest.fn().mockReturnValue({ id: "gpt-4", info: { maxTokens: 8192 } }),
|
||||
countTokens: vitest.fn().mockResolvedValue(200),
|
||||
completePrompt: vitest.fn().mockResolvedValue(
|
||||
`
|
||||
Based on the search_files timeout, here are my recommendations:
|
||||
|
||||
• Limit search to specific subdirectories instead of entire project
|
||||
• Use more specific regex patterns to reduce matches
|
||||
• Try list_files first to understand directory structure
|
||||
• Consider breaking search into multiple smaller operations
|
||||
`.trim(),
|
||||
),
|
||||
}
|
||||
|
||||
const mockTask: Partial<Task> = {
|
||||
api: mockApiHandler,
|
||||
}
|
||||
|
||||
const context = {
|
||||
toolName: "search_files" as const,
|
||||
timeoutMs: 30000,
|
||||
executionTimeMs: 32000,
|
||||
toolParams: {
|
||||
path: "/large-project",
|
||||
regex: ".*",
|
||||
file_pattern: "*.ts",
|
||||
},
|
||||
}
|
||||
|
||||
const result = await TimeoutFallbackHandler.generateAiFallback(context, mockTask as Task)
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.toolCall).toBeDefined()
|
||||
|
||||
// Should extract suggestions even with bullet points
|
||||
const followUp = result.toolCall?.params.follow_up || ""
|
||||
expect(followUp).toContain("Narrow the search scope")
|
||||
expect(followUp).toContain("simpler search patterns")
|
||||
expect(followUp).toContain("file type filters")
|
||||
expect(followUp).toContain("incrementally in smaller batches")
|
||||
})
|
||||
|
||||
test("should gracefully handle AI failure and use static fallback", async () => {
|
||||
// Mock API handler that fails
|
||||
const mockApiHandler: TestApiHandler = {
|
||||
createMessage: vitest.fn(),
|
||||
getModel: vitest.fn().mockReturnValue({ id: "test-model", info: { maxTokens: 4096 } }),
|
||||
countTokens: vitest.fn().mockResolvedValue(100),
|
||||
completePrompt: vitest.fn().mockRejectedValue(new Error("API rate limit exceeded")),
|
||||
}
|
||||
|
||||
const mockTask: Partial<Task> = {
|
||||
api: mockApiHandler,
|
||||
}
|
||||
|
||||
const context = {
|
||||
toolName: "read_file" as const,
|
||||
timeoutMs: 10000,
|
||||
executionTimeMs: 12000,
|
||||
toolParams: {
|
||||
path: "/very/large/file.log",
|
||||
},
|
||||
}
|
||||
|
||||
const result = await TimeoutFallbackHandler.generateAiFallback(context, mockTask as Task)
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.toolCall).toBeDefined()
|
||||
|
||||
// Should contain static fallback suggestions for read_file
|
||||
const followUp = result.toolCall?.params.follow_up || ""
|
||||
expect(followUp).toContain('Read "/very/large/file.log" in smaller chunks')
|
||||
expect(followUp).toContain("accessible and not locked")
|
||||
expect(followUp).toContain("different approach")
|
||||
expect(followUp).toContain("Increase the timeout")
|
||||
|
||||
// Verify AI was attempted but failed gracefully
|
||||
expect(mockApiHandler.completePrompt).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test("should work without task API handler", async () => {
|
||||
// Task without API handler
|
||||
const mockTask: Partial<Task> = {}
|
||||
|
||||
const context = {
|
||||
toolName: "browser_action" as const,
|
||||
timeoutMs: 15000,
|
||||
executionTimeMs: 16500,
|
||||
toolParams: {
|
||||
action: "click",
|
||||
coordinate: "450,300",
|
||||
},
|
||||
}
|
||||
|
||||
const result = await TimeoutFallbackHandler.generateAiFallback(context, mockTask as Task)
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.toolCall).toBeDefined()
|
||||
|
||||
// Should contain static fallback suggestions for browser_action
|
||||
const followUp = result.toolCall?.params.follow_up || ""
|
||||
expect(followUp).toContain('Simplify the "click"')
|
||||
expect(followUp).toContain("Wait for specific elements")
|
||||
expect(followUp).toContain("direct API calls")
|
||||
expect(followUp).toContain("Reset the browser session")
|
||||
})
|
||||
})
|
||||
554
src/core/timeout/__tests__/timeout-fallback.spec.ts
Normal file
554
src/core/timeout/__tests__/timeout-fallback.spec.ts
Normal file
|
|
@ -0,0 +1,554 @@
|
|||
// npx vitest run src/core/timeout/__tests__/timeout-fallback.spec.ts
|
||||
|
||||
import { describe, test, expect, beforeEach, vitest } from "vitest"
|
||||
import { TimeoutFallbackHandler } from "../TimeoutFallbackHandler"
|
||||
import type { TimeoutFallbackResult } from "../TimeoutFallbackHandler"
|
||||
import type { ApiHandler, SingleCompletionHandler } from "../../../api"
|
||||
import type { Task } from "../../task/Task"
|
||||
|
||||
// Create a mock API handler that extends ApiHandler and includes completePrompt
|
||||
interface MockApiHandler extends ApiHandler, SingleCompletionHandler {}
|
||||
|
||||
describe("TimeoutFallbackHandler", () => {
|
||||
let mockApiHandler: MockApiHandler
|
||||
let mockTask: Partial<Task>
|
||||
|
||||
beforeEach(() => {
|
||||
vitest.clearAllMocks()
|
||||
|
||||
// Mock API handler that simulates real AI responses
|
||||
mockApiHandler = {
|
||||
createMessage: vitest.fn(),
|
||||
getModel: vitest.fn().mockReturnValue({ id: "test-model", info: { maxTokens: 4096 } }),
|
||||
countTokens: vitest.fn().mockResolvedValue(100),
|
||||
completePrompt: vitest.fn(),
|
||||
}
|
||||
|
||||
// Mock task with API handler
|
||||
mockTask = {
|
||||
api: mockApiHandler,
|
||||
assistantMessageContent: [],
|
||||
cwd: "/test/dir",
|
||||
say: vitest.fn(),
|
||||
}
|
||||
})
|
||||
|
||||
describe("AI Fallback Generation", () => {
|
||||
test("should use AI to generate contextual suggestions when available", async () => {
|
||||
// Mock AI response with numbered suggestions
|
||||
const mockAiResponse = `Here are some suggestions for the timeout:
|
||||
|
||||
1. Break the npm install command into smaller package installations
|
||||
2. Clear npm cache and try again with npm cache clean --force
|
||||
3. Use npm install --no-optional to skip optional dependencies
|
||||
4. Check network connectivity and try with different registry`
|
||||
|
||||
;(mockApiHandler.completePrompt as any).mockResolvedValueOnce(mockAiResponse)
|
||||
|
||||
const context = {
|
||||
toolName: "execute_command" as const,
|
||||
timeoutMs: 30000,
|
||||
executionTimeMs: 35000,
|
||||
toolParams: { command: "npm install" },
|
||||
}
|
||||
|
||||
const result = await TimeoutFallbackHandler.generateAiFallback(context, mockTask as Task)
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.toolCall).toBeDefined()
|
||||
expect(result.toolCall?.name).toBe("ask_followup_question")
|
||||
expect(result.toolCall?.params.question).toContain("execute_command")
|
||||
expect(result.toolCall?.params.question).toContain("30 seconds")
|
||||
|
||||
// Check that AI-generated suggestions are included
|
||||
const followUp = result.toolCall?.params.follow_up || ""
|
||||
expect(followUp).toContain("Break the npm install command into smaller package installations")
|
||||
expect(followUp).toContain("Clear npm cache and try again")
|
||||
expect(followUp).toContain("Use npm install --no-optional")
|
||||
expect(followUp).toContain("Check network connectivity")
|
||||
|
||||
// Verify AI was called with proper prompt
|
||||
expect(mockApiHandler.completePrompt).toHaveBeenCalledWith(
|
||||
expect.stringContaining("execute_command operation has timed out"),
|
||||
)
|
||||
expect(mockApiHandler.completePrompt).toHaveBeenCalledWith(expect.stringContaining("npm install"))
|
||||
})
|
||||
|
||||
test("should fallback to static suggestions when AI fails", async () => {
|
||||
// Mock AI failure
|
||||
;(mockApiHandler.completePrompt as any).mockRejectedValueOnce(new Error("API Error"))
|
||||
|
||||
const context = {
|
||||
toolName: "execute_command" as const,
|
||||
timeoutMs: 30000,
|
||||
executionTimeMs: 35000,
|
||||
toolParams: { command: "npm test" },
|
||||
}
|
||||
|
||||
const result = await TimeoutFallbackHandler.generateAiFallback(context, mockTask as Task)
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.toolCall).toBeDefined()
|
||||
expect(result.toolCall?.name).toBe("ask_followup_question")
|
||||
|
||||
// Should contain static fallback suggestions
|
||||
const followUp = result.toolCall?.params.follow_up || ""
|
||||
expect(followUp).toContain('Break "npm test" into smaller')
|
||||
expect(followUp).toContain("background using")
|
||||
expect(followUp).toContain("alternative approach")
|
||||
expect(followUp).toContain("Increase the timeout")
|
||||
})
|
||||
|
||||
test("should fallback to static suggestions when API handler is unavailable", async () => {
|
||||
// Task without API handler
|
||||
const taskWithoutApi = {}
|
||||
|
||||
const context = {
|
||||
toolName: "read_file" as const,
|
||||
timeoutMs: 5000,
|
||||
executionTimeMs: 6000,
|
||||
toolParams: { path: "/large/file.txt" },
|
||||
}
|
||||
|
||||
const result = await TimeoutFallbackHandler.generateAiFallback(context, taskWithoutApi as Task)
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.toolCall).toBeDefined()
|
||||
|
||||
// Should contain static fallback suggestions for read_file
|
||||
const followUp = result.toolCall?.params.follow_up || ""
|
||||
expect(followUp).toContain('Read "/large/file.txt" in smaller chunks')
|
||||
expect(followUp).toContain("accessible and not locked")
|
||||
})
|
||||
|
||||
test("should parse AI response with different numbering formats", async () => {
|
||||
// Test different numbering formats
|
||||
const mockAiResponse = `Here are the suggestions:
|
||||
|
||||
1) Try breaking the command into parts
|
||||
2. Use a different approach
|
||||
3) Check system resources
|
||||
4. Increase timeout duration`
|
||||
|
||||
;(mockApiHandler.completePrompt as any).mockResolvedValueOnce(mockAiResponse)
|
||||
|
||||
const context = {
|
||||
toolName: "execute_command" as const,
|
||||
timeoutMs: 10000,
|
||||
executionTimeMs: 12000,
|
||||
toolParams: { command: "build script" },
|
||||
}
|
||||
|
||||
const result = await TimeoutFallbackHandler.generateAiFallback(context, mockTask as Task)
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
const followUp = result.toolCall?.params.follow_up || ""
|
||||
expect(followUp).toContain("Try breaking the command into parts")
|
||||
expect(followUp).toContain("Use a different approach")
|
||||
expect(followUp).toContain("Check system resources")
|
||||
expect(followUp).toContain("Increase timeout duration")
|
||||
})
|
||||
|
||||
test("should handle AI response without numbered list", async () => {
|
||||
// AI response without clear numbering
|
||||
const mockAiResponse = `You could try splitting the operation. Another option is to check the network. Maybe increase the timeout. Consider using a different tool.`
|
||||
|
||||
;(mockApiHandler.completePrompt as any).mockResolvedValueOnce(mockAiResponse)
|
||||
|
||||
const context = {
|
||||
toolName: "browser_action" as const,
|
||||
timeoutMs: 15000,
|
||||
executionTimeMs: 16000,
|
||||
toolParams: { action: "click" },
|
||||
}
|
||||
|
||||
const result = await TimeoutFallbackHandler.generateAiFallback(context, mockTask as Task)
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
const followUp = result.toolCall?.params.follow_up || ""
|
||||
|
||||
// Should extract sentences as suggestions
|
||||
expect(followUp).toContain("You could try splitting the operation")
|
||||
expect(followUp).toContain("Another option is to check the network")
|
||||
})
|
||||
|
||||
test("should include task context in AI prompt when available", async () => {
|
||||
const mockAiResponse = `1. Try a different approach\n2. Check the working directory\n3. Break into steps`
|
||||
;(mockApiHandler.completePrompt as any).mockResolvedValueOnce(mockAiResponse)
|
||||
|
||||
const context = {
|
||||
toolName: "search_files" as const,
|
||||
timeoutMs: 20000,
|
||||
executionTimeMs: 22000,
|
||||
toolParams: { path: "/project", regex: ".*\\.ts$" },
|
||||
taskContext: {
|
||||
currentStep: "Finding TypeScript files",
|
||||
workingDirectory: "/project/src",
|
||||
previousActions: ["read package.json", "list files"],
|
||||
},
|
||||
}
|
||||
|
||||
const result = await TimeoutFallbackHandler.generateAiFallback(context, mockTask as Task)
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
|
||||
// Verify the prompt included task context
|
||||
const calledPrompt = (mockApiHandler.completePrompt as any).mock.calls[0][0]
|
||||
expect(calledPrompt).toContain("Current step: Finding TypeScript files")
|
||||
expect(calledPrompt).toContain("Working directory: /project/src")
|
||||
expect(calledPrompt).toContain("search_files")
|
||||
expect(calledPrompt).toContain("ts$") // Just check for the pattern ending
|
||||
})
|
||||
|
||||
test("should limit suggestions to maximum of 4", async () => {
|
||||
// AI response with many suggestions
|
||||
const mockAiResponse = `Here are many suggestions:
|
||||
|
||||
1. First suggestion
|
||||
2. Second suggestion
|
||||
3. Third suggestion
|
||||
4. Fourth suggestion
|
||||
5. Fifth suggestion
|
||||
6. Sixth suggestion
|
||||
7. Seventh suggestion`
|
||||
|
||||
;(mockApiHandler.completePrompt as any).mockResolvedValueOnce(mockAiResponse)
|
||||
|
||||
const context = {
|
||||
toolName: "write_to_file" as const,
|
||||
timeoutMs: 8000,
|
||||
executionTimeMs: 9000,
|
||||
toolParams: { path: "/output.txt" },
|
||||
}
|
||||
|
||||
const result = await TimeoutFallbackHandler.generateAiFallback(context, mockTask as Task)
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
const followUp = result.toolCall?.params.follow_up || ""
|
||||
|
||||
// Count the number of <suggest> tags
|
||||
const suggestCount = (followUp.match(/<suggest>/g) || []).length
|
||||
expect(suggestCount).toBeLessThanOrEqual(4)
|
||||
|
||||
// Should include first 4 suggestions
|
||||
expect(followUp).toContain("First suggestion")
|
||||
expect(followUp).toContain("Fourth suggestion")
|
||||
// Should not include 5th and beyond
|
||||
expect(followUp).not.toContain("Fifth suggestion")
|
||||
})
|
||||
})
|
||||
|
||||
describe("End-to-End AI Tests", () => {
|
||||
test("should generate realistic AI suggestions for execute_command timeout", async () => {
|
||||
// Create a realistic mock API handler
|
||||
const testApiHandler: MockApiHandler = {
|
||||
createMessage: vitest.fn(),
|
||||
getModel: vitest.fn().mockReturnValue({ id: "claude-3-sonnet", info: { maxTokens: 4096 } }),
|
||||
countTokens: vitest.fn().mockResolvedValue(150),
|
||||
completePrompt: vitest.fn().mockResolvedValue(
|
||||
`
|
||||
Here are some suggestions for the npm install timeout:
|
||||
|
||||
1. Clear npm cache with "npm cache clean --force" and retry
|
||||
2. Break installation into smaller chunks by installing packages individually
|
||||
3. Use "npm install --no-optional" to skip optional dependencies
|
||||
4. Check network connectivity and try with a different registry
|
||||
`.trim(),
|
||||
),
|
||||
}
|
||||
|
||||
const testTask: Partial<Task> = {
|
||||
api: testApiHandler,
|
||||
}
|
||||
|
||||
const context = {
|
||||
toolName: "execute_command" as const,
|
||||
timeoutMs: 60000,
|
||||
executionTimeMs: 65000,
|
||||
toolParams: {
|
||||
command: "npm install",
|
||||
cwd: "/project",
|
||||
},
|
||||
}
|
||||
|
||||
const result = await TimeoutFallbackHandler.generateAiFallback(context, testTask as Task)
|
||||
|
||||
// Verify the result structure
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.toolCall).toBeDefined()
|
||||
expect(result.toolCall?.name).toBe("ask_followup_question")
|
||||
expect(result.toolCall?.params.question).toContain("execute_command")
|
||||
expect(result.toolCall?.params.question).toContain("60 seconds")
|
||||
|
||||
// Verify AI-generated suggestions are included
|
||||
const followUp = result.toolCall?.params.follow_up || ""
|
||||
expect(followUp).toContain("Clear npm cache")
|
||||
expect(followUp).toContain("Break installation into smaller chunks")
|
||||
expect(followUp).toContain("no-optional")
|
||||
expect(followUp).toContain("network connectivity")
|
||||
|
||||
// Verify the AI was called with a proper prompt
|
||||
expect(testApiHandler.completePrompt).toHaveBeenCalledWith(
|
||||
expect.stringContaining("execute_command operation has timed out"),
|
||||
)
|
||||
expect(testApiHandler.completePrompt).toHaveBeenCalledWith(expect.stringContaining("npm install"))
|
||||
expect(testApiHandler.completePrompt).toHaveBeenCalledWith(expect.stringContaining("60 seconds"))
|
||||
})
|
||||
|
||||
test("should handle AI response with different formatting", async () => {
|
||||
// Mock AI response with different numbering style
|
||||
const testApiHandler: MockApiHandler = {
|
||||
createMessage: vitest.fn(),
|
||||
getModel: vitest.fn().mockReturnValue({ id: "gpt-4", info: { maxTokens: 8192 } }),
|
||||
countTokens: vitest.fn().mockResolvedValue(200),
|
||||
completePrompt: vitest.fn().mockResolvedValue(
|
||||
`
|
||||
Based on the search_files timeout, here are my recommendations:
|
||||
|
||||
• Limit search to specific subdirectories instead of entire project
|
||||
• Use more specific regex patterns to reduce matches
|
||||
• Try list_files first to understand directory structure
|
||||
• Consider breaking search into multiple smaller operations
|
||||
`.trim(),
|
||||
),
|
||||
}
|
||||
|
||||
const testTask: Partial<Task> = {
|
||||
api: testApiHandler,
|
||||
}
|
||||
|
||||
const context = {
|
||||
toolName: "search_files" as const,
|
||||
timeoutMs: 30000,
|
||||
executionTimeMs: 32000,
|
||||
toolParams: {
|
||||
path: "/large-project",
|
||||
regex: ".*",
|
||||
file_pattern: "*.ts",
|
||||
},
|
||||
}
|
||||
|
||||
const result = await TimeoutFallbackHandler.generateAiFallback(context, testTask as Task)
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.toolCall).toBeDefined()
|
||||
|
||||
// Should extract suggestions even with bullet points
|
||||
const followUp = result.toolCall?.params.follow_up || ""
|
||||
expect(followUp).toContain("Narrow the search scope")
|
||||
expect(followUp).toContain("simpler search patterns")
|
||||
expect(followUp).toContain("file type filters")
|
||||
expect(followUp).toContain("incrementally in smaller batches")
|
||||
})
|
||||
|
||||
test("should gracefully handle AI failure and use static fallback", async () => {
|
||||
// Mock API handler that fails
|
||||
const testApiHandler: MockApiHandler = {
|
||||
createMessage: vitest.fn(),
|
||||
getModel: vitest.fn().mockReturnValue({ id: "test-model", info: { maxTokens: 4096 } }),
|
||||
countTokens: vitest.fn().mockResolvedValue(100),
|
||||
completePrompt: vitest.fn().mockRejectedValue(new Error("API rate limit exceeded")),
|
||||
}
|
||||
|
||||
const testTask: Partial<Task> = {
|
||||
api: testApiHandler,
|
||||
}
|
||||
|
||||
const context = {
|
||||
toolName: "read_file" as const,
|
||||
timeoutMs: 10000,
|
||||
executionTimeMs: 12000,
|
||||
toolParams: {
|
||||
path: "/very/large/file.log",
|
||||
},
|
||||
}
|
||||
|
||||
const result = await TimeoutFallbackHandler.generateAiFallback(context, testTask as Task)
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.toolCall).toBeDefined()
|
||||
|
||||
// Should contain static fallback suggestions for read_file
|
||||
const followUp = result.toolCall?.params.follow_up || ""
|
||||
expect(followUp).toContain('Read "/very/large/file.log" in smaller chunks')
|
||||
expect(followUp).toContain("accessible and not locked")
|
||||
expect(followUp).toContain("different approach")
|
||||
expect(followUp).toContain("Increase the timeout")
|
||||
|
||||
// Verify AI was attempted but failed gracefully
|
||||
expect(testApiHandler.completePrompt).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test("should work without task API handler", async () => {
|
||||
// Task without API handler
|
||||
const testTask: Partial<Task> = {}
|
||||
|
||||
const context = {
|
||||
toolName: "browser_action" as const,
|
||||
timeoutMs: 15000,
|
||||
executionTimeMs: 16500,
|
||||
toolParams: {
|
||||
action: "click",
|
||||
coordinate: "450,300",
|
||||
},
|
||||
}
|
||||
|
||||
const result = await TimeoutFallbackHandler.generateAiFallback(context, testTask as Task)
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.toolCall).toBeDefined()
|
||||
|
||||
// Should contain static fallback suggestions for browser_action
|
||||
const followUp = result.toolCall?.params.follow_up || ""
|
||||
expect(followUp).toContain('Simplify the "click"')
|
||||
expect(followUp).toContain("Wait for specific elements")
|
||||
expect(followUp).toContain("direct API calls")
|
||||
expect(followUp).toContain("Reset the browser session")
|
||||
})
|
||||
})
|
||||
|
||||
describe("Tool Call Response Generation", () => {
|
||||
test("should return response with ask_followup_question tool instructions", async () => {
|
||||
// Mock the TimeoutFallbackHandler to return a successful AI result
|
||||
const mockAiResult: TimeoutFallbackResult = {
|
||||
success: true,
|
||||
toolCall: {
|
||||
name: "ask_followup_question",
|
||||
params: {
|
||||
question:
|
||||
"The execute_command operation timed out after 5 seconds. How would you like to proceed?",
|
||||
follow_up:
|
||||
"<suggest>Try a different approach</suggest>\n<suggest>Break into smaller steps</suggest>",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Spy on the generateAiFallback method
|
||||
const generateSpy = vitest.spyOn(TimeoutFallbackHandler, "generateAiFallback")
|
||||
generateSpy.mockResolvedValue(mockAiResult)
|
||||
|
||||
// Call createTimeoutResponse
|
||||
const response = await TimeoutFallbackHandler.createTimeoutResponse(
|
||||
"execute_command",
|
||||
5000,
|
||||
6000,
|
||||
{ command: "npm install" },
|
||||
mockTask as Task,
|
||||
)
|
||||
|
||||
// Check that the response contains the base timeout message
|
||||
expect(response).toContain("timed out after 5 seconds")
|
||||
expect(response).toContain("Execution Time: 6s")
|
||||
|
||||
// Check that the response includes instructions to use ask_followup_question
|
||||
expect(response).toContain("You MUST now use the ask_followup_question tool")
|
||||
expect(response).toContain("<ask_followup_question>")
|
||||
expect(response).toContain(`<question>${mockAiResult.toolCall?.params.question}</question>`)
|
||||
expect(response).toContain("<follow_up>")
|
||||
expect(response).toContain(mockAiResult.toolCall?.params.follow_up)
|
||||
expect(response).toContain("</follow_up>")
|
||||
expect(response).toContain("</ask_followup_question>")
|
||||
expect(response).toContain("This is required to help the user decide how to proceed after the timeout.")
|
||||
|
||||
// Verify that assistantMessageContent was NOT modified
|
||||
expect(mockTask.assistantMessageContent).toHaveLength(0)
|
||||
|
||||
generateSpy.mockRestore()
|
||||
})
|
||||
|
||||
test("should return fallback message when AI generation fails", async () => {
|
||||
// Spy on the generateAiFallback method to return a failure
|
||||
const generateSpy = vitest.spyOn(TimeoutFallbackHandler, "generateAiFallback")
|
||||
generateSpy.mockResolvedValue({
|
||||
success: false,
|
||||
error: "AI generation failed",
|
||||
})
|
||||
|
||||
// Call createTimeoutResponse
|
||||
const response = await TimeoutFallbackHandler.createTimeoutResponse(
|
||||
"execute_command",
|
||||
5000,
|
||||
6000,
|
||||
{ command: "npm install" },
|
||||
mockTask as Task,
|
||||
)
|
||||
|
||||
// Check that the response contains the base timeout message
|
||||
expect(response).toContain("timed out after 5 seconds")
|
||||
expect(response).toContain("Execution Time: 6s")
|
||||
|
||||
// Check that the response contains the fallback message
|
||||
expect(response).toContain(
|
||||
"The operation timed out. Please consider breaking this into smaller steps or trying a different approach.",
|
||||
)
|
||||
|
||||
// Should not contain ask_followup_question instructions
|
||||
expect(response).not.toContain("ask_followup_question")
|
||||
|
||||
generateSpy.mockRestore()
|
||||
})
|
||||
})
|
||||
|
||||
describe("UI Integration", () => {
|
||||
test("should generate AI fallbacks using static method", async () => {
|
||||
const context = {
|
||||
toolName: "execute_command" as const,
|
||||
timeoutMs: 30000,
|
||||
executionTimeMs: 25000,
|
||||
toolParams: { command: "npm install" },
|
||||
}
|
||||
|
||||
const result = await TimeoutFallbackHandler.generateAiFallback(context)
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.toolCall).toBeDefined()
|
||||
expect(result.toolCall?.name).toBe("ask_followup_question")
|
||||
expect(result.toolCall?.params.question).toContain("execute_command")
|
||||
})
|
||||
|
||||
test("should create timeout response with AI fallbacks", async () => {
|
||||
const response = await TimeoutFallbackHandler.createTimeoutResponse("execute_command", 30000, 25000, {
|
||||
command: "npm install",
|
||||
})
|
||||
|
||||
expect(response).toContain("execute_command")
|
||||
expect(response).toContain("timed out")
|
||||
expect(response.length).toBeGreaterThan(100) // Should contain substantial content
|
||||
})
|
||||
|
||||
test("should handle different tool types with AI fallbacks", async () => {
|
||||
const commandResponse = await TimeoutFallbackHandler.createTimeoutResponse(
|
||||
"execute_command",
|
||||
30000,
|
||||
25000,
|
||||
{
|
||||
command: "npm test",
|
||||
},
|
||||
)
|
||||
|
||||
const browserResponse = await TimeoutFallbackHandler.createTimeoutResponse("browser_action", 30000, 25000, {
|
||||
action: "click",
|
||||
})
|
||||
|
||||
expect(commandResponse).toContain("execute_command")
|
||||
expect(browserResponse).toContain("browser_action")
|
||||
expect(commandResponse).not.toEqual(browserResponse)
|
||||
})
|
||||
|
||||
test("should validate UI setting flow", () => {
|
||||
// This test validates that timeout settings can be toggled
|
||||
const settings = {
|
||||
timeoutFallbackEnabled: true,
|
||||
toolExecutionTimeoutMs: 30000,
|
||||
}
|
||||
|
||||
// Simulate UI toggle
|
||||
settings.timeoutFallbackEnabled = false
|
||||
expect(settings.timeoutFallbackEnabled).toBe(false)
|
||||
|
||||
// Simulate timeout duration change
|
||||
settings.toolExecutionTimeoutMs = 60000
|
||||
expect(settings.toolExecutionTimeoutMs).toBe(60000)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -11,163 +11,264 @@ describe("Timeout Integration Tests", () => {
|
|||
vitest.clearAllMocks()
|
||||
})
|
||||
|
||||
test("TimeoutManager should handle basic timeout operations", async () => {
|
||||
const manager = TimeoutManager.getInstance()
|
||||
describe("TimeoutManager Integration", () => {
|
||||
test("should handle basic timeout operations", async () => {
|
||||
const manager = TimeoutManager.getInstance()
|
||||
|
||||
// Test successful operation within timeout
|
||||
const result = await manager.executeWithTimeout(
|
||||
async () => {
|
||||
// Test successful operation within timeout
|
||||
const result = await manager.executeWithTimeout(
|
||||
async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 10))
|
||||
return "success"
|
||||
},
|
||||
{
|
||||
toolName: "execute_command",
|
||||
timeoutMs: 100,
|
||||
enableFallback: true,
|
||||
},
|
||||
)
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.result).toBe("success")
|
||||
expect(result.timedOut).toBe(false)
|
||||
})
|
||||
|
||||
test("should handle timeout scenarios", async () => {
|
||||
const manager = TimeoutManager.getInstance()
|
||||
|
||||
// Test operation that times out
|
||||
const result = await manager.executeWithTimeout(
|
||||
async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 200))
|
||||
return "should not reach here"
|
||||
},
|
||||
{
|
||||
toolName: "execute_command",
|
||||
timeoutMs: 50,
|
||||
enableFallback: true,
|
||||
},
|
||||
)
|
||||
|
||||
expect(result.success).toBe(false)
|
||||
expect(result.timedOut).toBe(true)
|
||||
expect(result.error?.message).toContain("Operation timed out")
|
||||
})
|
||||
})
|
||||
|
||||
describe("ToolExecutionWrapper Integration", () => {
|
||||
test("should wrap operations correctly", async () => {
|
||||
const mockOperation = vitest.fn().mockImplementation(async (signal: AbortSignal) => {
|
||||
// Simulate checking abort signal
|
||||
if (signal.aborted) {
|
||||
throw new Error("Operation was aborted")
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 10))
|
||||
return "success"
|
||||
},
|
||||
{
|
||||
toolName: "execute_command",
|
||||
timeoutMs: 100,
|
||||
enableFallback: true,
|
||||
},
|
||||
)
|
||||
return [false, "test result"]
|
||||
})
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.result).toBe("success")
|
||||
expect(result.timedOut).toBe(false)
|
||||
})
|
||||
const result = await ToolExecutionWrapper.execute(
|
||||
mockOperation,
|
||||
{
|
||||
toolName: "execute_command",
|
||||
taskId: "test-task",
|
||||
timeoutMs: 100,
|
||||
enableFallback: true,
|
||||
},
|
||||
100,
|
||||
)
|
||||
|
||||
test("TimeoutManager should handle timeout scenarios", async () => {
|
||||
const manager = TimeoutManager.getInstance()
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.result).toEqual([false, "test result"])
|
||||
expect(mockOperation).toHaveBeenCalledWith(expect.any(AbortSignal))
|
||||
})
|
||||
|
||||
// Test operation that times out
|
||||
const result = await manager.executeWithTimeout(
|
||||
async () => {
|
||||
test("should handle timeout with fallback", async () => {
|
||||
const mockOperation = vitest.fn().mockImplementation(async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 200))
|
||||
return "should not reach here"
|
||||
},
|
||||
{
|
||||
toolName: "execute_command",
|
||||
timeoutMs: 50,
|
||||
enableFallback: true,
|
||||
},
|
||||
)
|
||||
return [false, "should not reach here"]
|
||||
})
|
||||
|
||||
expect(result.success).toBe(false)
|
||||
expect(result.timedOut).toBe(true)
|
||||
expect(result.error?.message).toContain("Operation timed out")
|
||||
})
|
||||
const result = await ToolExecutionWrapper.execute(
|
||||
mockOperation,
|
||||
{
|
||||
toolName: "execute_command",
|
||||
taskId: "test-task",
|
||||
timeoutMs: 50,
|
||||
enableFallback: true,
|
||||
},
|
||||
50,
|
||||
)
|
||||
|
||||
test("ToolExecutionWrapper should wrap operations correctly", async () => {
|
||||
const mockOperation = vitest.fn().mockImplementation(async (signal: AbortSignal) => {
|
||||
// Simulate checking abort signal
|
||||
if (signal.aborted) {
|
||||
throw new Error("Operation was aborted")
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 10))
|
||||
return [false, "test result"]
|
||||
expect(result.success).toBe(false)
|
||||
expect(result.timedOut).toBe(true)
|
||||
expect(result.fallbackTriggered).toBe(true)
|
||||
})
|
||||
|
||||
const result = await ToolExecutionWrapper.execute(
|
||||
mockOperation,
|
||||
{
|
||||
toolName: "execute_command",
|
||||
taskId: "test-task",
|
||||
timeoutMs: 100,
|
||||
enableFallback: true,
|
||||
},
|
||||
100,
|
||||
)
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.result).toEqual([false, "test result"])
|
||||
expect(mockOperation).toHaveBeenCalledWith(expect.any(AbortSignal))
|
||||
})
|
||||
|
||||
test("ToolExecutionWrapper should handle timeout with fallback", async () => {
|
||||
const mockOperation = vitest.fn().mockImplementation(async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 200))
|
||||
return [false, "should not reach here"]
|
||||
describe("TimeoutFallbackHandler Integration", () => {
|
||||
test("should create AI-powered responses", async () => {
|
||||
// Create a mock task to test tool injection
|
||||
const mockTask = {
|
||||
assistantMessageContent: [],
|
||||
cwd: "/test/dir",
|
||||
say: vitest.fn().mockResolvedValue(undefined),
|
||||
} as unknown as Task
|
||||
|
||||
const response = await TimeoutFallbackHandler.createTimeoutResponse(
|
||||
"execute_command",
|
||||
5000,
|
||||
6000,
|
||||
{ command: "npm install" },
|
||||
mockTask,
|
||||
)
|
||||
|
||||
// The response should contain the basic timeout information
|
||||
expect(response).toContain("execute_command")
|
||||
expect(response).toContain("5 seconds")
|
||||
expect(response).toContain("6s")
|
||||
expect(response.length).toBeGreaterThan(50)
|
||||
|
||||
// The response should now contain instructions to use ask_followup_question
|
||||
expect(response).toContain("You MUST now use the ask_followup_question tool")
|
||||
expect(response).toContain("<ask_followup_question>")
|
||||
expect(response).toContain("<question>")
|
||||
expect(response).toContain("timed out")
|
||||
expect(response).toContain("</question>")
|
||||
expect(response).toContain("<follow_up>")
|
||||
expect(response).toContain("</follow_up>")
|
||||
expect(response).toContain("</ask_followup_question>")
|
||||
|
||||
// Verify that assistantMessageContent was NOT modified
|
||||
expect(mockTask.assistantMessageContent).toHaveLength(0)
|
||||
})
|
||||
|
||||
const result = await ToolExecutionWrapper.execute(
|
||||
mockOperation,
|
||||
{
|
||||
toolName: "execute_command",
|
||||
taskId: "test-task",
|
||||
timeoutMs: 50,
|
||||
enableFallback: true,
|
||||
},
|
||||
50,
|
||||
)
|
||||
|
||||
expect(result.success).toBe(false)
|
||||
expect(result.timedOut).toBe(true)
|
||||
expect(result.fallbackTriggered).toBe(true)
|
||||
})
|
||||
|
||||
test("TimeoutFallbackHandler should create AI-powered responses", async () => {
|
||||
// Create a mock task to test tool injection
|
||||
const mockTask = {
|
||||
assistantMessageContent: [],
|
||||
cwd: "/test/dir",
|
||||
say: vitest.fn().mockResolvedValue(undefined),
|
||||
} as unknown as Task
|
||||
describe("AbortSignal Integration", () => {
|
||||
test("should be properly handled", async () => {
|
||||
const mockOperation = vitest.fn().mockImplementation(async (signal: AbortSignal) => {
|
||||
// Simulate a long-running operation that checks abort signal
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
if (signal.aborted) {
|
||||
reject(new Error("Operation was aborted"))
|
||||
} else {
|
||||
resolve("success")
|
||||
}
|
||||
}, 100)
|
||||
|
||||
const response = await TimeoutFallbackHandler.createTimeoutResponse(
|
||||
"execute_command",
|
||||
5000,
|
||||
6000,
|
||||
{ command: "npm install" },
|
||||
mockTask,
|
||||
)
|
||||
|
||||
// The response should contain the basic timeout information
|
||||
expect(response).toContain("execute_command")
|
||||
expect(response).toContain("5 seconds")
|
||||
expect(response).toContain("6s")
|
||||
expect(response.length).toBeGreaterThan(50)
|
||||
|
||||
// The response should now contain instructions to use ask_followup_question
|
||||
expect(response).toContain("You MUST now use the ask_followup_question tool")
|
||||
expect(response).toContain("<ask_followup_question>")
|
||||
expect(response).toContain("<question>")
|
||||
expect(response).toContain("timed out")
|
||||
expect(response).toContain("</question>")
|
||||
expect(response).toContain("<follow_up>")
|
||||
expect(response).toContain("</follow_up>")
|
||||
expect(response).toContain("</ask_followup_question>")
|
||||
|
||||
// Verify that assistantMessageContent was NOT modified
|
||||
expect(mockTask.assistantMessageContent).toHaveLength(0)
|
||||
})
|
||||
|
||||
test("AbortSignal should be properly handled", async () => {
|
||||
const mockOperation = vitest.fn().mockImplementation(async (signal: AbortSignal) => {
|
||||
// Simulate a long-running operation that checks abort signal
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
if (signal.aborted) {
|
||||
signal.addEventListener("abort", () => {
|
||||
clearTimeout(timeout)
|
||||
reject(new Error("Operation was aborted"))
|
||||
} else {
|
||||
resolve("success")
|
||||
}
|
||||
}, 100)
|
||||
|
||||
signal.addEventListener("abort", () => {
|
||||
clearTimeout(timeout)
|
||||
reject(new Error("Operation was aborted"))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
const result = await ToolExecutionWrapper.execute(
|
||||
mockOperation,
|
||||
{
|
||||
toolName: "execute_command",
|
||||
taskId: "test-task",
|
||||
timeoutMs: 50, // Shorter timeout to trigger abort
|
||||
enableFallback: false,
|
||||
},
|
||||
50,
|
||||
)
|
||||
|
||||
expect(result.success).toBe(false)
|
||||
expect(result.timedOut).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe("Cross-Component Integration", () => {
|
||||
test("should coordinate between TimeoutManager and ToolExecutionWrapper", async () => {
|
||||
const manager = TimeoutManager.getInstance()
|
||||
|
||||
const mockOperation = vitest.fn().mockImplementation(async (signal: AbortSignal) => {
|
||||
// Simulate operation that respects abort signal
|
||||
await new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(resolve, 150)
|
||||
signal.addEventListener("abort", () => {
|
||||
clearTimeout(timeout)
|
||||
reject(new Error("Operation was aborted"))
|
||||
})
|
||||
})
|
||||
return [false, "completed"]
|
||||
})
|
||||
|
||||
// Use ToolExecutionWrapper directly
|
||||
const result = await ToolExecutionWrapper.execute(
|
||||
mockOperation,
|
||||
{
|
||||
toolName: "read_file",
|
||||
taskId: "integration-test",
|
||||
timeoutMs: 100,
|
||||
enableFallback: true,
|
||||
},
|
||||
100,
|
||||
)
|
||||
|
||||
expect(result.success).toBe(false)
|
||||
expect(result.timedOut).toBe(true)
|
||||
})
|
||||
|
||||
const result = await ToolExecutionWrapper.execute(
|
||||
mockOperation,
|
||||
{
|
||||
toolName: "execute_command",
|
||||
taskId: "test-task",
|
||||
timeoutMs: 50, // Shorter timeout to trigger abort
|
||||
enableFallback: false,
|
||||
},
|
||||
50,
|
||||
)
|
||||
test("should handle nested timeout scenarios", async () => {
|
||||
const manager = TimeoutManager.getInstance()
|
||||
|
||||
expect(result.success).toBe(false)
|
||||
expect(result.timedOut).toBe(true)
|
||||
// Test nested timeout operations
|
||||
const outerResult = await manager.executeWithTimeout(
|
||||
async () => {
|
||||
// Inner timeout operation
|
||||
return await manager.executeWithTimeout(
|
||||
async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 150))
|
||||
return "inner success"
|
||||
},
|
||||
{
|
||||
toolName: "search_files",
|
||||
timeoutMs: 100,
|
||||
enableFallback: true,
|
||||
},
|
||||
)
|
||||
},
|
||||
{
|
||||
toolName: "execute_command",
|
||||
timeoutMs: 200,
|
||||
enableFallback: true,
|
||||
},
|
||||
)
|
||||
|
||||
// The inner operation should timeout, but the outer should succeed with the timeout result
|
||||
expect(outerResult.success).toBe(true)
|
||||
if (outerResult.result) {
|
||||
expect(outerResult.result.success).toBe(false)
|
||||
expect(outerResult.result.timedOut).toBe(true)
|
||||
}
|
||||
})
|
||||
|
||||
test("should maintain timeout event tracking across components", async () => {
|
||||
const manager = TimeoutManager.getInstance()
|
||||
manager.clearLastTimeoutEvent()
|
||||
|
||||
// Execute operation that will timeout
|
||||
await manager.executeWithTimeout(
|
||||
async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 200))
|
||||
return "should timeout"
|
||||
},
|
||||
{
|
||||
toolName: "browser_action",
|
||||
timeoutMs: 50,
|
||||
enableFallback: true,
|
||||
taskId: "tracking-test",
|
||||
},
|
||||
)
|
||||
|
||||
// Verify timeout event was tracked
|
||||
const timeoutEvent = manager.getLastTimeoutEvent()
|
||||
expect(timeoutEvent).toBeTruthy()
|
||||
expect(timeoutEvent?.toolName).toBe("browser_action")
|
||||
expect(timeoutEvent?.taskId).toBe("tracking-test")
|
||||
expect(timeoutEvent?.timeoutMs).toBe(50)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
178
src/core/timeout/__tests__/timeout-manager.spec.ts
Normal file
178
src/core/timeout/__tests__/timeout-manager.spec.ts
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
// npx vitest run src/core/timeout/__tests__/timeout-manager.spec.ts
|
||||
|
||||
import { describe, test, expect, beforeEach, vitest } from "vitest"
|
||||
import { TimeoutManager } from "../TimeoutManager"
|
||||
|
||||
describe("TimeoutManager", () => {
|
||||
let manager: TimeoutManager
|
||||
|
||||
beforeEach(() => {
|
||||
manager = TimeoutManager.getInstance()
|
||||
manager.clearLastTimeoutEvent()
|
||||
vitest.clearAllMocks()
|
||||
})
|
||||
|
||||
describe("Basic Operations", () => {
|
||||
test("should handle successful operation within timeout", async () => {
|
||||
const result = await manager.executeWithTimeout(
|
||||
async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 10))
|
||||
return "success"
|
||||
},
|
||||
{
|
||||
toolName: "execute_command",
|
||||
timeoutMs: 100,
|
||||
enableFallback: true,
|
||||
},
|
||||
)
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.result).toBe("success")
|
||||
expect(result.timedOut).toBe(false)
|
||||
})
|
||||
|
||||
test("should handle timeout scenarios", async () => {
|
||||
const result = await manager.executeWithTimeout(
|
||||
async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 200))
|
||||
return "should not reach here"
|
||||
},
|
||||
{
|
||||
toolName: "execute_command",
|
||||
timeoutMs: 50,
|
||||
enableFallback: true,
|
||||
},
|
||||
)
|
||||
|
||||
expect(result.success).toBe(false)
|
||||
expect(result.timedOut).toBe(true)
|
||||
expect(result.error?.message).toContain("Operation timed out")
|
||||
})
|
||||
})
|
||||
|
||||
describe("Timeout Event Tracking", () => {
|
||||
test("should track only the last timeout event", async () => {
|
||||
// First timeout
|
||||
await manager.executeWithTimeout(
|
||||
async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 200))
|
||||
return "should timeout"
|
||||
},
|
||||
{
|
||||
toolName: "execute_command",
|
||||
timeoutMs: 50,
|
||||
enableFallback: true,
|
||||
taskId: "task-1",
|
||||
},
|
||||
)
|
||||
|
||||
const firstTimeout = manager.getLastTimeoutEvent()
|
||||
expect(firstTimeout).toBeTruthy()
|
||||
expect(firstTimeout?.toolName).toBe("execute_command")
|
||||
expect(firstTimeout?.taskId).toBe("task-1")
|
||||
|
||||
// Second timeout should replace the first
|
||||
await manager.executeWithTimeout(
|
||||
async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 200))
|
||||
return "should timeout"
|
||||
},
|
||||
{
|
||||
toolName: "read_file",
|
||||
timeoutMs: 50,
|
||||
enableFallback: true,
|
||||
taskId: "task-2",
|
||||
},
|
||||
)
|
||||
|
||||
const secondTimeout = manager.getLastTimeoutEvent()
|
||||
expect(secondTimeout).toBeTruthy()
|
||||
expect(secondTimeout?.toolName).toBe("read_file")
|
||||
expect(secondTimeout?.taskId).toBe("task-2")
|
||||
expect(secondTimeout?.timestamp).toBeGreaterThan(firstTimeout!.timestamp)
|
||||
})
|
||||
|
||||
test("should clear last timeout event", async () => {
|
||||
// Create a timeout
|
||||
await manager.executeWithTimeout(
|
||||
async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 200))
|
||||
return "should timeout"
|
||||
},
|
||||
{
|
||||
toolName: "execute_command",
|
||||
timeoutMs: 50,
|
||||
enableFallback: true,
|
||||
},
|
||||
)
|
||||
|
||||
expect(manager.getLastTimeoutEvent()).toBeTruthy()
|
||||
|
||||
// Clear it
|
||||
manager.clearLastTimeoutEvent()
|
||||
expect(manager.getLastTimeoutEvent()).toBeNull()
|
||||
})
|
||||
|
||||
test("should return null when no timeout has occurred", () => {
|
||||
expect(manager.getLastTimeoutEvent()).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe("Operation Management", () => {
|
||||
test("cancelOperation should work with simplified operation IDs", async () => {
|
||||
// Start a long-running operation
|
||||
const operationPromise = manager.executeWithTimeout(
|
||||
async (signal) => {
|
||||
await new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(resolve, 1000)
|
||||
signal.addEventListener("abort", () => {
|
||||
clearTimeout(timeout)
|
||||
reject(new Error("Operation was aborted"))
|
||||
})
|
||||
})
|
||||
return "should be cancelled"
|
||||
},
|
||||
{
|
||||
toolName: "execute_command",
|
||||
timeoutMs: 2000,
|
||||
enableFallback: true,
|
||||
taskId: "cancel-test",
|
||||
},
|
||||
)
|
||||
|
||||
// Cancel it immediately
|
||||
const cancelled = manager.cancelOperation("execute_command", "cancel-test")
|
||||
expect(cancelled).toBe(true)
|
||||
|
||||
// Verify it was cancelled
|
||||
const result = await operationPromise
|
||||
expect(result.success).toBe(false)
|
||||
expect(result.error?.message).toContain("Operation was aborted")
|
||||
})
|
||||
|
||||
test("isOperationActive should work with simplified operation IDs", async () => {
|
||||
// Start an operation
|
||||
const operationPromise = manager.executeWithTimeout(
|
||||
async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
return "success"
|
||||
},
|
||||
{
|
||||
toolName: "read_file",
|
||||
timeoutMs: 200,
|
||||
enableFallback: true,
|
||||
taskId: "active-test",
|
||||
},
|
||||
)
|
||||
|
||||
// Check if it's active
|
||||
expect(manager.isOperationActive("read_file", "active-test")).toBe(true)
|
||||
|
||||
// Wait for completion
|
||||
await operationPromise
|
||||
|
||||
// Should no longer be active
|
||||
expect(manager.isOperationActive("read_file", "active-test")).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -1,98 +0,0 @@
|
|||
import { describe, test, expect, vi, beforeEach } from "vitest"
|
||||
import { TimeoutFallbackHandler } from "../TimeoutFallbackHandler"
|
||||
import { type TimeoutFallbackResult } from "../TimeoutFallbackHandler"
|
||||
import { Task } from "../../task/Task"
|
||||
|
||||
// Import the real module first
|
||||
import { TimeoutFallbackHandler as RealTimeoutFallbackHandler } from "../TimeoutFallbackHandler"
|
||||
|
||||
// Mock only the generateAiFallback method
|
||||
vi.spyOn(RealTimeoutFallbackHandler, "generateAiFallback")
|
||||
|
||||
describe("Tool Call Response Test", () => {
|
||||
let mockTask: Task
|
||||
|
||||
beforeEach(() => {
|
||||
// Create a minimal mock task with assistantMessageContent array
|
||||
mockTask = {
|
||||
assistantMessageContent: [],
|
||||
cwd: "/test/dir",
|
||||
say: vi.fn(),
|
||||
} as unknown as Task
|
||||
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
test("should return response with ask_followup_question tool instructions", async () => {
|
||||
// Mock the TimeoutFallbackHandler to return a successful AI result
|
||||
const mockAiResult: TimeoutFallbackResult = {
|
||||
success: true,
|
||||
toolCall: {
|
||||
name: "ask_followup_question",
|
||||
params: {
|
||||
question: "The execute_command operation timed out after 5 seconds. How would you like to proceed?",
|
||||
follow_up:
|
||||
"<suggest>Try a different approach</suggest>\n<suggest>Break into smaller steps</suggest>",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Mock the generateAiFallback method
|
||||
vi.mocked(RealTimeoutFallbackHandler.generateAiFallback).mockResolvedValue(mockAiResult)
|
||||
|
||||
// Call createTimeoutResponse
|
||||
const response = await TimeoutFallbackHandler.createTimeoutResponse(
|
||||
"execute_command",
|
||||
5000,
|
||||
6000,
|
||||
{ command: "npm install" },
|
||||
mockTask,
|
||||
)
|
||||
|
||||
// Check that the response contains the base timeout message
|
||||
expect(response).toContain("timed out after 5 seconds")
|
||||
expect(response).toContain("Execution Time: 6s")
|
||||
|
||||
// Check that the response includes instructions to use ask_followup_question
|
||||
expect(response).toContain("You MUST now use the ask_followup_question tool")
|
||||
expect(response).toContain("<ask_followup_question>")
|
||||
expect(response).toContain(`<question>${mockAiResult.toolCall?.params.question}</question>`)
|
||||
expect(response).toContain("<follow_up>")
|
||||
expect(response).toContain(mockAiResult.toolCall?.params.follow_up)
|
||||
expect(response).toContain("</follow_up>")
|
||||
expect(response).toContain("</ask_followup_question>")
|
||||
expect(response).toContain("This is required to help the user decide how to proceed after the timeout.")
|
||||
|
||||
// Verify that assistantMessageContent was NOT modified
|
||||
expect(mockTask.assistantMessageContent).toHaveLength(0)
|
||||
})
|
||||
|
||||
test("should return fallback message when AI generation fails", async () => {
|
||||
// Mock the generateAiFallback to return a failure
|
||||
vi.mocked(RealTimeoutFallbackHandler.generateAiFallback).mockResolvedValue({
|
||||
success: false,
|
||||
error: "AI generation failed",
|
||||
})
|
||||
|
||||
// Call createTimeoutResponse
|
||||
const response = await TimeoutFallbackHandler.createTimeoutResponse(
|
||||
"execute_command",
|
||||
5000,
|
||||
6000,
|
||||
{ command: "npm install" },
|
||||
mockTask,
|
||||
)
|
||||
|
||||
// Check that the response contains the base timeout message
|
||||
expect(response).toContain("timed out after 5 seconds")
|
||||
expect(response).toContain("Execution Time: 6s")
|
||||
|
||||
// Check that the response contains the fallback message
|
||||
expect(response).toContain(
|
||||
"The operation timed out. Please consider breaking this into smaller steps or trying a different approach.",
|
||||
)
|
||||
|
||||
// Should not contain ask_followup_question instructions
|
||||
expect(response).not.toContain("ask_followup_question")
|
||||
})
|
||||
})
|
||||
|
|
@ -1,70 +0,0 @@
|
|||
import { describe, it, expect, vi } from "vitest"
|
||||
import { TimeoutFallbackHandler } from "../TimeoutFallbackHandler"
|
||||
|
||||
describe("UI Integration - AI Timeout Fallbacks", () => {
|
||||
it("should generate AI fallbacks using static method", async () => {
|
||||
const context = {
|
||||
toolName: "execute_command" as const,
|
||||
timeoutMs: 30000,
|
||||
executionTimeMs: 25000,
|
||||
toolParams: { command: "npm install" },
|
||||
}
|
||||
|
||||
const result = await TimeoutFallbackHandler.generateAiFallback(context)
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.toolCall).toBeDefined()
|
||||
expect(result.toolCall?.name).toBe("ask_followup_question")
|
||||
expect(result.toolCall?.params.question).toContain("execute_command")
|
||||
})
|
||||
|
||||
it("should create timeout response with AI fallbacks", async () => {
|
||||
const response = await TimeoutFallbackHandler.createTimeoutResponse("execute_command", 30000, 25000, {
|
||||
command: "npm install",
|
||||
})
|
||||
|
||||
expect(response).toContain("execute_command")
|
||||
expect(response).toContain("timed out")
|
||||
expect(response.length).toBeGreaterThan(100) // Should contain substantial content
|
||||
})
|
||||
|
||||
it("should create timeout response when AI fallbacks fail", async () => {
|
||||
const response = await TimeoutFallbackHandler.createTimeoutResponse("execute_command", 30000, 25000, {
|
||||
command: "npm install",
|
||||
})
|
||||
|
||||
expect(response).toContain("execute_command")
|
||||
expect(response).toContain("timed out")
|
||||
expect(response.length).toBeGreaterThan(50) // Should contain basic timeout message
|
||||
})
|
||||
|
||||
it("should validate UI setting flow", () => {
|
||||
// This test validates that timeout settings can be toggled
|
||||
const settings = {
|
||||
timeoutFallbackEnabled: true,
|
||||
toolExecutionTimeoutMs: 30000,
|
||||
}
|
||||
|
||||
// Simulate UI toggle
|
||||
settings.timeoutFallbackEnabled = false
|
||||
expect(settings.timeoutFallbackEnabled).toBe(false)
|
||||
|
||||
// Simulate timeout duration change
|
||||
settings.toolExecutionTimeoutMs = 60000
|
||||
expect(settings.toolExecutionTimeoutMs).toBe(60000)
|
||||
})
|
||||
|
||||
it("should handle different tool types with AI fallbacks", async () => {
|
||||
const commandResponse = await TimeoutFallbackHandler.createTimeoutResponse("execute_command", 30000, 25000, {
|
||||
command: "npm test",
|
||||
})
|
||||
|
||||
const browserResponse = await TimeoutFallbackHandler.createTimeoutResponse("browser_action", 30000, 25000, {
|
||||
action: "click",
|
||||
})
|
||||
|
||||
expect(commandResponse).toContain("execute_command")
|
||||
expect(browserResponse).toContain("browser_action")
|
||||
expect(commandResponse).not.toEqual(browserResponse)
|
||||
})
|
||||
})
|
||||
|
|
@ -16,6 +16,8 @@ import { TerminalRegistry } from "../../integrations/terminal/TerminalRegistry"
|
|||
import { Terminal } from "../../integrations/terminal/Terminal"
|
||||
import { ToolExecutionWrapper, TimeoutFallbackHandler } from "../timeout"
|
||||
|
||||
const DEFAULT_TOOL_EXECUTION_TIMEOUT_MS = 60000 // 1 minute default
|
||||
|
||||
class ShellIntegrationError extends Error {}
|
||||
|
||||
export async function executeCommandTool(
|
||||
|
|
@ -64,7 +66,7 @@ export async function executeCommandTool(
|
|||
const {
|
||||
terminalOutputLineLimit = 500,
|
||||
terminalShellIntegrationDisabled = false,
|
||||
toolExecutionTimeoutMs = 300000, // 5 minutes default
|
||||
toolExecutionTimeoutMs = 60000, // 1 minute default
|
||||
} = clineProviderState ?? {}
|
||||
|
||||
const options: ExecuteCommandOptions = {
|
||||
|
|
@ -136,7 +138,7 @@ export async function executeCommand(
|
|||
// Get timeout from settings if not provided
|
||||
const clineProvider = await cline.providerRef.deref()
|
||||
const clineProviderState = await clineProvider?.getState()
|
||||
const defaultTimeoutMs = clineProviderState?.toolExecutionTimeoutMs ?? 300000 // 5 minutes default
|
||||
const defaultTimeoutMs = clineProviderState?.toolExecutionTimeoutMs ?? DEFAULT_TOOL_EXECUTION_TIMEOUT_MS
|
||||
const actualTimeoutMs = timeoutMs ?? defaultTimeoutMs
|
||||
const timeoutFallbackEnabled = clineProviderState?.timeoutFallbackEnabled ?? true
|
||||
|
||||
|
|
|
|||
|
|
@ -1524,7 +1524,7 @@ export class ClineProvider
|
|||
hasOpenedModeSelector: this.getGlobalState("hasOpenedModeSelector") ?? false,
|
||||
alwaysAllowFollowupQuestions: alwaysAllowFollowupQuestions ?? false,
|
||||
followupAutoApproveTimeoutMs: followupAutoApproveTimeoutMs ?? 60000,
|
||||
toolExecutionTimeoutMs: toolExecutionTimeoutMs ?? 300000,
|
||||
toolExecutionTimeoutMs: toolExecutionTimeoutMs ?? 60000,
|
||||
timeoutFallbackEnabled: timeoutFallbackEnabled ?? false,
|
||||
}
|
||||
}
|
||||
|
|
@ -1638,7 +1638,7 @@ export class ClineProvider
|
|||
terminalZshP10k: stateValues.terminalZshP10k ?? false,
|
||||
terminalZdotdir: stateValues.terminalZdotdir ?? false,
|
||||
terminalCompressProgressBar: stateValues.terminalCompressProgressBar ?? true,
|
||||
toolExecutionTimeoutMs: stateValues.toolExecutionTimeoutMs ?? 300000, // 5 minutes default
|
||||
toolExecutionTimeoutMs: stateValues.toolExecutionTimeoutMs ?? 60000,
|
||||
timeoutFallbackEnabled: stateValues.timeoutFallbackEnabled ?? false,
|
||||
mode: stateValues.mode ?? defaultModeSlug,
|
||||
language: stateValues.language ?? formatLanguage(vscode.env.language),
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ export const AutoApproveSettings = ({
|
|||
followupAutoApproveTimeoutMs = 60000,
|
||||
allowedCommands,
|
||||
timeoutFallbackEnabled,
|
||||
toolExecutionTimeoutMs,
|
||||
toolExecutionTimeoutMs = 60000,
|
||||
setCachedStateField,
|
||||
...props
|
||||
}: AutoApproveSettingsProps) => {
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ export interface ExtensionStateContextType extends ExtensionState {
|
|||
setFollowupAutoApproveTimeoutMs: (value: number) => void // Setter for the timeout
|
||||
timeoutFallbackEnabled?: boolean // New property for timeout fallback enabled
|
||||
setTimeoutFallbackEnabled: (value: boolean) => void // Setter for timeout fallback enabled
|
||||
toolExecutionTimeoutMs?: number // New property for tool execution timeout
|
||||
toolExecutionTimeoutMs: number | undefined // New property for tool execution timeout
|
||||
setToolExecutionTimeoutMs: (value: number) => void // Setter for tool execution timeout
|
||||
condensingApiConfigId?: string
|
||||
setCondensingApiConfigId: (value: string) => void
|
||||
|
|
@ -230,7 +230,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
|||
alwaysAllowUpdateTodoList: true,
|
||||
// Timeout settings
|
||||
timeoutFallbackEnabled: false, // Default to disabled
|
||||
toolExecutionTimeoutMs: 300000, // 5 minutes default
|
||||
toolExecutionTimeoutMs: undefined, // Will be set from global settings
|
||||
})
|
||||
|
||||
const [didHydrateState, setDidHydrateState] = useState(false)
|
||||
|
|
@ -245,7 +245,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
|||
const [alwaysAllowFollowupQuestions, setAlwaysAllowFollowupQuestions] = useState(false) // Add state for follow-up questions auto-approve
|
||||
const [followupAutoApproveTimeoutMs, setFollowupAutoApproveTimeoutMs] = useState<number | undefined>(undefined) // Will be set from global settings
|
||||
const [timeoutFallbackEnabled, setTimeoutFallbackEnabledState] = useState(false) // Add state for timeout fallback enabled
|
||||
const [toolExecutionTimeoutMs, setToolExecutionTimeoutMsState] = useState<number>(300000) // Add state for tool execution timeout (5 minutes default)
|
||||
const [toolExecutionTimeoutMs, setToolExecutionTimeoutMsState] = useState<number | undefined>(undefined) // Will be set from global settings
|
||||
const [marketplaceInstalledMetadata, setMarketplaceInstalledMetadata] = useState<MarketplaceInstalledMetadata>({
|
||||
project: {},
|
||||
global: {},
|
||||
|
|
|
|||
|
|
@ -230,7 +230,7 @@
|
|||
"hasQuestion": "Roo has a question:"
|
||||
},
|
||||
"taskCompleted": "Task Completed",
|
||||
"toolTimeout": "Tool Timeout",
|
||||
"toolTimeout": "Automatic Tool Timeout: Suggesting alternatives...",
|
||||
"error": "Error",
|
||||
"diffError": {
|
||||
"title": "Edit Unsuccessful"
|
||||
|
|
|
|||
|
|
@ -176,7 +176,7 @@
|
|||
"description": "Configure how Roo handles tool operations that exceed their timeout limits",
|
||||
"timeoutFallbackEnabled": {
|
||||
"label": "Enable timeout handling",
|
||||
"description": "Enable automatic timeout detection and fallback suggestions for long-running operations"
|
||||
"description": "Automatically timeout long-running operations and suggest fallback options."
|
||||
},
|
||||
"toolExecutionTimeoutMs": {
|
||||
"label": "Tool execution timeout (ms)",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue