Skip to content

Commit bdd8356

Browse files
test(code-index,tools): cover remaining lines left uncovered by #1297
The lint identifier PR #1297 merged to main with its codecov patch report at 81.33% (14 lines missing in src/services/code-index/config-manager.ts, src/services/code-index/service-factory.ts, src/core/tools/GenerateImageTool.ts); this PR adds the focused unit tests covering those lines and branch sides. No source logic changes.
1 parent 871bb98 commit bdd8356

3 files changed

Lines changed: 324 additions & 0 deletions

File tree

‎src/core/tools/__tests__/generateImageTool.test.ts‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import * as fs from "fs/promises"
66
import * as pathUtils from "../../../utils/pathUtils"
77
import * as fileUtils from "../../../utils/fs"
88
import { formatResponse } from "../../prompts/responses"
9+
import { t } from "../../../i18n"
910
import { EXPERIMENT_IDS } from "../../../shared/experiments"
1011
import { OpenRouterHandler } from "../../../api/providers/openrouter"
1112

@@ -369,4 +370,43 @@ describe("generateImageTool", () => {
369370
expect(mockPushToolResult).toHaveBeenCalledWith(expect.stringContaining("Unsupported image format"))
370371
})
371372
})
373+
374+
describe("openrouter api key validation", () => {
375+
it("should error when the OpenRouter API key is not configured", async () => {
376+
// An OpenRouter image model is selected but no API key is configured
377+
mockCline.providerRef.deref().getState.mockResolvedValue({
378+
experiments: {
379+
[EXPERIMENT_IDS.IMAGE_GENERATION]: true,
380+
},
381+
openRouterImageGenerationSelectedModel: "google/gemini-2.5-flash-image",
382+
})
383+
384+
const block: ToolUse = {
385+
type: "tool_use",
386+
name: "generate_image",
387+
params: {
388+
prompt: "Generate a test image",
389+
path: "test-image.png",
390+
},
391+
nativeArgs: {
392+
prompt: "Generate a test image",
393+
path: "test-image.png",
394+
},
395+
partial: false,
396+
}
397+
398+
await generateImageTool.handle(mockCline as Task, block as ToolUse<"generate_image">, {
399+
askApproval: mockAskApproval,
400+
handleError: mockHandleError,
401+
pushToolResult: mockPushToolResult,
402+
})
403+
404+
const expectedMessage = t("tools:generateImage.openRouterApiKeyRequired")
405+
expect(mockCline.say).toHaveBeenCalledWith("error", expectedMessage)
406+
expect(mockPushToolResult).toHaveBeenCalledWith(formatResponse.toolError(expectedMessage))
407+
// The tool must stop before asking for approval or generating anything
408+
expect(mockAskApproval).not.toHaveBeenCalled()
409+
expect(OpenRouterHandler).not.toHaveBeenCalled()
410+
})
411+
})
372412
})

‎src/services/code-index/__tests__/config-manager.spec.ts‎

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2035,4 +2035,132 @@ describe("CodeIndexConfigManager", () => {
20352035
})
20362036
})
20372037
})
2038+
2039+
describe("mistral, vercel-ai-gateway and bedrock provider configuration", () => {
2040+
it("should load Mistral provider configuration and mark it configured", async () => {
2041+
mockContextProxy.getGlobalState.mockReturnValue({
2042+
codebaseIndexEnabled: true,
2043+
codebaseIndexQdrantUrl: "http://qdrant.local",
2044+
codebaseIndexEmbedderProvider: providerIdentifiers.mistral,
2045+
})
2046+
mockContextProxy.getSecret.mockImplementation((key: string) => {
2047+
if (key === "codebaseIndexMistralApiKey") return "test-mistral-key"
2048+
return undefined
2049+
})
2050+
2051+
const result = await configManager.loadConfiguration()
2052+
2053+
expect(result.currentConfig.embedderProvider).toBe(providerIdentifiers.mistral)
2054+
expect(result.currentConfig.mistralOptions).toEqual({ apiKey: "test-mistral-key" })
2055+
expect(result.currentConfig.isConfigured).toBe(true)
2056+
expect(configManager.currentEmbedderProvider).toBe("mistral")
2057+
})
2058+
2059+
it("should return false from isConfigured for Mistral when the API key is missing", () => {
2060+
mockContextProxy.getGlobalState.mockReturnValue({
2061+
codebaseIndexEnabled: true,
2062+
codebaseIndexQdrantUrl: "http://qdrant.local",
2063+
codebaseIndexEmbedderProvider: providerIdentifiers.mistral,
2064+
})
2065+
mockContextProxy.getSecret.mockReturnValue(undefined)
2066+
2067+
configManager = new CodeIndexConfigManager(mockContextProxy)
2068+
expect(configManager.isConfigured()).toBe(false)
2069+
})
2070+
2071+
it("should load Vercel AI Gateway provider configuration and mark it configured", async () => {
2072+
mockContextProxy.getGlobalState.mockReturnValue({
2073+
codebaseIndexEnabled: true,
2074+
codebaseIndexQdrantUrl: "http://qdrant.local",
2075+
codebaseIndexEmbedderProvider: providerIdentifiers.vercelAiGateway,
2076+
})
2077+
mockContextProxy.getSecret.mockImplementation((key: string) => {
2078+
if (key === "codebaseIndexVercelAiGatewayApiKey") return "test-vercel-key"
2079+
return undefined
2080+
})
2081+
2082+
const result = await configManager.loadConfiguration()
2083+
2084+
expect(result.currentConfig.embedderProvider).toBe(providerIdentifiers.vercelAiGateway)
2085+
expect(result.currentConfig.vercelAiGatewayOptions).toEqual({ apiKey: "test-vercel-key" })
2086+
expect(result.currentConfig.isConfigured).toBe(true)
2087+
expect(configManager.currentEmbedderProvider).toBe("vercel-ai-gateway")
2088+
})
2089+
2090+
it("should return false from isConfigured for Vercel AI Gateway when the API key is missing", () => {
2091+
mockContextProxy.getGlobalState.mockReturnValue({
2092+
codebaseIndexEnabled: true,
2093+
codebaseIndexQdrantUrl: "http://qdrant.local",
2094+
codebaseIndexEmbedderProvider: providerIdentifiers.vercelAiGateway,
2095+
})
2096+
mockContextProxy.getSecret.mockReturnValue(undefined)
2097+
2098+
configManager = new CodeIndexConfigManager(mockContextProxy)
2099+
expect(configManager.isConfigured()).toBe(false)
2100+
})
2101+
2102+
it("should load Bedrock provider configuration with region and profile and mark it configured", async () => {
2103+
mockContextProxy.getGlobalState.mockReturnValue({
2104+
codebaseIndexEnabled: true,
2105+
codebaseIndexQdrantUrl: "http://qdrant.local",
2106+
codebaseIndexEmbedderProvider: providerIdentifiers.bedrock,
2107+
codebaseIndexBedrockRegion: "eu-west-1",
2108+
codebaseIndexBedrockProfile: "test-profile",
2109+
})
2110+
mockContextProxy.getSecret.mockReturnValue(undefined)
2111+
2112+
const result = await configManager.loadConfiguration()
2113+
2114+
expect(result.currentConfig.embedderProvider).toBe(providerIdentifiers.bedrock)
2115+
expect(result.currentConfig.bedrockOptions).toEqual({ region: "eu-west-1", profile: "test-profile" })
2116+
expect(result.currentConfig.isConfigured).toBe(true)
2117+
expect(configManager.currentEmbedderProvider).toBe("bedrock")
2118+
})
2119+
2120+
it("should default Bedrock region to us-east-1 when no region is configured", async () => {
2121+
mockContextProxy.getGlobalState.mockReturnValue({
2122+
codebaseIndexEnabled: true,
2123+
codebaseIndexQdrantUrl: "http://qdrant.local",
2124+
codebaseIndexEmbedderProvider: providerIdentifiers.bedrock,
2125+
})
2126+
mockContextProxy.getSecret.mockReturnValue(undefined)
2127+
2128+
const result = await configManager.loadConfiguration()
2129+
2130+
expect(result.currentConfig.bedrockOptions).toEqual({ region: "us-east-1", profile: undefined })
2131+
expect(result.currentConfig.isConfigured).toBe(true)
2132+
})
2133+
2134+
it("should return false from isConfigured for Bedrock when the Qdrant URL is missing", () => {
2135+
mockContextProxy.getGlobalState.mockReturnValue({
2136+
codebaseIndexEnabled: true,
2137+
codebaseIndexEmbedderProvider: providerIdentifiers.bedrock,
2138+
codebaseIndexBedrockRegion: "us-east-1",
2139+
})
2140+
mockContextProxy.getSecret.mockReturnValue(undefined)
2141+
2142+
configManager = new CodeIndexConfigManager(mockContextProxy)
2143+
expect(configManager.isConfigured()).toBe(false)
2144+
})
2145+
})
2146+
2147+
describe("isConfigured defensive fallback", () => {
2148+
it("should return false when the provider is not a recognized embedder provider", () => {
2149+
// The isConfigured() switch lists every EmbedderProvider member explicitly and
2150+
// ends in a defensive `return false` ("Should not happen if embedderProvider is
2151+
// always set correctly"). That fallback is unreachable through the public API
2152+
// because EmbedderProvider is a closed union, so exercise it by forcing the
2153+
// private field to a value outside the union.
2154+
mockContextProxy.getGlobalState.mockReturnValue({
2155+
codebaseIndexEnabled: true,
2156+
})
2157+
mockContextProxy.getSecret.mockReturnValue(undefined)
2158+
2159+
configManager = new CodeIndexConfigManager(mockContextProxy)
2160+
// The private `embedderProvider` field has no public setter; the cast is the
2161+
// only way to reach the defensive branch without changing source logic.
2162+
Object.defineProperty(configManager, "embedderProvider", { value: "not-a-provider" })
2163+
expect(configManager.isConfigured()).toBe(false)
2164+
})
2165+
})
20382166
})

‎src/services/code-index/__tests__/service-factory.spec.ts‎

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ import { CodeIndexOllamaEmbedder } from "../embedders/ollama"
55
import { OpenAICompatibleEmbedder } from "../embedders/openai-compatible"
66
import { GeminiEmbedder } from "../embedders/gemini"
77
import { QdrantVectorStore } from "../vector-store/qdrant-client"
8+
import { MistralEmbedder } from "../embedders/mistral"
9+
import { VercelAiGatewayEmbedder } from "../embedders/vercel-ai-gateway"
10+
import { BedrockEmbedder } from "../embedders/bedrock"
11+
import { OpenRouterEmbedder } from "../embedders/openrouter"
812

913
import { clearAllMocks } from "../../../test-utils/reset"
1014

@@ -14,6 +18,10 @@ vitest.mock("../embedders/ollama")
1418
vitest.mock("../embedders/openai-compatible")
1519
vitest.mock("../embedders/gemini")
1620
vitest.mock("../vector-store/qdrant-client")
21+
vitest.mock("../embedders/mistral")
22+
vitest.mock("../embedders/vercel-ai-gateway")
23+
vitest.mock("../embedders/bedrock")
24+
vitest.mock("../embedders/openrouter")
1725

1826
// Mock the embedding models module
1927
vitest.mock("../../../shared/embeddingModels", () => ({
@@ -35,6 +43,10 @@ const MockedCodeIndexOllamaEmbedder = CodeIndexOllamaEmbedder as MockedClass<typ
3543
const MockedOpenAICompatibleEmbedder = OpenAICompatibleEmbedder as MockedClass<typeof OpenAICompatibleEmbedder>
3644
const MockedGeminiEmbedder = GeminiEmbedder as MockedClass<typeof GeminiEmbedder>
3745
const MockedQdrantVectorStore = QdrantVectorStore as MockedClass<typeof QdrantVectorStore>
46+
const MockedMistralEmbedder = MistralEmbedder as MockedClass<typeof MistralEmbedder>
47+
const MockedVercelAiGatewayEmbedder = VercelAiGatewayEmbedder as MockedClass<typeof VercelAiGatewayEmbedder>
48+
const MockedBedrockEmbedder = BedrockEmbedder as MockedClass<typeof BedrockEmbedder>
49+
const MockedOpenRouterEmbedder = OpenRouterEmbedder as MockedClass<typeof OpenRouterEmbedder>
3850

3951
// Import the mocked functions
4052
import { getDefaultModelId, getModelDimension } from "../../../shared/embeddingModels"
@@ -348,6 +360,150 @@ describe("CodeIndexServiceFactory", () => {
348360
expect(() => factory.createEmbedder()).toThrow("serviceFactory.geminiConfigMissing")
349361
})
350362

363+
it("should pass model ID to Mistral embedder when using Mistral provider", () => {
364+
// Arrange
365+
const testModelId = "mistral-embed"
366+
const testConfig = {
367+
embedderProvider: providerIdentifiers.mistral,
368+
modelId: testModelId,
369+
mistralOptions: {
370+
apiKey: "test-mistral-key",
371+
},
372+
}
373+
mockConfigManager.getConfig.mockReturnValue(testConfig)
374+
375+
// Act
376+
factory.createEmbedder()
377+
378+
// Assert
379+
expect(MockedMistralEmbedder).toHaveBeenCalledWith("test-mistral-key", testModelId)
380+
})
381+
382+
it("should throw error when Mistral API key is missing", () => {
383+
// Arrange
384+
const testConfig = {
385+
embedderProvider: providerIdentifiers.mistral,
386+
modelId: "mistral-embed",
387+
mistralOptions: {
388+
apiKey: undefined,
389+
},
390+
}
391+
mockConfigManager.getConfig.mockReturnValue(testConfig)
392+
393+
// Act & Assert
394+
expect(() => factory.createEmbedder()).toThrow("serviceFactory.mistralConfigMissing")
395+
})
396+
397+
it("should pass model ID to Vercel AI Gateway embedder when using Vercel AI Gateway provider", () => {
398+
// Arrange
399+
const testModelId = "vercel-embed-model"
400+
const testConfig = {
401+
embedderProvider: providerIdentifiers.vercelAiGateway,
402+
modelId: testModelId,
403+
vercelAiGatewayOptions: {
404+
apiKey: "test-vercel-key",
405+
},
406+
}
407+
mockConfigManager.getConfig.mockReturnValue(testConfig)
408+
409+
// Act
410+
factory.createEmbedder()
411+
412+
// Assert
413+
expect(MockedVercelAiGatewayEmbedder).toHaveBeenCalledWith("test-vercel-key", testModelId)
414+
})
415+
416+
it("should throw error when Vercel AI Gateway API key is missing", () => {
417+
// Arrange
418+
const testConfig = {
419+
embedderProvider: providerIdentifiers.vercelAiGateway,
420+
modelId: "vercel-embed-model",
421+
vercelAiGatewayOptions: {
422+
apiKey: undefined,
423+
},
424+
}
425+
mockConfigManager.getConfig.mockReturnValue(testConfig)
426+
427+
// Act & Assert
428+
expect(() => factory.createEmbedder()).toThrow("serviceFactory.vercelAiGatewayConfigMissing")
429+
})
430+
431+
it("should pass region, profile and model ID to Bedrock embedder when using Bedrock provider", () => {
432+
// Arrange
433+
const testModelId = "amazon.titan-embed-text-v1"
434+
const testConfig = {
435+
embedderProvider: providerIdentifiers.bedrock,
436+
modelId: testModelId,
437+
bedrockOptions: {
438+
region: "eu-west-1",
439+
profile: "test-profile",
440+
},
441+
}
442+
mockConfigManager.getConfig.mockReturnValue(testConfig)
443+
444+
// Act
445+
factory.createEmbedder()
446+
447+
// Assert
448+
expect(MockedBedrockEmbedder).toHaveBeenCalledWith("eu-west-1", "test-profile", testModelId)
449+
})
450+
451+
it("should throw error when Bedrock region is missing", () => {
452+
// Arrange
453+
const testConfig = {
454+
embedderProvider: providerIdentifiers.bedrock,
455+
modelId: "amazon.titan-embed-text-v1",
456+
bedrockOptions: {
457+
region: undefined,
458+
profile: "test-profile",
459+
},
460+
}
461+
mockConfigManager.getConfig.mockReturnValue(testConfig)
462+
463+
// Act & Assert
464+
expect(() => factory.createEmbedder()).toThrow("serviceFactory.bedrockConfigMissing")
465+
})
466+
467+
it("should pass API key, model ID and specific provider to OpenRouter embedder", () => {
468+
// Arrange
469+
const testModelId = "openai/text-embedding-3-large"
470+
const testConfig = {
471+
embedderProvider: providerIdentifiers.openrouter,
472+
modelId: testModelId,
473+
openRouterOptions: {
474+
apiKey: "test-openrouter-key",
475+
specificProvider: providerIdentifiers.openai,
476+
},
477+
}
478+
mockConfigManager.getConfig.mockReturnValue(testConfig)
479+
480+
// Act
481+
factory.createEmbedder()
482+
483+
// Assert
484+
expect(MockedOpenRouterEmbedder).toHaveBeenCalledWith(
485+
"test-openrouter-key",
486+
testModelId,
487+
undefined,
488+
"openai",
489+
)
490+
})
491+
492+
it("should throw error when OpenRouter API key is missing", () => {
493+
// Arrange
494+
const testConfig = {
495+
embedderProvider: providerIdentifiers.openrouter,
496+
modelId: "openai/text-embedding-3-large",
497+
openRouterOptions: {
498+
apiKey: undefined,
499+
},
500+
}
501+
mockConfigManager.getConfig.mockReturnValue(testConfig)
502+
503+
// Act & Assert
504+
expect(() => factory.createEmbedder()).toThrow("serviceFactory.openRouterConfigMissing")
505+
})
506+
351507
it("should throw error for invalid embedder provider", () => {
352508
// Arrange
353509
const testConfig = {

0 commit comments

Comments
 (0)