mirror of
https://github.com/labring/FastGPT.git
synced 2026-03-02 01:02:30 +08:00
V4.14.4 features (#6036)
* feat: add query optimize and bill (#6021) * add query optimize and bill * perf: query extension * fix: embe model * remove log * remove log * fix: test --------- Co-authored-by: xxyyh <2289112474@qq> Co-authored-by: archer <545436317@qq.com> * feat: notice (#6013) * feat: record user's language * feat: notice points/dataset indexes; support count limit; update docker-compose.yml * fix: ts error * feat: send auth code i18n * chore: dataset notice limit * chore: adjust * fix: ts * fix: countLimit race condition; i18n en-prefix locale fallback to en --------- Co-authored-by: archer <545436317@qq.com> * perf: comment * perf: send inform code * fix: type error (#6029) * feat: add ip region for chat logs (#6010) * feat: add ip region for chat logs * refactor: use Geolite2.mmdb * fix: export chat logs * fix: return location directly * test: add unit test * perf: log show ip data * adjust commercial plans (#6008) * plan frontend * plan limit * coupon * discount coupon * fix * type * fix audit * type * plan name * legacy plan * track * feat: add discount coupon * fix * fix discount coupon * openapi * type * type * env * api type * fix * fix: simple agent plugin input & agent dashboard card (#6034) * refactor: remove gridfs (#6031) * fix: replace gridfs multer operations with s3 compatible ops * wip: s3 features * refactor: remove gridfs * fix * perf: mock test * doc * doc * doc * fix: test * fix: s3 * fix: mock s3 * remove invalid config * fix: init query extension * initv4144 (#6037) * chore: initv4144 * fix * version * fix: new plans (#6039) * fix: new plans * qr modal tip * fix: buffer raw text filename (#6040) * fix: initv4144 (#6041) * fix: pay refresh (#6042) * fix: migration shell * rename collection * clear timerlock * clear timerlock * perf: faq * perf: bill schema * fix: openapi * doc * fix: share var render * feat: delete dataset queue * plan usage display (#6043) * plan usage display * text * fix * fix: ts * perf: remove invalid code * perf: init shell * doc * perf: rename field * perf: avatar presign * init * custom plan text (#6045) * fix plans * fix * fixed * computed --------- Co-authored-by: archer <545436317@qq.com> * init shell * plan text & price page back button (#6046) * init * index * delete dataset * delete dataset * perf: delete dataset * init --------- Co-authored-by: YeYuheng <57035043+YYH211@users.noreply.github.com> Co-authored-by: xxyyh <2289112474@qq> Co-authored-by: Finley Ge <32237950+FinleyGe@users.noreply.github.com> Co-authored-by: Roy <whoeverimf5@gmail.com> Co-authored-by: heheer <heheer@sealos.io>
This commit is contained in:
61
test/cases/service/common/geo.test.ts
Normal file
61
test/cases/service/common/geo.test.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { getLocationFromIp } from '@fastgpt/service/common/geo';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
describe('Get Location From IP', () => {
|
||||
it('should return the `Other` when the ip is loopback address', () => {
|
||||
const ip = '::1';
|
||||
const locationEn = getLocationFromIp(ip, 'en');
|
||||
const locationZh = getLocationFromIp(ip, 'zh');
|
||||
|
||||
expect(locationEn).toBe('Other');
|
||||
expect(locationZh).toBe('其他');
|
||||
});
|
||||
|
||||
it('should return the `Other` when the ip is private address', () => {
|
||||
const ip = '192.168.0.1';
|
||||
const locationEn = getLocationFromIp(ip, 'en');
|
||||
const locationZh = getLocationFromIp(ip, 'zh');
|
||||
|
||||
expect(locationEn).toBe('Other');
|
||||
expect(locationZh).toBe('其他');
|
||||
});
|
||||
|
||||
it('should return the `Other` when the ip is invalid', () => {
|
||||
const ip = 'Invalid';
|
||||
const locationEn = getLocationFromIp(ip, 'en');
|
||||
const locationZh = getLocationFromIp(ip, 'zh');
|
||||
|
||||
expect(locationEn).toBe('Other');
|
||||
expect(locationZh).toBe('其他');
|
||||
});
|
||||
|
||||
it('should only return the country name', () => {
|
||||
const ip = '8.8.8.8';
|
||||
const locationEn = getLocationFromIp(ip, 'en');
|
||||
const locationZh = getLocationFromIp(ip, 'zh');
|
||||
|
||||
const ipv6 = '2001:4860:4860::8888';
|
||||
const locationEnIpv6 = getLocationFromIp(ipv6, 'en');
|
||||
const locationZhIpv6 = getLocationFromIp(ipv6, 'zh');
|
||||
|
||||
expect(locationEn).toBe('United States');
|
||||
expect(locationZh).toBe('美国');
|
||||
expect(locationEnIpv6).toBe('United States');
|
||||
expect(locationZhIpv6).toBe('美国');
|
||||
});
|
||||
|
||||
it('should return full location name', () => {
|
||||
const ip = '223.5.5.5';
|
||||
const locationEn = getLocationFromIp(ip, 'en');
|
||||
const locationZh = getLocationFromIp(ip, 'zh');
|
||||
|
||||
const ipv6 = '2400:3200:baba::1';
|
||||
const locationEnIpv6 = getLocationFromIp(ipv6, 'en');
|
||||
const locationZhIpv6 = getLocationFromIp(ipv6, 'zh');
|
||||
|
||||
expect(locationEn).toBe('China, Zhejiang, Hangzhou');
|
||||
expect(locationZh).toBe('中国,浙江,杭州');
|
||||
expect(locationEnIpv6).toBe('China, Zhejiang, Hangzhou');
|
||||
expect(locationZhIpv6).toBe('中国,浙江,杭州');
|
||||
});
|
||||
});
|
||||
324
test/cases/service/common/vectorDB/controller.test.ts
Normal file
324
test/cases/service/common/vectorDB/controller.test.ts
Normal file
@@ -0,0 +1,324 @@
|
||||
import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest';
|
||||
import {
|
||||
mockVectorInsert,
|
||||
mockVectorDelete,
|
||||
mockVectorEmbRecall,
|
||||
mockVectorInit,
|
||||
mockGetVectorDataByTime,
|
||||
mockGetVectorCountByTeamId,
|
||||
mockGetVectorCountByDatasetId,
|
||||
mockGetVectorCountByCollectionId,
|
||||
resetVectorMocks
|
||||
} from '@test/mocks/common/vector';
|
||||
import { mockGetVectorsByText } from '@test/mocks/core/ai/embedding';
|
||||
|
||||
// Import controller functions after mocks are set up
|
||||
import {
|
||||
initVectorStore,
|
||||
recallFromVectorStore,
|
||||
getVectorDataByTime,
|
||||
getVectorCountByTeamId,
|
||||
getVectorCountByDatasetId,
|
||||
getVectorCountByCollectionId,
|
||||
insertDatasetDataVector,
|
||||
deleteDatasetDataVector
|
||||
} from '@fastgpt/service/common/vectorDB/controller';
|
||||
|
||||
// Mock redis cache functions
|
||||
const mockGetRedisCache = vi.fn();
|
||||
const mockSetRedisCache = vi.fn();
|
||||
const mockDelRedisCache = vi.fn();
|
||||
const mockIncrValueToCache = vi.fn();
|
||||
|
||||
vi.mock('@fastgpt/service/common/redis/cache', () => ({
|
||||
setRedisCache: (...args: any[]) => mockSetRedisCache(...args),
|
||||
getRedisCache: (...args: any[]) => mockGetRedisCache(...args),
|
||||
delRedisCache: (...args: any[]) => mockDelRedisCache(...args),
|
||||
incrValueToCache: (...args: any[]) => mockIncrValueToCache(...args),
|
||||
CacheKeyEnum: {
|
||||
team_vector_count: 'team_vector_count',
|
||||
team_point_surplus: 'team_point_surplus',
|
||||
team_point_total: 'team_point_total'
|
||||
},
|
||||
CacheKeyEnumTime: {
|
||||
team_vector_count: 1800,
|
||||
team_point_surplus: 60,
|
||||
team_point_total: 60
|
||||
}
|
||||
}));
|
||||
|
||||
describe('VectorDB Controller', () => {
|
||||
beforeEach(() => {
|
||||
resetVectorMocks();
|
||||
mockGetRedisCache.mockReset();
|
||||
mockSetRedisCache.mockReset();
|
||||
mockDelRedisCache.mockReset();
|
||||
mockIncrValueToCache.mockReset();
|
||||
mockGetVectorsByText.mockClear();
|
||||
});
|
||||
|
||||
describe('initVectorStore', () => {
|
||||
it('should call Vector.init', async () => {
|
||||
await initVectorStore();
|
||||
expect(mockVectorInit).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('recallFromVectorStore', () => {
|
||||
it('should call Vector.embRecall with correct props', async () => {
|
||||
const props = {
|
||||
teamId: 'team_123',
|
||||
datasetIds: ['dataset_1', 'dataset_2'],
|
||||
vector: [0.1, 0.2, 0.3],
|
||||
limit: 10,
|
||||
forbidCollectionIdList: ['col_forbidden']
|
||||
};
|
||||
|
||||
const result = await recallFromVectorStore(props);
|
||||
|
||||
expect(mockVectorEmbRecall).toHaveBeenCalledWith(props);
|
||||
expect(result).toEqual({
|
||||
results: [
|
||||
{ id: '1', collectionId: 'col_1', score: 0.95 },
|
||||
{ id: '2', collectionId: 'col_2', score: 0.85 }
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle filterCollectionIdList', async () => {
|
||||
const props = {
|
||||
teamId: 'team_123',
|
||||
datasetIds: ['dataset_1'],
|
||||
vector: [0.1, 0.2],
|
||||
limit: 5,
|
||||
forbidCollectionIdList: [],
|
||||
filterCollectionIdList: ['col_1', 'col_2']
|
||||
};
|
||||
|
||||
await recallFromVectorStore(props);
|
||||
|
||||
expect(mockVectorEmbRecall).toHaveBeenCalledWith(props);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getVectorDataByTime', () => {
|
||||
it('should call Vector.getVectorDataByTime with correct date range', async () => {
|
||||
const start = new Date('2024-01-01');
|
||||
const end = new Date('2024-01-31');
|
||||
|
||||
const result = await getVectorDataByTime(start, end);
|
||||
|
||||
expect(mockGetVectorDataByTime).toHaveBeenCalledWith(start, end);
|
||||
expect(result).toEqual([
|
||||
{ id: '1', teamId: 'team_1', datasetId: 'dataset_1' },
|
||||
{ id: '2', teamId: 'team_1', datasetId: 'dataset_2' }
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getVectorCountByTeamId', () => {
|
||||
it('should return cached count if available', async () => {
|
||||
mockGetRedisCache.mockResolvedValue('150');
|
||||
|
||||
const result = await getVectorCountByTeamId('team_123');
|
||||
|
||||
expect(result).toBe(150);
|
||||
expect(mockGetVectorCountByTeamId).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should fetch from Vector and cache if no cache exists', async () => {
|
||||
mockGetRedisCache.mockResolvedValue(null);
|
||||
mockGetVectorCountByTeamId.mockResolvedValue(200);
|
||||
|
||||
const result = await getVectorCountByTeamId('team_456');
|
||||
|
||||
expect(result).toBe(200);
|
||||
expect(mockGetVectorCountByTeamId).toHaveBeenCalledWith('team_456');
|
||||
});
|
||||
|
||||
it('should handle undefined cache value', async () => {
|
||||
mockGetRedisCache.mockResolvedValue(undefined);
|
||||
mockGetVectorCountByTeamId.mockResolvedValue(50);
|
||||
|
||||
const result = await getVectorCountByTeamId('team_789');
|
||||
|
||||
expect(result).toBe(50);
|
||||
expect(mockGetVectorCountByTeamId).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getVectorCountByDatasetId', () => {
|
||||
it('should call Vector.getVectorCountByDatasetId', async () => {
|
||||
const result = await getVectorCountByDatasetId('team_1', 'dataset_1');
|
||||
|
||||
expect(mockGetVectorCountByDatasetId).toHaveBeenCalledWith('team_1', 'dataset_1');
|
||||
expect(result).toBe(50);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getVectorCountByCollectionId', () => {
|
||||
it('should call Vector.getVectorCountByCollectionId', async () => {
|
||||
const result = await getVectorCountByCollectionId('team_1', 'dataset_1', 'col_1');
|
||||
|
||||
expect(mockGetVectorCountByCollectionId).toHaveBeenCalledWith('team_1', 'dataset_1', 'col_1');
|
||||
expect(result).toBe(25);
|
||||
});
|
||||
});
|
||||
|
||||
describe('insertDatasetDataVector', () => {
|
||||
const mockModel = {
|
||||
model: 'text-embedding-ada-002',
|
||||
name: 'text-embedding-ada-002',
|
||||
charsPointsPrice: 0,
|
||||
maxToken: 8192,
|
||||
weight: 100,
|
||||
defaultToken: 512,
|
||||
dbConfig: {},
|
||||
queryExtensionModel: ''
|
||||
};
|
||||
|
||||
it('should generate embeddings and insert vectors', async () => {
|
||||
const mockVectors = [
|
||||
[0.1, 0.2],
|
||||
[0.3, 0.4]
|
||||
];
|
||||
mockGetVectorsByText.mockResolvedValue({
|
||||
tokens: 100,
|
||||
vectors: mockVectors
|
||||
});
|
||||
mockVectorInsert.mockResolvedValue({
|
||||
insertIds: ['id_1', 'id_2']
|
||||
});
|
||||
|
||||
const result = await insertDatasetDataVector({
|
||||
teamId: 'team_123',
|
||||
datasetId: 'dataset_456',
|
||||
collectionId: 'col_789',
|
||||
inputs: ['hello world', 'test text'],
|
||||
model: mockModel as any
|
||||
});
|
||||
|
||||
expect(mockGetVectorsByText).toHaveBeenCalledWith({
|
||||
model: mockModel,
|
||||
input: ['hello world', 'test text'],
|
||||
type: 'db'
|
||||
});
|
||||
expect(mockVectorInsert).toHaveBeenCalledWith({
|
||||
teamId: 'team_123',
|
||||
datasetId: 'dataset_456',
|
||||
collectionId: 'col_789',
|
||||
vectors: mockVectors
|
||||
});
|
||||
expect(result).toEqual({
|
||||
tokens: 100,
|
||||
insertIds: ['id_1', 'id_2']
|
||||
});
|
||||
});
|
||||
|
||||
it('should increment team vector cache', async () => {
|
||||
mockGetVectorsByText.mockResolvedValue({
|
||||
tokens: 50,
|
||||
vectors: [[0.1]]
|
||||
});
|
||||
mockVectorInsert.mockResolvedValue({
|
||||
insertIds: ['id_1']
|
||||
});
|
||||
|
||||
await insertDatasetDataVector({
|
||||
teamId: 'team_abc',
|
||||
datasetId: 'dataset_def',
|
||||
collectionId: 'col_ghi',
|
||||
inputs: ['single input'],
|
||||
model: mockModel as any
|
||||
});
|
||||
|
||||
// Cache increment is called asynchronously
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
expect(mockIncrValueToCache).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle empty inputs', async () => {
|
||||
mockGetVectorsByText.mockResolvedValue({
|
||||
tokens: 0,
|
||||
vectors: []
|
||||
});
|
||||
mockVectorInsert.mockResolvedValue({
|
||||
insertIds: []
|
||||
});
|
||||
|
||||
const result = await insertDatasetDataVector({
|
||||
teamId: 'team_123',
|
||||
datasetId: 'dataset_456',
|
||||
collectionId: 'col_789',
|
||||
inputs: [],
|
||||
model: mockModel as any
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
tokens: 0,
|
||||
insertIds: []
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteDatasetDataVector', () => {
|
||||
it('should delete by single id', async () => {
|
||||
const props = {
|
||||
teamId: 'team_123',
|
||||
id: 'vector_id_1'
|
||||
};
|
||||
|
||||
await deleteDatasetDataVector(props);
|
||||
|
||||
expect(mockVectorDelete).toHaveBeenCalledWith(props);
|
||||
});
|
||||
|
||||
it('should delete by datasetIds', async () => {
|
||||
const props = {
|
||||
teamId: 'team_123',
|
||||
datasetIds: ['dataset_1', 'dataset_2']
|
||||
};
|
||||
|
||||
await deleteDatasetDataVector(props);
|
||||
|
||||
expect(mockVectorDelete).toHaveBeenCalledWith(props);
|
||||
});
|
||||
|
||||
it('should delete by datasetIds and collectionIds', async () => {
|
||||
const props = {
|
||||
teamId: 'team_123',
|
||||
datasetIds: ['dataset_1'],
|
||||
collectionIds: ['col_1', 'col_2']
|
||||
};
|
||||
|
||||
await deleteDatasetDataVector(props);
|
||||
|
||||
expect(mockVectorDelete).toHaveBeenCalledWith(props);
|
||||
});
|
||||
|
||||
it('should delete by idList', async () => {
|
||||
const props = {
|
||||
teamId: 'team_123',
|
||||
idList: ['id_1', 'id_2', 'id_3']
|
||||
};
|
||||
|
||||
await deleteDatasetDataVector(props);
|
||||
|
||||
expect(mockVectorDelete).toHaveBeenCalledWith(props);
|
||||
});
|
||||
|
||||
it('should call delete and return result', async () => {
|
||||
mockVectorDelete.mockResolvedValue({ deletedCount: 5 });
|
||||
|
||||
const props = {
|
||||
teamId: 'team_cache_test',
|
||||
id: 'some_id'
|
||||
};
|
||||
|
||||
const result = await deleteDatasetDataVector(props);
|
||||
|
||||
expect(mockVectorDelete).toHaveBeenCalledWith(props);
|
||||
expect(result).toEqual({ deletedCount: 5 });
|
||||
});
|
||||
});
|
||||
});
|
||||
231
test/cases/service/core/ai/hooks/useTextCosine.test.ts
Normal file
231
test/cases/service/core/ai/hooks/useTextCosine.test.ts
Normal file
@@ -0,0 +1,231 @@
|
||||
import { describe, expect, it, vi, beforeEach } from 'vitest';
|
||||
import { useTextCosine } from '@fastgpt/service/core/ai/hooks/useTextCosine';
|
||||
import {
|
||||
generateMockEmbedding,
|
||||
createMockVectorsResponse,
|
||||
generateSimilarVector,
|
||||
generateOrthogonalVector,
|
||||
mockGetVectorsByText
|
||||
} from '../../../../../mocks/core/ai/embedding';
|
||||
|
||||
describe('useTextCosine', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('lazyGreedyQuerySelection', () => {
|
||||
it('should return empty array when candidates is empty', async () => {
|
||||
const { lazyGreedyQuerySelection } = useTextCosine({
|
||||
embeddingModel: 'text-embedding-ada-002'
|
||||
});
|
||||
const result = await lazyGreedyQuerySelection({
|
||||
originalText: 'test query',
|
||||
candidates: [],
|
||||
k: 3
|
||||
});
|
||||
|
||||
expect(result.selectedData).toEqual([]);
|
||||
});
|
||||
|
||||
it('should select k candidates when k <= candidates.length', async () => {
|
||||
const { lazyGreedyQuerySelection } = useTextCosine({
|
||||
embeddingModel: 'text-embedding-ada-002'
|
||||
});
|
||||
const result = await lazyGreedyQuerySelection({
|
||||
originalText: 'original text',
|
||||
candidates: ['candidate1', 'candidate2', 'candidate3'],
|
||||
k: 2
|
||||
});
|
||||
|
||||
expect(result.selectedData.length).toBe(2);
|
||||
});
|
||||
|
||||
it('should select all candidates when k > candidates.length', async () => {
|
||||
const { lazyGreedyQuerySelection } = useTextCosine({
|
||||
embeddingModel: 'text-embedding-ada-002'
|
||||
});
|
||||
const result = await lazyGreedyQuerySelection({
|
||||
originalText: 'original text',
|
||||
candidates: ['candidate1', 'candidate2'],
|
||||
k: 5
|
||||
});
|
||||
|
||||
expect(result.selectedData.length).toBe(2);
|
||||
});
|
||||
|
||||
it('should select single candidate correctly', async () => {
|
||||
const { lazyGreedyQuerySelection } = useTextCosine({
|
||||
embeddingModel: 'text-embedding-ada-002'
|
||||
});
|
||||
const result = await lazyGreedyQuerySelection({
|
||||
originalText: 'original text',
|
||||
candidates: ['only candidate'],
|
||||
k: 1
|
||||
});
|
||||
|
||||
expect(result.selectedData).toEqual(['only candidate']);
|
||||
});
|
||||
|
||||
it('should prefer candidates with higher relevance to original text', async () => {
|
||||
const originalVector = generateMockEmbedding('original text');
|
||||
// Create a candidate very similar to original
|
||||
const similarVector = generateSimilarVector(originalVector, 0.95);
|
||||
// Create a candidate very different from original
|
||||
const differentVector = generateOrthogonalVector(originalVector);
|
||||
|
||||
mockGetVectorsByText.mockResolvedValueOnce({
|
||||
tokens: 30,
|
||||
vectors: [originalVector, differentVector, similarVector]
|
||||
});
|
||||
|
||||
const { lazyGreedyQuerySelection } = useTextCosine({
|
||||
embeddingModel: 'text-embedding-ada-002'
|
||||
});
|
||||
const result = await lazyGreedyQuerySelection({
|
||||
originalText: 'original text',
|
||||
candidates: ['different', 'similar'],
|
||||
k: 1,
|
||||
alpha: 1.0 // Only consider relevance, not diversity
|
||||
});
|
||||
|
||||
// Should select the similar candidate first when alpha=1.0
|
||||
expect(result.selectedData[0]).toBe('similar');
|
||||
});
|
||||
|
||||
it('should balance relevance and diversity with default alpha', async () => {
|
||||
const { lazyGreedyQuerySelection } = useTextCosine({
|
||||
embeddingModel: 'text-embedding-ada-002'
|
||||
});
|
||||
const result = await lazyGreedyQuerySelection({
|
||||
originalText: 'original text',
|
||||
candidates: ['c1', 'c2', 'c3'],
|
||||
k: 3,
|
||||
alpha: 0.3 // Default alpha
|
||||
});
|
||||
|
||||
expect(result.selectedData.length).toBe(3);
|
||||
// All candidates should be selected
|
||||
expect(result.selectedData).toContain('c1');
|
||||
expect(result.selectedData).toContain('c2');
|
||||
expect(result.selectedData).toContain('c3');
|
||||
});
|
||||
|
||||
it('should call getVectorsByText with correct parameters', async () => {
|
||||
const { lazyGreedyQuerySelection } = useTextCosine({ embeddingModel: 'custom-model' });
|
||||
await lazyGreedyQuerySelection({
|
||||
originalText: 'test query',
|
||||
candidates: ['candidate'],
|
||||
k: 1
|
||||
});
|
||||
|
||||
expect(mockGetVectorsByText).toHaveBeenCalledWith({
|
||||
model: expect.anything(),
|
||||
input: ['test query', 'candidate'],
|
||||
type: 'query'
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle identical candidates correctly', async () => {
|
||||
const originalVector = generateMockEmbedding('original');
|
||||
const identicalVector = generateMockEmbedding('same');
|
||||
|
||||
mockGetVectorsByText.mockResolvedValueOnce({
|
||||
tokens: 30,
|
||||
vectors: [originalVector, identicalVector, identicalVector, identicalVector]
|
||||
});
|
||||
|
||||
const { lazyGreedyQuerySelection } = useTextCosine({
|
||||
embeddingModel: 'text-embedding-ada-002'
|
||||
});
|
||||
const result = await lazyGreedyQuerySelection({
|
||||
originalText: 'original',
|
||||
candidates: ['same1', 'same2', 'same3'],
|
||||
k: 2
|
||||
});
|
||||
|
||||
expect(result.selectedData.length).toBe(2);
|
||||
});
|
||||
|
||||
it('should respect alpha parameter for diversity weighting', async () => {
|
||||
const originalVector = generateMockEmbedding('original');
|
||||
// Create vectors with known similarities
|
||||
const similarVector = generateSimilarVector(originalVector, 0.9);
|
||||
const differentVector = generateOrthogonalVector(originalVector);
|
||||
|
||||
mockGetVectorsByText.mockResolvedValueOnce({
|
||||
tokens: 25,
|
||||
vectors: [originalVector, similarVector, differentVector]
|
||||
});
|
||||
|
||||
const { lazyGreedyQuerySelection } = useTextCosine({
|
||||
embeddingModel: 'text-embedding-ada-002'
|
||||
});
|
||||
|
||||
// With high alpha (more relevance)
|
||||
const resultHighAlpha = await lazyGreedyQuerySelection({
|
||||
originalText: 'original',
|
||||
candidates: ['similar', 'different'],
|
||||
k: 1,
|
||||
alpha: 0.9
|
||||
});
|
||||
|
||||
expect(resultHighAlpha.selectedData.length).toBe(1);
|
||||
});
|
||||
|
||||
it('should return correct embedding tokens', async () => {
|
||||
const mockResponse = createMockVectorsResponse(['test', 'candidate']);
|
||||
mockResponse.tokens = 12345; // Override tokens for specific test
|
||||
|
||||
mockGetVectorsByText.mockResolvedValueOnce(mockResponse);
|
||||
|
||||
const { lazyGreedyQuerySelection } = useTextCosine({
|
||||
embeddingModel: 'text-embedding-ada-002'
|
||||
});
|
||||
const result = await lazyGreedyQuerySelection({
|
||||
originalText: 'test',
|
||||
candidates: ['candidate'],
|
||||
k: 1
|
||||
});
|
||||
|
||||
expect(result.embeddingTokens).toBe(12345);
|
||||
});
|
||||
|
||||
it('should handle k=0 correctly', async () => {
|
||||
const { lazyGreedyQuerySelection } = useTextCosine({
|
||||
embeddingModel: 'text-embedding-ada-002'
|
||||
});
|
||||
const result = await lazyGreedyQuerySelection({
|
||||
originalText: 'test',
|
||||
candidates: ['candidate'],
|
||||
k: 0
|
||||
});
|
||||
|
||||
expect(result.selectedData).toEqual([]);
|
||||
});
|
||||
|
||||
it('should select diverse candidates when alpha is low', async () => {
|
||||
const originalVector = generateMockEmbedding('original');
|
||||
// Create 3 candidates: 2 similar to each other, 1 different
|
||||
const similar1 = generateSimilarVector(originalVector, 0.8);
|
||||
const similar2 = generateSimilarVector(similar1, 0.95); // Very close to similar1
|
||||
const different = generateOrthogonalVector(originalVector);
|
||||
|
||||
mockGetVectorsByText.mockResolvedValueOnce({
|
||||
tokens: 40,
|
||||
vectors: [originalVector, similar1, similar2, different]
|
||||
});
|
||||
|
||||
const { lazyGreedyQuerySelection } = useTextCosine({
|
||||
embeddingModel: 'text-embedding-ada-002'
|
||||
});
|
||||
const result = await lazyGreedyQuerySelection({
|
||||
originalText: 'original',
|
||||
candidates: ['similar1', 'similar2', 'different'],
|
||||
k: 2,
|
||||
alpha: 0.1 // Low alpha means more diversity
|
||||
});
|
||||
|
||||
expect(result.selectedData.length).toBe(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
23
test/mocks/common/bullmq.ts
Normal file
23
test/mocks/common/bullmq.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { vi } from 'vitest';
|
||||
|
||||
// Mock BullMQ to prevent queue connection errors
|
||||
vi.mock('@fastgpt/service/common/bullmq', async (importOriginal) => {
|
||||
const actual = (await importOriginal()) as any;
|
||||
|
||||
const mockQueue = {
|
||||
add: vi.fn().mockResolvedValue({ id: '1' }),
|
||||
close: vi.fn().mockResolvedValue(undefined),
|
||||
on: vi.fn()
|
||||
};
|
||||
|
||||
const mockWorker = {
|
||||
close: vi.fn().mockResolvedValue(undefined),
|
||||
on: vi.fn()
|
||||
};
|
||||
|
||||
return {
|
||||
...actual,
|
||||
getQueue: vi.fn(() => mockQueue),
|
||||
getWorker: vi.fn(() => mockWorker)
|
||||
};
|
||||
});
|
||||
10
test/mocks/common/geo.ts
Normal file
10
test/mocks/common/geo.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import path from 'node:path';
|
||||
import { vi } from 'vitest';
|
||||
|
||||
vi.mock('@fastgpt/service/common/geo/constants', async (importOriginal) => {
|
||||
const actual = (await importOriginal()) as any;
|
||||
return {
|
||||
...actual,
|
||||
dbPath: path.join(process.cwd(), 'projects/app/data/GeoLite2-City.mmdb')
|
||||
};
|
||||
});
|
||||
19
test/mocks/common/mongo.ts
Normal file
19
test/mocks/common/mongo.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { vi } from 'vitest';
|
||||
import { randomUUID } from 'crypto';
|
||||
import type { Mongoose } from '@fastgpt/service/common/mongo';
|
||||
|
||||
/**
|
||||
* Mock MongoDB connection for testing
|
||||
* Creates a unique database for each test run and drops it on connection
|
||||
*/
|
||||
vi.mock(import('@fastgpt/service/common/mongo/init'), async (importOriginal: any) => {
|
||||
const mod = await importOriginal();
|
||||
return {
|
||||
...mod,
|
||||
connectMongo: async (props: { db: Mongoose; url: string; connectedCb?: () => void }) => {
|
||||
const { db, url } = props;
|
||||
await db.connect(url, { dbName: randomUUID() });
|
||||
await db.connection.db?.dropDatabase();
|
||||
}
|
||||
};
|
||||
});
|
||||
81
test/mocks/common/redis.ts
Normal file
81
test/mocks/common/redis.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import { vi } from 'vitest';
|
||||
|
||||
// Create a comprehensive mock Redis client factory
|
||||
const createMockRedisClient = () => ({
|
||||
// Connection methods
|
||||
on: vi.fn().mockReturnThis(),
|
||||
connect: vi.fn().mockResolvedValue(undefined),
|
||||
disconnect: vi.fn().mockResolvedValue(undefined),
|
||||
quit: vi.fn().mockResolvedValue('OK'),
|
||||
duplicate: vi.fn(function (this: any) {
|
||||
return createMockRedisClient();
|
||||
}),
|
||||
|
||||
// Key-value operations
|
||||
get: vi.fn().mockResolvedValue(null),
|
||||
set: vi.fn().mockResolvedValue('OK'),
|
||||
del: vi.fn().mockResolvedValue(1),
|
||||
exists: vi.fn().mockResolvedValue(0),
|
||||
keys: vi.fn().mockResolvedValue([]),
|
||||
|
||||
// Hash operations
|
||||
hget: vi.fn().mockResolvedValue(null),
|
||||
hset: vi.fn().mockResolvedValue(1),
|
||||
hdel: vi.fn().mockResolvedValue(1),
|
||||
hgetall: vi.fn().mockResolvedValue({}),
|
||||
hmset: vi.fn().mockResolvedValue('OK'),
|
||||
|
||||
// Expiry operations
|
||||
expire: vi.fn().mockResolvedValue(1),
|
||||
ttl: vi.fn().mockResolvedValue(-1),
|
||||
expireat: vi.fn().mockResolvedValue(1),
|
||||
|
||||
// Increment operations
|
||||
incr: vi.fn().mockResolvedValue(1),
|
||||
decr: vi.fn().mockResolvedValue(1),
|
||||
incrby: vi.fn().mockResolvedValue(1),
|
||||
decrby: vi.fn().mockResolvedValue(1),
|
||||
incrbyfloat: vi.fn().mockResolvedValue(1),
|
||||
|
||||
// Server commands
|
||||
info: vi.fn().mockResolvedValue(''),
|
||||
ping: vi.fn().mockResolvedValue('PONG'),
|
||||
flushdb: vi.fn().mockResolvedValue('OK'),
|
||||
|
||||
// List operations
|
||||
lpush: vi.fn().mockResolvedValue(1),
|
||||
rpush: vi.fn().mockResolvedValue(1),
|
||||
lpop: vi.fn().mockResolvedValue(null),
|
||||
rpop: vi.fn().mockResolvedValue(null),
|
||||
llen: vi.fn().mockResolvedValue(0),
|
||||
|
||||
// Set operations
|
||||
sadd: vi.fn().mockResolvedValue(1),
|
||||
srem: vi.fn().mockResolvedValue(1),
|
||||
smembers: vi.fn().mockResolvedValue([]),
|
||||
sismember: vi.fn().mockResolvedValue(0)
|
||||
});
|
||||
|
||||
// Mock Redis connections to prevent connection errors in tests
|
||||
vi.mock('@fastgpt/service/common/redis', async (importOriginal) => {
|
||||
const actual = (await importOriginal()) as any;
|
||||
|
||||
return {
|
||||
...actual,
|
||||
newQueueRedisConnection: vi.fn(createMockRedisClient),
|
||||
newWorkerRedisConnection: vi.fn(createMockRedisClient),
|
||||
getGlobalRedisConnection: vi.fn(() => {
|
||||
if (!global.mockRedisClient) {
|
||||
global.mockRedisClient = createMockRedisClient();
|
||||
}
|
||||
return global.mockRedisClient;
|
||||
}),
|
||||
initRedisClient: vi.fn().mockResolvedValue(createMockRedisClient())
|
||||
};
|
||||
});
|
||||
|
||||
// Initialize global.redisClient with mock before any module imports it
|
||||
// This prevents getGlobalRedisConnection() from creating a real Redis client
|
||||
if (!global.redisClient) {
|
||||
global.redisClient = createMockRedisClient() as any;
|
||||
}
|
||||
167
test/mocks/common/s3.ts
Normal file
167
test/mocks/common/s3.ts
Normal file
@@ -0,0 +1,167 @@
|
||||
import { vi } from 'vitest';
|
||||
|
||||
// Create mock S3 bucket object for global use
|
||||
const createMockS3Bucket = () => ({
|
||||
name: 'mock-bucket',
|
||||
client: {},
|
||||
externalClient: {},
|
||||
exist: vi.fn().mockResolvedValue(true),
|
||||
delete: vi.fn().mockResolvedValue(undefined),
|
||||
putObject: vi.fn().mockResolvedValue(undefined),
|
||||
getObject: vi.fn().mockResolvedValue(null),
|
||||
statObject: vi.fn().mockResolvedValue({ size: 0, etag: 'mock-etag' }),
|
||||
move: vi.fn().mockResolvedValue(undefined),
|
||||
copy: vi.fn().mockResolvedValue(undefined),
|
||||
addDeleteJob: vi.fn().mockResolvedValue(undefined),
|
||||
createPostPresignedUrl: vi.fn().mockResolvedValue({
|
||||
url: 'http://localhost:9000/mock-bucket',
|
||||
fields: { key: 'mock-key' },
|
||||
maxSize: 100 * 1024 * 1024
|
||||
}),
|
||||
createExternalUrl: vi.fn().mockResolvedValue('http://localhost:9000/mock-bucket/mock-key'),
|
||||
createGetPresignedUrl: vi.fn().mockResolvedValue('http://localhost:9000/mock-bucket/mock-key'),
|
||||
createPublicUrl: vi.fn((key: string) => `http://localhost:9000/mock-bucket/${key}`)
|
||||
});
|
||||
|
||||
// Initialize global s3BucketMap early to prevent any real S3 connections
|
||||
const mockBucket = createMockS3Bucket();
|
||||
global.s3BucketMap = {
|
||||
'fastgpt-public': mockBucket,
|
||||
'fastgpt-private': mockBucket
|
||||
} as any;
|
||||
|
||||
// Mock minio Client to prevent real connections
|
||||
const createMockMinioClient = vi.hoisted(() => {
|
||||
return vi.fn().mockImplementation(() => ({
|
||||
bucketExists: vi.fn().mockResolvedValue(true),
|
||||
makeBucket: vi.fn().mockResolvedValue(undefined),
|
||||
setBucketPolicy: vi.fn().mockResolvedValue(undefined),
|
||||
copyObject: vi.fn().mockResolvedValue(undefined),
|
||||
removeObject: vi.fn().mockResolvedValue(undefined),
|
||||
putObject: vi.fn().mockResolvedValue({ etag: 'mock-etag' }),
|
||||
getObject: vi.fn().mockResolvedValue(null),
|
||||
statObject: vi.fn().mockResolvedValue({ size: 0, etag: 'mock-etag' }),
|
||||
presignedGetObject: vi.fn().mockResolvedValue('http://localhost:9000/mock-bucket/mock-object'),
|
||||
presignedPostPolicy: vi.fn().mockResolvedValue({
|
||||
postURL: 'http://localhost:9000/mock-bucket',
|
||||
formData: { key: 'mock-key' }
|
||||
}),
|
||||
newPostPolicy: vi.fn(() => ({
|
||||
setKey: vi.fn().mockReturnThis(),
|
||||
setBucket: vi.fn().mockReturnThis(),
|
||||
setContentType: vi.fn().mockReturnThis(),
|
||||
setContentLengthRange: vi.fn().mockReturnThis(),
|
||||
setExpires: vi.fn().mockReturnThis(),
|
||||
setUserMetaData: vi.fn().mockReturnThis()
|
||||
}))
|
||||
}));
|
||||
});
|
||||
|
||||
vi.mock('minio', () => ({
|
||||
Client: createMockMinioClient(),
|
||||
S3Error: class S3Error extends Error {},
|
||||
CopyConditions: vi.fn()
|
||||
}));
|
||||
|
||||
// Simplified S3 bucket class mock
|
||||
const createMockBucketClass = (defaultName: string) => {
|
||||
return class MockS3Bucket {
|
||||
public name: string;
|
||||
public options: any;
|
||||
public client = {};
|
||||
public externalClient = {};
|
||||
|
||||
constructor(bucket?: string, options?: any) {
|
||||
this.name = bucket || defaultName;
|
||||
this.options = options || {};
|
||||
}
|
||||
|
||||
async exist() {
|
||||
return true;
|
||||
}
|
||||
async delete() {}
|
||||
async putObject() {}
|
||||
async getObject() {
|
||||
return null;
|
||||
}
|
||||
async statObject() {
|
||||
return { size: 0, etag: 'mock-etag' };
|
||||
}
|
||||
async move() {}
|
||||
async copy() {}
|
||||
async addDeleteJob() {}
|
||||
async createPostPresignedUrl(params: any, options?: any) {
|
||||
return {
|
||||
url: 'http://localhost:9000/mock-bucket',
|
||||
fields: { key: `mock/${params.teamId || 'test'}/${params.filename}` },
|
||||
maxSize: (options?.maxFileSize || 100) * 1024 * 1024
|
||||
};
|
||||
}
|
||||
async createExternalUrl(params: any) {
|
||||
return `http://localhost:9000/mock-bucket/${params.key}`;
|
||||
}
|
||||
async createGetPresignedUrl(params: any) {
|
||||
return `http://localhost:9000/mock-bucket/${params.key}`;
|
||||
}
|
||||
createPublicUrl(objectKey: string) {
|
||||
return `http://localhost:9000/mock-bucket/${objectKey}`;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
vi.mock('@fastgpt/service/common/s3/buckets/base', () => ({
|
||||
S3BaseBucket: createMockBucketClass('fastgpt-bucket')
|
||||
}));
|
||||
|
||||
vi.mock('@fastgpt/service/common/s3/buckets/public', () => ({
|
||||
S3PublicBucket: createMockBucketClass('fastgpt-public')
|
||||
}));
|
||||
|
||||
vi.mock('@fastgpt/service/common/s3/buckets/private', () => ({
|
||||
S3PrivateBucket: createMockBucketClass('fastgpt-private')
|
||||
}));
|
||||
|
||||
// Mock S3 source modules
|
||||
vi.mock('@fastgpt/service/common/s3/sources/avatar', () => ({
|
||||
getS3AvatarSource: vi.fn(() => ({
|
||||
prefix: '/avatar/',
|
||||
createUploadAvatarURL: vi.fn().mockResolvedValue({
|
||||
url: 'http://localhost:9000/mock-bucket',
|
||||
fields: { key: 'mock-key' },
|
||||
maxSize: 5 * 1024 * 1024
|
||||
}),
|
||||
createPublicUrl: vi.fn((key: string) => `http://localhost:9000/mock-bucket/${key}`),
|
||||
removeAvatarTTL: vi.fn().mockResolvedValue(undefined),
|
||||
deleteAvatar: vi.fn().mockResolvedValue(undefined),
|
||||
refreshAvatar: vi.fn().mockResolvedValue(undefined),
|
||||
copyAvatar: vi.fn().mockResolvedValue('http://localhost:9000/mock-bucket/mock-avatar')
|
||||
}))
|
||||
}));
|
||||
|
||||
vi.mock('@fastgpt/service/common/s3/sources/dataset/index', () => ({
|
||||
getS3DatasetSource: vi.fn(() => ({
|
||||
createUploadDatasetFileURL: vi.fn().mockResolvedValue({
|
||||
url: 'http://localhost:9000/mock-bucket',
|
||||
fields: { key: 'mock-key' },
|
||||
maxSize: 500 * 1024 * 1024
|
||||
}),
|
||||
deleteDatasetFile: vi.fn().mockResolvedValue(undefined)
|
||||
})),
|
||||
S3DatasetSource: vi.fn()
|
||||
}));
|
||||
|
||||
vi.mock('@fastgpt/service/common/s3/sources/chat/index', () => ({
|
||||
S3ChatSource: vi.fn()
|
||||
}));
|
||||
|
||||
// Mock S3 initialization
|
||||
vi.mock('@fastgpt/service/common/s3', () => ({
|
||||
initS3Buckets: vi.fn(() => {
|
||||
const mockBucket = createMockS3Bucket();
|
||||
global.s3BucketMap = {
|
||||
'fastgpt-public': mockBucket,
|
||||
'fastgpt-private': mockBucket
|
||||
} as any;
|
||||
}),
|
||||
initS3MQWorker: vi.fn().mockResolvedValue(undefined)
|
||||
}));
|
||||
33
test/mocks/common/system.ts
Normal file
33
test/mocks/common/system.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { vi } from 'vitest';
|
||||
import { readFileSync } from 'fs';
|
||||
|
||||
/**
|
||||
* Mock system configuration for testing
|
||||
*/
|
||||
vi.mock(import('@/service/common/system'), async (importOriginal) => {
|
||||
const mod = await importOriginal();
|
||||
return {
|
||||
...mod,
|
||||
getSystemVersion: async () => {
|
||||
return '0.0.0';
|
||||
},
|
||||
readConfigData: async () => {
|
||||
return readFileSync('projects/app/data/config.json', 'utf-8');
|
||||
},
|
||||
initSystemConfig: async () => {
|
||||
// read env from projects/app/.env
|
||||
const str = readFileSync('projects/app/.env.local', 'utf-8');
|
||||
const lines = str.split('\n');
|
||||
const systemEnv: Record<string, string> = {};
|
||||
for (const line of lines) {
|
||||
const [key, value] = line.split('=');
|
||||
if (key && value) {
|
||||
systemEnv[key] = value;
|
||||
}
|
||||
}
|
||||
global.systemEnv = systemEnv as any;
|
||||
|
||||
return;
|
||||
}
|
||||
};
|
||||
});
|
||||
19
test/mocks/common/tracks.ts
Normal file
19
test/mocks/common/tracks.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { vi } from 'vitest';
|
||||
|
||||
// Mock tracking utilities - automatically mock all methods
|
||||
vi.mock('@fastgpt/service/common/middle/tracks/utils', async (importOriginal) => {
|
||||
const actual = (await importOriginal()) as any;
|
||||
|
||||
// Get all methods from original pushTrack and mock them
|
||||
const mockedPushTrack: Record<string, any> = {};
|
||||
if (actual.pushTrack) {
|
||||
Object.keys(actual.pushTrack).forEach((key) => {
|
||||
mockedPushTrack[key] = vi.fn();
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
...actual,
|
||||
pushTrack: mockedPushTrack
|
||||
};
|
||||
});
|
||||
79
test/mocks/common/vector.ts
Normal file
79
test/mocks/common/vector.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { vi } from 'vitest';
|
||||
|
||||
/**
|
||||
* Mock Vector Controller for testing
|
||||
*/
|
||||
|
||||
export const mockVectorInsert = vi.fn().mockResolvedValue({
|
||||
insertIds: ['id_1', 'id_2', 'id_3']
|
||||
});
|
||||
|
||||
export const mockVectorDelete = vi.fn().mockResolvedValue(undefined);
|
||||
|
||||
export const mockVectorEmbRecall = vi.fn().mockResolvedValue({
|
||||
results: [
|
||||
{ id: '1', collectionId: 'col_1', score: 0.95 },
|
||||
{ id: '2', collectionId: 'col_2', score: 0.85 }
|
||||
]
|
||||
});
|
||||
|
||||
export const mockVectorInit = vi.fn().mockResolvedValue(undefined);
|
||||
|
||||
export const mockGetVectorDataByTime = vi.fn().mockResolvedValue([
|
||||
{ id: '1', teamId: 'team_1', datasetId: 'dataset_1' },
|
||||
{ id: '2', teamId: 'team_1', datasetId: 'dataset_2' }
|
||||
]);
|
||||
|
||||
export const mockGetVectorCountByTeamId = vi.fn().mockResolvedValue(100);
|
||||
|
||||
export const mockGetVectorCountByDatasetId = vi.fn().mockResolvedValue(50);
|
||||
|
||||
export const mockGetVectorCountByCollectionId = vi.fn().mockResolvedValue(25);
|
||||
|
||||
const MockVectorCtrl = vi.fn().mockImplementation(() => ({
|
||||
init: mockVectorInit,
|
||||
insert: mockVectorInsert,
|
||||
delete: mockVectorDelete,
|
||||
embRecall: mockVectorEmbRecall,
|
||||
getVectorDataByTime: mockGetVectorDataByTime,
|
||||
getVectorCountByTeamId: mockGetVectorCountByTeamId,
|
||||
getVectorCountByDatasetId: mockGetVectorCountByDatasetId,
|
||||
getVectorCountByCollectionId: mockGetVectorCountByCollectionId
|
||||
}));
|
||||
|
||||
// Mock PgVectorCtrl
|
||||
vi.mock('@fastgpt/service/common/vectorDB/pg', () => ({
|
||||
PgVectorCtrl: MockVectorCtrl
|
||||
}));
|
||||
|
||||
// Mock ObVectorCtrl
|
||||
vi.mock('@fastgpt/service/common/vectorDB/oceanbase', () => ({
|
||||
ObVectorCtrl: MockVectorCtrl
|
||||
}));
|
||||
|
||||
// Mock MilvusCtrl
|
||||
vi.mock('@fastgpt/service/common/vectorDB/milvus', () => ({
|
||||
MilvusCtrl: MockVectorCtrl
|
||||
}));
|
||||
|
||||
// Mock constants - use PG_ADDRESS to ensure PgVectorCtrl is used
|
||||
vi.mock('@fastgpt/service/common/vectorDB/constants', () => ({
|
||||
DatasetVectorDbName: 'fastgpt',
|
||||
DatasetVectorTableName: 'modeldata',
|
||||
PG_ADDRESS: 'mock://pg',
|
||||
OCEANBASE_ADDRESS: undefined,
|
||||
MILVUS_ADDRESS: undefined,
|
||||
MILVUS_TOKEN: undefined
|
||||
}));
|
||||
|
||||
// Export mocks for test assertions
|
||||
export const resetVectorMocks = () => {
|
||||
mockVectorInsert.mockClear();
|
||||
mockVectorDelete.mockClear();
|
||||
mockVectorEmbRecall.mockClear();
|
||||
mockVectorInit.mockClear();
|
||||
mockGetVectorDataByTime.mockClear();
|
||||
mockGetVectorCountByTeamId.mockClear();
|
||||
mockGetVectorCountByDatasetId.mockClear();
|
||||
mockGetVectorCountByCollectionId.mockClear();
|
||||
};
|
||||
131
test/mocks/core/ai/embedding.ts
Normal file
131
test/mocks/core/ai/embedding.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
import { vi } from 'vitest';
|
||||
|
||||
/**
|
||||
* Mock embedding generation utilities for testing
|
||||
*/
|
||||
|
||||
/**
|
||||
* Generate a deterministic normalized vector based on text content
|
||||
* Uses a simple hash-based approach to ensure same text produces same vector
|
||||
*/
|
||||
export const generateMockEmbedding = (text: string, dimension: number = 1536): number[] => {
|
||||
// Simple hash function to generate seed from text
|
||||
let hash = 0;
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const char = text.charCodeAt(i);
|
||||
hash = (hash << 5) - hash + char;
|
||||
hash = hash & hash; // Convert to 32-bit integer
|
||||
}
|
||||
|
||||
// Generate vector using seeded random
|
||||
const vector: number[] = [];
|
||||
let seed = Math.abs(hash);
|
||||
for (let i = 0; i < dimension; i++) {
|
||||
// Linear congruential generator
|
||||
seed = (seed * 1103515245 + 12345) & 0x7fffffff;
|
||||
vector.push((seed / 0x7fffffff) * 2 - 1); // Range [-1, 1]
|
||||
}
|
||||
|
||||
// Normalize the vector (L2 norm = 1)
|
||||
const norm = Math.sqrt(vector.reduce((sum, val) => sum + val * val, 0));
|
||||
return vector.map((val) => val / norm);
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate multiple mock embeddings for a list of texts
|
||||
*/
|
||||
export const generateMockEmbeddings = (texts: string[], dimension: number = 1536): number[][] => {
|
||||
return texts.map((text) => generateMockEmbedding(text, dimension));
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a mock response for getVectorsByText
|
||||
*/
|
||||
export const createMockVectorsResponse = (
|
||||
texts: string | string[],
|
||||
dimension: number = 1536
|
||||
): { tokens: number; vectors: number[][] } => {
|
||||
const textArray = Array.isArray(texts) ? texts : [texts];
|
||||
const vectors = generateMockEmbeddings(textArray, dimension);
|
||||
|
||||
// Estimate tokens (roughly 1 token per 4 characters)
|
||||
const tokens = textArray.reduce((sum, text) => sum + Math.ceil(text.length / 4), 0);
|
||||
|
||||
return { tokens, vectors };
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate a vector similar to another vector with controlled similarity
|
||||
* @param baseVector - The base vector to create similarity from
|
||||
* @param similarity - Target cosine similarity (0-1), higher means more similar
|
||||
*/
|
||||
export const generateSimilarVector = (baseVector: number[], similarity: number = 0.9): number[] => {
|
||||
const dimension = baseVector.length;
|
||||
const noise = generateMockEmbedding(`noise_${Date.now()}_${Math.random()}`, dimension);
|
||||
|
||||
// Interpolate between base vector and noise
|
||||
const vector = baseVector.map((val, i) => val * similarity + noise[i] * (1 - similarity));
|
||||
|
||||
// Normalize
|
||||
const norm = Math.sqrt(vector.reduce((sum, val) => sum + val * val, 0));
|
||||
return vector.map((val) => val / norm);
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate a vector orthogonal (dissimilar) to the given vector
|
||||
*/
|
||||
export const generateOrthogonalVector = (baseVector: number[]): number[] => {
|
||||
const dimension = baseVector.length;
|
||||
const randomVector = generateMockEmbedding(`orthogonal_${Date.now()}`, dimension);
|
||||
|
||||
// Gram-Schmidt orthogonalization
|
||||
const dotProduct = baseVector.reduce((sum, val, i) => sum + val * randomVector[i], 0);
|
||||
const vector = randomVector.map((val, i) => val - dotProduct * baseVector[i]);
|
||||
|
||||
// Normalize
|
||||
const norm = Math.sqrt(vector.reduce((sum, val) => sum + val * val, 0));
|
||||
return vector.map((val) => val / norm);
|
||||
};
|
||||
|
||||
/**
|
||||
* Mock implementation for getVectorsByText
|
||||
* Automatically generates embeddings based on input text
|
||||
*/
|
||||
export const mockGetVectorsByText = vi.fn(
|
||||
async ({
|
||||
input,
|
||||
type
|
||||
}: {
|
||||
model: any;
|
||||
input: string[] | string;
|
||||
type?: string;
|
||||
}): Promise<{ tokens: number; vectors: number[][] }> => {
|
||||
const texts = Array.isArray(input) ? input : [input];
|
||||
return createMockVectorsResponse(texts);
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Setup global mock for embedding module
|
||||
*/
|
||||
vi.mock('@fastgpt/service/core/ai/embedding', async (importOriginal) => {
|
||||
const actual = (await importOriginal()) as any;
|
||||
return {
|
||||
...actual,
|
||||
getVectorsByText: mockGetVectorsByText
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* Setup global mock for AI model module
|
||||
*/
|
||||
vi.mock('@fastgpt/service/core/ai/model', async (importOriginal) => {
|
||||
const actual = (await importOriginal()) as any;
|
||||
return {
|
||||
...actual,
|
||||
getEmbeddingModel: vi.fn().mockReturnValue({
|
||||
model: 'text-embedding-ada-002',
|
||||
name: 'text-embedding-ada-002'
|
||||
})
|
||||
};
|
||||
});
|
||||
139
test/mocks/core/ai/llm.ts
Normal file
139
test/mocks/core/ai/llm.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
import { vi } from 'vitest';
|
||||
import type { ChatCompletion } from '@fastgpt/global/core/ai/type';
|
||||
|
||||
/**
|
||||
* Mock LLM response utilities for testing
|
||||
*/
|
||||
|
||||
/**
|
||||
* Create a mock non-streaming response with reason and text
|
||||
* This simulates a complete response from models that support reasoning (like o1)
|
||||
*/
|
||||
export const createMockCompleteResponseWithReason = (options?: {
|
||||
content?: string;
|
||||
reasoningContent?: string;
|
||||
finishReason?: 'stop' | 'length' | 'content_filter';
|
||||
promptTokens?: number;
|
||||
completionTokens?: number;
|
||||
}): ChatCompletion => {
|
||||
const {
|
||||
content = 'This is the answer to your question.',
|
||||
reasoningContent = 'First, I need to analyze the question...',
|
||||
finishReason = 'stop',
|
||||
promptTokens = 100,
|
||||
completionTokens = 50
|
||||
} = options || {};
|
||||
|
||||
return {
|
||||
id: `chatcmpl-${Date.now()}`,
|
||||
object: 'chat.completion',
|
||||
created: Math.floor(Date.now() / 1000),
|
||||
model: 'gpt-4o',
|
||||
choices: [
|
||||
{
|
||||
index: 0,
|
||||
message: {
|
||||
role: 'assistant',
|
||||
content,
|
||||
reasoning_content: reasoningContent,
|
||||
refusal: null
|
||||
} as any,
|
||||
logprobs: null,
|
||||
finish_reason: finishReason
|
||||
}
|
||||
],
|
||||
usage: {
|
||||
prompt_tokens: promptTokens,
|
||||
completion_tokens: completionTokens,
|
||||
total_tokens: promptTokens + completionTokens
|
||||
},
|
||||
system_fingerprint: 'fp_test'
|
||||
} as ChatCompletion;
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a mock non-streaming response with tool calls
|
||||
* This simulates a response where the model decides to call tools/functions
|
||||
*/
|
||||
export const createMockCompleteResponseWithTool = (options?: {
|
||||
toolCalls?: Array<{
|
||||
id?: string;
|
||||
name: string;
|
||||
arguments: string | Record<string, any>;
|
||||
}>;
|
||||
finishReason?: 'tool_calls' | 'stop';
|
||||
promptTokens?: number;
|
||||
completionTokens?: number;
|
||||
}): ChatCompletion => {
|
||||
const {
|
||||
toolCalls = [
|
||||
{
|
||||
id: 'call_test_001',
|
||||
name: 'get_weather',
|
||||
arguments: { location: 'Beijing', unit: 'celsius' }
|
||||
}
|
||||
],
|
||||
finishReason = 'tool_calls',
|
||||
promptTokens = 120,
|
||||
completionTokens = 30
|
||||
} = options || {};
|
||||
|
||||
return {
|
||||
id: `chatcmpl-${Date.now()}`,
|
||||
object: 'chat.completion',
|
||||
created: Math.floor(Date.now() / 1000),
|
||||
model: 'gpt-4o',
|
||||
choices: [
|
||||
{
|
||||
index: 0,
|
||||
message: {
|
||||
role: 'assistant',
|
||||
content: null,
|
||||
refusal: null,
|
||||
tool_calls: toolCalls.map((call, index) => ({
|
||||
id: call.id || `call_${Date.now()}_${index}`,
|
||||
type: 'function' as const,
|
||||
function: {
|
||||
name: call.name,
|
||||
arguments:
|
||||
typeof call.arguments === 'string' ? call.arguments : JSON.stringify(call.arguments)
|
||||
}
|
||||
}))
|
||||
},
|
||||
logprobs: null,
|
||||
finish_reason: finishReason
|
||||
}
|
||||
],
|
||||
usage: {
|
||||
prompt_tokens: promptTokens,
|
||||
completion_tokens: completionTokens,
|
||||
total_tokens: promptTokens + completionTokens
|
||||
},
|
||||
system_fingerprint: 'fp_test'
|
||||
} as ChatCompletion;
|
||||
};
|
||||
|
||||
/**
|
||||
* Mock implementation for createChatCompletion
|
||||
* Can be configured to return different types of responses based on test needs
|
||||
*/
|
||||
export const mockCreateChatCompletion = vi.fn(
|
||||
async (body: any, options?: any): Promise<ChatCompletion> => {
|
||||
// Default: return response with text
|
||||
if (body.tools && body.tools.length > 0) {
|
||||
return createMockCompleteResponseWithTool();
|
||||
}
|
||||
return createMockCompleteResponseWithReason();
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Setup global mock for LLM request module
|
||||
*/
|
||||
vi.mock('@fastgpt/service/core/ai/llm/request', async (importOriginal) => {
|
||||
const actual = (await importOriginal()) as any;
|
||||
return {
|
||||
...actual,
|
||||
createChatCompletion: mockCreateChatCompletion
|
||||
};
|
||||
});
|
||||
@@ -1,127 +1,13 @@
|
||||
import { vi } from 'vitest';
|
||||
import './request';
|
||||
|
||||
vi.mock('@fastgpt/service/support/audit/util', async (importOriginal) => {
|
||||
const actual = (await importOriginal()) as any;
|
||||
return {
|
||||
...actual,
|
||||
addAuditLog: vi.fn()
|
||||
};
|
||||
});
|
||||
|
||||
// Mock Redis connections to prevent connection errors in tests
|
||||
vi.mock('@fastgpt/service/common/redis', async (importOriginal) => {
|
||||
const actual = (await importOriginal()) as any;
|
||||
|
||||
// Create a mock Redis client
|
||||
const mockRedisClient = {
|
||||
on: vi.fn(),
|
||||
connect: vi.fn().mockResolvedValue(undefined),
|
||||
disconnect: vi.fn().mockResolvedValue(undefined),
|
||||
keys: vi.fn().mockResolvedValue([]),
|
||||
get: vi.fn().mockResolvedValue(null),
|
||||
set: vi.fn().mockResolvedValue('OK'),
|
||||
del: vi.fn().mockResolvedValue(1),
|
||||
exists: vi.fn().mockResolvedValue(0),
|
||||
expire: vi.fn().mockResolvedValue(1),
|
||||
ttl: vi.fn().mockResolvedValue(-1)
|
||||
};
|
||||
|
||||
return {
|
||||
...actual,
|
||||
newQueueRedisConnection: vi.fn(() => mockRedisClient),
|
||||
newWorkerRedisConnection: vi.fn(() => mockRedisClient),
|
||||
getGlobalRedisConnection: vi.fn(() => mockRedisClient)
|
||||
};
|
||||
});
|
||||
|
||||
// Mock BullMQ to prevent queue connection errors
|
||||
vi.mock('@fastgpt/service/common/bullmq', async (importOriginal) => {
|
||||
const actual = (await importOriginal()) as any;
|
||||
|
||||
const mockQueue = {
|
||||
add: vi.fn().mockResolvedValue({ id: '1' }),
|
||||
close: vi.fn().mockResolvedValue(undefined),
|
||||
on: vi.fn()
|
||||
};
|
||||
|
||||
const mockWorker = {
|
||||
close: vi.fn().mockResolvedValue(undefined),
|
||||
on: vi.fn()
|
||||
};
|
||||
|
||||
return {
|
||||
...actual,
|
||||
getQueue: vi.fn(() => mockQueue),
|
||||
getWorker: vi.fn(() => mockWorker)
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('@fastgpt/service/common/s3/buckets/base', async (importOriginal) => {
|
||||
const actual = (await importOriginal()) as any;
|
||||
|
||||
class MockS3BaseBucket {
|
||||
private _bucket: string;
|
||||
public options: any;
|
||||
|
||||
constructor(bucket: string, afterInits?: any, options?: any) {
|
||||
this._bucket = bucket;
|
||||
this.options = options || {};
|
||||
}
|
||||
|
||||
get name(): string {
|
||||
return this._bucket;
|
||||
}
|
||||
|
||||
get client(): any {
|
||||
return {
|
||||
bucketExists: vi.fn().mockResolvedValue(true),
|
||||
makeBucket: vi.fn().mockResolvedValue(undefined),
|
||||
setBucketPolicy: vi.fn().mockResolvedValue(undefined),
|
||||
copyObject: vi.fn().mockResolvedValue(undefined),
|
||||
removeObject: vi.fn().mockResolvedValue(undefined),
|
||||
presignedPostPolicy: vi.fn().mockResolvedValue({
|
||||
postURL: 'http://localhost:9000/mock-bucket',
|
||||
formData: { key: 'mock-key' }
|
||||
}),
|
||||
newPostPolicy: vi.fn(() => ({
|
||||
setKey: vi.fn(),
|
||||
setBucket: vi.fn(),
|
||||
setContentType: vi.fn(),
|
||||
setContentLengthRange: vi.fn(),
|
||||
setExpires: vi.fn(),
|
||||
setUserMetaData: vi.fn()
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
||||
move(src: string, dst: string, options?: any): Promise<void> {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
copy(src: string, dst: string, options?: any): any {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
exist(): Promise<boolean> {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
delete(objectKey: string, options?: any): Promise<void> {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
async createPostPresignedUrl(params: any, options?: any): Promise<any> {
|
||||
const key = `mock/${params.teamId}/${params.filename}`;
|
||||
return {
|
||||
url: 'http://localhost:9000/mock-bucket',
|
||||
fields: { key }
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...actual,
|
||||
S3BaseBucket: MockS3BaseBucket
|
||||
};
|
||||
});
|
||||
import './common/geo';
|
||||
import './common/mongo';
|
||||
import './common/redis';
|
||||
import './common/bullmq';
|
||||
import './common/s3';
|
||||
import './common/system';
|
||||
import './common/vector';
|
||||
import './common/tracks';
|
||||
import './support/audit/utils';
|
||||
import './support/user/auth/controller';
|
||||
import './core/ai/embedding';
|
||||
import './core/ai/llm';
|
||||
|
||||
@@ -5,15 +5,8 @@ import { MongoGroupMemberModel } from '@fastgpt/service/support/permission/membe
|
||||
import { getTmbInfoByTmbId } from '@fastgpt/service/support/user/team/controller';
|
||||
import { vi } from 'vitest';
|
||||
|
||||
// vi.mock(import('@/service/middleware/entry'), async () => {
|
||||
// const NextAPI = vi.fn((handler: any) => handler);
|
||||
// return {
|
||||
// NextAPI
|
||||
// };
|
||||
// });
|
||||
|
||||
vi.mock(import('@fastgpt/service/common/middle/entry'), async (importOriginal) => {
|
||||
const mod = await importOriginal();
|
||||
vi.mock('@fastgpt/service/common/middle/entry', async (importOriginal) => {
|
||||
const mod = (await importOriginal()) as any;
|
||||
const NextEntry = vi.fn(({ beforeCallback = [] }: { beforeCallback?: Promise<any>[] }) => {
|
||||
return (...args: any) => {
|
||||
return async function api(req: any, res: any) {
|
||||
@@ -67,8 +60,8 @@ export type MockReqType<B = any, Q = any> = {
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
vi.mock(import('@fastgpt/service/support/permission/auth/common'), async (importOriginal) => {
|
||||
const mod = await importOriginal();
|
||||
vi.mock('@fastgpt/service/support/permission/auth/common', async (importOriginal) => {
|
||||
const mod = (await importOriginal()) as any;
|
||||
const parseHeaderCert = vi.fn(
|
||||
({
|
||||
req,
|
||||
@@ -98,68 +91,69 @@ vi.mock(import('@fastgpt/service/support/permission/auth/common'), async (import
|
||||
canWrite: true
|
||||
};
|
||||
};
|
||||
|
||||
const setCookie = vi.fn();
|
||||
|
||||
return {
|
||||
...mod,
|
||||
parseHeaderCert,
|
||||
authCert
|
||||
authCert,
|
||||
setCookie
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock(
|
||||
import('@fastgpt/service/support/permission/memberGroup/controllers'),
|
||||
async (importOriginal) => {
|
||||
const mod = await importOriginal();
|
||||
const parseHeaderCert = vi.fn(
|
||||
({
|
||||
req,
|
||||
authToken = false,
|
||||
authRoot = false,
|
||||
authApiKey = false
|
||||
}: {
|
||||
req: MockReqType;
|
||||
authToken?: boolean;
|
||||
authRoot?: boolean;
|
||||
authApiKey?: boolean;
|
||||
}) => {
|
||||
const { auth } = req;
|
||||
if (!auth) {
|
||||
return Promise.reject(Error('unAuthorization(mock)'));
|
||||
}
|
||||
return Promise.resolve(auth);
|
||||
vi.mock('@fastgpt/service/support/permission/memberGroup/controllers', async (importOriginal) => {
|
||||
const mod = (await importOriginal()) as any;
|
||||
const parseHeaderCert = vi.fn(
|
||||
({
|
||||
req,
|
||||
authToken = false,
|
||||
authRoot = false,
|
||||
authApiKey = false
|
||||
}: {
|
||||
req: MockReqType;
|
||||
authToken?: boolean;
|
||||
authRoot?: boolean;
|
||||
authApiKey?: boolean;
|
||||
}) => {
|
||||
const { auth } = req;
|
||||
if (!auth) {
|
||||
return Promise.reject(Error('unAuthorization(mock)'));
|
||||
}
|
||||
);
|
||||
const authGroupMemberRole = vi.fn(async ({ groupId, role, ...props }: any) => {
|
||||
const result = await parseHeaderCert(props);
|
||||
const { teamId, tmbId, isRoot } = result;
|
||||
if (isRoot) {
|
||||
return {
|
||||
...result,
|
||||
permission: new TeamPermission({
|
||||
isOwner: true
|
||||
}),
|
||||
teamId,
|
||||
tmbId
|
||||
};
|
||||
}
|
||||
const [groupMember, tmb] = await Promise.all([
|
||||
MongoGroupMemberModel.findOne({ groupId, tmbId }),
|
||||
getTmbInfoByTmbId({ tmbId })
|
||||
]);
|
||||
return Promise.resolve(auth);
|
||||
}
|
||||
);
|
||||
const authGroupMemberRole = vi.fn(async ({ groupId, role, ...props }: any) => {
|
||||
const result = await parseHeaderCert(props);
|
||||
const { teamId, tmbId, isRoot } = result;
|
||||
if (isRoot) {
|
||||
return {
|
||||
...result,
|
||||
permission: new TeamPermission({
|
||||
isOwner: true
|
||||
}),
|
||||
teamId,
|
||||
tmbId
|
||||
};
|
||||
}
|
||||
const [groupMember, tmb] = await Promise.all([
|
||||
MongoGroupMemberModel.findOne({ groupId, tmbId }),
|
||||
getTmbInfoByTmbId({ tmbId })
|
||||
]);
|
||||
|
||||
// Team admin or role check
|
||||
if (tmb.permission.hasManagePer || (groupMember && role.includes(groupMember.role))) {
|
||||
return {
|
||||
...result,
|
||||
permission: tmb.permission,
|
||||
teamId,
|
||||
tmbId
|
||||
};
|
||||
}
|
||||
return Promise.reject(TeamErrEnum.unAuthTeam);
|
||||
});
|
||||
return {
|
||||
...mod,
|
||||
authGroupMemberRole
|
||||
};
|
||||
}
|
||||
);
|
||||
// Team admin or role check
|
||||
if (tmb.permission.hasManagePer || (groupMember && role.includes(groupMember.role))) {
|
||||
return {
|
||||
...result,
|
||||
permission: tmb.permission,
|
||||
teamId,
|
||||
tmbId
|
||||
};
|
||||
}
|
||||
return Promise.reject(TeamErrEnum.unAuthTeam);
|
||||
});
|
||||
return {
|
||||
...mod,
|
||||
authGroupMemberRole
|
||||
};
|
||||
});
|
||||
|
||||
9
test/mocks/support/audit/utils.ts
Normal file
9
test/mocks/support/audit/utils.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { vi } from 'vitest';
|
||||
|
||||
vi.mock('@fastgpt/service/support/user/audit/util', async (importOriginal) => {
|
||||
const actual = (await importOriginal()) as any;
|
||||
return {
|
||||
...actual,
|
||||
addAuditLog: vi.fn()
|
||||
};
|
||||
});
|
||||
10
test/mocks/support/user/auth/controller.ts
Normal file
10
test/mocks/support/user/auth/controller.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { vi } from 'vitest';
|
||||
|
||||
// Mock auth code validation
|
||||
vi.mock('@fastgpt/service/support/user/auth/controller', async (importOriginal) => {
|
||||
const actual = (await importOriginal()) as any;
|
||||
return {
|
||||
...actual,
|
||||
authCode: vi.fn().mockResolvedValue(true)
|
||||
};
|
||||
});
|
||||
@@ -5,57 +5,15 @@ import { initGlobalVariables } from '@/service/common/system';
|
||||
import { afterAll, beforeAll, beforeEach, inject, onTestFinished, vi } from 'vitest';
|
||||
import setupModels from './setupModels';
|
||||
import { clean } from './datas/users';
|
||||
import type { Mongoose } from '@fastgpt/service/common/mongo';
|
||||
import { connectionLogMongo, connectionMongo } from '@fastgpt/service/common/mongo';
|
||||
import { randomUUID } from 'crypto';
|
||||
import { delay } from '@fastgpt/global/common/system/utils';
|
||||
|
||||
vi.stubEnv('NODE_ENV', 'test');
|
||||
|
||||
vi.mock(import('@fastgpt/service/common/mongo/init'), async (importOriginal: any) => {
|
||||
const mod = await importOriginal();
|
||||
return {
|
||||
...mod,
|
||||
connectMongo: async (db: Mongoose, url: string) => {
|
||||
await db.connect(url, { dbName: randomUUID() });
|
||||
await db.connection.db?.dropDatabase();
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock(import('@/service/common/system'), async (importOriginal) => {
|
||||
const mod = await importOriginal();
|
||||
return {
|
||||
...mod,
|
||||
getSystemVersion: async () => {
|
||||
return '0.0.0';
|
||||
},
|
||||
readConfigData: async () => {
|
||||
return readFileSync('projects/app/data/config.json', 'utf-8');
|
||||
},
|
||||
initSystemConfig: async () => {
|
||||
// read env from projects/app/.env
|
||||
|
||||
const str = readFileSync('projects/app/.env.local', 'utf-8');
|
||||
const lines = str.split('\n');
|
||||
const systemEnv: Record<string, string> = {};
|
||||
for (const line of lines) {
|
||||
const [key, value] = line.split('=');
|
||||
if (key && value) {
|
||||
systemEnv[key] = value;
|
||||
}
|
||||
}
|
||||
global.systemEnv = systemEnv as any;
|
||||
|
||||
return;
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
beforeAll(async () => {
|
||||
vi.stubEnv('MONGODB_URI', inject('MONGODB_URI'));
|
||||
await connectMongo(connectionMongo, inject('MONGODB_URI'));
|
||||
await connectMongo(connectionLogMongo, inject('MONGODB_URI'));
|
||||
await connectMongo({ db: connectionMongo, url: inject('MONGODB_URI') });
|
||||
await connectMongo({ db: connectionLogMongo, url: inject('MONGODB_URI') });
|
||||
|
||||
initGlobalVariables();
|
||||
global.systemEnv = {} as any;
|
||||
@@ -86,16 +44,27 @@ afterAll(async () => {
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await connectMongo(connectionMongo, inject('MONGODB_URI'));
|
||||
await connectMongo(connectionLogMongo, inject('MONGODB_URI'));
|
||||
await connectMongo({ db: connectionMongo, url: inject('MONGODB_URI') });
|
||||
await connectMongo({ db: connectionLogMongo, url: inject('MONGODB_URI') });
|
||||
|
||||
onTestFinished(async () => {
|
||||
clean();
|
||||
await delay(200); // wait for asynchronous operations to complete
|
||||
await Promise.all([
|
||||
connectionMongo?.connection.db?.dropDatabase(),
|
||||
connectionLogMongo?.connection.db?.dropDatabase()
|
||||
]);
|
||||
// Wait for any ongoing transactions and operations to complete
|
||||
await delay(500);
|
||||
|
||||
// Ensure all sessions are closed before dropping database
|
||||
try {
|
||||
await Promise.all([
|
||||
connectionMongo?.connection.db?.dropDatabase(),
|
||||
connectionLogMongo?.connection.db?.dropDatabase()
|
||||
]);
|
||||
} catch (error) {
|
||||
// Ignore errors during cleanup
|
||||
console.warn('Error during test cleanup:', error);
|
||||
}
|
||||
|
||||
// Additional delay to prevent lock contention between tests
|
||||
await delay(100);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
18
test/test.ts
18
test/test.ts
@@ -1,18 +0,0 @@
|
||||
import { MongoUser } from '@fastgpt/service/support/user/schema';
|
||||
import { it, expect } from 'vitest';
|
||||
|
||||
it('should be a test', async () => {
|
||||
expect(1).toBe(1);
|
||||
});
|
||||
|
||||
it('should be able to connect to mongo', async () => {
|
||||
expect(global.mongodb).toBeDefined();
|
||||
expect(global.mongodb?.connection.readyState).toBe(1);
|
||||
await MongoUser.create({
|
||||
username: 'test',
|
||||
password: '123456'
|
||||
});
|
||||
const user = await MongoUser.findOne({ username: 'test' });
|
||||
expect(user).toBeDefined();
|
||||
expect(user?.username).toBe('test');
|
||||
});
|
||||
Reference in New Issue
Block a user