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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

### Bug Fixes

- **RovoDev**: Fixed `TypeError: terminated` from Node.js undici being incorrectly surfaced as an error dialog when aborting an in-flight chat request. The error is now silently handled as a normal abort, preventing spurious error messages and noisy telemetry — particularly in Boysenberry mode where long-running YOLO streams make mid-stream aborts more common.

- Fixed shell command injection vulnerability (VULN-1825192) in git operations. The `Shell` utility class now uses `shell: false` when spawning processes, and all git commands pass arguments as separate array elements rather than interpolating user-controlled values (e.g. branch names, file paths, commit hashes) directly into shell command strings. This prevents Remote Code Execution via maliciously crafted git branch names.

## What's new in 4.0.29
Expand Down
61 changes: 61 additions & 0 deletions src/rovo-dev/rovoDevChatProvider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,19 @@ describe('RovoDevChatProvider', () => {

expect(chatProvider['_lastMessageSentTime']).toBeUndefined();
});

it('should clear pending deferred request on cancellation so next prompt is sent as plain message', async () => {
await chatProvider.setReady(mockApiClient);

// Simulate a pending deferred tool call (e.g. ask_user_questions)
chatProvider['_pendingDeferredRequest'] = 'deferred-tool-call-123';

mockApiClient.cancel.mockResolvedValue({ cancelled: true, message: 'Cancelled' });

await chatProvider.executeCancel(false);

expect(chatProvider['_pendingDeferredRequest']).toBeUndefined();
});
});

describe('signalToolRequestChoiceSubmit', () => {
Expand Down Expand Up @@ -1044,5 +1057,53 @@ describe('RovoDevChatProvider', () => {
}),
);
});

it('should handle undici TypeError: terminated as an abort (no error dialog)', async () => {
// Simulates the Node.js undici error thrown when AbortController.abort() is called
// on an in-flight fetch. undici throws a plain TypeError with message "terminated"
// rather than an AbortError, so it must be caught explicitly.
const terminatedError = new TypeError('terminated');
mockApiClient.chat.mockRejectedValue(terminatedError);

const mockPrompt: RovoDevPrompt = {
text: 'test prompt',
context: [],
};

await chatProvider.executeChat(mockPrompt, []);

// Should NOT show an error dialog — terminated is a normal abort side-effect
expect(mockWebview.postMessage).not.toHaveBeenCalledWith(
expect.objectContaining({
type: RovoDevProviderMessageType.ShowDialog,
}),
);

// Should send CompleteMessage so the UI returns to an interactive state
expect(mockWebview.postMessage).toHaveBeenCalledWith(
expect.objectContaining({
type: RovoDevProviderMessageType.CompleteMessage,
}),
);
});

it('should still show error dialog for other TypeErrors (not terminated)', async () => {
// Ensures the fix is narrowly scoped and doesn't swallow real TypeErrors
const otherTypeError = new TypeError('Failed to fetch');
mockApiClient.chat.mockRejectedValue(otherTypeError);

const mockPrompt: RovoDevPrompt = {
text: 'test prompt',
context: [],
};

await chatProvider.executeChat(mockPrompt, []);

expect(mockWebview.postMessage).toHaveBeenCalledWith(
expect.objectContaining({
type: RovoDevProviderMessageType.ShowDialog,
}),
);
});
});
});
8 changes: 7 additions & 1 deletion src/rovo-dev/rovoDevChatProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,12 @@ export class RovoDevChatProvider {
public async executeCancel(fromNewSession: boolean): Promise<boolean> {
const webview = this._webView!;

// Clear any pending deferred tool call so the next user prompt is sent
// as a plain message instead of a tool-call response. Without this the
// backend would reject the prompt with "Cannot provide a new user prompt
// when the message history contains unprocessed tool calls".
this._pendingDeferredRequest = undefined;

let success: boolean;
if (this._rovoDevApiClient) {
if (this._pendingCancellation) {
Expand Down Expand Up @@ -967,7 +973,7 @@ export class RovoDevChatProvider {
try {
await func(this._rovoDevApiClient);
} catch (error) {
if (error.name === 'AbortError') {
if (error.name === 'AbortError' || (error instanceof TypeError && error.message === 'terminated')) {
await webview.postMessage({
type: RovoDevProviderMessageType.CompleteMessage,
promptId: this._currentPromptId,
Expand Down
Loading