From 76845973d61eb1be16f52f3d90c356d4c2d145cd Mon Sep 17 00:00:00 2001 From: Huggix Date: Sun, 27 Sep 2026 10:52:30 +0300 Subject: [PATCH 1/3] fix(nanogpt): preserve optional command parameters --- src/api/providers/__tests__/nanogpt.spec.ts | 70 +++++++++++++++++++ .../__tests__/NativeToolCallParser.spec.ts | 16 +++++ .../__tests__/execute_command.spec.ts | 30 ++++++++ .../tools/native-tools/execute_command.ts | 21 +++++- src/core/prompts/tools/native-tools/index.ts | 8 ++- src/core/task/__tests__/build-tools.spec.ts | 67 +++++++++++++++++- src/core/task/build-tools.ts | 4 +- .../__tests__/executeCommandTool.spec.ts | 5 ++ 8 files changed, 215 insertions(+), 6 deletions(-) create mode 100644 src/core/prompts/tools/native-tools/__tests__/execute_command.spec.ts diff --git a/src/api/providers/__tests__/nanogpt.spec.ts b/src/api/providers/__tests__/nanogpt.spec.ts index b24e8c9a11..55d2bc4d25 100644 --- a/src/api/providers/__tests__/nanogpt.spec.ts +++ b/src/api/providers/__tests__/nanogpt.spec.ts @@ -10,6 +10,7 @@ import { nanoGptDefaultModelId, providerIdentifiers } from "@roo-code/types" import { buildApiHandler } from "../../index" import { asyncStreamFrom, collectStream } from "../../../test-utils/stream" import { createReadFileTool } from "../../../core/prompts/tools/native-tools/read_file" +import { createExecuteCommandTool } from "../../../core/prompts/tools/native-tools/execute_command" import { NanoGptHandler } from "../nanogpt" import { getModels } from "../fetchers/modelCache" @@ -62,6 +63,75 @@ describe("NanoGptHandler", () => { expect(buildApiHandler({ apiProvider: providerIdentifiers.nanogpt })).toBeInstanceOf(NanoGptHandler) }) + it("disables strict generation without relaxing required fields in a supplied strict schema", async () => { + const tool = createExecuteCommandTool() + await collectStream( + new NanoGptHandler({ nanoGptModelId: "model:thinking" }).createMessage("sys", messages, { + taskId: "test-command-schema", + tools: [tool], + }), + ) + expect(mockCreate).toHaveBeenCalledWith( + expect.objectContaining({ + tools: [ + expect.objectContaining({ + function: expect.objectContaining({ + strict: false, + parameters: expect.objectContaining({ required: ["command", "cwd", "timeout"] }), + }), + }), + ], + }), + expect.anything(), + ) + expect(tool.function.strict).toBe(true) + }) + + it.each([undefined, null, 30])("preserves command arguments with timeout %s", async (timeout) => { + const args = JSON.stringify({ + command: "printf test", + ...(timeout === undefined ? {} : { cwd: null, timeout }), + }) + mockCreate.mockResolvedValue( + asyncStreamFrom([ + { + choices: [ + { + delta: { + tool_calls: [ + { + index: 0, + id: "call-command", + function: { + name: "execute_command", + arguments: args, + }, + }, + ], + }, + }, + ], + }, + ]), + ) + const tool = createExecuteCommandTool({ strict: false }) + const chunks = await collectStream( + new NanoGptHandler({ nanoGptModelId: "model:thinking" }).createMessage("sys", messages, { + taskId: "test-command-arguments", + tools: [tool], + tool_choice: "auto", + }), + ) + expect(mockCreate.mock.calls[0][0].tools[0].function).toMatchObject({ + strict: false, + parameters: { required: ["command"] }, + }) + expect(chunks).toEqual([ + { type: "tool_call_partial", index: 0, id: "call-command", name: "execute_command", arguments: args }, + ]) + expect(tool.function.parameters?.required).toEqual(["command"]) + }) + it("keeps the canonical model ID while applying request-only routing", async () => { const handler = new NanoGptHandler({ nanoGptModelId: "model:thinking", nanoGptRoutingPreference: "fast" }) await collectStream(handler.createMessage("system", messages)) diff --git a/src/core/assistant-message/__tests__/NativeToolCallParser.spec.ts b/src/core/assistant-message/__tests__/NativeToolCallParser.spec.ts index 47ea2a36cc..c7838bd89c 100644 --- a/src/core/assistant-message/__tests__/NativeToolCallParser.spec.ts +++ b/src/core/assistant-message/__tests__/NativeToolCallParser.spec.ts @@ -2,6 +2,22 @@ import { NativeToolCallParser } from "../NativeToolCallParser" describe("NativeToolCallParser", () => { describe("parseToolCall", () => { + describe("execute_command optional arguments", () => { + it.each([undefined, null, 30])("accepts a finalized command with timeout %s", (timeout) => { + const args = { command: "printf test", ...(timeout === undefined ? {} : { cwd: null, timeout }) } + const result = NativeToolCallParser.parseToolCall({ + id: "call-command", + name: "execute_command", + arguments: JSON.stringify(args), + }) + expect(result).toMatchObject({ + type: "tool_use", + name: "execute_command", + nativeArgs: { command: args.command, cwd: args.cwd, timeout }, + }) + }) + }) + describe("read_file tool", () => { it("should parse minimal single-file read_file args", () => { const toolCall = { diff --git a/src/core/prompts/tools/native-tools/__tests__/execute_command.spec.ts b/src/core/prompts/tools/native-tools/__tests__/execute_command.spec.ts new file mode 100644 index 0000000000..e26a5baf60 --- /dev/null +++ b/src/core/prompts/tools/native-tools/__tests__/execute_command.spec.ts @@ -0,0 +1,30 @@ +import executeCommand, { createExecuteCommandTool } from "../execute_command" + +describe("execute_command schema", () => { + it("preserves the strict default for existing integrations", () => { + expect(createExecuteCommandTool()).toEqual(executeCommand) + expect(executeCommand.function.strict).toBe(true) + expect(executeCommand.function.parameters.required).toEqual(["command", "cwd", "timeout"]) + }) + + it("requires only command for non-strict generation while retaining optional field types", () => { + const original = structuredClone(executeCommand) + const tool = createExecuteCommandTool({ strict: false }) + expect(tool.function.strict).toBe(false) + expect(tool.function.parameters).toEqual({ ...original.function.parameters, required: ["command"] }) + expect(tool.function.parameters?.properties).toMatchObject({ + command: { type: "string" }, + cwd: { type: ["string", "null"] }, + timeout: { type: ["number", "null"] }, + }) + expect(executeCommand).toEqual(original) + }) + + it("does not share required arrays across generated schemas", () => { + const first = createExecuteCommandTool({ strict: false }) + const second = createExecuteCommandTool({ strict: false }) + expect(first.function.parameters?.required).not.toBe(second.function.parameters?.required) + expect(second.function.parameters?.required).toEqual(["command"]) + expect(executeCommand.function.parameters.required).toEqual(["command", "cwd", "timeout"]) + }) +}) diff --git a/src/core/prompts/tools/native-tools/execute_command.ts b/src/core/prompts/tools/native-tools/execute_command.ts index 68c68dc5fd..eb86bf787f 100644 --- a/src/core/prompts/tools/native-tools/execute_command.ts +++ b/src/core/prompts/tools/native-tools/execute_command.ts @@ -25,7 +25,7 @@ const CWD_PARAMETER_DESCRIPTION = `Optional working directory for the command, r const TIMEOUT_PARAMETER_DESCRIPTION = `Timeout in seconds. When exceeded, the command continues running in the background and output collected so far is returned. Use this for long-running processes like dev servers, file watchers, or any command that may not exit on its own` -export default { +const executeCommand = { type: "function", function: { name: "execute_command", @@ -52,3 +52,22 @@ export default { }, }, } satisfies OpenAI.Chat.ChatCompletionTool + +export function createExecuteCommandTool({ + strict = true, +}: { strict?: boolean } = {}): OpenAI.Chat.ChatCompletionFunctionTool { + return { + ...executeCommand, + function: { + ...executeCommand.function, + strict, + parameters: { + ...executeCommand.function.parameters, + // Strict generation requires all fields; the executor only requires command. + required: strict ? [...executeCommand.function.parameters.required] : ["command"], + }, + }, + } +} + +export default executeCommand diff --git a/src/core/prompts/tools/native-tools/index.ts b/src/core/prompts/tools/native-tools/index.ts index 758914d2d6..800807b323 100644 --- a/src/core/prompts/tools/native-tools/index.ts +++ b/src/core/prompts/tools/native-tools/index.ts @@ -6,7 +6,7 @@ import askFollowupQuestion from "./ask_followup_question" import attemptCompletion from "./attempt_completion" import codebaseSearch from "./codebase_search" import editTool from "./edit" -import executeCommand from "./execute_command" +import executeCommand, { createExecuteCommandTool } from "./execute_command" import generateImage from "./generate_image" import listFiles from "./list_files" import newTask from "./new_task" @@ -31,6 +31,8 @@ export type { ReadFileToolOptions } from "./read_file" export interface NativeToolsOptions { /** Whether the model supports image processing (default: false) */ supportsImages?: boolean + /** Keep strict required-nullable command fields unless the integration opts out. */ + executeCommandStrict?: boolean } /** @@ -40,7 +42,7 @@ export interface NativeToolsOptions { * @returns Array of native tool definitions */ export function getNativeTools(options: NativeToolsOptions = {}): OpenAI.Chat.ChatCompletionTool[] { - const { supportsImages = false } = options + const { supportsImages = false, executeCommandStrict = true } = options const readFileOptions: ReadFileToolOptions = { supportsImages, @@ -53,7 +55,7 @@ export function getNativeTools(options: NativeToolsOptions = {}): OpenAI.Chat.Ch askFollowupQuestion, attemptCompletion, codebaseSearch, - executeCommand, + executeCommandStrict ? executeCommand : createExecuteCommandTool({ strict: false }), generateImage, listFiles, newTask, diff --git a/src/core/task/__tests__/build-tools.spec.ts b/src/core/task/__tests__/build-tools.spec.ts index 65990932a2..64ce4c442b 100644 --- a/src/core/task/__tests__/build-tools.spec.ts +++ b/src/core/task/__tests__/build-tools.spec.ts @@ -9,7 +9,7 @@ import type OpenAI from "openai" import type * as vscode from "vscode" -import type { McpServer, ModeConfig, ModelInfo } from "@roo-code/types" +import { providerIdentifiers, type McpServer, type ModeConfig, type ModelInfo } from "@roo-code/types" import type { ClineProvider } from "../../webview/ClineProvider" import type { McpHub } from "../../../services/mcp/McpHub" @@ -62,6 +62,71 @@ function toolNames(tools: OpenAI.Chat.ChatCompletionTool[]): string[] { .map((t) => t.function.name) } +describe("NanoGPT command parameter optionality", () => { + it("preserves every other native tool and required nullable MCP fields", async () => { + const inputSchema = { + type: "object", + properties: { timeout: { type: ["number", "null"] } }, + required: ["timeout"], + } + const options = { + provider: makeProvider([ + { + name: "test-server", + config: "{}", + status: "connected", + tools: [{ name: "test_tool", description: "test", inputSchema }], + }, + ]), + cwd: "/test/path", + mode: "code", + customModes: undefined, + experiments: {}, + } + const nano = await buildNativeToolsArrayWithRestrictions({ + ...options, + apiConfiguration: { apiProvider: providerIdentifiers.nanogpt }, + }) + const control = await buildNativeToolsArrayWithRestrictions({ + ...options, + apiConfiguration: { apiProvider: providerIdentifiers.openai }, + }) + const withoutCommand = (tools: OpenAI.Chat.ChatCompletionTool[]) => + tools.filter((tool) => tool.type !== "function" || tool.function.name !== "execute_command") + expect(withoutCommand(nano.tools)).toEqual(withoutCommand(control.tools)) + expect( + nano.tools.find((tool) => tool.type === "function" && tool.function.name.startsWith("mcp--")), + ).toMatchObject({ function: { parameters: { required: ["timeout"] } } }) + }) + + it.each([providerIdentifiers.nanogpt, providerIdentifiers.openai, undefined])( + "builds the command schema for %s without changing other tools", + async (apiProvider) => { + const result = await buildNativeToolsArrayWithRestrictions({ + provider: makeProvider(), + cwd: "/test/path", + mode: "code", + customModes: undefined, + experiments: {}, + apiConfiguration: apiProvider ? { apiProvider } : undefined, + }) + const command = result.tools.find( + (tool) => tool.type === "function" && tool.function.name === "execute_command", + ) + const isNanoGpt = apiProvider === providerIdentifiers.nanogpt + expect(command).toMatchObject({ + function: { + strict: !isNanoGpt, + parameters: { required: isNanoGpt ? ["command"] : ["command", "cwd", "timeout"] }, + }, + }) + expect( + result.tools.find((tool) => tool.type === "function" && tool.function.name === "attempt_completion"), + ).toMatchObject({ function: { strict: true, parameters: { required: ["result"] } } }) + }, + ) +}) + describe("buildNativeToolsArrayWithRestrictions — Gemini includeAllToolsWithRestrictions", () => { const provider = makeProvider() diff --git a/src/core/task/build-tools.ts b/src/core/task/build-tools.ts index 9d395eaa21..88d16ced1a 100644 --- a/src/core/task/build-tools.ts +++ b/src/core/task/build-tools.ts @@ -2,7 +2,7 @@ import path from "path" import type OpenAI from "openai" -import type { ProviderSettings, ModeConfig, ModelInfo } from "@roo-code/types" +import { providerIdentifiers, type ProviderSettings, type ModeConfig, type ModelInfo } from "@roo-code/types" import { customToolRegistry, formatNative } from "@roo-code/core" import type { ClineProvider } from "../webview/ClineProvider" @@ -115,6 +115,8 @@ export async function buildNativeToolsArrayWithRestrictions(options: BuildToolsO // Build native tools with dynamic read_file tool based on settings. const nativeTools = getNativeTools({ supportsImages, + // NanoGPT uses non-strict generation; retain the command executor's optional inputs. + executeCommandStrict: apiConfiguration?.apiProvider !== providerIdentifiers.nanogpt, }) // Resolve mode config to get allowedMcpServers for MCP server filtering. diff --git a/src/core/tools/__tests__/executeCommandTool.spec.ts b/src/core/tools/__tests__/executeCommandTool.spec.ts index a856b180ca..3a561760c3 100644 --- a/src/core/tools/__tests__/executeCommandTool.spec.ts +++ b/src/core/tools/__tests__/executeCommandTool.spec.ts @@ -498,6 +498,11 @@ describe("executeCommandTool", () => { }) describe("Command execution timeout configuration", () => { + it.each([undefined, null])("uses the same default wait for timeout %s", (timeout) => { + delete process.env.ROO_CLI_RUNTIME + expect(executeCommandModule.resolveAgentTimeoutMs(timeout)).toBe(0) + }) + it("should include timeout parameter in ExecuteCommandOptions", () => { // This test verifies that the timeout configuration is properly typed // The actual timeout logic is tested in integration tests From bb6b23cd603899638ccad611b664280ca6653587 Mon Sep 17 00:00:00 2001 From: Huggix Date: Sun, 27 Sep 2026 13:29:32 +0300 Subject: [PATCH 2/3] test: cover command cwd preservation and public schema defaults --- .../__tests__/NativeToolCallParser.spec.ts | 14 ++++++++++++++ .../__tests__/execute_command.spec.ts | 15 +++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/src/core/assistant-message/__tests__/NativeToolCallParser.spec.ts b/src/core/assistant-message/__tests__/NativeToolCallParser.spec.ts index c7838bd89c..5d4d1c9ba8 100644 --- a/src/core/assistant-message/__tests__/NativeToolCallParser.spec.ts +++ b/src/core/assistant-message/__tests__/NativeToolCallParser.spec.ts @@ -3,6 +3,20 @@ import { NativeToolCallParser } from "../NativeToolCallParser" describe("NativeToolCallParser", () => { describe("parseToolCall", () => { describe("execute_command optional arguments", () => { + it.each([undefined, null, 30])("preserves an explicit cwd with timeout %s", (timeout) => { + const args = { command: "printf test", cwd: "/tmp/workspace", timeout } + const result = NativeToolCallParser.parseToolCall({ + id: "call-command-cwd", + name: "execute_command", + arguments: JSON.stringify(args), + }) + expect(result).toMatchObject({ + type: "tool_use", + name: "execute_command", + nativeArgs: args, + }) + }) + it.each([undefined, null, 30])("accepts a finalized command with timeout %s", (timeout) => { const args = { command: "printf test", ...(timeout === undefined ? {} : { cwd: null, timeout }) } const result = NativeToolCallParser.parseToolCall({ diff --git a/src/core/prompts/tools/native-tools/__tests__/execute_command.spec.ts b/src/core/prompts/tools/native-tools/__tests__/execute_command.spec.ts index e26a5baf60..aeda312553 100644 --- a/src/core/prompts/tools/native-tools/__tests__/execute_command.spec.ts +++ b/src/core/prompts/tools/native-tools/__tests__/execute_command.spec.ts @@ -1,4 +1,5 @@ import executeCommand, { createExecuteCommandTool } from "../execute_command" +import { getNativeTools, nativeTools } from ".." describe("execute_command schema", () => { it("preserves the strict default for existing integrations", () => { @@ -7,6 +8,20 @@ describe("execute_command schema", () => { expect(executeCommand.function.parameters.required).toEqual(["command", "cwd", "timeout"]) }) + it.each([ + { name: "getNativeTools()", tools: () => getNativeTools() }, + { name: "nativeTools", tools: () => nativeTools }, + ])("preserves the strict command schema in $name", ({ tools }) => { + const command = tools().find((tool) => tool.type === "function" && tool.function.name === "execute_command") + expect(command).toEqual(executeCommand) + expect(command).toMatchObject({ + function: { + strict: true, + parameters: { required: ["command", "cwd", "timeout"] }, + }, + }) + }) + it("requires only command for non-strict generation while retaining optional field types", () => { const original = structuredClone(executeCommand) const tool = createExecuteCommandTool({ strict: false }) From 088906fd1e1e0b6884916eff7d2aaee3f57cbd94 Mon Sep 17 00:00:00 2001 From: Huggix Date: Mon, 28 Sep 2026 10:15:41 +0200 Subject: [PATCH 3/3] fix: preserve command schemas and isolate timeout tests --- .../providers/__tests__/base-provider.spec.ts | 47 +++++++++++++++++++ src/api/providers/base-provider.ts | 5 +- .../tools/native-tools/execute_command.ts | 6 ++- .../__tests__/executeCommandTool.spec.ts | 10 ++-- 4 files changed, 59 insertions(+), 9 deletions(-) diff --git a/src/api/providers/__tests__/base-provider.spec.ts b/src/api/providers/__tests__/base-provider.spec.ts index ced452f5a5..a796e255d8 100644 --- a/src/api/providers/__tests__/base-provider.spec.ts +++ b/src/api/providers/__tests__/base-provider.spec.ts @@ -4,6 +4,7 @@ import type { ModelInfo } from "@roo-code/types" import { BaseProvider } from "../base-provider" import type { ApiStream } from "../../transform/stream" +import executeCommand, { createExecuteCommandTool } from "../../../core/prompts/tools/native-tools/execute_command" // Create a concrete implementation for testing class TestProvider extends BaseProvider { @@ -41,6 +42,52 @@ describe("BaseProvider", () => { }) describe("convertToolSchemaForOpenAI", () => { + it("preserves nullable source properties while converting nested schemas", () => { + const schema = { + type: "object", + properties: { + name: { type: ["string", "null"] }, + value: { type: ["string", "number", "null"] }, + settings: { + type: ["object", "null"], + properties: { timeout: { type: ["number", "null"] } }, + }, + items: { + type: ["array", "null"], + items: { type: "object", properties: { cwd: { type: ["string", "null"] } } }, + }, + }, + } + const original = structuredClone(schema) + + const result = provider.testConvertToolSchemaForOpenAI(schema) + + expect(schema).toEqual(original) + expect(result.properties).toMatchObject({ + name: { type: "string" }, + value: { type: ["string", "number"] }, + settings: { type: "object", properties: { timeout: { type: "number" } } }, + items: { type: "array", items: { properties: { cwd: { type: "string" } } } }, + }) + }) + + it("preserves command schemas for a later NanoGPT request after strict conversion", () => { + const original = structuredClone(executeCommand) + const earlierNonStrict = createExecuteCommandTool({ strict: false }) + provider.testConvertToolsForOpenAI([executeCommand]) + const laterNonStrict = createExecuteCommandTool({ strict: false }) + + expect(executeCommand).toEqual(original) + for (const tool of [earlierNonStrict, laterNonStrict]) { + expect(tool.function.strict).toBe(false) + expect(tool.function.parameters?.required).toEqual(["command"]) + expect(tool.function.parameters?.properties).toMatchObject({ + cwd: { type: ["string", "null"] }, + timeout: { type: ["number", "null"] }, + }) + } + }) + it("should add additionalProperties: false to object schemas", () => { const schema = { type: "object", diff --git a/src/api/providers/base-provider.ts b/src/api/providers/base-provider.ts index 89366fb619..ea380c2414 100644 --- a/src/api/providers/base-provider.ts +++ b/src/api/providers/base-provider.ts @@ -84,12 +84,13 @@ export abstract class BaseProvider implements ApiHandler { // Recursively process nested objects and convert nullable types const newProps = { ...result.properties } for (const key of allKeys) { - const prop = newProps[key] + let prop = newProps[key] // Handle nullable types by removing null if (prop && Array.isArray(prop.type) && prop.type.includes("null")) { const nonNullTypes = prop.type.filter((t: string) => t !== "null") - prop.type = nonNullTypes.length === 1 ? nonNullTypes[0] : nonNullTypes + prop = { ...prop, type: nonNullTypes.length === 1 ? nonNullTypes[0] : nonNullTypes } + newProps[key] = prop } // Recursively process nested objects diff --git a/src/core/prompts/tools/native-tools/execute_command.ts b/src/core/prompts/tools/native-tools/execute_command.ts index eb86bf787f..213c83db55 100644 --- a/src/core/prompts/tools/native-tools/execute_command.ts +++ b/src/core/prompts/tools/native-tools/execute_command.ts @@ -25,6 +25,8 @@ const CWD_PARAMETER_DESCRIPTION = `Optional working directory for the command, r const TIMEOUT_PARAMETER_DESCRIPTION = `Timeout in seconds. When exceeded, the command continues running in the background and output collected so far is returned. Use this for long-running processes like dev servers, file watchers, or any command that may not exit on its own` +const REQUIRED_COMMAND_PARAMETERS = ["command"] as const + const executeCommand = { type: "function", function: { @@ -47,7 +49,7 @@ const executeCommand = { description: TIMEOUT_PARAMETER_DESCRIPTION, }, }, - required: ["command", "cwd", "timeout"], + required: [...REQUIRED_COMMAND_PARAMETERS, "cwd", "timeout"], additionalProperties: false, }, }, @@ -64,7 +66,7 @@ export function createExecuteCommandTool({ parameters: { ...executeCommand.function.parameters, // Strict generation requires all fields; the executor only requires command. - required: strict ? [...executeCommand.function.parameters.required] : ["command"], + required: strict ? [...executeCommand.function.parameters.required] : [...REQUIRED_COMMAND_PARAMETERS], }, }, } diff --git a/src/core/tools/__tests__/executeCommandTool.spec.ts b/src/core/tools/__tests__/executeCommandTool.spec.ts index 3a561760c3..3c71ac2366 100644 --- a/src/core/tools/__tests__/executeCommandTool.spec.ts +++ b/src/core/tools/__tests__/executeCommandTool.spec.ts @@ -65,9 +65,9 @@ describe("executeCommandTool", () => { let mockHandleError: any let mockPushToolResult: any let mockToolUse: ToolUse<"execute_command"> - const originalCliRuntime = process.env.ROO_CLI_RUNTIME beforeEach(() => { + vitest.stubEnv("ROO_CLI_RUNTIME", undefined) // Reset mocks vitest.clearAllMocks() vitest.useRealTimers() @@ -134,7 +134,7 @@ describe("executeCommandTool", () => { }) afterEach(() => { - process.env.ROO_CLI_RUNTIME = originalCliRuntime + vitest.unstubAllEnvs() vitest.useRealTimers() }) @@ -499,7 +499,7 @@ describe("executeCommandTool", () => { describe("Command execution timeout configuration", () => { it.each([undefined, null])("uses the same default wait for timeout %s", (timeout) => { - delete process.env.ROO_CLI_RUNTIME + vitest.stubEnv("ROO_CLI_RUNTIME", undefined) expect(executeCommandModule.resolveAgentTimeoutMs(timeout)).toBe(0) }) @@ -538,12 +538,12 @@ describe("executeCommandTool", () => { }) it("should ignore model timeout in CLI runtime", () => { - process.env.ROO_CLI_RUNTIME = "1" + vitest.stubEnv("ROO_CLI_RUNTIME", "1") expect(executeCommandModule.resolveAgentTimeoutMs(30)).toBe(0) }) it("should honor model timeout outside CLI runtime", () => { - delete process.env.ROO_CLI_RUNTIME + vitest.stubEnv("ROO_CLI_RUNTIME", undefined) expect(executeCommandModule.resolveAgentTimeoutMs(30)).toBe(30_000) }) })