mirror of
https://github.com/labring/FastGPT.git
synced 2026-05-07 01:02:55 +08:00
V4.14.9 dev (#6555)
* feat: encapsulate logger (#6535) * feat: encapsulate logger * update engines --------- Co-authored-by: archer <545436317@qq.com> * next config * dev shell * Agent sandbox (#6532) * docs: switch to docs layout and apply black theme (#6533) * feat: add Gemini 3.1 models - Add gemini-3.1-pro-preview (released February 19, 2026) - Add gemini-3.1-flash-lite-preview (released March 3, 2026) Both models support: - 1M context window - 64k max response - Vision - Tool choice * docs: switch to docs layout and apply black theme - Change layout from notebook to docs - Update logo to icon + text format - Apply fumadocs black theme - Simplify global.css (keep only navbar and TOC styles) - Fix icon components to properly accept className props - Add mobile text overflow handling - Update Node engine requirement to >=20.x * doc * doc * lock * fix: ts * doc * doc --------- Co-authored-by: archer <archer@archerdeMac-mini.local> Co-authored-by: archer <545436317@qq.com> * Doc (#6493) * cloud doc * doc refactor * doc move * seo * remove doc * yml * doc * fix: tsconfig * fix: tsconfig * sandbox version (#6497) * sandbox version * add sandbox log * update lock * fix * fix: sandbox * doc * add console * i18n * sandbxo in agent * feat: agent sandbox * lock * feat: sandbox ui * sandbox check exists * env tempalte * doc * lock * sandbox in chat window * sandbox entry * fix: test * rename var * sandbox config tip * update sandbox lifecircle * update prompt * rename provider test * sandbox logger * yml --------- Co-authored-by: Archer <archer@fastgpt.io> Co-authored-by: archer <archer@archerdeMac-mini.local> * perf: sandbox error tip * Add sandbox limit and fix some issue (#6550) * sandbox in plan * fix: some issue * fix: test * editor default path * fix: comment * perf: sandbox worksapce * doc * perf: del sandbox * sandbox build * fix: test * fix: pr comment --------- Co-authored-by: Ryo <whoeverimf5@gmail.com> Co-authored-by: Archer <archer@fastgpt.io> Co-authored-by: archer <archer@archerdeMac-mini.local>
This commit is contained in:
@@ -2,17 +2,15 @@ import { describe, test, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
import { isInternalAddress } from '@fastgpt/service/common/system/utils';
|
||||
|
||||
// Mock dns module
|
||||
vi.mock('node:dns/promises', () => ({
|
||||
vi.mock('dns/promises', () => ({
|
||||
default: {
|
||||
resolve4: vi.fn(),
|
||||
resolve6: vi.fn()
|
||||
},
|
||||
resolve4: vi.fn(),
|
||||
resolve6: vi.fn()
|
||||
}
|
||||
}));
|
||||
|
||||
// Import mocked dns after mock setup
|
||||
import * as dns from 'node:dns/promises';
|
||||
import dns from 'dns/promises';
|
||||
|
||||
describe('SSRF Protection - isInternalAddress', () => {
|
||||
const originalEnv = process.env.CHECK_INTERNAL_IP;
|
||||
|
||||
@@ -48,7 +48,16 @@ vi.mock('@fastgpt/service/core/ai/llm/promptCall', () => ({
|
||||
}));
|
||||
|
||||
vi.mock('@fastgpt/global/core/ai/llm/utils', () => ({
|
||||
removeDatasetCiteText: vi.fn((text: string) => text)
|
||||
removeDatasetCiteText: vi.fn((text: string) => text),
|
||||
getLLMSupportParams: vi.fn(() => ({
|
||||
vision: false,
|
||||
temperature: true,
|
||||
reasoning: false,
|
||||
topP: true,
|
||||
stop: true,
|
||||
responseFormat: false,
|
||||
supportToolCall: true
|
||||
}))
|
||||
}));
|
||||
|
||||
vi.mock('@fastgpt/service/core/ai/utils', () => ({
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { generateSandboxId } from '@fastgpt/global/core/ai/sandbox/constants';
|
||||
|
||||
describe('generateSandboxId', () => {
|
||||
it('should return same ID for same triplet', () => {
|
||||
const id1 = generateSandboxId('app1', 'user1', 'chat1');
|
||||
const id2 = generateSandboxId('app1', 'user1', 'chat1');
|
||||
expect(id1).toBe(id2);
|
||||
});
|
||||
|
||||
it('should return different IDs for different triplets', () => {
|
||||
const id1 = generateSandboxId('app1', 'user1', 'chat1');
|
||||
const id2 = generateSandboxId('app1', 'user1', 'chat2');
|
||||
const id3 = generateSandboxId('app2', 'user1', 'chat1');
|
||||
expect(id1).not.toBe(id2);
|
||||
expect(id1).not.toBe(id3);
|
||||
expect(id2).not.toBe(id3);
|
||||
});
|
||||
|
||||
it('should return a 16-char hex string', () => {
|
||||
const id = generateSandboxId('app1', 'user1', 'chat1');
|
||||
expect(id).toHaveLength(16);
|
||||
expect(id).toMatch(/^[0-9a-f]{16}$/);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,228 @@
|
||||
import { describe, it, expect, beforeEach, beforeAll, vi } from 'vitest';
|
||||
|
||||
// Mock the env module BEFORE any imports that use it
|
||||
vi.mock('@fastgpt/service/env', () => ({
|
||||
env: {
|
||||
AGENT_SANDBOX_PROVIDER: 'sealosdevbox',
|
||||
AGENT_SANDBOX_SEALOS_BASEURL: 'http://mock-sandbox.local',
|
||||
AGENT_SANDBOX_SEALOS_TOKEN: 'mock-token-12345'
|
||||
}
|
||||
}));
|
||||
|
||||
// Mock the SealosDevboxAdapter to avoid real API calls
|
||||
vi.mock('@fastgpt-sdk/sandbox-adapter', () => {
|
||||
class MockSealosDevboxAdapter {
|
||||
async create() {
|
||||
return undefined;
|
||||
}
|
||||
async start() {
|
||||
return undefined;
|
||||
}
|
||||
async stop() {
|
||||
return undefined;
|
||||
}
|
||||
async delete() {
|
||||
return undefined;
|
||||
}
|
||||
async getInfo() {
|
||||
return null;
|
||||
}
|
||||
async execute() {
|
||||
return { stdout: 'ok', stderr: '', exitCode: 0 };
|
||||
}
|
||||
async waitUntilReady() {
|
||||
return undefined;
|
||||
}
|
||||
async ensureRunning() {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
SealosDevboxAdapter: MockSealosDevboxAdapter
|
||||
};
|
||||
});
|
||||
|
||||
import { connectionMongo } from '@fastgpt/service/common/mongo';
|
||||
import { MongoSandboxInstance } from '@fastgpt/service/core/ai/sandbox/schema';
|
||||
import { SandboxStatusEnum } from '@fastgpt/global/core/ai/sandbox/constants';
|
||||
import {
|
||||
deleteSandboxesByChatIds,
|
||||
deleteSandboxesByAppId
|
||||
} from '@fastgpt/service/core/ai/sandbox/controller';
|
||||
|
||||
const { Types } = connectionMongo;
|
||||
const oid = () => String(new Types.ObjectId());
|
||||
|
||||
beforeAll(async () => {
|
||||
vi.clearAllMocks();
|
||||
await MongoSandboxInstance.deleteMany({});
|
||||
});
|
||||
|
||||
const appId1 = oid();
|
||||
const appId2 = oid();
|
||||
|
||||
describe('deleteSandboxesByChatIds', () => {
|
||||
beforeEach(async () => {
|
||||
await MongoSandboxInstance.create([
|
||||
{
|
||||
provider: 'sealosdevbox',
|
||||
sandboxId: 'sb1',
|
||||
appId: appId1,
|
||||
userId: 'u1',
|
||||
chatId: 'c1',
|
||||
status: 'running',
|
||||
lastActiveAt: new Date(),
|
||||
createdAt: new Date()
|
||||
},
|
||||
{
|
||||
provider: 'sealosdevbox',
|
||||
sandboxId: 'sb2',
|
||||
appId: appId1,
|
||||
userId: 'u1',
|
||||
chatId: 'c2',
|
||||
status: 'running',
|
||||
lastActiveAt: new Date(),
|
||||
createdAt: new Date()
|
||||
},
|
||||
{
|
||||
provider: 'sealosdevbox',
|
||||
sandboxId: 'sb3',
|
||||
appId: appId2,
|
||||
userId: 'u1',
|
||||
chatId: 'c3',
|
||||
status: 'running',
|
||||
lastActiveAt: new Date(),
|
||||
createdAt: new Date()
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
it('should call delete for specified chatIds', async () => {
|
||||
const countBefore = await MongoSandboxInstance.countDocuments({ appId: appId1 });
|
||||
expect(countBefore).toBe(2);
|
||||
|
||||
await deleteSandboxesByChatIds({ appId: appId1, chatIds: ['c1', 'c2'] });
|
||||
|
||||
// 验证不影响其他 appId 的数据
|
||||
expect(await MongoSandboxInstance.countDocuments({ appId: appId2 })).toBe(1);
|
||||
});
|
||||
|
||||
it('should not error when chatId does not exist', async () => {
|
||||
await expect(
|
||||
deleteSandboxesByChatIds({ appId: appId1, chatIds: ['nonexistent'] })
|
||||
).resolves.not.toThrow();
|
||||
});
|
||||
|
||||
it('should handle empty chatIds array', async () => {
|
||||
await expect(deleteSandboxesByChatIds({ appId: appId1, chatIds: [] })).resolves.not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteSandboxesByAppId', () => {
|
||||
beforeEach(async () => {
|
||||
await MongoSandboxInstance.create([
|
||||
{
|
||||
provider: 'sealosdevbox',
|
||||
sandboxId: 'sb1',
|
||||
appId: appId1,
|
||||
userId: 'u1',
|
||||
chatId: 'c1',
|
||||
status: 'running',
|
||||
lastActiveAt: new Date(),
|
||||
createdAt: new Date()
|
||||
},
|
||||
{
|
||||
provider: 'sealosdevbox',
|
||||
sandboxId: 'sb2',
|
||||
appId: appId1,
|
||||
userId: 'u1',
|
||||
chatId: 'c2',
|
||||
status: 'stoped',
|
||||
lastActiveAt: new Date(),
|
||||
createdAt: new Date()
|
||||
},
|
||||
{
|
||||
provider: 'sealosdevbox',
|
||||
sandboxId: 'sb3',
|
||||
appId: appId2,
|
||||
userId: 'u1',
|
||||
chatId: 'c3',
|
||||
status: 'running',
|
||||
lastActiveAt: new Date(),
|
||||
createdAt: new Date()
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
it('should call delete for all sandboxes under appId', async () => {
|
||||
const countBefore = await MongoSandboxInstance.countDocuments({ appId: appId1 });
|
||||
expect(countBefore).toBe(2);
|
||||
|
||||
await deleteSandboxesByAppId(appId1);
|
||||
|
||||
// 验证不影响其他 appId 的数据
|
||||
expect(await MongoSandboxInstance.countDocuments({ appId: appId2 })).toBe(1);
|
||||
});
|
||||
|
||||
it('should not error when appId has no sandboxes', async () => {
|
||||
const emptyAppId = oid();
|
||||
await expect(deleteSandboxesByAppId(emptyAppId)).resolves.not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('cronJob - suspendInactiveSandboxes', () => {
|
||||
it('should identify running sandboxes inactive > 5 min', async () => {
|
||||
const old = new Date(Date.now() - 10 * 60 * 1000);
|
||||
const recent = new Date();
|
||||
|
||||
await MongoSandboxInstance.create([
|
||||
{
|
||||
provider: 'sealosdevbox',
|
||||
sandboxId: 'old1',
|
||||
appId: appId1,
|
||||
userId: 'u',
|
||||
chatId: 'c1',
|
||||
status: 'running',
|
||||
lastActiveAt: old,
|
||||
createdAt: old
|
||||
},
|
||||
{
|
||||
provider: 'sealosdevbox',
|
||||
sandboxId: 'recent1',
|
||||
appId: appId1,
|
||||
userId: 'u',
|
||||
chatId: 'c2',
|
||||
status: 'running',
|
||||
lastActiveAt: recent,
|
||||
createdAt: recent
|
||||
},
|
||||
{
|
||||
provider: 'sealosdevbox',
|
||||
sandboxId: 'already',
|
||||
appId: appId1,
|
||||
userId: 'u',
|
||||
chatId: 'c3',
|
||||
status: 'stoped',
|
||||
lastActiveAt: old,
|
||||
createdAt: old
|
||||
}
|
||||
]);
|
||||
|
||||
// 模拟定时任务的查询逻辑
|
||||
const instances = await MongoSandboxInstance.find({
|
||||
status: SandboxStatusEnum.running,
|
||||
lastActiveAt: { $lt: new Date(Date.now() - 5 * 60 * 1000) }
|
||||
}).lean();
|
||||
|
||||
// 验证查询逻辑正确:只找到超过 5 分钟未活动的 running 状态沙盒
|
||||
expect(instances).toHaveLength(1);
|
||||
expect(instances[0].sandboxId).toBe('old1');
|
||||
|
||||
// 验证不包含最近活动的沙盒
|
||||
expect(instances.find((i) => i.sandboxId === 'recent1')).toBeUndefined();
|
||||
|
||||
// 验证不包含已停止的沙盒
|
||||
expect(instances.find((i) => i.sandboxId === 'already')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,314 @@
|
||||
import { describe, it, expect, afterAll, beforeAll, vi } from 'vitest';
|
||||
import { MongoSandboxInstance } from '@fastgpt/service/core/ai/sandbox/schema';
|
||||
import {
|
||||
SandboxClient,
|
||||
deleteSandboxesByChatIds,
|
||||
deleteSandboxesByAppId
|
||||
} from '@fastgpt/service/core/ai/sandbox/controller';
|
||||
import { connectionMongo } from '@fastgpt/service/common/mongo';
|
||||
import { SandboxStatusEnum } from '@fastgpt/global/core/ai/sandbox/constants';
|
||||
import { delay } from '@fastgpt/global/common/system/utils';
|
||||
|
||||
const { Types } = connectionMongo;
|
||||
|
||||
const hasSandboxEnv = !!(
|
||||
process.env.AGENT_SANDBOX_PROVIDER &&
|
||||
process.env.AGENT_SANDBOX_SEALOS_BASEURL &&
|
||||
process.env.AGENT_SANDBOX_SEALOS_TOKEN
|
||||
);
|
||||
vi.mock('@fastgpt/service/env', () => ({
|
||||
env: {
|
||||
AGENT_SANDBOX_PROVIDER: process.env.AGENT_SANDBOX_PROVIDER,
|
||||
AGENT_SANDBOX_SEALOS_BASEURL: process.env.AGENT_SANDBOX_SEALOS_BASEURL,
|
||||
AGENT_SANDBOX_SEALOS_TOKEN: process.env.AGENT_SANDBOX_SEALOS_TOKEN
|
||||
}
|
||||
}));
|
||||
|
||||
describe.skipIf(!hasSandboxEnv).sequential('Sandbox Integration', () => {
|
||||
const testDir = '/tmp/workspace';
|
||||
const testParams = {
|
||||
appId: String(new Types.ObjectId()),
|
||||
userId: 'integration-user',
|
||||
chatId: `integration-chat-${Date.now()}`
|
||||
};
|
||||
let sandbox: SandboxClient;
|
||||
|
||||
// 测试开始前,确认 workspace 存在
|
||||
beforeAll(async () => {
|
||||
sandbox = new SandboxClient(testParams);
|
||||
const result = await sandbox.exec(`mkdir -p ${testDir} && cd ${testDir}`);
|
||||
expect(result.exitCode).toBe(0);
|
||||
await delay(2000);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
// 清理测试创建的沙盒实例
|
||||
try {
|
||||
await sandbox.delete();
|
||||
} catch (error) {
|
||||
console.warn('Failed to cleanup sandbox:', error);
|
||||
}
|
||||
});
|
||||
|
||||
it('should create sandbox and execute echo command', async () => {
|
||||
const result = await sandbox.exec('echo hello');
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.stdout).toContain('hello');
|
||||
});
|
||||
|
||||
it('should return non-zero exitCode for failing command', async () => {
|
||||
const result = await sandbox.exec('exit 1');
|
||||
expect(result.exitCode).not.toBe(0);
|
||||
});
|
||||
|
||||
it('should share filesystem within same session', async () => {
|
||||
await sandbox.exec(`touch ${testDir}/test-integration.txt`);
|
||||
const result = await sandbox.exec(`ls ${testDir}/test-integration.txt`);
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.stdout).toContain('test-integration.txt');
|
||||
});
|
||||
|
||||
it('should delete sandbox and clean DB on deleteSandboxesByChatIds', async () => {
|
||||
await sandbox.exec('echo setup');
|
||||
await deleteSandboxesByChatIds({ appId: testParams.appId, chatIds: [testParams.chatId] });
|
||||
|
||||
const count = await MongoSandboxInstance.countDocuments({ chatId: testParams.chatId });
|
||||
expect(count).toBe(0);
|
||||
});
|
||||
|
||||
// ===== 错误处理和边界情况 =====
|
||||
describe('Error Handling', () => {
|
||||
it('should handle command timeout gracefully', async () => {
|
||||
// 超时会抛出异常而不是返回错误码
|
||||
await expect(sandbox.exec('sleep 3', 1)).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('should handle invalid commands', async () => {
|
||||
const result = await sandbox.exec('nonexistent-command-xyz');
|
||||
expect(result.exitCode).not.toBe(0);
|
||||
expect(result.stderr).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should handle empty command', async () => {
|
||||
// 空命令在某些沙盒实现中可能失败,改为测试 true 命令
|
||||
const result = await sandbox.exec('true');
|
||||
expect(result.exitCode).toBe(0);
|
||||
});
|
||||
|
||||
it('should handle very long output', async () => {
|
||||
const result = await sandbox.exec('seq 1 10000');
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.stdout.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
// ===== 状态管理测试 =====
|
||||
describe('State Management', () => {
|
||||
it('should update status to running after exec', async () => {
|
||||
await sandbox.exec('echo test');
|
||||
|
||||
const doc = await MongoSandboxInstance.findOne({ chatId: testParams.chatId });
|
||||
expect(doc?.status).toBe(SandboxStatusEnum.running);
|
||||
expect(doc?.lastActiveAt).toBeDefined();
|
||||
});
|
||||
|
||||
it('should stop sandbox and update status', async () => {
|
||||
await sandbox.exec('echo test');
|
||||
await sandbox.stop();
|
||||
|
||||
const doc = await MongoSandboxInstance.findOne({ chatId: testParams.chatId });
|
||||
expect(doc?.status).toBe(SandboxStatusEnum.stoped);
|
||||
});
|
||||
|
||||
it('should update lastActiveAt on each exec', async () => {
|
||||
await sandbox.exec('echo first');
|
||||
|
||||
const firstDoc = await MongoSandboxInstance.findOne({ chatId: testParams.chatId });
|
||||
const firstTime = firstDoc?.lastActiveAt;
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
await sandbox.exec('echo second');
|
||||
|
||||
const secondDoc = await MongoSandboxInstance.findOne({ chatId: testParams.chatId });
|
||||
const secondTime = secondDoc?.lastActiveAt;
|
||||
|
||||
expect(secondTime?.getTime()).toBeGreaterThan(firstTime?.getTime() || 0);
|
||||
});
|
||||
|
||||
it('should persist sandbox metadata correctly', async () => {
|
||||
await sandbox.exec('echo test');
|
||||
|
||||
const doc = await MongoSandboxInstance.findOne({ chatId: testParams.chatId });
|
||||
expect(String(doc?.appId)).toBe(testParams.appId);
|
||||
expect(doc?.userId).toBe(testParams.userId);
|
||||
expect(doc?.chatId).toBe(testParams.chatId);
|
||||
expect(doc?.createdAt).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
// ===== 批量操作测试 =====
|
||||
describe('Batch Operations', () => {
|
||||
it('should delete multiple sandboxes by chatIds', async () => {
|
||||
const chatId1 = `${testParams.chatId}-1`;
|
||||
const chatId2 = `${testParams.chatId}-2`;
|
||||
|
||||
const sandbox1 = new SandboxClient({ ...testParams, chatId: chatId1 });
|
||||
const sandbox2 = new SandboxClient({ ...testParams, chatId: chatId2 });
|
||||
|
||||
await sandbox1.exec('echo test1');
|
||||
await sandbox2.exec('echo test2');
|
||||
|
||||
await deleteSandboxesByChatIds({
|
||||
appId: testParams.appId,
|
||||
chatIds: [chatId1, chatId2]
|
||||
});
|
||||
|
||||
const count = await MongoSandboxInstance.countDocuments({
|
||||
chatId: { $in: [chatId1, chatId2] }
|
||||
});
|
||||
expect(count).toBe(0);
|
||||
});
|
||||
|
||||
it('should delete all sandboxes by appId', async () => {
|
||||
const chatId1 = `${testParams.chatId}-app-1`;
|
||||
const chatId2 = `${testParams.chatId}-app-2`;
|
||||
|
||||
const sandbox1 = new SandboxClient({ ...testParams, chatId: chatId1 });
|
||||
const sandbox2 = new SandboxClient({ ...testParams, chatId: chatId2 });
|
||||
|
||||
await sandbox1.exec('echo test1');
|
||||
await sandbox2.exec('echo test2');
|
||||
|
||||
await deleteSandboxesByAppId(testParams.appId);
|
||||
|
||||
const count = await MongoSandboxInstance.countDocuments({ appId: testParams.appId });
|
||||
expect(count).toBe(0);
|
||||
});
|
||||
|
||||
it('should handle empty chatIds array gracefully', async () => {
|
||||
await expect(
|
||||
deleteSandboxesByChatIds({ appId: testParams.appId, chatIds: [] })
|
||||
).resolves.not.toThrow();
|
||||
});
|
||||
|
||||
it('should handle non-existent chatIds gracefully', async () => {
|
||||
await expect(
|
||||
deleteSandboxesByChatIds({
|
||||
appId: testParams.appId,
|
||||
chatIds: ['non-existent-chat-id']
|
||||
})
|
||||
).resolves.not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
// ===== 并发和竞态条件 =====
|
||||
describe('Concurrency', () => {
|
||||
it('should handle concurrent exec calls on same sandbox', async () => {
|
||||
// 先确保沙盒已初始化
|
||||
await sandbox.exec('echo init');
|
||||
|
||||
const results = await Promise.all([
|
||||
sandbox.exec('echo test1'),
|
||||
sandbox.exec('echo test2'),
|
||||
sandbox.exec('echo test3')
|
||||
]);
|
||||
|
||||
results.forEach((result) => {
|
||||
expect(result.exitCode).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle concurrent sandbox creation with same chatId', async () => {
|
||||
const sandbox1 = new SandboxClient(testParams);
|
||||
const sandbox2 = new SandboxClient(testParams);
|
||||
|
||||
const results = await Promise.all([sandbox1.exec('echo test1'), sandbox2.exec('echo test2')]);
|
||||
|
||||
results.forEach((result) => {
|
||||
expect(result.exitCode).toBe(0);
|
||||
});
|
||||
|
||||
const count = await MongoSandboxInstance.countDocuments({ chatId: testParams.chatId });
|
||||
expect(count).toBe(1); // 应该只有一个文档
|
||||
});
|
||||
|
||||
it('should handle concurrent delete operations', async () => {
|
||||
await sandbox.exec('echo test');
|
||||
|
||||
await Promise.all([
|
||||
deleteSandboxesByChatIds({ appId: testParams.appId, chatIds: [testParams.chatId] }),
|
||||
deleteSandboxesByChatIds({ appId: testParams.appId, chatIds: [testParams.chatId] })
|
||||
]);
|
||||
|
||||
const count = await MongoSandboxInstance.countDocuments({ chatId: testParams.chatId });
|
||||
expect(count).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
// ===== 文件系统持久化测试 =====
|
||||
describe('Filesystem Persistence', () => {
|
||||
it('should persist files across multiple exec calls', async () => {
|
||||
await sandbox.exec(`echo "content" > ${testDir}/test.txt`);
|
||||
const result1 = await sandbox.exec(`cat ${testDir}/test.txt`);
|
||||
expect(result1.stdout).toContain('content');
|
||||
|
||||
await sandbox.exec(`echo "more" >> ${testDir}/test.txt`);
|
||||
const result2 = await sandbox.exec(`cat ${testDir}/test.txt`);
|
||||
expect(result2.stdout).toContain('content');
|
||||
expect(result2.stdout).toContain('more');
|
||||
});
|
||||
|
||||
it('should handle directory operations', async () => {
|
||||
await sandbox.exec(`touch ${testDir}/file.txt`);
|
||||
const result = await sandbox.exec(`ls ${testDir}`);
|
||||
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.stdout).toContain('file.txt');
|
||||
});
|
||||
|
||||
it('should handle file permissions', async () => {
|
||||
await sandbox.exec(`touch ${testDir}/script.sh`);
|
||||
await sandbox.exec(`chmod +x ${testDir}/script.sh`);
|
||||
await sandbox.exec(`echo "#!/bin/bash\necho executed" > ${testDir}/script.sh`);
|
||||
|
||||
const result = await sandbox.exec(`${testDir}/script.sh`);
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.stdout).toContain('executed');
|
||||
});
|
||||
});
|
||||
|
||||
// ===== 环境变量和工作目录测试 =====
|
||||
describe('Environment and Working Directory', () => {
|
||||
it('should maintain working directory across commands', async () => {
|
||||
await sandbox.exec(`cd ${testDir} && pwd`);
|
||||
const result = await sandbox.exec('pwd');
|
||||
// 注意:每次 exec 可能重置工作目录,这取决于实现
|
||||
expect(result.exitCode).toBe(0);
|
||||
});
|
||||
|
||||
it('should handle environment variables', async () => {
|
||||
const result = await sandbox.exec('export TEST_VAR=hello && echo $TEST_VAR');
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.stdout).toContain('hello');
|
||||
});
|
||||
});
|
||||
|
||||
// ===== 资源限制测试 =====
|
||||
describe('Resource Limits', () => {
|
||||
it('should handle large file creation', async () => {
|
||||
const result = await sandbox.exec(`dd if=/dev/zero of=${testDir}/large.bin bs=1M count=10`);
|
||||
expect(result.exitCode).toBe(0);
|
||||
|
||||
const sizeResult = await sandbox.exec(`ls -lh ${testDir}/large.bin`);
|
||||
expect(sizeResult.stdout).toContain('10M');
|
||||
});
|
||||
|
||||
it('should handle process spawning', async () => {
|
||||
// 先确保沙盒已初始化
|
||||
await sandbox.exec('echo init');
|
||||
|
||||
const result = await sandbox.exec('for i in {1..5}; do echo "process $i" & done; wait');
|
||||
expect(result.exitCode).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user