un-inception/clients/python/async/tests/test_async_operations.py
russell@unturf.com 1e01d09883 feat: per-client Makefile infrastructure for 4-mode testing
Add per-client Makefiles for C, Python, and Go with:
- CLI mode: Tests --help, arg parsing, syntax validation
- Library mode: Unit tests, import verification
- Integration mode: API contract validation (with credentials)
- Functional mode: Real-world scenario tests

C client:
- 22 library tests (SHA-256, HMAC-SHA256, detect_language)
- Full unsandbox.c implementation with examples

Python client (sync + async):
- Delegates to sync/ and async/ subdirectories
- pytest-based test suites with coverage
- Examples for concurrent execution, streaming

Go client:
- Delegates to sync/ and async/ subdirectories
- go test integration with vet and fmt

Also update detect-changes.sh to detect changes in
both root-level un.* files AND clients/ directory.
2026-01-15 16:39:56 -05:00

210 lines
6.9 KiB
Python

"""
Tests for async operations (requires mocking or real API access)
"""
import pytest
import asyncio
import sys
import os
from unittest.mock import AsyncMock, patch, MagicMock
# Add src to path
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
import un_async
from un_async import (
execute_code,
execute_async,
get_job,
wait_for_job,
cancel_job,
list_jobs,
)
@pytest.fixture
def mock_credentials(monkeypatch):
"""Provide mock credentials."""
monkeypatch.setenv("UNSANDBOX_PUBLIC_KEY", "test_public_key")
monkeypatch.setenv("UNSANDBOX_SECRET_KEY", "test_secret_key")
class TestAsyncOperations:
"""Test async API operations."""
@pytest.mark.asyncio
async def test_execute_code_completed(self, mock_credentials):
"""Test execute_code when job completes immediately."""
mock_response = {
"job_id": "job_123",
"status": "completed",
"stdout": "Hello World\n",
"stderr": "",
"exit_code": 0,
}
with patch("un_async._make_request", new_callable=AsyncMock) as mock_req:
# First call for execute, then wait_for_job polls
mock_req.side_effect = [
mock_response, # execute_code calls _make_request
]
result = await execute_code("python", "print('Hello World')")
assert result["status"] == "completed"
assert "Hello World" in result["stdout"]
assert result["exit_code"] == 0
@pytest.mark.asyncio
async def test_execute_async_returns_job_id(self, mock_credentials):
"""Test execute_async returns job_id immediately."""
mock_response = {
"job_id": "job_456",
"status": "pending",
}
with patch("un_async._make_request", new_callable=AsyncMock) as mock_req:
mock_req.return_value = mock_response
job_id = await execute_async("python", "print('test')")
assert job_id == "job_456"
mock_req.assert_called_once()
@pytest.mark.asyncio
async def test_get_job(self, mock_credentials):
"""Test getting job status."""
mock_response = {
"job_id": "job_789",
"status": "running",
}
with patch("un_async._make_request", new_callable=AsyncMock) as mock_req:
mock_req.return_value = mock_response
result = await get_job("job_789")
assert result["job_id"] == "job_789"
assert result["status"] == "running"
@pytest.mark.asyncio
async def test_wait_for_job_polling(self, mock_credentials):
"""Test wait_for_job polls until completion."""
# Simulate polling: pending -> running -> completed
mock_responses = [
{"job_id": "job_100", "status": "pending"},
{"job_id": "job_100", "status": "running"},
{"job_id": "job_100", "status": "completed", "stdout": "done"},
]
with patch("un_async._make_request", new_callable=AsyncMock) as mock_req:
mock_req.side_effect = mock_responses
with patch("asyncio.sleep", new_callable=AsyncMock):
result = await wait_for_job("job_100")
assert result["status"] == "completed"
assert result["stdout"] == "done"
assert mock_req.call_count == 3
@pytest.mark.asyncio
async def test_cancel_job(self, mock_credentials):
"""Test cancelling a job."""
mock_response = {
"job_id": "job_kill",
"status": "cancelled",
}
with patch("un_async._make_request", new_callable=AsyncMock) as mock_req:
mock_req.return_value = mock_response
result = await cancel_job("job_kill")
assert result["status"] == "cancelled"
@pytest.mark.asyncio
async def test_list_jobs(self, mock_credentials):
"""Test listing all jobs."""
mock_response = {
"jobs": [
{"id": "job_1", "status": "completed"},
{"id": "job_2", "status": "running"},
]
}
with patch("un_async._make_request", new_callable=AsyncMock) as mock_req:
mock_req.return_value = mock_response
jobs = await list_jobs()
assert len(jobs) == 2
assert jobs[0]["id"] == "job_1"
assert jobs[1]["id"] == "job_2"
class TestAsyncConcurrency:
"""Test concurrent async operations."""
@pytest.mark.asyncio
async def test_concurrent_executions(self, mock_credentials):
"""Test running multiple executions concurrently."""
mock_response = {
"job_id": "job_concurrent",
"status": "completed",
"stdout": "result",
}
with patch("un_async._make_request", new_callable=AsyncMock) as mock_req:
mock_req.return_value = mock_response
# Run 3 concurrent executions
results = await asyncio.gather(
execute_code("python", "print(1)"),
execute_code("javascript", "console.log(2)"),
execute_code("bash", "echo 3"),
)
assert len(results) == 3
assert all(r["status"] == "completed" for r in results)
@pytest.mark.asyncio
async def test_concurrent_with_different_statuses(self, mock_credentials):
"""Test concurrent operations with different final statuses."""
responses = [
{"job_id": "1", "status": "completed", "stdout": "ok"},
{"job_id": "2", "status": "failed", "stderr": "error"},
{"job_id": "3", "status": "timeout"},
]
with patch("un_async._make_request", new_callable=AsyncMock) as mock_req:
mock_req.side_effect = responses
results = await asyncio.gather(
execute_code("python", "print(1)"),
execute_code("python", "raise Exception()"),
execute_code("python", "while True: pass"),
)
assert results[0]["status"] == "completed"
assert results[1]["status"] == "failed"
assert results[2]["status"] == "timeout"
class TestAsyncSignature:
"""Test that functions have async signatures."""
def test_functions_are_coroutines(self):
"""Verify key functions are async."""
import inspect
# These should be coroutine functions
assert inspect.iscoroutinefunction(execute_code)
assert inspect.iscoroutinefunction(execute_async)
assert inspect.iscoroutinefunction(get_job)
assert inspect.iscoroutinefunction(wait_for_job)
assert inspect.iscoroutinefunction(cancel_job)
assert inspect.iscoroutinefunction(list_jobs)
# These should NOT be coroutine functions (sync helpers)
assert not inspect.iscoroutinefunction(un_async.detect_language)
assert not inspect.iscoroutinefunction(un_async._resolve_credentials)