grasp-sdk 0.1.9__py3-none-any.whl → 0.2.0b1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of grasp-sdk might be problematic. Click here for more details.
- examples/example_async_context.py +122 -0
- examples/example_binary_file_support.py +127 -0
- examples/example_grasp_usage.py +82 -0
- examples/example_readfile_usage.py +136 -0
- examples/grasp_terminal.py +111 -0
- examples/grasp_usage.py +111 -0
- examples/test_async_context.py +64 -0
- examples/test_grasp_classes.py +79 -0
- examples/test_python_script.py +160 -0
- examples/test_removed_methods.py +80 -0
- examples/test_terminal_updates.py +196 -0
- grasp_sdk/__init__.py +129 -239
- grasp_sdk/grasp/__init__.py +24 -0
- grasp_sdk/grasp/browser.py +162 -0
- grasp_sdk/grasp/index.py +127 -0
- grasp_sdk/grasp/server.py +250 -0
- grasp_sdk/grasp/session.py +146 -0
- grasp_sdk/grasp/utils.py +90 -0
- grasp_sdk/models/__init__.py +11 -6
- grasp_sdk/services/__init__.py +8 -1
- grasp_sdk/services/browser.py +68 -28
- grasp_sdk/services/filesystem.py +123 -0
- grasp_sdk/services/sandbox.py +404 -26
- grasp_sdk/services/terminal.py +182 -0
- grasp_sdk/utils/__init__.py +1 -2
- grasp_sdk/utils/auth.py +6 -8
- grasp_sdk/utils/config.py +2 -32
- {grasp_sdk-0.1.9.dist-info → grasp_sdk-0.2.0b1.dist-info}/METADATA +2 -3
- grasp_sdk-0.2.0b1.dist-info/RECORD +33 -0
- {grasp_sdk-0.1.9.dist-info → grasp_sdk-0.2.0b1.dist-info}/top_level.txt +1 -0
- grasp_sdk-0.1.9.dist-info/RECORD +0 -14
- {grasp_sdk-0.1.9.dist-info → grasp_sdk-0.2.0b1.dist-info}/WHEEL +0 -0
- {grasp_sdk-0.1.9.dist-info → grasp_sdk-0.2.0b1.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""测试 Grasp 异步上下文管理器功能"""
|
|
3
|
+
|
|
4
|
+
import asyncio
|
|
5
|
+
import os
|
|
6
|
+
from grasp_sdk import Grasp
|
|
7
|
+
|
|
8
|
+
async def test_async_context_manager():
|
|
9
|
+
"""测试异步上下文管理器功能"""
|
|
10
|
+
print("测试 Grasp 异步上下文管理器...")
|
|
11
|
+
|
|
12
|
+
# 初始化 Grasp SDK
|
|
13
|
+
grasp = Grasp({'apiKey': 'test-key'})
|
|
14
|
+
|
|
15
|
+
# 测试 launch_context 方法
|
|
16
|
+
print("\n1. 测试 launch_context 方法:")
|
|
17
|
+
context_manager = grasp.launch_context({
|
|
18
|
+
'browser': {
|
|
19
|
+
'type': 'chromium',
|
|
20
|
+
'headless': True,
|
|
21
|
+
'timeout': 30000
|
|
22
|
+
}
|
|
23
|
+
})
|
|
24
|
+
print(f" ✓ launch_context 返回类型: {type(context_manager).__name__}")
|
|
25
|
+
print(f" ✓ 是否为同一实例: {context_manager is grasp}")
|
|
26
|
+
|
|
27
|
+
# 测试异步上下文管理器方法存在性
|
|
28
|
+
print("\n2. 测试异步上下文管理器方法:")
|
|
29
|
+
print(f" ✓ 具有 __aenter__ 方法: {hasattr(grasp, '__aenter__')}")
|
|
30
|
+
print(f" ✓ 具有 __aexit__ 方法: {hasattr(grasp, '__aexit__')}")
|
|
31
|
+
|
|
32
|
+
# 测试错误情况
|
|
33
|
+
print("\n3. 测试错误处理:")
|
|
34
|
+
try:
|
|
35
|
+
# 尝试在没有设置 launch_options 的情况下使用上下文管理器
|
|
36
|
+
fresh_grasp = Grasp({'apiKey': 'test-key'})
|
|
37
|
+
async with fresh_grasp as session:
|
|
38
|
+
pass
|
|
39
|
+
except RuntimeError as e:
|
|
40
|
+
print(f" ✓ 正确捕获错误: {e}")
|
|
41
|
+
|
|
42
|
+
# 模拟使用异步上下文管理器的语法(不实际连接)
|
|
43
|
+
print("\n4. 异步上下文管理器语法示例:")
|
|
44
|
+
print(" 代码示例:")
|
|
45
|
+
print(" async with grasp.launch_context({")
|
|
46
|
+
print(" 'browser': {")
|
|
47
|
+
print(" 'type': 'chromium',")
|
|
48
|
+
print(" 'headless': True,")
|
|
49
|
+
print(" 'timeout': 30000")
|
|
50
|
+
print(" }")
|
|
51
|
+
print(" }) as session:")
|
|
52
|
+
print(" # 使用 session 进行操作")
|
|
53
|
+
print(" browser = session.browser")
|
|
54
|
+
print(" terminal = session.terminal")
|
|
55
|
+
print(" files = session.files")
|
|
56
|
+
|
|
57
|
+
print("\n✅ 异步上下文管理器功能测试完成!")
|
|
58
|
+
print("\n📝 使用说明:")
|
|
59
|
+
print(" - 使用 grasp.launch_context(options) 设置启动选项")
|
|
60
|
+
print(" - 然后使用 async with 语法自动管理会话生命周期")
|
|
61
|
+
print(" - 会话将在退出上下文时自动关闭")
|
|
62
|
+
|
|
63
|
+
if __name__ == '__main__':
|
|
64
|
+
asyncio.run(test_async_context_manager())
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""测试 Grasp 类的功能"""
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
from grasp_sdk import Grasp, GraspSession, GraspBrowser
|
|
6
|
+
|
|
7
|
+
def test_grasp_classes():
|
|
8
|
+
"""测试所有 Grasp 类的基本功能"""
|
|
9
|
+
print("🧪 测试 Grasp 类功能...")
|
|
10
|
+
|
|
11
|
+
# 测试 Grasp 类初始化
|
|
12
|
+
print("\n1. 测试 Grasp 类初始化:")
|
|
13
|
+
|
|
14
|
+
# 通过字典设置 apiKey
|
|
15
|
+
grasp1 = Grasp({'apiKey': 'test-key-from-dict'})
|
|
16
|
+
print(f" ✓ 通过字典初始化: {grasp1.key == 'test-key-from-dict'}")
|
|
17
|
+
|
|
18
|
+
# 通过环境变量设置 apiKey
|
|
19
|
+
os.environ['GRASP_KEY'] = 'test-key-from-env'
|
|
20
|
+
grasp2 = Grasp()
|
|
21
|
+
print(f" ✓ 通过环境变量初始化: {grasp2.key == 'test-key-from-env'}")
|
|
22
|
+
|
|
23
|
+
# 测试类的导入
|
|
24
|
+
print("\n2. 测试类的导入:")
|
|
25
|
+
print(f" ✓ Grasp 类可用: {Grasp is not None}")
|
|
26
|
+
print(f" ✓ GraspSession 类可用: {GraspSession is not None}")
|
|
27
|
+
print(f" ✓ GraspBrowser 类可用: {GraspBrowser is not None}")
|
|
28
|
+
|
|
29
|
+
# 测试方法可用性
|
|
30
|
+
print("\n3. 测试方法可用性:")
|
|
31
|
+
grasp = Grasp({'apiKey': 'test-key'})
|
|
32
|
+
|
|
33
|
+
# 测试 Grasp 类方法
|
|
34
|
+
print(f" ✓ Grasp.launch 方法可用: {hasattr(grasp, 'launch')}")
|
|
35
|
+
print(f" ✓ Grasp.connect 方法可用: {hasattr(grasp, 'connect')}")
|
|
36
|
+
print(f" ✓ Grasp.launch_browser 静态方法可用: {hasattr(Grasp, 'launch_browser')}")
|
|
37
|
+
|
|
38
|
+
# 测试异步上下文管理器功能
|
|
39
|
+
print("\n4. 测试异步上下文管理器功能:")
|
|
40
|
+
try:
|
|
41
|
+
# 测试 launch_context 方法
|
|
42
|
+
context_manager = grasp.launch_context({
|
|
43
|
+
'browser': {
|
|
44
|
+
'type': 'chromium',
|
|
45
|
+
'headless': True,
|
|
46
|
+
'timeout': 30000
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
print(f" ✓ launch_context 方法可用: {hasattr(grasp, 'launch_context')}")
|
|
50
|
+
print(f" ✓ 返回自身实例: {context_manager is grasp}")
|
|
51
|
+
|
|
52
|
+
# 测试异步上下文管理器方法
|
|
53
|
+
print(f" ✓ __aenter__ 方法可用: {hasattr(grasp, '__aenter__')}")
|
|
54
|
+
print(f" ✓ __aexit__ 方法可用: {hasattr(grasp, '__aexit__')}")
|
|
55
|
+
|
|
56
|
+
except Exception as e:
|
|
57
|
+
print(f" ❌ 异步上下文管理器测试失败: {e}")
|
|
58
|
+
|
|
59
|
+
print("\n✅ 所有测试通过!")
|
|
60
|
+
print("\n📋 已实现的方法:")
|
|
61
|
+
print(" - Grasp.launch()")
|
|
62
|
+
print(" - Grasp.connect()")
|
|
63
|
+
print(" - Grasp.launch_browser()")
|
|
64
|
+
print(" - Grasp.launch_context() [新增]")
|
|
65
|
+
print(" - Grasp.__aenter__() [新增]")
|
|
66
|
+
print(" - Grasp.__aexit__() [新增]")
|
|
67
|
+
print(" - GraspSession.close()")
|
|
68
|
+
print(" - GraspBrowser.get_endpoint()")
|
|
69
|
+
print(" - GraspTerminal.create()")
|
|
70
|
+
print("\n⏭️ 已跳过的方法 (已废弃):")
|
|
71
|
+
print(" - Grasp.shutdown()")
|
|
72
|
+
print(" - Grasp.createRunner()")
|
|
73
|
+
print("\n🎯 新功能:")
|
|
74
|
+
print(" - 支持 async with grasp.launch_context(options) as session 语法")
|
|
75
|
+
print(" - 自动管理会话生命周期")
|
|
76
|
+
print(" - 会话在退出上下文时自动关闭")
|
|
77
|
+
|
|
78
|
+
if __name__ == '__main__':
|
|
79
|
+
test_grasp_classes()
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"""Example demonstrating Python script execution support in Grasp SDK.
|
|
2
|
+
|
|
3
|
+
This example shows how to use the enhanced run_script method to execute
|
|
4
|
+
Python scripts alongside JavaScript in the E2B sandbox environment.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
from grasp_sdk.services.sandbox import SandboxService
|
|
9
|
+
from grasp_sdk.models import ISandboxConfig
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
async def main():
|
|
13
|
+
"""Main example function demonstrating Python script support."""
|
|
14
|
+
|
|
15
|
+
# Configure sandbox service
|
|
16
|
+
config: ISandboxConfig = {
|
|
17
|
+
'key': 'your-api-key-here', # Replace with your actual API key
|
|
18
|
+
'timeout': 30000,
|
|
19
|
+
'workspace': 'your-workspace-id', # Replace with your workspace ID
|
|
20
|
+
'debug': True
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
# Create sandbox service
|
|
24
|
+
sandbox = SandboxService(config)
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
# Create and start sandbox
|
|
28
|
+
await sandbox.create_sandbox('base')
|
|
29
|
+
print("✅ Sandbox created successfully")
|
|
30
|
+
|
|
31
|
+
# Example 1: Basic Python script execution
|
|
32
|
+
print("\n🐍 Example 1: Basic Python script")
|
|
33
|
+
python_code = """
|
|
34
|
+
print("Hello from Python in E2B sandbox!")
|
|
35
|
+
print(f"Python version: {__import__('sys').version}")
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
result = await sandbox.run_script(python_code, {'type': 'py'})
|
|
39
|
+
print(f"Output: {getattr(result, 'stdout', str(result))}")
|
|
40
|
+
|
|
41
|
+
# Example 2: Python script with mathematical operations
|
|
42
|
+
print("\n🧮 Example 2: Python mathematical operations")
|
|
43
|
+
math_code = """
|
|
44
|
+
import math
|
|
45
|
+
|
|
46
|
+
# Calculate some mathematical operations
|
|
47
|
+
numbers = [1, 2, 3, 4, 5]
|
|
48
|
+
print(f"Numbers: {numbers}")
|
|
49
|
+
print(f"Sum: {sum(numbers)}")
|
|
50
|
+
print(f"Square root of 16: {math.sqrt(16)}")
|
|
51
|
+
print(f"Pi: {math.pi:.4f}")
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
result = await sandbox.run_script(math_code, {'type': 'py'})
|
|
55
|
+
print(f"Output: {getattr(result, 'stdout', str(result))}")
|
|
56
|
+
|
|
57
|
+
# Example 3: Python script with environment variables
|
|
58
|
+
print("\n🌍 Example 3: Python script with environment variables")
|
|
59
|
+
env_code = """
|
|
60
|
+
import os
|
|
61
|
+
|
|
62
|
+
print(f"Custom variable: {os.environ.get('MY_CUSTOM_VAR', 'Not set')}")
|
|
63
|
+
print(f"Python path: {os.environ.get('PYTHONPATH', 'Default')}")
|
|
64
|
+
print(f"Current working directory: {os.getcwd()}")
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
result = await sandbox.run_script(env_code, {
|
|
68
|
+
'type': 'py',
|
|
69
|
+
'envs': {
|
|
70
|
+
'MY_CUSTOM_VAR': 'Hello from environment!',
|
|
71
|
+
'PYTHONPATH': '/custom/python/path'
|
|
72
|
+
}
|
|
73
|
+
})
|
|
74
|
+
print(f"Output: {getattr(result, 'stdout', str(result))}")
|
|
75
|
+
|
|
76
|
+
# Example 4: Python script with package installation
|
|
77
|
+
print("\n📦 Example 4: Python script with package installation")
|
|
78
|
+
package_code = """
|
|
79
|
+
import requests
|
|
80
|
+
import json
|
|
81
|
+
|
|
82
|
+
# Make a simple HTTP request
|
|
83
|
+
response = requests.get('https://httpbin.org/json')
|
|
84
|
+
data = response.json()
|
|
85
|
+
print(f"HTTP Status: {response.status_code}")
|
|
86
|
+
print(f"Response data: {json.dumps(data, indent=2)}")
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
result = await sandbox.run_script(package_code, {
|
|
90
|
+
'type': 'py',
|
|
91
|
+
'preCommand': 'pip install requests && '
|
|
92
|
+
})
|
|
93
|
+
print(f"Output: {getattr(result, 'stdout', str(result))}")
|
|
94
|
+
|
|
95
|
+
# Example 5: Python script with custom working directory
|
|
96
|
+
print("\n📁 Example 5: Python script with custom working directory")
|
|
97
|
+
file_code = """
|
|
98
|
+
import os
|
|
99
|
+
|
|
100
|
+
# Create a test file
|
|
101
|
+
with open('test_file.txt', 'w') as f:
|
|
102
|
+
f.write('Hello from Python file operation!')
|
|
103
|
+
|
|
104
|
+
# Read the file back
|
|
105
|
+
with open('test_file.txt', 'r') as f:
|
|
106
|
+
content = f.read()
|
|
107
|
+
print(f"File content: {content}")
|
|
108
|
+
|
|
109
|
+
print(f"Current directory: {os.getcwd()}")
|
|
110
|
+
print(f"Files in directory: {os.listdir('.')}")
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
result = await sandbox.run_script(file_code, {
|
|
114
|
+
'type': 'py',
|
|
115
|
+
'cwd': '/home/user/python_workspace'
|
|
116
|
+
})
|
|
117
|
+
print(f"Output: {getattr(result, 'stdout', str(result))}")
|
|
118
|
+
|
|
119
|
+
# Example 6: Compare with JavaScript execution
|
|
120
|
+
print("\n🔄 Example 6: JavaScript vs Python comparison")
|
|
121
|
+
|
|
122
|
+
# JavaScript version
|
|
123
|
+
js_code = """
|
|
124
|
+
const numbers = [1, 2, 3, 4, 5];
|
|
125
|
+
const sum = numbers.reduce((a, b) => a + b, 0);
|
|
126
|
+
console.log(`JavaScript - Numbers: ${numbers}`);
|
|
127
|
+
console.log(`JavaScript - Sum: ${sum}`);
|
|
128
|
+
console.log(`JavaScript - Node version: ${process.version}`);
|
|
129
|
+
"""
|
|
130
|
+
|
|
131
|
+
js_result = await sandbox.run_script(js_code, {'type': 'cjs'})
|
|
132
|
+
print(f"JavaScript Output: {getattr(js_result, 'stdout', str(js_result))}")
|
|
133
|
+
|
|
134
|
+
# Python version
|
|
135
|
+
py_code = """
|
|
136
|
+
numbers = [1, 2, 3, 4, 5]
|
|
137
|
+
total = sum(numbers)
|
|
138
|
+
print(f"Python - Numbers: {numbers}")
|
|
139
|
+
print(f"Python - Sum: {total}")
|
|
140
|
+
print(f"Python - Version: {__import__('sys').version.split()[0]}")
|
|
141
|
+
"""
|
|
142
|
+
|
|
143
|
+
py_result = await sandbox.run_script(py_code, {'type': 'py'})
|
|
144
|
+
print(f"Python Output: {getattr(py_result, 'stdout', str(py_result))}")
|
|
145
|
+
|
|
146
|
+
print("\n✅ All examples completed successfully!")
|
|
147
|
+
|
|
148
|
+
except Exception as error:
|
|
149
|
+
print(f"❌ Error: {error}")
|
|
150
|
+
|
|
151
|
+
finally:
|
|
152
|
+
# Clean up sandbox
|
|
153
|
+
await sandbox.destroy()
|
|
154
|
+
print("\n🧹 Sandbox destroyed")
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
if __name__ == "__main__":
|
|
158
|
+
# Run the example
|
|
159
|
+
print("🚀 Starting Python script execution examples...")
|
|
160
|
+
asyncio.run(main())
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
测试已删除方法的验证脚本
|
|
4
|
+
|
|
5
|
+
验证 create_terminal 和 create_filesystem 方法已被正确移除。
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import sys
|
|
9
|
+
import os
|
|
10
|
+
|
|
11
|
+
# 添加父目录到路径
|
|
12
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
13
|
+
|
|
14
|
+
def test_removed_methods():
|
|
15
|
+
"""测试已删除的方法不再可用"""
|
|
16
|
+
print("🧪 测试已删除方法的验证")
|
|
17
|
+
print("=" * 30)
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
from grasp_sdk import Grasp, GraspSession, GraspBrowser
|
|
21
|
+
print("✅ 核心类导入成功")
|
|
22
|
+
except ImportError as e:
|
|
23
|
+
print(f"❌ 核心类导入失败: {e}")
|
|
24
|
+
return False
|
|
25
|
+
|
|
26
|
+
# 测试 create_terminal 方法不存在(预期 ImportError)
|
|
27
|
+
try:
|
|
28
|
+
# 这行代码预期会失败,因为方法已被删除
|
|
29
|
+
from grasp_sdk import create_terminal # type: ignore
|
|
30
|
+
print("❌ create_terminal 方法仍然存在(应该已被删除)")
|
|
31
|
+
return False
|
|
32
|
+
except ImportError:
|
|
33
|
+
print("✅ create_terminal 方法已成功删除")
|
|
34
|
+
|
|
35
|
+
# 测试 create_filesystem 方法不存在(预期 ImportError)
|
|
36
|
+
try:
|
|
37
|
+
# 这行代码预期会失败,因为方法已被删除
|
|
38
|
+
from grasp_sdk import create_filesystem # type: ignore
|
|
39
|
+
print("❌ create_filesystem 方法仍然存在(应该已被删除)")
|
|
40
|
+
return False
|
|
41
|
+
except ImportError:
|
|
42
|
+
print("✅ create_filesystem 方法已成功删除")
|
|
43
|
+
|
|
44
|
+
# 验证 GraspSession 仍然提供 terminal 和 files 属性
|
|
45
|
+
print("\n📋 验证 GraspSession 的替代功能:")
|
|
46
|
+
print(" - session.terminal (替代 create_terminal)")
|
|
47
|
+
print(" - session.files (替代 create_filesystem)")
|
|
48
|
+
|
|
49
|
+
# 检查 __all__ 列表
|
|
50
|
+
try:
|
|
51
|
+
import grasp_sdk
|
|
52
|
+
all_exports = grasp_sdk.__all__
|
|
53
|
+
|
|
54
|
+
if 'create_terminal' in all_exports:
|
|
55
|
+
print("❌ create_terminal 仍在 __all__ 列表中")
|
|
56
|
+
return False
|
|
57
|
+
else:
|
|
58
|
+
print("✅ create_terminal 已从 __all__ 列表中移除")
|
|
59
|
+
|
|
60
|
+
if 'create_filesystem' in all_exports:
|
|
61
|
+
print("❌ create_filesystem 仍在 __all__ 列表中")
|
|
62
|
+
return False
|
|
63
|
+
else:
|
|
64
|
+
print("✅ create_filesystem 已从 __all__ 列表中移除")
|
|
65
|
+
|
|
66
|
+
except Exception as e:
|
|
67
|
+
print(f"⚠️ 检查 __all__ 列表时出错: {e}")
|
|
68
|
+
|
|
69
|
+
print("\n🎉 所有验证通过!create_terminal 和 create_filesystem 方法已成功移除")
|
|
70
|
+
print("\n💡 使用建议:")
|
|
71
|
+
print(" - 使用 session.terminal 替代 create_terminal(connection)")
|
|
72
|
+
print(" - 使用 session.files 替代 create_filesystem(connection)")
|
|
73
|
+
|
|
74
|
+
return True
|
|
75
|
+
|
|
76
|
+
if __name__ == "__main__":
|
|
77
|
+
success = test_removed_methods()
|
|
78
|
+
if not success:
|
|
79
|
+
sys.exit(1)
|
|
80
|
+
print("\n✅ 测试完成")
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Test script to verify terminal service updates in Python SDK.
|
|
4
|
+
|
|
5
|
+
This script tests the updated terminal service functionality,
|
|
6
|
+
including the new kill() method and response.json() compatibility.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import sys
|
|
11
|
+
import os
|
|
12
|
+
from unittest.mock import Mock, AsyncMock, patch
|
|
13
|
+
|
|
14
|
+
# Add the parent directory to the path to import grasp_sdk
|
|
15
|
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
|
|
16
|
+
|
|
17
|
+
from grasp_sdk.services.terminal import TerminalService, StreamableCommandResult
|
|
18
|
+
from grasp_sdk.services.sandbox import SandboxService, CommandEventEmitter
|
|
19
|
+
from grasp_sdk.services.browser import CDPConnection
|
|
20
|
+
from grasp_sdk.models import ISandboxConfig
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class MockCommandEventEmitter(CommandEventEmitter):
|
|
24
|
+
"""Mock command event emitter for testing."""
|
|
25
|
+
|
|
26
|
+
def __init__(self):
|
|
27
|
+
super().__init__()
|
|
28
|
+
self.killed = False
|
|
29
|
+
self.stdout_data = "Test output\n"
|
|
30
|
+
self.stderr_data = "Test error\n"
|
|
31
|
+
|
|
32
|
+
async def wait(self):
|
|
33
|
+
"""Mock wait method."""
|
|
34
|
+
await asyncio.sleep(0.1) # Simulate some processing time
|
|
35
|
+
return {
|
|
36
|
+
'exit_code': 0,
|
|
37
|
+
'stdout': self.stdout_data,
|
|
38
|
+
'stderr': self.stderr_data
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async def kill(self):
|
|
42
|
+
"""Mock kill method."""
|
|
43
|
+
self.killed = True
|
|
44
|
+
print("Command killed successfully")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
async def test_streamable_command_result():
|
|
48
|
+
"""Test StreamableCommandResult functionality."""
|
|
49
|
+
print("\n=== Testing StreamableCommandResult ===")
|
|
50
|
+
|
|
51
|
+
# Create mock emitter and task
|
|
52
|
+
emitter = MockCommandEventEmitter()
|
|
53
|
+
task = asyncio.create_task(emitter.wait())
|
|
54
|
+
|
|
55
|
+
# Create StreamableCommandResult
|
|
56
|
+
result = StreamableCommandResult(emitter, task)
|
|
57
|
+
|
|
58
|
+
# Test stdout/stderr streams
|
|
59
|
+
print("✓ StreamableCommandResult created successfully")
|
|
60
|
+
|
|
61
|
+
# Test response.json() method
|
|
62
|
+
response_data = await result.json()
|
|
63
|
+
print(f"✓ Response JSON: {response_data}")
|
|
64
|
+
|
|
65
|
+
# Test end() method
|
|
66
|
+
end_result = await result.end()
|
|
67
|
+
print(f"✓ End result: {end_result}")
|
|
68
|
+
|
|
69
|
+
# Test kill() method
|
|
70
|
+
await result.kill()
|
|
71
|
+
print(f"✓ Kill method executed, emitter killed: {emitter.killed}")
|
|
72
|
+
|
|
73
|
+
print("✓ All StreamableCommandResult tests passed!")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
async def test_terminal_service():
|
|
77
|
+
"""Test TerminalService functionality."""
|
|
78
|
+
print("\n=== Testing TerminalService ===")
|
|
79
|
+
|
|
80
|
+
# Create mock sandbox and connection
|
|
81
|
+
sandbox_config: ISandboxConfig = {
|
|
82
|
+
'key': 'test-key',
|
|
83
|
+
'timeout': 30000,
|
|
84
|
+
'debug': True
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
mock_sandbox = Mock(spec=SandboxService)
|
|
88
|
+
mock_sandbox.is_debug = True
|
|
89
|
+
mock_sandbox.run_command = AsyncMock()
|
|
90
|
+
|
|
91
|
+
mock_connection = Mock(spec=CDPConnection)
|
|
92
|
+
mock_connection.ws_url = 'ws://localhost:8080'
|
|
93
|
+
|
|
94
|
+
# Create terminal service
|
|
95
|
+
terminal = TerminalService(mock_sandbox, mock_connection)
|
|
96
|
+
print("✓ TerminalService created successfully")
|
|
97
|
+
|
|
98
|
+
# Mock websockets.connect
|
|
99
|
+
with patch('grasp_sdk.services.terminal.websockets.connect') as mock_connect:
|
|
100
|
+
mock_ws = AsyncMock()
|
|
101
|
+
mock_ws.open = True
|
|
102
|
+
mock_connect.return_value.__aenter__ = AsyncMock(return_value=mock_ws)
|
|
103
|
+
mock_connect.return_value.__aexit__ = AsyncMock(return_value=None)
|
|
104
|
+
# For direct await usage
|
|
105
|
+
async def mock_connect_func(*args, **kwargs):
|
|
106
|
+
return mock_ws
|
|
107
|
+
mock_connect.side_effect = mock_connect_func
|
|
108
|
+
|
|
109
|
+
# Mock emitter
|
|
110
|
+
mock_emitter = MockCommandEventEmitter()
|
|
111
|
+
mock_sandbox.run_command.return_value = mock_emitter
|
|
112
|
+
|
|
113
|
+
# Test run_command
|
|
114
|
+
result = await terminal.run_command('echo "Hello World"', {'timeout_ms': 5000})
|
|
115
|
+
print("✓ run_command executed successfully")
|
|
116
|
+
|
|
117
|
+
# Verify the command was called with correct options
|
|
118
|
+
mock_sandbox.run_command.assert_called_once()
|
|
119
|
+
call_args = mock_sandbox.run_command.call_args
|
|
120
|
+
command_arg = call_args[0][0]
|
|
121
|
+
options_arg = call_args[0][1]
|
|
122
|
+
|
|
123
|
+
print(f"✓ Command: {command_arg}")
|
|
124
|
+
print(f"✓ Options: {options_arg}")
|
|
125
|
+
|
|
126
|
+
# Verify inBackground option is set correctly
|
|
127
|
+
assert options_arg['inBackground'] == True, "inBackground option should be True"
|
|
128
|
+
assert options_arg['nohup'] == False, "Nohup option should be False"
|
|
129
|
+
print("✓ Command options verified")
|
|
130
|
+
|
|
131
|
+
# Test result methods
|
|
132
|
+
response_data = await result.json()
|
|
133
|
+
print(f"✓ Response JSON: {response_data}")
|
|
134
|
+
|
|
135
|
+
# Test kill method
|
|
136
|
+
await result.kill()
|
|
137
|
+
print("✓ Kill method executed successfully")
|
|
138
|
+
|
|
139
|
+
# Test close method
|
|
140
|
+
terminal.close()
|
|
141
|
+
print("✓ Terminal service closed successfully")
|
|
142
|
+
|
|
143
|
+
print("✓ All TerminalService tests passed!")
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
async def test_command_options_compatibility():
|
|
147
|
+
"""Test command options compatibility with TypeScript version."""
|
|
148
|
+
print("\n=== Testing Command Options Compatibility ===")
|
|
149
|
+
|
|
150
|
+
from grasp_sdk.models import ICommandOptions
|
|
151
|
+
|
|
152
|
+
# Test that ICommandOptions supports 'inBackground' parameter
|
|
153
|
+
options: ICommandOptions = {
|
|
154
|
+
'inBackground': True,
|
|
155
|
+
'timeout_ms': 5000,
|
|
156
|
+
'cwd': '/tmp',
|
|
157
|
+
'nohup': False
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
print(f"✓ Command options created: {options}")
|
|
161
|
+
print("✓ 'inBackground' parameter is supported in ICommandOptions")
|
|
162
|
+
|
|
163
|
+
# Test that we can access the inBackground option
|
|
164
|
+
background_value = options.get('inBackground', False)
|
|
165
|
+
assert background_value == True, "inBackground option should be accessible"
|
|
166
|
+
print("✓ inBackground option accessible and correct")
|
|
167
|
+
|
|
168
|
+
print("✓ All command options compatibility tests passed!")
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
async def main():
|
|
172
|
+
"""Run all tests."""
|
|
173
|
+
print("Starting Terminal Service Update Tests...")
|
|
174
|
+
|
|
175
|
+
try:
|
|
176
|
+
await test_streamable_command_result()
|
|
177
|
+
await test_terminal_service()
|
|
178
|
+
await test_command_options_compatibility()
|
|
179
|
+
|
|
180
|
+
print("\n🎉 All tests passed! Terminal service updates are working correctly.")
|
|
181
|
+
print("\n📋 Summary of updates:")
|
|
182
|
+
print(" • Added kill() method to StreamableCommandResult")
|
|
183
|
+
print(" • Added response.json() compatibility method")
|
|
184
|
+
print(" • Maintained ICommandOptions to use 'inBackground' for Python compatibility")
|
|
185
|
+
print(" • Improved error handling and WebSocket management")
|
|
186
|
+
print(" • Synchronized timeout values with TypeScript version")
|
|
187
|
+
|
|
188
|
+
except Exception as e:
|
|
189
|
+
print(f"\n❌ Test failed: {e}")
|
|
190
|
+
import traceback
|
|
191
|
+
traceback.print_exc()
|
|
192
|
+
sys.exit(1)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
if __name__ == '__main__':
|
|
196
|
+
asyncio.run(main())
|