Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
1c2fad9
initial fix for issues #1240 and #505
DaubnerF Sep 1, 2026
d52e160
Merge branch 'main' into bugfix_for_1240_505
DaubnerF Sep 2, 2026
41a7698
1st round of fixes
DaubnerF Sep 2, 2026
a607ec9
Merge branch 'main' into bugfix_for_1240_505
DaubnerF Sep 2, 2026
f27387b
fixed comments
DaubnerF Sep 2, 2026
8f2245f
Merge branch 'main' into bugfix_for_1240_505
DaubnerF Sep 3, 2026
bc6f8ff
increase test coverage
DaubnerF Sep 3, 2026
6968a98
revert: remove Windows shell invocation from stryker-diff
DaubnerF Sep 4, 2026
e00a2b1
fix: address CodeRabbit review on tool-policy prompt unification
DaubnerF Sep 4, 2026
943a16e
fix(test): correct apiModelId in generateSystemPrompt state mock
DaubnerF Sep 4, 2026
bd7894c
drop use_mcp_tool from policy when no MCP tool is permitted
DaubnerF Sep 4, 2026
c289453
Merge branch 'main' into bugfix_for_1240_505
DaubnerF Sep 5, 2026
26a3a68
code hardening
DaubnerF Sep 7, 2026
7e42bf5
Merge branch 'Zoo-Code-Org:main' into bugfix_for_1240_505
DaubnerF Sep 7, 2026
528b023
bound model fetch with timeout, typed provider state test doubles
DaubnerF Sep 8, 2026
aa8f108
cover preview model fetch timeout path with tests
DaubnerF Sep 8, 2026
415422b
pin completion-time history save ordering with unit tests
DaubnerF Sep 8, 2026
1db50a1
poll history length in restart e2e to tolerate atomic write window
DaubnerF Sep 8, 2026
227ab61
Merge remote-tracking branch 'upstream/main' into bugfix_for_1240_505
DaubnerF Sep 8, 2026
6a769b5
share one model-info snapshot per request between prompt and tools
DaubnerF Sep 8, 2026
92c6f32
resolve provider state once before the MCP wait
DaubnerF Sep 8, 2026
2ce570a
cover the undefined provider state path in the system prompt tests
DaubnerF Sep 8, 2026
ac35870
reuse one model-info snapshot per request and honor cancellation
DaubnerF Sep 8, 2026
b75e73b
pin the retry count the request seam receives
DaubnerF Sep 8, 2026
b1236cd
refactor(task): require callers to thread provider state into system …
DaubnerF Sep 8, 2026
85beb67
fix(api): cancel abandoned model-metadata waits via AbortSignal
DaubnerF Sep 9, 2026
d06883a
test(api): cover abort-signal detach paths and thread request model s…
DaubnerF Sep 9, 2026
2366d23
Apply disabled and excluded tool policy to dynamic MCP declarations
DaubnerF Sep 10, 2026
cfd835e
Forward request options through API retry recursion
DaubnerF Sep 10, 2026
aa894b4
Forward derived model snapshot through API retry recursion
DaubnerF Sep 10, 2026
f943876
Tighten build-tools test assertions and provider double
DaubnerF Sep 10, 2026
28439fb
Use the request model snapshot for context-window recovery math
DaubnerF Sep 10, 2026
0a41225
Merge branch 'Zoo-Code-Org:main' into bugfix_for_1240_505
DaubnerF Sep 10, 2026
9b0a6a5
Stop manual condensation when the task is cancelled
DaubnerF Sep 10, 2026
7955068
Recheck cancellation before summarizing and rewriting history
DaubnerF Sep 10, 2026
17d6176
Make the first cancellation checkpoint observable to tests
DaubnerF Sep 11, 2026
795b639
Correct a rationale comment in the cancellation tests
DaubnerF Sep 11, 2026
855531d
Merge branch 'Zoo-Code-Org:main' into bugfix_for_1240_505
DaubnerF Sep 11, 2026
73670b8
Merge remote-tracking branch 'origin/bugfix_for_1240_505' into bugfix…
DaubnerF Sep 11, 2026
575d35d
Narrow the change set to the tool-policy work and its regression tests
DaubnerF Sep 11, 2026
610acb1
Merge branch 'Zoo-Code-Org:main' into bugfix_for_1240_505
DaubnerF Sep 11, 2026
fddb744
Merge origin/main (01c7357a7) into bugfix_for_1240_505
DaubnerF Sep 11, 2026
4bef223
Remove dead export, untriggerable timer guard, and duplicated prompt-…
DaubnerF Sep 11, 2026
ad6a9a1
Merge remote-tracking branch 'origin/bugfix_for_1240_505' into bugfix…
DaubnerF Sep 11, 2026
24c2fba
Merge branch 'Zoo-Code-Org:main' into bugfix_for_1240_505
DaubnerF Sep 12, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,15 @@ vi.mock("@roo-code/core", () => ({
},
}))

// Mock the tool handlers so the tests only exercise validation (toolRequirements)
// and never the real tool execution logic.
vi.mock("../../tools/AttemptCompletionTool", () => ({
attemptCompletionTool: { handle: vi.fn().mockResolvedValue(undefined) },
}))
vi.mock("../../tools/AskFollowupQuestionTool", () => ({
askFollowupQuestionTool: { handle: vi.fn().mockResolvedValue(undefined) },
}))

// presentAssistantMessage records tool usage through TelemetryService.instance.
vi.mock("@roo-code/telemetry", () => ({
TelemetryService: {
Expand Down Expand Up @@ -333,6 +342,169 @@ describe("presentAssistantMessage - Custom Tool Recording", () => {
edit: false,
})
})

it("marks a disabled attempt_completion as blocked and answers it with an error tool_result", async () => {
// An explicit disabledTools entry outranks the always-available class,
// so a disabled attempt_completion reaches the validator like any
// other tool; its rejection must surface as the standard validation-
// error tool_result instead of completing the task.
mockTask.assistantMessageContent = [
{
type: "tool_use",
id: "tool_call_protocol_123",
name: "attempt_completion",
params: {},
nativeArgs: {},
partial: false,
},
]

mockTask.providerRef = {
deref: () => ({
getState: vi.fn().mockResolvedValue({
mode: "code",
customModes: [],
experiments: {
customTools: false,
},
disabledTools: ["attempt_completion"],
}),
}),
}

// Mirror the real validator's rejection for a requirement that maps
// to false (validateToolUse.spec pins the predicate itself).
vi.mocked(validateToolUse).mockImplementationOnce(() => {
throw new Error('Tool "attempt_completion" is not allowed in code mode.')
})

await presentAssistantMessage(mockTask)

const validateToolUseMock = vi.mocked(validateToolUse)
expect(validateToolUseMock).toHaveBeenCalled()
const toolRequirements = validateToolUseMock.mock.calls[0][3]
expect(toolRequirements).toMatchObject({ attempt_completion: false })

const errorToolResults = mockTask.userMessageContent.filter((block: unknown) => {
const b = block as { type?: string; is_error?: boolean }
return b.type === "tool_result" && b.is_error
})
expect(errorToolResults).toHaveLength(1)
expect(mockTask.consecutiveMistakeCount).toBe(1)

// The completion handler must not run for the rejected call.
const { attemptCompletionTool } = await import("../../tools/AttemptCompletionTool")
expect(attemptCompletionTool.handle).not.toHaveBeenCalled()
})

it("treats a model-excluded attempt_completion as blocked and answers it with an error tool_result", async () => {
// A model excludedTools entry suppresses the protocol tool in the
// effective policy, so the execution gate must see the same
// restriction with disabledTools unset.
mockTask.assistantMessageContent = [
{
type: "tool_use",
id: "tool_call_protocol_excluded_123",
name: "attempt_completion",
params: {},
nativeArgs: {},
partial: false,
},
]

mockTask.api.getModel = () => ({ id: "test-model", info: { excludedTools: ["attempt_completion"] } })

mockTask.providerRef = {
deref: () => ({
getState: vi.fn().mockResolvedValue({
mode: "code",
customModes: [],
experiments: {
customTools: false,
},
}),
}),
}

// Mirror the real validator's rejection for a requirement that maps
// to false (validateToolUse.spec pins the predicate itself).
vi.mocked(validateToolUse).mockImplementationOnce(() => {
throw new Error('Tool "attempt_completion" is not allowed in code mode.')
})

await presentAssistantMessage(mockTask)

const validateToolUseMock = vi.mocked(validateToolUse)
expect(validateToolUseMock).toHaveBeenCalled()
const toolRequirements = validateToolUseMock.mock.calls[0][3]
expect(toolRequirements).toMatchObject({ attempt_completion: false })

const errorToolResults = mockTask.userMessageContent.filter((block: unknown) => {
const b = block as { type?: string; is_error?: boolean }
return b.type === "tool_result" && b.is_error
})
expect(errorToolResults).toHaveLength(1)
expect(mockTask.consecutiveMistakeCount).toBe(1)

// The completion handler must not run for the rejected call.
const { attemptCompletionTool } = await import("../../tools/AttemptCompletionTool")
expect(attemptCompletionTool.handle).not.toHaveBeenCalled()

// Absent model metadata must not derail the requirements build: the
// protocol-tool leg simply sees no exclusions, and the call validates
// normally instead of erroring out.
mockTask.api.getModel = () => undefined
mockTask.currentStreamingContentIndex = 0
mockTask.userMessageContent = []
mockTask.consecutiveMistakeCount = 0
mockTask.didAlreadyUseTool = false
mockTask.didCompleteReadingStream = false

await presentAssistantMessage(mockTask)

expect(validateToolUseMock).toHaveBeenCalledTimes(2)
expect(validateToolUseMock.mock.calls[1][3]).toEqual({})
expect(mockTask.consecutiveMistakeCount).toBe(0)
const phase2Errors = mockTask.userMessageContent.filter((block: { type?: string; is_error?: boolean }) => {
return block.type === "tool_result" && block.is_error
})
expect(phase2Errors).toHaveLength(0)
})

it("still marks ordinary tools (ask_followup_question) as blocked", async () => {
mockTask.assistantMessageContent = [
{
type: "tool_use",
id: "tool_call_ordinary_123",
name: "ask_followup_question",
params: { question: "Which option?" },
nativeArgs: { question: "Which option?" },
partial: false,
},
]

mockTask.providerRef = {
deref: () => ({
getState: vi.fn().mockResolvedValue({
mode: "code",
customModes: [],
experiments: {
customTools: false,
},
disabledTools: ["ask_followup_question"],
}),
}),
}

await presentAssistantMessage(mockTask)

const validateToolUseMock = vi.mocked(validateToolUse)
expect(validateToolUseMock).toHaveBeenCalled()
const toolRequirements = validateToolUseMock.mock.calls[0][3]
expect(toolRequirements).toMatchObject({
ask_followup_question: false,
})
})
})

describe("Partial blocks", () => {
Expand Down
16 changes: 6 additions & 10 deletions src/core/assistant-message/presentAssistantMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import { skillTool } from "../tools/SkillTool"
import { generateImageTool } from "../tools/GenerateImageTool"
import { applyDiffTool as applyDiffToolClass } from "../tools/ApplyDiffTool"
import { isValidToolName, validateToolUse } from "../tools/validateToolUse"
import { buildToolRequirements } from "../prompts/tools/effective-tool-policy"
import { codebaseSearchTool } from "../tools/CodebaseSearchTool"

import { formatResponse } from "../prompts/responses"
Expand Down Expand Up @@ -604,16 +605,11 @@ export async function presentAssistantMessage(cline: Task) {
const isCustomTool = Boolean(stateExperiments?.customTools && customToolRegistry.has(block.name))

try {
const toolRequirements =
disabledTools?.reduce(
(acc: Record<string, boolean>, tool: string) => {
acc[tool] = false
const resolvedToolName = resolveToolAlias(tool)
acc[resolvedToolName] = false
return acc
},
{} as Record<string, boolean>,
) ?? {}
// Build requirements through the shared policy module so every suppressed
// entry — disabled tools, and an excluded or disabled protocol tool — reaches
// the validator, which checks them before the always-available class. See
// `buildToolRequirements` in effective-tool-policy.ts.
const toolRequirements = buildToolRequirements(disabledTools, modelInfo?.info)

validateToolUse(
block.name as ToolName,
Expand Down
Loading
Loading