@cloudflare/sandbox 0.4.18 → 0.5.2

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.
Files changed (40) hide show
  1. package/.turbo/turbo-build.log +17 -9
  2. package/CHANGELOG.md +64 -0
  3. package/Dockerfile +5 -1
  4. package/LICENSE +176 -0
  5. package/README.md +1 -1
  6. package/dist/dist-gVyG2H2h.js +612 -0
  7. package/dist/dist-gVyG2H2h.js.map +1 -0
  8. package/dist/index.d.ts +94 -1834
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +489 -678
  11. package/dist/index.js.map +1 -1
  12. package/dist/openai/index.d.ts +67 -0
  13. package/dist/openai/index.d.ts.map +1 -0
  14. package/dist/openai/index.js +362 -0
  15. package/dist/openai/index.js.map +1 -0
  16. package/dist/sandbox-B3vJ541e.d.ts +1729 -0
  17. package/dist/sandbox-B3vJ541e.d.ts.map +1 -0
  18. package/package.json +16 -2
  19. package/src/clients/base-client.ts +107 -46
  20. package/src/index.ts +19 -2
  21. package/src/openai/index.ts +465 -0
  22. package/src/request-handler.ts +2 -1
  23. package/src/sandbox.ts +684 -62
  24. package/src/storage-mount/credential-detection.ts +41 -0
  25. package/src/storage-mount/errors.ts +51 -0
  26. package/src/storage-mount/index.ts +17 -0
  27. package/src/storage-mount/provider-detection.ts +93 -0
  28. package/src/storage-mount/types.ts +17 -0
  29. package/src/version.ts +1 -1
  30. package/tests/base-client.test.ts +218 -0
  31. package/tests/get-sandbox.test.ts +24 -1
  32. package/tests/git-client.test.ts +7 -39
  33. package/tests/openai-shell-editor.test.ts +434 -0
  34. package/tests/port-client.test.ts +25 -35
  35. package/tests/process-client.test.ts +73 -107
  36. package/tests/sandbox.test.ts +128 -1
  37. package/tests/storage-mount/credential-detection.test.ts +119 -0
  38. package/tests/storage-mount/provider-detection.test.ts +77 -0
  39. package/tsconfig.json +2 -2
  40. package/tsdown.config.ts +3 -2
@@ -0,0 +1,41 @@
1
+ import type { BucketCredentials, MountBucketOptions } from '@repo/shared';
2
+ import { MissingCredentialsError } from './errors';
3
+
4
+ /**
5
+ * Detect credentials for bucket mounting from environment variables
6
+ * Priority order:
7
+ * 1. Explicit options.credentials
8
+ * 2. Standard AWS env vars: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY
9
+ * 3. Error: no credentials found
10
+ *
11
+ * @param options - Mount options
12
+ * @param envVars - Environment variables
13
+ * @returns Detected credentials
14
+ * @throws MissingCredentialsError if no credentials found
15
+ */
16
+ export function detectCredentials(
17
+ options: MountBucketOptions,
18
+ envVars: Record<string, string | undefined>
19
+ ): BucketCredentials {
20
+ // Priority 1: Explicit credentials in options
21
+ if (options.credentials) {
22
+ return options.credentials;
23
+ }
24
+
25
+ // Priority 2: Standard AWS env vars
26
+ const awsAccessKeyId = envVars.AWS_ACCESS_KEY_ID;
27
+ const awsSecretAccessKey = envVars.AWS_SECRET_ACCESS_KEY;
28
+
29
+ if (awsAccessKeyId && awsSecretAccessKey) {
30
+ return {
31
+ accessKeyId: awsAccessKeyId,
32
+ secretAccessKey: awsSecretAccessKey
33
+ };
34
+ }
35
+
36
+ // No credentials found - throw error with helpful message
37
+ throw new MissingCredentialsError(
38
+ `No credentials found. Set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY ` +
39
+ `environment variables, or pass explicit credentials in options.`
40
+ );
41
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Bucket mounting error classes
3
+ *
4
+ * These are SDK-side validation errors that follow the same pattern as SecurityError.
5
+ * They are thrown before any container interaction occurs.
6
+ */
7
+
8
+ import { ErrorCode } from '@repo/shared/errors';
9
+
10
+ /**
11
+ * Base error for bucket mounting operations
12
+ */
13
+ export class BucketMountError extends Error {
14
+ public readonly code: ErrorCode;
15
+
16
+ constructor(message: string, code: ErrorCode = ErrorCode.BUCKET_MOUNT_ERROR) {
17
+ super(message);
18
+ this.name = 'BucketMountError';
19
+ this.code = code;
20
+ }
21
+ }
22
+
23
+ /**
24
+ * Thrown when S3FS mount command fails
25
+ */
26
+ export class S3FSMountError extends BucketMountError {
27
+ constructor(message: string) {
28
+ super(message, ErrorCode.S3FS_MOUNT_ERROR);
29
+ this.name = 'S3FSMountError';
30
+ }
31
+ }
32
+
33
+ /**
34
+ * Thrown when no credentials found in environment
35
+ */
36
+ export class MissingCredentialsError extends BucketMountError {
37
+ constructor(message: string) {
38
+ super(message, ErrorCode.MISSING_CREDENTIALS);
39
+ this.name = 'MissingCredentialsError';
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Thrown when bucket name, mount path, or options are invalid
45
+ */
46
+ export class InvalidMountConfigError extends BucketMountError {
47
+ constructor(message: string) {
48
+ super(message, ErrorCode.INVALID_MOUNT_CONFIG);
49
+ this.name = 'InvalidMountConfigError';
50
+ }
51
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Bucket mounting functionality
3
+ */
4
+
5
+ export { detectCredentials } from './credential-detection';
6
+ export {
7
+ BucketMountError,
8
+ InvalidMountConfigError,
9
+ MissingCredentialsError,
10
+ S3FSMountError
11
+ } from './errors';
12
+ export {
13
+ detectProviderFromUrl,
14
+ getProviderFlags,
15
+ resolveS3fsOptions
16
+ } from './provider-detection';
17
+ export type { MountInfo } from './types';
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Provider detection and s3fs flag configuration
3
+ *
4
+ * Based on s3fs-fuse documentation:
5
+ * https://github.com/s3fs-fuse/s3fs-fuse/wiki/Non-Amazon-S3
6
+ */
7
+
8
+ import type { BucketProvider } from '@repo/shared';
9
+
10
+ /**
11
+ * Detect provider from endpoint URL using pattern matching
12
+ */
13
+ export function detectProviderFromUrl(endpoint: string): BucketProvider | null {
14
+ try {
15
+ const url = new URL(endpoint);
16
+ const hostname = url.hostname.toLowerCase();
17
+
18
+ if (hostname.endsWith('.r2.cloudflarestorage.com')) {
19
+ return 'r2';
20
+ }
21
+
22
+ // Match AWS S3: *.amazonaws.com or s3.amazonaws.com
23
+ if (
24
+ hostname.endsWith('.amazonaws.com') ||
25
+ hostname === 's3.amazonaws.com'
26
+ ) {
27
+ return 's3';
28
+ }
29
+
30
+ if (hostname === 'storage.googleapis.com') {
31
+ return 'gcs';
32
+ }
33
+
34
+ return null;
35
+ } catch {
36
+ return null;
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Get s3fs flags for a given provider
42
+ *
43
+ * Based on s3fs-fuse wiki recommendations:
44
+ * https://github.com/s3fs-fuse/s3fs-fuse/wiki/Non-Amazon-S3
45
+ */
46
+ export function getProviderFlags(provider: BucketProvider | null): string[] {
47
+ if (!provider) {
48
+ return ['use_path_request_style'];
49
+ }
50
+
51
+ switch (provider) {
52
+ case 'r2':
53
+ return ['nomixupload'];
54
+
55
+ case 's3':
56
+ return [];
57
+
58
+ case 'gcs':
59
+ return [];
60
+
61
+ default:
62
+ return ['use_path_request_style'];
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Resolve s3fs options by combining provider defaults with user overrides
68
+ */
69
+ export function resolveS3fsOptions(
70
+ provider: BucketProvider | null,
71
+ userOptions?: string[]
72
+ ): string[] {
73
+ const providerFlags = getProviderFlags(provider);
74
+
75
+ if (!userOptions || userOptions.length === 0) {
76
+ return providerFlags;
77
+ }
78
+
79
+ // Merge provider flags with user options
80
+ // User options take precedence (come last in the array)
81
+ const allFlags = [...providerFlags, ...userOptions];
82
+
83
+ // Deduplicate flags (keep last occurrence)
84
+ const flagMap = new Map<string, string>();
85
+
86
+ for (const flag of allFlags) {
87
+ // Split on '=' to get the flag name
88
+ const [flagName] = flag.split('=');
89
+ flagMap.set(flagName, flag);
90
+ }
91
+
92
+ return Array.from(flagMap.values());
93
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Internal bucket mounting types
3
+ */
4
+
5
+ import type { BucketProvider } from '@repo/shared';
6
+
7
+ /**
8
+ * Internal tracking information for active mounts
9
+ */
10
+ export interface MountInfo {
11
+ bucket: string;
12
+ mountPath: string;
13
+ endpoint: string;
14
+ provider: BucketProvider | null;
15
+ passwordFilePath: string;
16
+ mounted: boolean;
17
+ }
package/src/version.ts CHANGED
@@ -3,4 +3,4 @@
3
3
  * This file is auto-updated by .github/changeset-version.ts during releases
4
4
  * DO NOT EDIT MANUALLY - Changes will be overwritten on the next version bump
5
5
  */
6
- export const SDK_VERSION = '0.4.18';
6
+ export const SDK_VERSION = '0.5.2';
@@ -361,4 +361,222 @@ describe('BaseHttpClient', () => {
361
361
  }
362
362
  });
363
363
  });
364
+
365
+ describe('container startup retry logic', () => {
366
+ it('should retry 503 errors with "no container instance available"', async () => {
367
+ vi.useFakeTimers();
368
+
369
+ mockFetch
370
+ .mockResolvedValueOnce(
371
+ new Response('Error: There is no container instance available', {
372
+ status: 503
373
+ })
374
+ )
375
+ .mockResolvedValueOnce(
376
+ new Response(JSON.stringify({ success: true, data: 'recovered' }), {
377
+ status: 200
378
+ })
379
+ );
380
+
381
+ const promise = client.testRequest<TestDataResponse>('/api/test');
382
+ await vi.advanceTimersByTimeAsync(5_000);
383
+ const result = await promise;
384
+
385
+ expect(mockFetch).toHaveBeenCalledTimes(2);
386
+ expect(result.success).toBe(true);
387
+ expect(result.data).toBe('recovered');
388
+
389
+ vi.useRealTimers();
390
+ });
391
+
392
+ it('should retry 500 errors with "container port not found"', async () => {
393
+ vi.useFakeTimers();
394
+
395
+ mockFetch
396
+ .mockResolvedValueOnce(
397
+ new Response('Connection refused: container port not found', {
398
+ status: 500
399
+ })
400
+ )
401
+ .mockResolvedValueOnce(
402
+ new Response(JSON.stringify({ success: true }), { status: 200 })
403
+ );
404
+
405
+ const promise = client.testRequest('/api/test');
406
+ await vi.advanceTimersByTimeAsync(5_000);
407
+ const result = await promise;
408
+
409
+ expect(mockFetch).toHaveBeenCalledTimes(2);
410
+ expect(result.success).toBe(true);
411
+
412
+ vi.useRealTimers();
413
+ });
414
+
415
+ it('should retry 500 errors with "the container is not listening"', async () => {
416
+ vi.useFakeTimers();
417
+
418
+ mockFetch
419
+ .mockResolvedValueOnce(
420
+ new Response('Error: the container is not listening on port 3000', {
421
+ status: 500
422
+ })
423
+ )
424
+ .mockResolvedValueOnce(
425
+ new Response(JSON.stringify({ success: true }), { status: 200 })
426
+ );
427
+
428
+ const promise = client.testRequest('/api/test');
429
+ await vi.advanceTimersByTimeAsync(5_000);
430
+ const result = await promise;
431
+
432
+ expect(mockFetch).toHaveBeenCalledTimes(2);
433
+ expect(result.success).toBe(true);
434
+
435
+ vi.useRealTimers();
436
+ });
437
+
438
+ it('should NOT retry 500 errors with "no such image"', async () => {
439
+ mockFetch.mockResolvedValueOnce(
440
+ new Response('Error: no such image: my-container:latest', {
441
+ status: 500
442
+ })
443
+ );
444
+
445
+ await expect(client.testRequest('/api/test')).rejects.toThrow();
446
+ expect(mockFetch).toHaveBeenCalledTimes(1); // No retry
447
+ });
448
+
449
+ it('should NOT retry 500 errors with "container already exists"', async () => {
450
+ mockFetch.mockResolvedValueOnce(
451
+ new Response('Error: container already exists', { status: 500 })
452
+ );
453
+
454
+ await expect(client.testRequest('/api/test')).rejects.toThrow();
455
+ expect(mockFetch).toHaveBeenCalledTimes(1); // No retry
456
+ });
457
+
458
+ it('should NOT retry 500 errors with unknown patterns', async () => {
459
+ mockFetch.mockResolvedValueOnce(
460
+ new Response('Internal server error: database connection failed', {
461
+ status: 500
462
+ })
463
+ );
464
+
465
+ await expect(client.testRequest('/api/test')).rejects.toThrow();
466
+ expect(mockFetch).toHaveBeenCalledTimes(1); // Fail-safe: don't retry
467
+ });
468
+
469
+ it('should NOT retry 404 or other non-500/503 errors', async () => {
470
+ mockFetch.mockResolvedValueOnce(
471
+ new Response('Not found', { status: 404 })
472
+ );
473
+
474
+ await expect(client.testRequest('/api/test')).rejects.toThrow();
475
+ expect(mockFetch).toHaveBeenCalledTimes(1);
476
+ });
477
+
478
+ it('should respect MIN_TIME_FOR_RETRY_MS and stop retrying', async () => {
479
+ vi.useFakeTimers();
480
+
481
+ // Mock responses that would trigger retry
482
+ mockFetch.mockResolvedValue(
483
+ new Response('No container instance available', { status: 503 })
484
+ );
485
+
486
+ const promise = client.testRequest('/api/test');
487
+
488
+ // Fast-forward past retry budget (120s)
489
+ await vi.advanceTimersByTimeAsync(125_000);
490
+
491
+ // Should eventually give up and throw the 503 error
492
+ await expect(promise).rejects.toThrow();
493
+
494
+ vi.useRealTimers();
495
+ });
496
+
497
+ it('should use exponential backoff: 3s, 6s, 12s, 24s, 30s', async () => {
498
+ vi.useFakeTimers();
499
+ const delays: number[] = [];
500
+ let callCount = 0;
501
+
502
+ mockFetch.mockImplementation(async () => {
503
+ delays.push(Date.now());
504
+ callCount++;
505
+ // After 5 attempts, return success to avoid timeout
506
+ if (callCount >= 5) {
507
+ return new Response(JSON.stringify({ success: true }), {
508
+ status: 200
509
+ });
510
+ }
511
+ return new Response('No container instance available', { status: 503 });
512
+ });
513
+
514
+ const promise = client.testRequest('/api/test');
515
+
516
+ // Advance time to allow all retries
517
+ await vi.advanceTimersByTimeAsync(80_000);
518
+
519
+ await promise;
520
+
521
+ // Check delays between attempts (approximately)
522
+ // Attempt 1 at 0ms, Attempt 2 at ~3000ms, Attempt 3 at ~9000ms, etc.
523
+ expect(delays.length).toBeGreaterThanOrEqual(4);
524
+
525
+ vi.useRealTimers();
526
+ });
527
+
528
+ it('should retry multiple transient errors in sequence', async () => {
529
+ vi.useFakeTimers();
530
+
531
+ mockFetch
532
+ .mockResolvedValueOnce(
533
+ new Response('No container instance available', { status: 503 })
534
+ )
535
+ .mockResolvedValueOnce(
536
+ new Response('Container port not found', { status: 500 })
537
+ )
538
+ .mockResolvedValueOnce(
539
+ new Response('The container is not listening', { status: 500 })
540
+ )
541
+ .mockResolvedValueOnce(
542
+ new Response(JSON.stringify({ success: true }), { status: 200 })
543
+ );
544
+
545
+ const promise = client.testRequest('/api/test');
546
+
547
+ // Advance time to allow all retries (3s + 6s + 12s = 21s)
548
+ await vi.advanceTimersByTimeAsync(25_000);
549
+
550
+ const result = await promise;
551
+
552
+ expect(mockFetch).toHaveBeenCalledTimes(4);
553
+ expect(result.success).toBe(true);
554
+
555
+ vi.useRealTimers();
556
+ });
557
+
558
+ it('should handle case-insensitive error matching', async () => {
559
+ vi.useFakeTimers();
560
+
561
+ mockFetch
562
+ .mockResolvedValueOnce(
563
+ new Response('ERROR: CONTAINER PORT NOT FOUND', { status: 500 })
564
+ )
565
+ .mockResolvedValueOnce(
566
+ new Response(JSON.stringify({ success: true }), { status: 200 })
567
+ );
568
+
569
+ const promise = client.testRequest('/api/test');
570
+
571
+ // Advance time for first retry (3s)
572
+ await vi.advanceTimersByTimeAsync(5_000);
573
+
574
+ const result = await promise;
575
+
576
+ expect(mockFetch).toHaveBeenCalledTimes(2);
577
+ expect(result.success).toBe(true);
578
+
579
+ vi.useRealTimers();
580
+ });
581
+ });
364
582
  });
@@ -44,7 +44,10 @@ describe('getSandbox', () => {
44
44
  const sandbox = getSandbox(mockNamespace, 'test-sandbox');
45
45
 
46
46
  expect(sandbox).toBeDefined();
47
- expect(sandbox.setSandboxName).toHaveBeenCalledWith('test-sandbox');
47
+ expect(sandbox.setSandboxName).toHaveBeenCalledWith(
48
+ 'test-sandbox',
49
+ undefined
50
+ );
48
51
  });
49
52
 
50
53
  it('should apply sleepAfter option when provided as string', () => {
@@ -146,4 +149,24 @@ describe('getSandbox', () => {
146
149
  expect(sandbox.setBaseUrl).toHaveBeenCalledWith('https://example.com');
147
150
  expect(sandbox.setKeepAlive).toHaveBeenCalledWith(true);
148
151
  });
152
+
153
+ it('should preserve sandbox ID case by default', () => {
154
+ const mockNamespace = {} as any;
155
+ getSandbox(mockNamespace, 'MyProject-ABC123');
156
+
157
+ expect(mockGetContainer).toHaveBeenCalledWith(
158
+ mockNamespace,
159
+ 'MyProject-ABC123'
160
+ );
161
+ });
162
+
163
+ it('should normalize sandbox ID to lowercase when normalizeId option is true', () => {
164
+ const mockNamespace = {} as any;
165
+ getSandbox(mockNamespace, 'MyProject-ABC123', { normalizeId: true });
166
+
167
+ expect(mockGetContainer).toHaveBeenCalledWith(
168
+ mockNamespace,
169
+ 'myproject-abc123'
170
+ );
171
+ });
149
172
  });
@@ -1,5 +1,5 @@
1
+ import type { GitCheckoutResult } from '@repo/shared';
1
2
  import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
- import type { GitCheckoutResponse } from '../src/clients';
3
3
  import { GitClient } from '../src/clients/git-client';
4
4
  import {
5
5
  GitAuthenticationError,
@@ -35,12 +35,8 @@ describe('GitClient', () => {
35
35
 
36
36
  describe('repository cloning', () => {
37
37
  it('should clone public repositories successfully', async () => {
38
- const mockResponse: GitCheckoutResponse = {
38
+ const mockResponse: GitCheckoutResult = {
39
39
  success: true,
40
- stdout:
41
- "Cloning into 'react'...\nReceiving objects: 100% (1284/1284), done.",
42
- stderr: '',
43
- exitCode: 0,
44
40
  repoUrl: 'https://github.com/facebook/react.git',
45
41
  branch: 'main',
46
42
  targetDir: 'react',
@@ -59,15 +55,11 @@ describe('GitClient', () => {
59
55
  expect(result.success).toBe(true);
60
56
  expect(result.repoUrl).toBe('https://github.com/facebook/react.git');
61
57
  expect(result.branch).toBe('main');
62
- expect(result.exitCode).toBe(0);
63
58
  });
64
59
 
65
60
  it('should clone repositories to specific branches', async () => {
66
- const mockResponse: GitCheckoutResponse = {
61
+ const mockResponse: GitCheckoutResult = {
67
62
  success: true,
68
- stdout: "Cloning into 'project'...\nSwitching to branch 'development'",
69
- stderr: '',
70
- exitCode: 0,
71
63
  repoUrl: 'https://github.com/company/project.git',
72
64
  branch: 'development',
73
65
  targetDir: 'project',
@@ -89,11 +81,8 @@ describe('GitClient', () => {
89
81
  });
90
82
 
91
83
  it('should clone repositories to custom directories', async () => {
92
- const mockResponse: GitCheckoutResponse = {
84
+ const mockResponse: GitCheckoutResult = {
93
85
  success: true,
94
- stdout: "Cloning into 'workspace/my-app'...\nDone.",
95
- stderr: '',
96
- exitCode: 0,
97
86
  repoUrl: 'https://github.com/user/my-app.git',
98
87
  branch: 'main',
99
88
  targetDir: 'workspace/my-app',
@@ -115,12 +104,8 @@ describe('GitClient', () => {
115
104
  });
116
105
 
117
106
  it('should handle large repository clones with warnings', async () => {
118
- const mockResponse: GitCheckoutResponse = {
107
+ const mockResponse: GitCheckoutResult = {
119
108
  success: true,
120
- stdout:
121
- "Cloning into 'linux'...\nReceiving objects: 100% (8125432/8125432), 2.34 GiB, done.",
122
- stderr: 'warning: filtering not recognized by server',
123
- exitCode: 0,
124
109
  repoUrl: 'https://github.com/torvalds/linux.git',
125
110
  branch: 'master',
126
111
  targetDir: 'linux',
@@ -137,15 +122,11 @@ describe('GitClient', () => {
137
122
  );
138
123
 
139
124
  expect(result.success).toBe(true);
140
- expect(result.stderr).toContain('warning:');
141
125
  });
142
126
 
143
127
  it('should handle SSH repository URLs', async () => {
144
- const mockResponse: GitCheckoutResponse = {
128
+ const mockResponse: GitCheckoutResult = {
145
129
  success: true,
146
- stdout: "Cloning into 'private-project'...\nDone.",
147
- stderr: '',
148
- exitCode: 0,
149
130
  repoUrl: 'git@github.com:company/private-project.git',
150
131
  branch: 'main',
151
132
  targetDir: 'private-project',
@@ -175,8 +156,6 @@ describe('GitClient', () => {
175
156
  JSON.stringify({
176
157
  success: true,
177
158
  stdout: `Cloning into '${repoName}'...\nDone.`,
178
- stderr: '',
179
- exitCode: 0,
180
159
  repoUrl: body.repoUrl,
181
160
  branch: body.branch || 'main',
182
161
  targetDir: body.targetDir || repoName,
@@ -320,11 +299,8 @@ describe('GitClient', () => {
320
299
  });
321
300
 
322
301
  it('should handle partial clone failures', async () => {
323
- const mockResponse: GitCheckoutResponse = {
302
+ const mockResponse: GitCheckoutResult = {
324
303
  success: false,
325
- stdout: "Cloning into 'repo'...\nReceiving objects: 45% (450/1000)",
326
- stderr: 'error: RPC failed\nfatal: early EOF',
327
- exitCode: 128,
328
304
  repoUrl: 'https://github.com/problematic/repo.git',
329
305
  branch: 'main',
330
306
  targetDir: 'repo',
@@ -341,8 +317,6 @@ describe('GitClient', () => {
341
317
  );
342
318
 
343
319
  expect(result.success).toBe(false);
344
- expect(result.exitCode).toBe(128);
345
- expect(result.stderr).toContain('RPC failed');
346
320
  });
347
321
  });
348
322
 
@@ -434,9 +408,6 @@ describe('GitClient', () => {
434
408
  new Response(
435
409
  JSON.stringify({
436
410
  success: true,
437
- stdout: "Cloning into 'private-repo'...\nDone.",
438
- stderr: '',
439
- exitCode: 0,
440
411
  repoUrl:
441
412
  'https://oauth2:ghp_token123@github.com/user/private-repo.git',
442
413
  branch: 'main',
@@ -463,9 +434,6 @@ describe('GitClient', () => {
463
434
  new Response(
464
435
  JSON.stringify({
465
436
  success: true,
466
- stdout: "Cloning into 'react'...\nDone.",
467
- stderr: '',
468
- exitCode: 0,
469
437
  repoUrl: 'https://github.com/facebook/react.git',
470
438
  branch: 'main',
471
439
  targetDir: '/workspace/react',