- Added snapshot functions to SDK requirements - Updated testing guide with snapshot examples - Added snapshot checklist items for refactoring - Clarified image() function in utilities section All agents refactoring SDKs must now implement: - sessionSnapshot(session_id, opts) - serviceSnapshot(service_id, opts) - listSnapshots(opts) - restoreSnapshot(snapshot_id, opts) - deleteSnapshot(snapshot_id, opts)
623 lines
15 KiB
Markdown
623 lines
15 KiB
Markdown
# SDK Testing Guide for Refactoring
|
|
|
|
This guide explains how to test UN SDK library implementations during refactoring. It enables agents to validate their own work independently.
|
|
|
|
## Overview
|
|
|
|
Each SDK (un.py, un.js, un.go, etc.) must provide:
|
|
|
|
1. **Library functions** - The core API that can be imported/required by other code
|
|
2. **CLI functionality** - Command-line interface for direct code execution
|
|
3. **Tests** - Unit, integration, and functional tests to validate behavior
|
|
|
|
## Test Categories
|
|
|
|
### 1. Unit Tests (No API Key Required)
|
|
Focus on library function behavior without external API calls:
|
|
|
|
- **Credential loading** - Functions correctly parse credentials from env vars, files, and arguments
|
|
- **HMAC signature generation** - Signatures match the expected format (64 hex chars)
|
|
- **Cache checking** - Languages cache exists and can be read
|
|
- **Extension detection** - File extensions correctly map to language identifiers
|
|
- **Function signatures** - All expected functions exist and are callable
|
|
- **Error handling** - Invalid inputs produce appropriate errors
|
|
|
|
**Example Unit Test (Python):**
|
|
```python
|
|
def test_hmac_signature():
|
|
"""Test HMAC-SHA256 signature generation"""
|
|
sig = un._sign_request(
|
|
'test-secret-key',
|
|
'1704067200', # timestamp
|
|
'POST', # method
|
|
'/execute', # endpoint
|
|
'{}' # body
|
|
)
|
|
|
|
assert len(sig) == 64, f"Invalid signature length: {len(sig)}"
|
|
assert all(c in '0123456789abcdef' for c in sig), "Invalid hex characters"
|
|
```
|
|
|
|
### 2. Integration Tests (Requires API Key)
|
|
Test actual API communication with real infrastructure:
|
|
|
|
- **execute()** - Synchronous code execution and result retrieval
|
|
- **executeAsync()** - Asynchronous job submission and status polling
|
|
- **wait()** - Job polling with exponential backoff
|
|
- **languages()** - Language list retrieval with caching
|
|
- **get_job()** - Job status retrieval
|
|
- **cancel_job()** - Job cancellation
|
|
|
|
**Example Integration Test (Python):**
|
|
```python
|
|
def test_execute_integration():
|
|
"""Test execute function with real API"""
|
|
result = un.execute(
|
|
'python',
|
|
'print("hello world")',
|
|
network_mode='zerotrust'
|
|
)
|
|
|
|
assert result['exit_code'] == 0
|
|
assert 'hello world' in result['stdout']
|
|
```
|
|
|
|
### 3. Functional Tests (Requires API Key)
|
|
End-to-end workflow validation:
|
|
|
|
- **CLI execution** - Run code file through CLI interface
|
|
- **Output parsing** - Correctly display results to user
|
|
- **Error handling** - Handle API errors gracefully
|
|
- **Input/output files** - Process input files and write outputs
|
|
|
|
**Example Functional Test:**
|
|
```python
|
|
def test_cli_execution():
|
|
"""Test CLI end-to-end"""
|
|
import subprocess
|
|
|
|
result = subprocess.run(
|
|
['python', 'un.py', 'test_script.py'],
|
|
capture_output=True,
|
|
text=True
|
|
)
|
|
|
|
assert result.returncode == 0
|
|
assert 'expected output' in result.stdout
|
|
```
|
|
|
|
## SDK Library Requirements
|
|
|
|
Every SDK must implement these functions:
|
|
|
|
### Core Execution Functions
|
|
```python
|
|
# Synchronous execution
|
|
result = execute(
|
|
language: str,
|
|
code: str,
|
|
network_mode: str = 'zerotrust',
|
|
ttl: int = 60,
|
|
env: dict = None,
|
|
files: list = None,
|
|
input_file: str = None
|
|
) -> dict
|
|
|
|
# Asynchronous execution
|
|
job = executeAsync(
|
|
language: str,
|
|
code: str,
|
|
network_mode: str = 'zerotrust',
|
|
ttl: int = 60,
|
|
env: dict = None,
|
|
files: list = None
|
|
) -> dict
|
|
|
|
# Wait for job completion
|
|
result = wait(job_id: str, timeout: int = 3600) -> dict
|
|
```
|
|
|
|
### Job Management Functions
|
|
```python
|
|
# Get job status
|
|
job = get_job(job_id: str) -> dict
|
|
|
|
# Cancel job
|
|
cancel_job(job_id: str) -> dict
|
|
|
|
# List jobs
|
|
jobs = list_jobs(limit: int = 100) -> list[dict]
|
|
```
|
|
|
|
### Utility Functions
|
|
```python
|
|
# Get supported languages
|
|
languages = languages(cache_ttl: int = 3600) -> list[str]
|
|
|
|
# Generate image from text prompt
|
|
result = image(prompt: str, model: str = None, size: str = "1024x1024") -> dict
|
|
```
|
|
|
|
### Snapshot Functions
|
|
```python
|
|
# Create session snapshot
|
|
snap = session_snapshot(session_id: str) -> dict
|
|
|
|
# Create service snapshot
|
|
snap = service_snapshot(service_id: str) -> dict
|
|
|
|
# List all snapshots
|
|
snaps = list_snapshots() -> dict
|
|
|
|
# Restore from snapshot
|
|
result = restore_snapshot(snapshot_id: str) -> dict
|
|
|
|
# Delete snapshot
|
|
delete_snapshot(snapshot_id: str) -> dict
|
|
```
|
|
|
|
### Credential Functions
|
|
```python
|
|
# Load credentials from multiple sources
|
|
public_key, secret_key = _get_credentials(
|
|
public_key: str = None,
|
|
secret_key: str = None
|
|
) -> tuple[str, str]
|
|
|
|
# Load accounts.csv
|
|
accounts = _load_accounts_csv(path: str = None) -> list[tuple[str, str]]
|
|
```
|
|
|
|
### Signature Functions
|
|
```python
|
|
# Generate HMAC-SHA256 signature
|
|
signature = _sign_request(
|
|
secret_key: str,
|
|
timestamp: str,
|
|
method: str,
|
|
endpoint: str,
|
|
body: str
|
|
) -> str
|
|
```
|
|
|
|
## Credential Priority System
|
|
|
|
SDKs must check credentials in this order:
|
|
|
|
1. **Function arguments** - Highest priority (explicit parameter)
|
|
2. **Environment variables** - `UNSANDBOX_PUBLIC_KEY` and `UNSANDBOX_SECRET_KEY`
|
|
3. **Home directory config** - `~/.unsandbox/accounts.csv`
|
|
4. **Local directory config** - `./accounts.csv` in current working directory
|
|
5. **Error** - Raise error if no credentials found
|
|
|
|
**Example (Python):**
|
|
```python
|
|
def _get_credentials(public_key=None, secret_key=None):
|
|
"""Load credentials with 4-tier fallback system"""
|
|
|
|
# Tier 1: Function arguments
|
|
if public_key and secret_key:
|
|
return public_key, secret_key
|
|
|
|
# Tier 2: Environment variables
|
|
pk = os.environ.get('UNSANDBOX_PUBLIC_KEY')
|
|
sk = os.environ.get('UNSANDBOX_SECRET_KEY')
|
|
if pk and sk:
|
|
return pk, sk
|
|
|
|
# Tier 3: Home directory
|
|
home_file = Path.home() / '.unsandbox' / 'accounts.csv'
|
|
if home_file.exists():
|
|
accounts = _load_accounts_csv(str(home_file))
|
|
if accounts:
|
|
return accounts[0] # Use first account
|
|
|
|
# Tier 4: Local directory
|
|
local_file = Path('./accounts.csv')
|
|
if local_file.exists():
|
|
accounts = _load_accounts_csv(str(local_file))
|
|
if accounts:
|
|
return accounts[0]
|
|
|
|
raise AuthenticationError("No credentials found")
|
|
```
|
|
|
|
## Caching Requirements
|
|
|
|
### Languages Cache
|
|
- **Location**: `~/.unsandbox/languages.json`
|
|
- **TTL**: 1 hour (3600 seconds)
|
|
- **Format**: JSON array of language strings
|
|
- **Update**: Only update if cache is older than TTL AND API call succeeds
|
|
|
|
**Example (Python):**
|
|
```python
|
|
def languages(cache_ttl=3600):
|
|
"""Get supported languages with 1-hour cache"""
|
|
|
|
cache_path = Path.home() / '.unsandbox' / 'languages.json'
|
|
|
|
# Check if cache exists and is fresh
|
|
if cache_path.exists():
|
|
age = time.time() - cache_path.stat().st_mtime
|
|
if age < cache_ttl:
|
|
return json.load(open(cache_path))
|
|
|
|
# Fetch from API
|
|
response = _make_request('GET', '/languages')
|
|
langs = response['languages']
|
|
|
|
# Update cache (create directory if needed)
|
|
cache_path.parent.mkdir(parents=True, exist_ok=True)
|
|
cache_path.write_text(json.dumps(langs))
|
|
|
|
return langs
|
|
```
|
|
|
|
## HMAC Signature Format
|
|
|
|
All API requests must include proper HMAC signatures:
|
|
|
|
**Signature Input:**
|
|
```
|
|
{timestamp}:{method}:{endpoint}:{body}
|
|
```
|
|
|
|
**Example:**
|
|
```
|
|
1704067200:POST:/execute:{"language":"python","code":"print(1)"}
|
|
```
|
|
|
|
**Signature Calculation:**
|
|
```python
|
|
import hmac
|
|
import hashlib
|
|
|
|
signature = hmac.new(
|
|
secret_key.encode(),
|
|
message.encode(),
|
|
hashlib.sha256
|
|
).hexdigest()
|
|
```
|
|
|
|
**Request Headers:**
|
|
```
|
|
Authorization: Bearer {public_key}
|
|
X-Timestamp: {timestamp}
|
|
X-Signature: {signature}
|
|
Content-Type: application/json
|
|
```
|
|
|
|
## Testing Workflow for Agents
|
|
|
|
### 1. Before Refactoring
|
|
```bash
|
|
# Create a checkpoint of the original SDK
|
|
cd /home/fox/git/un-inception
|
|
git stash
|
|
|
|
# Run baseline tests
|
|
./tests/run_sdk_tests.sh --unit
|
|
```
|
|
|
|
### 2. During Refactoring
|
|
```bash
|
|
# As you implement library functions, add tests
|
|
# Edit tests/test_sdk_library.py to add tests for your language
|
|
|
|
# Periodically validate unit tests pass
|
|
python3 tests/test_sdk_library.py --languages your_language
|
|
```
|
|
|
|
### 3. After Refactoring
|
|
```bash
|
|
# Run full test suite
|
|
./tests/run_sdk_tests.sh --unit
|
|
|
|
# If API key available, test integration
|
|
export UNSANDBOX_API_KEY='your-key'
|
|
./tests/run_sdk_tests.sh --integration
|
|
|
|
# Test CLI still works
|
|
python un.py examples/hello.py
|
|
node un.js examples/hello.js
|
|
go run un.go examples/hello.go
|
|
```
|
|
|
|
### 4. Validation Before Commit
|
|
```bash
|
|
# Run all tests
|
|
./tests/run_sdk_tests.sh --all
|
|
|
|
# Verify both library AND CLI work
|
|
node un.js -s javascript 'console.log("hello from cli")'
|
|
python un.py -s python 'print("hello from cli")'
|
|
go run un.go -s go 'fmt.Println("hello from cli")'
|
|
|
|
# Commit when all tests pass
|
|
git add un.py un.js un.go
|
|
git commit -m "Refactor SDK: add library exports + HMAC auth + caching"
|
|
```
|
|
|
|
## Common Testing Mistakes to Avoid
|
|
|
|
### ❌ Wrong: Only testing CLI functionality
|
|
```python
|
|
# BAD - doesn't test library functions
|
|
result = subprocess.run(['python', 'un.py', 'code.py'])
|
|
```
|
|
|
|
### ✅ Correct: Testing both library AND CLI
|
|
```python
|
|
# GOOD - test library functions exist and work
|
|
from un import execute, executeAsync
|
|
|
|
# AND test CLI
|
|
result = subprocess.run(['python', 'un.py', 'code.py'])
|
|
```
|
|
|
|
### ❌ Wrong: Ignoring credential loading
|
|
```python
|
|
# BAD - hardcoded credentials
|
|
def execute(language, code):
|
|
pk = 'unsb-pk-xxxx' # WRONG!
|
|
sk = 'unsb-sk-xxxx' # WRONG!
|
|
```
|
|
|
|
### ✅ Correct: Flexible credential loading
|
|
```python
|
|
# GOOD - respect credential sources
|
|
def execute(language, code, public_key=None, secret_key=None):
|
|
pk, sk = _get_credentials(public_key, secret_key)
|
|
# Now use pk, sk
|
|
```
|
|
|
|
### ❌ Wrong: Ignoring cache TTL
|
|
```python
|
|
# BAD - always calls API
|
|
def languages():
|
|
return requests.get('/languages').json()
|
|
```
|
|
|
|
### ✅ Correct: Proper caching with TTL
|
|
```python
|
|
# GOOD - cache with 1-hour TTL
|
|
def languages(cache_ttl=3600):
|
|
cache_file = Path.home() / '.unsandbox' / 'languages.json'
|
|
|
|
if cache_file.exists():
|
|
age = time.time() - cache_file.stat().st_mtime
|
|
if age < cache_ttl:
|
|
return json.load(open(cache_file))
|
|
|
|
# Fetch, cache, return
|
|
```
|
|
|
|
## Adding Tests for a New Language
|
|
|
|
When refactoring a new SDK:
|
|
|
|
1. **Add unit tests** to `test_sdk_library.py`:
|
|
```python
|
|
def test_ruby_sdk(self):
|
|
"""Test Ruby SDK library functions"""
|
|
# Import, test credentials, HMAC, etc.
|
|
```
|
|
|
|
2. **Add to test runner** `run_sdk_tests.sh`:
|
|
```bash
|
|
# Run Ruby SDK tests
|
|
echo -e "${BLUE}Running Ruby SDK tests...${RESET}"
|
|
if command -v ruby &> /dev/null; then
|
|
ruby "$SCRIPT_DIR/test_un_ruby.rb" 2>&1 || true
|
|
else
|
|
echo -e "${YELLOW}⚠ Ruby not found${RESET}"
|
|
fi
|
|
```
|
|
|
|
3. **Create test file** `tests/test_un_ruby.rb`:
|
|
```ruby
|
|
require_relative '../un.rb'
|
|
|
|
# Test HMAC signature
|
|
sig = Un._sign_request('test-sk', '1704067200', 'POST', '/execute', '{}')
|
|
if sig.length == 64 && sig.match?(/^[0-9a-f]+$/)
|
|
puts "✓ PASS: HMAC-SHA256 signature generation"
|
|
else
|
|
puts "✗ FAIL: HMAC-SHA256 signature generation"
|
|
end
|
|
|
|
# Test execute function exists
|
|
if Un.respond_to?(:execute)
|
|
puts "✓ PASS: execute function exists"
|
|
else
|
|
puts "✗ FAIL: execute function exists"
|
|
end
|
|
```
|
|
|
|
## Troubleshooting Tests
|
|
|
|
### Test: Import Error
|
|
```
|
|
Error: Cannot import un module
|
|
```
|
|
|
|
**Fix:** Ensure:
|
|
1. SDK file (un.py, un.js, etc.) is in parent directory
|
|
2. Correct extension (.py for Python, .js for JavaScript)
|
|
3. No syntax errors in SDK file
|
|
|
|
### Test: HMAC Signature Fails
|
|
```
|
|
FAIL: HMAC-SHA256 signature - invalid signature length
|
|
```
|
|
|
|
**Fix:** Check:
|
|
1. Using `hmac` library correctly
|
|
2. Using SHA256 hash algorithm
|
|
3. Converting result to hex string (64 chars)
|
|
4. Secret key is string/bytes, not encoded yet
|
|
|
|
### Test: API Integration Fails
|
|
```
|
|
FAIL: execute() function - status 401
|
|
```
|
|
|
|
**Fix:**
|
|
1. Check `UNSANDBOX_API_KEY` is set: `echo $UNSANDBOX_API_KEY`
|
|
2. Verify API key is valid and not expired
|
|
3. Check credentials are loaded correctly: `_get_credentials()`
|
|
4. Verify HMAC signature generation is working
|
|
|
|
### Test: Language Not Found
|
|
```
|
|
⚠ Python 3 not found
|
|
```
|
|
|
|
**Fix:** Install the required interpreter:
|
|
```bash
|
|
# Ubuntu/Debian
|
|
sudo apt-get install python3 nodejs golang-go ruby perl php
|
|
|
|
# macOS
|
|
brew install python node go ruby perl php
|
|
```
|
|
|
|
## Success Criteria
|
|
|
|
An SDK is ready for production when:
|
|
|
|
1. ✅ **All unit tests pass** - Library functions work correctly
|
|
2. ✅ **All integration tests pass** - API communication works
|
|
3. ✅ **All functional tests pass** - CLI still works
|
|
4. ✅ **Examples execute** - Python/JS examples work with new SDK
|
|
5. ✅ **Credentials load** - All 4 credential sources work
|
|
6. ✅ **Caching works** - Languages cache is created and used
|
|
7. ✅ **CLI still works** - Original CLI functionality preserved
|
|
8. ✅ **Documentation updated** - Examples reflect new SDK usage
|
|
|
|
## Example: Complete Ruby SDK Refactoring + Testing
|
|
|
|
### Step 1: Check Ruby is available
|
|
```bash
|
|
ruby --version
|
|
```
|
|
|
|
### Step 2: Refactor un.rb with library exports
|
|
```ruby
|
|
# un.rb - Ruby SDK with library exports
|
|
require 'net/http'
|
|
require 'json'
|
|
require 'openssl'
|
|
require 'base64'
|
|
require 'time'
|
|
|
|
module Un
|
|
def self.execute(language, code, opts = {})
|
|
# Implementation
|
|
end
|
|
|
|
def self.executeAsync(language, code, opts = {})
|
|
# Implementation
|
|
end
|
|
|
|
private
|
|
|
|
def self._sign_request(secret, timestamp, method, endpoint, body)
|
|
message = "#{timestamp}:#{method}:#{endpoint}:#{body}"
|
|
OpenSSL::HMAC.hexdigest('SHA256', secret, message)
|
|
end
|
|
end
|
|
|
|
# CLI entry point
|
|
if __FILE__ == $0
|
|
# Parse args and call Un.execute
|
|
end
|
|
```
|
|
|
|
### Step 3: Add unit tests to test_sdk_library.py
|
|
```python
|
|
def test_ruby_sdk(self):
|
|
"""Test Ruby SDK library functions"""
|
|
language = 'ruby'
|
|
|
|
try:
|
|
# Create test script
|
|
test_code = '''
|
|
require_relative '../un.rb'
|
|
|
|
sig = Un._sign_request('test-sk', '1704067200', 'POST', '/execute', '{}')
|
|
if sig.length == 64
|
|
puts "✓ PASS: HMAC signature"
|
|
else
|
|
puts "✗ FAIL: HMAC signature"
|
|
end
|
|
'''
|
|
# Run test and parse output
|
|
except Exception as e:
|
|
self.results.add_result(language, 'unit', 'SDK import', False, str(e))
|
|
```
|
|
|
|
### Step 4: Create test_un_ruby.rb
|
|
```ruby
|
|
#!/usr/bin/env ruby
|
|
require_relative '../un.rb'
|
|
|
|
puts "Testing Ruby SDK..."
|
|
|
|
# Test 1: HMAC signature
|
|
sig = Un._sign_request('test-sk', '1704067200', 'POST', '/execute', '{}')
|
|
if sig && sig.length == 64 && sig.match?(/^[0-9a-f]+$/)
|
|
puts "✓ PASS: HMAC-SHA256 signature"
|
|
else
|
|
puts "✗ FAIL: HMAC-SHA256 signature"
|
|
end
|
|
|
|
# Test 2: execute function
|
|
if Un.respond_to?(:execute)
|
|
puts "✓ PASS: execute function exists"
|
|
else
|
|
puts "✗ FAIL: execute function exists"
|
|
end
|
|
```
|
|
|
|
### Step 5: Run tests
|
|
```bash
|
|
./tests/run_sdk_tests.sh --languages ruby
|
|
|
|
# Or manually
|
|
ruby tests/test_un_ruby.rb
|
|
```
|
|
|
|
### Step 6: Validate CLI still works
|
|
```bash
|
|
# Test CLI with inline code
|
|
ruby un.rb -s ruby 'puts "hello from ruby"'
|
|
|
|
# Test CLI with file
|
|
echo 'puts "hello from file"' > test.rb
|
|
ruby un.rb test.rb
|
|
```
|
|
|
|
### Step 7: Commit when all tests pass
|
|
```bash
|
|
git add un.rb tests/test_un_ruby.rb tests/test_sdk_library.py
|
|
git commit -m "Refactor Ruby SDK: add library exports + HMAC auth + caching
|
|
|
|
- Implement execute, executeAsync, wait, get_job, cancel_job, list_jobs
|
|
- Add 4-tier credential system (args > env > home > local)
|
|
- Add 1-hour cache for languages list
|
|
- Implement HMAC-SHA256 request signing
|
|
- Preserve original CLI functionality
|
|
- All unit tests passing"
|
|
```
|
|
|
|
## Questions?
|
|
|
|
If tests fail or you need clarification:
|
|
1. Check the error message - it usually indicates the problem
|
|
2. Review the example SDKs (un.py, un.js) for reference
|
|
3. Look at existing test files for patterns
|
|
4. Run individual tests to isolate issues
|
|
|
|
Good luck with your SDK refactoring!
|