Skip to content

Commit 91f37f5

Browse files
authored
Merge pull request #4 from Serverless-Devs/feat-custom-sandbox
feat(sandbox): add custom sandbox support with enhanced test coverage
2 parents 696b8ea + 95679fb commit 91f37f5

16 files changed

Lines changed: 3689 additions & 23 deletions

jest.config.js

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -26,30 +26,31 @@ export default {
2626
'src/**/*.ts',
2727
'!src/**/*.d.ts',
2828
'!src/**/index.ts',
29-
// Exclude auto-generated API control files
29+
// Exclude auto-generated API control files (thin wrappers, auto-generated)
3030
'!src/**/api/control.ts',
31-
// Exclude server and sandbox modules (as per project requirements)
31+
// Exclude server module (requires HTTP server testing infrastructure)
3232
'!src/server/**',
33-
'!src/sandbox/**',
34-
// Exclude integration modules
33+
// Exclude integration modules (external service dependencies)
3534
'!src/integration/**',
3635
// Exclude low-level HTTP client (requires complex network mocking)
3736
'!src/utils/data-api.ts',
38-
// Exclude API data layer (thin wrappers around data-api)
39-
'!src/**/api/data.ts',
40-
// Exclude OpenAPI parser (requires complex HTTP/schema mocking)
41-
'!src/toolset/openapi.ts',
42-
'!src/toolset/api/openapi.ts',
37+
// Exclude sandbox data API layer (requires network mocking, similar to data-api.ts)
38+
'!src/sandbox/api/*.ts',
39+
// Exclude sandbox subclasses that heavily depend on Data API (aio, browser, code-interpreter)
40+
'!src/sandbox/aio-sandbox.ts',
41+
'!src/sandbox/browser-sandbox.ts',
42+
'!src/sandbox/code-interpreter-sandbox.ts',
43+
// Exclude sandbox.ts and client.ts (complex API response handling and templateType branching logic)
44+
'!src/sandbox/sandbox.ts',
45+
'!src/sandbox/client.ts',
4346
// Exclude MCP adapter (requires external MCP server)
4447
'!src/toolset/api/mcp.ts',
45-
// Exclude agent-runtime model (codeFromFile requires complex fs/archiver mocking)
46-
'!src/agent-runtime/model.ts',
47-
// Exclude logging utilities (complex stack frame parsing, not core business logic)
48-
'!src/utils/log.ts',
49-
// Exclude toolset.ts (contains complex external SDK client creation and OpenAPI/MCP invocation)
48+
// Exclude OpenAPI parser (complex HTTP/schema mocking, partially covered)
49+
'!src/toolset/openapi.ts',
50+
// Exclude toolset.ts (complex external SDK client creation and OpenAPI/MCP invocation, 86% covered)
5051
'!src/toolset/toolset.ts',
51-
// Exclude agent-runtime client.ts (invokeOpenai method requires Data API client)
52-
'!src/agent-runtime/client.ts',
52+
// Exclude logging utilities (complex stack frame parsing, 72% covered)
53+
'!src/utils/log.ts',
5354
],
5455
coverageDirectory: 'coverage',
5556
coverageReporters: ['text', 'lcov', 'json'],

src/sandbox/client.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ export class SandboxClient {
174174
input: request,
175175
config: cfg,
176176
});
177-
return (result.items || []).map((item) => new Template(item, cfg));
177+
return (result?.items || []).map((item) => new Template(item, cfg));
178178
};
179179

180180
// ============ Sandbox Operations ============

src/sandbox/custom-sandbox.ts

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/**
2+
* Custom Sandbox
3+
*
4+
* 自定义镜像沙箱 / Custom Image Sandbox
5+
*/
6+
7+
import { Config } from "../utils/config";
8+
9+
import {
10+
NASConfig,
11+
OSSMountConfig,
12+
PolarFsConfig,
13+
TemplateType,
14+
} from "./model";
15+
import { Sandbox } from "./sandbox";
16+
import { SandboxDataAPI } from "./api/sandbox-data";
17+
18+
/**
19+
* Custom Sandbox
20+
*
21+
* 自定义镜像沙箱类 / Custom Image Sandbox Class
22+
*/
23+
export class CustomSandbox extends Sandbox {
24+
static templateType = TemplateType.CUSTOM;
25+
26+
/**
27+
* Create a Custom Sandbox from template
28+
* 从模板创建自定义沙箱 / Create Custom Sandbox from Template
29+
*/
30+
static async createFromTemplate(
31+
templateName: string,
32+
options?: {
33+
sandboxIdleTimeoutSeconds?: number;
34+
nasConfig?: NASConfig;
35+
ossMountConfig?: OSSMountConfig;
36+
polarFsConfig?: PolarFsConfig;
37+
},
38+
config?: Config
39+
): Promise<CustomSandbox> {
40+
const sandbox = await Sandbox.create(
41+
{
42+
templateName,
43+
sandboxIdleTimeoutSeconds: options?.sandboxIdleTimeoutSeconds,
44+
nasConfig: options?.nasConfig,
45+
ossMountConfig: options?.ossMountConfig,
46+
polarFsConfig: options?.polarFsConfig,
47+
},
48+
config
49+
);
50+
51+
const customSandbox = new CustomSandbox(sandbox, config);
52+
return customSandbox;
53+
}
54+
55+
constructor(sandbox: Sandbox, config?: Config) {
56+
super(sandbox, config);
57+
}
58+
59+
private _dataApi?: SandboxDataAPI;
60+
61+
/**
62+
* Get data API client
63+
*/
64+
get dataApi(): SandboxDataAPI {
65+
if (!this._dataApi) {
66+
this._dataApi = new SandboxDataAPI({
67+
sandboxId: this.sandboxId || "",
68+
config: this._config,
69+
});
70+
}
71+
return this._dataApi;
72+
}
73+
74+
/**
75+
* Get base URL for the sandbox
76+
* 获取沙箱的基础 URL / Get base URL for the sandbox
77+
*
78+
* @returns 基础 URL / Base URL
79+
*/
80+
getBaseUrl(): string {
81+
const cfg = Config.withConfigs(this._config);
82+
return `${cfg.dataEndpoint}/sandboxes/${this.sandboxId}`;
83+
}
84+
}

src/sandbox/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export { Template } from "./template";
88
export { CodeInterpreterSandbox } from "./code-interpreter-sandbox";
99
export { BrowserSandbox } from "./browser-sandbox";
1010
export { AioSandbox } from "./aio-sandbox";
11+
export { CustomSandbox } from "./custom-sandbox";
1112

1213
// Data API exports
1314
export { SandboxDataAPI } from "./api/sandbox-data";

src/sandbox/model.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ export enum TemplateType {
1414
CODE_INTERPRETER = "CodeInterpreter",
1515
BROWSER = "Browser",
1616
AIO = "AllInOne",
17+
/**
18+
* 自定义镜像 / Custom Image
19+
*/
20+
CUSTOM = "CustomImage",
1721
}
1822

1923
/**
@@ -250,6 +254,18 @@ export interface TemplateArmsConfiguration {
250254
export interface TemplateContainerConfiguration {
251255
image?: string;
252256
command?: string[];
257+
/**
258+
* ACR 实例 ID / ACR Instance ID
259+
*/
260+
acrInstanceId?: string;
261+
/**
262+
* 镜像注册表类型 / Image Registry Type
263+
*/
264+
imageRegistryType?: string;
265+
/**
266+
* 端口 / Port
267+
*/
268+
port?: number;
253269
}
254270

255271
/**

src/sandbox/sandbox.ts

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
* This module defines the base Sandbox resource class.
66
*/
77

8+
import { ClientError, HTTPError } from '@/utils';
89
import { Config } from '../utils/config';
910
import { ResourceBase, updateObjectProperties } from '../utils/resource';
1011

@@ -15,6 +16,7 @@ import {
1516
SandboxState,
1617
TemplateType,
1718
} from './model';
19+
import { SandboxDataAPI } from './api/sandbox-data';
1820

1921
/**
2022
* Base Sandbox resource class
@@ -159,7 +161,71 @@ export class Sandbox extends ResourceBase implements SandboxData {
159161
config?: Config;
160162
}): Promise<Sandbox> {
161163
const { id, templateType, config } = params;
162-
return await Sandbox.getClient().getSandbox({ id, templateType, config });
164+
try {
165+
const cfg = Config.withConfigs(config);
166+
167+
// Use Data API to get sandbox
168+
const dataApi = new SandboxDataAPI({
169+
sandboxId: id,
170+
config: cfg,
171+
});
172+
173+
const result = await dataApi.getSandbox({
174+
sandboxId: id,
175+
config: cfg,
176+
});
177+
178+
// Check if get was successful
179+
if (result.code !== 'SUCCESS') {
180+
throw new ClientError(
181+
0,
182+
`Failed to get sandbox: ${result.message || 'Unknown error'}`
183+
);
184+
}
185+
186+
// Extract data and create Sandbox instance
187+
const data = result.data || {};
188+
const baseSandbox = Sandbox.fromInnerObject(data as any, config);
189+
190+
// If templateType is specified, return the appropriate subclass
191+
if (templateType) {
192+
// Dynamically import to avoid circular dependencies
193+
switch (templateType) {
194+
case TemplateType.CODE_INTERPRETER: {
195+
const { CodeInterpreterSandbox } =
196+
await import('./code-interpreter-sandbox');
197+
// Pass baseSandbox instead of raw data
198+
const sandbox = new CodeInterpreterSandbox(baseSandbox, config);
199+
return sandbox;
200+
}
201+
case TemplateType.BROWSER: {
202+
const { BrowserSandbox } = await import('./browser-sandbox');
203+
// Pass baseSandbox instead of raw data
204+
const sandbox = new BrowserSandbox(baseSandbox, config);
205+
return sandbox;
206+
}
207+
case TemplateType.AIO: {
208+
const { AioSandbox } = await import('./aio-sandbox');
209+
// Pass baseSandbox instead of raw data
210+
const sandbox = new AioSandbox(baseSandbox, config);
211+
return sandbox;
212+
}
213+
case TemplateType.CUSTOM: {
214+
const { CustomSandbox } = await import('./custom-sandbox');
215+
// Pass baseSandbox instead of raw data
216+
const sandbox = new CustomSandbox(baseSandbox, config);
217+
return sandbox;
218+
}
219+
}
220+
}
221+
222+
return baseSandbox;
223+
} catch (error) {
224+
if (error instanceof HTTPError) {
225+
throw error.toResourceError('Sandbox', id);
226+
}
227+
throw error;
228+
}
163229
}
164230

165231
/**

0 commit comments

Comments
 (0)