From 663923c2d1a8d4346f5c8acd8406db9ac93b618e Mon Sep 17 00:00:00 2001 From: gubin-dev Date: Fri, 18 Sep 2026 17:34:07 +0300 Subject: [PATCH 1/4] test(code-index): readiness coverage --- .../codebase-search-readiness.spec.ts | 93 +++++++++++++++++++ .../build-tools-readiness.integration.spec.ts | 74 +++++++++++++++ 2 files changed, 167 insertions(+) create mode 100644 src/core/prompts/tools/__tests__/codebase-search-readiness.spec.ts create mode 100644 src/core/task/__tests__/build-tools-readiness.integration.spec.ts diff --git a/src/core/prompts/tools/__tests__/codebase-search-readiness.spec.ts b/src/core/prompts/tools/__tests__/codebase-search-readiness.spec.ts new file mode 100644 index 0000000000..1982fbf471 --- /dev/null +++ b/src/core/prompts/tools/__tests__/codebase-search-readiness.spec.ts @@ -0,0 +1,93 @@ +import type OpenAI from "openai" +import type { ModeConfig } from "@roo-code/types" +import type { CodeIndexManager } from "../../../../services/code-index/manager" +import { filterNativeToolsForMode } from "../filter-tools-for-mode" +import { resolveEffectiveToolPolicy } from "../effective-tool-policy" + +type Readiness = Pick + +function makeManager(flags: Readiness): CodeIndexManager { + // These filters only read the three public readiness getters; no manager services are needed. + return flags as CodeIndexManager +} + +const ready = { isFeatureEnabled: true, isFeatureConfigured: true, isInitialized: true } +const nativeTools: OpenAI.Chat.ChatCompletionTool[] = [ + "codebase_search", + "read_file", + "list_files", + "search_files", +].map((name) => ({ type: "function", function: { name, parameters: { type: "object", properties: {} } } })) +const noReadMode: ModeConfig = { slug: "no-read", name: "No read", roleDefinition: "No reading", groups: ["command"] } + +function checkAvailability( + manager: CodeIndexManager | undefined, + expected: boolean, + mode = "code", + settings: { disabledTools?: string[] } = {}, +) { + const names = filterNativeToolsForMode(nativeTools, mode, [noReadMode], {}, manager, settings).flatMap((tool) => + "function" in tool ? [tool.function.name] : [], + ) + const policy = resolveEffectiveToolPolicy({ + mode, + customModes: [noReadMode], + codeIndexManager: manager, + disabledTools: settings.disabledTools, + }) + expect(names.includes("codebase_search")).toBe(expected) + expect(policy.tools.has("codebase_search")).toBe(expected) + for (const tool of ["read_file", "list_files", "search_files"] as const) { + expect(names.includes(tool)).toBe(mode === "code") + expect(policy.tools.has(tool)).toBe(mode === "code") + } +} + +describe("codebase_search readiness across mode filtering APIs", () => { + it("excludes search without a manager while retaining ordinary read tools", () => { + checkAvailability(undefined, false) + }) + + for (const isFeatureEnabled of [false, true]) { + for (const isFeatureConfigured of [false, true]) { + for (const isInitialized of [false, true]) { + it(`agrees for enabled=${isFeatureEnabled}, configured=${isFeatureConfigured}, initialized=${isInitialized}`, () => { + checkAvailability( + makeManager({ isFeatureEnabled, isFeatureConfigured, isInitialized }), + isFeatureEnabled && isFeatureConfigured && isInitialized, + ) + }) + } + } + } + + it.each(["isFeatureEnabled", "isFeatureConfigured", "isInitialized"] as const)( + "rereads live %s changes", + (flag) => { + const flags = { ...ready } + const manager = makeManager(flags) + checkAvailability(manager, true) + flags[flag] = false + checkAvailability(manager, false) + flags[flag] = true + checkAvailability(manager, true) + }, + ) + + it("keeps alternating managers isolated", () => { + const enabled = makeManager({ ...ready }) + const disabled = makeManager({ ...ready, isFeatureEnabled: false }) + checkAvailability(enabled, true) + checkAvailability(disabled, false) + checkAvailability(undefined, false) + checkAvailability(enabled, true) + }) + + it("does not bypass a mode without the read group", () => { + checkAvailability(makeManager({ ...ready }), false, "no-read") + }) + + it("does not bypass disabledTools with a ready manager", () => { + checkAvailability(makeManager({ ...ready }), false, "code", { disabledTools: ["codebase_search"] }) + }) +}) diff --git a/src/core/task/__tests__/build-tools-readiness.integration.spec.ts b/src/core/task/__tests__/build-tools-readiness.integration.spec.ts new file mode 100644 index 0000000000..f769b027ae --- /dev/null +++ b/src/core/task/__tests__/build-tools-readiness.integration.spec.ts @@ -0,0 +1,74 @@ +import type OpenAI from "openai" +import type { CodeIndexManager } from "../../../services/code-index/manager" +import { CodeIndexManagerRegistry } from "../../../services/code-index/code-index-manager-registry" +import { makeExtensionContext } from "../../../test-utils/vscode" +import type { ClineProvider } from "../../webview/ClineProvider" +import { buildNativeToolsArrayWithRestrictions } from "../build-tools" + +vi.mock("../../../services/code-index/code-index-manager-registry", () => ({ + CodeIndexManagerRegistry: { getOrCreate: vi.fn() }, +})) + +function toolNames(tools: OpenAI.Chat.ChatCompletionTool[]) { + return tools.flatMap((tool) => ("function" in tool ? [tool.function.name] : [])) +} + +describe("task tool building with real readiness filtering", () => { + beforeEach(() => vi.clearAllMocks()) + + it.each([false, true])("uses task cwd/context and live manager readiness (restrictions=%s)", async (restricted) => { + const context = makeExtensionContext() + // The builder only consumes context and getMcpHub; avoid constructing the webview provider. + const provider = { context, getMcpHub: () => undefined } as ClineProvider + const flags = { isFeatureEnabled: true, isFeatureConfigured: true, isInitialized: true } + // Only the public readiness getters are consumed by the real filter. + const readyManager = flags as CodeIndexManager + const unreadyManager = { ...flags, isInitialized: false } as CodeIndexManager + const managers = new Map([ + ["/tasks/ready", readyManager], + ["/tasks/unready", unreadyManager], + ]) + vi.mocked(CodeIndexManagerRegistry.getOrCreate).mockImplementation((receivedContext, cwd) => { + expect(receivedContext).toBe(context) + return managers.get(cwd ?? "") + }) + + async function check(cwd: string, expected: boolean, mode = "code", disabledTools: string[] = []) { + const result = await buildNativeToolsArrayWithRestrictions({ + provider, + cwd, + mode, + customModes: [{ slug: "no-read", name: "No read", roleDefinition: "No reading", groups: ["command"] }], + experiments: {}, + apiConfiguration: {}, + disabledTools, + includeAllToolsWithRestrictions: restricted, + }) + expect(CodeIndexManagerRegistry.getOrCreate).toHaveBeenLastCalledWith(context, cwd) + const definitions = toolNames(result.tools) + const callable = restricted ? result.allowedFunctionNames : definitions + expect(callable).toBeDefined() + expect(callable?.includes("codebase_search")).toBe(expected) + expect(callable?.includes("read_file")).toBe(mode === "code") + if (restricted) { + // Historical definitions remain present; only allowedFunctionNames controls calls. + expect(definitions).toContain("codebase_search") + } else { + expect(result.allowedFunctionNames).toBeUndefined() + } + } + + await check("/tasks/ready", true) + await check("/tasks/unready", false) + await check("/tasks/missing", false) + await check("/tasks/ready", true) + for (const flag of ["isFeatureEnabled", "isFeatureConfigured", "isInitialized"] as const) { + flags[flag] = false + await check("/tasks/ready", false) + flags[flag] = true + await check("/tasks/ready", true) + } + await check("/tasks/ready", false, "no-read") + await check("/tasks/ready", false, "code", ["codebase_search"]) + }) +}) From 7a219bcaaa027bfa7069560c76be525119c1a499 Mon Sep 17 00:00:00 2001 From: gubin-dev Date: Fri, 18 Sep 2026 17:58:14 +0300 Subject: [PATCH 2/4] test(code-index): clarify search readiness scenarios --- .../codebase-search-readiness.spec.ts | 189 +++++++++++----- .../build-tools-readiness.integration.spec.ts | 208 ++++++++++++++---- 2 files changed, 288 insertions(+), 109 deletions(-) diff --git a/src/core/prompts/tools/__tests__/codebase-search-readiness.spec.ts b/src/core/prompts/tools/__tests__/codebase-search-readiness.spec.ts index 1982fbf471..2e05f6c6cd 100644 --- a/src/core/prompts/tools/__tests__/codebase-search-readiness.spec.ts +++ b/src/core/prompts/tools/__tests__/codebase-search-readiness.spec.ts @@ -1,93 +1,160 @@ import type OpenAI from "openai" -import type { ModeConfig } from "@roo-code/types" +import { toolNamesSchema, type ModeConfig } from "@roo-code/types" import type { CodeIndexManager } from "../../../../services/code-index/manager" import { filterNativeToolsForMode } from "../filter-tools-for-mode" import { resolveEffectiveToolPolicy } from "../effective-tool-policy" +import { getNativeTools } from "../native-tools" +const tools = toolNamesSchema.enum +const ordinaryReadTools = [tools.read_file, tools.list_files, tools.search_files] type Readiness = Pick function makeManager(flags: Readiness): CodeIndexManager { - // These filters only read the three public readiness getters; no manager services are needed. + // These consumers only read the public readiness getters, not manager services. return flags as CodeIndexManager } -const ready = { isFeatureEnabled: true, isFeatureConfigured: true, isInitialized: true } -const nativeTools: OpenAI.Chat.ChatCompletionTool[] = [ - "codebase_search", - "read_file", - "list_files", - "search_files", -].map((name) => ({ type: "function", function: { name, parameters: { type: "object", properties: {} } } })) -const noReadMode: ModeConfig = { slug: "no-read", name: "No read", roleDefinition: "No reading", groups: ["command"] } - -function checkAvailability( - manager: CodeIndexManager | undefined, - expected: boolean, - mode = "code", - settings: { disabledTools?: string[] } = {}, -) { - const names = filterNativeToolsForMode(nativeTools, mode, [noReadMode], {}, manager, settings).flatMap((tool) => - "function" in tool ? [tool.function.name] : [], - ) - const policy = resolveEffectiveToolPolicy({ - mode, - customModes: [noReadMode], - codeIndexManager: manager, - disabledTools: settings.disabledTools, - }) - expect(names.includes("codebase_search")).toBe(expected) - expect(policy.tools.has("codebase_search")).toBe(expected) - for (const tool of ["read_file", "list_files", "search_files"] as const) { - expect(names.includes(tool)).toBe(mode === "code") - expect(policy.tools.has(tool)).toBe(mode === "code") - } +function toolNames(definitions: OpenAI.Chat.ChatCompletionTool[]) { + return definitions.flatMap((tool) => ("function" in tool ? [tool.function.name] : [])) } -describe("codebase_search readiness across mode filtering APIs", () => { - it("excludes search without a manager while retaining ordinary read tools", () => { - checkAvailability(undefined, false) +describe("codebase_search readiness", () => { + it("excludes search without a manager but retains ordinary read tools", () => { + const policy = resolveEffectiveToolPolicy({ mode: "code" }) + const filtered = filterNativeToolsForMode(getNativeTools(), "code", [], {}) + + expect(policy.tools).not.toContain(tools.codebase_search) + expect(toolNames(filtered)).not.toContain(tools.codebase_search) + for (const tool of ordinaryReadTools) { + expect(policy.tools).toContain(tool) + expect(toolNames(filtered)).toContain(tool) + } }) - for (const isFeatureEnabled of [false, true]) { - for (const isFeatureConfigured of [false, true]) { - for (const isInitialized of [false, true]) { - it(`agrees for enabled=${isFeatureEnabled}, configured=${isFeatureConfigured}, initialized=${isInitialized}`, () => { - checkAvailability( - makeManager({ isFeatureEnabled, isFeatureConfigured, isInitialized }), - isFeatureEnabled && isFeatureConfigured && isInitialized, - ) - }) + it.each([ + { isFeatureEnabled: false, isFeatureConfigured: false, isInitialized: false }, + { isFeatureEnabled: false, isFeatureConfigured: false, isInitialized: true }, + { isFeatureEnabled: false, isFeatureConfigured: true, isInitialized: false }, + { isFeatureEnabled: false, isFeatureConfigured: true, isInitialized: true }, + { isFeatureEnabled: true, isFeatureConfigured: false, isInitialized: false }, + { isFeatureEnabled: true, isFeatureConfigured: false, isInitialized: true }, + { isFeatureEnabled: true, isFeatureConfigured: true, isInitialized: false }, + ])( + "excludes search for enabled=$isFeatureEnabled, configured=$isFeatureConfigured, initialized=$isInitialized", + (flags) => { + const manager = makeManager(flags) + const policy = resolveEffectiveToolPolicy({ mode: "code", codeIndexManager: manager }) + const filtered = filterNativeToolsForMode(getNativeTools(), "code", [], {}, manager) + + expect(policy.tools).not.toContain(tools.codebase_search) + expect(toolNames(filtered)).not.toContain(tools.codebase_search) + for (const tool of ordinaryReadTools) { + expect(policy.tools).toContain(tool) + expect(toolNames(filtered)).toContain(tool) } + }, + ) + + it("includes search when all three readiness conditions are met", () => { + const manager = makeManager({ isFeatureEnabled: true, isFeatureConfigured: true, isInitialized: true }) + const policy = resolveEffectiveToolPolicy({ mode: "code", codeIndexManager: manager }) + const filtered = filterNativeToolsForMode(getNativeTools(), "code", [], {}, manager) + + expect(policy.tools).toContain(tools.codebase_search) + expect(toolNames(filtered)).toContain(tools.codebase_search) + for (const tool of ordinaryReadTools) { + expect(policy.tools).toContain(tool) + expect(toolNames(filtered)).toContain(tool) } - } + }) it.each(["isFeatureEnabled", "isFeatureConfigured", "isInitialized"] as const)( - "rereads live %s changes", + "rereads %s when the same manager becomes unavailable and recovers", (flag) => { - const flags = { ...ready } + const flags = { isFeatureEnabled: true, isFeatureConfigured: true, isInitialized: true } const manager = makeManager(flags) - checkAvailability(manager, true) + + expect(resolveEffectiveToolPolicy({ mode: "code", codeIndexManager: manager }).tools).toContain( + tools.codebase_search, + ) + expect(toolNames(filterNativeToolsForMode(getNativeTools(), "code", [], {}, manager))).toContain( + tools.codebase_search, + ) + flags[flag] = false - checkAvailability(manager, false) + + expect(resolveEffectiveToolPolicy({ mode: "code", codeIndexManager: manager }).tools).not.toContain( + tools.codebase_search, + ) + expect(toolNames(filterNativeToolsForMode(getNativeTools(), "code", [], {}, manager))).not.toContain( + tools.codebase_search, + ) + flags[flag] = true - checkAvailability(manager, true) + + expect(resolveEffectiveToolPolicy({ mode: "code", codeIndexManager: manager }).tools).toContain( + tools.codebase_search, + ) + expect(toolNames(filterNativeToolsForMode(getNativeTools(), "code", [], {}, manager))).toContain( + tools.codebase_search, + ) }, ) - it("keeps alternating managers isolated", () => { - const enabled = makeManager({ ...ready }) - const disabled = makeManager({ ...ready, isFeatureEnabled: false }) - checkAvailability(enabled, true) - checkAvailability(disabled, false) - checkAvailability(undefined, false) - checkAvailability(enabled, true) + it("does not reuse readiness from another manager or a missing manager", () => { + const ready = makeManager({ isFeatureEnabled: true, isFeatureConfigured: true, isInitialized: true }) + const disabled = makeManager({ isFeatureEnabled: false, isFeatureConfigured: true, isInitialized: true }) + + expect(resolveEffectiveToolPolicy({ mode: "code", codeIndexManager: ready }).tools).toContain( + tools.codebase_search, + ) + expect(toolNames(filterNativeToolsForMode(getNativeTools(), "code", [], {}, ready))).toContain( + tools.codebase_search, + ) + + for (const manager of [disabled, undefined]) { + expect(resolveEffectiveToolPolicy({ mode: "code", codeIndexManager: manager }).tools).not.toContain( + tools.codebase_search, + ) + expect(toolNames(filterNativeToolsForMode(getNativeTools(), "code", [], {}, manager))).not.toContain( + tools.codebase_search, + ) + } + + expect(resolveEffectiveToolPolicy({ mode: "code", codeIndexManager: ready }).tools).toContain( + tools.codebase_search, + ) + expect(toolNames(filterNativeToolsForMode(getNativeTools(), "code", [], {}, ready))).toContain( + tools.codebase_search, + ) }) - it("does not bypass a mode without the read group", () => { - checkAvailability(makeManager({ ...ready }), false, "no-read") + it("does not grant read permissions merely because the manager is ready", () => { + const manager = makeManager({ isFeatureEnabled: true, isFeatureConfigured: true, isInitialized: true }) + const mode: ModeConfig = { slug: "no-read", name: "No read", roleDefinition: "No reading", groups: ["command"] } + const policy = resolveEffectiveToolPolicy({ mode: mode.slug, customModes: [mode], codeIndexManager: manager }) + const filtered = filterNativeToolsForMode(getNativeTools(), mode.slug, [mode], {}, manager) + + for (const tool of [tools.codebase_search, ...ordinaryReadTools]) { + expect(policy.tools).not.toContain(tool) + expect(toolNames(filtered)).not.toContain(tool) + } + // The mode remains usable; this is not an accidentally empty result. + expect(policy.tools).toContain(tools.execute_command) + expect(toolNames(filtered)).toContain(tools.execute_command) }) - it("does not bypass disabledTools with a ready manager", () => { - checkAvailability(makeManager({ ...ready }), false, "code", { disabledTools: ["codebase_search"] }) + it("honors explicit search disabling without disabling ordinary read tools", () => { + const manager = makeManager({ isFeatureEnabled: true, isFeatureConfigured: true, isInitialized: true }) + const disabledTools = [tools.codebase_search] + const policy = resolveEffectiveToolPolicy({ mode: "code", codeIndexManager: manager, disabledTools }) + const filtered = filterNativeToolsForMode(getNativeTools(), "code", [], {}, manager, { disabledTools }) + + expect(policy.tools).not.toContain(tools.codebase_search) + expect(toolNames(filtered)).not.toContain(tools.codebase_search) + for (const tool of ordinaryReadTools) { + expect(policy.tools).toContain(tool) + expect(toolNames(filtered)).toContain(tool) + } }) }) diff --git a/src/core/task/__tests__/build-tools-readiness.integration.spec.ts b/src/core/task/__tests__/build-tools-readiness.integration.spec.ts index f769b027ae..5852e135b4 100644 --- a/src/core/task/__tests__/build-tools-readiness.integration.spec.ts +++ b/src/core/task/__tests__/build-tools-readiness.integration.spec.ts @@ -1,4 +1,5 @@ import type OpenAI from "openai" +import { toolNamesSchema } from "@roo-code/types" import type { CodeIndexManager } from "../../../services/code-index/manager" import { CodeIndexManagerRegistry } from "../../../services/code-index/code-index-manager-registry" import { makeExtensionContext } from "../../../test-utils/vscode" @@ -9,66 +10,177 @@ vi.mock("../../../services/code-index/code-index-manager-registry", () => ({ CodeIndexManagerRegistry: { getOrCreate: vi.fn() }, })) -function toolNames(tools: OpenAI.Chat.ChatCompletionTool[]) { - return tools.flatMap((tool) => ("function" in tool ? [tool.function.name] : [])) +const tools = toolNamesSchema.enum +const ordinaryReadTools = [tools.read_file, tools.list_files, tools.search_files] + +function toolNames(definitions: OpenAI.Chat.ChatCompletionTool[]) { + return definitions.flatMap((tool) => ("function" in tool ? [tool.function.name] : [])) +} + +function makeManager(flags: Pick) { + // The real filter only consumes these public readiness getters, not manager services. + return flags as CodeIndexManager } -describe("task tool building with real readiness filtering", () => { - beforeEach(() => vi.clearAllMocks()) +describe.each([ + { strategy: "filtered definitions", includeAllToolsWithRestrictions: false }, + { strategy: "all definitions with an allowlist", includeAllToolsWithRestrictions: true }, +])("task readiness with $strategy", ({ includeAllToolsWithRestrictions }) => { + beforeEach(() => vi.mocked(CodeIndexManagerRegistry.getOrCreate).mockReset()) - it.each([false, true])("uses task cwd/context and live manager readiness (restrictions=%s)", async (restricted) => { + function makeOptions() { const context = makeExtensionContext() - // The builder only consumes context and getMcpHub; avoid constructing the webview provider. + // Only context and getMcpHub are needed; constructing a webview provider is unrelated to this test. const provider = { context, getMcpHub: () => undefined } as ClineProvider - const flags = { isFeatureEnabled: true, isFeatureConfigured: true, isInitialized: true } - // Only the public readiness getters are consumed by the real filter. - const readyManager = flags as CodeIndexManager - const unreadyManager = { ...flags, isInitialized: false } as CodeIndexManager + return { + provider, + cwd: "/tasks/ready", + mode: "code", + customModes: [], + experiments: {}, + apiConfiguration: {}, + includeAllToolsWithRestrictions, + } + } + + it("uses the task context and cwd without leaking readiness between workspaces", async () => { + const options = makeOptions() + const ready = makeManager({ isFeatureEnabled: true, isFeatureConfigured: true, isInitialized: true }) + const unready = makeManager({ isFeatureEnabled: true, isFeatureConfigured: true, isInitialized: false }) const managers = new Map([ - ["/tasks/ready", readyManager], - ["/tasks/unready", unreadyManager], + ["/tasks/ready", ready], + ["/tasks/unready", unready], ]) - vi.mocked(CodeIndexManagerRegistry.getOrCreate).mockImplementation((receivedContext, cwd) => { - expect(receivedContext).toBe(context) - return managers.get(cwd ?? "") - }) + vi.mocked(CodeIndexManagerRegistry.getOrCreate).mockImplementation((_context, cwd) => managers.get(cwd ?? "")) - async function check(cwd: string, expected: boolean, mode = "code", disabledTools: string[] = []) { - const result = await buildNativeToolsArrayWithRestrictions({ - provider, - cwd, - mode, - customModes: [{ slug: "no-read", name: "No read", roleDefinition: "No reading", groups: ["command"] }], - experiments: {}, - apiConfiguration: {}, - disabledTools, - includeAllToolsWithRestrictions: restricted, - }) - expect(CodeIndexManagerRegistry.getOrCreate).toHaveBeenLastCalledWith(context, cwd) - const definitions = toolNames(result.tools) - const callable = restricted ? result.allowedFunctionNames : definitions - expect(callable).toBeDefined() - expect(callable?.includes("codebase_search")).toBe(expected) - expect(callable?.includes("read_file")).toBe(mode === "code") - if (restricted) { - // Historical definitions remain present; only allowedFunctionNames controls calls. - expect(definitions).toContain("codebase_search") - } else { - expect(result.allowedFunctionNames).toBeUndefined() - } + const first = await buildNativeToolsArrayWithRestrictions(options) + expect(CodeIndexManagerRegistry.getOrCreate).toHaveBeenLastCalledWith(options.provider.context, "/tasks/ready") + expect(includeAllToolsWithRestrictions ? first.allowedFunctionNames : toolNames(first.tools)).toContain( + tools.codebase_search, + ) + + const other = await buildNativeToolsArrayWithRestrictions({ ...options, cwd: "/tasks/unready" }) + expect(CodeIndexManagerRegistry.getOrCreate).toHaveBeenLastCalledWith( + options.provider.context, + "/tasks/unready", + ) + if (includeAllToolsWithRestrictions) { + expect(other.allowedFunctionNames).toBeDefined() + expect(other.allowedFunctionNames).not.toContain(tools.codebase_search) + // Keep definitions for historical calls, while forbidding new calls. + expect(toolNames(other.tools)).toContain(tools.codebase_search) + } else { + expect(other.allowedFunctionNames).toBeUndefined() + expect(toolNames(other.tools)).not.toContain(tools.codebase_search) + } + + const restored = await buildNativeToolsArrayWithRestrictions(options) + expect(CodeIndexManagerRegistry.getOrCreate).toHaveBeenLastCalledWith(options.provider.context, "/tasks/ready") + expect(includeAllToolsWithRestrictions ? restored.allowedFunctionNames : toolNames(restored.tools)).toContain( + tools.codebase_search, + ) + expect(CodeIndexManagerRegistry.getOrCreate).toHaveBeenCalledTimes(3) + }) + + it("omits search without a manager while retaining ordinary read tools", async () => { + vi.mocked(CodeIndexManagerRegistry.getOrCreate).mockReturnValue(undefined) + const result = await buildNativeToolsArrayWithRestrictions({ ...makeOptions(), cwd: "/tasks/missing" }) + + if (includeAllToolsWithRestrictions) { + expect(result.allowedFunctionNames).toBeDefined() + expect(result.allowedFunctionNames).not.toContain(tools.codebase_search) + expect(toolNames(result.tools)).toContain(tools.codebase_search) + } else { + expect(result.allowedFunctionNames).toBeUndefined() + expect(toolNames(result.tools)).not.toContain(tools.codebase_search) } + for (const tool of ordinaryReadTools) { + expect(includeAllToolsWithRestrictions ? result.allowedFunctionNames : toolNames(result.tools)).toContain( + tool, + ) + } + }) + + it.each(["isFeatureEnabled", "isFeatureConfigured", "isInitialized"] as const)( + "rereads %s on subsequent builds with the same manager", + async (flag) => { + const options = makeOptions() + const flags = { isFeatureEnabled: true, isFeatureConfigured: true, isInitialized: true } + vi.mocked(CodeIndexManagerRegistry.getOrCreate).mockReturnValue(makeManager(flags)) + + const initial = await buildNativeToolsArrayWithRestrictions(options) + expect(includeAllToolsWithRestrictions ? initial.allowedFunctionNames : toolNames(initial.tools)).toContain( + tools.codebase_search, + ) - await check("/tasks/ready", true) - await check("/tasks/unready", false) - await check("/tasks/missing", false) - await check("/tasks/ready", true) - for (const flag of ["isFeatureEnabled", "isFeatureConfigured", "isInitialized"] as const) { flags[flag] = false - await check("/tasks/ready", false) + const unavailable = await buildNativeToolsArrayWithRestrictions(options) + if (includeAllToolsWithRestrictions) { + expect(unavailable.allowedFunctionNames).toBeDefined() + expect(unavailable.allowedFunctionNames).not.toContain(tools.codebase_search) + expect(toolNames(unavailable.tools)).toContain(tools.codebase_search) + } else { + expect(unavailable.allowedFunctionNames).toBeUndefined() + expect(toolNames(unavailable.tools)).not.toContain(tools.codebase_search) + } + for (const tool of ordinaryReadTools) { + expect( + includeAllToolsWithRestrictions ? unavailable.allowedFunctionNames : toolNames(unavailable.tools), + ).toContain(tool) + } + flags[flag] = true - await check("/tasks/ready", true) + const recovered = await buildNativeToolsArrayWithRestrictions(options) + expect( + includeAllToolsWithRestrictions ? recovered.allowedFunctionNames : toolNames(recovered.tools), + ).toContain(tools.codebase_search) + }, + ) + + it("does not grant read tools to a command-only mode even with a ready manager", async () => { + vi.mocked(CodeIndexManagerRegistry.getOrCreate).mockReturnValue( + makeManager({ isFeatureEnabled: true, isFeatureConfigured: true, isInitialized: true }), + ) + const result = await buildNativeToolsArrayWithRestrictions({ + ...makeOptions(), + mode: "no-read", + customModes: [{ slug: "no-read", name: "No read", roleDefinition: "No reading", groups: ["command"] }], + }) + + if (includeAllToolsWithRestrictions) { + expect(result.allowedFunctionNames).toBeDefined() + expect(toolNames(result.tools)).toContain(tools.codebase_search) + } else { + expect(result.allowedFunctionNames).toBeUndefined() + } + const callable = includeAllToolsWithRestrictions ? result.allowedFunctionNames : toolNames(result.tools) + for (const tool of [tools.codebase_search, ...ordinaryReadTools]) { + expect(callable).not.toContain(tool) + } + expect(callable).toContain(tools.execute_command) + }) + + it("honors disabledTools with a ready manager without disabling ordinary read tools", async () => { + vi.mocked(CodeIndexManagerRegistry.getOrCreate).mockReturnValue( + makeManager({ isFeatureEnabled: true, isFeatureConfigured: true, isInitialized: true }), + ) + const result = await buildNativeToolsArrayWithRestrictions({ + ...makeOptions(), + disabledTools: [tools.codebase_search], + }) + + if (includeAllToolsWithRestrictions) { + expect(result.allowedFunctionNames).toBeDefined() + expect(result.allowedFunctionNames).not.toContain(tools.codebase_search) + expect(toolNames(result.tools)).toContain(tools.codebase_search) + } else { + expect(result.allowedFunctionNames).toBeUndefined() + expect(toolNames(result.tools)).not.toContain(tools.codebase_search) + } + for (const tool of ordinaryReadTools) { + expect(includeAllToolsWithRestrictions ? result.allowedFunctionNames : toolNames(result.tools)).toContain( + tool, + ) } - await check("/tasks/ready", false, "no-read") - await check("/tasks/ready", false, "code", ["codebase_search"]) }) }) From 64dbc301d8339f20c1b6342806c2afb1241f4f98 Mon Sep 17 00:00:00 2001 From: gubin-dev Date: Fri, 18 Sep 2026 18:08:58 +0300 Subject: [PATCH 3/4] test(tools): cover codebase search mode permissions directly --- .../tools/__tests__/validateToolUse.spec.ts | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/core/tools/__tests__/validateToolUse.spec.ts b/src/core/tools/__tests__/validateToolUse.spec.ts index 9e4a8bbd0c..d839cdc646 100644 --- a/src/core/tools/__tests__/validateToolUse.spec.ts +++ b/src/core/tools/__tests__/validateToolUse.spec.ts @@ -1,6 +1,6 @@ // npx vitest run src/core/tools/__tests__/validateToolUse.spec.ts -import type { ModeConfig } from "@roo-code/types" +import { toolNamesSchema, type ModeConfig } from "@roo-code/types" import { modes } from "../../../shared/modes" import { TOOL_GROUPS } from "../../../shared/tools" @@ -49,6 +49,28 @@ describe("mode-validator", () => { }) describe("custom modes", () => { + it("allows codebase search in a read-only mode without manager readiness input", () => { + const mode: ModeConfig = { + slug: "read-only", + name: "Read only", + roleDefinition: "Read the codebase", + groups: ["read"], + } + + expect(isToolAllowedForMode(toolNamesSchema.enum.codebase_search, mode.slug, [mode])).toBe(true) + }) + + it("rejects codebase search in a command-only mode", () => { + const mode: ModeConfig = { + slug: "command-only", + name: "Command only", + roleDefinition: "Run commands without read tools", + groups: ["command"], + } + + expect(isToolAllowedForMode(toolNamesSchema.enum.codebase_search, mode.slug, [mode])).toBe(false) + }) + it("allows tools from custom mode configuration", () => { const customModes: ModeConfig[] = [ { From 847c7e8d1e294f7074a1a6bc9805292dd3c26cab Mon Sep 17 00:00:00 2001 From: Elliott de Launay Date: Sat, 19 Sep 2026 00:47:09 +0000 Subject: [PATCH 4/4] test(build-tools-readiness): add liveness check and extract callable helper --- .../build-tools-readiness.integration.spec.ts | 41 ++++++++----------- 1 file changed, 17 insertions(+), 24 deletions(-) diff --git a/src/core/task/__tests__/build-tools-readiness.integration.spec.ts b/src/core/task/__tests__/build-tools-readiness.integration.spec.ts index 5852e135b4..e1f8888c9d 100644 --- a/src/core/task/__tests__/build-tools-readiness.integration.spec.ts +++ b/src/core/task/__tests__/build-tools-readiness.integration.spec.ts @@ -43,6 +43,10 @@ describe.each([ } } + function callable(result: Awaited>) { + return includeAllToolsWithRestrictions ? result.allowedFunctionNames : toolNames(result.tools) + } + it("uses the task context and cwd without leaking readiness between workspaces", async () => { const options = makeOptions() const ready = makeManager({ isFeatureEnabled: true, isFeatureConfigured: true, isInitialized: true }) @@ -55,9 +59,7 @@ describe.each([ const first = await buildNativeToolsArrayWithRestrictions(options) expect(CodeIndexManagerRegistry.getOrCreate).toHaveBeenLastCalledWith(options.provider.context, "/tasks/ready") - expect(includeAllToolsWithRestrictions ? first.allowedFunctionNames : toolNames(first.tools)).toContain( - tools.codebase_search, - ) + expect(callable(first)).toContain(tools.codebase_search) const other = await buildNativeToolsArrayWithRestrictions({ ...options, cwd: "/tasks/unready" }) expect(CodeIndexManagerRegistry.getOrCreate).toHaveBeenLastCalledWith( @@ -73,12 +75,13 @@ describe.each([ expect(other.allowedFunctionNames).toBeUndefined() expect(toolNames(other.tools)).not.toContain(tools.codebase_search) } + for (const tool of ordinaryReadTools) { + expect(callable(other)).toContain(tool) + } const restored = await buildNativeToolsArrayWithRestrictions(options) expect(CodeIndexManagerRegistry.getOrCreate).toHaveBeenLastCalledWith(options.provider.context, "/tasks/ready") - expect(includeAllToolsWithRestrictions ? restored.allowedFunctionNames : toolNames(restored.tools)).toContain( - tools.codebase_search, - ) + expect(callable(restored)).toContain(tools.codebase_search) expect(CodeIndexManagerRegistry.getOrCreate).toHaveBeenCalledTimes(3) }) @@ -95,9 +98,7 @@ describe.each([ expect(toolNames(result.tools)).not.toContain(tools.codebase_search) } for (const tool of ordinaryReadTools) { - expect(includeAllToolsWithRestrictions ? result.allowedFunctionNames : toolNames(result.tools)).toContain( - tool, - ) + expect(callable(result)).toContain(tool) } }) @@ -109,9 +110,7 @@ describe.each([ vi.mocked(CodeIndexManagerRegistry.getOrCreate).mockReturnValue(makeManager(flags)) const initial = await buildNativeToolsArrayWithRestrictions(options) - expect(includeAllToolsWithRestrictions ? initial.allowedFunctionNames : toolNames(initial.tools)).toContain( - tools.codebase_search, - ) + expect(callable(initial)).toContain(tools.codebase_search) flags[flag] = false const unavailable = await buildNativeToolsArrayWithRestrictions(options) @@ -124,16 +123,12 @@ describe.each([ expect(toolNames(unavailable.tools)).not.toContain(tools.codebase_search) } for (const tool of ordinaryReadTools) { - expect( - includeAllToolsWithRestrictions ? unavailable.allowedFunctionNames : toolNames(unavailable.tools), - ).toContain(tool) + expect(callable(unavailable)).toContain(tool) } flags[flag] = true const recovered = await buildNativeToolsArrayWithRestrictions(options) - expect( - includeAllToolsWithRestrictions ? recovered.allowedFunctionNames : toolNames(recovered.tools), - ).toContain(tools.codebase_search) + expect(callable(recovered)).toContain(tools.codebase_search) }, ) @@ -153,11 +148,11 @@ describe.each([ } else { expect(result.allowedFunctionNames).toBeUndefined() } - const callable = includeAllToolsWithRestrictions ? result.allowedFunctionNames : toolNames(result.tools) + const callableSet = callable(result) for (const tool of [tools.codebase_search, ...ordinaryReadTools]) { - expect(callable).not.toContain(tool) + expect(callableSet).not.toContain(tool) } - expect(callable).toContain(tools.execute_command) + expect(callableSet).toContain(tools.execute_command) }) it("honors disabledTools with a ready manager without disabling ordinary read tools", async () => { @@ -178,9 +173,7 @@ describe.each([ expect(toolNames(result.tools)).not.toContain(tools.codebase_search) } for (const tool of ordinaryReadTools) { - expect(includeAllToolsWithRestrictions ? result.allowedFunctionNames : toolNames(result.tools)).toContain( - tool, - ) + expect(callable(result)).toContain(tool) } }) })