Deploy 2026-05-04
Browse filesCo-authored-by: OpenAI Codex <codex@openai.com>
backend/dependencies.py
CHANGED
|
@@ -12,7 +12,7 @@ from typing import Any
|
|
| 12 |
import httpx
|
| 13 |
from fastapi import HTTPException, Request, status
|
| 14 |
|
| 15 |
-
from agent.core.hf_tokens import bearer_token_from_header
|
| 16 |
|
| 17 |
from agent.core.hf_access import fetch_whoami_v2
|
| 18 |
|
|
@@ -36,6 +36,8 @@ DEV_USER: dict[str, Any] = {
|
|
| 36 |
"plan": "org", # Dev runs at the Pro/Org quota tier so local testing isn't capped.
|
| 37 |
}
|
| 38 |
|
|
|
|
|
|
|
| 39 |
# Plan field discovery — log the whoami-v2 shape once at DEBUG so we can
|
| 40 |
# confirm the actual key in production without hammering the HF API.
|
| 41 |
_WHOAMI_SHAPE_LOGGED = False
|
|
@@ -135,6 +137,7 @@ async def _extract_user_from_token(token: str) -> dict[str, Any] | None:
|
|
| 135 |
return None
|
| 136 |
user = _user_from_info(user_info)
|
| 137 |
user["plan"] = await _fetch_user_plan(token)
|
|
|
|
| 138 |
return user
|
| 139 |
|
| 140 |
|
|
@@ -145,13 +148,13 @@ async def _dev_user_from_env() -> dict[str, Any]:
|
|
| 145 |
real HF namespace. Deriving the dev user from HF_TOKEN keeps local uploads
|
| 146 |
pointed at the token owner's dataset instead of dev/ml-intern-sessions.
|
| 147 |
"""
|
| 148 |
-
token = os.environ.get("HF_TOKEN", "")
|
| 149 |
if not token:
|
| 150 |
-
return DEV_USER
|
| 151 |
|
| 152 |
whoami = await fetch_whoami_v2(token)
|
| 153 |
if not isinstance(whoami, dict):
|
| 154 |
-
return DEV_USER
|
| 155 |
|
| 156 |
username = None
|
| 157 |
for key in ("name", "user", "preferred_username"):
|
|
@@ -160,13 +163,14 @@ async def _dev_user_from_env() -> dict[str, Any]:
|
|
| 160 |
username = value
|
| 161 |
break
|
| 162 |
if not username:
|
| 163 |
-
return DEV_USER
|
| 164 |
|
| 165 |
return {
|
| 166 |
"user_id": username,
|
| 167 |
"username": username,
|
| 168 |
"authenticated": True,
|
| 169 |
"plan": await _fetch_user_plan(token),
|
|
|
|
| 170 |
}
|
| 171 |
|
| 172 |
|
|
|
|
| 12 |
import httpx
|
| 13 |
from fastapi import HTTPException, Request, status
|
| 14 |
|
| 15 |
+
from agent.core.hf_tokens import bearer_token_from_header, clean_hf_token
|
| 16 |
|
| 17 |
from agent.core.hf_access import fetch_whoami_v2
|
| 18 |
|
|
|
|
| 36 |
"plan": "org", # Dev runs at the Pro/Org quota tier so local testing isn't capped.
|
| 37 |
}
|
| 38 |
|
| 39 |
+
INTERNAL_HF_TOKEN_KEY = "_hf_token"
|
| 40 |
+
|
| 41 |
# Plan field discovery — log the whoami-v2 shape once at DEBUG so we can
|
| 42 |
# confirm the actual key in production without hammering the HF API.
|
| 43 |
_WHOAMI_SHAPE_LOGGED = False
|
|
|
|
| 137 |
return None
|
| 138 |
user = _user_from_info(user_info)
|
| 139 |
user["plan"] = await _fetch_user_plan(token)
|
| 140 |
+
user[INTERNAL_HF_TOKEN_KEY] = clean_hf_token(token)
|
| 141 |
return user
|
| 142 |
|
| 143 |
|
|
|
|
| 148 |
real HF namespace. Deriving the dev user from HF_TOKEN keeps local uploads
|
| 149 |
pointed at the token owner's dataset instead of dev/ml-intern-sessions.
|
| 150 |
"""
|
| 151 |
+
token = clean_hf_token(os.environ.get("HF_TOKEN", ""))
|
| 152 |
if not token:
|
| 153 |
+
return dict(DEV_USER)
|
| 154 |
|
| 155 |
whoami = await fetch_whoami_v2(token)
|
| 156 |
if not isinstance(whoami, dict):
|
| 157 |
+
return dict(DEV_USER)
|
| 158 |
|
| 159 |
username = None
|
| 160 |
for key in ("name", "user", "preferred_username"):
|
|
|
|
| 163 |
username = value
|
| 164 |
break
|
| 165 |
if not username:
|
| 166 |
+
return dict(DEV_USER)
|
| 167 |
|
| 168 |
return {
|
| 169 |
"user_id": username,
|
| 170 |
"username": username,
|
| 171 |
"authenticated": True,
|
| 172 |
"plan": await _fetch_user_plan(token),
|
| 173 |
+
INTERNAL_HF_TOKEN_KEY: token,
|
| 174 |
}
|
| 175 |
|
| 176 |
|
backend/routes/agent.py
CHANGED
|
@@ -10,7 +10,11 @@ import logging
|
|
| 10 |
import os
|
| 11 |
from typing import Any
|
| 12 |
|
| 13 |
-
from dependencies import
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
from fastapi import (
|
| 15 |
APIRouter,
|
| 16 |
Depends,
|
|
@@ -209,6 +213,12 @@ async def _enforce_gated_model_quota(
|
|
| 209 |
await session_manager.persist_session_snapshot(agent_session)
|
| 210 |
|
| 211 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 212 |
async def _check_session_access(
|
| 213 |
session_id: str,
|
| 214 |
user: dict[str, Any],
|
|
@@ -216,7 +226,7 @@ async def _check_session_access(
|
|
| 216 |
preload_sandbox: bool = True,
|
| 217 |
) -> AgentSession:
|
| 218 |
"""Verify and lazily load the user's session. Raises 403 or 404."""
|
| 219 |
-
hf_token = resolve_hf_request_token(request) if request is not None else
|
| 220 |
agent_session = await session_manager.ensure_session_loaded(
|
| 221 |
session_id,
|
| 222 |
user["user_id"],
|
|
@@ -318,9 +328,7 @@ async def generate_title(
|
|
| 318 |
reasoning model — reasoning_effort=low keeps the reasoning budget small
|
| 319 |
so the 60-token output budget isn't consumed before the title is written.
|
| 320 |
"""
|
| 321 |
-
api_key = resolve_hf_router_token(
|
| 322 |
-
user.get("hf_token") if isinstance(user, dict) else None
|
| 323 |
-
)
|
| 324 |
try:
|
| 325 |
response = await acompletion(
|
| 326 |
# Double openai/ prefix: LiteLLM strips the first as its provider
|
|
|
|
| 10 |
import os
|
| 11 |
from typing import Any
|
| 12 |
|
| 13 |
+
from dependencies import (
|
| 14 |
+
INTERNAL_HF_TOKEN_KEY,
|
| 15 |
+
get_current_user,
|
| 16 |
+
require_huggingface_org_member,
|
| 17 |
+
)
|
| 18 |
from fastapi import (
|
| 19 |
APIRouter,
|
| 20 |
Depends,
|
|
|
|
| 213 |
await session_manager.persist_session_snapshot(agent_session)
|
| 214 |
|
| 215 |
|
| 216 |
+
def _user_hf_token(user: dict[str, Any] | None) -> str | None:
|
| 217 |
+
if not isinstance(user, dict):
|
| 218 |
+
return None
|
| 219 |
+
return user.get(INTERNAL_HF_TOKEN_KEY)
|
| 220 |
+
|
| 221 |
+
|
| 222 |
async def _check_session_access(
|
| 223 |
session_id: str,
|
| 224 |
user: dict[str, Any],
|
|
|
|
| 226 |
preload_sandbox: bool = True,
|
| 227 |
) -> AgentSession:
|
| 228 |
"""Verify and lazily load the user's session. Raises 403 or 404."""
|
| 229 |
+
hf_token = resolve_hf_request_token(request) if request is not None else _user_hf_token(user)
|
| 230 |
agent_session = await session_manager.ensure_session_loaded(
|
| 231 |
session_id,
|
| 232 |
user["user_id"],
|
|
|
|
| 328 |
reasoning model — reasoning_effort=low keeps the reasoning budget small
|
| 329 |
so the 60-token output budget isn't consumed before the title is written.
|
| 330 |
"""
|
| 331 |
+
api_key = resolve_hf_router_token(_user_hf_token(user))
|
|
|
|
|
|
|
| 332 |
try:
|
| 333 |
response = await acompletion(
|
| 334 |
# Double openai/ prefix: LiteLLM strips the first as its provider
|
backend/routes/auth.py
CHANGED
|
@@ -167,6 +167,5 @@ async def get_me(user: dict = Depends(get_current_user)) -> dict:
|
|
| 167 |
|
| 168 |
Uses the shared auth dependency which handles cookie + Bearer token.
|
| 169 |
"""
|
| 170 |
-
return user
|
| 171 |
-
|
| 172 |
|
|
|
|
| 167 |
|
| 168 |
Uses the shared auth dependency which handles cookie + Bearer token.
|
| 169 |
"""
|
| 170 |
+
return {key: value for key, value in user.items() if not key.startswith("_")}
|
|
|
|
| 171 |
|
backend/session_manager.py
CHANGED
|
@@ -376,6 +376,40 @@ class SessionManager:
|
|
| 376 |
agent_session.hf_username = hf_username
|
| 377 |
agent_session.session.hf_username = hf_username
|
| 378 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 379 |
async def _clear_persisted_sandbox_metadata(self, session_id: str) -> None:
|
| 380 |
try:
|
| 381 |
await self._store().update_session_fields(
|
|
@@ -526,6 +560,10 @@ class SessionManager:
|
|
| 526 |
hf_token=hf_token,
|
| 527 |
hf_username=hf_username,
|
| 528 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 529 |
return existing
|
| 530 |
return None
|
| 531 |
|
|
@@ -543,6 +581,10 @@ class SessionManager:
|
|
| 543 |
hf_token=hf_token,
|
| 544 |
hf_username=hf_username,
|
| 545 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 546 |
return existing
|
| 547 |
return None
|
| 548 |
|
|
|
|
| 376 |
agent_session.hf_username = hf_username
|
| 377 |
agent_session.session.hf_username = hf_username
|
| 378 |
|
| 379 |
+
@staticmethod
|
| 380 |
+
def _has_active_sandbox_preload(agent_session: AgentSession) -> bool:
|
| 381 |
+
task = getattr(agent_session.session, "sandbox_preload_task", None)
|
| 382 |
+
return bool(task and not task.done())
|
| 383 |
+
|
| 384 |
+
@staticmethod
|
| 385 |
+
def _preload_failed_for_missing_hf_token(agent_session: AgentSession) -> bool:
|
| 386 |
+
error = getattr(agent_session.session, "sandbox_preload_error", None)
|
| 387 |
+
return isinstance(error, str) and error.startswith("No HF token available")
|
| 388 |
+
|
| 389 |
+
def _restart_cpu_preload_if_token_recovered(
|
| 390 |
+
self,
|
| 391 |
+
agent_session: AgentSession,
|
| 392 |
+
*,
|
| 393 |
+
preload_sandbox: bool,
|
| 394 |
+
) -> None:
|
| 395 |
+
if not preload_sandbox:
|
| 396 |
+
return
|
| 397 |
+
session = agent_session.session
|
| 398 |
+
if getattr(session, "sandbox", None):
|
| 399 |
+
return
|
| 400 |
+
if self._has_active_sandbox_preload(agent_session):
|
| 401 |
+
return
|
| 402 |
+
if not (agent_session.hf_token or getattr(session, "hf_token", None)):
|
| 403 |
+
return
|
| 404 |
+
|
| 405 |
+
if not self._preload_failed_for_missing_hf_token(agent_session):
|
| 406 |
+
return
|
| 407 |
+
|
| 408 |
+
session.sandbox_preload_error = None
|
| 409 |
+
session.sandbox_preload_task = None
|
| 410 |
+
session.sandbox_preload_cancel_event = None
|
| 411 |
+
self._start_cpu_sandbox_preload(agent_session)
|
| 412 |
+
|
| 413 |
async def _clear_persisted_sandbox_metadata(self, session_id: str) -> None:
|
| 414 |
try:
|
| 415 |
await self._store().update_session_fields(
|
|
|
|
| 560 |
hf_token=hf_token,
|
| 561 |
hf_username=hf_username,
|
| 562 |
)
|
| 563 |
+
self._restart_cpu_preload_if_token_recovered(
|
| 564 |
+
existing,
|
| 565 |
+
preload_sandbox=preload_sandbox,
|
| 566 |
+
)
|
| 567 |
return existing
|
| 568 |
return None
|
| 569 |
|
|
|
|
| 581 |
hf_token=hf_token,
|
| 582 |
hf_username=hf_username,
|
| 583 |
)
|
| 584 |
+
self._restart_cpu_preload_if_token_recovered(
|
| 585 |
+
existing,
|
| 586 |
+
preload_sandbox=preload_sandbox,
|
| 587 |
+
)
|
| 588 |
return existing
|
| 589 |
return None
|
| 590 |
|
tests/unit/test_auth_token_propagation.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Tests for authenticated HF token propagation through backend dependencies."""
|
| 2 |
+
|
| 3 |
+
import sys
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
from types import SimpleNamespace
|
| 6 |
+
|
| 7 |
+
import pytest
|
| 8 |
+
|
| 9 |
+
_BACKEND_DIR = Path(__file__).resolve().parent.parent.parent / "backend"
|
| 10 |
+
if str(_BACKEND_DIR) not in sys.path:
|
| 11 |
+
sys.path.insert(0, str(_BACKEND_DIR))
|
| 12 |
+
|
| 13 |
+
import dependencies # noqa: E402
|
| 14 |
+
from routes import auth # noqa: E402
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
@pytest.mark.asyncio
|
| 18 |
+
async def test_current_user_carries_internal_hf_token(monkeypatch):
|
| 19 |
+
monkeypatch.setattr(dependencies, "AUTH_ENABLED", True)
|
| 20 |
+
dependencies._token_cache.clear()
|
| 21 |
+
|
| 22 |
+
async def fake_validate_token(token):
|
| 23 |
+
assert token == "hf-user-token"
|
| 24 |
+
return {"sub": "user-id", "preferred_username": "alice"}
|
| 25 |
+
|
| 26 |
+
async def fake_fetch_user_plan(token):
|
| 27 |
+
assert token == "hf-user-token"
|
| 28 |
+
return "pro"
|
| 29 |
+
|
| 30 |
+
monkeypatch.setattr(dependencies, "_validate_token", fake_validate_token)
|
| 31 |
+
monkeypatch.setattr(dependencies, "_fetch_user_plan", fake_fetch_user_plan)
|
| 32 |
+
|
| 33 |
+
request = SimpleNamespace(
|
| 34 |
+
headers={"Authorization": "Bearer hf-user-token"},
|
| 35 |
+
cookies={},
|
| 36 |
+
)
|
| 37 |
+
|
| 38 |
+
user = await dependencies.get_current_user(request)
|
| 39 |
+
|
| 40 |
+
assert user["user_id"] == "user-id"
|
| 41 |
+
assert user["username"] == "alice"
|
| 42 |
+
assert user["plan"] == "pro"
|
| 43 |
+
assert user[dependencies.INTERNAL_HF_TOKEN_KEY] == "hf-user-token"
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
@pytest.mark.asyncio
|
| 47 |
+
async def test_auth_me_does_not_expose_internal_hf_token():
|
| 48 |
+
user = {
|
| 49 |
+
"user_id": "user-id",
|
| 50 |
+
"username": "alice",
|
| 51 |
+
"authenticated": True,
|
| 52 |
+
dependencies.INTERNAL_HF_TOKEN_KEY: "hf-user-token",
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
response = await auth.get_me(user)
|
| 56 |
+
|
| 57 |
+
assert response == {
|
| 58 |
+
"user_id": "user-id",
|
| 59 |
+
"username": "alice",
|
| 60 |
+
"authenticated": True,
|
| 61 |
+
}
|
tests/unit/test_session_manager_persistence.py
CHANGED
|
@@ -281,6 +281,101 @@ async def test_existing_session_updates_token_after_access_check():
|
|
| 281 |
assert existing.session.hf_token == "new-token"
|
| 282 |
|
| 283 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 284 |
@pytest.mark.asyncio
|
| 285 |
async def test_concurrent_lazy_restore_starts_only_one_agent_task():
|
| 286 |
store = RestoreStore(delay=0.01)
|
|
|
|
| 281 |
assert existing.session.hf_token == "new-token"
|
| 282 |
|
| 283 |
|
| 284 |
+
@pytest.mark.asyncio
|
| 285 |
+
async def test_existing_session_retries_preload_after_token_recovered():
|
| 286 |
+
manager = _manager_with_store(NoopSessionStore())
|
| 287 |
+
existing = _runtime_agent_session("s1", user_id="owner", hf_token=None)
|
| 288 |
+
done_task = asyncio.get_running_loop().create_future()
|
| 289 |
+
done_task.set_result(None)
|
| 290 |
+
existing.session.sandbox_preload_task = done_task
|
| 291 |
+
existing.session.sandbox_preload_error = (
|
| 292 |
+
"No HF token available. Cannot create sandbox."
|
| 293 |
+
)
|
| 294 |
+
manager.sessions["s1"] = existing
|
| 295 |
+
started: list[str] = []
|
| 296 |
+
|
| 297 |
+
def fake_start_cpu_sandbox_preload(agent_session):
|
| 298 |
+
started.append(agent_session.session_id)
|
| 299 |
+
|
| 300 |
+
manager._start_cpu_sandbox_preload = fake_start_cpu_sandbox_preload # type: ignore[method-assign]
|
| 301 |
+
|
| 302 |
+
result = await manager.ensure_session_loaded(
|
| 303 |
+
"s1",
|
| 304 |
+
user_id="owner",
|
| 305 |
+
hf_token="new-token",
|
| 306 |
+
)
|
| 307 |
+
|
| 308 |
+
assert result is existing
|
| 309 |
+
assert existing.hf_token == "new-token"
|
| 310 |
+
assert existing.session.hf_token == "new-token"
|
| 311 |
+
assert existing.session.sandbox_preload_error is None
|
| 312 |
+
assert existing.session.sandbox_preload_task is None
|
| 313 |
+
assert started == ["s1"]
|
| 314 |
+
|
| 315 |
+
|
| 316 |
+
@pytest.mark.asyncio
|
| 317 |
+
async def test_existing_session_does_not_retry_preload_when_disabled():
|
| 318 |
+
manager = _manager_with_store(NoopSessionStore())
|
| 319 |
+
existing = _runtime_agent_session("s1", user_id="owner", hf_token=None)
|
| 320 |
+
done_task = asyncio.get_running_loop().create_future()
|
| 321 |
+
done_task.set_result(None)
|
| 322 |
+
existing.session.sandbox_preload_task = done_task
|
| 323 |
+
existing.session.sandbox_preload_error = (
|
| 324 |
+
"No HF token available. Cannot create sandbox."
|
| 325 |
+
)
|
| 326 |
+
manager.sessions["s1"] = existing
|
| 327 |
+
started: list[str] = []
|
| 328 |
+
|
| 329 |
+
def fake_start_cpu_sandbox_preload(agent_session):
|
| 330 |
+
started.append(agent_session.session_id)
|
| 331 |
+
|
| 332 |
+
manager._start_cpu_sandbox_preload = fake_start_cpu_sandbox_preload # type: ignore[method-assign]
|
| 333 |
+
|
| 334 |
+
result = await manager.ensure_session_loaded(
|
| 335 |
+
"s1",
|
| 336 |
+
user_id="owner",
|
| 337 |
+
hf_token="new-token",
|
| 338 |
+
preload_sandbox=False,
|
| 339 |
+
)
|
| 340 |
+
|
| 341 |
+
assert result is existing
|
| 342 |
+
assert existing.hf_token == "new-token"
|
| 343 |
+
assert existing.session.hf_token == "new-token"
|
| 344 |
+
assert existing.session.sandbox_preload_error == (
|
| 345 |
+
"No HF token available. Cannot create sandbox."
|
| 346 |
+
)
|
| 347 |
+
assert started == []
|
| 348 |
+
|
| 349 |
+
|
| 350 |
+
@pytest.mark.asyncio
|
| 351 |
+
async def test_existing_session_does_not_restart_preload_after_teardown():
|
| 352 |
+
manager = _manager_with_store(NoopSessionStore())
|
| 353 |
+
existing = _runtime_agent_session("s1", user_id="owner", hf_token="token")
|
| 354 |
+
done_task = asyncio.get_running_loop().create_future()
|
| 355 |
+
done_task.set_result(None)
|
| 356 |
+
existing.session.sandbox = None
|
| 357 |
+
existing.session.sandbox_preload_task = done_task
|
| 358 |
+
existing.session.sandbox_preload_error = None
|
| 359 |
+
manager.sessions["s1"] = existing
|
| 360 |
+
started: list[str] = []
|
| 361 |
+
|
| 362 |
+
def fake_start_cpu_sandbox_preload(agent_session):
|
| 363 |
+
started.append(agent_session.session_id)
|
| 364 |
+
|
| 365 |
+
manager._start_cpu_sandbox_preload = fake_start_cpu_sandbox_preload # type: ignore[method-assign]
|
| 366 |
+
|
| 367 |
+
result = await manager.ensure_session_loaded(
|
| 368 |
+
"s1",
|
| 369 |
+
user_id="owner",
|
| 370 |
+
hf_token="token",
|
| 371 |
+
)
|
| 372 |
+
|
| 373 |
+
assert result is existing
|
| 374 |
+
assert existing.session.sandbox_preload_task is done_task
|
| 375 |
+
assert existing.session.sandbox_preload_error is None
|
| 376 |
+
assert started == []
|
| 377 |
+
|
| 378 |
+
|
| 379 |
@pytest.mark.asyncio
|
| 380 |
async def test_concurrent_lazy_restore_starts_only_one_agent_task():
|
| 381 |
store = RestoreStore(delay=0.01)
|