Files
stack/apps/api/src/knowledge/services/embedding.service.spec.ts
Jason Woltje 6c88e2b96d fix(#338): Don't instantiate OpenAI client with missing API key
- Skip client initialization when OPENAI_API_KEY not configured
- Set openai property to null instead of creating with dummy key
- Methods return gracefully when embeddings not available
- Updated tests to verify client is not instantiated without key

Refs #338

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-05 16:21:17 -06:00

165 lines
4.4 KiB
TypeScript

import { describe, it, expect, beforeEach, vi, afterEach } from "vitest";
import { EmbeddingService } from "./embedding.service";
import { PrismaService } from "../../prisma/prisma.service";
// Mock OpenAI with a proper class
const mockEmbeddingsCreate = vi.fn();
vi.mock("openai", () => {
return {
default: class MockOpenAI {
embeddings = {
create: mockEmbeddingsCreate,
};
},
};
});
describe("EmbeddingService", () => {
let service: EmbeddingService;
let prismaService: PrismaService;
let originalEnv: string | undefined;
beforeEach(() => {
// Store original env
originalEnv = process.env.OPENAI_API_KEY;
prismaService = {
$executeRaw: vi.fn(),
knowledgeEmbedding: {
deleteMany: vi.fn(),
},
} as unknown as PrismaService;
// Clear mock call history
vi.clearAllMocks();
});
afterEach(() => {
// Restore original env
if (originalEnv) {
process.env.OPENAI_API_KEY = originalEnv;
} else {
delete process.env.OPENAI_API_KEY;
}
});
describe("constructor", () => {
it("should not instantiate OpenAI client when API key is missing", () => {
delete process.env.OPENAI_API_KEY;
service = new EmbeddingService(prismaService);
// Verify service is not configured (client is null)
expect(service.isConfigured()).toBe(false);
});
it("should instantiate OpenAI client when API key is provided", () => {
process.env.OPENAI_API_KEY = "test-api-key";
service = new EmbeddingService(prismaService);
// Verify service is configured (client is not null)
expect(service.isConfigured()).toBe(true);
});
});
// Default service setup (without API key) for remaining tests
function createServiceWithoutKey(): EmbeddingService {
delete process.env.OPENAI_API_KEY;
return new EmbeddingService(prismaService);
}
describe("isConfigured", () => {
it("should return false when OPENAI_API_KEY is not set", () => {
service = createServiceWithoutKey();
expect(service.isConfigured()).toBe(false);
});
it("should return true when OPENAI_API_KEY is set", () => {
process.env.OPENAI_API_KEY = "test-key";
service = new EmbeddingService(prismaService);
expect(service.isConfigured()).toBe(true);
});
});
describe("prepareContentForEmbedding", () => {
beforeEach(() => {
service = createServiceWithoutKey();
});
it("should combine title and content with title weighting", () => {
const title = "Test Title";
const content = "Test content goes here";
const result = service.prepareContentForEmbedding(title, content);
expect(result).toContain(title);
expect(result).toContain(content);
// Title should appear twice for weighting
expect(result.split(title).length - 1).toBe(2);
});
it("should handle empty content", () => {
const title = "Test Title";
const content = "";
const result = service.prepareContentForEmbedding(title, content);
expect(result).toBe(`${title}\n\n${title}`);
});
});
describe("generateAndStoreEmbedding", () => {
it("should skip generation when not configured", async () => {
service = createServiceWithoutKey();
await service.generateAndStoreEmbedding("test-id", "test content");
expect(prismaService.$executeRaw).not.toHaveBeenCalled();
});
});
describe("deleteEmbedding", () => {
beforeEach(() => {
service = createServiceWithoutKey();
});
it("should delete embedding for entry", async () => {
const entryId = "test-entry-id";
await service.deleteEmbedding(entryId);
expect(prismaService.knowledgeEmbedding.deleteMany).toHaveBeenCalledWith({
where: { entryId },
});
});
});
describe("batchGenerateEmbeddings", () => {
it("should return 0 when not configured", async () => {
service = createServiceWithoutKey();
const entries = [
{ id: "1", content: "content 1" },
{ id: "2", content: "content 2" },
];
const result = await service.batchGenerateEmbeddings(entries);
expect(result).toBe(0);
});
});
describe("generateEmbedding", () => {
it("should throw error when not configured", async () => {
service = createServiceWithoutKey();
await expect(service.generateEmbedding("test text")).rejects.toThrow(
"OPENAI_API_KEY not configured"
);
});
});
});