From 39bf770db818f63c057f2ecdecff3688e3d05762 Mon Sep 17 00:00:00 2001 From: Alexander Dines Date: Mon, 24 Nov 2025 16:56:21 -0800 Subject: [PATCH 1/3] cp dines --- .github/workflows/build-package-test.yml | 29 +++++ jest.config.ts | 13 +- package.json | 1 + scripts/utils/fix-index-exports.cjs | 6 +- tests/smoketests/build-package.test.ts | 155 +++++++++++++++++++++++ 5 files changed, 198 insertions(+), 6 deletions(-) create mode 100644 .github/workflows/build-package-test.yml create mode 100644 tests/smoketests/build-package.test.ts diff --git a/.github/workflows/build-package-test.yml b/.github/workflows/build-package-test.yml new file mode 100644 index 000000000..ecec0971a --- /dev/null +++ b/.github/workflows/build-package-test.yml @@ -0,0 +1,29 @@ +name: Build Package Test + +on: + push: + branches: + - main + pull_request: + workflow_dispatch: + +jobs: + build-package-test: + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Checkout + uses: runloopai/checkout@main + + - name: Setup Node + uses: runloopai/setup-node@main + with: + node-version: '20' + cache: 'yarn' + + - name: Install dependencies + run: yarn --frozen-lockfile + + - name: Run build package test + run: yarn test:built-package + diff --git a/jest.config.ts b/jest.config.ts index fc0e9ee8d..38942f81a 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -1,11 +1,12 @@ import type { JestConfigWithTsJest } from 'ts-jest'; const runSmoketests = process.env['RUN_SMOKETESTS'] === '1'; +const runBuiltPackageTest = process.env['RUN_BUILT_PACKAGE_TEST'] === '1'; const config: JestConfigWithTsJest = { preset: 'ts-jest/presets/default-esm', testEnvironment: 'node', - testTimeout: runSmoketests ? 300000 : 120000, // 5 minutes for smoke tests, 2 minutes for regular tests + testTimeout: (runSmoketests || runBuiltPackageTest) ? 300000 : 120000, // 5 minutes for smoke tests, 2 minutes for regular tests transform: { '^.+\\.(t|j)sx?$': ['@swc/jest', { sourceMaps: 'inline' }], }, @@ -23,12 +24,16 @@ const config: JestConfigWithTsJest = { testPathIgnorePatterns: [ 'scripts', // When running smoke tests, ignore regular tests; when running regular tests, ignore smoke tests - ...(runSmoketests ? - ['/tests/(?!smoketests).*'] // Ignore all test files except those in smoketests/ + ...(runSmoketests && !runBuiltPackageTest ? + [ + '/tests/(?!smoketests).*', // Ignore all test files except those in smoketests/ + '/tests/smoketests/build-package.test.ts', // Exclude build-package test from regular smoke test runs + ] + : runBuiltPackageTest ? [] // Don't ignore anything when running built-package test : ['/tests/smoketests/']), // Ignore smoke tests when running regular tests ], // Add display name for smoke tests to make it clearer in output - ...(runSmoketests && { displayName: 'Smoke Tests' }), + ...((runSmoketests || runBuiltPackageTest) && { displayName: 'Smoke Tests' }), }; export default config; diff --git a/package.json b/package.json index 18b06eb25..da07b0059 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "test:objects": "RUN_SMOKETESTS=1 jest --config jest.config.objects.js --verbose", "test:objects-coverage": "RUN_SMOKETESTS=1 jest --verbose --config jest.config.objects.js --coverage --coverageReporters=text --coverageReporters=json-summary", "test:objects-coverage:html": "RUN_SMOKETESTS=1 jest --verbose --config jest.config.objects.js --coverage --coverageReporters=html --coverageReporters=text && open coverage-objects/index.html", + "test:built-package": "yarn build && RUN_BUILT_PACKAGE_TEST=1 jest --verbose tests/smoketests/build-package.test.ts", "build": "./scripts/build", "prepublishOnly": "echo 'to publish, run yarn build && (cd dist; yarn publish)' && exit 1", "format": "prettier --write --cache --cache-strategy metadata . !dist", diff --git a/scripts/utils/fix-index-exports.cjs b/scripts/utils/fix-index-exports.cjs index 761a9881d..a7f94c3e5 100644 --- a/scripts/utils/fix-index-exports.cjs +++ b/scripts/utils/fix-index-exports.cjs @@ -7,8 +7,10 @@ const sdkJs = : path.resolve(__dirname, '..', '..', 'dist', 'sdk.js'); let before = fs.readFileSync(sdkJs, 'utf8'); +// Match exports.default = and preserve all existing exports +// by ensuring module.exports points to the exports object instead of replacing it let after = before.replace( - /^\s*exports\.default\s*=\s*(\w+)/m, - 'exports = module.exports = $1;\nexports.default = $1', + /^\s*exports\.default\s*=\s*([^;]+);/m, + 'module.exports = exports;\nexports.default = $1;', ); fs.writeFileSync(sdkJs, after, 'utf8'); diff --git a/tests/smoketests/build-package.test.ts b/tests/smoketests/build-package.test.ts new file mode 100644 index 000000000..ae5c5b9f0 --- /dev/null +++ b/tests/smoketests/build-package.test.ts @@ -0,0 +1,155 @@ +import { + RunloopSDK, + RunloopAPI, + Devbox, + Blueprint, + Snapshot, + StorageObject, + DevboxOps, + BlueprintOps, + SnapshotOps, + StorageObjectOps, + DevboxCmdOps, + DevboxFileOps, + DevboxNetOps, + Execution, + ExecutionResult, + type ExecuteStreamingCallbacks, + type ClientOptions, +} from '../../dist/sdk'; + +describe('smoketest: built package import', () => { + let sdk: RunloopSDK; + + beforeAll(() => { + // Initialize SDK from built package - using dummy token since we're only testing imports/types + sdk = new RunloopSDK({ + bearerToken: 'dummy-token-for-import-test', + baseURL: 'https://api.runloop.ai', + timeout: 120_000, + maxRetries: 1, + }); + }); + + describe('RunloopSDK from built package', () => { + test('should create SDK instance from built package', () => { + expect(sdk).toBeDefined(); + expect(sdk.devbox).toBeDefined(); + expect(sdk.blueprint).toBeDefined(); + expect(sdk.snapshot).toBeDefined(); + expect(sdk.storageObject).toBeDefined(); + expect(sdk.api).toBeDefined(); + }); + + test('should provide access to legacy API', () => { + expect(sdk.api).toBeDefined(); + expect(sdk.api.devboxes).toBeDefined(); + expect(sdk.api.blueprints).toBeDefined(); + expect(sdk.api.objects).toBeDefined(); + }); + + test('should verify RunloopSDK namespace exports are available', () => { + // Test that namespace exports are accessible + // These are exported from the RunloopSDK namespace + expect(DevboxOps).toBeDefined(); + expect(BlueprintOps).toBeDefined(); + expect(SnapshotOps).toBeDefined(); + expect(StorageObjectOps).toBeDefined(); + expect(Devbox).toBeDefined(); + expect(Blueprint).toBeDefined(); + expect(Snapshot).toBeDefined(); + expect(StorageObject).toBeDefined(); + }); + + test('should verify additional SDK classes are available', () => { + expect(DevboxCmdOps).toBeDefined(); + expect(DevboxFileOps).toBeDefined(); + expect(DevboxNetOps).toBeDefined(); + expect(Execution).toBeDefined(); + expect(ExecutionResult).toBeDefined(); + }); + + test('should verify types are available', () => { + // Type check - if this compiles, the type is available + const callback: ExecuteStreamingCallbacks = { + stdout: () => {}, + stderr: () => {}, + output: () => {}, + }; + expect(callback).toBeDefined(); + expect(typeof callback.stdout).toBe('function'); + expect(typeof callback.stderr).toBe('function'); + expect(typeof callback.output).toBe('function'); + }); + + test('should allow wrapping runloop types', () => { + // Test that types can be imported and used for type annotations + const options: ClientOptions = { + bearerToken: 'test-token', + baseURL: 'https://api.runloop.ai', + timeout: 120_000, + maxRetries: 1, + }; + expect(options).toBeDefined(); + expect(options.bearerToken).toBeDefined(); + expect(options.baseURL).toBeDefined(); + + // Verify type wrapping works by creating a wrapper function + function createSDKWithOptions(opts: ClientOptions): RunloopSDK { + return new RunloopSDK(opts); + } + const wrappedSDK = createSDKWithOptions(options); + expect(wrappedSDK).toBeInstanceOf(RunloopSDK); + }); + + test('should verify RunloopAPI namespace and nested resources', () => { + expect(RunloopAPI).toBeDefined(); + expect(RunloopAPI.Devboxes).toBeDefined(); + expect(RunloopAPI.Blueprints).toBeDefined(); + expect(RunloopAPI.Objects).toBeDefined(); + expect(RunloopAPI.Secrets).toBeDefined(); + expect(RunloopAPI.Agents).toBeDefined(); + expect(RunloopAPI.Benchmarks).toBeDefined(); + expect(RunloopAPI.Scenarios).toBeDefined(); + expect(RunloopAPI.Repositories).toBeDefined(); + }); + + test('should allow creating new types based on execution.result() return type', () => { + // Extract the return type from execution.result() + // execution.result() returns Promise + type ExecutionResultType = Awaited>; + + // Create a new type based on the extracted type + type WrappedExecutionResult = { + result: ExecutionResultType; + timestamp: number; + metadata?: Record; + }; + + // Verify the type works by creating an instance + // Note: We can't actually call execution.result() without a real execution, + // but we can verify the type extraction works by using ExecutionResult directly + const mockResult = new ExecutionResult(sdk.api, 'devbox-123', 'execution-456', { + execution_id: 'execution-456', + devbox_id: 'devbox-123', + status: 'completed', + exit_code: 0, + } as any); + + const wrapped: WrappedExecutionResult = { + result: mockResult, + timestamp: Date.now(), + metadata: { test: true }, + }; + + expect(wrapped).toBeDefined(); + expect(wrapped.result).toBeInstanceOf(ExecutionResult); + expect(wrapped.timestamp).toBeGreaterThan(0); + expect(wrapped.metadata?.['test']).toBe(true); + + // Verify the type is correctly inferred + const resultType: ExecutionResultType = mockResult; + expect(resultType).toBeInstanceOf(ExecutionResult); + }); + }); +}); From f08d5e00dbac4ee281300e3651e7c9b0534bec0d Mon Sep 17 00:00:00 2001 From: Alexander Dines Date: Mon, 24 Nov 2025 17:00:51 -0800 Subject: [PATCH 2/3] cp dines --- jest.config.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/jest.config.ts b/jest.config.ts index 38942f81a..7e5cadf98 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -6,7 +6,7 @@ const runBuiltPackageTest = process.env['RUN_BUILT_PACKAGE_TEST'] === '1'; const config: JestConfigWithTsJest = { preset: 'ts-jest/presets/default-esm', testEnvironment: 'node', - testTimeout: (runSmoketests || runBuiltPackageTest) ? 300000 : 120000, // 5 minutes for smoke tests, 2 minutes for regular tests + testTimeout: runSmoketests || runBuiltPackageTest ? 300000 : 120000, // 5 minutes for smoke tests, 2 minutes for regular tests transform: { '^.+\\.(t|j)sx?$': ['@swc/jest', { sourceMaps: 'inline' }], }, @@ -29,7 +29,8 @@ const config: JestConfigWithTsJest = { '/tests/(?!smoketests).*', // Ignore all test files except those in smoketests/ '/tests/smoketests/build-package.test.ts', // Exclude build-package test from regular smoke test runs ] - : runBuiltPackageTest ? [] // Don't ignore anything when running built-package test + : runBuiltPackageTest ? + [] // Don't ignore anything when running built-package test : ['/tests/smoketests/']), // Ignore smoke tests when running regular tests ], // Add display name for smoke tests to make it clearer in output From 520991225918f341c486fc38fe8fb4162b42f8b4 Mon Sep 17 00:00:00 2001 From: Alexander Dines Date: Mon, 24 Nov 2025 17:03:31 -0800 Subject: [PATCH 3/3] cp dines --- tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tsconfig.json b/tsconfig.json index 1e9f1e477..032ca6504 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,6 @@ { "include": ["src", "tests", "examples"], - "exclude": ["src/_shims/**/*-deno.ts"], + "exclude": ["src/_shims/**/*-deno.ts", "tests/smoketests/build-package.test.ts"], "compilerOptions": { "target": "es2020", "lib": ["es2020"],