From 0998b4f7b97c5b69b5da0efab83b9ba065c4bd0d Mon Sep 17 00:00:00 2001 From: EarlGrey <92405518+earlgreylabs@users.noreply.github.com> Date: Wed, 27 May 2026 10:29:26 -0400 Subject: [PATCH] chore(deps): remove @langchain/openai dependency and simplify test mocks --- package-lock.json | 84 ---------------------------- package.json | 1 - src/docs/store/DocumentStore.test.ts | 20 ++----- src/utils/embeddings.test.ts | 73 ++++++++++++++++++++++++ src/utils/embeddings.ts | 3 + 5 files changed, 80 insertions(+), 101 deletions(-) create mode 100644 src/utils/embeddings.test.ts diff --git a/package-lock.json b/package-lock.json index e2eaaf9a..aa59a20a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,6 @@ "license": "BSD-3-Clause", "dependencies": { "@langchain/core": "1.1.38", - "@langchain/openai": "1.4.1", "@langchain/textsplitters": "1.0.1", "@modelcontextprotocol/sdk": "1.29.0", "@xenova/transformers": "2.17.2", @@ -1013,32 +1012,6 @@ "url": "https://github.com/sponsors/colinhacks" } }, - "node_modules/@langchain/openai": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@langchain/openai/-/openai-1.4.1.tgz", - "integrity": "sha512-jaHk4TnLqWrQ1KYmavvwCImW6x8pBy6LLTK73tzSMg7HBLbq0g/l7EkpMcxZWDOvyufuCXUqO2bj47apcOhw6Q==", - "license": "MIT", - "dependencies": { - "js-tiktoken": "^1.0.12", - "openai": "^6.32.0", - "zod": "^3.25.76 || ^4" - }, - "engines": { - "node": ">=20" - }, - "peerDependencies": { - "@langchain/core": "^1.1.38" - } - }, - "node_modules/@langchain/openai/node_modules/zod": { - "version": "4.1.12", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.12.tgz", - "integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, "node_modules/@langchain/textsplitters": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@langchain/textsplitters/-/textsplitters-1.0.1.tgz", @@ -6001,27 +5974,6 @@ "platform": "^1.3.6" } }, - "node_modules/openai": { - "version": "6.33.0", - "resolved": "https://registry.npmjs.org/openai/-/openai-6.33.0.tgz", - "integrity": "sha512-xAYN1W3YsDXJWA5F277135YfkEk6H7D3D6vWwRhJ3OEkzRgcyK8z/P5P9Gyi/wB4N8kK9kM5ZjprfvyHagKmpw==", - "license": "Apache-2.0", - "bin": { - "openai": "bin/cli" - }, - "peerDependencies": { - "ws": "^8.18.0", - "zod": "^3.25 || ^4.0" - }, - "peerDependenciesMeta": { - "ws": { - "optional": true - }, - "zod": { - "optional": true - } - } - }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -8106,24 +8058,6 @@ } } }, - "node_modules/vite-node/node_modules/yaml": { - "version": "2.8.3", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", - "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", - "dev": true, - "license": "ISC", - "optional": true, - "peer": true, - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - }, - "funding": { - "url": "https://github.com/sponsors/eemeli" - } - }, "node_modules/vitest": { "version": "3.0.8", "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.0.8.tgz", @@ -8327,24 +8261,6 @@ } } }, - "node_modules/vitest/node_modules/yaml": { - "version": "2.8.3", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", - "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", - "dev": true, - "license": "ISC", - "optional": true, - "peer": true, - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - }, - "funding": { - "url": "https://github.com/sponsors/eemeli" - } - }, "node_modules/w3c-xmlserializer": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", diff --git a/package.json b/package.json index 1ca266b6..cec153c8 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,6 @@ }, "dependencies": { "@langchain/core": "1.1.38", - "@langchain/openai": "1.4.1", "@langchain/textsplitters": "1.0.1", "@modelcontextprotocol/sdk": "1.29.0", "@xenova/transformers": "2.17.2", diff --git a/src/docs/store/DocumentStore.test.ts b/src/docs/store/DocumentStore.test.ts index 2b7fc8ca..ac2afde9 100644 --- a/src/docs/store/DocumentStore.test.ts +++ b/src/docs/store/DocumentStore.test.ts @@ -2,12 +2,10 @@ import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest'; // --- Mocking Setup --- -// Mock OpenAIEmbeddings -const mockEmbedDocuments = vi.fn().mockResolvedValue([[0.1, 0.2, 0.3]]); // Keep this if addDocuments is tested elsewhere - -// Mock the module to export a mock function for the class constructor -vi.mock('@langchain/openai', () => ({ - OpenAIEmbeddings: vi.fn(), // Mock the class export as a vi.fn() +// Mock the embeddings utility +vi.mock('../../utils/embeddings', () => ({ + embedDocument: vi.fn().mockResolvedValue([0.1, 0.2, 0.3]), + embedDocuments: vi.fn().mockResolvedValue([[0.1, 0.2, 0.3]]), })); // Mock better-sqlite3 @@ -36,28 +34,18 @@ vi.mock('sqlite-vec', () => ({ // --- Test Suite --- -// Import the mocked constructor function -import { OpenAIEmbeddings } from '@langchain/openai'; // Import DocumentStore AFTER mocks are defined import { DocumentStore } from './DocumentStore'; -// Cast OpenAIEmbeddings to the correct Vitest mock type for configuration -const MockedOpenAIEmbeddingsConstructor = OpenAIEmbeddings as ReturnType; - describe('DocumentStore', () => { let documentStore: DocumentStore; beforeEach(async () => { vi.clearAllMocks(); // Clear call history etc. - // Configure the mock constructor's implementation for THIS test run - MockedOpenAIEmbeddingsConstructor.mockImplementation(() => ({ - embedDocuments: mockEmbedDocuments, - })); mockPrepare.mockReturnValue(mockStatement); // <-- Re-configure prepare mock return value // Now create the store and initialize. - // initialize() will call 'new OpenAIEmbeddings()', which uses our fresh mock implementation. documentStore = new DocumentStore(':memory:'); await documentStore.initialize(); }); diff --git a/src/utils/embeddings.test.ts b/src/utils/embeddings.test.ts new file mode 100644 index 00000000..ff7381ca --- /dev/null +++ b/src/utils/embeddings.test.ts @@ -0,0 +1,73 @@ +import { describe, expect, it, vi, beforeEach } from 'vitest'; + +// 1. Create the mock extractor function using vi.hoisted to ensure it is initialized before imports +const { mockExtractor } = vi.hoisted(() => ({ + mockExtractor: vi.fn().mockImplementation((input: string | string[]) => { + const count = Array.isArray(input) ? input.length : 1; + const vectors = Array.from({ length: count }, (_, i) => [ + Math.round((0.1 + i * 0.1) * 10) / 10, + Math.round((0.2 + i * 0.1) * 10) / 10, + Math.round((0.3 + i * 0.1) * 10) / 10, + ]); + return Promise.resolve({ + tolist: () => vectors, + }); + }), +})); + +// 2. Mock @xenova/transformers pipeline +vi.mock('@xenova/transformers', () => ({ + pipeline: vi.fn().mockResolvedValue(mockExtractor), +})); + +// 3. Import functions after the mock has been configured +import { embedDocument, embedDocuments } from './embeddings'; + +describe('embeddings utility', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('should embed a single document using Xenova feature extraction', async () => { + const result = await embedDocument('test query'); + + expect(result).toEqual([0.1, 0.2, 0.3]); + expect(mockExtractor).toHaveBeenCalledTimes(1); + expect(mockExtractor).toHaveBeenCalledWith('test query', { + pooling: 'mean', + normalize: true, + }); + }); + + it('should embed an empty string successfully', async () => { + const result = await embedDocument(''); + + expect(result).toEqual([0.1, 0.2, 0.3]); + expect(mockExtractor).toHaveBeenCalledTimes(1); + expect(mockExtractor).toHaveBeenCalledWith('', { + pooling: 'mean', + normalize: true, + }); + }); + + it('should embed multiple documents and return a 2D array of the same length', async () => { + const result = await embedDocuments(['doc1', 'doc2']); + + expect(result).toEqual([ + [0.1, 0.2, 0.3], + [0.2, 0.3, 0.4], + ]); + expect(mockExtractor).toHaveBeenCalledTimes(1); + expect(mockExtractor).toHaveBeenCalledWith(['doc1', 'doc2'], { + pooling: 'mean', + normalize: true, + }); + }); + + it('should return an empty array without calling the extractor if empty array is passed', async () => { + const result = await embedDocuments([]); + + expect(result).toEqual([]); + expect(mockExtractor).not.toHaveBeenCalled(); + }); +}); diff --git a/src/utils/embeddings.ts b/src/utils/embeddings.ts index e5f52b86..478ec26f 100644 --- a/src/utils/embeddings.ts +++ b/src/utils/embeddings.ts @@ -10,6 +10,9 @@ export async function embedDocument(q: string): Promise { } export async function embedDocuments(q: string[]): Promise { + if (q.length === 0) { + return []; + } const out = await extractor(q, { pooling: 'mean', normalize: true }); return out.tolist(); }