mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-06 08:18:39 +00:00
Merge branch 'RooCodeInc:main' into main
This commit is contained in:
commit
ab10140c23
13 changed files with 478 additions and 153 deletions
|
|
@ -12,7 +12,9 @@ import { TelemetryService } from "@roo-code/telemetry"
|
|||
import { CloudServiceCallbacks } from "./types"
|
||||
import type { AuthService } from "./auth"
|
||||
import { WebAuthService, StaticTokenAuthService } from "./auth"
|
||||
import { SettingsService } from "./SettingsService"
|
||||
import type { SettingsService } from "./SettingsService"
|
||||
import { CloudSettingsService } from "./CloudSettingsService"
|
||||
import { StaticSettingsService } from "./StaticSettingsService"
|
||||
import { TelemetryClient } from "./TelemetryClient"
|
||||
import { ShareService, TaskNotFoundError } from "./ShareService"
|
||||
|
||||
|
|
@ -59,13 +61,20 @@ export class CloudService {
|
|||
this.authService.on("logged-out", this.authListener)
|
||||
this.authService.on("user-info", this.authListener)
|
||||
|
||||
this.settingsService = new SettingsService(
|
||||
this.context,
|
||||
this.authService,
|
||||
() => this.callbacks.stateChanged?.(),
|
||||
this.log,
|
||||
)
|
||||
this.settingsService.initialize()
|
||||
// Check for static settings environment variable
|
||||
const staticOrgSettings = process.env.ROO_CODE_CLOUD_ORG_SETTINGS
|
||||
if (staticOrgSettings && staticOrgSettings.length > 0) {
|
||||
this.settingsService = new StaticSettingsService(staticOrgSettings, this.log)
|
||||
} else {
|
||||
const cloudSettingsService = new CloudSettingsService(
|
||||
this.context,
|
||||
this.authService,
|
||||
() => this.callbacks.stateChanged?.(),
|
||||
this.log,
|
||||
)
|
||||
cloudSettingsService.initialize()
|
||||
this.settingsService = cloudSettingsService
|
||||
}
|
||||
|
||||
this.telemetryClient = new TelemetryClient(this.authService, this.settingsService)
|
||||
|
||||
|
|
|
|||
136
packages/cloud/src/CloudSettingsService.ts
Normal file
136
packages/cloud/src/CloudSettingsService.ts
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
import * as vscode from "vscode"
|
||||
|
||||
import {
|
||||
ORGANIZATION_ALLOW_ALL,
|
||||
OrganizationAllowList,
|
||||
OrganizationSettings,
|
||||
organizationSettingsSchema,
|
||||
} from "@roo-code/types"
|
||||
|
||||
import { getRooCodeApiUrl } from "./Config"
|
||||
import type { AuthService } from "./auth"
|
||||
import { RefreshTimer } from "./RefreshTimer"
|
||||
import type { SettingsService } from "./SettingsService"
|
||||
|
||||
const ORGANIZATION_SETTINGS_CACHE_KEY = "organization-settings"
|
||||
|
||||
export class CloudSettingsService implements SettingsService {
|
||||
private context: vscode.ExtensionContext
|
||||
private authService: AuthService
|
||||
private settings: OrganizationSettings | undefined = undefined
|
||||
private timer: RefreshTimer
|
||||
private log: (...args: unknown[]) => void
|
||||
|
||||
constructor(
|
||||
context: vscode.ExtensionContext,
|
||||
authService: AuthService,
|
||||
callback: () => void,
|
||||
log?: (...args: unknown[]) => void,
|
||||
) {
|
||||
this.context = context
|
||||
this.authService = authService
|
||||
this.log = log || console.log
|
||||
|
||||
this.timer = new RefreshTimer({
|
||||
callback: async () => {
|
||||
return await this.fetchSettings(callback)
|
||||
},
|
||||
successInterval: 30000,
|
||||
initialBackoffMs: 1000,
|
||||
maxBackoffMs: 30000,
|
||||
})
|
||||
}
|
||||
|
||||
public initialize(): void {
|
||||
this.loadCachedSettings()
|
||||
|
||||
// Clear cached settings if we have missed a log out.
|
||||
if (this.authService.getState() == "logged-out" && this.settings) {
|
||||
this.removeSettings()
|
||||
}
|
||||
|
||||
this.authService.on("active-session", () => {
|
||||
this.timer.start()
|
||||
})
|
||||
|
||||
this.authService.on("logged-out", () => {
|
||||
this.timer.stop()
|
||||
this.removeSettings()
|
||||
})
|
||||
|
||||
if (this.authService.hasActiveSession()) {
|
||||
this.timer.start()
|
||||
}
|
||||
}
|
||||
|
||||
private async fetchSettings(callback: () => void): Promise<boolean> {
|
||||
const token = this.authService.getSessionToken()
|
||||
|
||||
if (!token) {
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${getRooCodeApiUrl()}/api/organization-settings`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
this.log(
|
||||
"[cloud-settings] Failed to fetch organization settings:",
|
||||
response.status,
|
||||
response.statusText,
|
||||
)
|
||||
return false
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
const result = organizationSettingsSchema.safeParse(data)
|
||||
|
||||
if (!result.success) {
|
||||
this.log("[cloud-settings] Invalid organization settings format:", result.error)
|
||||
return false
|
||||
}
|
||||
|
||||
const newSettings = result.data
|
||||
|
||||
if (!this.settings || this.settings.version !== newSettings.version) {
|
||||
this.settings = newSettings
|
||||
await this.cacheSettings()
|
||||
callback()
|
||||
}
|
||||
|
||||
return true
|
||||
} catch (error) {
|
||||
this.log("[cloud-settings] Error fetching organization settings:", error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private async cacheSettings(): Promise<void> {
|
||||
await this.context.globalState.update(ORGANIZATION_SETTINGS_CACHE_KEY, this.settings)
|
||||
}
|
||||
|
||||
private loadCachedSettings(): void {
|
||||
this.settings = this.context.globalState.get<OrganizationSettings>(ORGANIZATION_SETTINGS_CACHE_KEY)
|
||||
}
|
||||
|
||||
public getAllowList(): OrganizationAllowList {
|
||||
return this.settings?.allowList || ORGANIZATION_ALLOW_ALL
|
||||
}
|
||||
|
||||
public getSettings(): OrganizationSettings | undefined {
|
||||
return this.settings
|
||||
}
|
||||
|
||||
private async removeSettings(): Promise<void> {
|
||||
this.settings = undefined
|
||||
await this.cacheSettings()
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this.timer.stop()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,135 +1,23 @@
|
|||
import * as vscode from "vscode"
|
||||
import type { OrganizationAllowList, OrganizationSettings } from "@roo-code/types"
|
||||
|
||||
import {
|
||||
ORGANIZATION_ALLOW_ALL,
|
||||
OrganizationAllowList,
|
||||
OrganizationSettings,
|
||||
organizationSettingsSchema,
|
||||
} from "@roo-code/types"
|
||||
/**
|
||||
* Interface for settings services that provide organization settings
|
||||
*/
|
||||
export interface SettingsService {
|
||||
/**
|
||||
* Get the organization allow list
|
||||
* @returns The organization allow list or default if none available
|
||||
*/
|
||||
getAllowList(): OrganizationAllowList
|
||||
|
||||
import { getRooCodeApiUrl } from "./Config"
|
||||
import type { AuthService } from "./auth"
|
||||
import { RefreshTimer } from "./RefreshTimer"
|
||||
/**
|
||||
* Get the current organization settings
|
||||
* @returns The organization settings or undefined if none available
|
||||
*/
|
||||
getSettings(): OrganizationSettings | undefined
|
||||
|
||||
const ORGANIZATION_SETTINGS_CACHE_KEY = "organization-settings"
|
||||
|
||||
export class SettingsService {
|
||||
private context: vscode.ExtensionContext
|
||||
private authService: AuthService
|
||||
private settings: OrganizationSettings | undefined = undefined
|
||||
private timer: RefreshTimer
|
||||
private log: (...args: unknown[]) => void
|
||||
|
||||
constructor(
|
||||
context: vscode.ExtensionContext,
|
||||
authService: AuthService,
|
||||
callback: () => void,
|
||||
log?: (...args: unknown[]) => void,
|
||||
) {
|
||||
this.context = context
|
||||
this.authService = authService
|
||||
this.log = log || console.log
|
||||
|
||||
this.timer = new RefreshTimer({
|
||||
callback: async () => {
|
||||
return await this.fetchSettings(callback)
|
||||
},
|
||||
successInterval: 30000,
|
||||
initialBackoffMs: 1000,
|
||||
maxBackoffMs: 30000,
|
||||
})
|
||||
}
|
||||
|
||||
public initialize(): void {
|
||||
this.loadCachedSettings()
|
||||
|
||||
// Clear cached settings if we have missed a log out.
|
||||
if (this.authService.getState() == "logged-out" && this.settings) {
|
||||
this.removeSettings()
|
||||
}
|
||||
|
||||
this.authService.on("active-session", () => {
|
||||
this.timer.start()
|
||||
})
|
||||
|
||||
this.authService.on("logged-out", () => {
|
||||
this.timer.stop()
|
||||
this.removeSettings()
|
||||
})
|
||||
|
||||
if (this.authService.hasActiveSession()) {
|
||||
this.timer.start()
|
||||
}
|
||||
}
|
||||
|
||||
private async fetchSettings(callback: () => void): Promise<boolean> {
|
||||
const token = this.authService.getSessionToken()
|
||||
|
||||
if (!token) {
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${getRooCodeApiUrl()}/api/organization-settings`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
this.log(
|
||||
"[cloud-settings] Failed to fetch organization settings:",
|
||||
response.status,
|
||||
response.statusText,
|
||||
)
|
||||
return false
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
const result = organizationSettingsSchema.safeParse(data)
|
||||
|
||||
if (!result.success) {
|
||||
this.log("[cloud-settings] Invalid organization settings format:", result.error)
|
||||
return false
|
||||
}
|
||||
|
||||
const newSettings = result.data
|
||||
|
||||
if (!this.settings || this.settings.version !== newSettings.version) {
|
||||
this.settings = newSettings
|
||||
await this.cacheSettings()
|
||||
callback()
|
||||
}
|
||||
|
||||
return true
|
||||
} catch (error) {
|
||||
this.log("[cloud-settings] Error fetching organization settings:", error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private async cacheSettings(): Promise<void> {
|
||||
await this.context.globalState.update(ORGANIZATION_SETTINGS_CACHE_KEY, this.settings)
|
||||
}
|
||||
|
||||
private loadCachedSettings(): void {
|
||||
this.settings = this.context.globalState.get<OrganizationSettings>(ORGANIZATION_SETTINGS_CACHE_KEY)
|
||||
}
|
||||
|
||||
public getAllowList(): OrganizationAllowList {
|
||||
return this.settings?.allowList || ORGANIZATION_ALLOW_ALL
|
||||
}
|
||||
|
||||
public getSettings(): OrganizationSettings | undefined {
|
||||
return this.settings
|
||||
}
|
||||
|
||||
public async removeSettings(): Promise<void> {
|
||||
this.settings = undefined
|
||||
await this.cacheSettings()
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this.timer.stop()
|
||||
}
|
||||
/**
|
||||
* Dispose of the settings service and clean up resources
|
||||
*/
|
||||
dispose(): void
|
||||
}
|
||||
|
|
|
|||
41
packages/cloud/src/StaticSettingsService.ts
Normal file
41
packages/cloud/src/StaticSettingsService.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import {
|
||||
ORGANIZATION_ALLOW_ALL,
|
||||
OrganizationAllowList,
|
||||
OrganizationSettings,
|
||||
organizationSettingsSchema,
|
||||
} from "@roo-code/types"
|
||||
|
||||
import type { SettingsService } from "./SettingsService"
|
||||
|
||||
export class StaticSettingsService implements SettingsService {
|
||||
private settings: OrganizationSettings
|
||||
private log: (...args: unknown[]) => void
|
||||
|
||||
constructor(envValue: string, log?: (...args: unknown[]) => void) {
|
||||
this.log = log || console.log
|
||||
this.settings = this.parseEnvironmentSettings(envValue)
|
||||
}
|
||||
|
||||
private parseEnvironmentSettings(envValue: string): OrganizationSettings {
|
||||
try {
|
||||
const decodedValue = Buffer.from(envValue, "base64").toString("utf-8")
|
||||
const parsedJson = JSON.parse(decodedValue)
|
||||
return organizationSettingsSchema.parse(parsedJson)
|
||||
} catch (error) {
|
||||
this.log(`[StaticSettingsService] failed to parse static settings: ${error.message}`, error)
|
||||
throw new Error("Failed to parse static settings", { cause: error })
|
||||
}
|
||||
}
|
||||
|
||||
public getAllowList(): OrganizationAllowList {
|
||||
return this.settings?.allowList || ORGANIZATION_ALLOW_ALL
|
||||
}
|
||||
|
||||
public getSettings(): OrganizationSettings | undefined {
|
||||
return this.settings
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
// No resources to clean up for static settings
|
||||
}
|
||||
}
|
||||
|
|
@ -8,7 +8,7 @@ import { BaseTelemetryClient } from "@roo-code/telemetry"
|
|||
|
||||
import { getRooCodeApiUrl } from "./Config"
|
||||
import type { AuthService } from "./auth"
|
||||
import { SettingsService } from "./SettingsService"
|
||||
import type { SettingsService } from "./SettingsService"
|
||||
|
||||
export class TelemetryClient extends BaseTelemetryClient {
|
||||
constructor(
|
||||
|
|
|
|||
146
packages/cloud/src/__tests__/CloudService.integration.test.ts
Normal file
146
packages/cloud/src/__tests__/CloudService.integration.test.ts
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
// npx vitest run src/__tests__/CloudService.integration.test.ts
|
||||
|
||||
import * as vscode from "vscode"
|
||||
import { CloudService } from "../CloudService"
|
||||
import { StaticSettingsService } from "../StaticSettingsService"
|
||||
import { CloudSettingsService } from "../CloudSettingsService"
|
||||
|
||||
vi.mock("vscode", () => ({
|
||||
ExtensionContext: vi.fn(),
|
||||
window: {
|
||||
showInformationMessage: vi.fn(),
|
||||
showErrorMessage: vi.fn(),
|
||||
},
|
||||
env: {
|
||||
openExternal: vi.fn(),
|
||||
},
|
||||
Uri: {
|
||||
parse: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
describe("CloudService Integration - Settings Service Selection", () => {
|
||||
let mockContext: vscode.ExtensionContext
|
||||
|
||||
beforeEach(() => {
|
||||
CloudService.resetInstance()
|
||||
|
||||
mockContext = {
|
||||
subscriptions: [],
|
||||
workspaceState: {
|
||||
get: vi.fn(),
|
||||
update: vi.fn(),
|
||||
keys: vi.fn().mockReturnValue([]),
|
||||
},
|
||||
secrets: {
|
||||
get: vi.fn(),
|
||||
store: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
onDidChange: vi.fn().mockReturnValue({ dispose: vi.fn() }),
|
||||
},
|
||||
globalState: {
|
||||
get: vi.fn(),
|
||||
update: vi.fn(),
|
||||
setKeysForSync: vi.fn(),
|
||||
keys: vi.fn().mockReturnValue([]),
|
||||
},
|
||||
extensionUri: { scheme: "file", path: "/mock/path" },
|
||||
extensionPath: "/mock/path",
|
||||
extensionMode: 1,
|
||||
asAbsolutePath: vi.fn((relativePath: string) => `/mock/path/${relativePath}`),
|
||||
storageUri: { scheme: "file", path: "/mock/storage" },
|
||||
extension: {
|
||||
packageJSON: {
|
||||
version: "1.0.0",
|
||||
},
|
||||
},
|
||||
} as unknown as vscode.ExtensionContext
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
CloudService.resetInstance()
|
||||
delete process.env.ROO_CODE_CLOUD_ORG_SETTINGS
|
||||
delete process.env.ROO_CODE_CLOUD_TOKEN
|
||||
})
|
||||
|
||||
it("should use CloudSettingsService when no environment variable is set", async () => {
|
||||
// Ensure no environment variables are set
|
||||
delete process.env.ROO_CODE_CLOUD_ORG_SETTINGS
|
||||
delete process.env.ROO_CODE_CLOUD_TOKEN
|
||||
|
||||
const cloudService = await CloudService.createInstance(mockContext)
|
||||
|
||||
// Access the private settingsService to check its type
|
||||
const settingsService = (cloudService as unknown as { settingsService: unknown }).settingsService
|
||||
expect(settingsService).toBeInstanceOf(CloudSettingsService)
|
||||
})
|
||||
|
||||
it("should use StaticSettingsService when ROO_CODE_CLOUD_ORG_SETTINGS is set", async () => {
|
||||
const validSettings = {
|
||||
version: 1,
|
||||
cloudSettings: {
|
||||
recordTaskMessages: true,
|
||||
enableTaskSharing: true,
|
||||
taskShareExpirationDays: 30,
|
||||
},
|
||||
defaultSettings: {
|
||||
enableCheckpoints: true,
|
||||
},
|
||||
allowList: {
|
||||
allowAll: true,
|
||||
providers: {},
|
||||
},
|
||||
}
|
||||
|
||||
// Set the environment variable
|
||||
process.env.ROO_CODE_CLOUD_ORG_SETTINGS = Buffer.from(JSON.stringify(validSettings)).toString("base64")
|
||||
|
||||
const cloudService = await CloudService.createInstance(mockContext)
|
||||
|
||||
// Access the private settingsService to check its type
|
||||
const settingsService = (cloudService as unknown as { settingsService: unknown }).settingsService
|
||||
expect(settingsService).toBeInstanceOf(StaticSettingsService)
|
||||
|
||||
// Verify the settings are correctly loaded
|
||||
expect(cloudService.getAllowList()).toEqual(validSettings.allowList)
|
||||
})
|
||||
|
||||
it("should throw error when ROO_CODE_CLOUD_ORG_SETTINGS contains invalid data", async () => {
|
||||
// Set invalid environment variable
|
||||
process.env.ROO_CODE_CLOUD_ORG_SETTINGS = "invalid-base64-data"
|
||||
|
||||
await expect(CloudService.createInstance(mockContext)).rejects.toThrow("Failed to initialize CloudService")
|
||||
})
|
||||
|
||||
it("should prioritize static token auth when both environment variables are set", async () => {
|
||||
const validSettings = {
|
||||
version: 1,
|
||||
cloudSettings: {
|
||||
recordTaskMessages: true,
|
||||
enableTaskSharing: true,
|
||||
taskShareExpirationDays: 30,
|
||||
},
|
||||
defaultSettings: {
|
||||
enableCheckpoints: true,
|
||||
},
|
||||
allowList: {
|
||||
allowAll: true,
|
||||
providers: {},
|
||||
},
|
||||
}
|
||||
|
||||
// Set both environment variables
|
||||
process.env.ROO_CODE_CLOUD_TOKEN = "test-token"
|
||||
process.env.ROO_CODE_CLOUD_ORG_SETTINGS = Buffer.from(JSON.stringify(validSettings)).toString("base64")
|
||||
|
||||
const cloudService = await CloudService.createInstance(mockContext)
|
||||
|
||||
// Should use StaticSettingsService for settings
|
||||
const settingsService = (cloudService as unknown as { settingsService: unknown }).settingsService
|
||||
expect(settingsService).toBeInstanceOf(StaticSettingsService)
|
||||
|
||||
// Should use StaticTokenAuthService for auth (from the existing logic)
|
||||
expect(cloudService.isAuthenticated()).toBe(true)
|
||||
expect(cloudService.hasActiveSession()).toBe(true)
|
||||
})
|
||||
})
|
||||
|
|
@ -5,7 +5,7 @@ import type { ClineMessage } from "@roo-code/types"
|
|||
|
||||
import { CloudService } from "../CloudService"
|
||||
import { WebAuthService } from "../auth/WebAuthService"
|
||||
import { SettingsService } from "../SettingsService"
|
||||
import { CloudSettingsService } from "../CloudSettingsService"
|
||||
import { ShareService, TaskNotFoundError } from "../ShareService"
|
||||
import { TelemetryClient } from "../TelemetryClient"
|
||||
import { TelemetryService } from "@roo-code/telemetry"
|
||||
|
|
@ -29,7 +29,7 @@ vi.mock("@roo-code/telemetry")
|
|||
|
||||
vi.mock("../auth/WebAuthService")
|
||||
|
||||
vi.mock("../SettingsService")
|
||||
vi.mock("../CloudSettingsService")
|
||||
|
||||
vi.mock("../ShareService")
|
||||
|
||||
|
|
@ -150,7 +150,7 @@ describe("CloudService", () => {
|
|||
}
|
||||
|
||||
vi.mocked(WebAuthService).mockImplementation(() => mockAuthService as unknown as WebAuthService)
|
||||
vi.mocked(SettingsService).mockImplementation(() => mockSettingsService as unknown as SettingsService)
|
||||
vi.mocked(CloudSettingsService).mockImplementation(() => mockSettingsService as unknown as CloudSettingsService)
|
||||
vi.mocked(ShareService).mockImplementation(() => mockShareService as unknown as ShareService)
|
||||
vi.mocked(TelemetryClient).mockImplementation(() => mockTelemetryClient as unknown as TelemetryClient)
|
||||
|
||||
|
|
@ -176,7 +176,7 @@ describe("CloudService", () => {
|
|||
|
||||
expect(cloudService).toBeInstanceOf(CloudService)
|
||||
expect(WebAuthService).toHaveBeenCalledWith(mockContext, expect.any(Function))
|
||||
expect(SettingsService).toHaveBeenCalledWith(
|
||||
expect(CloudSettingsService).toHaveBeenCalledWith(
|
||||
mockContext,
|
||||
mockAuthService,
|
||||
expect.any(Function),
|
||||
|
|
|
|||
102
packages/cloud/src/__tests__/StaticSettingsService.test.ts
Normal file
102
packages/cloud/src/__tests__/StaticSettingsService.test.ts
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
// npx vitest run src/__tests__/StaticSettingsService.test.ts
|
||||
|
||||
import { StaticSettingsService } from "../StaticSettingsService"
|
||||
|
||||
describe("StaticSettingsService", () => {
|
||||
const validSettings = {
|
||||
version: 1,
|
||||
cloudSettings: {
|
||||
recordTaskMessages: true,
|
||||
enableTaskSharing: true,
|
||||
taskShareExpirationDays: 30,
|
||||
},
|
||||
defaultSettings: {
|
||||
enableCheckpoints: true,
|
||||
maxOpenTabsContext: 10,
|
||||
},
|
||||
allowList: {
|
||||
allowAll: false,
|
||||
providers: {
|
||||
anthropic: {
|
||||
allowAll: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const validBase64 = Buffer.from(JSON.stringify(validSettings)).toString("base64")
|
||||
|
||||
describe("constructor", () => {
|
||||
it("should parse valid base64 encoded JSON settings", () => {
|
||||
const service = new StaticSettingsService(validBase64)
|
||||
expect(service.getSettings()).toEqual(validSettings)
|
||||
})
|
||||
|
||||
it("should throw error for invalid base64", () => {
|
||||
expect(() => new StaticSettingsService("invalid-base64!@#")).toThrow("Failed to parse static settings")
|
||||
})
|
||||
|
||||
it("should throw error for invalid JSON", () => {
|
||||
const invalidJson = Buffer.from("{ invalid json }").toString("base64")
|
||||
expect(() => new StaticSettingsService(invalidJson)).toThrow("Failed to parse static settings")
|
||||
})
|
||||
|
||||
it("should throw error for invalid schema", () => {
|
||||
const invalidSettings = { invalid: "schema" }
|
||||
const invalidBase64 = Buffer.from(JSON.stringify(invalidSettings)).toString("base64")
|
||||
expect(() => new StaticSettingsService(invalidBase64)).toThrow("Failed to parse static settings")
|
||||
})
|
||||
})
|
||||
|
||||
describe("getAllowList", () => {
|
||||
it("should return the allow list from settings", () => {
|
||||
const service = new StaticSettingsService(validBase64)
|
||||
expect(service.getAllowList()).toEqual(validSettings.allowList)
|
||||
})
|
||||
})
|
||||
|
||||
describe("getSettings", () => {
|
||||
it("should return the parsed settings", () => {
|
||||
const service = new StaticSettingsService(validBase64)
|
||||
expect(service.getSettings()).toEqual(validSettings)
|
||||
})
|
||||
})
|
||||
|
||||
describe("dispose", () => {
|
||||
it("should be a no-op for static settings", () => {
|
||||
const service = new StaticSettingsService(validBase64)
|
||||
expect(() => service.dispose()).not.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe("logging", () => {
|
||||
it("should use provided logger for errors", () => {
|
||||
const mockLog = vi.fn()
|
||||
expect(() => new StaticSettingsService("invalid-base64!@#", mockLog)).toThrow()
|
||||
|
||||
expect(mockLog).toHaveBeenCalledWith(
|
||||
expect.stringContaining("[StaticSettingsService] failed to parse static settings:"),
|
||||
expect.any(Error),
|
||||
)
|
||||
})
|
||||
|
||||
it("should use console.log as default logger for errors", () => {
|
||||
const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {})
|
||||
expect(() => new StaticSettingsService("invalid-base64!@#")).toThrow()
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining("[StaticSettingsService] failed to parse static settings:"),
|
||||
expect.any(Error),
|
||||
)
|
||||
|
||||
consoleSpy.mockRestore()
|
||||
})
|
||||
|
||||
it("should not log anything for successful parsing", () => {
|
||||
const mockLog = vi.fn()
|
||||
new StaticSettingsService(validBase64, mockLog)
|
||||
|
||||
expect(mockLog).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -190,10 +190,8 @@ export const parseOpenRouterModel = ({
|
|||
|
||||
const supportsPromptCache = typeof cacheWritesPrice !== "undefined" && typeof cacheReadsPrice !== "undefined"
|
||||
|
||||
const useMaxTokens = OPEN_ROUTER_REASONING_BUDGET_MODELS.has(id) || id.startsWith("anthropic/")
|
||||
|
||||
const modelInfo: ModelInfo = {
|
||||
maxTokens: useMaxTokens ? maxTokens || 0 : 0,
|
||||
maxTokens: maxTokens || Math.ceil(model.context_length * 0.2),
|
||||
contextWindow: model.context_length,
|
||||
supportsImages: modality?.includes("image") ?? false,
|
||||
supportsPromptCache,
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ If asked to create a project mode, create it in .roomodes in the workspace root.
|
|||
* groups: Array of allowed tool groups (can be empty). Each group can be specified either as a string (e.g., "edit" to allow editing any file) or with file restrictions (e.g., ["edit", { fileRegex: "\\.md$", description: "Markdown files only" }] to only allow editing markdown files)
|
||||
|
||||
- The following fields are optional but highly recommended:
|
||||
* description: A short, human-readable description of what this mode does (5 words)
|
||||
* whenToUse: A clear description of when this mode should be selected and what types of tasks it's best suited for. This helps the Orchestrator mode make better decisions.
|
||||
* customInstructions: Additional instructions for how the mode should operate
|
||||
|
||||
|
|
@ -36,6 +37,7 @@ Both files should follow this structure (in YAML format):
|
|||
customModes:
|
||||
- slug: designer # Required: unique slug with lowercase letters, numbers, and hyphens
|
||||
name: Designer # Required: mode display name
|
||||
description: UI/UX design systems expert # Optional but recommended: short description (5 words)
|
||||
roleDefinition: >-
|
||||
You are Roo, a UI/UX expert specializing in design systems and frontend development. Your expertise includes:
|
||||
- Creating and maintaining design systems
|
||||
|
|
@ -43,8 +45,8 @@ customModes:
|
|||
- Working with CSS, HTML, and modern frontend frameworks
|
||||
- Ensuring consistent user experiences across platforms # Required: non-empty
|
||||
whenToUse: >-
|
||||
Use this mode when creating or modifying UI components, implementing design systems,
|
||||
or ensuring responsive web interfaces. This mode is especially effective with CSS,
|
||||
Use this mode when creating or modifying UI components, implementing design systems,
|
||||
or ensuring responsive web interfaces. This mode is especially effective with CSS,
|
||||
HTML, and modern frontend frameworks. # Optional but recommended
|
||||
groups: # Required: array of tool groups (can be empty)
|
||||
- read # Read files group (read_file, fetch_instructions, search_files, list_files, list_code_definition_names)
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ export async function newTaskTool(
|
|||
const partialMessage = JSON.stringify({
|
||||
tool: "newTask",
|
||||
mode: removeClosingTag("mode", mode),
|
||||
message: removeClosingTag("message", message),
|
||||
content: removeClosingTag("message", message),
|
||||
})
|
||||
|
||||
await cline.ask("tool", partialMessage, block.partial).catch(() => {})
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ describe("getMaxTokensForModel", () => {
|
|||
expect(getModelMaxOutputTokens({ modelId, model, settings })).toBe(8000)
|
||||
})
|
||||
|
||||
it("should return undefined for non-thinking models with undefined maxTokens", () => {
|
||||
it("should return 20% of context window for non-thinking models with undefined maxTokens", () => {
|
||||
const model: ModelInfo = {
|
||||
contextWindow: 200_000,
|
||||
supportsPromptCache: true,
|
||||
|
|
@ -76,7 +76,8 @@ describe("getMaxTokensForModel", () => {
|
|||
modelMaxTokens: 4000,
|
||||
}
|
||||
|
||||
expect(getModelMaxOutputTokens({ modelId, model, settings })).toBeUndefined()
|
||||
// Should return 20% of context window when maxTokens is undefined
|
||||
expect(getModelMaxOutputTokens({ modelId, model, settings })).toBe(40000)
|
||||
})
|
||||
|
||||
test("should return maxTokens from modelInfo when thinking is false", () => {
|
||||
|
|
|
|||
|
|
@ -71,7 +71,9 @@ export const getModelMaxOutputTokens = ({
|
|||
return ANTHROPIC_DEFAULT_MAX_TOKENS
|
||||
}
|
||||
|
||||
return model.maxTokens ?? undefined
|
||||
// If maxTokens is 0 or undefined, fall back to 20% of context window
|
||||
// This matches the sliding window logic
|
||||
return model.maxTokens || Math.ceil(model.contextWindow * 0.2)
|
||||
}
|
||||
|
||||
// GetModelsOptions
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue