diff --git a/.github/workflows/sdk-coverage.yml b/.github/workflows/sdk-coverage.yml index 288c3dc29..7f273e2f8 100644 --- a/.github/workflows/sdk-coverage.yml +++ b/.github/workflows/sdk-coverage.yml @@ -78,9 +78,9 @@ jobs: const testsPassed = '${{ steps.tests.outcome }}' === 'success'; const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; const COMMENT_MARKER = ''; - + let lines = [COMMENT_MARKER]; - + if (!testsPassed) { let failedTests = []; try { @@ -173,17 +173,17 @@ jobs: lines.push(`[📋 View workflow run](${runUrl})`); } } - + const comment = lines.join('\n'); - + const { data: comments } = await github.rest.issues.listComments({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number }); - + const existingComment = comments.find(c => c.body.includes(COMMENT_MARKER)); - + if (existingComment) { await github.rest.issues.updateComment({ owner: context.repo.owner, @@ -199,4 +199,3 @@ jobs: body: comment }); } - diff --git a/.github/workflows/smoke-tests.yml b/.github/workflows/smoke-tests.yml index 3ffc19b8e..0e483415e 100644 --- a/.github/workflows/smoke-tests.yml +++ b/.github/workflows/smoke-tests.yml @@ -11,7 +11,7 @@ on: required: true type: string discriminator: - default: "true" + default: 'true' type: string workflow_dispatch: inputs: diff --git a/EXAMPLES.md b/EXAMPLES.md index 515e014d0..46d76cd91 100644 --- a/EXAMPLES.md +++ b/EXAMPLES.md @@ -16,6 +16,7 @@ Runnable examples live in [`examples/`](./examples). - [Secrets with Devbox and Agent Gateway](#secrets-with-devbox) + ## Blueprint with Build Context **Use case:** Create a blueprint using the object store to provide docker build context files, then verify files are copied into the image. @@ -23,6 +24,7 @@ Runnable examples live in [`examples/`](./examples). **Tags:** `blueprint`, `object-store`, `build-context`, `devbox`, `cleanup` ### Workflow + - Create a temporary directory with sample application files - Upload the directory to object storage as build context - Create a blueprint with a Dockerfile that copies the context files @@ -31,14 +33,17 @@ Runnable examples live in [`examples/`](./examples). - Shutdown devbox and delete blueprint and storage object ### Prerequisites + - `RUNLOOP_API_KEY` ### Run + ```sh yarn tsn -T examples/blueprint-with-build-context.ts ``` ### Test + ```sh yarn test:examples ``` @@ -46,6 +51,7 @@ yarn test:examples **Source:** [`examples/blueprint-with-build-context.ts`](./examples/blueprint-with-build-context.ts) + ## Devbox From Blueprint (Run Command, Shutdown) **Use case:** Create a devbox from a blueprint, run a command, fetch logs, validate output, and cleanly tear everything down. @@ -53,6 +59,7 @@ yarn test:examples **Tags:** `devbox`, `blueprint`, `commands`, `logs`, `cleanup` ### Workflow + - Create a blueprint - Fetch blueprint build logs - Create a devbox from the blueprint @@ -62,14 +69,17 @@ yarn test:examples - Shutdown devbox and delete blueprint ### Prerequisites + - `RUNLOOP_API_KEY` ### Run + ```sh yarn tsn -T examples/devbox-from-blueprint-lifecycle.ts ``` ### Test + ```sh yarn test:examples ``` @@ -77,6 +87,7 @@ yarn test:examples **Source:** [`examples/devbox-from-blueprint-lifecycle.ts`](./examples/devbox-from-blueprint-lifecycle.ts) + ## Devbox Mounts (Agent, Code, Object) **Use case:** Launch a devbox that combines an agent mount for Claude Code, a code mount for the Runloop CLI repo, and an object mount for startup files. @@ -84,6 +95,7 @@ yarn test:examples **Tags:** `devbox`, `mounts`, `agent`, `code`, `object`, `claude-code`, `agent-gateway`, `ttl` ### Workflow + - Create or reuse an agent by name - Create a secret for an agent and route it through agent gateway - Upload a temporary directory as a storage object with a TTL @@ -93,15 +105,18 @@ yarn test:examples - Shutdown the devbox and delete the temporary secret and object ### Prerequisites + - `RUNLOOP_API_KEY` - `ANTHROPIC_API_KEY` ### Run + ```sh ANTHROPIC_API_KEY=sk-ant-xxx yarn tsn -T examples/devbox-mounts.ts ``` ### Test + ```sh yarn test:examples ``` @@ -109,6 +124,7 @@ yarn test:examples **Source:** [`examples/devbox-mounts.ts`](./examples/devbox-mounts.ts) + ## Devbox Snapshots (Suspend, Resume, Restore, Delete) **Use case:** Upload a file to a devbox, preserve it across suspend and resume, create a disk snapshot, restore multiple devboxes from that snapshot, mutate each copy independently, and delete the snapshot when finished. @@ -116,6 +132,7 @@ yarn test:examples **Tags:** `devbox`, `snapshot`, `suspend`, `resume`, `files`, `cleanup` ### Workflow + - Create a source devbox - Upload a file and mutate it into a shared baseline - Suspend and resume the source devbox @@ -125,14 +142,17 @@ yarn test:examples - Shutdown the devboxes and delete the snapshot ### Prerequisites + - `RUNLOOP_API_KEY` ### Run + ```sh yarn tsn -T examples/devbox-snapshots.ts ``` ### Test + ```sh yarn test:examples ``` @@ -140,6 +160,7 @@ yarn test:examples **Source:** [`examples/devbox-snapshots.ts`](./examples/devbox-snapshots.ts) + ## Devbox Tunnel (HTTP Server Access) **Use case:** Create a devbox with a tunnel, start an HTTP server, and access the server from the local machine through the tunnel. @@ -147,6 +168,7 @@ yarn test:examples **Tags:** `devbox`, `tunnel`, `networking`, `http` ### Workflow + - Create a devbox with a tunnel - Start an HTTP server inside the devbox - Read the tunnel details from the devbox @@ -155,14 +177,17 @@ yarn test:examples - Shutdown the devbox ### Prerequisites + - `RUNLOOP_API_KEY` ### Run + ```sh yarn tsn -T examples/devbox-tunnel.ts ``` ### Test + ```sh yarn test:examples ``` @@ -170,6 +195,7 @@ yarn test:examples **Source:** [`examples/devbox-tunnel.ts`](./examples/devbox-tunnel.ts) + ## MCP Hub + Claude Code + GitHub **Use case:** Connect Claude Code running in a devbox to GitHub tools through MCP Hub without exposing raw GitHub credentials to the devbox. @@ -177,6 +203,7 @@ yarn test:examples **Tags:** `mcp`, `devbox`, `github`, `commands`, `cleanup` ### Workflow + - Create an MCP config for GitHub - Store GitHub token as a Runloop secret - Launch a devbox with MCP Hub wiring @@ -185,16 +212,19 @@ yarn test:examples - Shutdown devbox and clean up cloud resources ### Prerequisites + - `RUNLOOP_API_KEY` - `GITHUB_TOKEN (GitHub PAT with repo scope)` - `ANTHROPIC_API_KEY` ### Run + ```sh GITHUB_TOKEN=ghp_xxx ANTHROPIC_API_KEY=sk-ant-xxx yarn tsn -T examples/mcp-github-tools.ts ``` ### Test + ```sh yarn test:examples ``` @@ -202,6 +232,7 @@ yarn test:examples **Source:** [`examples/mcp-github-tools.ts`](./examples/mcp-github-tools.ts) + ## Secrets with Devbox and Agent Gateway **Use case:** Use a normal secret for sensitive app data in the devbox and agent gateway for upstream API credentials that should never be exposed to the agent. @@ -209,6 +240,7 @@ yarn test:examples **Tags:** `secrets`, `devbox`, `agent-gateway`, `credentials`, `environment-variables`, `cleanup` ### Workflow + - Create a secret for application data that should be available inside the devbox - Create a separate secret for an upstream API credential - Create an agent gateway config for an upstream API @@ -217,14 +249,17 @@ yarn test:examples - Shutdown the devbox and delete the gateway config and both secrets ### Prerequisites + - `RUNLOOP_API_KEY` ### Run + ```sh yarn tsn -T examples/secrets-with-devbox.ts ``` ### Test + ```sh yarn test:examples ``` diff --git a/packages/mcp-server/manifest.json b/packages/mcp-server/manifest.json index 17640c75c..d8649021f 100644 --- a/packages/mcp-server/manifest.json +++ b/packages/mcp-server/manifest.json @@ -18,9 +18,7 @@ "entry_point": "index.js", "mcp_config": { "command": "node", - "args": [ - "${__dirname}/index.js" - ], + "args": ["${__dirname}/index.js"], "env": { "RUNLOOP_API_KEY": "${user_config.RUNLOOP_API_KEY}" } @@ -41,7 +39,5 @@ "node": ">=18.0.0" } }, - "keywords": [ - "api" - ] + "keywords": ["api"] } diff --git a/scripts/generate-examples-md.cjs b/scripts/generate-examples-md.cjs index 6d92b4874..3b2d1bce0 100644 --- a/scripts/generate-examples-md.cjs +++ b/scripts/generate-examples-md.cjs @@ -27,10 +27,7 @@ function readExampleFiles() { .readdirSync(EXAMPLES_DIR) .filter( (file) => - file.endsWith('.ts') && - !file.startsWith('_') && - file !== 'types.ts' && - file !== 'registry.ts', + file.endsWith('.ts') && !file.startsWith('_') && file !== 'types.ts' && file !== 'registry.ts', ) .sort(); } @@ -107,9 +104,7 @@ function ensureLlmsReferences(examples) { } if (referencedFiles.size === 0) { - throw new Error( - `${path.relative(ROOT, LLMS_FILE)}: expected at least one reference to examples/*.ts`, - ); + throw new Error(`${path.relative(ROOT, LLMS_FILE)}: expected at least one reference to examples/*.ts`); } const generatedFiles = new Set(examples.map((example) => example.fileName)); @@ -125,6 +120,7 @@ function ensureLlmsReferences(examples) { function markdownForExample(example) { return [ ``, + '', `## ${example.title}`, '', `**Use case:** ${example.use_case}`, @@ -132,17 +128,21 @@ function markdownForExample(example) { `**Tags:** ${example.tags.map((tag) => `\`${tag}\``).join(', ')}`, '', '### Workflow', + '', ...example.workflow.map((step) => `- ${step}`), '', '### Prerequisites', + '', ...example.prerequisites.map((item) => `- \`${item}\``), '', '### Run', + '', '```sh', example.run, '```', '', '### Test', + '', '```sh', example.test, '```', @@ -256,7 +256,9 @@ function main() { fs.writeFileSync(OUTPUT_FILE, markdown); fs.writeFileSync(OUTPUT_REGISTRY_FILE, registrySource); process.stdout.write(`Wrote ${path.relative(ROOT, OUTPUT_FILE)} from ${examples.length} example(s)\n`); - process.stdout.write(`Wrote ${path.relative(ROOT, OUTPUT_REGISTRY_FILE)} from ${examples.length} example(s)\n`); + process.stdout.write( + `Wrote ${path.relative(ROOT, OUTPUT_REGISTRY_FILE)} from ${examples.length} example(s)\n`, + ); } main(); diff --git a/tests/lib/devbox-state.test.ts b/tests/lib/devbox-state.test.ts index 8282df88c..15c2a79c7 100644 --- a/tests/lib/devbox-state.test.ts +++ b/tests/lib/devbox-state.test.ts @@ -4,7 +4,9 @@ import { APIError } from '../../src/error'; type MockDevbox = { status: string; id: string }; -function makeOptions(overrides: Partial> = {}): DevboxStateWaitOptions { +function makeOptions( + overrides: Partial> = {}, +): DevboxStateWaitOptions { return { client: overrides.client ?? { post: jest.fn() }, devboxId: 'dbx-123', @@ -47,37 +49,53 @@ describe('awaitDevboxState', () => { const failure: MockDevbox = { status: 'failure', id: 'dbx-123' }; const post = jest.fn().mockResolvedValue(failure); - await expect( - awaitDevboxState(makeOptions({ client: { post } })), - ).rejects.toThrow('Devbox dbx-123 ended in failure'); + await expect(awaitDevboxState(makeOptions({ client: { post } }))).rejects.toThrow( + 'Devbox dbx-123 ended in failure', + ); }); test('should use timeoutMs field for the long-poll deadline', async () => { const post = jest.fn().mockImplementation( - (_url: string, opts: { signal?: AbortSignal }) => new Promise((resolve, reject) => { - const timer = setTimeout(() => resolve({ status: 'provisioning', id: 'dbx-123' }), 100); - opts?.signal?.addEventListener('abort', () => { clearTimeout(timer); reject(new Error('aborted')); }, { once: true }); - }), + (_url: string, opts: { signal?: AbortSignal }) => + new Promise((resolve, reject) => { + const timer = setTimeout(() => resolve({ status: 'provisioning', id: 'dbx-123' }), 100); + opts?.signal?.addEventListener( + 'abort', + () => { + clearTimeout(timer); + reject(new Error('aborted')); + }, + { once: true }, + ); + }), ); - await expect( - awaitDevboxState(makeOptions({ client: { post }, timeoutMs: 150 })), - ).rejects.toThrow(PollingTimeoutError); + await expect(awaitDevboxState(makeOptions({ client: { post }, timeoutMs: 150 }))).rejects.toThrow( + PollingTimeoutError, + ); }); test('should enforce timeout mid-request by aborting the request', async () => { const post = jest.fn().mockImplementation( - (_url: string, opts: { signal?: AbortSignal }) => new Promise((resolve, reject) => { - const timer = setTimeout(() => resolve({ status: 'provisioning', id: 'dbx-123' }), 5000); - timer.unref(); - opts?.signal?.addEventListener('abort', () => { clearTimeout(timer); reject(new Error('aborted')); }, { once: true }); - }), + (_url: string, opts: { signal?: AbortSignal }) => + new Promise((resolve, reject) => { + const timer = setTimeout(() => resolve({ status: 'provisioning', id: 'dbx-123' }), 5000); + timer.unref(); + opts?.signal?.addEventListener( + 'abort', + () => { + clearTimeout(timer); + reject(new Error('aborted')); + }, + { once: true }, + ); + }), ); const start = Date.now(); - await expect( - awaitDevboxState(makeOptions({ client: { post }, timeoutMs: 100 })), - ).rejects.toThrow(PollingTimeoutError); + await expect(awaitDevboxState(makeOptions({ client: { post }, timeoutMs: 100 }))).rejects.toThrow( + PollingTimeoutError, + ); const elapsed = Date.now() - start; expect(elapsed).toBeLessThan(1000); }); diff --git a/tests/lib/long-poll-options.test.ts b/tests/lib/long-poll-options.test.ts index 658109974..858e19fc3 100644 --- a/tests/lib/long-poll-options.test.ts +++ b/tests/lib/long-poll-options.test.ts @@ -1,9 +1,15 @@ -import { PollingTimeoutError, resolveLongPollTimeoutMs, _resetDeprecationWarning } from '../../src/lib/polling'; +import { + PollingTimeoutError, + resolveLongPollTimeoutMs, + _resetDeprecationWarning, +} from '../../src/lib/polling'; import { awaitDevboxState, DevboxStateWaitOptions } from '../../src/lib/devbox-state'; type MockDevbox = { status: string; id: string }; -function makeOptions(overrides: Partial> = {}): DevboxStateWaitOptions { +function makeOptions( + overrides: Partial> = {}, +): DevboxStateWaitOptions { return { client: overrides.client ?? { post: jest.fn() }, devboxId: 'dbx-123', @@ -60,27 +66,32 @@ describe('LongPollRequestOptions resolution', () => { describe('end-to-end via awaitDevboxState', () => { function slowTransition(): jest.Mock { - return jest.fn().mockImplementation( - () => new Promise((resolve) => setTimeout(() => resolve({ status: 'provisioning', id: 'dbx-123' }), 100)), - ); + return jest + .fn() + .mockImplementation( + () => + new Promise((resolve) => + setTimeout(() => resolve({ status: 'provisioning', id: 'dbx-123' }), 100), + ), + ); } test('times out via longPoll.timeoutMs path', async () => { const post = slowTransition(); const timeoutMs = resolveTimeoutMs({ longPoll: { timeoutMs: 150 } }); - await expect( - awaitDevboxState(makeOptions({ client: { post }, timeoutMs })), - ).rejects.toThrow(PollingTimeoutError); + await expect(awaitDevboxState(makeOptions({ client: { post }, timeoutMs }))).rejects.toThrow( + PollingTimeoutError, + ); }); test('times out via deprecated polling.timeoutMs path', async () => { const post = slowTransition(); const timeoutMs = resolveTimeoutMs({ polling: { timeoutMs: 150 } }); - await expect( - awaitDevboxState(makeOptions({ client: { post }, timeoutMs })), - ).rejects.toThrow(PollingTimeoutError); + await expect(awaitDevboxState(makeOptions({ client: { post }, timeoutMs }))).rejects.toThrow( + PollingTimeoutError, + ); }); test('longPoll.timeoutMs wins over polling.timeoutMs (generous longPoll, tiny polling)', async () => { @@ -145,9 +156,7 @@ describe('resolveLongPollTimeoutMs deprecation warnings', () => { test('warns when ignored polling fields are present', () => { resolveLongPollTimeoutMs({ polling: { maxAttempts: 5 } } as any); expect(warnSpy).toHaveBeenCalledTimes(1); - expect(warnSpy).toHaveBeenCalledWith( - expect.stringContaining('maxAttempts'), - ); + expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('maxAttempts')); }); test('lists all ignored fields in a single warning', () => { @@ -170,9 +179,7 @@ describe('resolveLongPollTimeoutMs deprecation warnings', () => { test('warns about deprecated polling.timeoutMs when longPoll is absent', () => { resolveLongPollTimeoutMs({ polling: { timeoutMs: 3000 } }); expect(warnSpy).toHaveBeenCalledTimes(1); - expect(warnSpy).toHaveBeenCalledWith( - expect.stringContaining('deprecated'), - ); + expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('deprecated')); }); test('does not warn when only longPoll is used', () => { @@ -189,7 +196,9 @@ describe('resolveLongPollTimeoutMs deprecation warnings', () => { test('still returns the correct timeoutMs', () => { expect(resolveLongPollTimeoutMs({ polling: { maxAttempts: 5, timeoutMs: 3000 } } as any)).toBe(3000); _resetDeprecationWarning(); - expect(resolveLongPollTimeoutMs({ longPoll: { timeoutMs: 7000 }, polling: { timeoutMs: 1000 } })).toBe(7000); + expect(resolveLongPollTimeoutMs({ longPoll: { timeoutMs: 7000 }, polling: { timeoutMs: 1000 } })).toBe( + 7000, + ); _resetDeprecationWarning(); expect(resolveLongPollTimeoutMs(undefined)).toBeUndefined(); }); diff --git a/tests/lib/streaming-reconnection.test.ts b/tests/lib/streaming-reconnection.test.ts index 8a140ed0e..86d2d530b 100644 --- a/tests/lib/streaming-reconnection.test.ts +++ b/tests/lib/streaming-reconnection.test.ts @@ -415,13 +415,10 @@ describe('withStreamAutoReconnect', () => { controller: new AbortController(), } as unknown as APIResponseProps; - const p = new StreamBackedAPIPromise( - Promise.resolve(props), - () => { - dataStarted = true; - return Promise.resolve(new Stream(async function* () {}, props.controller)); - }, - ); + const p = new StreamBackedAPIPromise(Promise.resolve(props), () => { + dataStarted = true; + return Promise.resolve(new Stream(async function* () {}, props.controller)); + }); expect(dataStarted).toBe(false); await p.asResponse(); diff --git a/tests/objects/axon.test.ts b/tests/objects/axon.test.ts index 3b72c8793..61ead1983 100644 --- a/tests/objects/axon.test.ts +++ b/tests/objects/axon.test.ts @@ -162,11 +162,7 @@ describe('Axon', () => { const stream = await axon.subscribeSse(); - expect(mockClient.axons.subscribeSse).toHaveBeenCalledWith( - 'axn_123456789', - undefined, - undefined, - ); + expect(mockClient.axons.subscribeSse).toHaveBeenCalledWith('axn_123456789', undefined, undefined); expect(stream).toBe(mockStream); }); @@ -176,11 +172,9 @@ describe('Axon', () => { await axon.subscribeSse(undefined, { timeout: 60000 }); - expect(mockClient.axons.subscribeSse).toHaveBeenCalledWith( - 'axn_123456789', - undefined, - { timeout: 60000 }, - ); + expect(mockClient.axons.subscribeSse).toHaveBeenCalledWith('axn_123456789', undefined, { + timeout: 60000, + }); }); }); diff --git a/tests/objects/scorer.test.ts b/tests/objects/scorer.test.ts index ddcc82672..65daf93a5 100644 --- a/tests/objects/scorer.test.ts +++ b/tests/objects/scorer.test.ts @@ -1,8 +1,5 @@ import { Scorer } from '../../src/sdk/scorer'; -import type { - ScorerRetrieveResponse, - ScorerUpdateResponse, -} from '../../src/resources/scenarios/scorers'; +import type { ScorerRetrieveResponse, ScorerUpdateResponse } from '../../src/resources/scenarios/scorers'; // Mock the Runloop client jest.mock('../../src/index'); @@ -28,7 +25,6 @@ describe('Scorer', () => { type: 'my_custom_scorer', bash_script: 'echo "1.0"', }; - }); describe('fromId', () => { @@ -187,6 +183,5 @@ describe('Scorer', () => { 'Update failed', ); }); - }); }); diff --git a/tests/polling.test.ts b/tests/polling.test.ts index a3dbc7ff8..2f4369702 100644 --- a/tests/polling.test.ts +++ b/tests/polling.test.ts @@ -460,10 +460,7 @@ describe('Polling', () => { const mockResult = { status: 'provisioning', id: 'x' }; const running = { status: 'running', id: 'x' }; const initialRequest = jest.fn().mockResolvedValue(mockResult); - const pollingRequest = jest - .fn() - .mockResolvedValueOnce(mockResult) - .mockResolvedValueOnce(running); + const pollingRequest = jest.fn().mockResolvedValueOnce(mockResult).mockResolvedValueOnce(running); await poll(initialRequest, pollingRequest, { shouldStop: (r: any) => r.status === 'running', @@ -624,9 +621,7 @@ describe('longPollUntil', () => { const serverError = new APIError(500, {}, 'Internal Server Error', {}); const request = jest.fn().mockRejectedValue(serverError); - await expect( - longPollUntil(request, { shouldStop: () => true }), - ).rejects.toThrow(serverError); + await expect(longPollUntil(request, { shouldStop: () => true })).rejects.toThrow(serverError); expect(request).toHaveBeenCalledTimes(1); }); @@ -634,17 +629,23 @@ describe('longPollUntil', () => { const error = new Error('Network failure'); const request = jest.fn().mockRejectedValue(error); - await expect( - longPollUntil(request, { shouldStop: () => true }), - ).rejects.toThrow(error); + await expect(longPollUntil(request, { shouldStop: () => true })).rejects.toThrow(error); }); test('should throw PollingTimeoutError when timeoutMs is exceeded', async () => { const request = jest.fn().mockImplementation( - (signal: AbortSignal) => new Promise((resolve, reject) => { - const timer = setTimeout(() => resolve({ status: 'provisioning' }), 100); - signal.addEventListener('abort', () => { clearTimeout(timer); reject(new Error('aborted')); }, { once: true }); - }), + (signal: AbortSignal) => + new Promise((resolve, reject) => { + const timer = setTimeout(() => resolve({ status: 'provisioning' }), 100); + signal.addEventListener( + 'abort', + () => { + clearTimeout(timer); + reject(new Error('aborted')); + }, + { once: true }, + ); + }), ); await expect( @@ -657,11 +658,19 @@ describe('longPollUntil', () => { test('should enforce timeout mid-request by aborting the request', async () => { const request = jest.fn().mockImplementation( - (signal: AbortSignal) => new Promise((resolve, reject) => { - const timer = setTimeout(() => resolve({ status: 'provisioning' }), 5000); - timer.unref(); - signal.addEventListener('abort', () => { clearTimeout(timer); reject(new Error('aborted')); }, { once: true }); - }), + (signal: AbortSignal) => + new Promise((resolve, reject) => { + const timer = setTimeout(() => resolve({ status: 'provisioning' }), 5000); + timer.unref(); + signal.addEventListener( + 'abort', + () => { + clearTimeout(timer); + reject(new Error('aborted')); + }, + { once: true }, + ); + }), ); const start = Date.now(); @@ -678,13 +687,13 @@ describe('longPollUntil', () => { test('should throw when timeoutMs is zero or negative', async () => { const request = jest.fn(); - await expect( - longPollUntil(request, { timeoutMs: 0, shouldStop: () => true }), - ).rejects.toThrow('timeoutMs must be positive'); + await expect(longPollUntil(request, { timeoutMs: 0, shouldStop: () => true })).rejects.toThrow( + 'timeoutMs must be positive', + ); - await expect( - longPollUntil(request, { timeoutMs: -1, shouldStop: () => true }), - ).rejects.toThrow('timeoutMs must be positive'); + await expect(longPollUntil(request, { timeoutMs: -1, shouldStop: () => true })).rejects.toThrow( + 'timeoutMs must be positive', + ); }); test('should succeed within timeoutMs', async () => { @@ -761,11 +770,19 @@ describe('longPollUntil', () => { test('should throw LongPollAbortError when signal is aborted mid-request', async () => { const controller = new AbortController(); const request = jest.fn().mockImplementation( - (signal: AbortSignal) => new Promise((resolve, reject) => { - const timer = setTimeout(() => resolve({ status: 'provisioning' }), 5000); - timer.unref(); - signal.addEventListener('abort', () => { clearTimeout(timer); reject(new Error('aborted')); }, { once: true }); - }), + (signal: AbortSignal) => + new Promise((resolve, reject) => { + const timer = setTimeout(() => resolve({ status: 'provisioning' }), 5000); + timer.unref(); + signal.addEventListener( + 'abort', + () => { + clearTimeout(timer); + reject(new Error('aborted')); + }, + { once: true }, + ); + }), ); const start = Date.now(); @@ -809,7 +826,14 @@ describe('longPollUntil', () => { return new Promise((resolve, reject) => { const timer = setTimeout(() => resolve({ status: 'provisioning' }), 5000); timer.unref(); - signal.addEventListener('abort', () => { clearTimeout(timer); reject(new Error('aborted')); }, { once: true }); + signal.addEventListener( + 'abort', + () => { + clearTimeout(timer); + reject(new Error('aborted')); + }, + { once: true }, + ); }); } return Promise.resolve(provisioning); @@ -833,11 +857,19 @@ describe('longPollUntil', () => { test('abort signal should work together with timeoutMs', async () => { const controller = new AbortController(); const request = jest.fn().mockImplementation( - (signal: AbortSignal) => new Promise((resolve, reject) => { - const timer = setTimeout(() => resolve({ status: 'provisioning' }), 5000); - timer.unref(); - signal.addEventListener('abort', () => { clearTimeout(timer); reject(new Error('aborted')); }, { once: true }); - }), + (signal: AbortSignal) => + new Promise((resolve, reject) => { + const timer = setTimeout(() => resolve({ status: 'provisioning' }), 5000); + timer.unref(); + signal.addEventListener( + 'abort', + () => { + clearTimeout(timer); + reject(new Error('aborted')); + }, + { once: true }, + ); + }), ); setTimeout(() => controller.abort(), 50); @@ -853,16 +885,21 @@ describe('longPollUntil', () => { test('should pass AbortSignal to request and abort it on timeout', async () => { const receivedSignals: AbortSignal[] = []; - const request = jest.fn().mockImplementation( - (signal: AbortSignal) => { - receivedSignals.push(signal); - return new Promise((resolve, reject) => { - const timer = setTimeout(() => resolve({ status: 'provisioning' }), 5000); - timer.unref(); - signal.addEventListener('abort', () => { clearTimeout(timer); reject(new Error('aborted')); }, { once: true }); - }); - }, - ); + const request = jest.fn().mockImplementation((signal: AbortSignal) => { + receivedSignals.push(signal); + return new Promise((resolve, reject) => { + const timer = setTimeout(() => resolve({ status: 'provisioning' }), 5000); + timer.unref(); + signal.addEventListener( + 'abort', + () => { + clearTimeout(timer); + reject(new Error('aborted')); + }, + { once: true }, + ); + }); + }); await expect( longPollUntil(request, { @@ -878,16 +915,21 @@ describe('longPollUntil', () => { test('should pass AbortSignal to request and abort it on external signal', async () => { const controller = new AbortController(); const receivedSignals: AbortSignal[] = []; - const request = jest.fn().mockImplementation( - (signal: AbortSignal) => { - receivedSignals.push(signal); - return new Promise((resolve, reject) => { - const timer = setTimeout(() => resolve({ status: 'provisioning' }), 5000); - timer.unref(); - signal.addEventListener('abort', () => { clearTimeout(timer); reject(new Error('aborted')); }, { once: true }); - }); - }, - ); + const request = jest.fn().mockImplementation((signal: AbortSignal) => { + receivedSignals.push(signal); + return new Promise((resolve, reject) => { + const timer = setTimeout(() => resolve({ status: 'provisioning' }), 5000); + timer.unref(); + signal.addEventListener( + 'abort', + () => { + clearTimeout(timer); + reject(new Error('aborted')); + }, + { once: true }, + ); + }); + }); setTimeout(() => controller.abort(), 50); diff --git a/tests/resources/executions-stream-offset.test.ts b/tests/resources/executions-stream-offset.test.ts index ff59a2f93..71a11ece7 100644 --- a/tests/resources/executions-stream-offset.test.ts +++ b/tests/resources/executions-stream-offset.test.ts @@ -4,10 +4,7 @@ import { Executions, type ExecutionUpdateChunk } from '../../src/resources/devbo import { Stream } from '../../src/streaming'; import type { Runloop } from '../../src/index'; -function sseChunkStreamPromise( - sseBody: string, - path: string, -): APIPromise> { +function sseChunkStreamPromise(sseBody: string, path: string): APIPromise> { const controller = new AbortController(); const stream = Stream.fromSSEResponse( new Response(sseBody, { headers: { 'content-type': 'text/event-stream' } }) as any, @@ -54,11 +51,7 @@ describe('Executions stream offset with reconnect', () => { pull(c) { pullCount += 1; if (pullCount === 1) { - c.enqueue( - encoder.encode( - `data: ${JSON.stringify({ output: 'a', offset: 60 })}\n\n`, - ), - ); + c.enqueue(encoder.encode(`data: ${JSON.stringify({ output: 'a', offset: 60 })}\n\n`)); } else { c.error(timeout); } diff --git a/tests/smoketests/devboxes.test.ts b/tests/smoketests/devboxes.test.ts index 2ce6b15db..ab399050f 100644 --- a/tests/smoketests/devboxes.test.ts +++ b/tests/smoketests/devboxes.test.ts @@ -184,9 +184,9 @@ describe('smoketest: devboxes', () => { test('await running (createAndAwaitRunning, 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`); - }); + 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( { @@ -246,9 +246,9 @@ describe('smoketest: devboxes', () => { 'createAndAwaitRunning timeout (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`); - }); + 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( diff --git a/tests/smoketests/polling-streaming-flow.test.ts b/tests/smoketests/polling-streaming-flow.test.ts index 930a6135e..9c9c035ab 100644 --- a/tests/smoketests/polling-streaming-flow.test.ts +++ b/tests/smoketests/polling-streaming-flow.test.ts @@ -138,7 +138,11 @@ const client = makeClient(); longPoll: { timeoutMs: 10 * 60 * 1000 }, }); - const stream = await client.devboxes.executions.streamStdoutUpdates(devbox.id, started.execution_id, {}); + const stream = await client.devboxes.executions.streamStdoutUpdates( + devbox.id, + started.execution_id, + {}, + ); let out = ''; for await (const chunk of stream) { out += chunk.output;