@cloudflare/sandbox 0.0.0-4572082 → 0.0.0-465ce06

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/dist/index.js CHANGED
@@ -13,7 +13,7 @@ import {
13
13
  UtilityClient,
14
14
  getSandbox,
15
15
  proxyToSandbox
16
- } from "./chunk-EXQOIRZI.js";
16
+ } from "./chunk-SJVEUUYH.js";
17
17
  import {
18
18
  CodeInterpreter,
19
19
  Execution,
@@ -34,6 +34,7 @@ import {
34
34
  parseSSEStream,
35
35
  responseToAsyncIterable
36
36
  } from "./chunk-EKSWCBCA.js";
37
+ import "./chunk-KWOEDJUN.js";
37
38
  export {
38
39
  CodeInterpreter,
39
40
  CommandClient,
@@ -1,5 +1,5 @@
1
1
  import { CreateContextOptions, CodeContext, RunCodeOptions, Execution } from '@repo/shared';
2
- import { b as Sandbox } from './sandbox-D9K2ypln.js';
2
+ import { b as Sandbox } from './sandbox-DMlNr93l.js';
3
3
  import 'cloudflare:workers';
4
4
  import '@cloudflare/containers';
5
5
 
@@ -1,4 +1,4 @@
1
- import { b as Sandbox } from './sandbox-D9K2ypln.js';
1
+ import { b as Sandbox } from './sandbox-DMlNr93l.js';
2
2
  import '@repo/shared';
3
3
  import 'cloudflare:workers';
4
4
  import '@cloudflare/containers';
@@ -1,10 +1,11 @@
1
1
  import {
2
2
  isLocalhostPattern,
3
3
  proxyToSandbox
4
- } from "./chunk-EXQOIRZI.js";
4
+ } from "./chunk-SJVEUUYH.js";
5
5
  import "./chunk-JXZMAU2C.js";
6
6
  import "./chunk-Z532A7QC.js";
7
7
  import "./chunk-EKSWCBCA.js";
8
+ import "./chunk-KWOEDJUN.js";
8
9
  export {
9
10
  isLocalhostPattern,
10
11
  proxyToSandbox
@@ -1,5 +1,5 @@
1
1
  import * as _repo_shared from '@repo/shared';
2
- import { Logger, MkdirResult, WriteFileResult, ReadFileResult, DeleteFileResult, RenameFileResult, MoveFileResult, ListFilesOptions, ListFilesResult, GitCheckoutResult, CreateContextOptions, CodeContext, OutputMessage, Result, ExecutionError, PortExposeResult, PortCloseResult, PortListResult, ProcessStartResult, ProcessListResult, ProcessInfoResult, ProcessKillResult, ProcessCleanupResult, ProcessLogsResult, ISandbox, ExecOptions, ExecResult, ProcessOptions, Process, StreamOptions, SessionOptions, ExecutionSession, RunCodeOptions, ExecutionResult } from '@repo/shared';
2
+ import { Logger, MkdirResult, WriteFileResult, ReadFileResult, DeleteFileResult, RenameFileResult, MoveFileResult, ListFilesOptions, ListFilesResult, GitCheckoutResult, CreateContextOptions, CodeContext, OutputMessage, Result, ExecutionError, PortExposeResult, PortCloseResult, PortListResult, ProcessStartResult, ProcessListResult, ProcessInfoResult, ProcessKillResult, ProcessCleanupResult, ProcessLogsResult, ISandbox, ExecOptions, ExecResult, ProcessOptions, Process, StreamOptions, SessionOptions, ExecutionSession, RunCodeOptions, ExecutionResult, SandboxOptions } from '@repo/shared';
3
3
  import { DurableObject } from 'cloudflare:workers';
4
4
  import { Container } from '@cloudflare/containers';
5
5
 
@@ -418,6 +418,11 @@ declare class UtilityClient extends BaseHttpClient {
418
418
  * @param options - Session configuration (id, env, cwd)
419
419
  */
420
420
  createSession(options: CreateSessionRequest): Promise<CreateSessionResponse>;
421
+ /**
422
+ * Get the container version
423
+ * Returns the version embedded in the Docker image during build
424
+ */
425
+ getVersion(): Promise<string>;
421
426
  }
422
427
 
423
428
  /**
@@ -435,12 +440,10 @@ declare class SandboxClient {
435
440
  constructor(options: HttpClientOptions);
436
441
  }
437
442
 
438
- declare function getSandbox(ns: DurableObjectNamespace<Sandbox>, id: string, options?: {
439
- baseUrl: string;
440
- }): DurableObjectStub<Sandbox<unknown>>;
443
+ declare function getSandbox(ns: DurableObjectNamespace<Sandbox>, id: string, options?: SandboxOptions): DurableObjectStub<Sandbox<unknown>>;
441
444
  declare class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
442
445
  defaultPort: number;
443
- sleepAfter: string;
446
+ sleepAfter: string | number;
444
447
  client: SandboxClient;
445
448
  private codeInterpreter;
446
449
  private sandboxName;
@@ -452,12 +455,18 @@ declare class Sandbox<Env = unknown> extends Container<Env> implements ISandbox
452
455
  constructor(ctx: DurableObject['ctx'], env: Env);
453
456
  setSandboxName(name: string): Promise<void>;
454
457
  setBaseUrl(baseUrl: string): Promise<void>;
458
+ setSleepAfter(sleepAfter: string | number): Promise<void>;
455
459
  setEnvVars(envVars: Record<string, string>): Promise<void>;
456
460
  /**
457
461
  * Cleanup and destroy the sandbox container
458
462
  */
459
463
  destroy(): Promise<void>;
460
464
  onStart(): void;
465
+ /**
466
+ * Check if the container version matches the SDK version
467
+ * Logs a warning if there's a mismatch
468
+ */
469
+ private checkVersionCompatibility;
461
470
  onStop(): void;
462
471
  onError(error: unknown): void;
463
472
  fetch(request: Request): Promise<Response>;
@@ -465,6 +474,10 @@ declare class Sandbox<Env = unknown> extends Container<Env> implements ISandbox
465
474
  /**
466
475
  * Ensure default session exists - lazy initialization
467
476
  * This is called automatically by all public methods that need a session
477
+ *
478
+ * The session is persisted to Durable Object storage to survive hot reloads
479
+ * during development. If a session already exists in the container after reload,
480
+ * we reuse it instead of trying to create a new one.
468
481
  */
469
482
  private ensureDefaultSession;
470
483
  exec(command: string, options?: ExecOptions): Promise<ExecResult>;
package/dist/sandbox.d.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  import '@repo/shared';
2
2
  import 'cloudflare:workers';
3
3
  import '@cloudflare/containers';
4
- export { b as Sandbox, g as getSandbox } from './sandbox-D9K2ypln.js';
4
+ export { b as Sandbox, g as getSandbox } from './sandbox-DMlNr93l.js';
package/dist/sandbox.js CHANGED
@@ -1,10 +1,11 @@
1
1
  import {
2
2
  Sandbox,
3
3
  getSandbox
4
- } from "./chunk-EXQOIRZI.js";
4
+ } from "./chunk-SJVEUUYH.js";
5
5
  import "./chunk-JXZMAU2C.js";
6
6
  import "./chunk-Z532A7QC.js";
7
7
  import "./chunk-EKSWCBCA.js";
8
+ import "./chunk-KWOEDJUN.js";
8
9
  export {
9
10
  Sandbox,
10
11
  getSandbox
@@ -0,0 +1,8 @@
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
+ declare const SDK_VERSION = "0.4.7";
7
+
8
+ export { SDK_VERSION };
@@ -0,0 +1,7 @@
1
+ import {
2
+ SDK_VERSION
3
+ } from "./chunk-KWOEDJUN.js";
4
+ export {
5
+ SDK_VERSION
6
+ };
7
+ //# sourceMappingURL=version.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cloudflare/sandbox",
3
- "version": "0.0.0-4572082",
3
+ "version": "0.0.0-465ce06",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/cloudflare/sandbox-sdk"
@@ -24,9 +24,9 @@
24
24
  "check": "biome check && npm run typecheck",
25
25
  "fix": "biome check --fix && npm run typecheck",
26
26
  "typecheck": "tsc --noEmit",
27
- "docker:local": "cd ../.. && docker build -f packages/sandbox/Dockerfile -t cloudflare/sandbox-test:$npm_package_version .",
28
- "docker:publish": "cd ../.. && docker buildx build --platform linux/amd64,linux/arm64 -f packages/sandbox/Dockerfile -t cloudflare/sandbox:$npm_package_version --push .",
29
- "docker:publish:beta": "cd ../.. && docker buildx build --platform linux/amd64,linux/arm64 -f packages/sandbox/Dockerfile -t cloudflare/sandbox:$npm_package_version-beta --push .",
27
+ "docker:local": "cd ../.. && docker build -f packages/sandbox/Dockerfile --build-arg SANDBOX_VERSION=$npm_package_version -t cloudflare/sandbox-test:$npm_package_version .",
28
+ "docker:publish": "cd ../.. && docker buildx build --platform linux/amd64,linux/arm64 -f packages/sandbox/Dockerfile --build-arg SANDBOX_VERSION=$npm_package_version -t cloudflare/sandbox:$npm_package_version --push .",
29
+ "docker:publish:beta": "cd ../.. && docker buildx build --platform linux/amd64,linux/arm64 -f packages/sandbox/Dockerfile --build-arg SANDBOX_VERSION=$npm_package_version -t cloudflare/sandbox:$npm_package_version-beta --push .",
30
30
  "test": "vitest run --config vitest.config.ts",
31
31
  "test:e2e": "cd ../.. && vitest run --config vitest.e2e.config.ts \"$@\""
32
32
  },
@@ -59,5 +59,6 @@ export type {
59
59
  export type {
60
60
  CommandsResponse,
61
61
  PingResponse,
62
+ VersionResponse,
62
63
  } from './utility-client';
63
64
  export { UtilityClient } from './utility-client';
@@ -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
  }
@@ -105,7 +105,8 @@ export async function proxyToSandbox<E extends SandboxEnv>(
105
105
 
106
106
  function extractSandboxRoute(url: URL): RouteInfo | null {
107
107
  // Parse subdomain pattern: port-sandboxId-token.domain (tokens mandatory)
108
- const subdomainMatch = url.hostname.match(/^(\d{4,5})-([^.-][^.]*[^.-]|[^.-])-([a-zA-Z0-9_-]{12,20})\.(.+)$/);
108
+ // Token is always exactly 16 chars (generated by generatePortToken)
109
+ const subdomainMatch = url.hostname.match(/^(\d{4,5})-([^.-][^.]*?[^.-]|[^.-])-([a-z0-9_-]{16})\.(.+)$/);
109
110
 
110
111
  if (!subdomainMatch) {
111
112
  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(ns: DurableObjectNamespace<Sandbox>, id: string, options?: {
33
- baseUrl: string
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 = "3m"; // Sleep the sandbox if no requests are made in this timeframe
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;
@@ -84,9 +92,10 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
84
92
  // The CodeInterpreter extracts client.interpreter from the sandbox
85
93
  this.codeInterpreter = new CodeInterpreter(this);
86
94
 
87
- // Load the sandbox name and port tokens from storage on initialization
95
+ // Load the sandbox name, port tokens, and default session from storage on initialization
88
96
  this.ctx.blockConcurrencyWhile(async () => {
89
97
  this.sandboxName = await this.ctx.storage.get<string>('sandboxName') || null;
98
+ this.defaultSession = await this.ctx.storage.get<string>('defaultSession') || null;
90
99
  const storedTokens = await this.ctx.storage.get<Record<string, string>>('portTokens') || {};
91
100
 
92
101
  // Convert stored tokens back to Map
@@ -117,6 +126,11 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
117
126
  }
118
127
  }
119
128
 
129
+ // RPC method to set the sleep timeout
130
+ async setSleepAfter(sleepAfter: string | number): Promise<void> {
131
+ this.sleepAfter = sleepAfter;
132
+ }
133
+
120
134
  // RPC method to set environment variables
121
135
  async setEnvVars(envVars: Record<string, string>): Promise<void> {
122
136
  // Update local state for new sessions
@@ -148,6 +162,54 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
148
162
 
149
163
  override onStart() {
150
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
+ }
151
213
  }
152
214
 
153
215
  override onStop() {
@@ -199,20 +261,39 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
199
261
  /**
200
262
  * Ensure default session exists - lazy initialization
201
263
  * This is called automatically by all public methods that need a session
264
+ *
265
+ * The session is persisted to Durable Object storage to survive hot reloads
266
+ * during development. If a session already exists in the container after reload,
267
+ * we reuse it instead of trying to create a new one.
202
268
  */
203
269
  private async ensureDefaultSession(): Promise<string> {
204
270
  if (!this.defaultSession) {
205
271
  const sessionId = `sandbox-${this.sandboxName || 'default'}`;
206
272
 
207
- // Create session in container
208
- await this.client.utils.createSession({
209
- id: sessionId,
210
- env: this.envVars || {},
211
- cwd: '/workspace',
212
- });
213
-
214
- this.defaultSession = sessionId;
215
- this.logger.debug('Default session initialized', { sessionId });
273
+ try {
274
+ // Try to create session in container
275
+ await this.client.utils.createSession({
276
+ id: sessionId,
277
+ env: this.envVars || {},
278
+ cwd: '/workspace',
279
+ });
280
+
281
+ this.defaultSession = sessionId;
282
+ // Persist to storage so it survives hot reloads
283
+ await this.ctx.storage.put('defaultSession', sessionId);
284
+ this.logger.debug('Default session initialized', { sessionId });
285
+ } catch (error: any) {
286
+ // If session already exists (e.g., after hot reload), reuse it
287
+ if (error?.message?.includes('already exists')) {
288
+ this.logger.debug('Reusing existing session after reload', { sessionId });
289
+ this.defaultSession = sessionId;
290
+ // Persist to storage in case it wasn't saved before
291
+ await this.ctx.storage.put('defaultSession', sessionId);
292
+ } else {
293
+ // Re-throw other errors
294
+ throw error;
295
+ }
296
+ }
216
297
  }
217
298
  return this.defaultSession;
218
299
  }
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.7';
@@ -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
+ });
@@ -1,7 +1,8 @@
1
1
  import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
2
  import type {
3
3
  CommandsResponse,
4
- PingResponse
4
+ PingResponse,
5
+ VersionResponse
5
6
  } from '../src/clients';
6
7
  import { UtilityClient } from '../src/clients/utility-client';
7
8
  import {
@@ -25,6 +26,13 @@ const mockCommandsResponse = (commands: string[], overrides: Partial<CommandsRes
25
26
  ...overrides
26
27
  });
27
28
 
29
+ const mockVersionResponse = (version: string = '0.4.5', overrides: Partial<VersionResponse> = {}): VersionResponse => ({
30
+ success: true,
31
+ version,
32
+ timestamp: '2023-01-01T00:00:00Z',
33
+ ...overrides
34
+ });
35
+
28
36
  describe('UtilityClient', () => {
29
37
  let client: UtilityClient;
30
38
  let mockFetch: ReturnType<typeof vi.fn>;
@@ -249,6 +257,64 @@ describe('UtilityClient', () => {
249
257
  });
250
258
  });
251
259
 
260
+ describe('version checking', () => {
261
+ it('should get container version successfully', async () => {
262
+ mockFetch.mockResolvedValue(new Response(
263
+ JSON.stringify(mockVersionResponse('0.4.5')),
264
+ { status: 200 }
265
+ ));
266
+
267
+ const result = await client.getVersion();
268
+
269
+ expect(result).toBe('0.4.5');
270
+ });
271
+
272
+ it('should handle different version formats', async () => {
273
+ const versions = ['1.0.0', '2.5.3-beta', '0.0.1', '10.20.30'];
274
+
275
+ for (const version of versions) {
276
+ mockFetch.mockResolvedValueOnce(new Response(
277
+ JSON.stringify(mockVersionResponse(version)),
278
+ { status: 200 }
279
+ ));
280
+
281
+ const result = await client.getVersion();
282
+ expect(result).toBe(version);
283
+ }
284
+ });
285
+
286
+ it('should return "unknown" when version endpoint does not exist (backward compatibility)', async () => {
287
+ // Simulate 404 or other error for old containers
288
+ mockFetch.mockResolvedValue(new Response(
289
+ JSON.stringify({ error: 'Not Found' }),
290
+ { status: 404 }
291
+ ));
292
+
293
+ const result = await client.getVersion();
294
+
295
+ expect(result).toBe('unknown');
296
+ });
297
+
298
+ it('should return "unknown" on network failure (backward compatibility)', async () => {
299
+ mockFetch.mockRejectedValue(new Error('Network connection failed'));
300
+
301
+ const result = await client.getVersion();
302
+
303
+ expect(result).toBe('unknown');
304
+ });
305
+
306
+ it('should handle version response with unknown value', async () => {
307
+ mockFetch.mockResolvedValue(new Response(
308
+ JSON.stringify(mockVersionResponse('unknown')),
309
+ { status: 200 }
310
+ ));
311
+
312
+ const result = await client.getVersion();
313
+
314
+ expect(result).toBe('unknown');
315
+ });
316
+ });
317
+
252
318
  describe('constructor options', () => {
253
319
  it('should initialize with minimal options', () => {
254
320
  const minimalClient = new UtilityClient();
@@ -0,0 +1,16 @@
1
+ import { describe, expect, test } from 'vitest';
2
+ import packageJson from '../package.json';
3
+ import { SDK_VERSION } from '../src/version';
4
+
5
+ describe('Version Sync', () => {
6
+ test('SDK_VERSION matches package.json version', () => {
7
+ // Verify versions match
8
+ expect(SDK_VERSION).toBe(packageJson.version);
9
+ });
10
+
11
+ test('SDK_VERSION is a valid semver version', () => {
12
+ // Check if version matches semver pattern (major.minor.patch)
13
+ const semverPattern = /^\d+\.\d+\.\d+(-[\w.]+)?$/;
14
+ expect(SDK_VERSION).toMatch(semverPattern);
15
+ });
16
+ });