{
  "name": "long-context-api-migration",
  "description": "Migrate 4 Python files from synchronous request-style API with error codes to async with exceptions",
  "dataset": "terminal-bench-local",
  "difficulty": "hard",
  "instruction": "You have 4 Python files (user_service.py, order_service.py, payment_service.py, notification_service.py) that use an old synchronous API style. Each file has 3-4 functions that need to be migrated to the new async style.\n\nRead MIGRATION_GUIDE.md for the full specification of what changes are required.\n\nKey changes for EVERY function in all 4 files:\n1. Convert `def` to `async def`\n2. Replace `client.get()`/`client.post()` calls with `await client.async_get()`/`await client.async_post()`\n3. Replace error code checking (`if response['status'] != 200`) with try/except blocks using `ServiceError`\n4. Replace `return {'status': code, 'error': msg}` error returns with `raise ServiceError(msg, code)`\n5. Replace `return {'status': 200, 'data': ...}` success returns with just `return data` (the dict that was under 'data')\n6. Update the `client` import: change `from old_client import SyncClient` to `from new_client import AsyncClient`\n7. Update client instantiation: change `client = SyncClient(base_url)` to `client = AsyncClient(base_url)`\n\nThe old_client and new_client modules are provided for reference. The test will verify all functions are properly async, use the new client, and handle errors with exceptions.\n\nRun python3 test_migration.py to verify.",
  "setup_files": {
    "MIGRATION_GUIDE.md": "# API Migration Guide: Sync to Async\n\n## Overview\nAll service files must be migrated from the old synchronous client to the new async client.\n\n## Changes Required\n\n### 1. Import Changes\nOld: `from old_client import SyncClient`\nNew: `from new_client import AsyncClient, ServiceError`\n\n### 2. Client Instantiation\nOld: `client = SyncClient('http://api.example.com')`\nNew: `client = AsyncClient('http://api.example.com')`\n\n### 3. Function Signatures\nOld: `def get_user(user_id):`\nNew: `async def get_user(user_id):`\n\n### 4. Client Calls\nOld: `response = client.get(f'/users/{user_id}')`\nNew: `response = await client.async_get(f'/users/{user_id}')`\n\nOld: `response = client.post('/users', data=payload)`\nNew: `response = await client.async_post('/users', data=payload)`\n\n### 5. Error Handling\nOld pattern:\n```python\nresponse = client.get(url)\nif response['status'] != 200:\n    return {'status': response['status'], 'error': response.get('message', 'Unknown error')}\nreturn {'status': 200, 'data': response['body']}\n```\n\nNew pattern:\n```python\ntry:\n    response = await client.async_get(url)\n    return response\nexcept ServiceError:\n    raise  # Re-raise ServiceError from client\n```\n\nNote: The new async client methods return the response body directly on success and raise ServiceError on failure. So the new functions should just return the response directly and let ServiceError propagate.\n\n### 6. Validation Errors\nOld: `return {'status': 400, 'error': 'Invalid input'}`\nNew: `raise ServiceError('Invalid input', 400)`\n\nAll places where the old code returned an error dict must raise ServiceError instead.\n",
    "old_client.py": "\"\"\"Old synchronous client - DO NOT MODIFY.\"\"\"\n\n\nclass SyncClient:\n    \"\"\"Simulates a synchronous HTTP client.\"\"\"\n    \n    def __init__(self, base_url):\n        self.base_url = base_url\n        self._data_store = {}\n    \n    def get(self, path, params=None):\n        \"\"\"Synchronous GET request. Returns dict with 'status' and 'body' or 'message'.\"\"\"\n        key = path.rstrip('/')\n        if key in self._data_store:\n            return {'status': 200, 'body': self._data_store[key]}\n        return {'status': 404, 'message': f'Not found: {path}'}\n    \n    def post(self, path, data=None):\n        \"\"\"Synchronous POST request. Returns dict with 'status' and 'body' or 'message'.\"\"\"\n        key = path.rstrip('/')\n        self._data_store[key] = data\n        return {'status': 200, 'body': data}\n    \n    def delete(self, path):\n        \"\"\"Synchronous DELETE request.\"\"\"\n        key = path.rstrip('/')\n        if key in self._data_store:\n            del self._data_store[key]\n            return {'status': 200, 'body': {'deleted': True}}\n        return {'status': 404, 'message': f'Not found: {path}'}\n",
    "new_client.py": "\"\"\"New async client - DO NOT MODIFY.\"\"\"\n\n\nclass ServiceError(Exception):\n    \"\"\"Error raised by async client on non-200 responses.\"\"\"\n    \n    def __init__(self, message, status_code=500):\n        super().__init__(message)\n        self.message = message\n        self.status_code = status_code\n\n\nclass AsyncClient:\n    \"\"\"Simulates an async HTTP client.\"\"\"\n    \n    def __init__(self, base_url):\n        self.base_url = base_url\n        self._data_store = {}\n    \n    async def async_get(self, path, params=None):\n        \"\"\"Async GET request. Returns body on success, raises ServiceError on failure.\"\"\"\n        key = path.rstrip('/')\n        if key in self._data_store:\n            return self._data_store[key]\n        raise ServiceError(f'Not found: {path}', 404)\n    \n    async def async_post(self, path, data=None):\n        \"\"\"Async POST request. Returns body on success, raises ServiceError on failure.\"\"\"\n        key = path.rstrip('/')\n        self._data_store[key] = data\n        return data\n    \n    async def async_delete(self, path):\n        \"\"\"Async DELETE request. Returns body on success, raises ServiceError on failure.\"\"\"\n        key = path.rstrip('/')\n        if key in self._data_store:\n            del self._data_store[key]\n            return {'deleted': True}\n        raise ServiceError(f'Not found: {path}', 404)\n",
    "user_service.py": "\"\"\"User management service.\"\"\"\n\nfrom old_client import SyncClient\n\nclient = SyncClient('http://api.example.com')\n\n\ndef get_user(user_id):\n    \"\"\"Fetch a user by ID.\"\"\"\n    if not user_id:\n        return {'status': 400, 'error': 'User ID is required'}\n    \n    response = client.get(f'/users/{user_id}')\n    if response['status'] != 200:\n        return {'status': response['status'], 'error': response.get('message', 'Unknown error')}\n    \n    return {'status': 200, 'data': response['body']}\n\n\ndef create_user(name, email):\n    \"\"\"Create a new user.\"\"\"\n    if not name or not email:\n        return {'status': 400, 'error': 'Name and email are required'}\n    \n    if '@' not in email:\n        return {'status': 400, 'error': 'Invalid email format'}\n    \n    payload = {'name': name, 'email': email}\n    response = client.post('/users', data=payload)\n    if response['status'] != 200:\n        return {'status': response['status'], 'error': response.get('message', 'Unknown error')}\n    \n    return {'status': 200, 'data': response['body']}\n\n\ndef update_user(user_id, updates):\n    \"\"\"Update a user's information.\"\"\"\n    if not user_id:\n        return {'status': 400, 'error': 'User ID is required'}\n    \n    if not updates or not isinstance(updates, dict):\n        return {'status': 400, 'error': 'Updates must be a non-empty dict'}\n    \n    # First check user exists\n    response = client.get(f'/users/{user_id}')\n    if response['status'] != 200:\n        return {'status': 404, 'error': f'User {user_id} not found'}\n    \n    current = response['body']\n    current.update(updates)\n    response = client.post(f'/users/{user_id}', data=current)\n    if response['status'] != 200:\n        return {'status': response['status'], 'error': response.get('message', 'Unknown error')}\n    \n    return {'status': 200, 'data': response['body']}\n\n\ndef delete_user(user_id):\n    \"\"\"Delete a user.\"\"\"\n    if not user_id:\n        return {'status': 400, 'error': 'User ID is required'}\n    \n    response = client.delete(f'/users/{user_id}')\n    if response['status'] != 200:\n        return {'status': response['status'], 'error': response.get('message', 'Unknown error')}\n    \n    return {'status': 200, 'data': response['body']}\n",
    "order_service.py": "\"\"\"Order management service.\"\"\"\n\nfrom old_client import SyncClient\n\nclient = SyncClient('http://api.example.com')\n\n\ndef create_order(user_id, items):\n    \"\"\"Create a new order for a user.\"\"\"\n    if not user_id:\n        return {'status': 400, 'error': 'User ID is required'}\n    \n    if not items or not isinstance(items, list):\n        return {'status': 400, 'error': 'Items must be a non-empty list'}\n    \n    total = sum(item.get('price', 0) * item.get('quantity', 0) for item in items)\n    \n    payload = {\n        'user_id': user_id,\n        'items': items,\n        'total': total,\n        'status': 'pending'\n    }\n    response = client.post('/orders', data=payload)\n    if response['status'] != 200:\n        return {'status': response['status'], 'error': response.get('message', 'Unknown error')}\n    \n    return {'status': 200, 'data': response['body']}\n\n\ndef get_order(order_id):\n    \"\"\"Get order details.\"\"\"\n    if not order_id:\n        return {'status': 400, 'error': 'Order ID is required'}\n    \n    response = client.get(f'/orders/{order_id}')\n    if response['status'] != 200:\n        return {'status': response['status'], 'error': response.get('message', 'Unknown error')}\n    \n    return {'status': 200, 'data': response['body']}\n\n\ndef update_order_status(order_id, new_status):\n    \"\"\"Update the status of an order.\"\"\"\n    valid_statuses = ['pending', 'confirmed', 'shipped', 'delivered', 'cancelled']\n    \n    if not order_id:\n        return {'status': 400, 'error': 'Order ID is required'}\n    \n    if new_status not in valid_statuses:\n        return {'status': 400, 'error': f'Invalid status. Must be one of: {valid_statuses}'}\n    \n    response = client.get(f'/orders/{order_id}')\n    if response['status'] != 200:\n        return {'status': 404, 'error': f'Order {order_id} not found'}\n    \n    order = response['body']\n    order['status'] = new_status\n    response = client.post(f'/orders/{order_id}', data=order)\n    if response['status'] != 200:\n        return {'status': response['status'], 'error': response.get('message', 'Unknown error')}\n    \n    return {'status': 200, 'data': response['body']}\n",
    "payment_service.py": "\"\"\"Payment processing service.\"\"\"\n\nfrom old_client import SyncClient\n\nclient = SyncClient('http://api.example.com')\n\n\ndef process_payment(order_id, amount, method):\n    \"\"\"Process a payment for an order.\"\"\"\n    valid_methods = ['credit_card', 'debit_card', 'paypal', 'bank_transfer']\n    \n    if not order_id:\n        return {'status': 400, 'error': 'Order ID is required'}\n    \n    if amount <= 0:\n        return {'status': 400, 'error': 'Amount must be positive'}\n    \n    if method not in valid_methods:\n        return {'status': 400, 'error': f'Invalid payment method. Must be one of: {valid_methods}'}\n    \n    payload = {\n        'order_id': order_id,\n        'amount': amount,\n        'method': method,\n        'status': 'completed'\n    }\n    response = client.post(f'/payments/{order_id}', data=payload)\n    if response['status'] != 200:\n        return {'status': response['status'], 'error': response.get('message', 'Unknown error')}\n    \n    return {'status': 200, 'data': response['body']}\n\n\ndef get_payment(payment_id):\n    \"\"\"Get payment details.\"\"\"\n    if not payment_id:\n        return {'status': 400, 'error': 'Payment ID is required'}\n    \n    response = client.get(f'/payments/{payment_id}')\n    if response['status'] != 200:\n        return {'status': response['status'], 'error': response.get('message', 'Unknown error')}\n    \n    return {'status': 200, 'data': response['body']}\n\n\ndef refund_payment(payment_id, reason):\n    \"\"\"Refund a payment.\"\"\"\n    if not payment_id:\n        return {'status': 400, 'error': 'Payment ID is required'}\n    \n    if not reason:\n        return {'status': 400, 'error': 'Refund reason is required'}\n    \n    response = client.get(f'/payments/{payment_id}')\n    if response['status'] != 200:\n        return {'status': 404, 'error': f'Payment {payment_id} not found'}\n    \n    payment = response['body']\n    refund_data = {\n        'original_payment': payment,\n        'reason': reason,\n        'status': 'refunded'\n    }\n    response = client.post(f'/refunds/{payment_id}', data=refund_data)\n    if response['status'] != 200:\n        return {'status': response['status'], 'error': response.get('message', 'Unknown error')}\n    \n    return {'status': 200, 'data': response['body']}\n",
    "notification_service.py": "\"\"\"Notification service.\"\"\"\n\nfrom old_client import SyncClient\n\nclient = SyncClient('http://api.example.com')\n\n\ndef send_notification(user_id, message, channel='email'):\n    \"\"\"Send a notification to a user.\"\"\"\n    valid_channels = ['email', 'sms', 'push']\n    \n    if not user_id:\n        return {'status': 400, 'error': 'User ID is required'}\n    \n    if not message:\n        return {'status': 400, 'error': 'Message is required'}\n    \n    if channel not in valid_channels:\n        return {'status': 400, 'error': f'Invalid channel. Must be one of: {valid_channels}'}\n    \n    payload = {\n        'user_id': user_id,\n        'message': message,\n        'channel': channel,\n        'status': 'sent'\n    }\n    response = client.post(f'/notifications/{user_id}', data=payload)\n    if response['status'] != 200:\n        return {'status': response['status'], 'error': response.get('message', 'Unknown error')}\n    \n    return {'status': 200, 'data': response['body']}\n\n\ndef get_notifications(user_id):\n    \"\"\"Get all notifications for a user.\"\"\"\n    if not user_id:\n        return {'status': 400, 'error': 'User ID is required'}\n    \n    response = client.get(f'/notifications/{user_id}')\n    if response['status'] != 200:\n        return {'status': response['status'], 'error': response.get('message', 'Unknown error')}\n    \n    return {'status': 200, 'data': response['body']}\n\n\ndef send_bulk_notifications(user_ids, message, channel='email'):\n    \"\"\"Send the same notification to multiple users.\"\"\"\n    if not user_ids or not isinstance(user_ids, list):\n        return {'status': 400, 'error': 'User IDs must be a non-empty list'}\n    \n    if not message:\n        return {'status': 400, 'error': 'Message is required'}\n    \n    results = []\n    for user_id in user_ids:\n        result = send_notification(user_id, message, channel)\n        results.append({'user_id': user_id, 'result': result})\n    \n    return {'status': 200, 'data': results}\n",
    "test_migration.py": "\"\"\"Verify all 4 service files have been properly migrated to async.\"\"\"\nimport sys\nimport ast\nimport asyncio\nimport inspect\n\nerrors = []\n\nservice_files = [\n    'user_service.py',\n    'order_service.py',\n    'payment_service.py',\n    'notification_service.py',\n]\n\n# Expected async functions per file\nexpected_functions = {\n    'user_service.py': ['get_user', 'create_user', 'update_user', 'delete_user'],\n    'order_service.py': ['create_order', 'get_order', 'update_order_status'],\n    'payment_service.py': ['process_payment', 'get_payment', 'refund_payment'],\n    'notification_service.py': ['send_notification', 'get_notifications', 'send_bulk_notifications'],\n}\n\nfor filename in service_files:\n    try:\n        with open(filename, 'r') as f:\n            source = f.read()\n    except FileNotFoundError:\n        errors.append(f\"{filename}: file not found\")\n        continue\n    \n    # Check imports\n    if 'from old_client import' in source or 'import old_client' in source:\n        errors.append(f\"{filename}: still imports old_client\")\n    \n    if 'from new_client import AsyncClient' not in source:\n        errors.append(f\"{filename}: does not import AsyncClient from new_client\")\n    \n    if 'ServiceError' not in source:\n        errors.append(f\"{filename}: does not reference ServiceError\")\n    \n    # Check no sync client\n    if 'SyncClient' in source:\n        errors.append(f\"{filename}: still references SyncClient\")\n    \n    # Parse AST to check async functions\n    try:\n        tree = ast.parse(source)\n    except SyntaxError as e:\n        errors.append(f\"{filename}: syntax error: {e}\")\n        continue\n    \n    # Check all expected functions are async\n    func_nodes = {}\n    for node in ast.walk(tree):\n        if isinstance(node, ast.AsyncFunctionDef):\n            func_nodes[node.name] = 'async'\n        elif isinstance(node, ast.FunctionDef):\n            func_nodes[node.name] = 'sync'\n    \n    for func_name in expected_functions[filename]:\n        if func_name not in func_nodes:\n            errors.append(f\"{filename}: function '{func_name}' not found\")\n        elif func_nodes[func_name] != 'async':\n            errors.append(f\"{filename}: function '{func_name}' is not async\")\n    \n    # Check that old patterns are gone\n    if \"response['status']\" in source:\n        errors.append(f\"{filename}: still checks response['status'] (old pattern)\")\n    \n    if \"'status': 200\" in source or '\"status\": 200' in source:\n        errors.append(f\"{filename}: still returns status: 200 dicts (old pattern)\")\n    \n    # Check that async client methods are used\n    if 'client.get(' in source:\n        errors.append(f\"{filename}: still uses client.get() instead of await client.async_get()\")\n    \n    if 'client.post(' in source and 'client.async_post(' not in source:\n        errors.append(f\"{filename}: still uses client.post() instead of await client.async_post()\")\n    \n    # For files that had client.delete\n    if 'client.delete(' in source and 'client.async_delete(' not in source:\n        errors.append(f\"{filename}: still uses client.delete() instead of await client.async_delete()\")\n    \n    # Check await is used\n    if 'await ' not in source:\n        errors.append(f\"{filename}: no 'await' keyword found\")\n\n# === Runtime Tests ===\n# Import and test the async functions actually work\ntry:\n    # We need to test in an async context\n    async def run_runtime_tests():\n        runtime_errors = []\n        \n        # Test user_service\n        try:\n            from user_service import get_user, create_user, update_user, delete_user\n            from new_client import ServiceError\n            \n            # Test create_user\n            result = await create_user('Alice', 'alice@example.com')\n            if not isinstance(result, dict) or 'name' not in result:\n                runtime_errors.append(\"user_service.create_user should return the data dict directly\")\n            \n            # Test validation error raises ServiceError\n            try:\n                await create_user('', 'test@test.com')\n                runtime_errors.append(\"user_service.create_user should raise ServiceError for empty name\")\n            except ServiceError as e:\n                if e.status_code != 400:\n                    runtime_errors.append(f\"user_service.create_user ServiceError should have status 400, got {e.status_code}\")\n            \n            # Test invalid email\n            try:\n                await create_user('Bob', 'invalid-email')\n                runtime_errors.append(\"user_service.create_user should raise ServiceError for invalid email\")\n            except ServiceError:\n                pass\n                \n        except ImportError as e:\n            runtime_errors.append(f\"Cannot import user_service: {e}\")\n        except Exception as e:\n            runtime_errors.append(f\"user_service runtime error: {type(e).__name__}: {e}\")\n        \n        # Test order_service\n        try:\n            from order_service import create_order, get_order, update_order_status\n            from new_client import ServiceError\n            \n            items = [{'name': 'Widget', 'price': 10.0, 'quantity': 2}]\n            result = await create_order('user1', items)\n            if not isinstance(result, dict) or 'items' not in result:\n                runtime_errors.append(\"order_service.create_order should return data dict directly\")\n            \n            try:\n                await create_order('', items)\n                runtime_errors.append(\"order_service.create_order should raise ServiceError for empty user_id\")\n            except ServiceError:\n                pass\n                \n        except ImportError as e:\n            runtime_errors.append(f\"Cannot import order_service: {e}\")\n        except Exception as e:\n            runtime_errors.append(f\"order_service runtime error: {type(e).__name__}: {e}\")\n        \n        # Test payment_service\n        try:\n            from payment_service import process_payment, get_payment, refund_payment\n            from new_client import ServiceError\n            \n            result = await process_payment('order1', 25.00, 'credit_card')\n            if not isinstance(result, dict) or 'amount' not in result:\n                runtime_errors.append(\"payment_service.process_payment should return data dict directly\")\n            \n            try:\n                await process_payment('order1', -5, 'credit_card')\n                runtime_errors.append(\"payment_service.process_payment should raise ServiceError for negative amount\")\n            except ServiceError:\n                pass\n                \n        except ImportError as e:\n            runtime_errors.append(f\"Cannot import payment_service: {e}\")\n        except Exception as e:\n            runtime_errors.append(f\"payment_service runtime error: {type(e).__name__}: {e}\")\n        \n        # Test notification_service\n        try:\n            from notification_service import send_notification, get_notifications, send_bulk_notifications\n            from new_client import ServiceError\n            \n            result = await send_notification('user1', 'Hello!')\n            if not isinstance(result, dict) or 'message' not in result:\n                runtime_errors.append(\"notification_service.send_notification should return data dict directly\")\n            \n            try:\n                await send_notification('user1', '', 'email')\n                runtime_errors.append(\"notification_service.send_notification should raise ServiceError for empty message\")\n            except ServiceError:\n                pass\n            \n            # Test bulk - should also be async and return list\n            result = await send_bulk_notifications(['u1', 'u2'], 'Bulk hello')\n            if not isinstance(result, list):\n                runtime_errors.append(\"notification_service.send_bulk_notifications should return a list\")\n                \n        except ImportError as e:\n            runtime_errors.append(f\"Cannot import notification_service: {e}\")\n        except Exception as e:\n            runtime_errors.append(f\"notification_service runtime error: {type(e).__name__}: {e}\")\n        \n        return runtime_errors\n    \n    runtime_errors = asyncio.run(run_runtime_tests())\n    errors.extend(runtime_errors)\n    \nexcept Exception as e:\n    errors.append(f\"Runtime test framework error: {type(e).__name__}: {e}\")\n\nif errors:\n    print(f\"FAILED with {len(errors)} error(s):\")\n    for e in errors:\n        print(f\"  - {e}\")\n    sys.exit(1)\nelse:\n    print(\"ALL CHECKS PASSED\")\n    print(\"  - All 4 files migrated to async\")\n    print(\"  - All imports updated to new_client\")\n    print(\"  - All functions converted to async def\")\n    print(\"  - All client calls use await + async methods\")\n    print(\"  - Error handling uses ServiceError exceptions\")\n    print(\"  - Return values are data dicts (not status wrappers)\")\n    print(\"  - Runtime tests pass for all services\")\n    sys.exit(0)\n"
  },
  "verify": "cd $BENCH_WORK_DIR && python3 test_migration.py",
  "timeout": 360000,
  "tags": ["long-context", "migration", "async", "python", "multi-file", "coordinated-changes"]
}