Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
5d395c1
feat(task-persistence): add TaskOrganizationStore with atomic persist…
Aug 1, 2026
6ffe9da
feat(task-org-ipc): add task organization IPC message handler and pro…
Aug 2, 2026
caa9e4a
feat(task-organization): add DnD folder management and task grouping
k1yt Jul 25, 2026
da91537
fix(history): prevent workspace cross-contamination of tasks, pins, a…
Jul 27, 2026
a518712
fix(history): hide workspace-specific folders when no workspace is open
Jul 27, 2026
1e4e86a
fix: resolve TaskOrganizationStore test failures
Jul 31, 2026
a9653ed
fix(knip): ignore B10 unused file TaskStatusBadge and dnd-kit depende…
Aug 2, 2026
4d8ec8c
fix(history): align DraggableTaskEntry tests with role-stripping, add…
Aug 2, 2026
fad6f9b
fix(task-organization): resolve lock bypass, root-task orphaning, sta…
Aug 3, 2026
74aee22
fix(task-organization): resolve lock bypass, root-task orphaning, sta…
Aug 3, 2026
c43c6f3
fix(task-organization): reject same-revision writes and harden watche…
Aug 3, 2026
504ab98
fix(task-organization): guard reconcile against constructor-order race
Aug 3, 2026
0d90a6a
fix(task-organization): guard taskOrganization revision in full-state…
Aug 3, 2026
be9c93c
fix(history): scope folder pins to the workspace filter and align emp…
Aug 3, 2026
bd15a14
feat(history): add pin toggles to grouped-mode task rows for Welcome/…
Aug 3, 2026
076b4f7
feat(history): show pinned shortcuts at the top of Welcome Recent Tasks
Aug 4, 2026
e1a673e
ci: retrigger workflow after runner capacity issue
Aug 6, 2026
899e5a2
fix: prune stale eslint-suppressions.json entries
Aug 6, 2026
2296a80
chore: remove temp file progress.txt
Aug 6, 2026
7405019
feat: restore pinned folder expand/collapse UI in HistoryView and His…
Aug 6, 2026
86a5814
test(b10): add coverage tests for task organization UI, DnD edge case…
Aug 7, 2026
3e88d47
refactor(cli): canonicalize provider identifiers (#1110)
WebMad Aug 7, 2026
d33e40d
[Refactor] Reuse shared API options in provider tests (#1178)
zoomote[bot] Aug 7, 2026
bd2d8a2
fix: remove unused TaskOrganizationMutationResultV1 import
Aug 7, 2026
f67bcd5
test(b10): boost diff coverage to 98.3% with handleMessage, selection…
Aug 7, 2026
e6964f5
docs(b10): add JSDoc to exported symbols and boost safeWriteJson/Task…
Aug 7, 2026
102f55a
fix: replace 28 any types with type-safe alternatives in safeWriteJso…
Aug 7, 2026
494660e
chore: remove temporary docs and scripts from PR diff
Aug 7, 2026
276e425
refactor: reuse shared XAI response client mock (#1182)
zoomote[bot] Aug 7, 2026
2f29257
test: add Playwright snapshot and e2e test for task organization UI
Aug 8, 2026
2fcfe90
refactor: reuse shared CustomModesManager test helpers (#1190)
zoomote[bot] Aug 8, 2026
e08a424
fix(test): add maxDiffPixelRatio tolerance for cross-platform visual …
Aug 8, 2026
58121c4
fix(test): use maxDiffPixels for cross-platform visual test tolerance
Aug 8, 2026
dbb6965
fix(test): resolve PR #1129 CI failures
Aug 8, 2026
12c7e70
fix(test): resolve PR #1129 CI failures (visual mount, lint, e2e fixt…
Aug 8, 2026
200d2aa
fix(telemetry): record tool usage once centrally, sanitize raw tool n…
edelauna Aug 8, 2026
fb57aeb
refactor: reuse code-index reset helpers (#1194)
zoomote[bot] Aug 8, 2026
d5085a2
refactor: reuse shared config test helpers (#1195)
zoomote[bot] Aug 8, 2026
5afa9b3
refactor: reuse terminal test reset helpers (#1197)
zoomote[bot] Aug 8, 2026
c2e8e2e
chore: merge main and update eslint suppressions
Aug 8, 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
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,10 @@ qdrant_storage/
plans/

roo-cli-*.tar.gz*

# Session reports and temp artifacts
docs/26*/
coverage-json/
scripts/fix_*.py
scripts/resolve_*.py
scripts/insert_*.py
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