Skip to content
Open
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
170 changes: 170 additions & 0 deletions sdk/js/__test__/shell-exec-env.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
/**
* Tests for the env parameter in ShellExecRequest.
* Validates that the env field is correctly typed and included in the request interface.
*/
import { describe, it, expect } from 'vitest';
import * as fs from 'fs';
import * as path from 'path';
import type { ShellExecRequest } from '../src/api/resources/shell/client/requests/ShellExecRequest.js';

const SRC_DIR = path.join(__dirname, '../src');

describe('ShellExecRequest env parameter', () => {
describe('TypeScript interface', () => {
it('should accept env as Record<string, string>', () => {
const request: ShellExecRequest = {
command: 'echo $MY_VAR',
env: {
MY_VAR: 'hello',
PATH: '/usr/bin:/bin',
},
};

expect(request.env).toEqual({
MY_VAR: 'hello',
PATH: '/usr/bin:/bin',
});
expect(request.command).toBe('echo $MY_VAR');
});

it('should accept env as null', () => {
const request: ShellExecRequest = {
command: 'ls',
env: null,
};

expect(request.env).toBeNull();
});

it('should accept env as undefined (omitted)', () => {
const request: ShellExecRequest = {
command: 'ls',
};

expect(request.env).toBeUndefined();
});

it('should accept empty env object', () => {
const request: ShellExecRequest = {
command: 'ls',
env: {},
};

expect(request.env).toEqual({});
});

it('should work with all other parameters combined', () => {
const request: ShellExecRequest = {
command: 'echo $DB_HOST',
id: 'session-1',
exec_dir: '/tmp',
async_mode: false,
timeout: 30,
strict: true,
no_change_timeout: 10,
hard_timeout: 60,
preserve_symlinks: false,
truncate: true,
env: {
DB_HOST: 'localhost',
DB_PORT: '5432',
NODE_ENV: 'test',
},
};

expect(request.env).toEqual({
DB_HOST: 'localhost',
DB_PORT: '5432',
NODE_ENV: 'test',
});
expect(request.command).toBe('echo $DB_HOST');
expect(request.id).toBe('session-1');
});

it('should handle env with special characters in values', () => {
const request: ShellExecRequest = {
command: 'printenv',
env: {
SPECIAL: 'value with spaces',
QUOTED: '"double quoted"',
EQUALS: 'key=value',
EMPTY: '',
UNICODE: 'unicode-\u00e9\u00e8\u00ea',
},
};

expect(request.env?.['SPECIAL']).toBe('value with spaces');
expect(request.env?.['QUOTED']).toBe('"double quoted"');
expect(request.env?.['EQUALS']).toBe('key=value');
expect(request.env?.['EMPTY']).toBe('');
expect(request.env?.['UNICODE']).toContain('unicode-');
});
});

describe('Source code verification', () => {
it('ShellExecRequest.ts should contain env field definition', () => {
const filePath = path.join(
SRC_DIR,
'api/resources/shell/client/requests/ShellExecRequest.ts'
);
const content = fs.readFileSync(filePath, 'utf-8');

expect(content).toContain('env?:');
expect(content).toContain('Record<string, string>');
expect(content).toContain('Environment variables');
});

it('Shell Client.ts should send request body including env', () => {
const filePath = path.join(
SRC_DIR,
'api/resources/shell/client/Client.ts'
);
const content = fs.readFileSync(filePath, 'utf-8');

// The JS client sends the entire request object as body,
// so env will be included automatically
expect(content).toContain('body: request');
});
});

describe('JSON serialization', () => {
it('should serialize env correctly in request body', () => {
const request: ShellExecRequest = {
command: 'env',
env: {
FOO: 'bar',
BAZ: 'qux',
},
};

const json = JSON.stringify(request);
const parsed = JSON.parse(json);

expect(parsed.env).toEqual({ FOO: 'bar', BAZ: 'qux' });
expect(parsed.command).toBe('env');
});

it('should omit env from JSON when undefined', () => {
const request: ShellExecRequest = {
command: 'ls',
};

const json = JSON.stringify(request);
const parsed = JSON.parse(json);

expect(parsed).not.toHaveProperty('env');
});

it('should include null env in JSON', () => {
const request: ShellExecRequest = {
command: 'ls',
env: null,
};

const json = JSON.stringify(request);
const parsed = JSON.parse(json);

expect(parsed.env).toBeNull();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,6 @@ export interface ShellExecRequest {
preserve_symlinks?: boolean;
/** If True, truncate output when it exceeds 30000 characters (default: True) */
truncate?: boolean;
/** Environment variables to set for the command execution. These will be merged with the existing process environment, with provided values taking precedence. */
env?: Record<string, string> | null;
}
12 changes: 12 additions & 0 deletions sdk/python/agent_sandbox/shell/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ def exec_command(
hard_timeout: typing.Optional[float] = OMIT,
preserve_symlinks: typing.Optional[bool] = OMIT,
truncate: typing.Optional[bool] = OMIT,
env: typing.Optional[typing.Dict[str, str]] = OMIT,
request_options: typing.Optional[RequestOptions] = None,
) -> ResponseShellCommandResult:
"""
Expand Down Expand Up @@ -85,6 +86,10 @@ def exec_command(
truncate : typing.Optional[bool]
If True, truncate output when it exceeds 30000 characters (default: True)

env : typing.Optional[typing.Dict[str, str]]
Environment variables to set for the command execution. These will be merged with
the existing process environment, with provided values taking precedence.

request_options : typing.Optional[RequestOptions]
Request-specific configuration.

Expand Down Expand Up @@ -115,6 +120,7 @@ def exec_command(
hard_timeout=hard_timeout,
preserve_symlinks=preserve_symlinks,
truncate=truncate,
env=env,
request_options=request_options,
)
return _response.data
Expand Down Expand Up @@ -507,6 +513,7 @@ async def exec_command(
hard_timeout: typing.Optional[float] = OMIT,
preserve_symlinks: typing.Optional[bool] = OMIT,
truncate: typing.Optional[bool] = OMIT,
env: typing.Optional[typing.Dict[str, str]] = OMIT,
request_options: typing.Optional[RequestOptions] = None,
) -> ResponseShellCommandResult:
"""
Expand Down Expand Up @@ -545,6 +552,10 @@ async def exec_command(
truncate : typing.Optional[bool]
If True, truncate output when it exceeds 30000 characters (default: True)

env : typing.Optional[typing.Dict[str, str]]
Environment variables to set for the command execution. These will be merged with
the existing process environment, with provided values taking precedence.

request_options : typing.Optional[RequestOptions]
Request-specific configuration.

Expand Down Expand Up @@ -583,6 +594,7 @@ async def main() -> None:
hard_timeout=hard_timeout,
preserve_symlinks=preserve_symlinks,
truncate=truncate,
env=env,
request_options=request_options,
)
return _response.data
Expand Down
12 changes: 12 additions & 0 deletions sdk/python/agent_sandbox/shell/raw_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ def exec_command(
hard_timeout: typing.Optional[float] = OMIT,
preserve_symlinks: typing.Optional[bool] = OMIT,
truncate: typing.Optional[bool] = OMIT,
env: typing.Optional[typing.Dict[str, str]] = OMIT,
request_options: typing.Optional[RequestOptions] = None,
) -> HttpResponse[ResponseShellCommandResult]:
"""
Expand Down Expand Up @@ -80,6 +81,10 @@ def exec_command(
truncate : typing.Optional[bool]
If True, truncate output when it exceeds 30000 characters (default: True)

env : typing.Optional[typing.Dict[str, str]]
Environment variables to set for the command execution. These will be merged with
the existing process environment, with provided values taking precedence.

request_options : typing.Optional[RequestOptions]
Request-specific configuration.

Expand All @@ -102,6 +107,7 @@ def exec_command(
"hard_timeout": hard_timeout,
"preserve_symlinks": preserve_symlinks,
"truncate": truncate,
"env": env,
},
headers={
"content-type": "application/json",
Expand Down Expand Up @@ -699,6 +705,7 @@ async def exec_command(
hard_timeout: typing.Optional[float] = OMIT,
preserve_symlinks: typing.Optional[bool] = OMIT,
truncate: typing.Optional[bool] = OMIT,
env: typing.Optional[typing.Dict[str, str]] = OMIT,
request_options: typing.Optional[RequestOptions] = None,
) -> AsyncHttpResponse[ResponseShellCommandResult]:
"""
Expand Down Expand Up @@ -737,6 +744,10 @@ async def exec_command(
truncate : typing.Optional[bool]
If True, truncate output when it exceeds 30000 characters (default: True)

env : typing.Optional[typing.Dict[str, str]]
Environment variables to set for the command execution. These will be merged with
the existing process environment, with provided values taking precedence.

request_options : typing.Optional[RequestOptions]
Request-specific configuration.

Expand All @@ -759,6 +770,7 @@ async def exec_command(
"hard_timeout": hard_timeout,
"preserve_symlinks": preserve_symlinks,
"truncate": truncate,
"env": env,
},
headers={
"content-type": "application/json",
Expand Down
Empty file added sdk/python/tests/__init__.py
Empty file.
Loading