feat: add logger

This commit is contained in:
tao.chen
2026-08-12 12:43:31 +08:00
parent 3981c32781
commit 416ff4d06a
8 changed files with 381 additions and 6 deletions
View File
@@ -0,0 +1,275 @@
"""Unit tests for the three new directory methods on RuntimeClient.
Covers:
- `create_directory` — PUT with `{"type": "directory"}` body.
- `delete_directory` — DELETE, surfaces Jupyter's 409 on non-empty dirs.
- `ensure_directory` — GET-first, falls back to `create_directory` on 404.
Uses `respx` to mock httpx transport so we don't need a live Jupyter.
The runtime descriptor (`get_workspace`) is patched to a synchronous return
so `_ensure_workspace` short-circuits without hitting the Runtime service.
"""
from __future__ import annotations
import httpx
import pytest
import respx
from backend.runtime_client import RuntimeClient, RuntimeClientError
WORKSPACE_ID = "01HWS0000000000000000000A"
BASE_URL = "http://runtime"
PORT = 34567
TOKEN = "test-token"
JUPYTER_URL = f"{BASE_URL}:{PORT}/jupyter/{WORKSPACE_ID}/api/contents"
def _running_descriptor() -> dict:
return {
"status": "running",
"workspace_id": WORKSPACE_ID,
"base_url": BASE_URL,
"port": PORT,
"token": TOKEN,
}
@pytest.fixture
def client() -> httpx.AsyncClient:
return httpx.AsyncClient(timeout=httpx.Timeout(5.0))
@pytest.fixture
def runtime(client: httpx.AsyncClient) -> RuntimeClient:
rt = RuntimeClient(client)
# Bypass the real `_ensure_workspace` so tests don't have to mock the
# Runtime service. The descriptor is otherwise identical to what the
# production path returns.
rt._ensure_workspace = _ensure_workspace_stub # type: ignore[assignment]
return rt
async def _ensure_workspace_stub(workspace_id: str) -> dict:
return _running_descriptor()
# ---------------------------------------------------------------------------
# create_directory
# ---------------------------------------------------------------------------
async def test_create_directory_happy_path(runtime: RuntimeClient) -> None:
with respx.mock(assert_all_called=True) as router:
route = router.put(f"{JUPYTER_URL}/01DIRAAAAAAAAAAAAAAA").mock(
return_value=httpx.Response(
201,
json={
"name": "01DIRAAAAAAAAAAAAAAA",
"type": "directory",
"path": "01DIRAAAAAAAAAAAAAAA",
},
)
)
async with httpx.AsyncClient() as transport:
runtime.client = transport # type: ignore[assignment]
result = await runtime.create_directory(
WORKSPACE_ID, name="01DIRAAAAAAAAAAAAAAA"
)
assert route.called
assert result["type"] == "directory"
# Verify request body shape (PUT contents/ directory).
request = route.calls[0].request
assert request.headers["Authorization"] == f"token {TOKEN}"
assert request.headers["Content-Type"] == "application/json"
assert request.content == b'{"type":"directory"}'
async def test_create_directory_nested_path(runtime: RuntimeClient) -> None:
"""Nested path `{parent_ulid}/{dir_ulid}` lands on Jupyter correctly."""
nested = "01DIR_PARENT_ULID/01DIR_CHILD_ULID"
with respx.mock(assert_all_called=True) as router:
route = router.put(f"{JUPYTER_URL}/{nested}").mock(
return_value=httpx.Response(
201, json={"name": "01DIR_CHILD_ULID", "type": "directory"}
)
)
async with httpx.AsyncClient() as transport:
runtime.client = transport # type: ignore[assignment]
await runtime.create_directory(WORKSPACE_ID, name=nested)
assert route.called
async def test_create_directory_propagates_jupyter_4xx(
runtime: RuntimeClient,
) -> None:
with respx.mock() as router:
put_route = router.put(f"{JUPYTER_URL}/01DIRAAAAAAAAAAAAAAA").mock(
return_value=httpx.Response(
400,
json={
"detail": {
"code": "BAD_REQUEST",
"message": "invalid name",
}
},
)
)
async with httpx.AsyncClient() as transport:
runtime.client = transport # type: ignore[assignment]
with pytest.raises(RuntimeClientError) as exc_info:
await runtime.create_directory(
WORKSPACE_ID, name="01DIRAAAAAAAAAAAAAAA"
)
assert put_route.called
assert exc_info.value.status_code == 400
# `_jupyter_request` surfaces the whole JSON body as `detail` — the
# inner `detail` envelope is preserved verbatim.
assert exc_info.value.detail == {
"detail": {
"code": "BAD_REQUEST",
"message": "invalid name",
}
}
# ---------------------------------------------------------------------------
# delete_directory
# ---------------------------------------------------------------------------
async def test_delete_directory_happy_path(runtime: RuntimeClient) -> None:
with respx.mock(assert_all_called=True) as router:
route = router.delete(f"{JUPYTER_URL}/01DIRAAAAAAAAAAAAAAA").mock(
return_value=httpx.Response(204)
)
async with httpx.AsyncClient() as transport:
runtime.client = transport # type: ignore[assignment]
result = await runtime.delete_directory(
WORKSPACE_ID, name="01DIRAAAAAAAAAAAAAAA"
)
assert route.called
assert result is None
async def test_delete_directory_non_empty_409(runtime: RuntimeClient) -> None:
"""Jupyter rejects non-empty directory deletes with 409; surface as-is."""
with respx.mock() as router:
router.delete(f"{JUPYTER_URL}/01DIRAAAAAAAAAAAAAAA").mock(
return_value=httpx.Response(
409,
json={
"detail": {
"code": "DIRECTORY_NOT_EMPTY",
"message": "Directory is not empty",
}
},
)
)
async with httpx.AsyncClient() as transport:
runtime.client = transport # type: ignore[assignment]
with pytest.raises(RuntimeClientError) as exc_info:
await runtime.delete_directory(
WORKSPACE_ID, name="01DIRAAAAAAAAAAAAAAA"
)
assert exc_info.value.status_code == 409
# ---------------------------------------------------------------------------
# ensure_directory
# ---------------------------------------------------------------------------
async def test_ensure_directory_already_exists(runtime: RuntimeClient) -> None:
"""GET succeeds → no PUT. The lazy-backfill is a no-op."""
put_called = False
def _track_put(request: httpx.Request) -> httpx.Response:
nonlocal put_called
put_called = True
return httpx.Response(201, json={})
with respx.mock(assert_all_called=False) as router:
router.get(f"{JUPYTER_URL}/01DIRAAAAAAAAAAAAAAA").mock(
return_value=httpx.Response(
200,
json={"name": "01DIRAAAAAAAAAAAAAAA", "type": "directory"},
)
)
router.put(f"{JUPYTER_URL}/01DIRAAAAAAAAAAAAAAA").mock(
side_effect=_track_put
)
async with httpx.AsyncClient() as transport:
runtime.client = transport # type: ignore[assignment]
await runtime.ensure_directory(
WORKSPACE_ID, "01DIRAAAAAAAAAAAAAAA"
)
assert not put_called, "PUT must not be issued when GET already shows the dir exists"
async def test_ensure_directory_missing_creates_it(
runtime: RuntimeClient,
) -> None:
"""GET 404 → PUT. The lazy-backfill creates the missing directory."""
with respx.mock(assert_all_called=True) as router:
get_route = router.get(f"{JUPYTER_URL}/01DIRAAAAAAAAAAAAAAA").mock(
return_value=httpx.Response(
404, json={"detail": {"code": "NOT_FOUND"}}
)
)
put_route = router.put(f"{JUPYTER_URL}/01DIRAAAAAAAAAAAAAAA").mock(
return_value=httpx.Response(
201, json={"type": "directory"}
)
)
async with httpx.AsyncClient() as transport:
runtime.client = transport # type: ignore[assignment]
await runtime.ensure_directory(
WORKSPACE_ID, "01DIRAAAAAAAAAAAAAAA"
)
assert get_route.called
assert put_route.called
async def test_ensure_directory_propagates_non_404_error(
runtime: RuntimeClient,
) -> None:
"""GET 500 → propagate; do NOT fall through to PUT."""
put_called = False
def _track_put(request: httpx.Request) -> httpx.Response:
nonlocal put_called
put_called = True
return httpx.Response(201, json={})
with respx.mock(assert_all_called=False) as router:
router.get(f"{JUPYTER_URL}/01DIRAAAAAAAAAAAAAAA").mock(
return_value=httpx.Response(500, text="internal error")
)
router.put(f"{JUPYTER_URL}/01DIRAAAAAAAAAAAAAAA").mock(
side_effect=_track_put
)
async with httpx.AsyncClient() as transport:
runtime.client = transport # type: ignore[assignment]
with pytest.raises(RuntimeClientError) as exc_info:
await runtime.ensure_directory(
WORKSPACE_ID, "01DIRAAAAAAAAAAAAAAA"
)
assert exc_info.value.status_code == 500
assert not put_called, "PUT must not be issued after a non-404 GET error"