@cloudflare/sandbox 0.0.0-104f455 → 0.0.0-153e416
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +53 -0
- package/Dockerfile +6 -0
- package/dist/chunk-FE4PJSRB.js +7 -0
- package/dist/chunk-FE4PJSRB.js.map +1 -0
- package/dist/{chunk-2P3MDMNJ.js → chunk-SVWLTRHD.js} +94 -5
- package/dist/chunk-SVWLTRHD.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -1
- package/dist/interpreter.d.ts +1 -1
- package/dist/request-handler.d.ts +1 -1
- package/dist/request-handler.js +2 -1
- package/dist/{sandbox-CZTMzV2R.d.ts → sandbox-DWQVgVTY.d.ts} +21 -5
- package/dist/sandbox.d.ts +1 -1
- package/dist/sandbox.js +2 -1
- package/dist/version.d.ts +8 -0
- package/dist/version.js +7 -0
- package/dist/version.js.map +1 -0
- package/package.json +6 -6
- package/src/clients/file-client.ts +26 -0
- package/src/clients/index.ts +1 -0
- package/src/clients/utility-client.ts +25 -0
- package/src/request-handler.ts +12 -2
- package/src/sandbox.ts +84 -7
- package/src/version.ts +6 -0
- package/tests/file-client.test.ts +76 -0
- package/tests/get-sandbox.test.ts +110 -0
- package/tests/request-handler.test.ts +240 -0
- package/tests/sandbox.test.ts +94 -5
- package/tests/utility-client.test.ts +67 -1
- package/tests/version.test.ts +16 -0
- package/dist/chunk-2P3MDMNJ.js.map +0 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
DeleteFileResult,
|
|
3
|
+
FileExistsResult,
|
|
3
4
|
ListFilesOptions,
|
|
4
5
|
ListFilesResult,
|
|
5
6
|
MkdirResult,
|
|
@@ -266,4 +267,29 @@ export class FileClient extends BaseHttpClient {
|
|
|
266
267
|
throw error;
|
|
267
268
|
}
|
|
268
269
|
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Check if a file or directory exists
|
|
273
|
+
* @param path - Path to check
|
|
274
|
+
* @param sessionId - The session ID for this operation
|
|
275
|
+
*/
|
|
276
|
+
async exists(
|
|
277
|
+
path: string,
|
|
278
|
+
sessionId: string
|
|
279
|
+
): Promise<FileExistsResult> {
|
|
280
|
+
try {
|
|
281
|
+
const data = {
|
|
282
|
+
path,
|
|
283
|
+
sessionId,
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
const response = await this.post<FileExistsResult>('/api/exists', data);
|
|
287
|
+
|
|
288
|
+
this.logSuccess('Path existence checked', `${path} (exists: ${response.exists})`);
|
|
289
|
+
return response;
|
|
290
|
+
} catch (error) {
|
|
291
|
+
this.logError('exists', error);
|
|
292
|
+
throw error;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
269
295
|
}
|
package/src/clients/index.ts
CHANGED
|
@@ -17,6 +17,13 @@ export interface CommandsResponse extends BaseApiResponse {
|
|
|
17
17
|
count: number;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Response interface for getting container version
|
|
22
|
+
*/
|
|
23
|
+
export interface VersionResponse extends BaseApiResponse {
|
|
24
|
+
version: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
20
27
|
/**
|
|
21
28
|
* Request interface for creating sessions
|
|
22
29
|
*/
|
|
@@ -91,4 +98,22 @@ export class UtilityClient extends BaseHttpClient {
|
|
|
91
98
|
throw error;
|
|
92
99
|
}
|
|
93
100
|
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Get the container version
|
|
104
|
+
* Returns the version embedded in the Docker image during build
|
|
105
|
+
*/
|
|
106
|
+
async getVersion(): Promise<string> {
|
|
107
|
+
try {
|
|
108
|
+
const response = await this.get<VersionResponse>('/api/version');
|
|
109
|
+
|
|
110
|
+
this.logSuccess('Version retrieved', response.version);
|
|
111
|
+
return response.version;
|
|
112
|
+
} catch (error) {
|
|
113
|
+
// If version endpoint doesn't exist (old container), return 'unknown'
|
|
114
|
+
// This allows for backward compatibility
|
|
115
|
+
this.logger.debug('Failed to get container version (may be old container)', { error });
|
|
116
|
+
return 'unknown';
|
|
117
|
+
}
|
|
118
|
+
}
|
|
94
119
|
}
|
package/src/request-handler.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createLogger, type LogContext, TraceContext } from "@repo/shared";
|
|
2
|
+
import { switchPort } from "@cloudflare/containers";
|
|
2
3
|
import { getSandbox, type Sandbox } from "./sandbox";
|
|
3
4
|
import {
|
|
4
5
|
sanitizeSandboxId,
|
|
@@ -70,6 +71,14 @@ export async function proxyToSandbox<E extends SandboxEnv>(
|
|
|
70
71
|
}
|
|
71
72
|
}
|
|
72
73
|
|
|
74
|
+
// Detect WebSocket upgrade request
|
|
75
|
+
const upgradeHeader = request.headers.get('Upgrade');
|
|
76
|
+
if (upgradeHeader?.toLowerCase() === 'websocket') {
|
|
77
|
+
// WebSocket path: Must use fetch() not containerFetch()
|
|
78
|
+
// This bypasses JSRPC serialization boundary which cannot handle WebSocket upgrades
|
|
79
|
+
return await sandbox.fetch(switchPort(request, port));
|
|
80
|
+
}
|
|
81
|
+
|
|
73
82
|
// Build proxy request with proper headers
|
|
74
83
|
let proxyUrl: string;
|
|
75
84
|
|
|
@@ -96,7 +105,7 @@ export async function proxyToSandbox<E extends SandboxEnv>(
|
|
|
96
105
|
duplex: 'half',
|
|
97
106
|
});
|
|
98
107
|
|
|
99
|
-
return sandbox.containerFetch(proxyRequest, port);
|
|
108
|
+
return await sandbox.containerFetch(proxyRequest, port);
|
|
100
109
|
} catch (error) {
|
|
101
110
|
logger.error('Proxy routing error', error instanceof Error ? error : new Error(String(error)));
|
|
102
111
|
return new Response('Proxy routing error', { status: 500 });
|
|
@@ -105,7 +114,8 @@ export async function proxyToSandbox<E extends SandboxEnv>(
|
|
|
105
114
|
|
|
106
115
|
function extractSandboxRoute(url: URL): RouteInfo | null {
|
|
107
116
|
// Parse subdomain pattern: port-sandboxId-token.domain (tokens mandatory)
|
|
108
|
-
|
|
117
|
+
// Token is always exactly 16 chars (generated by generatePortToken)
|
|
118
|
+
const subdomainMatch = url.hostname.match(/^(\d{4,5})-([^.-][^.]*?[^.-]|[^.-])-([a-z0-9_-]{16})\.(.+)$/);
|
|
109
119
|
|
|
110
120
|
if (!subdomainMatch) {
|
|
111
121
|
return null;
|
package/src/sandbox.ts
CHANGED
|
@@ -13,6 +13,7 @@ import type {
|
|
|
13
13
|
ProcessOptions,
|
|
14
14
|
ProcessStatus,
|
|
15
15
|
RunCodeOptions,
|
|
16
|
+
SandboxOptions,
|
|
16
17
|
SessionOptions,
|
|
17
18
|
StreamOptions
|
|
18
19
|
} from "@repo/shared";
|
|
@@ -28,25 +29,32 @@ import {
|
|
|
28
29
|
validatePort
|
|
29
30
|
} from "./security";
|
|
30
31
|
import { parseSSEStream } from "./sse-parser";
|
|
32
|
+
import { SDK_VERSION } from "./version";
|
|
31
33
|
|
|
32
|
-
export function getSandbox(
|
|
33
|
-
|
|
34
|
-
|
|
34
|
+
export function getSandbox(
|
|
35
|
+
ns: DurableObjectNamespace<Sandbox>,
|
|
36
|
+
id: string,
|
|
37
|
+
options?: SandboxOptions
|
|
38
|
+
) {
|
|
35
39
|
const stub = getContainer(ns, id);
|
|
36
40
|
|
|
37
41
|
// Store the name on first access
|
|
38
42
|
stub.setSandboxName?.(id);
|
|
39
43
|
|
|
40
|
-
if(options?.baseUrl) {
|
|
44
|
+
if (options?.baseUrl) {
|
|
41
45
|
stub.setBaseUrl(options.baseUrl);
|
|
42
46
|
}
|
|
43
47
|
|
|
48
|
+
if (options?.sleepAfter !== undefined) {
|
|
49
|
+
stub.setSleepAfter(options.sleepAfter);
|
|
50
|
+
}
|
|
51
|
+
|
|
44
52
|
return stub;
|
|
45
53
|
}
|
|
46
54
|
|
|
47
55
|
export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
48
56
|
defaultPort = 3000; // Default port for the container's Bun server
|
|
49
|
-
sleepAfter = "
|
|
57
|
+
sleepAfter: string | number = "10m"; // Sleep the sandbox if no requests are made in this timeframe
|
|
50
58
|
|
|
51
59
|
client: SandboxClient;
|
|
52
60
|
private codeInterpreter: CodeInterpreter;
|
|
@@ -118,6 +126,11 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
118
126
|
}
|
|
119
127
|
}
|
|
120
128
|
|
|
129
|
+
// RPC method to set the sleep timeout
|
|
130
|
+
async setSleepAfter(sleepAfter: string | number): Promise<void> {
|
|
131
|
+
this.sleepAfter = sleepAfter;
|
|
132
|
+
}
|
|
133
|
+
|
|
121
134
|
// RPC method to set environment variables
|
|
122
135
|
async setEnvVars(envVars: Record<string, string>): Promise<void> {
|
|
123
136
|
// Update local state for new sessions
|
|
@@ -149,6 +162,54 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
149
162
|
|
|
150
163
|
override onStart() {
|
|
151
164
|
this.logger.debug('Sandbox started');
|
|
165
|
+
|
|
166
|
+
// Check version compatibility asynchronously (don't block startup)
|
|
167
|
+
this.checkVersionCompatibility().catch(error => {
|
|
168
|
+
this.logger.error('Version compatibility check failed', error instanceof Error ? error : new Error(String(error)));
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Check if the container version matches the SDK version
|
|
174
|
+
* Logs a warning if there's a mismatch
|
|
175
|
+
*/
|
|
176
|
+
private async checkVersionCompatibility(): Promise<void> {
|
|
177
|
+
try {
|
|
178
|
+
// Get the SDK version (imported from version.ts)
|
|
179
|
+
const sdkVersion = SDK_VERSION;
|
|
180
|
+
|
|
181
|
+
// Get container version
|
|
182
|
+
const containerVersion = await this.client.utils.getVersion();
|
|
183
|
+
|
|
184
|
+
// If container version is unknown, it's likely an old container without the endpoint
|
|
185
|
+
if (containerVersion === 'unknown') {
|
|
186
|
+
this.logger.warn(
|
|
187
|
+
'Container version check: Container version could not be determined. ' +
|
|
188
|
+
'This may indicate an outdated container image. ' +
|
|
189
|
+
'Please update your container to match SDK version ' + sdkVersion
|
|
190
|
+
);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Check if versions match
|
|
195
|
+
if (containerVersion !== sdkVersion) {
|
|
196
|
+
const message =
|
|
197
|
+
`Version mismatch detected! SDK version (${sdkVersion}) does not match ` +
|
|
198
|
+
`container version (${containerVersion}). This may cause compatibility issues. ` +
|
|
199
|
+
`Please update your container image to version ${sdkVersion}`;
|
|
200
|
+
|
|
201
|
+
// Log warning - we can't reliably detect dev vs prod environment in Durable Objects
|
|
202
|
+
// so we always use warning level as requested by the user
|
|
203
|
+
this.logger.warn(message);
|
|
204
|
+
} else {
|
|
205
|
+
this.logger.debug('Version check passed', { sdkVersion, containerVersion });
|
|
206
|
+
}
|
|
207
|
+
} catch (error) {
|
|
208
|
+
// Don't fail the sandbox initialization if version check fails
|
|
209
|
+
this.logger.debug('Version compatibility check encountered an error', {
|
|
210
|
+
error: error instanceof Error ? error.message : String(error)
|
|
211
|
+
});
|
|
212
|
+
}
|
|
152
213
|
}
|
|
153
214
|
|
|
154
215
|
override onStop() {
|
|
@@ -177,7 +238,17 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
177
238
|
await this.ctx.storage.put('sandboxName', name);
|
|
178
239
|
}
|
|
179
240
|
|
|
180
|
-
//
|
|
241
|
+
// Detect WebSocket upgrade request
|
|
242
|
+
const upgradeHeader = request.headers.get('Upgrade');
|
|
243
|
+
const isWebSocket = upgradeHeader?.toLowerCase() === 'websocket';
|
|
244
|
+
|
|
245
|
+
if (isWebSocket) {
|
|
246
|
+
// WebSocket path: Let parent Container class handle WebSocket proxying
|
|
247
|
+
// This bypasses containerFetch() which uses JSRPC and cannot handle WebSocket upgrades
|
|
248
|
+
return await super.fetch(request);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Non-WebSocket: Use existing port determination and HTTP routing logic
|
|
181
252
|
const port = this.determinePort(url);
|
|
182
253
|
|
|
183
254
|
// Route to the appropriate port
|
|
@@ -223,7 +294,7 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
223
294
|
this.logger.debug('Default session initialized', { sessionId });
|
|
224
295
|
} catch (error: any) {
|
|
225
296
|
// If session already exists (e.g., after hot reload), reuse it
|
|
226
|
-
if (error?.message?.includes('already exists')
|
|
297
|
+
if (error?.message?.includes('already exists')) {
|
|
227
298
|
this.logger.debug('Reusing existing session after reload', { sessionId });
|
|
228
299
|
this.defaultSession = sessionId;
|
|
229
300
|
// Persist to storage in case it wasn't saved before
|
|
@@ -636,6 +707,11 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
636
707
|
return this.client.files.listFiles(path, session, options);
|
|
637
708
|
}
|
|
638
709
|
|
|
710
|
+
async exists(path: string, sessionId?: string) {
|
|
711
|
+
const session = sessionId ?? await this.ensureDefaultSession();
|
|
712
|
+
return this.client.files.exists(path, session);
|
|
713
|
+
}
|
|
714
|
+
|
|
639
715
|
async exposePort(port: number, options: { name?: string; hostname: string }) {
|
|
640
716
|
// Check if hostname is workers.dev domain (doesn't support wildcard subdomains)
|
|
641
717
|
if (options.hostname.endsWith('.workers.dev')) {
|
|
@@ -873,6 +949,7 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
873
949
|
renameFile: (oldPath, newPath) => this.renameFile(oldPath, newPath, sessionId),
|
|
874
950
|
moveFile: (sourcePath, destPath) => this.moveFile(sourcePath, destPath, sessionId),
|
|
875
951
|
listFiles: (path, options) => this.client.files.listFiles(path, sessionId, options),
|
|
952
|
+
exists: (path) => this.exists(path, sessionId),
|
|
876
953
|
|
|
877
954
|
// Git operations
|
|
878
955
|
gitCheckout: (repoUrl, options) => this.gitCheckout(repoUrl, { ...options, sessionId }),
|
package/src/version.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SDK version - automatically synchronized with package.json by Changesets
|
|
3
|
+
* This file is auto-updated by .github/changeset-version.ts during releases
|
|
4
|
+
* DO NOT EDIT MANUALLY - Changes will be overwritten on the next version bump
|
|
5
|
+
*/
|
|
6
|
+
export const SDK_VERSION = '0.4.11';
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
DeleteFileResult,
|
|
3
|
+
FileExistsResult,
|
|
3
4
|
ListFilesResult,
|
|
4
5
|
MkdirResult,
|
|
5
6
|
MoveFileResult,
|
|
@@ -584,6 +585,81 @@ database:
|
|
|
584
585
|
});
|
|
585
586
|
});
|
|
586
587
|
|
|
588
|
+
describe('exists', () => {
|
|
589
|
+
it('should return true when file exists', async () => {
|
|
590
|
+
const mockResponse: FileExistsResult = {
|
|
591
|
+
success: true,
|
|
592
|
+
path: '/workspace/test.txt',
|
|
593
|
+
exists: true,
|
|
594
|
+
timestamp: '2023-01-01T00:00:00Z',
|
|
595
|
+
};
|
|
596
|
+
|
|
597
|
+
mockFetch.mockResolvedValue(new Response(JSON.stringify(mockResponse), { status: 200 }));
|
|
598
|
+
|
|
599
|
+
const result = await client.exists('/workspace/test.txt', 'session-exists');
|
|
600
|
+
|
|
601
|
+
expect(result.success).toBe(true);
|
|
602
|
+
expect(result.exists).toBe(true);
|
|
603
|
+
expect(result.path).toBe('/workspace/test.txt');
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
it('should return false when file does not exist', async () => {
|
|
607
|
+
const mockResponse: FileExistsResult = {
|
|
608
|
+
success: true,
|
|
609
|
+
path: '/workspace/nonexistent.txt',
|
|
610
|
+
exists: false,
|
|
611
|
+
timestamp: '2023-01-01T00:00:00Z',
|
|
612
|
+
};
|
|
613
|
+
|
|
614
|
+
mockFetch.mockResolvedValue(new Response(JSON.stringify(mockResponse), { status: 200 }));
|
|
615
|
+
|
|
616
|
+
const result = await client.exists('/workspace/nonexistent.txt', 'session-exists');
|
|
617
|
+
|
|
618
|
+
expect(result.success).toBe(true);
|
|
619
|
+
expect(result.exists).toBe(false);
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
it('should return true when directory exists', async () => {
|
|
623
|
+
const mockResponse: FileExistsResult = {
|
|
624
|
+
success: true,
|
|
625
|
+
path: '/workspace/some-dir',
|
|
626
|
+
exists: true,
|
|
627
|
+
timestamp: '2023-01-01T00:00:00Z',
|
|
628
|
+
};
|
|
629
|
+
|
|
630
|
+
mockFetch.mockResolvedValue(new Response(JSON.stringify(mockResponse), { status: 200 }));
|
|
631
|
+
|
|
632
|
+
const result = await client.exists('/workspace/some-dir', 'session-exists');
|
|
633
|
+
|
|
634
|
+
expect(result.success).toBe(true);
|
|
635
|
+
expect(result.exists).toBe(true);
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
it('should send correct request payload', async () => {
|
|
639
|
+
const mockResponse: FileExistsResult = {
|
|
640
|
+
success: true,
|
|
641
|
+
path: '/test/path',
|
|
642
|
+
exists: true,
|
|
643
|
+
timestamp: '2023-01-01T00:00:00Z',
|
|
644
|
+
};
|
|
645
|
+
|
|
646
|
+
mockFetch.mockResolvedValue(new Response(JSON.stringify(mockResponse), { status: 200 }));
|
|
647
|
+
|
|
648
|
+
await client.exists('/test/path', 'session-test');
|
|
649
|
+
|
|
650
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
651
|
+
expect.stringContaining('/api/exists'),
|
|
652
|
+
expect.objectContaining({
|
|
653
|
+
method: 'POST',
|
|
654
|
+
body: JSON.stringify({
|
|
655
|
+
path: '/test/path',
|
|
656
|
+
sessionId: 'session-test',
|
|
657
|
+
})
|
|
658
|
+
})
|
|
659
|
+
);
|
|
660
|
+
});
|
|
661
|
+
});
|
|
662
|
+
|
|
587
663
|
describe('error handling', () => {
|
|
588
664
|
it('should handle network failures gracefully', async () => {
|
|
589
665
|
mockFetch.mockRejectedValue(new Error('Network connection failed'));
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { getSandbox } from '../src/sandbox';
|
|
3
|
+
|
|
4
|
+
// Mock the Container module
|
|
5
|
+
vi.mock('@cloudflare/containers', () => ({
|
|
6
|
+
Container: class Container {
|
|
7
|
+
ctx: any;
|
|
8
|
+
env: any;
|
|
9
|
+
sleepAfter: string | number = '10m';
|
|
10
|
+
constructor(ctx: any, env: any) {
|
|
11
|
+
this.ctx = ctx;
|
|
12
|
+
this.env = env;
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
getContainer: vi.fn(),
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
describe('getSandbox', () => {
|
|
19
|
+
let mockStub: any;
|
|
20
|
+
let mockGetContainer: any;
|
|
21
|
+
|
|
22
|
+
beforeEach(async () => {
|
|
23
|
+
vi.clearAllMocks();
|
|
24
|
+
|
|
25
|
+
// Create a fresh mock stub for each test
|
|
26
|
+
mockStub = {
|
|
27
|
+
sleepAfter: '10m',
|
|
28
|
+
setSandboxName: vi.fn(),
|
|
29
|
+
setBaseUrl: vi.fn(),
|
|
30
|
+
setSleepAfter: vi.fn((value: string | number) => {
|
|
31
|
+
mockStub.sleepAfter = value;
|
|
32
|
+
}),
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// Mock getContainer to return our stub
|
|
36
|
+
const containers = await import('@cloudflare/containers');
|
|
37
|
+
mockGetContainer = vi.mocked(containers.getContainer);
|
|
38
|
+
mockGetContainer.mockReturnValue(mockStub);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should create a sandbox instance with default sleepAfter', () => {
|
|
42
|
+
const mockNamespace = {} as any;
|
|
43
|
+
const sandbox = getSandbox(mockNamespace, 'test-sandbox');
|
|
44
|
+
|
|
45
|
+
expect(sandbox).toBeDefined();
|
|
46
|
+
expect(sandbox.setSandboxName).toHaveBeenCalledWith('test-sandbox');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should apply sleepAfter option when provided as string', () => {
|
|
50
|
+
const mockNamespace = {} as any;
|
|
51
|
+
const sandbox = getSandbox(mockNamespace, 'test-sandbox', {
|
|
52
|
+
sleepAfter: '5m',
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
expect(sandbox.sleepAfter).toBe('5m');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should apply sleepAfter option when provided as number', () => {
|
|
59
|
+
const mockNamespace = {} as any;
|
|
60
|
+
const sandbox = getSandbox(mockNamespace, 'test-sandbox', {
|
|
61
|
+
sleepAfter: 300, // 5 minutes in seconds
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
expect(sandbox.sleepAfter).toBe(300);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should apply baseUrl option when provided', () => {
|
|
68
|
+
const mockNamespace = {} as any;
|
|
69
|
+
const sandbox = getSandbox(mockNamespace, 'test-sandbox', {
|
|
70
|
+
baseUrl: 'https://example.com',
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
expect(sandbox.setBaseUrl).toHaveBeenCalledWith('https://example.com');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should apply both sleepAfter and baseUrl options together', () => {
|
|
77
|
+
const mockNamespace = {} as any;
|
|
78
|
+
const sandbox = getSandbox(mockNamespace, 'test-sandbox', {
|
|
79
|
+
sleepAfter: '10m',
|
|
80
|
+
baseUrl: 'https://example.com',
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
expect(sandbox.sleepAfter).toBe('10m');
|
|
84
|
+
expect(sandbox.setBaseUrl).toHaveBeenCalledWith('https://example.com');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('should not apply sleepAfter when not provided', () => {
|
|
88
|
+
const mockNamespace = {} as any;
|
|
89
|
+
const sandbox = getSandbox(mockNamespace, 'test-sandbox');
|
|
90
|
+
|
|
91
|
+
// Should remain default value from Container
|
|
92
|
+
expect(sandbox.sleepAfter).toBe('10m');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should accept various time string formats for sleepAfter', () => {
|
|
96
|
+
const mockNamespace = {} as any;
|
|
97
|
+
const testCases = ['30s', '1m', '10m', '1h', '2h'];
|
|
98
|
+
|
|
99
|
+
for (const timeString of testCases) {
|
|
100
|
+
// Reset the mock stub for each iteration
|
|
101
|
+
mockStub.sleepAfter = '3m';
|
|
102
|
+
|
|
103
|
+
const sandbox = getSandbox(mockNamespace, `test-sandbox-${timeString}`, {
|
|
104
|
+
sleepAfter: timeString,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
expect(sandbox.sleepAfter).toBe(timeString);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
});
|