diff --git a/.github/workflows/sdk-coverage.yml b/.github/workflows/sdk-coverage.yml index b2de62a34..19c5e0a35 100644 --- a/.github/workflows/sdk-coverage.yml +++ b/.github/workflows/sdk-coverage.yml @@ -16,6 +16,9 @@ on: - dev - prod +concurrency: + group: ${{ github.event_name == 'pull_request' && 'dev' || inputs.environment }} + jobs: smoke-and-coverage: runs-on: ubuntu-latest diff --git a/.github/workflows/smoke-tests.yml b/.github/workflows/smoke-tests.yml index 10bb3456c..889cdb732 100644 --- a/.github/workflows/smoke-tests.yml +++ b/.github/workflows/smoke-tests.yml @@ -23,6 +23,9 @@ on: - dev - prod +concurrency: + group: ${{ inputs.environment }} + jobs: smoke-tests: runs-on: ubuntu-latest @@ -37,7 +40,6 @@ jobs: uses: runloopai/setup-node@main with: node-version: '20' - cache: yarn - name: Install dependencies run: yarn --frozen-lockfile diff --git a/examples/blueprint-with-build-context.ts b/examples/blueprint-with-build-context.ts index b5e89ef82..a6e90b6ec 100644 --- a/examples/blueprint-with-build-context.ts +++ b/examples/blueprint-with-build-context.ts @@ -70,7 +70,7 @@ export async function recipe(ctx: RecipeContext): Promise { dockerfile: 'FROM ubuntu:22.04\nWORKDIR /app\nCOPY . .', build_context: storageObject, }, - { polling: { timeoutMs: BLUEPRINT_POLL_TIMEOUT_MS } }, + { longPoll: { timeoutMs: BLUEPRINT_POLL_TIMEOUT_MS } }, ); cleanup.add(`blueprint:${blueprint.id}`, () => sdk.blueprint.fromId(blueprint.id).delete()); diff --git a/examples/devbox-from-blueprint-lifecycle.ts b/examples/devbox-from-blueprint-lifecycle.ts index 46df0d800..1990859ca 100644 --- a/examples/devbox-from-blueprint-lifecycle.ts +++ b/examples/devbox-from-blueprint-lifecycle.ts @@ -49,7 +49,7 @@ export async function recipe(ctx: RecipeContext): Promise { name: uniqueName('example-blueprint'), dockerfile: 'FROM ubuntu:22.04\nRUN echo "Hello from your blueprint"', }, - { polling: { timeoutMs: BLUEPRINT_POLL_TIMEOUT_MS } }, + { longPoll: { timeoutMs: BLUEPRINT_POLL_TIMEOUT_MS } }, ); cleanup.add(`blueprint:${blueprint.id}`, () => sdk.blueprint.fromId(blueprint.id).delete()); diff --git a/src/resources/scenarios/scenarios.ts b/src/resources/scenarios/scenarios.ts index 0936b335c..970ae44dd 100644 --- a/src/resources/scenarios/scenarios.ts +++ b/src/resources/scenarios/scenarios.ts @@ -23,7 +23,7 @@ import { ScenariosCursorIDPage, type ScenariosCursorIDPageParams, } from '../../pagination'; -import { PollingOptions } from '@runloop/api-client/lib/polling'; +import { LongPollRequestOptions } from '@runloop/api-client/lib/polling'; import { DevboxView } from '../devboxes'; export class Scenarios extends APIResource { @@ -132,7 +132,7 @@ export class Scenarios extends APIResource { */ async startRunAndAwaitEnvReady( body: ScenarioStartRunParams, - options?: Core.RequestOptions & { polling?: Partial> }, + options?: LongPollRequestOptions, ): Promise { const run = await this.startRun(body, options); await this._client.devboxes.awaitRunning(run.devbox_id, options); diff --git a/src/sdk.ts b/src/sdk.ts index ecf3ea49e..ca41598a2 100644 --- a/src/sdk.ts +++ b/src/sdk.ts @@ -726,14 +726,12 @@ export class BlueprintOps { /** * Create a new blueprint. * @param {BlueprintCreateParams} params - Parameters for creating the blueprint. - * @param {Core.RequestOptions & { polling?: Partial> }} [options] - Request options including polling configuration. + * @param {LongPollRequestOptions} [options] - Request options with optional long-poll configuration. * @returns {Promise} A {@link Blueprint} instance. */ async create( params: BlueprintCreateParams, - options?: Core.RequestOptions & { - polling?: Partial>; - }, + options?: LongPollRequestOptions, ): Promise { return Blueprint.create(this.client, params, options); } diff --git a/src/sdk/blueprint.ts b/src/sdk/blueprint.ts index 361aab0f1..108465388 100644 --- a/src/sdk/blueprint.ts +++ b/src/sdk/blueprint.ts @@ -6,7 +6,7 @@ import type { BlueprintBuildLogsListView, } from '../resources/blueprints'; import type { DevboxCreateParams, DevboxView } from '../resources/devboxes/devboxes'; -import type { PollingOptions } from '../lib/polling'; +import type { LongPollRequestOptions } from '../lib/polling'; import type { IgnoreMatcher } from '../lib/ignore-matcher'; import { Devbox } from './devbox'; import { StorageObject } from './storage-object'; @@ -80,15 +80,13 @@ export class Blueprint { * * @param {Runloop} client - The Runloop client instance * @param {CreateParams} params - Parameters for creating the blueprint - * @param {Core.RequestOptions & { polling?: Partial> }} [options] - Request options with optional polling configuration + * @param {LongPollRequestOptions} [options] - Request options with optional long-poll configuration * @returns {Promise} A {@link Blueprint} instance with completed build */ static async create( client: Runloop, params: CreateParams, - options?: Core.RequestOptions & { - polling?: Partial>; - }, + options?: LongPollRequestOptions, ): Promise { const { build_context, build_context_dir, ...other } = params as any; let rawParams: BlueprintCreateParams; @@ -214,14 +212,12 @@ export class Blueprint { * @private * * @param {Omit} [params] - Additional devbox creation parameters (optional) - * @param {Core.RequestOptions & { polling?: Partial> }} [options] - Request options with optional polling configuration + * @param {LongPollRequestOptions} [options] - Request options with optional long-poll configuration * @returns {Promise} A new {@link Devbox} instance created from this blueprint */ async createDevbox( params?: Omit, - options?: Core.RequestOptions & { - polling?: Partial>; - }, + options?: LongPollRequestOptions, ): Promise { const createParams: DevboxCreateParams = { ...params, diff --git a/src/sdk/scenario.ts b/src/sdk/scenario.ts index 25ef414f5..0c9bb3d94 100644 --- a/src/sdk/scenario.ts +++ b/src/sdk/scenario.ts @@ -6,7 +6,7 @@ import type { ScenarioStartRunParams, } from '../resources/scenarios/scenarios'; import type { DevboxView } from '../resources/devboxes/devboxes'; -import { PollingOptions } from '../lib/polling'; +import { LongPollRequestOptions } from '../lib/polling'; import { ScenarioRun } from './scenario-run'; /** @@ -177,13 +177,10 @@ export class Scenario { * ``` * * @param {ScenarioRunParams} [params] - Run parameters - * @param {Core.RequestOptions & { polling?: Partial> }} [options] - Request options with optional polling config + * @param {LongPollRequestOptions} [options] - Request options with optional long-poll config * @returns {Promise} ScenarioRun instance with ready devbox */ - async run( - params?: ScenarioRunParams, - options?: Core.RequestOptions & { polling?: Partial> }, - ): Promise { + async run(params?: ScenarioRunParams, options?: LongPollRequestOptions): Promise { const runView = await this.client.scenarios.startRunAndAwaitEnvReady( { scenario_id: this._id, ...params }, options, diff --git a/tests/smoketests/blueprints.test.ts b/tests/smoketests/blueprints.test.ts index 40c79e626..0b73f4685 100644 --- a/tests/smoketests/blueprints.test.ts +++ b/tests/smoketests/blueprints.test.ts @@ -24,7 +24,7 @@ describe('smoketest: blueprints', () => { name: blueprintName, }, { - polling: { maxAttempts: 180, pollingIntervalMs: 5_000, timeoutMs: 30 * 60 * 1000 }, + longPoll: { timeoutMs: 30 * 60 * 1000 }, }, ); expect(created.status).toBe('build_complete'); @@ -101,7 +101,7 @@ describe('smoketest: blueprints', () => { }, }, { - polling: { maxAttempts: 180, pollingIntervalMs: 5_000, timeoutMs: 30 * 60 * 1000 }, + longPoll: { timeoutMs: 30 * 60 * 1000 }, }, ); diff --git a/tests/smoketests/devboxes.test.ts b/tests/smoketests/devboxes.test.ts index 7132a79f3..2ce6b15db 100644 --- a/tests/smoketests/devboxes.test.ts +++ b/tests/smoketests/devboxes.test.ts @@ -42,6 +42,10 @@ describe('smoketest: devboxes', () => { test.concurrent( 'create devbox with authenticated tunnel in create params (deprecated polling path)', async () => { + const warnSpy = jest.spyOn(console, 'warn').mockImplementation((...args: unknown[]) => { + if (typeof args[0] === 'string' && args[0].includes('[runloop-api-client]')) return; + process.stderr.write(`console.warn: ${args.join(' ')}\n`); + }); let devbox: DevboxView | undefined; try { devbox = await client.devboxes.createAndAwaitRunning( @@ -63,6 +67,7 @@ describe('smoketest: devboxes', () => { expect(devbox.tunnel?.auth_mode).toBe('authenticated'); expect(devbox.tunnel?.auth_token).toBeTruthy(); } finally { + warnSpy.mockRestore(); if (devbox) { await client.devboxes.shutdown(devbox.id); } @@ -178,17 +183,25 @@ describe('smoketest: devboxes', () => { ); test('await running (createAndAwaitRunning, deprecated polling path)', async () => { - const created = await client.devboxes.createAndAwaitRunning( - { - name: uniqueName('smoketest-devbox2'), - launch_parameters: { resource_size_request: 'X_SMALL', keep_alive_time_seconds: 60 * 5 }, // 5 minutes - }, - { - polling: { timeoutMs: 20 * 60 * 1000 }, - }, - ); - expect(created.status).toBe('running'); - devboxId = created.id; + const warnSpy = jest.spyOn(console, 'warn').mockImplementation((...args: unknown[]) => { + if (typeof args[0] === 'string' && args[0].includes('[runloop-api-client]')) return; + process.stderr.write(`console.warn: ${args.join(' ')}\n`); + }); + try { + const created = await client.devboxes.createAndAwaitRunning( + { + name: uniqueName('smoketest-devbox2'), + launch_parameters: { resource_size_request: 'X_SMALL', keep_alive_time_seconds: 60 * 5 }, // 5 minutes + }, + { + polling: { timeoutMs: 20 * 60 * 1000 }, + }, + ); + expect(created.status).toBe('running'); + devboxId = created.id; + } finally { + warnSpy.mockRestore(); + } }); test('list devboxes', async () => { @@ -232,17 +245,25 @@ describe('smoketest: devboxes', () => { test.concurrent( 'createAndAwaitRunning timeout (deprecated polling path)', async () => { - await expect( - client.devboxes.createAndAwaitRunning( - { - name: uniqueName('smoketest-devbox-await-running-timeout'), - launch_parameters: { launch_commands: ['sleep 70'], keep_alive_time_seconds: 30 }, - }, - { - polling: { timeoutMs: 100 }, - }, - ), - ).rejects.toThrow(); + const warnSpy = jest.spyOn(console, 'warn').mockImplementation((...args: unknown[]) => { + if (typeof args[0] === 'string' && args[0].includes('[runloop-api-client]')) return; + process.stderr.write(`console.warn: ${args.join(' ')}\n`); + }); + try { + await expect( + client.devboxes.createAndAwaitRunning( + { + name: uniqueName('smoketest-devbox-await-running-timeout'), + launch_parameters: { launch_commands: ['sleep 70'], keep_alive_time_seconds: 30 }, + }, + { + polling: { timeoutMs: 100 }, + }, + ), + ).rejects.toThrow(); + } finally { + warnSpy.mockRestore(); + } }, SHORT_TIMEOUT * 4, ); diff --git a/tests/smoketests/executions.test.ts b/tests/smoketests/executions.test.ts index 2b6bf8f1a..d1677d1f1 100644 --- a/tests/smoketests/executions.test.ts +++ b/tests/smoketests/executions.test.ts @@ -30,14 +30,22 @@ describe('smoketest: executions', () => { ); test('execute async and await completion (deprecated polling path)', async () => { - const started = await client.devboxes.executions.executeAsync(devboxId!, { - command: 'echo hello && sleep 1', - }); - execId = started.execution_id; - const completed = await client.devboxes.executions.awaitCompleted(devboxId!, execId!, { - polling: { timeoutMs: 10 * 60 * 1000 }, + const warnSpy = jest.spyOn(console, 'warn').mockImplementation((...args: unknown[]) => { + if (typeof args[0] === 'string' && args[0].includes('[runloop-api-client]')) return; + process.stderr.write(`console.warn: ${args.join(' ')}\n`); }); - expect(completed.status).toBe('completed'); + try { + const started = await client.devboxes.executions.executeAsync(devboxId!, { + command: 'echo hello && sleep 1', + }); + execId = started.execution_id; + const completed = await client.devboxes.executions.awaitCompleted(devboxId!, execId!, { + polling: { timeoutMs: 10 * 60 * 1000 }, + }); + expect(completed.status).toBe('completed'); + } finally { + warnSpy.mockRestore(); + } }); test('tail stdout logs', async () => { diff --git a/tests/smoketests/object-oriented/blueprint.test.ts b/tests/smoketests/object-oriented/blueprint.test.ts index f69249139..6d7878f3b 100644 --- a/tests/smoketests/object-oriented/blueprint.test.ts +++ b/tests/smoketests/object-oriented/blueprint.test.ts @@ -16,7 +16,7 @@ describe('smoketest: object-oriented blueprint', () => { dockerfile: 'FROM ubuntu:22.04\nRUN apt-get update && apt-get install -y curl', system_setup_commands: ['echo "Blueprint setup complete"'], }, - { polling: { timeoutMs: 10 * 60 * 1000 } }, + { longPoll: { timeoutMs: 10 * 60 * 1000 } }, ); blueprintId = blueprint.id; }, LONG_TIMEOUT); @@ -144,7 +144,7 @@ WORKDIR /app COPY . .`, build_context: storageObject, }, - { polling: { timeoutMs: 10 * 60 * 1000 } }, + { longPoll: { timeoutMs: 10 * 60 * 1000 } }, ); expect(blueprint).toBeDefined(); @@ -226,7 +226,7 @@ WORKDIR /app COPY . .`, build_context_dir: contextDir, }, - { polling: { timeoutMs: 10 * 60 * 1000 } }, + { longPoll: { timeoutMs: 10 * 60 * 1000 } }, ); expect(blueprint).toBeDefined(); @@ -288,7 +288,7 @@ COPY . .`, name: uniqueName('sdk-blueprint-retrieve'), dockerfile: 'FROM ubuntu:22.04', }, - { polling: { timeoutMs: 10 * 60 * 1000 } }, + { longPoll: { timeoutMs: 10 * 60 * 1000 } }, ); expect(blueprint?.id).toBeTruthy(); @@ -326,7 +326,7 @@ COPY . .`, dockerfile: 'FROM ubuntu:22.04\nRUN apt-get update', network_policy_id: policy.id, }, - { polling: { timeoutMs: 10 * 60 * 1000 } }, + { longPoll: { timeoutMs: 10 * 60 * 1000 } }, ); expect(blueprint).toBeDefined(); @@ -371,7 +371,7 @@ COPY . .`, network_policy_id: policy.id, }, }, - { polling: { timeoutMs: 10 * 60 * 1000 } }, + { longPoll: { timeoutMs: 10 * 60 * 1000 } }, ); expect(blueprint).toBeDefined(); diff --git a/tests/smoketests/object-oriented/devbox.test.ts b/tests/smoketests/object-oriented/devbox.test.ts index 77a947003..26a469761 100644 --- a/tests/smoketests/object-oriented/devbox.test.ts +++ b/tests/smoketests/object-oriented/devbox.test.ts @@ -441,13 +441,13 @@ describe('smoketest: object-oriented devbox', () => { test.concurrent( 'create devbox from blueprint ID', async () => { - // First create a blueprint with extended polling timeout + // 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', }, - { polling: { timeoutMs: 10 * 60 * 1000 } }, + { longPoll: { timeoutMs: 10 * 60 * 1000 } }, ); expect(blueprint).toBeDefined(); @@ -473,14 +473,14 @@ describe('smoketest: object-oriented devbox', () => { test.concurrent( 'create devbox from blueprint name', async () => { - // First create a blueprint with a specific name and extended polling timeout + // 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', }, - { polling: { timeoutMs: 10 * 60 * 1000 } }, + { longPoll: { timeoutMs: 10 * 60 * 1000 } }, ); expect(blueprint).toBeDefined(); diff --git a/tests/smoketests/object-oriented/scenario-run.test.ts b/tests/smoketests/object-oriented/scenario-run.test.ts index d9268ad60..d8affb4b3 100644 --- a/tests/smoketests/object-oriented/scenario-run.test.ts +++ b/tests/smoketests/object-oriented/scenario-run.test.ts @@ -75,7 +75,7 @@ describe('smoketest: object-oriented scenario-run', () => { expect(run).toBeDefined(); const result = await run.awaitEnvReady({ - polling: { maxAttempts: 120, pollingIntervalMs: 5_000, timeoutMs: 20 * 60 * 1000 }, + longPoll: { timeoutMs: 20 * 60 * 1000 }, }); expect(result).toBeDefined(); @@ -117,7 +117,7 @@ describe('smoketest: object-oriented scenario-run', () => { expect(run).toBeDefined(); const result = await run.scoreAndComplete({ - polling: { maxAttempts: 120, pollingIntervalMs: 5_000, timeoutMs: 20 * 60 * 1000 }, + longPoll: { timeoutMs: 20 * 60 * 1000 }, }); expect(result).toBeDefined(); @@ -193,7 +193,7 @@ describe('smoketest: object-oriented scenario-run', () => { // Wait for environment to be ready await run.awaitEnvReady({ - polling: { maxAttempts: 120, pollingIntervalMs: 5_000, timeoutMs: 20 * 60 * 1000 }, + longPoll: { timeoutMs: 20 * 60 * 1000 }, }); // Cancel the run @@ -235,7 +235,7 @@ describe('smoketest: object-oriented scenario-run', () => { // Wait for environment to be ready await run.awaitEnvReady({ - polling: { maxAttempts: 120, pollingIntervalMs: 5_000, timeoutMs: 20 * 60 * 1000 }, + longPoll: { timeoutMs: 20 * 60 * 1000 }, }); // Score @@ -245,7 +245,7 @@ describe('smoketest: object-oriented scenario-run', () => { // Wait for scoring to complete const awaitResult = await run.awaitScored({ - polling: { maxAttempts: 120, pollingIntervalMs: 5_000, timeoutMs: 20 * 60 * 1000 }, + longPoll: { timeoutMs: 20 * 60 * 1000 }, }); expect(awaitResult).toBeDefined(); expect(['scored', 'completed', 'failed']).toContain(awaitResult.state); @@ -286,12 +286,12 @@ describe('smoketest: object-oriented scenario-run', () => { // Wait for environment to be ready await run.awaitEnvReady({ - polling: { maxAttempts: 120, pollingIntervalMs: 5_000, timeoutMs: 20 * 60 * 1000 }, + longPoll: { timeoutMs: 20 * 60 * 1000 }, }); // Score and await in one call const result = await run.scoreAndAwait({ - polling: { maxAttempts: 120, pollingIntervalMs: 5_000, timeoutMs: 20 * 60 * 1000 }, + longPoll: { timeoutMs: 20 * 60 * 1000 }, }); expect(result).toBeDefined(); expect(['scored', 'completed', 'failed']).toContain(result.state); diff --git a/tests/smoketests/object-oriented/scenario.test.ts b/tests/smoketests/object-oriented/scenario.test.ts index 4373e3e34..0f83ef5e2 100644 --- a/tests/smoketests/object-oriented/scenario.test.ts +++ b/tests/smoketests/object-oriented/scenario.test.ts @@ -188,7 +188,7 @@ describe('smoketest: object-oriented scenario', () => { devboxId = run.devboxId; await run.awaitEnvReady({ - polling: { maxAttempts: 120, pollingIntervalMs: 5_000, timeoutMs: SHORT_TIMEOUT }, + longPoll: { timeoutMs: SHORT_TIMEOUT }, }); await run.cancel(); }, @@ -200,7 +200,7 @@ describe('smoketest: object-oriented scenario', () => { async () => { const run = await scenario.run( { run_name: uniqueName('sdk-run-sync') }, - { polling: { maxAttempts: 120, pollingIntervalMs: 5_000, timeoutMs: SHORT_TIMEOUT } }, + { longPoll: { timeoutMs: SHORT_TIMEOUT } }, ); expect(run).toBeInstanceOf(ScenarioRun); diff --git a/tests/smoketests/polling-streaming-flow.test.ts b/tests/smoketests/polling-streaming-flow.test.ts index 8748ffb1d..930a6135e 100644 --- a/tests/smoketests/polling-streaming-flow.test.ts +++ b/tests/smoketests/polling-streaming-flow.test.ts @@ -40,7 +40,7 @@ const client = makeClient(); const run = new ScenarioRun(client, runView.id, runView.devbox_id); await run.awaitEnvReady({ - polling: { maxAttempts: 120, pollingIntervalMs: 5_000, timeoutMs: 20 * 60 * 1000 }, + longPoll: { timeoutMs: 20 * 60 * 1000 }, }); await run.score(); @@ -50,7 +50,7 @@ const client = makeClient(); await expect( run.awaitScored({ - polling: { pollingIntervalMs: 500, maxAttempts: 500, timeoutMs: 30 * 60 * 1000 }, + longPoll: { timeoutMs: 30 * 60 * 1000 }, signal: ac.signal, }), ).rejects.toThrow(LongPollAbortError); @@ -83,7 +83,7 @@ const client = makeClient(); await expect( client.scenarios.runs.awaitScored(runView.id, { - polling: { pollingIntervalMs: 500, maxAttempts: 500, timeoutMs: 30 * 60 * 1000 }, + longPoll: { timeoutMs: 30 * 60 * 1000 }, signal: ac.signal, }), ).rejects.toThrow(LongPollAbortError); diff --git a/tests/smoketests/scenarios-benchmarks.test.ts b/tests/smoketests/scenarios-benchmarks.test.ts index 6d3dea467..686f42502 100644 --- a/tests/smoketests/scenarios-benchmarks.test.ts +++ b/tests/smoketests/scenarios-benchmarks.test.ts @@ -34,7 +34,7 @@ describe('smoketest: scenarios and benchmarks', () => { const run = await client.scenarios.startRunAndAwaitEnvReady( { scenario_id: scenarioId! }, { - polling: { maxAttempts: 120, pollingIntervalMs: 5_000, timeoutMs: 20 * 60 * 1000 }, + longPoll: { timeoutMs: 20 * 60 * 1000 }, }, ); expect(run.scenario_id).toBe(scenarioId); @@ -49,7 +49,7 @@ describe('smoketest: scenarios and benchmarks', () => { let scored: ScenarioRunView | undefined; try { scored = await client.scenarios.runs.scoreAndComplete(runId!, { - polling: { maxAttempts: 120, pollingIntervalMs: 5_000, timeoutMs: 20 * 60 * 1000 }, + longPoll: { timeoutMs: 20 * 60 * 1000 }, }); expect(['completed', 'scored', 'running', 'failed', 'timeout', 'canceled']).toContain(scored.state); } finally { diff --git a/tests/smoketests/snapshots.test.ts b/tests/smoketests/snapshots.test.ts index 8b76fa7a8..3faf58e22 100644 --- a/tests/smoketests/snapshots.test.ts +++ b/tests/smoketests/snapshots.test.ts @@ -32,6 +32,10 @@ describe('smoketest: devbox snapshots', () => { }, 30_000); test('launch devbox from snapshot (deprecated polling path)', async () => { + const warnSpy = jest.spyOn(console, 'warn').mockImplementation((...args: unknown[]) => { + if (typeof args[0] === 'string' && args[0].includes('[runloop-api-client]')) return; + process.stderr.write(`console.warn: ${args.join(' ')}\n`); + }); let devbox: DevboxView | undefined; try { devbox = await client.devboxes.createAndAwaitRunning( @@ -45,6 +49,7 @@ describe('smoketest: devbox snapshots', () => { ); expect(devbox.snapshot_id).toBe(snapshotId); } finally { + warnSpy.mockRestore(); if (devbox) { await client.devboxes.shutdown(devbox.id); }