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
171 changes: 171 additions & 0 deletions src/services/code-index/__tests__/service-factory.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,125 @@ describe("CodeIndexServiceFactory", () => {
mockGetDefaultModelId.mockReturnValue("default-model")
})

it("should prioritize detectedDimension over all other dimension sources", () => {
// Arrange
const testConfig = {
embedderProvider: "openai-compatible",
modelId: "custom-model",
modelDimension: 1024, // Manual config should be ignored
qdrantUrl: "http://localhost:6333",
qdrantApiKey: "test-key",
}
mockConfigManager.getConfig.mockReturnValue(testConfig as any)
mockGetModelDimension.mockReturnValue(768) // Profile dimension should be ignored

// Act - pass detected dimension from validation
factory.createVectorStore(4096)

// Assert - should use detected dimension (4096), not profile (768) or manual (1024)
expect(MockedQdrantVectorStore).toHaveBeenCalledWith(
"/test/workspace",
"http://localhost:6333",
4096, // Auto-detected dimension takes priority
"test-key",
)
})

it("should use detected dimension from Ollama embedder", () => {
// Arrange - simulates Ollama with qwen3-embedding returning 4096 dimensions
const testConfig = {
embedderProvider: "ollama",
modelId: "qwen3-embedding",
modelDimension: 1536, // User's incorrect manual config
qdrantUrl: "http://localhost:6333",
qdrantApiKey: "test-key",
}
mockConfigManager.getConfig.mockReturnValue(testConfig as any)
mockGetModelDimension.mockReturnValue(undefined) // Unknown model

// Act - pass detected dimension from validation (like the issue scenario)
factory.createVectorStore(4096)

// Assert - should use auto-detected 4096, not user's incorrect 1536
expect(MockedQdrantVectorStore).toHaveBeenCalledWith(
"/test/workspace",
"http://localhost:6333",
4096,
"test-key",
)
})

it("should fall back to profile dimension when detected dimension is not provided", () => {
// Arrange
const testConfig = {
embedderProvider: "openai",
modelId: "text-embedding-3-large",
qdrantUrl: "http://localhost:6333",
qdrantApiKey: "test-key",
}
mockConfigManager.getConfig.mockReturnValue(testConfig as any)
mockGetModelDimension.mockReturnValue(3072)

// Act - no detected dimension provided
factory.createVectorStore()

// Assert - should use profile dimension
expect(mockGetModelDimension).toHaveBeenCalledWith("openai", "text-embedding-3-large")
expect(MockedQdrantVectorStore).toHaveBeenCalledWith(
"/test/workspace",
"http://localhost:6333",
3072,
"test-key",
)
})

it("should fall back to manual dimension when detected and profile are unavailable", () => {
// Arrange
const testConfig = {
embedderProvider: "openai-compatible",
modelId: "unknown-model",
modelDimension: 2048,
qdrantUrl: "http://localhost:6333",
qdrantApiKey: "test-key",
}
mockConfigManager.getConfig.mockReturnValue(testConfig as any)
mockGetModelDimension.mockReturnValue(undefined)

// Act - no detected dimension, no profile dimension
factory.createVectorStore()

// Assert - should use manual dimension
expect(MockedQdrantVectorStore).toHaveBeenCalledWith(
"/test/workspace",
"http://localhost:6333",
2048,
"test-key",
)
})

it("should ignore zero or negative detected dimension", () => {
// Arrange
const testConfig = {
embedderProvider: "openai",
modelId: "text-embedding-3-small",
qdrantUrl: "http://localhost:6333",
qdrantApiKey: "test-key",
}
mockConfigManager.getConfig.mockReturnValue(testConfig as any)
mockGetModelDimension.mockReturnValue(1536)

// Act - pass invalid detected dimension
factory.createVectorStore(0)

// Assert - should fall back to profile dimension
expect(MockedQdrantVectorStore).toHaveBeenCalledWith(
"/test/workspace",
"http://localhost:6333",
1536,
"test-key",
)
})

it("should use config.modelId for OpenAI provider", () => {
// Arrange
const testModelId = "text-embedding-3-large"
Expand Down Expand Up @@ -670,6 +789,58 @@ describe("CodeIndexServiceFactory", () => {
}
})

it("should return detectedDimension from embedder validation", async () => {
// Arrange
const testConfig = {
embedderProvider: "ollama",
modelId: "qwen3-embedding",
ollamaOptions: {
ollamaBaseUrl: "http://localhost:11434",
},
}
mockConfigManager.getConfig.mockReturnValue(testConfig as any)
MockedCodeIndexOllamaEmbedder.mockImplementation(() => mockEmbedderInstance)
// Mock embedder returning detected dimension
mockEmbedderInstance.validateConfiguration.mockResolvedValue({
valid: true,
detectedDimension: 4096,
})

// Act
const embedder = factory.createEmbedder()
const result = await factory.validateEmbedder(embedder)

// Assert
expect(result).toEqual({ valid: true, detectedDimension: 4096 })
expect(mockEmbedderInstance.validateConfiguration).toHaveBeenCalled()
})

it("should return detectedDimension from base64 embedding validation", async () => {
// Arrange
const testConfig = {
embedderProvider: "openai-compatible",
modelId: "custom-model",
openAiCompatibleOptions: {
baseUrl: "https://api.example.com/v1",
apiKey: "test-api-key",
},
}
mockConfigManager.getConfig.mockReturnValue(testConfig as any)
MockedOpenAICompatibleEmbedder.mockImplementation(() => mockEmbedderInstance)
// Mock embedder returning detected dimension from base64 parsing
mockEmbedderInstance.validateConfiguration.mockResolvedValue({
valid: true,
detectedDimension: 1536,
})

// Act
const embedder = factory.createEmbedder()
const result = await factory.validateEmbedder(embedder)

// Assert
expect(result).toEqual({ valid: true, detectedDimension: 1536 })
})

it("should validate OpenAI embedder successfully", async () => {
// Arrange
const testConfig = {
Expand Down
33 changes: 33 additions & 0 deletions src/services/code-index/embedders/__tests__/ollama.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ describe("CodeIndexOllamaEmbedder", () => {

expect(result.valid).toBe(true)
expect(result.error).toBeUndefined()
expect(result.detectedDimension).toBe(3) // Auto-detected from test embedding
expect(mockFetch).toHaveBeenCalledTimes(2)

// Check first call (GET /api/tags)
Expand All @@ -214,6 +215,38 @@ describe("CodeIndexOllamaEmbedder", () => {
expect(secondCall[1]?.signal).toBeDefined() // AbortSignal for timeout
})

it("should detect dimension from realistic embedding size", async () => {
// Mock successful /api/tags call
mockFetch.mockImplementationOnce(() =>
Promise.resolve({
ok: true,
status: 200,
json: () =>
Promise.resolve({
models: [{ name: "nomic-embed-text:latest" }],
}),
} as Response),
)

// Mock successful /api/embed test call with 4096-dimension embedding (like qwen3-embedding)
const largeEmbedding = new Array(4096).fill(0).map((_, i) => i * 0.001)
mockFetch.mockImplementationOnce(() =>
Promise.resolve({
ok: true,
status: 200,
json: () =>
Promise.resolve({
embeddings: [largeEmbedding],
}),
} as Response),
)

const result = await embedder.validateConfiguration()

expect(result.valid).toBe(true)
expect(result.detectedDimension).toBe(4096)
})

it("should fail validation when service is not available", async () => {
mockFetch.mockRejectedValueOnce(new Error("ECONNREFUSED"))

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -978,6 +978,7 @@ describe("OpenAICompatibleEmbedder", () => {

expect(result.valid).toBe(true)
expect(result.error).toBeUndefined()
expect(result.detectedDimension).toBe(3) // Auto-detected from array embedding
expect(mockEmbeddingsCreate).toHaveBeenCalledWith({
input: ["test"],
model: testModelId,
Expand All @@ -1003,6 +1004,7 @@ describe("OpenAICompatibleEmbedder", () => {

expect(result.valid).toBe(true)
expect(result.error).toBeUndefined()
expect(result.detectedDimension).toBe(3) // Auto-detected from array embedding
expect(mockFetch).toHaveBeenCalledWith(
fullUrl,
expect.objectContaining({
Expand All @@ -1014,6 +1016,25 @@ describe("OpenAICompatibleEmbedder", () => {
)
})

it("should detect dimension from base64 encoded embedding", async () => {
embedder = new OpenAICompatibleEmbedder(testBaseUrl, testApiKey, testModelId)

// Create a 1536-dimension embedding as base64 (like text-embedding-3-small)
const embedding = new Float32Array(1536).fill(0.1)
const base64String = Buffer.from(embedding.buffer).toString("base64")

const mockResponse = {
data: [{ embedding: base64String }],
usage: { prompt_tokens: 2, total_tokens: 2 },
}
mockEmbeddingsCreate.mockResolvedValue(mockResponse)

const result = await embedder.validateConfiguration()

expect(result.valid).toBe(true)
expect(result.detectedDimension).toBe(1536) // Auto-detected from base64 (1536 * 4 bytes / 4 = 1536)
})

it("should fail validation with authentication error", async () => {
embedder = new OpenAICompatibleEmbedder(testBaseUrl, testApiKey, testModelId)

Expand Down
12 changes: 8 additions & 4 deletions src/services/code-index/embedders/bedrock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -263,10 +263,11 @@ export class BedrockEmbedder implements IEmbedder {
}

/**
* Validates the Bedrock embedder configuration by attempting a minimal embedding request
* @returns Promise resolving to validation result with success status and optional error message
* Validates the Bedrock embedder configuration by attempting a minimal embedding request.
* Also detects the actual embedding dimension from a test embedding.
* @returns Promise resolving to validation result with success status, optional error message, and detected dimension
*/
async validateConfiguration(): Promise<{ valid: boolean; error?: string }> {
async validateConfiguration(): Promise<{ valid: boolean; error?: string; detectedDimension?: number }> {
return withValidationErrorHandling(async () => {
try {
// Test with a minimal embedding request
Expand All @@ -280,7 +281,10 @@ export class BedrockEmbedder implements IEmbedder {
}
}

return { valid: true }
// Get the dimension from the embedding
const detectedDimension = result.embedding.length

return { valid: true, detectedDimension }
} catch (error: any) {
// Check for specific AWS errors
if (error.name === "UnrecognizedClientException") {
Expand Down
7 changes: 4 additions & 3 deletions src/services/code-index/embedders/gemini.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,11 @@ export class GeminiEmbedder implements IEmbedder {
}

/**
* Validates the Gemini embedder configuration by delegating to the underlying OpenAI-compatible embedder
* @returns Promise resolving to validation result with success status and optional error message
* Validates the Gemini embedder configuration by delegating to the underlying OpenAI-compatible embedder.
* Also detects the actual embedding dimension from a test embedding.
* @returns Promise resolving to validation result with success status, optional error message, and detected dimension
*/
async validateConfiguration(): Promise<{ valid: boolean; error?: string }> {
async validateConfiguration(): Promise<{ valid: boolean; error?: string; detectedDimension?: number }> {
try {
// Delegate validation to the OpenAI-compatible embedder
// The error messages will be specific to Gemini since we're using Gemini's base URL
Expand Down
7 changes: 4 additions & 3 deletions src/services/code-index/embedders/mistral.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,11 @@ export class MistralEmbedder implements IEmbedder {
}

/**
* Validates the Mistral embedder configuration by delegating to the underlying OpenAI-compatible embedder
* @returns Promise resolving to validation result with success status and optional error message
* Validates the Mistral embedder configuration by delegating to the underlying OpenAI-compatible embedder.
* Also detects the actual embedding dimension from a test embedding.
* @returns Promise resolving to validation result with success status, optional error message, and detected dimension
*/
async validateConfiguration(): Promise<{ valid: boolean; error?: string }> {
async validateConfiguration(): Promise<{ valid: boolean; error?: string; detectedDimension?: number }> {
try {
// Delegate validation to the OpenAI-compatible embedder
// The error messages will be specific to Mistral since we're using Mistral's base URL
Expand Down
21 changes: 17 additions & 4 deletions src/services/code-index/embedders/ollama.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,10 +138,11 @@ export class CodeIndexOllamaEmbedder implements IEmbedder {
}

/**
* Validates the Ollama embedder configuration by checking service availability and model existence
* @returns Promise resolving to validation result with success status and optional error message
* Validates the Ollama embedder configuration by checking service availability and model existence.
* Also detects the actual embedding dimension from a test embedding.
* @returns Promise resolving to validation result with success status, optional error message, and detected dimension
*/
async validateConfiguration(): Promise<{ valid: boolean; error?: string }> {
async validateConfiguration(): Promise<{ valid: boolean; error?: string; detectedDimension?: number }> {
return withValidationErrorHandling(
async () => {
// First check if Ollama service is running by trying to list models
Expand Down Expand Up @@ -228,7 +229,19 @@ export class CodeIndexOllamaEmbedder implements IEmbedder {
}
}

return { valid: true }
// Parse the test response to get the embedding dimension
const testData = await testResponse.json()
const embeddings = testData.embeddings
let detectedDimension: number | undefined

if (embeddings && Array.isArray(embeddings) && embeddings.length > 0) {
const firstEmbedding = embeddings[0]
if (Array.isArray(firstEmbedding)) {
detectedDimension = firstEmbedding.length
}
}

return { valid: true, detectedDimension }
},
"ollama",
{
Expand Down
Loading
Loading