Skip to content

Commit e02c8e3

Browse files
ross-rlclaude
andauthored
ci(smoketests): cut about two minutes from the smoke tests (#846)
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent 00cca2a commit e02c8e3

7 files changed

Lines changed: 449 additions & 425 deletions

File tree

‎.github/workflows/sdk-coverage.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ jobs:
5858
id: tests
5959
run: |
6060
set -o pipefail
61-
yarn test:objects-coverage --maxWorkers=800% --color 2>&1 | \
61+
yarn test:objects-coverage --maxWorkers=32 --color 2>&1 | \
6262
tee -i test-output.log
6363
6464
- name: Upload coverage report

‎.github/workflows/smoke-tests.yml‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,11 @@ jobs:
7373
run: yarn check:examples-md
7474

7575
# Jest resolves @runloop/api-client to src/, so these tests do not need a build.
76+
# Each test file gets its own worker because the tests wait on the API, not
77+
# the CPU. The count must be a plain number: Jest reads a percentage above
78+
# 100% as that many workers ("800%" starts 800 processes).
7679
- name: Run smoke tests
77-
run: yarn test:smoke --maxWorkers=800% --color
80+
run: yarn test:smoke --maxWorkers=32 --color
7881

7982
# Checks the built package over HTTP/2 outside Jest. It runs beside the smoke
8083
# tests so the build is not on their critical path.

‎tests/smoketests/object-oriented/README.md‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,22 @@ This directory contains comprehensive smoke tests for the object-oriented Runloo
1717
- File operations (read, write)
1818
- Devbox listing and retrieval by ID
1919

20+
### `devbox-from-blueprint.test.ts`
21+
22+
- Creating devboxes from blueprints (by ID and by name) and from snapshots
23+
- Asynchronous disk snapshots
24+
2025
### `blueprint.test.ts`
2126

2227
- Blueprint lifecycle (create, get info, delete)
2328
- Blueprint logs retrieval
2429
- Creating devboxes from blueprints
30+
31+
### `blueprint-build.test.ts`
32+
33+
- Blueprint builds with object-storage and directory build contexts, honoring `.dockerignore`
2534
- Blueprint listing and retrieval by ID
35+
- Blueprints with network policies for the build and for launched devboxes
2636

2737
### `snapshot.test.ts`
2838

Lines changed: 305 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,305 @@
1+
import { LONG_TIMEOUT, uniqueName, makeClientSDK, cleanUpPolicy } from '../utils';
2+
import { Blueprint, Devbox, NetworkPolicy, StorageObject } from '@runloop/api-client/sdk';
3+
4+
const sdk = makeClientSDK();
5+
6+
describe('smoketest: object-oriented blueprint', () => {
7+
describe('blueprint build context with object storage and .dockerignore', () => {
8+
test.concurrent(
9+
'creates blueprint with COPY using object-based build context honoring .dockerignore',
10+
async () => {
11+
const fs = require('fs/promises');
12+
const path = require('path');
13+
const os = require('os');
14+
15+
let contextDir: string | undefined;
16+
let storageObject: StorageObject | undefined;
17+
let blueprint: Blueprint | undefined;
18+
let devbox: Devbox | undefined;
19+
20+
try {
21+
// Create temporary build context directory
22+
contextDir = await fs.mkdtemp(path.join(os.tmpdir(), 'runloop-context-'));
23+
const includedPath = path.join(contextDir, 'included.txt');
24+
const ignoredPath = path.join(contextDir, 'ignored.txt');
25+
const dockerignorePath = path.join(contextDir, '.dockerignore');
26+
27+
await fs.writeFile(includedPath, 'Hello from context');
28+
await fs.writeFile(ignoredPath, 'This file should be ignored');
29+
await fs.writeFile(dockerignorePath, 'ignored.txt\n');
30+
31+
// Upload directory as build context object (tgz)
32+
if (!contextDir) {
33+
throw new Error('Context directory not created before uploadFromDir');
34+
}
35+
storageObject = await sdk.storageObject.uploadFromDir(contextDir, {
36+
name: uniqueName('sdk-context-object'),
37+
ttl_ms: 3600000, // 1 hour
38+
});
39+
40+
// Create blueprint that uses the uploaded object as build context
41+
blueprint = await sdk.blueprint.create(
42+
{
43+
name: uniqueName('sdk-blueprint-context'),
44+
dockerfile: `FROM ubuntu:22.04
45+
WORKDIR /app
46+
COPY . .`,
47+
build_context: storageObject,
48+
},
49+
{ longPoll: { timeoutMs: 10 * 60 * 1000 } },
50+
);
51+
52+
expect(blueprint).toBeDefined();
53+
expect(blueprint.id).toBeTruthy();
54+
55+
// Create devbox from the blueprint and verify copied files
56+
devbox = await blueprint.createDevbox({
57+
name: uniqueName('devbox-from-context'),
58+
launch_parameters: {
59+
resource_size_request: 'X_SMALL',
60+
keep_alive_time_seconds: 60 * 5, // 5 minutes
61+
},
62+
});
63+
64+
expect(devbox).toBeDefined();
65+
expect(devbox.id).toBeTruthy();
66+
67+
// included.txt should be present
68+
const includeCheck = await devbox.cmd.exec('test -f /app/included.txt');
69+
expect(includeCheck.exitCode).toBe(0);
70+
71+
// Optionally assert content
72+
const includeContentResult = await devbox.cmd.exec('cat /app/included.txt');
73+
const includeContent = await includeContentResult.stdout();
74+
expect(includeContent.trim()).toBe('Hello from context');
75+
76+
// ignored.txt should be absent due to .dockerignore
77+
const ignoredCheck = await devbox.cmd.exec('test ! -f /app/ignored.txt');
78+
expect(ignoredCheck.exitCode).toBe(0);
79+
80+
// .dockerignore itself should also not be present in the image
81+
const dockerignoreCheck = await devbox.cmd.exec('test ! -f /app/.dockerignore');
82+
expect(dockerignoreCheck.exitCode).toBe(0);
83+
} finally {
84+
if (devbox) {
85+
await devbox.shutdown().catch(() => {});
86+
}
87+
if (blueprint) {
88+
await blueprint.delete().catch(() => {});
89+
}
90+
if (storageObject) {
91+
await storageObject.delete().catch(() => {});
92+
}
93+
if (contextDir) {
94+
await fs.rm(contextDir, { recursive: true, force: true }).catch(() => {});
95+
}
96+
}
97+
},
98+
LONG_TIMEOUT,
99+
);
100+
101+
test.concurrent(
102+
'creates blueprint with build_context_dir parameter (string path)',
103+
async () => {
104+
const fs = require('fs/promises');
105+
const path = require('path');
106+
const os = require('os');
107+
108+
let contextDir: string | undefined;
109+
let blueprint: Blueprint | undefined;
110+
let devbox: Devbox | undefined;
111+
112+
try {
113+
// Create temporary build context directory
114+
contextDir = await fs.mkdtemp(path.join(os.tmpdir(), 'runloop-context-dir-'));
115+
const filePath = path.join(contextDir, 'hello.txt');
116+
await fs.writeFile(filePath, 'Hello from build_context_dir!');
117+
118+
// Create blueprint using build_context_dir parameter (string form)
119+
// This tests the build_context_dir branch in Blueprint.create()
120+
if (!contextDir) {
121+
throw new Error('Context directory not created');
122+
}
123+
blueprint = await sdk.blueprint.create(
124+
{
125+
name: uniqueName('sdk-blueprint-context-dir'),
126+
dockerfile: `FROM ubuntu:22.04
127+
WORKDIR /app
128+
COPY . .`,
129+
build_context_dir: contextDir,
130+
},
131+
{ longPoll: { timeoutMs: 10 * 60 * 1000 } },
132+
);
133+
134+
expect(blueprint).toBeDefined();
135+
expect(blueprint.id).toBeTruthy();
136+
137+
// Verify instance methods work
138+
const info = await blueprint.getInfo();
139+
expect(info.id).toBe(blueprint.id);
140+
141+
const logs = await blueprint.logs();
142+
expect(logs.logs).toBeDefined();
143+
144+
// Create devbox to verify the build context was used
145+
devbox = await blueprint.createDevbox({
146+
name: uniqueName('devbox-from-context-dir'),
147+
launch_parameters: {
148+
resource_size_request: 'X_SMALL',
149+
keep_alive_time_seconds: 60 * 5,
150+
},
151+
});
152+
153+
expect(devbox).toBeDefined();
154+
155+
// Verify the file from build context is present
156+
const result = await devbox.cmd.exec('cat /app/hello.txt');
157+
expect(result.exitCode).toBe(0);
158+
const content = await result.stdout();
159+
expect(content.trim()).toBe('Hello from build_context_dir!');
160+
} finally {
161+
if (devbox) {
162+
await devbox.shutdown().catch(() => {});
163+
}
164+
if (blueprint) {
165+
await blueprint.delete().catch(() => {});
166+
}
167+
if (contextDir) {
168+
await fs.rm(contextDir, { recursive: true, force: true }).catch(() => {});
169+
}
170+
}
171+
},
172+
LONG_TIMEOUT,
173+
);
174+
});
175+
176+
describe('blueprint list and retrieval', () => {
177+
test.concurrent('list blueprints', async () => {
178+
const blueprints = await sdk.blueprint.list({ limit: 10 });
179+
expect(Array.isArray(blueprints)).toBe(true);
180+
});
181+
182+
test.concurrent(
183+
'get blueprint by ID',
184+
async () => {
185+
// First create a blueprint
186+
let blueprint: Blueprint | undefined;
187+
try {
188+
blueprint = await sdk.blueprint.create(
189+
{
190+
name: uniqueName('sdk-blueprint-retrieve'),
191+
dockerfile: 'FROM ubuntu:22.04',
192+
},
193+
{ longPoll: { timeoutMs: 10 * 60 * 1000 } },
194+
);
195+
expect(blueprint?.id).toBeTruthy();
196+
197+
// Retrieve it byID
198+
const retrieved = sdk.blueprint.fromId(blueprint.id);
199+
expect(retrieved.id).toBe(blueprint.id);
200+
} finally {
201+
if (blueprint) {
202+
await blueprint.delete();
203+
}
204+
}
205+
},
206+
LONG_TIMEOUT,
207+
);
208+
});
209+
210+
describe('blueprint with network policy', () => {
211+
test.concurrent(
212+
'create blueprint with network_policy_id for build',
213+
async () => {
214+
let policy: NetworkPolicy | undefined;
215+
let blueprint: Blueprint | undefined;
216+
try {
217+
// Create a network policy for the build process
218+
policy = await sdk.networkPolicy.create({
219+
name: uniqueName('sdk-policy-for-blueprint-build'),
220+
allow_all: true, // Allow all for build to download packages
221+
});
222+
expect(policy.id).toBeTruthy();
223+
224+
// Create blueprint with network_policy_id at top level (for build)
225+
blueprint = await sdk.blueprint.create(
226+
{
227+
name: uniqueName('sdk-blueprint-with-build-policy'),
228+
dockerfile: 'FROM ubuntu:22.04\nRUN apt-get update',
229+
network_policy_id: policy.id,
230+
},
231+
{ longPoll: { timeoutMs: 10 * 60 * 1000 } },
232+
);
233+
234+
expect(blueprint).toBeDefined();
235+
expect(blueprint.id).toBeTruthy();
236+
237+
// Verify blueprint was created
238+
const info = await blueprint.getInfo();
239+
expect(info.id).toBe(blueprint.id);
240+
} finally {
241+
if (blueprint) {
242+
await blueprint.delete().catch(() => {});
243+
}
244+
await cleanUpPolicy(policy);
245+
}
246+
},
247+
LONG_TIMEOUT,
248+
);
249+
250+
test.concurrent(
251+
'create blueprint with launch_parameters.network_policy_id',
252+
async () => {
253+
let policy: NetworkPolicy | undefined;
254+
let blueprint: Blueprint | undefined;
255+
let devbox: Devbox | undefined;
256+
try {
257+
// Create a network policy for devboxes launched from this blueprint
258+
policy = await sdk.networkPolicy.create({
259+
name: uniqueName('sdk-policy-for-blueprint-launch'),
260+
allow_all: false,
261+
allowed_hostnames: ['github.com'],
262+
});
263+
expect(policy.id).toBeTruthy();
264+
265+
// Create blueprint with launch_parameters including network_policy_id
266+
blueprint = await sdk.blueprint.create(
267+
{
268+
name: uniqueName('sdk-blueprint-with-launch-policy'),
269+
dockerfile: 'FROM ubuntu:22.04',
270+
launch_parameters: {
271+
resource_size_request: 'X_SMALL',
272+
keep_alive_time_seconds: 60 * 5,
273+
network_policy_id: policy.id,
274+
},
275+
},
276+
{ longPoll: { timeoutMs: 10 * 60 * 1000 } },
277+
);
278+
279+
expect(blueprint).toBeDefined();
280+
expect(blueprint.id).toBeTruthy();
281+
282+
// Verify blueprint was created
283+
const info = await blueprint.getInfo();
284+
expect(info.id).toBe(blueprint.id);
285+
286+
// Create a devbox from the blueprint to verify it works
287+
devbox = await blueprint.createDevbox({
288+
name: uniqueName('devbox-from-policy-blueprint'),
289+
});
290+
expect(devbox).toBeDefined();
291+
expect(devbox.id).toBeTruthy();
292+
} finally {
293+
if (devbox) {
294+
await devbox.shutdown().catch(() => {});
295+
}
296+
if (blueprint) {
297+
await blueprint.delete().catch(() => {});
298+
}
299+
await cleanUpPolicy(policy);
300+
}
301+
},
302+
LONG_TIMEOUT,
303+
);
304+
});
305+
});

0 commit comments

Comments
 (0)