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 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. 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;