Skip to content

Commit b9ca9a1

Browse files
authored
fix(sdk): Fix exports of the SDK not getting clobbered (#662)
* cp dines * cp dines * cp dines
1 parent f21065f commit b9ca9a1

6 files changed

Lines changed: 200 additions & 7 deletions

File tree

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
name: Build Package Test
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
pull_request:
8+
workflow_dispatch:
9+
10+
jobs:
11+
build-package-test:
12+
runs-on: ubuntu-latest
13+
timeout-minutes: 10
14+
steps:
15+
- name: Checkout
16+
uses: runloopai/checkout@main
17+
18+
- name: Setup Node
19+
uses: runloopai/setup-node@main
20+
with:
21+
node-version: '20'
22+
cache: 'yarn'
23+
24+
- name: Install dependencies
25+
run: yarn --frozen-lockfile
26+
27+
- name: Run build package test
28+
run: yarn test:built-package
29+

jest.config.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import type { JestConfigWithTsJest } from 'ts-jest';
22

33
const runSmoketests = process.env['RUN_SMOKETESTS'] === '1';
4+
const runBuiltPackageTest = process.env['RUN_BUILT_PACKAGE_TEST'] === '1';
45

56
const config: JestConfigWithTsJest = {
67
preset: 'ts-jest/presets/default-esm',
78
testEnvironment: 'node',
8-
testTimeout: runSmoketests ? 300000 : 120000, // 5 minutes for smoke tests, 2 minutes for regular tests
9+
testTimeout: runSmoketests || runBuiltPackageTest ? 300000 : 120000, // 5 minutes for smoke tests, 2 minutes for regular tests
910
transform: {
1011
'^.+\\.(t|j)sx?$': ['@swc/jest', { sourceMaps: 'inline' }],
1112
},
@@ -23,12 +24,17 @@ const config: JestConfigWithTsJest = {
2324
testPathIgnorePatterns: [
2425
'scripts',
2526
// When running smoke tests, ignore regular tests; when running regular tests, ignore smoke tests
26-
...(runSmoketests ?
27-
['<rootDir>/tests/(?!smoketests).*'] // Ignore all test files except those in smoketests/
27+
...(runSmoketests && !runBuiltPackageTest ?
28+
[
29+
'<rootDir>/tests/(?!smoketests).*', // Ignore all test files except those in smoketests/
30+
'<rootDir>/tests/smoketests/build-package.test.ts', // Exclude build-package test from regular smoke test runs
31+
]
32+
: runBuiltPackageTest ?
33+
[] // Don't ignore anything when running built-package test
2834
: ['<rootDir>/tests/smoketests/']), // Ignore smoke tests when running regular tests
2935
],
3036
// Add display name for smoke tests to make it clearer in output
31-
...(runSmoketests && { displayName: 'Smoke Tests' }),
37+
...((runSmoketests || runBuiltPackageTest) && { displayName: 'Smoke Tests' }),
3238
};
3339

3440
export default config;

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"test:objects": "RUN_SMOKETESTS=1 jest --config jest.config.objects.js --verbose",
2323
"test:objects-coverage": "RUN_SMOKETESTS=1 jest --verbose --config jest.config.objects.js --coverage --coverageReporters=text --coverageReporters=json-summary",
2424
"test:objects-coverage:html": "RUN_SMOKETESTS=1 jest --verbose --config jest.config.objects.js --coverage --coverageReporters=html --coverageReporters=text && open coverage-objects/index.html",
25+
"test:built-package": "yarn build && RUN_BUILT_PACKAGE_TEST=1 jest --verbose tests/smoketests/build-package.test.ts",
2526
"build": "./scripts/build",
2627
"prepublishOnly": "echo 'to publish, run yarn build && (cd dist; yarn publish)' && exit 1",
2728
"format": "prettier --write --cache --cache-strategy metadata . !dist",

scripts/utils/fix-index-exports.cjs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@ const sdkJs =
77
: path.resolve(__dirname, '..', '..', 'dist', 'sdk.js');
88

99
let before = fs.readFileSync(sdkJs, 'utf8');
10+
// Match exports.default = <anything> and preserve all existing exports
11+
// by ensuring module.exports points to the exports object instead of replacing it
1012
let after = before.replace(
11-
/^\s*exports\.default\s*=\s*(\w+)/m,
12-
'exports = module.exports = $1;\nexports.default = $1',
13+
/^\s*exports\.default\s*=\s*([^;]+);/m,
14+
'module.exports = exports;\nexports.default = $1;',
1315
);
1416
fs.writeFileSync(sdkJs, after, 'utf8');
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import {
2+
RunloopSDK,
3+
RunloopAPI,
4+
Devbox,
5+
Blueprint,
6+
Snapshot,
7+
StorageObject,
8+
DevboxOps,
9+
BlueprintOps,
10+
SnapshotOps,
11+
StorageObjectOps,
12+
DevboxCmdOps,
13+
DevboxFileOps,
14+
DevboxNetOps,
15+
Execution,
16+
ExecutionResult,
17+
type ExecuteStreamingCallbacks,
18+
type ClientOptions,
19+
} from '../../dist/sdk';
20+
21+
describe('smoketest: built package import', () => {
22+
let sdk: RunloopSDK;
23+
24+
beforeAll(() => {
25+
// Initialize SDK from built package - using dummy token since we're only testing imports/types
26+
sdk = new RunloopSDK({
27+
bearerToken: 'dummy-token-for-import-test',
28+
baseURL: 'https://api.runloop.ai',
29+
timeout: 120_000,
30+
maxRetries: 1,
31+
});
32+
});
33+
34+
describe('RunloopSDK from built package', () => {
35+
test('should create SDK instance from built package', () => {
36+
expect(sdk).toBeDefined();
37+
expect(sdk.devbox).toBeDefined();
38+
expect(sdk.blueprint).toBeDefined();
39+
expect(sdk.snapshot).toBeDefined();
40+
expect(sdk.storageObject).toBeDefined();
41+
expect(sdk.api).toBeDefined();
42+
});
43+
44+
test('should provide access to legacy API', () => {
45+
expect(sdk.api).toBeDefined();
46+
expect(sdk.api.devboxes).toBeDefined();
47+
expect(sdk.api.blueprints).toBeDefined();
48+
expect(sdk.api.objects).toBeDefined();
49+
});
50+
51+
test('should verify RunloopSDK namespace exports are available', () => {
52+
// Test that namespace exports are accessible
53+
// These are exported from the RunloopSDK namespace
54+
expect(DevboxOps).toBeDefined();
55+
expect(BlueprintOps).toBeDefined();
56+
expect(SnapshotOps).toBeDefined();
57+
expect(StorageObjectOps).toBeDefined();
58+
expect(Devbox).toBeDefined();
59+
expect(Blueprint).toBeDefined();
60+
expect(Snapshot).toBeDefined();
61+
expect(StorageObject).toBeDefined();
62+
});
63+
64+
test('should verify additional SDK classes are available', () => {
65+
expect(DevboxCmdOps).toBeDefined();
66+
expect(DevboxFileOps).toBeDefined();
67+
expect(DevboxNetOps).toBeDefined();
68+
expect(Execution).toBeDefined();
69+
expect(ExecutionResult).toBeDefined();
70+
});
71+
72+
test('should verify types are available', () => {
73+
// Type check - if this compiles, the type is available
74+
const callback: ExecuteStreamingCallbacks = {
75+
stdout: () => {},
76+
stderr: () => {},
77+
output: () => {},
78+
};
79+
expect(callback).toBeDefined();
80+
expect(typeof callback.stdout).toBe('function');
81+
expect(typeof callback.stderr).toBe('function');
82+
expect(typeof callback.output).toBe('function');
83+
});
84+
85+
test('should allow wrapping runloop types', () => {
86+
// Test that types can be imported and used for type annotations
87+
const options: ClientOptions = {
88+
bearerToken: 'test-token',
89+
baseURL: 'https://api.runloop.ai',
90+
timeout: 120_000,
91+
maxRetries: 1,
92+
};
93+
expect(options).toBeDefined();
94+
expect(options.bearerToken).toBeDefined();
95+
expect(options.baseURL).toBeDefined();
96+
97+
// Verify type wrapping works by creating a wrapper function
98+
function createSDKWithOptions(opts: ClientOptions): RunloopSDK {
99+
return new RunloopSDK(opts);
100+
}
101+
const wrappedSDK = createSDKWithOptions(options);
102+
expect(wrappedSDK).toBeInstanceOf(RunloopSDK);
103+
});
104+
105+
test('should verify RunloopAPI namespace and nested resources', () => {
106+
expect(RunloopAPI).toBeDefined();
107+
expect(RunloopAPI.Devboxes).toBeDefined();
108+
expect(RunloopAPI.Blueprints).toBeDefined();
109+
expect(RunloopAPI.Objects).toBeDefined();
110+
expect(RunloopAPI.Secrets).toBeDefined();
111+
expect(RunloopAPI.Agents).toBeDefined();
112+
expect(RunloopAPI.Benchmarks).toBeDefined();
113+
expect(RunloopAPI.Scenarios).toBeDefined();
114+
expect(RunloopAPI.Repositories).toBeDefined();
115+
});
116+
117+
test('should allow creating new types based on execution.result() return type', () => {
118+
// Extract the return type from execution.result()
119+
// execution.result() returns Promise<ExecutionResult>
120+
type ExecutionResultType = Awaited<ReturnType<Execution['result']>>;
121+
122+
// Create a new type based on the extracted type
123+
type WrappedExecutionResult = {
124+
result: ExecutionResultType;
125+
timestamp: number;
126+
metadata?: Record<string, unknown>;
127+
};
128+
129+
// Verify the type works by creating an instance
130+
// Note: We can't actually call execution.result() without a real execution,
131+
// but we can verify the type extraction works by using ExecutionResult directly
132+
const mockResult = new ExecutionResult(sdk.api, 'devbox-123', 'execution-456', {
133+
execution_id: 'execution-456',
134+
devbox_id: 'devbox-123',
135+
status: 'completed',
136+
exit_code: 0,
137+
} as any);
138+
139+
const wrapped: WrappedExecutionResult = {
140+
result: mockResult,
141+
timestamp: Date.now(),
142+
metadata: { test: true },
143+
};
144+
145+
expect(wrapped).toBeDefined();
146+
expect(wrapped.result).toBeInstanceOf(ExecutionResult);
147+
expect(wrapped.timestamp).toBeGreaterThan(0);
148+
expect(wrapped.metadata?.['test']).toBe(true);
149+
150+
// Verify the type is correctly inferred
151+
const resultType: ExecutionResultType = mockResult;
152+
expect(resultType).toBeInstanceOf(ExecutionResult);
153+
});
154+
});
155+
});

tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"include": ["src", "tests", "examples"],
3-
"exclude": ["src/_shims/**/*-deno.ts"],
3+
"exclude": ["src/_shims/**/*-deno.ts", "tests/smoketests/build-package.test.ts"],
44
"compilerOptions": {
55
"target": "es2020",
66
"lib": ["es2020"],

0 commit comments

Comments
 (0)