Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
134 changes: 115 additions & 19 deletions apps/cli/src/commands/cli/__tests__/list.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,45 @@
import fs from "fs"
import os from "os"
import path from "path"
import { EventEmitter } from "events"

import { openRouterDefaultModelId, providerIdentifiers } from "@roo-code/types"

import { readWorkspaceTaskSessions } from "@/lib/task-history/index.js"
import { isRecord } from "@/lib/utils/guards.js"

import { listSessions, parseFormat } from "../list.js"
import { listModels, listSessions, parseFormat } from "../list.js"

const extensionHostMock = vi.hoisted(() => ({
activate: vi.fn(async () => undefined),
dispose: vi.fn(async () => undefined),
options: [] as unknown[],
responses: [] as unknown[],
sendToExtension: vi.fn(),
}))

vi.mock("@/agent/index.js", () => ({
ExtensionHost: class extends EventEmitter {
client = {
isInitialized: () => true,
on: vi.fn(() => () => undefined),
}

constructor(options: unknown) {
super()
extensionHostMock.options.push(options)
}

activate = extensionHostMock.activate
dispose = extensionHostMock.dispose

sendToExtension(message: unknown): void {
extensionHostMock.sendToExtension(message)
for (const response of extensionHostMock.responses) {
this.emit("extensionWebviewMessage", response)
}
}
},
}))

vi.mock("@/lib/task-history/index.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("@/lib/task-history/index.js")>()
Expand Down Expand Up @@ -39,30 +77,88 @@ describe("parseFormat", () => {
})
})

describe("router model extraction", () => {
// This mirrors the extraction logic in requestOpenRouterModels (list.ts:226-228)
const extractOpenRouterModels = (routerModelsRaw: unknown) => {
const routerModels = isRecord(routerModelsRaw) ? routerModelsRaw : {}
const openRouterModels = routerModels.openrouter
return isRecord(openRouterModels) ? openRouterModels : {}
}
describe("listModels", () => {
let tempDir: string
let workspacePath: string
let extensionPath: string

it("extracts openrouter models from valid routerModels", () => {
const models = { "openai/gpt-4.1": { contextWindow: 128000, supportsPromptCache: false } }
const result = extractOpenRouterModels({ openrouter: models })
expect(result).toEqual(models)
beforeEach(() => {
vi.clearAllMocks()
extensionHostMock.options.length = 0
extensionHostMock.responses.length = 0

tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "roo-list-test-"))
workspacePath = path.join(tempDir, "workspace")
extensionPath = path.join(tempDir, "extension")
fs.mkdirSync(workspacePath)
fs.mkdirSync(extensionPath)
fs.writeFileSync(path.join(extensionPath, "extension.js"), "")
})

it("returns empty object when routerModels is null", () => {
expect(extractOpenRouterModels(null)).toEqual({})
afterEach(() => {
fs.rmSync(tempDir, { recursive: true, force: true })
vi.restoreAllMocks()
})

it("returns empty object when openrouter key is missing", () => {
expect(extractOpenRouterModels({ requesty: {} })).toEqual({})
const captureStdout = async (fn: () => Promise<void>): Promise<string> => {
const stdoutSpy = vi.spyOn(process.stdout, "write").mockImplementation(() => true)
await fn()
return stdoutSpy.mock.calls.map(([chunk]) => String(chunk)).join("")
}

it("creates a host with resolved paths and returns OpenRouter models", async () => {
const models = { "openai/gpt-4.1": { contextWindow: 128000, supportsPromptCache: false } }
extensionHostMock.responses.push(
{ type: "unrelatedMessage" },
{ type: "routerModels", routerModels: { [providerIdentifiers.openrouter]: models } },
)

const output = await captureStdout(() =>
listModels({
format: "json",
workspace: path.relative(process.cwd(), workspacePath),
extension: path.relative(process.cwd(), extensionPath),
apiKey: "test-api-key",
debug: true,
}),
)

expect(extensionHostMock.options).toEqual([
expect.objectContaining({
mode: "code",
provider: providerIdentifiers.openrouter,
model: openRouterDefaultModelId,
apiKey: "test-api-key",
workspacePath,
extensionPath,
nonInteractive: true,
ephemeral: true,
debug: true,
exitOnComplete: true,
exitOnError: false,
disableOutput: true,
}),
])
expect(extensionHostMock.activate).toHaveBeenCalledOnce()
expect(extensionHostMock.sendToExtension).toHaveBeenCalledWith({
type: "requestRouterModels",
values: { provider: providerIdentifiers.openrouter },
})
expect(extensionHostMock.dispose).toHaveBeenCalledOnce()
expect(JSON.parse(output)).toEqual({ models })
})

it("returns empty object when openrouter value is not a record", () => {
expect(extractOpenRouterModels({ openrouter: "invalid" })).toEqual({})
it.each([
["a malformed routerModels value", null],
["a malformed OpenRouter value", { [providerIdentifiers.openrouter]: "invalid" }],
])("returns an empty model record for %s", async (_description, routerModels) => {
extensionHostMock.responses.push({ type: "routerModels", routerModels })

const output = await captureStdout(() =>
listModels({ format: "json", workspace: workspacePath, extension: extensionPath }),
)

expect(JSON.parse(output)).toEqual({ models: {} })
})
})

Expand Down
146 changes: 146 additions & 0 deletions apps/cli/src/commands/cli/__tests__/run.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,152 @@ import fs from "fs"
import path from "path"
import os from "os"

import { providerIdentifiers } from "@roo-code/types"
import { DEFAULT_FLAGS, FlagOptions } from "@/types/index.js"
import {
resolveLegacyRequireApproval,
resolveModel,
resolveProvider,
resolveReasoningEffort,
resolveWorkspacePath,
run,
} from "../run.js"

const runCommandMocks = vi.hoisted(() => ({
activate: vi.fn(async () => undefined),
dispose: vi.fn(async () => undefined),
loadSettings: vi.fn(),
options: [] as unknown[],
runTask: vi.fn(async () => undefined),
}))

vi.mock("@/lib/storage/index.js", () => ({
loadSettings: runCommandMocks.loadSettings,
}))

vi.mock("@/agent/index.js", () => ({
ExtensionHost: class {
client = {}

constructor(options: unknown) {
runCommandMocks.options.push(options)
}

activate = runCommandMocks.activate
dispose = runCommandMocks.dispose
runTask = runCommandMocks.runTask
},
}))

describe("resolveModel", () => {
it("uses the CLI flag before the settings model", () => {
expect(resolveModel("flag-model", "settings-model")).toBe("flag-model")
})

it("uses the settings model when the CLI flag is absent", () => {
expect(resolveModel(undefined, "settings-model")).toBe("settings-model")
})

it("uses the default model when neither the CLI flag nor settings provide one", () => {
expect(resolveModel()).toBe(DEFAULT_FLAGS.model)
})
})

describe("resolveReasoningEffort", () => {
it("uses CLI, settings, and default values in priority order", () => {
expect(resolveReasoningEffort("high", "low")).toBe("high")
expect(resolveReasoningEffort(undefined, "low")).toBe("low")
expect(resolveReasoningEffort()).toBe(DEFAULT_FLAGS.reasoningEffort)
})
})

describe("resolveProvider", () => {
it("uses CLI, settings, and openrouter values in priority order", () => {
expect(resolveProvider(providerIdentifiers.anthropic, providerIdentifiers.gemini)).toBe(
providerIdentifiers.anthropic,
)
expect(resolveProvider(undefined, providerIdentifiers.gemini)).toBe(providerIdentifiers.gemini)
expect(resolveProvider()).toBe(providerIdentifiers.openrouter)
})
})

describe("resolveWorkspacePath", () => {
it("resolves the provided workspace path", () => {
expect(resolveWorkspacePath("relative/workspace")).toBe(path.resolve("relative/workspace"))
})

it("uses the current working directory when workspace is absent", () => {
expect(resolveWorkspacePath()).toBe(process.cwd())
})
})

describe("resolveLegacyRequireApproval", () => {
it.each([
{ requireApproval: true, dangerouslySkipPermissions: true, expected: true },
{ requireApproval: false, dangerouslySkipPermissions: false, expected: false },
{ requireApproval: undefined, dangerouslySkipPermissions: false, expected: true },
{ requireApproval: undefined, dangerouslySkipPermissions: true, expected: false },
{ requireApproval: undefined, dangerouslySkipPermissions: undefined, expected: undefined },
])(
"resolves requireApproval=$requireApproval and dangerouslySkipPermissions=$dangerouslySkipPermissions",
({ requireApproval, dangerouslySkipPermissions, expected }) => {
expect(resolveLegacyRequireApproval(requireApproval, dangerouslySkipPermissions)).toBe(expected)
},
)
})

describe("run command option resolution", () => {
let workspacePath: string

beforeEach(() => {
vi.clearAllMocks()
runCommandMocks.options.length = 0
workspacePath = fs.mkdtempSync(path.join(os.tmpdir(), "roo-run-test-"))
})

afterEach(() => {
fs.rmSync(workspacePath, { recursive: true, force: true })
vi.restoreAllMocks()
})

it("passes resolved settings and workspace values to the extension host", async () => {
runCommandMocks.loadSettings.mockResolvedValue({
model: "settings-model",
reasoningEffort: "high",
provider: providerIdentifiers.anthropic,
dangerouslySkipPermissions: false,
})
const exitSpy = vi.spyOn(process, "exit").mockImplementation(() => undefined as never)
const flags: FlagOptions = {
continue: false,
workspace: path.relative(process.cwd(), workspacePath),
print: true,
stdinPromptStream: false,
signalOnlyExit: false,
debug: false,
requireApproval: false,
exitOnError: false,
apiKey: "test-api-key",
ephemeral: true,
oneshot: false,
}

await run("test prompt", flags)

expect(runCommandMocks.options).toEqual([
expect.objectContaining({
model: "settings-model",
reasoningEffort: "high",
provider: providerIdentifiers.anthropic,
workspacePath,
nonInteractive: false,
}),
])
expect(runCommandMocks.runTask).toHaveBeenCalledWith("test prompt", undefined)
expect(exitSpy).toHaveBeenCalledWith(0)
})
})

describe("run command --prompt-file option", () => {
let tempDir: string
let promptFilePath: string
Expand Down
10 changes: 5 additions & 5 deletions apps/cli/src/commands/cli/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import pWaitFor from "p-wait-for"

import type { TaskSessionEntry } from "@roo-code/core/cli"
import type { Command, ModelRecord, WebviewMessage } from "@roo-code/types"
import { openRouterDefaultModelId } from "@roo-code/types"
import { openRouterDefaultModelId, providerIdentifiers } from "@roo-code/types"

import { ExtensionHost, type ExtensionHostOptions } from "@/agent/index.js"
import { readWorkspaceTaskSessions } from "@/lib/task-history/index.js"
Expand Down Expand Up @@ -105,13 +105,13 @@ function outputSessionsText(sessions: SessionLike[]): void {
async function createListHost(options: BaseListOptions, hostOptions: ListHostOptions): Promise<ExtensionHost> {
const workspacePath = resolveWorkspacePath(options.workspace)
const extensionPath = resolveExtensionPath(options.extension)
const apiKey = options.apiKey || getApiKeyFromEnv("openrouter")
const apiKey = options.apiKey || getApiKeyFromEnv(providerIdentifiers.openrouter)

const extensionHostOptions: ExtensionHostOptions = {
mode: "code",
reasoningEffort: undefined,
user: null,
provider: "openrouter",
provider: providerIdentifiers.openrouter,
model: openRouterDefaultModelId,
apiKey,
workspacePath,
Expand Down Expand Up @@ -217,14 +217,14 @@ function requestModes(host: ExtensionHost): Promise<ModeLike[]> {
function requestOpenRouterModels(host: ExtensionHost): Promise<ModelRecord> {
return requestFromExtension(
host,
{ type: "requestRouterModels", values: { provider: "openrouter" } },
{ type: "requestRouterModels", values: { provider: providerIdentifiers.openrouter } },
(message) => {
if (message.type !== "routerModels") {
return undefined
}

const routerModels = isRecord(message.routerModels) ? message.routerModels : {}
const openRouterModels = routerModels.openrouter
const openRouterModels = routerModels[providerIdentifiers.openrouter]
return isRecord(openRouterModels) ? (openRouterModels as ModelRecord) : {}
},
)
Expand Down
Loading
Loading