diff --git a/src/resources/blueprints.ts b/src/resources/blueprints.ts index 993c3f0d0..cf0971bd4 100644 --- a/src/resources/blueprints.ts +++ b/src/resources/blueprints.ts @@ -56,7 +56,9 @@ function validateFileMounts(fileMounts?: { [key: string]: string } | null): Arra const over = sizeBytes - FILE_MOUNT_MAX_SIZE_BYTES; errors.push( `file_mount '${mountPath}' is ${formatBytes(over)} over the limit (` + - `${formatBytes(sizeBytes)} / ${formatBytes(FILE_MOUNT_MAX_SIZE_BYTES)}). Use object_mounts instead.`, + `${formatBytes(sizeBytes)} / ${formatBytes( + FILE_MOUNT_MAX_SIZE_BYTES, + )}). Use object_mounts instead.`, ); } totalSizeBytes += sizeBytes; @@ -66,7 +68,9 @@ function validateFileMounts(fileMounts?: { [key: string]: string } | null): Arra const totalOver = totalSizeBytes - FILE_MOUNT_TOTAL_MAX_SIZE_BYTES; errors.push( `total file_mounts size is ${formatBytes(totalOver)} over the limit (` + - `${formatBytes(totalSizeBytes)} / ${formatBytes(FILE_MOUNT_TOTAL_MAX_SIZE_BYTES)}). Use object_mounts instead.`, + `${formatBytes(totalSizeBytes)} / ${formatBytes( + FILE_MOUNT_TOTAL_MAX_SIZE_BYTES, + )}). Use object_mounts instead.`, ); } diff --git a/src/sdk.ts b/src/sdk.ts index e7c8943f8..c46b7fb28 100644 --- a/src/sdk.ts +++ b/src/sdk.ts @@ -595,18 +595,6 @@ export class StorageObjectOps { } } -// @deprecated Use {@link RunloopSDK} instead. -/** - * @deprecated Use {@link RunloopSDK} instead. - * @example - * ```typescript - * import { RunloopSDK } from '@runloop/api-client'; - * const sdk = new RunloopSDK(); - * const devbox = await sdk.devbox.create({ name: 'my-devbox' }); - * ``` - */ -export default Runloop; - export declare namespace RunloopSDK { export { RunloopSDK as Client, @@ -620,6 +608,7 @@ export declare namespace RunloopSDK { StorageObject as StorageObject, }; } + // Export SDK classes from sdk/sdk.ts - these are separate from RunloopSDK to avoid circular dependencies export { Devbox, diff --git a/src/sdk/storage-object.ts b/src/sdk/storage-object.ts index 2f968e299..7ecca31be 100644 --- a/src/sdk/storage-object.ts +++ b/src/sdk/storage-object.ts @@ -6,6 +6,7 @@ import type { ObjectDownloadURLView, ObjectListParams, } from '../resources/objects'; +import { fetch } from '../_shims/index'; import * as fs from 'node:fs/promises'; import * as path from 'node:path'; import * as tar from 'tar'; @@ -312,7 +313,7 @@ export class StorageObject { try { const response = await fetch(uploadUrl, { method: 'PUT', - body: new Blob([text]), + body: text, }); if (!response.ok) { @@ -387,7 +388,7 @@ export class StorageObject { try { const response = await fetch(uploadUrl, { method: 'PUT', - body: new Blob([buffer]), + body: buffer, }); if (!response.ok) { @@ -430,14 +431,16 @@ export class StorageObject { let buffer; try { const tarStream = tar.create({ gzip: true, cwd: dirPath }, ['.']); - const chunks = []; + const chunks: Buffer[] = []; for await (const chunk of tarStream) { chunks.push(chunk); } - buffer = Buffer.concat(chunks); + buffer = Buffer.concat(chunks as readonly Uint8Array[]); } catch (error) { throw new Error( - `Failed to create tarball from directory ${dirPath}: ${error instanceof Error ? error.message : 'Unknown error'}`, + `Failed to create tarball from directory ${dirPath}: ${ + error instanceof Error ? error.message : 'Unknown error' + }`, ); } diff --git a/src/uploads.ts b/src/uploads.ts index 2264b5704..5fde000d0 100644 --- a/src/uploads.ts +++ b/src/uploads.ts @@ -156,9 +156,7 @@ async function getBytes(value: ToFileInput): Promise> { } } else { throw new Error( - `Unexpected data type: ${typeof value}; constructor: ${ - value?.constructor?.name - }; props: ${propsForError(value)}`, + `Unexpected data type: ${typeof value}; constructor: ${value?.constructor?.name}; props: ${propsForError(value)}`, ); } diff --git a/tests/api-resources/agents.test.ts b/tests/api-resources/agents.test.ts index 6513f221d..2d263095e 100644 --- a/tests/api-resources/agents.test.ts +++ b/tests/api-resources/agents.test.ts @@ -1,6 +1,6 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -import Runloop from '@runloop/api-client'; +import { Runloop } from '@runloop/api-client'; import { Response } from 'node-fetch'; const client = new Runloop({ diff --git a/tests/index.test.ts b/tests/index.test.ts index 6db7c9732..8ce09d171 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -285,8 +285,8 @@ describe('retries', () => { let count = 0; const testFetch = async (url: RequestInfo, { signal }: RequestInit = {}): Promise => { if (count++ === 0) { - return new Promise((resolve, reject) => - signal?.addEventListener('abort', () => reject(new Error('timed out'))), + return new Promise( + (resolve, reject) => signal?.addEventListener('abort', () => reject(new Error('timed out'))), ); } return new Response(JSON.stringify({ a: 1 }), { headers: { 'Content-Type': 'application/json' } }); diff --git a/tests/lib/streaming-reconnection.test.ts b/tests/lib/streaming-reconnection.test.ts index d6f72abe7..97e169f0f 100644 --- a/tests/lib/streaming-reconnection.test.ts +++ b/tests/lib/streaming-reconnection.test.ts @@ -1,3 +1,4 @@ +import { ReadableStream, ReadableStreamDefaultController } from 'stream/web'; import { APIError } from '../../src/error'; import { withStreamAutoReconnect } from '../../src/lib/streaming-reconnection'; import { Stream } from '../../src/streaming'; @@ -37,7 +38,7 @@ function createStreamFactory( let lastDeliveredOffset = offset ?? 0; const readable = new ReadableStream({ - async pull(controller) { + async pull(controller: ReadableStreamDefaultController) { const trigger = takeTrigger(lastDeliveredOffset); if (trigger) { controller.error(trigger.error ?? new Error('Synthetic failure')); @@ -220,7 +221,7 @@ describe('withStreamAutoReconnect', () => { creatorCalls += 1; const abortController = new AbortController(); const readable = new ReadableStream({ - pull(controller) { + pull(controller: ReadableStreamDefaultController) { controller.error(error); }, cancel() { diff --git a/tests/objects/storage-object.test.ts b/tests/objects/storage-object.test.ts index d3d2e5572..9c71e62df 100644 --- a/tests/objects/storage-object.test.ts +++ b/tests/objects/storage-object.test.ts @@ -4,8 +4,17 @@ import type { ObjectView, ObjectDownloadURLView } from '../../src/resources/obje // Mock the Runloop client jest.mock('../../src/index'); -// Mock fetch globally -(global as any).fetch = jest.fn(); +// Mock node-fetch (used by shims in Node.js environment) +// We'll get a reference to the mock in beforeEach +jest.mock('node-fetch', () => { + const mockFn = jest.fn(); + // Store on global so we can access it + (global as any).__nodeFetchMock = mockFn; + return { + default: mockFn, + __esModule: true, + }; +}); // Mock fs and path modules jest.mock('node:fs/promises', () => ({ @@ -32,11 +41,17 @@ describe('StorageObject (New API)', () => { let mockObjectData: ObjectView; let mockFs: any; let mockPath: any; + let mockFetch: jest.Mock; beforeEach(() => { // Get mocked modules mockFs = require('node:fs/promises'); mockPath = require('node:path'); + + // Get the mocked fetch function from node-fetch + const nodeFetch = require('node-fetch'); + mockFetch = nodeFetch.default; + (global as any).fetch = mockFetch; // Create mock client instance with proper structure mockClient = { @@ -93,7 +108,7 @@ describe('StorageObject (New API)', () => { }; // Reset fetch mock - ((global as any).fetch as jest.Mock).mockReset(); + mockFetch.mockReset(); }); describe('create', () => { @@ -234,11 +249,11 @@ describe('StorageObject (New API)', () => { statusText: 'OK', }; - ((global as any).fetch as jest.Mock).mockResolvedValue(mockFetchResponse); + mockFetch.mockResolvedValue(mockFetchResponse); await storageObject.uploadContent('Hello, World!'); - expect((global as any).fetch).toHaveBeenCalledWith(mockObjectData.upload_url, { + expect(mockFetch).toHaveBeenCalledWith(mockObjectData.upload_url, { method: 'PUT', body: Buffer.from('Hello, World!', 'utf-8'), }); @@ -254,12 +269,12 @@ describe('StorageObject (New API)', () => { statusText: 'OK', }; - ((global as any).fetch as jest.Mock).mockResolvedValue(mockFetchResponse); + mockFetch.mockResolvedValue(mockFetchResponse); const buffer = Buffer.from([0x89, 0x50, 0x4e, 0x47]); await storageObject.uploadContent(buffer); - expect((global as any).fetch).toHaveBeenCalledWith(mockObjectData.upload_url, { + expect(mockFetch).toHaveBeenCalledWith(mockObjectData.upload_url, { method: 'PUT', body: buffer, }); @@ -284,7 +299,7 @@ describe('StorageObject (New API)', () => { text: jest.fn().mockResolvedValue('Forbidden'), }; - ((global as any).fetch as jest.Mock).mockResolvedValue(mockFetchResponse); + mockFetch.mockResolvedValue(mockFetchResponse); await expect(storageObject.uploadContent('test')).rejects.toThrow('Upload failed: 403'); }); @@ -355,11 +370,11 @@ describe('StorageObject (New API)', () => { text: jest.fn().mockResolvedValue('File contents'), }; - ((global as any).fetch as jest.Mock).mockResolvedValue(mockFetchResponse); + mockFetch.mockResolvedValue(mockFetchResponse); const content = await storageObject.downloadAsText(); - expect((global as any).fetch).toHaveBeenCalledWith(mockDownloadUrl.download_url); + expect(mockFetch).toHaveBeenCalledWith(mockDownloadUrl.download_url); expect(content).toBe('File contents'); }); @@ -376,7 +391,7 @@ describe('StorageObject (New API)', () => { statusText: 'Not Found', }; - ((global as any).fetch as jest.Mock).mockResolvedValue(mockFetchResponse); + mockFetch.mockResolvedValue(mockFetchResponse); await expect(storageObject.downloadAsText()).rejects.toThrow('Download failed: 404 Not Found'); }); @@ -396,11 +411,11 @@ describe('StorageObject (New API)', () => { arrayBuffer: jest.fn().mockResolvedValue(mockArrayBuffer), }; - ((global as any).fetch as jest.Mock).mockResolvedValue(mockFetchResponse); + mockFetch.mockResolvedValue(mockFetchResponse); const buffer = await storageObject.downloadAsBuffer(); - expect((global as any).fetch).toHaveBeenCalledWith(mockDownloadUrl.download_url); + expect(mockFetch).toHaveBeenCalledWith(mockDownloadUrl.download_url); expect(Buffer.isBuffer(buffer)).toBe(true); expect(buffer.length).toBe(4); }); @@ -435,7 +450,7 @@ describe('StorageObject (New API)', () => { // Upload - mock getInfo for uploadContent mockClient.objects.retrieve.mockResolvedValue(mockObjectData); - ((global as any).fetch as jest.Mock).mockResolvedValue({ ok: true }); + mockFetch.mockResolvedValue({ ok: true }); await obj.uploadContent('Test content'); // Complete @@ -448,7 +463,7 @@ describe('StorageObject (New API)', () => { download_url: 'https://s3.example.com/download/workflow-test.txt', }; mockClient.objects.download.mockResolvedValue(mockDownloadUrl); - ((global as any).fetch as jest.Mock).mockResolvedValue({ + mockFetch.mockResolvedValue({ ok: true, text: jest.fn().mockResolvedValue('Test content'), }); @@ -463,7 +478,7 @@ describe('StorageObject (New API)', () => { // Clear all mocks jest.clearAllMocks(); // Reset global fetch mock - ((global as any).fetch as jest.Mock).mockClear(); + mockFetch.mockClear(); }); it('should upload a text file with auto-detected content-type', async () => { @@ -479,7 +494,7 @@ describe('StorageObject (New API)', () => { mockClient.objects.retrieve.mockResolvedValue(mockObjectInfo); mockClient.objects.complete.mockResolvedValue(mockCompletedData); - ((global as any).fetch as jest.Mock).mockResolvedValue({ + mockFetch.mockResolvedValue({ ok: true, status: 200, statusText: 'OK', @@ -509,7 +524,7 @@ describe('StorageObject (New API)', () => { mockClient.objects.retrieve.mockResolvedValue(mockObjectInfo); mockClient.objects.complete.mockResolvedValue(mockCompletedData); - ((global as any).fetch as jest.Mock).mockResolvedValue({ + mockFetch.mockResolvedValue({ ok: true, status: 200, statusText: 'OK', @@ -559,7 +574,7 @@ describe('StorageObject (New API)', () => { mockClient.objects.create.mockResolvedValue(mockObjectData); mockClient.objects.retrieve.mockResolvedValue(mockObjectInfo); - ((global as any).fetch as jest.Mock).mockResolvedValue({ + mockFetch.mockResolvedValue({ ok: false, status: 500, statusText: 'Internal Server Error', @@ -583,7 +598,7 @@ describe('StorageObject (New API)', () => { mockClient.objects.retrieve.mockResolvedValue(mockObjectInfo); mockClient.objects.complete.mockResolvedValue(mockCompletedData); - ((global as any).fetch as jest.Mock).mockResolvedValue({ + mockFetch.mockResolvedValue({ ok: true, status: 200, statusText: 'OK', @@ -610,7 +625,7 @@ describe('StorageObject (New API)', () => { // Clear all mocks jest.clearAllMocks(); // Reset global fetch mock - ((global as any).fetch as jest.Mock).mockClear(); + mockFetch.mockClear(); }); it('should upload text content with text content-type', async () => { @@ -623,7 +638,7 @@ describe('StorageObject (New API)', () => { mockClient.objects.retrieve.mockResolvedValue(mockObjectInfo); mockClient.objects.complete.mockResolvedValue(mockCompletedData); - ((global as any).fetch as jest.Mock).mockResolvedValue({ + mockFetch.mockResolvedValue({ ok: true, status: 200, statusText: 'OK', @@ -636,7 +651,7 @@ describe('StorageObject (New API)', () => { undefined, ); // uploadFromText uses Blob for fetch body - const fetchCalls = ((global as any).fetch as jest.Mock).mock.calls; + const fetchCalls = mockFetch.mock.calls; expect(fetchCalls[0][0]).toBe('https://upload.example.com/text'); expect(fetchCalls[0][1].method).toBe('PUT'); expect(fetchCalls[0][1].body).toBeInstanceOf(Blob); @@ -654,7 +669,7 @@ describe('StorageObject (New API)', () => { mockClient.objects.retrieve.mockResolvedValue(mockObjectInfo); mockClient.objects.complete.mockResolvedValue(mockCompletedData); - ((global as any).fetch as jest.Mock).mockResolvedValue({ + mockFetch.mockResolvedValue({ ok: true, status: 200, statusText: 'OK', @@ -679,7 +694,7 @@ describe('StorageObject (New API)', () => { mockClient.objects.create.mockResolvedValue(mockObjectData); mockClient.objects.retrieve.mockResolvedValue(mockObjectInfo); - ((global as any).fetch as jest.Mock).mockResolvedValue({ + mockFetch.mockResolvedValue({ ok: false, status: 403, statusText: 'Forbidden', @@ -700,7 +715,7 @@ describe('StorageObject (New API)', () => { mockClient.objects.retrieve.mockResolvedValue(mockObjectInfo); mockClient.objects.complete.mockResolvedValue(mockCompletedData); - ((global as any).fetch as jest.Mock).mockResolvedValue({ + mockFetch.mockResolvedValue({ ok: true, status: 200, statusText: 'OK', @@ -721,7 +736,7 @@ describe('StorageObject (New API)', () => { // Clear all mocks jest.clearAllMocks(); // Reset global fetch mock - ((global as any).fetch as jest.Mock).mockClear(); + mockFetch.mockClear(); }); it('should upload buffer with specified content-type and name', async () => { @@ -734,7 +749,7 @@ describe('StorageObject (New API)', () => { mockClient.objects.retrieve.mockResolvedValue(mockObjectInfo); mockClient.objects.complete.mockResolvedValue(mockCompletedData); - ((global as any).fetch as jest.Mock).mockResolvedValue({ + mockFetch.mockResolvedValue({ ok: true, status: 200, statusText: 'OK', @@ -749,7 +764,7 @@ describe('StorageObject (New API)', () => { { metadata: { source: 'buffer' } }, ); // uploadFromBuffer uses Blob for fetch body - const fetchCalls = ((global as any).fetch as jest.Mock).mock.calls; + const fetchCalls = mockFetch.mock.calls; expect(fetchCalls[0][0]).toBe('https://upload.example.com/buffer'); expect(fetchCalls[0][1].method).toBe('PUT'); expect(fetchCalls[0][1].body).toBeInstanceOf(Blob); @@ -779,7 +794,7 @@ describe('StorageObject (New API)', () => { mockClient.objects.create.mockResolvedValue(mockObjectData); mockClient.objects.retrieve.mockResolvedValue(mockObjectInfo); - ((global as any).fetch as jest.Mock).mockResolvedValue({ + mockFetch.mockResolvedValue({ ok: false, status: 403, statusText: 'Forbidden', @@ -800,7 +815,7 @@ describe('StorageObject (New API)', () => { mockClient.objects.retrieve.mockResolvedValue(mockObjectInfo); mockClient.objects.complete.mockResolvedValue(mockCompletedData); - ((global as any).fetch as jest.Mock).mockResolvedValue({ + mockFetch.mockResolvedValue({ ok: true, status: 200, statusText: 'OK', @@ -823,7 +838,7 @@ describe('StorageObject (New API)', () => { // Clear all mocks jest.clearAllMocks(); // Reset global fetch mock - ((global as any).fetch as jest.Mock).mockClear(); + mockFetch.mockClear(); // Get tar mock mockTar = require('tar'); }); @@ -848,7 +863,7 @@ describe('StorageObject (New API)', () => { mockClient.objects.retrieve.mockResolvedValue(mockObjectInfo); mockClient.objects.complete.mockResolvedValue(mockCompletedData); - ((global as any).fetch as jest.Mock).mockResolvedValue({ + mockFetch.mockResolvedValue({ ok: true, status: 200, statusText: 'OK', @@ -885,7 +900,7 @@ describe('StorageObject (New API)', () => { mockClient.objects.create.mockResolvedValue(mockObjectData); mockClient.objects.complete.mockResolvedValue(mockCompletedData); - ((global as any).fetch as jest.Mock).mockResolvedValue({ + mockFetch.mockResolvedValue({ ok: true, status: 200, statusText: 'OK', @@ -944,7 +959,7 @@ describe('StorageObject (New API)', () => { const mockObjectData = { id: 'dir-999', upload_url: 'https://upload.example.com/dir' }; mockClient.objects.create.mockResolvedValue(mockObjectData); - ((global as any).fetch as jest.Mock).mockResolvedValue({ + mockFetch.mockResolvedValue({ ok: false, status: 500, statusText: 'Internal Server Error', diff --git a/tests/smoketests/object-oriented/storage-object.test.ts b/tests/smoketests/object-oriented/storage-object.test.ts index b28708ed9..fde73ab30 100644 --- a/tests/smoketests/object-oriented/storage-object.test.ts +++ b/tests/smoketests/object-oriented/storage-object.test.ts @@ -257,7 +257,7 @@ describe('smoketest: object-oriented storage object', () => { }, }); await tarStream.write(data); - const content = Buffer.concat(contentChunks); + const content = Buffer.concat(contentChunks as readonly Uint8Array[]); expect(content.toString('utf-8')).toBe('Hello from uploadFromDir!'); } finally { await fs.unlink(contentPath).catch(() => {});