This commit is contained in:
Deleted user 2026-05-27 11:34:03 +08:00 committed by GitHub
commit 08df54e56b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 201 additions and 29 deletions

View file

@ -1125,6 +1125,85 @@ describe("consolidateReasoningDetails", () => {
expect(result).toHaveLength(1)
expect(result[0].summary).toBe("Summary part 1Summary part 2")
})
// Regression tests for https://github.com/RooCodeInc/Roo-Code/issues/11629
it("should preserve reasoning.text type when grouped with reasoning.encrypted at same index", () => {
// This is the exact scenario from the bug report: Gemini returns both
// reasoning.text and reasoning.encrypted at index 0. The text entry
// must keep its original type and not be overwritten by the encrypted type.
const details: ReasoningDetail[] = [
{
type: "reasoning.text",
text: "**Initiating the Analysis**...",
id: "rs_text_1",
format: "google-gemini-v1",
index: 0,
},
{
type: "reasoning.encrypted",
data: "EtwECtkEAb4+9vu...",
id: "rs_enc_1",
format: "google-gemini-v1",
index: 0,
},
]
const result = consolidateReasoningDetails(details)
// Should have two entries: one text, one encrypted
expect(result).toHaveLength(2)
const textEntry = result.find((r) => r.text)
const encryptedEntry = result.find((r) => r.data)
// Text entry must retain type "reasoning.text", NOT "reasoning.encrypted"
expect(textEntry?.type).toBe("reasoning.text")
expect(textEntry?.text).toBe("**Initiating the Analysis**...")
// Encrypted entry should keep its correct type
expect(encryptedEntry?.type).toBe("reasoning.encrypted")
expect(encryptedEntry?.data).toBe("EtwECtkEAb4+9vu...")
})
it("should preserve mislabeled text entries (type=reasoning.encrypted with text but no data)", () => {
// If a text entry was previously stored with the wrong type (reasoning.encrypted)
// but has a text field and no data field, it should NOT be dropped as corrupted.
const details: ReasoningDetail[] = [
{
type: "reasoning.encrypted",
text: "Some reasoning text",
// No data field - previously this was dropped as "corrupted"
id: "rs_mislabeled",
format: "google-gemini-v1",
index: 0,
},
]
const result = consolidateReasoningDetails(details)
// Should preserve the entry and fix its type to reasoning.text
expect(result).toHaveLength(1)
expect(result[0].text).toBe("Some reasoning text")
expect(result[0].type).toBe("reasoning.text")
})
it("should still drop truly corrupted encrypted blocks (no data AND no text)", () => {
const details: ReasoningDetail[] = [
{
type: "reasoning.encrypted",
// No data, no text - truly corrupted
id: "rs_corrupted",
format: "google-gemini-v1",
index: 0,
},
]
const result = consolidateReasoningDetails(details)
// Truly corrupted block should still be dropped
expect(result).toHaveLength(0)
})
})
describe("sanitizeGeminiMessages", () => {
@ -1302,4 +1381,73 @@ describe("sanitizeGeminiMessages", () => {
expect(result).toEqual(messages)
})
// Regression test for https://github.com/RooCodeInc/Roo-Code/issues/11629
it("should preserve tool_calls when reasoning_details contain mislabeled text entries", () => {
// This reproduces the exact bug: reasoning_details has a text entry mislabeled
// as "reasoning.encrypted" (no data field) and a real encrypted entry.
// Previously, the mislabeled text entry was dropped as "corrupted", and if
// the encrypted entry's ID didn't match the tool call, all tool_calls were dropped.
const messages = [
{
role: "assistant",
content: "",
tool_calls: [
{
id: "tool_attempt_completion_BI9Id6PH8KtbhYBnuB4I",
type: "function",
function: {
name: "attempt_completion",
arguments: '{"result":"Test received successfully."}',
},
},
],
reasoning_details: [
{
// Mislabeled: type says encrypted but has text, no data
type: "reasoning.encrypted",
text: "**Acknowledging the Input**\nI see this as a test message...",
id: "tool_attempt_completion_BI9Id6PH8KtbhYBnuB4I",
format: "google-gemini-v1",
index: 0,
},
{
type: "reasoning.encrypted",
data: "EtwECtkEAb4+9vuFAKED...",
id: "tool_attempt_completion_BI9Id6PH8KtbhYBnuB4I",
format: "google-gemini-v1",
index: 0,
},
],
},
{
role: "tool",
tool_call_id: "tool_attempt_completion_BI9Id6PH8KtbhYBnuB4I",
content: "The user is satisfied with the result.",
},
] as any
const result = sanitizeGeminiMessages(messages, "google/gemini-3.1-pro-preview")
// Assistant message should preserve tool_calls
expect(result).toHaveLength(2)
const assistantMsg = result[0] as any
expect(assistantMsg.tool_calls).toBeDefined()
expect(assistantMsg.tool_calls).toHaveLength(1)
expect(assistantMsg.tool_calls[0].id).toBe("tool_attempt_completion_BI9Id6PH8KtbhYBnuB4I")
// reasoning_details should have both entries (text with corrected type + encrypted)
expect(assistantMsg.reasoning_details).toBeDefined()
expect(assistantMsg.reasoning_details.length).toBeGreaterThanOrEqual(1)
// The text entry should have its type corrected to reasoning.text
const textDetail = assistantMsg.reasoning_details.find((d: any) => d.text)
if (textDetail) {
expect(textDetail.type).toBe("reasoning.text")
}
// Tool result message should be preserved
expect(result[1].role).toBe("tool")
expect((result[1] as any).tool_call_id).toBe("tool_attempt_completion_BI9Id6PH8KtbhYBnuB4I")
})
})

View file

@ -50,7 +50,11 @@ export function consolidateReasoningDetails(reasoningDetails: ReasoningDetail[])
// Drop corrupted encrypted reasoning blocks that would otherwise trigger:
// "Invalid input: expected string, received undefined" for reasoning_details.*.data
// See: https://github.com/cline/cline/issues/8214
if (detail.type === "reasoning.encrypted" && !detail.data) {
// Only drop if it truly has no usable content (no `data` AND no `text`).
// A mislabeled entry with `type: "reasoning.encrypted"` but a valid `text`
// field should be preserved (it's a text entry with incorrect type).
// See: https://github.com/RooCodeInc/Roo-Code/issues/11629
if (detail.type === "reasoning.encrypted" && !detail.data && !detail.text) {
continue
}
@ -65,46 +69,66 @@ export function consolidateReasoningDetails(reasoningDetails: ReasoningDetail[])
const consolidated: ReasoningDetail[] = []
for (const [index, details] of groupedByIndex.entries()) {
// Concatenate all text parts
// Concatenate all text parts.
// Track types separately for text/summary entries vs encrypted entries,
// because a group can contain both a reasoning.text and a reasoning.encrypted
// entry at the same index. Using a single shared `type` variable would cause
// the last entry's type to overwrite earlier ones, mislabeling text entries
// as "reasoning.encrypted".
// See: https://github.com/RooCodeInc/Roo-Code/issues/11629
let concatenatedText = ""
let concatenatedSummary = ""
let signature: string | undefined
let id: string | undefined
let format = "unknown"
let type = "reasoning.text"
let textSignature: string | undefined
let textId: string | undefined
let textFormat = "unknown"
let textType = "reasoning.text"
for (const detail of details) {
if (detail.text) {
concatenatedText += detail.text
// Track type/metadata from text-bearing entries only
if (detail.type) {
textType = detail.type === "reasoning.encrypted" ? "reasoning.text" : detail.type
}
if (detail.signature) {
textSignature = detail.signature
}
if (detail.id) {
textId = detail.id
}
if (detail.format) {
textFormat = detail.format
}
}
if (detail.summary) {
concatenatedSummary += detail.summary
}
// Keep the signature from the last item that has one
if (detail.signature) {
signature = detail.signature
}
// Keep the id from the last item that has one
if (detail.id) {
id = detail.id
}
// Keep format and type from any item (they should all be the same)
if (detail.format) {
format = detail.format
}
if (detail.type) {
type = detail.type
// Use text entry metadata for summaries too, but don't overwrite
// if we already got it from a text entry
if (!concatenatedText) {
if (detail.type) {
textType = detail.type
}
if (detail.signature) {
textSignature = detail.signature
}
if (detail.id) {
textId = detail.id
}
if (detail.format) {
textFormat = detail.format
}
}
}
}
// Create consolidated entry for text
if (concatenatedText) {
const consolidatedEntry: ReasoningDetail = {
type: type,
type: textType,
text: concatenatedText,
signature: signature ?? undefined,
id: id ?? undefined,
format: format,
signature: textSignature ?? undefined,
id: textId ?? undefined,
format: textFormat,
index: index,
}
consolidated.push(consolidatedEntry)
@ -113,11 +137,11 @@ export function consolidateReasoningDetails(reasoningDetails: ReasoningDetail[])
// Create consolidated entry for summary (used by some providers)
if (concatenatedSummary && !concatenatedText) {
const consolidatedEntry: ReasoningDetail = {
type: type,
type: textType,
summary: concatenatedSummary,
signature: signature ?? undefined,
id: id ?? undefined,
format: format,
signature: textSignature ?? undefined,
id: textId ?? undefined,
format: textFormat,
index: index,
}
consolidated.push(consolidatedEntry)