mirror of
https://github.com/labring/FastGPT.git
synced 2026-01-21 01:15:42 +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:
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)
|
||||
};
|
||||
});
|
||||
Reference in New Issue
Block a user