mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-10-09 03:17:58 +00:00
Merge 62f9bcdc8d into c3cae397a1
This commit is contained in:
commit
fa9dc44b7e
2 changed files with 201 additions and 29 deletions
|
|
@ -1125,6 +1125,85 @@ describe("consolidateReasoningDetails", () => {
|
||||||
expect(result).toHaveLength(1)
|
expect(result).toHaveLength(1)
|
||||||
expect(result[0].summary).toBe("Summary part 1Summary part 2")
|
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", () => {
|
describe("sanitizeGeminiMessages", () => {
|
||||||
|
|
@ -1302,4 +1381,73 @@ describe("sanitizeGeminiMessages", () => {
|
||||||
|
|
||||||
expect(result).toEqual(messages)
|
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")
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,11 @@ export function consolidateReasoningDetails(reasoningDetails: ReasoningDetail[])
|
||||||
// Drop corrupted encrypted reasoning blocks that would otherwise trigger:
|
// Drop corrupted encrypted reasoning blocks that would otherwise trigger:
|
||||||
// "Invalid input: expected string, received undefined" for reasoning_details.*.data
|
// "Invalid input: expected string, received undefined" for reasoning_details.*.data
|
||||||
// See: https://github.com/cline/cline/issues/8214
|
// 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
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -65,46 +69,66 @@ export function consolidateReasoningDetails(reasoningDetails: ReasoningDetail[])
|
||||||
const consolidated: ReasoningDetail[] = []
|
const consolidated: ReasoningDetail[] = []
|
||||||
|
|
||||||
for (const [index, details] of groupedByIndex.entries()) {
|
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 concatenatedText = ""
|
||||||
let concatenatedSummary = ""
|
let concatenatedSummary = ""
|
||||||
let signature: string | undefined
|
let textSignature: string | undefined
|
||||||
let id: string | undefined
|
let textId: string | undefined
|
||||||
let format = "unknown"
|
let textFormat = "unknown"
|
||||||
let type = "reasoning.text"
|
let textType = "reasoning.text"
|
||||||
|
|
||||||
for (const detail of details) {
|
for (const detail of details) {
|
||||||
if (detail.text) {
|
if (detail.text) {
|
||||||
concatenatedText += 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) {
|
if (detail.summary) {
|
||||||
concatenatedSummary += detail.summary
|
concatenatedSummary += detail.summary
|
||||||
}
|
// Use text entry metadata for summaries too, but don't overwrite
|
||||||
// Keep the signature from the last item that has one
|
// if we already got it from a text entry
|
||||||
if (detail.signature) {
|
if (!concatenatedText) {
|
||||||
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) {
|
if (detail.type) {
|
||||||
type = 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
|
// Create consolidated entry for text
|
||||||
if (concatenatedText) {
|
if (concatenatedText) {
|
||||||
const consolidatedEntry: ReasoningDetail = {
|
const consolidatedEntry: ReasoningDetail = {
|
||||||
type: type,
|
type: textType,
|
||||||
text: concatenatedText,
|
text: concatenatedText,
|
||||||
signature: signature ?? undefined,
|
signature: textSignature ?? undefined,
|
||||||
id: id ?? undefined,
|
id: textId ?? undefined,
|
||||||
format: format,
|
format: textFormat,
|
||||||
index: index,
|
index: index,
|
||||||
}
|
}
|
||||||
consolidated.push(consolidatedEntry)
|
consolidated.push(consolidatedEntry)
|
||||||
|
|
@ -113,11 +137,11 @@ export function consolidateReasoningDetails(reasoningDetails: ReasoningDetail[])
|
||||||
// Create consolidated entry for summary (used by some providers)
|
// Create consolidated entry for summary (used by some providers)
|
||||||
if (concatenatedSummary && !concatenatedText) {
|
if (concatenatedSummary && !concatenatedText) {
|
||||||
const consolidatedEntry: ReasoningDetail = {
|
const consolidatedEntry: ReasoningDetail = {
|
||||||
type: type,
|
type: textType,
|
||||||
summary: concatenatedSummary,
|
summary: concatenatedSummary,
|
||||||
signature: signature ?? undefined,
|
signature: textSignature ?? undefined,
|
||||||
id: id ?? undefined,
|
id: textId ?? undefined,
|
||||||
format: format,
|
format: textFormat,
|
||||||
index: index,
|
index: index,
|
||||||
}
|
}
|
||||||
consolidated.push(consolidatedEntry)
|
consolidated.push(consolidatedEntry)
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue