From 2172e0d0d30bf732b7056c4311c16817a5a9c32b Mon Sep 17 00:00:00 2001 From: Ross Date: Mon, 28 Sep 2026 11:15:29 -0700 Subject: [PATCH 1/3] test(smoketests): run slow blueprint and devbox groups in their own files Jest runs the describe blocks of one file one after another, so the blueprint file waited for its lifecycle build before starting the build-context, retrieval and network-policy builds, and was always the last file to finish. The independent build groups and the devbox from-blueprint group now live in separate files, which Jest runs in parallel. Test names are unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) --- tests/smoketests/object-oriented/README.md | 10 + .../object-oriented/blueprint-build.test.ts | 305 ++++++++++++++++++ .../object-oriented/blueprint.test.ts | 303 +---------------- .../devbox-from-blueprint.test.ts | 126 ++++++++ .../smoketests/object-oriented/devbox.test.ts | 123 +------ 5 files changed, 444 insertions(+), 423 deletions(-) create mode 100644 tests/smoketests/object-oriented/blueprint-build.test.ts create mode 100644 tests/smoketests/object-oriented/devbox-from-blueprint.test.ts diff --git a/tests/smoketests/object-oriented/README.md b/tests/smoketests/object-oriented/README.md index f68a8401f..13cfba130 100644 --- a/tests/smoketests/object-oriented/README.md +++ b/tests/smoketests/object-oriented/README.md @@ -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` diff --git a/tests/smoketests/object-oriented/blueprint-build.test.ts b/tests/smoketests/object-oriented/blueprint-build.test.ts new file mode 100644 index 000000000..3e2fdf329 --- /dev/null +++ b/tests/smoketests/object-oriented/blueprint-build.test.ts @@ -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, + ); + }); +}); diff --git a/tests/smoketests/object-oriented/blueprint.test.ts b/tests/smoketests/object-oriented/blueprint.test.ts index 6d7878f3b..521e17b95 100644 --- a/tests/smoketests/object-oriented/blueprint.test.ts +++ b/tests/smoketests/object-oriented/blueprint.test.ts @@ -1,5 +1,5 @@ -import { SHORT_TIMEOUT, LONG_TIMEOUT, uniqueName, makeClientSDK, cleanUpPolicy } from '../utils'; -import { Blueprint, Devbox, NetworkPolicy, StorageObject } from '@runloop/api-client/sdk'; +import { SHORT_TIMEOUT, LONG_TIMEOUT, uniqueName, makeClientSDK } from '../utils'; +import { Blueprint, Devbox } from '@runloop/api-client/sdk'; const sdk = makeClientSDK(); @@ -101,303 +101,4 @@ 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, - ); - }); }); diff --git a/tests/smoketests/object-oriented/devbox-from-blueprint.test.ts b/tests/smoketests/object-oriented/devbox-from-blueprint.test.ts new file mode 100644 index 000000000..1e88a91d9 --- /dev/null +++ b/tests/smoketests/object-oriented/devbox-from-blueprint.test.ts @@ -0,0 +1,126 @@ +import { makeClientSDK, LONG_TIMEOUT, uniqueName } from '../utils'; + +const sdk = makeClientSDK(); + +describe('smoketest: object-oriented devbox', () => { + describe('devbox creation from blueprint and snapshot', () => { + test.concurrent( + 'create devbox from blueprint ID', + async () => { + // First create a blueprint with extended long-poll timeout + const blueprint = await sdk.blueprint.create( + { + name: uniqueName('sdk-blueprint-for-devbox'), + dockerfile: 'FROM ubuntu:22.04\nRUN apt-get update && apt-get install -y curl', + }, + { longPoll: { timeoutMs: 10 * 60 * 1000 } }, + ); + expect(blueprint).toBeDefined(); + + // Create devbox from blueprint using SDK method with blueprint ID + const devbox = await sdk.devbox.createFromBlueprintId(blueprint.id, { + name: uniqueName('sdk-devbox-from-blueprint-id'), + launch_parameters: { resource_size_request: 'X_SMALL', keep_alive_time_seconds: 60 * 5 }, + }); + expect(devbox).toBeDefined(); + expect(devbox.id).toBeTruthy(); + + // Verify it's running + const info = await devbox.getInfo(); + expect(info.status).toBe('running'); + + // Clean up + await devbox.shutdown(); + await blueprint.delete(); + }, + LONG_TIMEOUT, + ); + + test.concurrent( + 'create devbox from blueprint name', + async () => { + // First create a blueprint with a specific name and extended long-poll timeout + const blueprintName = uniqueName('sdk-blueprint-name-test'); + const blueprint = await sdk.blueprint.create( + { + name: blueprintName, + dockerfile: 'FROM ubuntu:22.04\nRUN apt-get update && apt-get install -y wget', + }, + { longPoll: { timeoutMs: 10 * 60 * 1000 } }, + ); + expect(blueprint).toBeDefined(); + + // Create devbox from blueprint using SDK method with blueprint name + const devbox = await sdk.devbox.createFromBlueprintName(blueprintName, { + name: uniqueName('sdk-devbox-from-blueprint-name'), + launch_parameters: { resource_size_request: 'X_SMALL', keep_alive_time_seconds: 60 * 5 }, + }); + expect(devbox).toBeDefined(); + expect(devbox.id).toBeTruthy(); + + // Verify it's running + const info = await devbox.getInfo(); + expect(info.status).toBe('running'); + + // Clean up + await devbox.shutdown(); + await blueprint.delete(); + }, + LONG_TIMEOUT, + ); + + test.concurrent('create devbox from snapshot', async () => { + // First create a devbox + const sourceDevbox = await sdk.devbox.create({ + name: uniqueName('sdk-devbox-for-snapshot'), + launch_parameters: { resource_size_request: 'X_SMALL', keep_alive_time_seconds: 60 * 5 }, + }); + expect(sourceDevbox).toBeDefined(); + + // Create a snapshot + const snapshot = await sourceDevbox.snapshotDisk({ + name: uniqueName('sdk-snapshot-for-devbox'), + commit_message: 'Test snapshot for devbox creation', + }); + expect(snapshot).toBeDefined(); + + // Create devbox from snapshot using SDK method + const devbox = await sdk.devbox.createFromSnapshot(snapshot.id, { + name: uniqueName('sdk-devbox-from-snapshot'), + launch_parameters: { resource_size_request: 'X_SMALL', keep_alive_time_seconds: 60 * 5 }, + }); + expect(devbox).toBeDefined(); + expect(devbox.id).toBeTruthy(); + + // Verify it's running + const info = await devbox.getInfo(); + expect(info.status).toBe('running'); + + // Clean up + await devbox.shutdown(); + await sourceDevbox.shutdown(); + await snapshot.delete(); + }); + + test.concurrent('snapshot disk async', async () => { + const sourceDevbox = await sdk.devbox.create({ + name: uniqueName('sdk-devbox-for-async-snapshot'), + launch_parameters: { resource_size_request: 'X_SMALL', keep_alive_time_seconds: 60 * 5 }, + }); + + let snapshot: Awaited> | undefined; + try { + snapshot = await sourceDevbox.snapshotDisk({ + name: uniqueName('sdk-async-snapshot'), + commit_message: 'Async snapshot test', + }); + expect(snapshot).toBeDefined(); + expect(snapshot.id).toBeTruthy(); + } finally { + // force=true required because the async snapshot may still be in progress + await sdk.api.devboxes.shutdown(sourceDevbox.id); + if (snapshot) await snapshot.delete(); + } + }); + }); +}); diff --git a/tests/smoketests/object-oriented/devbox.test.ts b/tests/smoketests/object-oriented/devbox.test.ts index 26a469761..09f458db7 100644 --- a/tests/smoketests/object-oriented/devbox.test.ts +++ b/tests/smoketests/object-oriented/devbox.test.ts @@ -1,6 +1,6 @@ import { toFile } from '@runloop/api-client'; import { Devbox, NetworkPolicy } from '@runloop/api-client/sdk'; -import { makeClientSDK, SHORT_TIMEOUT, LONG_TIMEOUT, uniqueName, cleanUpPolicy } from '../utils'; +import { makeClientSDK, SHORT_TIMEOUT, uniqueName, cleanUpPolicy } from '../utils'; import { uuidv7 } from 'uuidv7'; const sdk = makeClientSDK(); @@ -437,127 +437,6 @@ describe('smoketest: object-oriented devbox', () => { }); }); - describe('devbox creation from blueprint and snapshot', () => { - test.concurrent( - 'create devbox from blueprint ID', - async () => { - // First create a blueprint with extended long-poll timeout - const blueprint = await sdk.blueprint.create( - { - name: uniqueName('sdk-blueprint-for-devbox'), - dockerfile: 'FROM ubuntu:22.04\nRUN apt-get update && apt-get install -y curl', - }, - { longPoll: { timeoutMs: 10 * 60 * 1000 } }, - ); - expect(blueprint).toBeDefined(); - - // Create devbox from blueprint using SDK method with blueprint ID - const devbox = await sdk.devbox.createFromBlueprintId(blueprint.id, { - name: uniqueName('sdk-devbox-from-blueprint-id'), - launch_parameters: { resource_size_request: 'X_SMALL', keep_alive_time_seconds: 60 * 5 }, - }); - expect(devbox).toBeDefined(); - expect(devbox.id).toBeTruthy(); - - // Verify it's running - const info = await devbox.getInfo(); - expect(info.status).toBe('running'); - - // Clean up - await devbox.shutdown(); - await blueprint.delete(); - }, - LONG_TIMEOUT, - ); - - test.concurrent( - 'create devbox from blueprint name', - async () => { - // First create a blueprint with a specific name and extended long-poll timeout - const blueprintName = uniqueName('sdk-blueprint-name-test'); - const blueprint = await sdk.blueprint.create( - { - name: blueprintName, - dockerfile: 'FROM ubuntu:22.04\nRUN apt-get update && apt-get install -y wget', - }, - { longPoll: { timeoutMs: 10 * 60 * 1000 } }, - ); - expect(blueprint).toBeDefined(); - - // Create devbox from blueprint using SDK method with blueprint name - const devbox = await sdk.devbox.createFromBlueprintName(blueprintName, { - name: uniqueName('sdk-devbox-from-blueprint-name'), - launch_parameters: { resource_size_request: 'X_SMALL', keep_alive_time_seconds: 60 * 5 }, - }); - expect(devbox).toBeDefined(); - expect(devbox.id).toBeTruthy(); - - // Verify it's running - const info = await devbox.getInfo(); - expect(info.status).toBe('running'); - - // Clean up - await devbox.shutdown(); - await blueprint.delete(); - }, - LONG_TIMEOUT, - ); - - test.concurrent('create devbox from snapshot', async () => { - // First create a devbox - const sourceDevbox = await sdk.devbox.create({ - name: uniqueName('sdk-devbox-for-snapshot'), - launch_parameters: { resource_size_request: 'X_SMALL', keep_alive_time_seconds: 60 * 5 }, - }); - expect(sourceDevbox).toBeDefined(); - - // Create a snapshot - const snapshot = await sourceDevbox.snapshotDisk({ - name: uniqueName('sdk-snapshot-for-devbox'), - commit_message: 'Test snapshot for devbox creation', - }); - expect(snapshot).toBeDefined(); - - // Create devbox from snapshot using SDK method - const devbox = await sdk.devbox.createFromSnapshot(snapshot.id, { - name: uniqueName('sdk-devbox-from-snapshot'), - launch_parameters: { resource_size_request: 'X_SMALL', keep_alive_time_seconds: 60 * 5 }, - }); - expect(devbox).toBeDefined(); - expect(devbox.id).toBeTruthy(); - - // Verify it's running - const info = await devbox.getInfo(); - expect(info.status).toBe('running'); - - // Clean up - await devbox.shutdown(); - await sourceDevbox.shutdown(); - await snapshot.delete(); - }); - - test.concurrent('snapshot disk async', async () => { - const sourceDevbox = await sdk.devbox.create({ - name: uniqueName('sdk-devbox-for-async-snapshot'), - launch_parameters: { resource_size_request: 'X_SMALL', keep_alive_time_seconds: 60 * 5 }, - }); - - let snapshot: Awaited> | undefined; - try { - snapshot = await sourceDevbox.snapshotDisk({ - name: uniqueName('sdk-async-snapshot'), - commit_message: 'Async snapshot test', - }); - expect(snapshot).toBeDefined(); - expect(snapshot.id).toBeTruthy(); - } finally { - // force=true required because the async snapshot may still be in progress - await sdk.api.devboxes.shutdown(sourceDevbox.id); - if (snapshot) await snapshot.delete(); - } - }); - }); - describe('command execution with streaming callbacks', () => { let devbox: Devbox; From 704c6a098f25c15498eedf1fe4c0300257e6f085 Mon Sep 17 00:00:00 2001 From: Ross Date: Mon, 28 Sep 2026 11:15:29 -0700 Subject: [PATCH 2/3] ci(smoketests): start 32 Jest workers instead of 800 Jest treats a --maxWorkers percentage above 100% as a plain count, so "800%" started 800 worker processes on a 4-CPU runner. Every run spent about 46 seconds starting them before the first test file began. Co-Authored-By: Claude Opus 5.5 (1M context) --- .github/workflows/smoke-tests.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/smoke-tests.yml b/.github/workflows/smoke-tests.yml index e946bfa2c..4052e2275 100644 --- a/.github/workflows/smoke-tests.yml +++ b/.github/workflows/smoke-tests.yml @@ -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. From f2b1f3a9f1a2dacc92d7392b793c0dfdacbb945b Mon Sep 17 00:00:00 2001 From: Ross Date: Mon, 28 Sep 2026 11:15:30 -0700 Subject: [PATCH 3/3] ci(sdk-coverage): start 32 Jest workers instead of 800 Same fix as the smoke tests: Jest reads "800%" as 800 workers. Co-Authored-By: Claude Opus 5.5 (1M context) --- .github/workflows/sdk-coverage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/sdk-coverage.yml b/.github/workflows/sdk-coverage.yml index 288c3dc29..584de9933 100644 --- a/.github/workflows/sdk-coverage.yml +++ b/.github/workflows/sdk-coverage.yml @@ -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