Skip to content

Commit fd1d2ec

Browse files
rgarciaclaude
andcommitted
feat: default telemetry subresource to direct-VM routing + extra fallback tests
Address adversarial review notes: - Land the spec's stated default allowlist ["curl", "telemetry"] so the telemetry stream (the motivating GET /telemetry consumer) is routed to the VM and eligible for control-plane fallback out of the box. - Add coverage for 503/504 (parametrized), async gone-404 fallback, and async POST non-fallback parity. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent b8e1509 commit fd1d2ec

2 files changed

Lines changed: 63 additions & 3 deletions

File tree

src/kernel/lib/browser_routing/routing.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ class BrowserRoutingConfig:
4141
def browser_routing_config_from_env() -> BrowserRoutingConfig:
4242
raw = os.environ.get("KERNEL_BROWSER_ROUTING_SUBRESOURCES")
4343
if raw is None:
44-
return BrowserRoutingConfig(subresources=("curl",))
44+
return BrowserRoutingConfig(subresources=("curl", "telemetry"))
4545
if raw.strip() == "":
4646
return BrowserRoutingConfig()
4747

tests/test_browser_routing.py

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -313,9 +313,9 @@ def test_browser_route_from_browser_requires_base_url_and_jwt() -> None:
313313
assert browser_route_from_browser({**_fake_browser(), "cdp_ws_url": None}) is None
314314

315315

316-
def test_browser_routing_config_from_env_defaults_to_curl(monkeypatch: pytest.MonkeyPatch) -> None:
316+
def test_browser_routing_config_from_env_defaults_to_curl_and_telemetry(monkeypatch: pytest.MonkeyPatch) -> None:
317317
monkeypatch.delenv("KERNEL_BROWSER_ROUTING_SUBRESOURCES", raising=False)
318-
assert browser_routing_config_from_env().subresources == ("curl",)
318+
assert browser_routing_config_from_env().subresources == ("curl", "telemetry")
319319

320320

321321
def test_browser_routing_config_from_env_empty_string_disables_routing(monkeypatch: pytest.MonkeyPatch) -> None:
@@ -495,3 +495,63 @@ async def test_async_dead_vm_502_falls_back_to_control_plane(monkeypatch: pytest
495495
cp_request = cast(httpx.Request, cast(Any, cp_route.calls[0]).request)
496496
assert cp_request.headers.get("Authorization") == f"Bearer {api_key}"
497497
assert cp_request.url.params.get("jwt") is None
498+
499+
500+
@pytest.mark.parametrize("status", [503, 504])
501+
@respx.mock
502+
def test_dead_vm_503_504_fall_back_to_control_plane(monkeypatch: pytest.MonkeyPatch, status: int) -> None:
503+
# 503/504 share the unreachable-status path with 502; lock in both explicitly.
504+
monkeypatch.setenv("KERNEL_BROWSER_ROUTING_SUBRESOURCES", "process")
505+
vm_route = respx.get(_VM_STATUS).mock(return_value=httpx.Response(status, text="unavailable"))
506+
cp_route = respx.get(_CP_STATUS).mock(return_value=httpx.Response(200, json={"state": "exited"}))
507+
with Kernel(base_url=base_url, api_key=api_key, max_retries=0, _strict_response_validation=True) as client:
508+
_cache_browser(client)
509+
out = client.browsers.process.status("p-1", id="sess-1")
510+
511+
assert vm_route.called
512+
assert cp_route.called
513+
assert out.state == "exited"
514+
515+
516+
@pytest.mark.asyncio
517+
@respx.mock
518+
async def test_async_gone_signal_404_falls_back_to_control_plane(monkeypatch: pytest.MonkeyPatch) -> None:
519+
monkeypatch.setenv("KERNEL_BROWSER_ROUTING_SUBRESOURCES", "process")
520+
vm_route = respx.get(_VM_STATUS).mock(return_value=httpx.Response(404, json=_GONE_BODY, headers=_GONE_HEADERS))
521+
cp_route = respx.get(_CP_STATUS).mock(return_value=httpx.Response(200, json={"state": "exited"}))
522+
async with AsyncKernel(base_url=base_url, api_key=api_key, max_retries=0, _strict_response_validation=True) as client:
523+
route = browser_route_from_browser(_fake_browser())
524+
assert route is not None
525+
client.browser_route_cache.set(route)
526+
out = await client.browsers.process.status("p-1", id="sess-1")
527+
528+
assert vm_route.called
529+
assert cp_route.called
530+
assert out.state == "exited"
531+
cp_request = cast(httpx.Request, cast(Any, cp_route.calls[0]).request)
532+
assert cp_request.headers.get("Authorization") == f"Bearer {api_key}"
533+
assert cp_request.url.params.get("jwt") is None
534+
535+
536+
@pytest.mark.asyncio
537+
@respx.mock
538+
async def test_async_post_to_dead_vm_does_not_fall_back(monkeypatch: pytest.MonkeyPatch) -> None:
539+
from kernel import APIStatusError
540+
541+
monkeypatch.setenv("KERNEL_BROWSER_ROUTING_SUBRESOURCES", "process")
542+
vm_route = respx.post("http://browser-session.test/browser/kernel/process/exec").mock(
543+
return_value=httpx.Response(502, text="Upstream not available")
544+
)
545+
cp_route = respx.post(f"{base_url}/browsers/sess-1/process/exec").mock(
546+
return_value=httpx.Response(200, json={"exit_code": 0, "stdout_b64": "", "stderr_b64": ""})
547+
)
548+
async with AsyncKernel(base_url=base_url, api_key=api_key, max_retries=0, _strict_response_validation=True) as client:
549+
route = browser_route_from_browser(_fake_browser())
550+
assert route is not None
551+
client.browser_route_cache.set(route)
552+
with pytest.raises(APIStatusError):
553+
await client.browsers.process.exec("sess-1", command="echo", args=["hi"])
554+
555+
assert vm_route.called
556+
# POST is side-effecting; must never be re-issued to the control plane (async parity).
557+
assert not cp_route.called

0 commit comments

Comments
 (0)