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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 48 additions & 16 deletions src/sdk/execution.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { Runloop } from '../index';
import type * as Core from '../core';
import type { DevboxAsyncExecutionDetailView } from '../resources/devboxes/devboxes';
import { RunloopError } from '../error';
import type { DevboxAsyncExecutionDetailView, DevboxSendStdInResult } from '../resources/devboxes/devboxes';
import type { ExecutionSendStdInParams } from '../resources/devboxes/executions';
import { longPollUntil, resolveLongPollTimeoutMs, type LongPollRequestOptions } from '../lib/polling';
import { ExecutionResult } from './execution-result';

Expand Down Expand Up @@ -64,21 +66,51 @@ export class Execution {
}
}

// Doesn't work as expected, the execution is killed when the stdin is sent.
// /**
// * Send input to the execution's stdin.
// *
// * @param input - The input to send
// * @param options - Request options
// */
// async sendStdIn(input: string, options?: Core.RequestOptions): Promise<void> {
// await this.client.devboxes.executions.sendStdIn(
// this._devboxId,
// this._executionId,
// { text: input },
// options,
// );
// }
/**
* Send input to the execution's stdin. The execution must have been started with `attach_stdin: true`.
*
* @example
* ```typescript
* const execution = await devbox.cmd.execAsync('cat', { attach_stdin: true });
* await execution.sendStdIn('Hello from stdin!\n');
* await execution.closeStdIn();
* const result = await execution.result();
* ```
*
* @param {string} input - The text to write to stdin
* @param {Core.RequestOptions} [options] - Request options
* @returns {Promise<void>} Promise that resolves once the input has been delivered
* @throws {RunloopError} If the API reports that the input was not delivered
*/
async sendStdIn(input: string, options?: Core.RequestOptions): Promise<void> {
await this.sendStdInRequest({ text: input }, options);
}

/**
* Close the execution's stdin by sending EOF, so commands that read until end of input can finish.
*
* @param {Core.RequestOptions} [options] - Request options
* @returns {Promise<void>} Promise that resolves once EOF has been delivered
* @throws {RunloopError} If the API reports that EOF was not delivered
*/
async closeStdIn(options?: Core.RequestOptions): Promise<void> {
await this.sendStdInRequest({ signal: 'EOF' }, options);
}

private async sendStdInRequest(
body: ExecutionSendStdInParams,
options?: Core.RequestOptions,
): Promise<void> {
// executions.sendStdIn() would treat `{ signal: 'EOF' }` as RequestOptions (its `signal` key collides
// with the AbortSignal option) and drop the body, so post to the endpoint directly.
const response = await this.client.post<unknown, DevboxSendStdInResult>(
`/v1/devboxes/${this._devboxId}/executions/${this._executionId}/send_std_in`,
{ ...options, body },
);
if (!response.success) {
throw new RunloopError(`Failed to send stdin to execution ${this._executionId}`);
}
}

/**
* Wait for the execution to complete and return the result.
Expand Down
77 changes: 77 additions & 0 deletions tests/objects/execution.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { Execution } from '../../src/sdk/execution';
import { RunloopError } from '../../src/error';
import type { DevboxAsyncExecutionDetailView } from '../../src/resources/devboxes/devboxes';

jest.mock('../../src/index');

describe('Execution', () => {
let mockClient: any;
let execution: Execution;

beforeEach(() => {
mockClient = {
post: jest.fn(),
} as any;

const initialResult: DevboxAsyncExecutionDetailView = {
devbox_id: 'devbox-123',
execution_id: 'exec-456',
status: 'running',
};
execution = new Execution(mockClient, 'devbox-123', 'exec-456', initialResult);
});

describe('sendStdIn', () => {
it('sends text to the execution stdin', async () => {
mockClient.post.mockResolvedValue({
devbox_id: 'devbox-123',
execution_id: 'exec-456',
success: true,
});

await execution.sendStdIn('hello\n', { timeout: 1000 });

expect(mockClient.post).toHaveBeenCalledWith(
'/v1/devboxes/devbox-123/executions/exec-456/send_std_in',
{ timeout: 1000, body: { text: 'hello\n' } },
);
});

it('rejects when the API reports the input was not sent', async () => {
mockClient.post.mockResolvedValue({
devbox_id: 'devbox-123',
execution_id: 'exec-456',
success: false,
});

await expect(execution.sendStdIn('hello\n')).rejects.toThrow(RunloopError);
});
});

describe('closeStdIn', () => {
it('sends an EOF signal to the execution stdin', async () => {
mockClient.post.mockResolvedValue({
devbox_id: 'devbox-123',
execution_id: 'exec-456',
success: true,
});

await execution.closeStdIn();

expect(mockClient.post).toHaveBeenCalledWith(
'/v1/devboxes/devbox-123/executions/exec-456/send_std_in',
{ body: { signal: 'EOF' } },
);
});

it('rejects when the API reports EOF was not sent', async () => {
mockClient.post.mockResolvedValue({
devbox_id: 'devbox-123',
execution_id: 'exec-456',
success: false,
});

await expect(execution.closeStdIn()).rejects.toThrow(RunloopError);
});
});
});
23 changes: 7 additions & 16 deletions tests/smoketests/object-oriented/execution.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,33 +89,24 @@ describe('smoketest: object-oriented execution', () => {
}
});

test.skip('start execution with stdin enabled', async () => {
test('start execution with stdin enabled', async () => {
expect(devbox).toBeDefined();
execution = await devbox.cmd.execAsync('cat', {
attach_stdin: true,
});
expect(execution).toBeDefined();
expect(execution.executionId).toBeTruthy();
expect((await execution.getState()).status).toBe('running');
expect((await execution.getState()).status).not.toBe('completed');
});

test.skip('send input to execution', async () => {
test('send input to execution', async () => {
expect(execution).toBeDefined();
expect((await execution.getState()).status).toBe('running');
try {
//await execution.sendStdIn('Hello from stdin!\n');
} catch (error) {
console.error('Error sending input to execution:', error);
}

// Wait a bit for the input to be processed
await new Promise((resolve) => setTimeout(resolve, 1000));

// Kill the execution to get the result
await execution.kill();
expect((await execution.getState()).status).not.toBe('completed');
await execution.sendStdIn('Hello from stdin!\n');
await execution.closeStdIn();

const result = await execution.result();
expect(result).toBeDefined();
expect(result.exitCode).toBe(0);

const output = await result.stdout();
expect(output).toContain('Hello from stdin!');
Expand Down
Loading