Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
24 changes: 16 additions & 8 deletions src/runloop_api_client/resources/devboxes/devboxes.py
Original file line number Diff line number Diff line change
Expand Up @@ -867,7 +867,7 @@ def execute(
id: str,
*,
command: str,
command_id: str = str(uuid7()),
command_id: str | None = None,
last_n: str | Omit = omit,
optimistic_timeout: Optional[int] | Omit = omit,
shell_name: Optional[str] | Omit = omit,
Expand All @@ -892,7 +892,8 @@ def execute(
specified the command is run from the directory based on the recent state of the
persistent shell.

command_id: The command ID in UUIDv7 string format for idempotency and tracking
command_id: The command ID in UUIDv7 string format for idempotency and tracking.
A fresh UUID is generated per call if not provided.

last_n: Last n lines of standard error / standard out to return (default: 100)

Expand All @@ -915,6 +916,8 @@ def execute(
"""
if not id:
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
if command_id is None:
command_id = str(uuid7())
if not is_given(timeout) and self._client.timeout == DEFAULT_TIMEOUT:
timeout = 600
return self._post(
Expand Down Expand Up @@ -944,7 +947,7 @@ def execute_and_await_completion(
devbox_id: str,
*,
command: str,
command_id: str = str(uuid7()),
command_id: str | None = None,
last_n: str | Omit = omit,
optimistic_timeout: Optional[int] | Omit = omit,
shell_name: Optional[str] | Omit = omit,
Expand All @@ -963,9 +966,11 @@ def execute_and_await_completion(
return the result within the initial request's timeout. If the execution is not yet
complete, it switches to using wait_for_command to minimize latency while waiting.

A command_id (UUIDv7) is automatically generated for idempotency and tracking.
A command_id (UUIDv7) is automatically generated per call for idempotency and tracking.
You can provide your own command_id to enable custom retry logic or external tracking.
"""
if command_id is None:
command_id = str(uuid7())
execution = self.execute(
devbox_id,
command=command,
Expand Down Expand Up @@ -2543,7 +2548,7 @@ async def execute(
id: str,
*,
command: str,
command_id: str = str(uuid7()),
command_id: str | None = None,
last_n: str | Omit = omit,
optimistic_timeout: Optional[int] | Omit = omit,
shell_name: Optional[str] | Omit = omit,
Expand All @@ -2568,7 +2573,8 @@ async def execute(
specified the command is run from the directory based on the recent state of the
persistent shell.

command_id: The command ID in UUIDv7 string format for idempotency and tracking
command_id: The command ID in UUIDv7 string format for idempotency and tracking.
A fresh UUID is generated per call if not provided.

last_n: Last n lines of standard error / standard out to return (default: 100)

Expand All @@ -2591,6 +2597,8 @@ async def execute(
"""
if not id:
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
if command_id is None:
command_id = str(uuid7())
if not is_given(timeout) and self._client.timeout == DEFAULT_TIMEOUT:
timeout = 600
return await self._post(
Expand Down Expand Up @@ -2620,7 +2628,7 @@ async def execute_and_await_completion(
devbox_id: str,
*,
command: str,
command_id: str = str(uuid7()),
command_id: str | None = None,
last_n: str | Omit = omit,
optimistic_timeout: Optional[int] | Omit = omit,
shell_name: Optional[str] | Omit = omit,
Expand All @@ -2639,7 +2647,7 @@ async def execute_and_await_completion(
return the result within the initial request's timeout. If the execution is not yet
complete, it switches to using wait_for_command to minimize latency while waiting.

A command_id (UUIDv7) is automatically generated for idempotency and tracking.
A command_id (UUIDv7) is automatically generated per call for idempotency and tracking.
You can provide your own command_id to enable custom retry logic or external tracking.
"""

Expand Down
86 changes: 86 additions & 0 deletions tests/test_command_id.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
"""Tests for command_id default generation in execute / execute_and_await_completion.

Verifies that each call generates a fresh UUIDv7 rather than reusing a frozen
default (the bug fixed in this change).
"""

from __future__ import annotations

import json

import httpx
import pytest
import respx

from runloop_api_client import AsyncRunloop, Runloop

BASE = "http://localhost"
EXECUTE_PATTERN = f"{BASE}/v1/devboxes/dbx_test/execute"

STUB_RESPONSE = {
"execution_id": "exec_1",
"command_id": "ignored",
"devbox_id": "dbx_test",
"status": "completed",
"exit_status": 0,
"stdout": "",
"stderr": "",
}


class TestCommandIdUniqueness:
"""Every call without an explicit command_id must produce a distinct UUID."""

@respx.mock
def test_sync_execute_generates_unique_ids(self) -> None:
route = respx.post(EXECUTE_PATTERN).mock(
return_value=httpx.Response(200, json=STUB_RESPONSE)
)
client = Runloop(base_url=BASE, bearer_token="test")

for _ in range(5):
client.devboxes.execute(id="dbx_test", command="echo hi")

assert route.call_count == 5
ids = [json.loads(call.request.content)["command_id"] for call in route.calls]
assert len(set(ids)) == 5, f"All command_ids should be unique, got: {ids}"

@respx.mock
def test_sync_execute_respects_explicit_id(self) -> None:
route = respx.post(EXECUTE_PATTERN).mock(
return_value=httpx.Response(200, json=STUB_RESPONSE)
)
client = Runloop(base_url=BASE, bearer_token="test")

client.devboxes.execute(id="dbx_test", command="echo hi", command_id="my-custom-id")

body = json.loads(route.calls[0].request.content)
assert body["command_id"] == "my-custom-id"

@respx.mock
@pytest.mark.asyncio
async def test_async_execute_generates_unique_ids(self) -> None:
route = respx.post(EXECUTE_PATTERN).mock(
return_value=httpx.Response(200, json=STUB_RESPONSE)
)
client = AsyncRunloop(base_url=BASE, bearer_token="test")

for _ in range(5):
await client.devboxes.execute(id="dbx_test", command="echo hi")

assert route.call_count == 5
ids = [json.loads(call.request.content)["command_id"] for call in route.calls]
assert len(set(ids)) == 5, f"All command_ids should be unique, got: {ids}"

@respx.mock
@pytest.mark.asyncio
async def test_async_execute_respects_explicit_id(self) -> None:
route = respx.post(EXECUTE_PATTERN).mock(
return_value=httpx.Response(200, json=STUB_RESPONSE)
)
client = AsyncRunloop(base_url=BASE, bearer_token="test")

await client.devboxes.execute(id="dbx_test", command="echo hi", command_id="my-custom-id")

body = json.loads(route.calls[0].request.content)
assert body["command_id"] == "my-custom-id"
Loading