mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
Co-authored-by: Roo Code <roomote@roocode.com> Co-authored-by: Matt Rubens <mrubens@users.noreply.github.com>
436 lines
12 KiB
TypeScript
436 lines
12 KiB
TypeScript
// npx vitest run api/providers/__tests__/roo.spec.ts
|
|
|
|
import { Anthropic } from "@anthropic-ai/sdk"
|
|
import { rooDefaultModelId, rooModels } from "@roo-code/types"
|
|
|
|
import { ApiHandlerOptions } from "../../../shared/api"
|
|
|
|
// Mock OpenAI client
|
|
const mockCreate = vitest.fn()
|
|
|
|
vitest.mock("openai", () => {
|
|
return {
|
|
__esModule: true,
|
|
default: vitest.fn().mockImplementation(() => ({
|
|
chat: {
|
|
completions: {
|
|
create: mockCreate.mockImplementation(async (options) => {
|
|
if (!options.stream) {
|
|
return {
|
|
id: "test-completion",
|
|
choices: [
|
|
{
|
|
message: { role: "assistant", content: "Test response" },
|
|
finish_reason: "stop",
|
|
index: 0,
|
|
},
|
|
],
|
|
usage: {
|
|
prompt_tokens: 10,
|
|
completion_tokens: 5,
|
|
total_tokens: 15,
|
|
},
|
|
}
|
|
}
|
|
|
|
return {
|
|
[Symbol.asyncIterator]: async function* () {
|
|
yield {
|
|
choices: [
|
|
{
|
|
delta: { content: "Test response" },
|
|
index: 0,
|
|
},
|
|
],
|
|
usage: null,
|
|
}
|
|
yield {
|
|
choices: [
|
|
{
|
|
delta: {},
|
|
index: 0,
|
|
},
|
|
],
|
|
usage: {
|
|
prompt_tokens: 10,
|
|
completion_tokens: 5,
|
|
total_tokens: 15,
|
|
},
|
|
}
|
|
},
|
|
}
|
|
}),
|
|
},
|
|
},
|
|
})),
|
|
}
|
|
})
|
|
|
|
// Mock CloudService - Define functions outside to avoid initialization issues
|
|
const mockGetSessionToken = vitest.fn()
|
|
const mockHasInstance = vitest.fn()
|
|
|
|
// Create mock functions that we can control
|
|
const mockGetSessionTokenFn = vitest.fn()
|
|
const mockHasInstanceFn = vitest.fn()
|
|
|
|
vitest.mock("@roo-code/cloud", () => ({
|
|
CloudService: {
|
|
hasInstance: () => mockHasInstanceFn(),
|
|
get instance() {
|
|
return {
|
|
authService: {
|
|
getSessionToken: () => mockGetSessionTokenFn(),
|
|
},
|
|
}
|
|
},
|
|
},
|
|
}))
|
|
|
|
// Mock i18n
|
|
vitest.mock("../../../i18n", () => ({
|
|
t: vitest.fn((key: string) => {
|
|
if (key === "common:errors.roo.authenticationRequired") {
|
|
return "Authentication required for Roo Code Cloud"
|
|
}
|
|
return key
|
|
}),
|
|
}))
|
|
|
|
// Import after mocks are set up
|
|
import { RooHandler } from "../roo"
|
|
import { CloudService } from "@roo-code/cloud"
|
|
import { t } from "../../../i18n"
|
|
|
|
describe("RooHandler", () => {
|
|
let handler: RooHandler
|
|
let mockOptions: ApiHandlerOptions
|
|
const systemPrompt = "You are a helpful assistant."
|
|
const messages: Anthropic.Messages.MessageParam[] = [
|
|
{
|
|
role: "user",
|
|
content: "Hello!",
|
|
},
|
|
]
|
|
|
|
beforeEach(() => {
|
|
mockOptions = {
|
|
apiModelId: "roo/sonic",
|
|
}
|
|
// Set up CloudService mocks for successful authentication
|
|
mockHasInstanceFn.mockReturnValue(true)
|
|
mockGetSessionTokenFn.mockReturnValue("test-session-token")
|
|
mockCreate.mockClear()
|
|
vitest.clearAllMocks()
|
|
})
|
|
|
|
describe("constructor", () => {
|
|
it("should initialize with valid session token", () => {
|
|
handler = new RooHandler(mockOptions)
|
|
expect(handler).toBeInstanceOf(RooHandler)
|
|
expect(handler.getModel().id).toBe(mockOptions.apiModelId)
|
|
})
|
|
|
|
it("should throw error if CloudService is not available", () => {
|
|
mockHasInstanceFn.mockReturnValue(false)
|
|
expect(() => {
|
|
new RooHandler(mockOptions)
|
|
}).toThrow("Authentication required for Roo Code Cloud")
|
|
expect(t).toHaveBeenCalledWith("common:errors.roo.authenticationRequired")
|
|
})
|
|
|
|
it("should throw error if session token is not available", () => {
|
|
mockHasInstanceFn.mockReturnValue(true)
|
|
mockGetSessionTokenFn.mockReturnValue(null)
|
|
expect(() => {
|
|
new RooHandler(mockOptions)
|
|
}).toThrow("Authentication required for Roo Code Cloud")
|
|
expect(t).toHaveBeenCalledWith("common:errors.roo.authenticationRequired")
|
|
})
|
|
|
|
it("should initialize with default model if no model specified", () => {
|
|
handler = new RooHandler({})
|
|
expect(handler).toBeInstanceOf(RooHandler)
|
|
expect(handler.getModel().id).toBe(rooDefaultModelId)
|
|
})
|
|
|
|
it("should pass correct configuration to base class", () => {
|
|
handler = new RooHandler(mockOptions)
|
|
expect(handler).toBeInstanceOf(RooHandler)
|
|
// The handler should be initialized with correct base URL and API key
|
|
// We can't directly test the parent class constructor, but we can verify the handler works
|
|
expect(handler).toBeDefined()
|
|
})
|
|
})
|
|
|
|
describe("createMessage", () => {
|
|
beforeEach(() => {
|
|
handler = new RooHandler(mockOptions)
|
|
})
|
|
|
|
it("should handle streaming responses", async () => {
|
|
const stream = handler.createMessage(systemPrompt, messages)
|
|
const chunks: any[] = []
|
|
for await (const chunk of stream) {
|
|
chunks.push(chunk)
|
|
}
|
|
|
|
expect(chunks.length).toBeGreaterThan(0)
|
|
const textChunks = chunks.filter((chunk) => chunk.type === "text")
|
|
expect(textChunks).toHaveLength(1)
|
|
expect(textChunks[0].text).toBe("Test response")
|
|
})
|
|
|
|
it("should include usage information", async () => {
|
|
const stream = handler.createMessage(systemPrompt, messages)
|
|
const chunks: any[] = []
|
|
for await (const chunk of stream) {
|
|
chunks.push(chunk)
|
|
}
|
|
|
|
const usageChunks = chunks.filter((chunk) => chunk.type === "usage")
|
|
expect(usageChunks).toHaveLength(1)
|
|
expect(usageChunks[0].inputTokens).toBe(10)
|
|
expect(usageChunks[0].outputTokens).toBe(5)
|
|
})
|
|
|
|
it("should handle API errors", async () => {
|
|
mockCreate.mockRejectedValueOnce(new Error("API Error"))
|
|
const stream = handler.createMessage(systemPrompt, messages)
|
|
await expect(async () => {
|
|
for await (const _chunk of stream) {
|
|
// Should not reach here
|
|
}
|
|
}).rejects.toThrow("API Error")
|
|
})
|
|
|
|
it("should handle empty response content", async () => {
|
|
mockCreate.mockResolvedValueOnce({
|
|
[Symbol.asyncIterator]: async function* () {
|
|
yield {
|
|
choices: [
|
|
{
|
|
delta: { content: null },
|
|
index: 0,
|
|
},
|
|
],
|
|
usage: {
|
|
prompt_tokens: 10,
|
|
completion_tokens: 0,
|
|
total_tokens: 10,
|
|
},
|
|
}
|
|
},
|
|
})
|
|
|
|
const stream = handler.createMessage(systemPrompt, messages)
|
|
const chunks: any[] = []
|
|
for await (const chunk of stream) {
|
|
chunks.push(chunk)
|
|
}
|
|
|
|
const textChunks = chunks.filter((chunk) => chunk.type === "text")
|
|
expect(textChunks).toHaveLength(0)
|
|
const usageChunks = chunks.filter((chunk) => chunk.type === "usage")
|
|
expect(usageChunks).toHaveLength(1)
|
|
})
|
|
|
|
it("should handle multiple messages in conversation", async () => {
|
|
const multipleMessages: Anthropic.Messages.MessageParam[] = [
|
|
{ role: "user", content: "First message" },
|
|
{ role: "assistant", content: "First response" },
|
|
{ role: "user", content: "Second message" },
|
|
]
|
|
|
|
const stream = handler.createMessage(systemPrompt, multipleMessages)
|
|
const chunks: any[] = []
|
|
for await (const chunk of stream) {
|
|
chunks.push(chunk)
|
|
}
|
|
|
|
expect(mockCreate).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
messages: expect.arrayContaining([
|
|
expect.objectContaining({ role: "system", content: systemPrompt }),
|
|
expect.objectContaining({ role: "user", content: "First message" }),
|
|
expect.objectContaining({ role: "assistant", content: "First response" }),
|
|
expect.objectContaining({ role: "user", content: "Second message" }),
|
|
]),
|
|
}),
|
|
)
|
|
})
|
|
})
|
|
|
|
describe("completePrompt", () => {
|
|
beforeEach(() => {
|
|
handler = new RooHandler(mockOptions)
|
|
})
|
|
|
|
it("should complete prompt successfully", async () => {
|
|
const result = await handler.completePrompt("Test prompt")
|
|
expect(result).toBe("Test response")
|
|
expect(mockCreate).toHaveBeenCalledWith({
|
|
model: mockOptions.apiModelId,
|
|
messages: [{ role: "user", content: "Test prompt" }],
|
|
})
|
|
})
|
|
|
|
it("should handle API errors", async () => {
|
|
mockCreate.mockRejectedValueOnce(new Error("API Error"))
|
|
await expect(handler.completePrompt("Test prompt")).rejects.toThrow(
|
|
"Roo Code Cloud completion error: API Error",
|
|
)
|
|
})
|
|
|
|
it("should handle empty response", async () => {
|
|
mockCreate.mockResolvedValueOnce({
|
|
choices: [{ message: { content: "" } }],
|
|
})
|
|
const result = await handler.completePrompt("Test prompt")
|
|
expect(result).toBe("")
|
|
})
|
|
|
|
it("should handle missing response content", async () => {
|
|
mockCreate.mockResolvedValueOnce({
|
|
choices: [{ message: {} }],
|
|
})
|
|
const result = await handler.completePrompt("Test prompt")
|
|
expect(result).toBe("")
|
|
})
|
|
})
|
|
|
|
describe("getModel", () => {
|
|
beforeEach(() => {
|
|
handler = new RooHandler(mockOptions)
|
|
})
|
|
|
|
it("should return model info for specified model", () => {
|
|
const modelInfo = handler.getModel()
|
|
expect(modelInfo.id).toBe(mockOptions.apiModelId)
|
|
expect(modelInfo.info).toBeDefined()
|
|
// roo/sonic is a valid model in rooModels
|
|
expect(modelInfo.info).toBe(rooModels["roo/sonic"])
|
|
})
|
|
|
|
it("should return default model when no model specified", () => {
|
|
const handlerWithoutModel = new RooHandler({})
|
|
const modelInfo = handlerWithoutModel.getModel()
|
|
expect(modelInfo.id).toBe(rooDefaultModelId)
|
|
expect(modelInfo.info).toBeDefined()
|
|
expect(modelInfo.info).toBe(rooModels[rooDefaultModelId])
|
|
})
|
|
|
|
it("should handle unknown model ID with fallback info", () => {
|
|
const handlerWithUnknownModel = new RooHandler({
|
|
apiModelId: "unknown-model-id",
|
|
})
|
|
const modelInfo = handlerWithUnknownModel.getModel()
|
|
expect(modelInfo.id).toBe("unknown-model-id")
|
|
expect(modelInfo.info).toBeDefined()
|
|
// Should return fallback info for unknown models
|
|
expect(modelInfo.info.maxTokens).toBe(8192)
|
|
expect(modelInfo.info.contextWindow).toBe(262_144)
|
|
expect(modelInfo.info.supportsImages).toBe(false)
|
|
expect(modelInfo.info.supportsPromptCache).toBe(true)
|
|
expect(modelInfo.info.inputPrice).toBe(0)
|
|
expect(modelInfo.info.outputPrice).toBe(0)
|
|
})
|
|
|
|
it("should return correct model info for all Roo models", () => {
|
|
// Test each model in rooModels
|
|
const modelIds = Object.keys(rooModels) as Array<keyof typeof rooModels>
|
|
|
|
for (const modelId of modelIds) {
|
|
const handlerWithModel = new RooHandler({ apiModelId: modelId })
|
|
const modelInfo = handlerWithModel.getModel()
|
|
expect(modelInfo.id).toBe(modelId)
|
|
expect(modelInfo.info).toBe(rooModels[modelId])
|
|
}
|
|
})
|
|
})
|
|
|
|
describe("temperature and model configuration", () => {
|
|
it("should use default temperature of 0.7", async () => {
|
|
handler = new RooHandler(mockOptions)
|
|
const stream = handler.createMessage(systemPrompt, messages)
|
|
for await (const _chunk of stream) {
|
|
// Consume stream
|
|
}
|
|
|
|
expect(mockCreate).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
temperature: 0.7,
|
|
}),
|
|
)
|
|
})
|
|
|
|
it("should respect custom temperature setting", async () => {
|
|
handler = new RooHandler({
|
|
...mockOptions,
|
|
modelTemperature: 0.9,
|
|
})
|
|
const stream = handler.createMessage(systemPrompt, messages)
|
|
for await (const _chunk of stream) {
|
|
// Consume stream
|
|
}
|
|
|
|
expect(mockCreate).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
temperature: 0.9,
|
|
}),
|
|
)
|
|
})
|
|
|
|
it("should use correct API endpoint", () => {
|
|
// The base URL should be set to Roo's API endpoint
|
|
// We can't directly test the OpenAI client configuration, but we can verify the handler initializes
|
|
handler = new RooHandler(mockOptions)
|
|
expect(handler).toBeInstanceOf(RooHandler)
|
|
// The handler should work with the Roo API endpoint
|
|
})
|
|
})
|
|
|
|
describe("authentication flow", () => {
|
|
it("should use session token as API key", () => {
|
|
const testToken = "test-session-token-123"
|
|
mockGetSessionTokenFn.mockReturnValue(testToken)
|
|
|
|
handler = new RooHandler(mockOptions)
|
|
expect(handler).toBeInstanceOf(RooHandler)
|
|
expect(mockGetSessionTokenFn).toHaveBeenCalled()
|
|
})
|
|
|
|
it("should handle undefined auth service", () => {
|
|
mockHasInstanceFn.mockReturnValue(true)
|
|
// Mock CloudService with undefined authService
|
|
const originalGetter = Object.getOwnPropertyDescriptor(CloudService, "instance")?.get
|
|
|
|
try {
|
|
Object.defineProperty(CloudService, "instance", {
|
|
get: () => ({ authService: undefined }),
|
|
configurable: true,
|
|
})
|
|
|
|
expect(() => {
|
|
new RooHandler(mockOptions)
|
|
}).toThrow("Authentication required for Roo Code Cloud")
|
|
} finally {
|
|
// Always restore original getter, even if test fails
|
|
if (originalGetter) {
|
|
Object.defineProperty(CloudService, "instance", {
|
|
get: originalGetter,
|
|
configurable: true,
|
|
})
|
|
}
|
|
}
|
|
})
|
|
|
|
it("should handle empty session token", () => {
|
|
mockGetSessionTokenFn.mockReturnValue("")
|
|
|
|
expect(() => {
|
|
new RooHandler(mockOptions)
|
|
}).toThrow("Authentication required for Roo Code Cloud")
|
|
})
|
|
})
|
|
})
|