Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/sdk-coverage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ jobs:
id: tests
run: |
set -o pipefail
yarn test:objects-coverage --maxWorkers=800% --color 2>&1 | \
yarn test:objects-coverage --maxWorkers=32 --color 2>&1 | \
tee -i test-output.log

- name: Upload coverage report
Expand Down
5 changes: 4 additions & 1 deletion .github/workflows/smoke-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,11 @@ jobs:
run: yarn check:examples-md

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

# Checks the built package over HTTP/2 outside Jest. It runs beside the smoke
# tests so the build is not on their critical path.
Expand Down
10 changes: 10 additions & 0 deletions tests/smoketests/object-oriented/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,22 @@ This directory contains comprehensive smoke tests for the object-oriented Runloo
- File operations (read, write)
- Devbox listing and retrieval by ID

### `devbox-from-blueprint.test.ts`

- Creating devboxes from blueprints (by ID and by name) and from snapshots
- Asynchronous disk snapshots

### `blueprint.test.ts`

- Blueprint lifecycle (create, get info, delete)
- Blueprint logs retrieval
- Creating devboxes from blueprints

### `blueprint-build.test.ts`

- Blueprint builds with object-storage and directory build contexts, honoring `.dockerignore`
- Blueprint listing and retrieval by ID
- Blueprints with network policies for the build and for launched devboxes

### `snapshot.test.ts`

Expand Down
305 changes: 305 additions & 0 deletions tests/smoketests/object-oriented/blueprint-build.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,305 @@
import { LONG_TIMEOUT, uniqueName, makeClientSDK, cleanUpPolicy } from '../utils';
import { Blueprint, Devbox, NetworkPolicy, StorageObject } from '@runloop/api-client/sdk';

const sdk = makeClientSDK();

describe('smoketest: object-oriented blueprint', () => {
describe('blueprint build context with object storage and .dockerignore', () => {
test.concurrent(
'creates blueprint with COPY using object-based build context honoring .dockerignore',
async () => {
const fs = require('fs/promises');
const path = require('path');
const os = require('os');

let contextDir: string | undefined;
let storageObject: StorageObject | undefined;
let blueprint: Blueprint | undefined;
let devbox: Devbox | undefined;

try {
// Create temporary build context directory
contextDir = await fs.mkdtemp(path.join(os.tmpdir(), 'runloop-context-'));
const includedPath = path.join(contextDir, 'included.txt');
const ignoredPath = path.join(contextDir, 'ignored.txt');
const dockerignorePath = path.join(contextDir, '.dockerignore');

await fs.writeFile(includedPath, 'Hello from context');
await fs.writeFile(ignoredPath, 'This file should be ignored');
await fs.writeFile(dockerignorePath, 'ignored.txt\n');

// Upload directory as build context object (tgz)
if (!contextDir) {
throw new Error('Context directory not created before uploadFromDir');
}
storageObject = await sdk.storageObject.uploadFromDir(contextDir, {
name: uniqueName('sdk-context-object'),
ttl_ms: 3600000, // 1 hour
});

// Create blueprint that uses the uploaded object as build context
blueprint = await sdk.blueprint.create(
{
name: uniqueName('sdk-blueprint-context'),
dockerfile: `FROM ubuntu:22.04
WORKDIR /app
COPY . .`,
build_context: storageObject,
},
{ longPoll: { timeoutMs: 10 * 60 * 1000 } },
);

expect(blueprint).toBeDefined();
expect(blueprint.id).toBeTruthy();

// Create devbox from the blueprint and verify copied files
devbox = await blueprint.createDevbox({
name: uniqueName('devbox-from-context'),
launch_parameters: {
resource_size_request: 'X_SMALL',
keep_alive_time_seconds: 60 * 5, // 5 minutes
},
});

expect(devbox).toBeDefined();
expect(devbox.id).toBeTruthy();

// included.txt should be present
const includeCheck = await devbox.cmd.exec('test -f /app/included.txt');
expect(includeCheck.exitCode).toBe(0);

// Optionally assert content
const includeContentResult = await devbox.cmd.exec('cat /app/included.txt');
const includeContent = await includeContentResult.stdout();
expect(includeContent.trim()).toBe('Hello from context');

// ignored.txt should be absent due to .dockerignore
const ignoredCheck = await devbox.cmd.exec('test ! -f /app/ignored.txt');
expect(ignoredCheck.exitCode).toBe(0);

// .dockerignore itself should also not be present in the image
const dockerignoreCheck = await devbox.cmd.exec('test ! -f /app/.dockerignore');
expect(dockerignoreCheck.exitCode).toBe(0);
} finally {
if (devbox) {
await devbox.shutdown().catch(() => {});
}
if (blueprint) {
await blueprint.delete().catch(() => {});
}
if (storageObject) {
await storageObject.delete().catch(() => {});
}
if (contextDir) {
await fs.rm(contextDir, { recursive: true, force: true }).catch(() => {});
}
}
},
LONG_TIMEOUT,
);

test.concurrent(
'creates blueprint with build_context_dir parameter (string path)',
async () => {
const fs = require('fs/promises');
const path = require('path');
const os = require('os');

let contextDir: string | undefined;
let blueprint: Blueprint | undefined;
let devbox: Devbox | undefined;

try {
// Create temporary build context directory
contextDir = await fs.mkdtemp(path.join(os.tmpdir(), 'runloop-context-dir-'));
const filePath = path.join(contextDir, 'hello.txt');
await fs.writeFile(filePath, 'Hello from build_context_dir!');

// Create blueprint using build_context_dir parameter (string form)
// This tests the build_context_dir branch in Blueprint.create()
if (!contextDir) {
throw new Error('Context directory not created');
}
blueprint = await sdk.blueprint.create(
{
name: uniqueName('sdk-blueprint-context-dir'),
dockerfile: `FROM ubuntu:22.04
WORKDIR /app
COPY . .`,
build_context_dir: contextDir,
},
{ longPoll: { timeoutMs: 10 * 60 * 1000 } },
);

expect(blueprint).toBeDefined();
expect(blueprint.id).toBeTruthy();

// Verify instance methods work
const info = await blueprint.getInfo();
expect(info.id).toBe(blueprint.id);

const logs = await blueprint.logs();
expect(logs.logs).toBeDefined();

// Create devbox to verify the build context was used
devbox = await blueprint.createDevbox({
name: uniqueName('devbox-from-context-dir'),
launch_parameters: {
resource_size_request: 'X_SMALL',
keep_alive_time_seconds: 60 * 5,
},
});

expect(devbox).toBeDefined();

// Verify the file from build context is present
const result = await devbox.cmd.exec('cat /app/hello.txt');
expect(result.exitCode).toBe(0);
const content = await result.stdout();
expect(content.trim()).toBe('Hello from build_context_dir!');
} finally {
if (devbox) {
await devbox.shutdown().catch(() => {});
}
if (blueprint) {
await blueprint.delete().catch(() => {});
}
if (contextDir) {
await fs.rm(contextDir, { recursive: true, force: true }).catch(() => {});
}
}
},
LONG_TIMEOUT,
);
});

describe('blueprint list and retrieval', () => {
test.concurrent('list blueprints', async () => {
const blueprints = await sdk.blueprint.list({ limit: 10 });
expect(Array.isArray(blueprints)).toBe(true);
});

test.concurrent(
'get blueprint by ID',
async () => {
// First create a blueprint
let blueprint: Blueprint | undefined;
try {
blueprint = await sdk.blueprint.create(
{
name: uniqueName('sdk-blueprint-retrieve'),
dockerfile: 'FROM ubuntu:22.04',
},
{ longPoll: { timeoutMs: 10 * 60 * 1000 } },
);
expect(blueprint?.id).toBeTruthy();

// Retrieve it byID
const retrieved = sdk.blueprint.fromId(blueprint.id);
expect(retrieved.id).toBe(blueprint.id);
} finally {
if (blueprint) {
await blueprint.delete();
}
}
},
LONG_TIMEOUT,
);
});

describe('blueprint with network policy', () => {
test.concurrent(
'create blueprint with network_policy_id for build',
async () => {
let policy: NetworkPolicy | undefined;
let blueprint: Blueprint | undefined;
try {
// Create a network policy for the build process
policy = await sdk.networkPolicy.create({
name: uniqueName('sdk-policy-for-blueprint-build'),
allow_all: true, // Allow all for build to download packages
});
expect(policy.id).toBeTruthy();

// Create blueprint with network_policy_id at top level (for build)
blueprint = await sdk.blueprint.create(
{
name: uniqueName('sdk-blueprint-with-build-policy'),
dockerfile: 'FROM ubuntu:22.04\nRUN apt-get update',
network_policy_id: policy.id,
},
{ longPoll: { timeoutMs: 10 * 60 * 1000 } },
);

expect(blueprint).toBeDefined();
expect(blueprint.id).toBeTruthy();

// Verify blueprint was created
const info = await blueprint.getInfo();
expect(info.id).toBe(blueprint.id);
} finally {
if (blueprint) {
await blueprint.delete().catch(() => {});
}
await cleanUpPolicy(policy);
}
},
LONG_TIMEOUT,
);

test.concurrent(
'create blueprint with launch_parameters.network_policy_id',
async () => {
let policy: NetworkPolicy | undefined;
let blueprint: Blueprint | undefined;
let devbox: Devbox | undefined;
try {
// Create a network policy for devboxes launched from this blueprint
policy = await sdk.networkPolicy.create({
name: uniqueName('sdk-policy-for-blueprint-launch'),
allow_all: false,
allowed_hostnames: ['github.com'],
});
expect(policy.id).toBeTruthy();

// Create blueprint with launch_parameters including network_policy_id
blueprint = await sdk.blueprint.create(
{
name: uniqueName('sdk-blueprint-with-launch-policy'),
dockerfile: 'FROM ubuntu:22.04',
launch_parameters: {
resource_size_request: 'X_SMALL',
keep_alive_time_seconds: 60 * 5,
network_policy_id: policy.id,
},
},
{ longPoll: { timeoutMs: 10 * 60 * 1000 } },
);

expect(blueprint).toBeDefined();
expect(blueprint.id).toBeTruthy();

// Verify blueprint was created
const info = await blueprint.getInfo();
expect(info.id).toBe(blueprint.id);

// Create a devbox from the blueprint to verify it works
devbox = await blueprint.createDevbox({
name: uniqueName('devbox-from-policy-blueprint'),
});
expect(devbox).toBeDefined();
expect(devbox.id).toBeTruthy();
} finally {
if (devbox) {
await devbox.shutdown().catch(() => {});
}
if (blueprint) {
await blueprint.delete().catch(() => {});
}
await cleanUpPolicy(policy);
}
},
LONG_TIMEOUT,
);
});
});
Loading
Loading