add sandbox runtime control endpoints
This commit is contained in:
parent
0ca0bac9bf
commit
1b38bcfeab
17 changed files with 1408 additions and 119 deletions
|
|
@ -27,16 +27,26 @@ from adapter.http.fastapi import app as app_module
|
|||
from adapter.observability.noop import NoopMetrics, NoopTracer
|
||||
from adapter.observability.runtime import ObservabilityRuntime
|
||||
from adapter.sandbox.reconciliation import SandboxSessionReconciler
|
||||
from domain.error import SandboxError, SandboxStartError
|
||||
from domain.sandbox import SandboxSession, SandboxStatus
|
||||
from domain.error import SandboxConflictError, SandboxError, SandboxStartError
|
||||
from domain.sandbox import SandboxEndpoint, SandboxSession, SandboxStatus
|
||||
from repository.sandbox_lock import ProcessLocalSandboxLifecycleLocker
|
||||
from repository.sandbox_session import InMemorySandboxSessionRepository
|
||||
from usecase.interface import Attrs
|
||||
from usecase.sandbox import CleanupExpiredSandboxes, CreateSandbox, CreateSandboxCommand
|
||||
from usecase.sandbox import (
|
||||
CleanupExpiredSandboxes,
|
||||
CreateSandbox,
|
||||
CreateSandboxCommand,
|
||||
DeleteSandbox,
|
||||
DeleteSandboxCommand,
|
||||
DeleteSandboxResult,
|
||||
)
|
||||
|
||||
CHAT_ID = UUID('123e4567-e89b-12d3-a456-426614174000')
|
||||
NON_CANONICAL_CHAT_ID = '123E4567E89B12D3A456426614174000'
|
||||
SESSION_ID = UUID('00000000-0000-0000-0000-000000000011')
|
||||
AGENT_ID = 'agent-alpha'
|
||||
VOLUME_HOST_PATH = '/srv/sandbox/request-volume'
|
||||
ENDPOINT = SandboxEndpoint(ip='172.20.0.8', port=8000)
|
||||
|
||||
|
||||
class FakeLogger:
|
||||
|
|
@ -82,6 +92,18 @@ class FakeCleanupExpiredSandboxes(CleanupExpiredSandboxes):
|
|||
return []
|
||||
|
||||
|
||||
class FakeDeleteSandboxUsecase(DeleteSandbox):
|
||||
def __init__(self, result: DeleteSandboxResult | None = None) -> None:
|
||||
self._result = result
|
||||
self.commands: list[DeleteSandboxCommand] = []
|
||||
|
||||
def execute(self, command: DeleteSandboxCommand) -> DeleteSandboxResult:
|
||||
self.commands.append(command)
|
||||
if self._result is None:
|
||||
return DeleteSandboxResult(chat_id=command.chat_id, result='not_found')
|
||||
return self._result
|
||||
|
||||
|
||||
class FakeDockerClient(DockerClient):
|
||||
def __init__(self, base_url: str | None = None) -> None:
|
||||
self.base_url = base_url
|
||||
|
|
@ -197,10 +219,18 @@ class FakeLifecycleRuntime:
|
|||
*,
|
||||
session_id: UUID,
|
||||
chat_id: UUID,
|
||||
agent_id: str,
|
||||
volume_host_path: str,
|
||||
created_at: datetime,
|
||||
expires_at: datetime,
|
||||
) -> SandboxSession:
|
||||
self.create_calls.append(CreateSandboxCommand(chat_id=chat_id))
|
||||
self.create_calls.append(
|
||||
CreateSandboxCommand(
|
||||
chat_id=chat_id,
|
||||
agent_id=agent_id,
|
||||
volume_host_path=volume_host_path,
|
||||
)
|
||||
)
|
||||
session = SandboxSession(
|
||||
session_id=session_id,
|
||||
chat_id=chat_id,
|
||||
|
|
@ -208,6 +238,9 @@ class FakeLifecycleRuntime:
|
|||
status=SandboxStatus.RUNNING,
|
||||
created_at=created_at,
|
||||
expires_at=expires_at,
|
||||
agent_id=agent_id,
|
||||
volume_host_path=volume_host_path,
|
||||
endpoint=ENDPOINT,
|
||||
)
|
||||
self._sessions = [
|
||||
existing for existing in self._sessions if existing.chat_id != chat_id
|
||||
|
|
@ -218,6 +251,9 @@ class FakeLifecycleRuntime:
|
|||
def stop(self, container_id: str) -> None:
|
||||
self.stop_calls.append(container_id)
|
||||
|
||||
def delete(self, container_id: str) -> None:
|
||||
self.stop_calls.append(container_id)
|
||||
|
||||
|
||||
class FixedSandboxState:
|
||||
def __init__(self, sessions: list[SandboxSession]) -> None:
|
||||
|
|
@ -287,6 +323,8 @@ def build_config() -> AppConfig:
|
|||
docker=DockerConfig(base_url='unix:///var/run/docker.sock'),
|
||||
sandbox=SandboxConfig(
|
||||
image='sandbox:latest',
|
||||
network_name='sandbox',
|
||||
agent_service_port=8000,
|
||||
ttl_seconds=300,
|
||||
cleanup_interval_seconds=60,
|
||||
chats_root='/tmp/chats',
|
||||
|
|
@ -295,6 +333,7 @@ def build_config() -> AppConfig:
|
|||
chat_mount_path='/workspace/chat',
|
||||
dependencies_mount_path='/workspace/dependencies',
|
||||
lambda_tools_mount_path='/workspace/lambda-tools',
|
||||
volume_mount_path='/workspace/volume',
|
||||
),
|
||||
security=SecurityConfig(
|
||||
token_header='Authorization',
|
||||
|
|
@ -310,6 +349,7 @@ def build_container(
|
|||
cleanup_usecase: CleanupExpiredSandboxes,
|
||||
logger: FakeLogger,
|
||||
docker_client: FakeDockerClient,
|
||||
delete_sandbox_usecase: DeleteSandbox | None = None,
|
||||
sandbox_reconciler: SandboxSessionReconciler | None = None,
|
||||
) -> AppContainer:
|
||||
observability = ObservabilityRuntime(
|
||||
|
|
@ -330,6 +370,7 @@ def build_container(
|
|||
usecases = AppUsecases(
|
||||
create_sandbox=create_sandbox_usecase,
|
||||
cleanup_expired_sandboxes=cleanup_usecase,
|
||||
delete_sandbox=delete_sandbox_usecase or FakeDeleteSandboxUsecase(),
|
||||
)
|
||||
return AppContainer(
|
||||
config=config,
|
||||
|
|
@ -419,6 +460,10 @@ async def get_json(app: FastAPI, path: str) -> tuple[int, dict[str, object]]:
|
|||
return await request_json(app, 'GET', path)
|
||||
|
||||
|
||||
async def delete_json(app: FastAPI, path: str) -> tuple[int, dict[str, object]]:
|
||||
return await request_json(app, 'DELETE', path)
|
||||
|
||||
|
||||
async def exercise_create_request(
|
||||
app: FastAPI,
|
||||
payload: dict[str, str],
|
||||
|
|
@ -445,6 +490,19 @@ async def exercise_get_request(
|
|||
await app.router.shutdown()
|
||||
|
||||
|
||||
async def exercise_delete_request(
|
||||
app: FastAPI,
|
||||
path: str,
|
||||
) -> tuple[int, dict[str, object]]:
|
||||
await app.router.startup()
|
||||
try:
|
||||
status, response = await delete_json(app, path)
|
||||
await asyncio.sleep(0)
|
||||
return status, response
|
||||
finally:
|
||||
await app.router.shutdown()
|
||||
|
||||
|
||||
def test_post_create_returns_session_with_canonical_chat_id(monkeypatch) -> None:
|
||||
config = build_config()
|
||||
expires_at = datetime(2026, 4, 2, 12, 5, tzinfo=UTC)
|
||||
|
|
@ -455,6 +513,9 @@ def test_post_create_returns_session_with_canonical_chat_id(monkeypatch) -> None
|
|||
status=SandboxStatus.RUNNING,
|
||||
created_at=expires_at - timedelta(minutes=5),
|
||||
expires_at=expires_at,
|
||||
agent_id=AGENT_ID,
|
||||
volume_host_path=VOLUME_HOST_PATH,
|
||||
endpoint=ENDPOINT,
|
||||
)
|
||||
logger = FakeLogger()
|
||||
create_usecase = FakeCreateSandboxUsecase(session=session)
|
||||
|
|
@ -475,19 +536,31 @@ def test_post_create_returns_session_with_canonical_chat_id(monkeypatch) -> None
|
|||
app = app_module.create_app(config=config)
|
||||
|
||||
status_code, response = asyncio.run(
|
||||
exercise_create_request(app, {'chat_id': NON_CANONICAL_CHAT_ID})
|
||||
exercise_create_request(
|
||||
app,
|
||||
{
|
||||
'chat_id': NON_CANONICAL_CHAT_ID,
|
||||
'agent_id': AGENT_ID,
|
||||
'volume_host_path': VOLUME_HOST_PATH,
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
assert status_code == 200
|
||||
assert response == {
|
||||
'session_id': str(SESSION_ID),
|
||||
'chat_id': str(CHAT_ID),
|
||||
'agent_id': AGENT_ID,
|
||||
'volume_host_path': VOLUME_HOST_PATH,
|
||||
'container_id': 'container-123',
|
||||
'endpoint': {'ip': '172.20.0.8', 'port': 8000},
|
||||
'status': 'running',
|
||||
'expires_at': '2026-04-02T12:05:00Z',
|
||||
}
|
||||
assert len(create_usecase.commands) == 1
|
||||
assert create_usecase.commands[0].chat_id == CHAT_ID
|
||||
assert create_usecase.commands[0].agent_id == AGENT_ID
|
||||
assert create_usecase.commands[0].volume_host_path == VOLUME_HOST_PATH
|
||||
assert cleanup_usecase.calls >= 1
|
||||
assert any(
|
||||
message == 'http_request'
|
||||
|
|
@ -498,6 +571,55 @@ def test_post_create_returns_session_with_canonical_chat_id(monkeypatch) -> None
|
|||
assert docker_client.close_calls == 1
|
||||
|
||||
|
||||
def test_post_create_canonicalizes_volume_path_before_usecase(monkeypatch) -> None:
|
||||
config = build_config()
|
||||
expires_at = datetime(2026, 4, 2, 12, 5, tzinfo=UTC)
|
||||
session = SandboxSession(
|
||||
session_id=SESSION_ID,
|
||||
chat_id=CHAT_ID,
|
||||
container_id='container-123',
|
||||
status=SandboxStatus.RUNNING,
|
||||
created_at=expires_at - timedelta(minutes=5),
|
||||
expires_at=expires_at,
|
||||
agent_id=AGENT_ID,
|
||||
volume_host_path=VOLUME_HOST_PATH,
|
||||
endpoint=ENDPOINT,
|
||||
)
|
||||
logger = FakeLogger()
|
||||
create_usecase = FakeCreateSandboxUsecase(session=session)
|
||||
cleanup_usecase = FakeCleanupExpiredSandboxes()
|
||||
docker_client = FakeDockerClient()
|
||||
container = build_container(
|
||||
config,
|
||||
create_usecase,
|
||||
cleanup_usecase,
|
||||
logger,
|
||||
docker_client,
|
||||
)
|
||||
monkeypatch.setattr(app_module, 'build_container', lambda **kwargs: container)
|
||||
monkeypatch.setattr(
|
||||
app_module.FastAPIInstrumentor, 'instrument_app', lambda *args, **kwargs: None
|
||||
)
|
||||
app = app_module.create_app(config=config)
|
||||
|
||||
status_code, response = asyncio.run(
|
||||
exercise_create_request(
|
||||
app,
|
||||
{
|
||||
'chat_id': str(CHAT_ID),
|
||||
'agent_id': AGENT_ID,
|
||||
'volume_host_path': '/srv/sandbox/a/../request-volume',
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
assert status_code == 200
|
||||
assert response['volume_host_path'] == VOLUME_HOST_PATH
|
||||
assert len(create_usecase.commands) == 1
|
||||
assert create_usecase.commands[0].volume_host_path == VOLUME_HOST_PATH
|
||||
assert docker_client.close_calls == 1
|
||||
|
||||
|
||||
def test_post_create_rejects_non_uuid_chat_id(monkeypatch) -> None:
|
||||
config = build_config()
|
||||
expires_at = datetime(2026, 4, 2, 12, 5, tzinfo=UTC)
|
||||
|
|
@ -528,7 +650,14 @@ def test_post_create_rejects_non_uuid_chat_id(monkeypatch) -> None:
|
|||
app = app_module.create_app(config=config)
|
||||
|
||||
status_code, response = asyncio.run(
|
||||
exercise_create_request(app, {'chat_id': 'x/../y'})
|
||||
exercise_create_request(
|
||||
app,
|
||||
{
|
||||
'chat_id': 'x/../y',
|
||||
'agent_id': AGENT_ID,
|
||||
'volume_host_path': VOLUME_HOST_PATH,
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
assert status_code == 422
|
||||
|
|
@ -537,6 +666,94 @@ def test_post_create_rejects_non_uuid_chat_id(monkeypatch) -> None:
|
|||
assert docker_client.close_calls == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'payload',
|
||||
[
|
||||
{'chat_id': str(CHAT_ID), 'volume_host_path': VOLUME_HOST_PATH},
|
||||
{'chat_id': str(CHAT_ID), 'agent_id': AGENT_ID, 'volume_host_path': 'relative'},
|
||||
],
|
||||
)
|
||||
def test_post_create_rejects_missing_or_invalid_runtime_params(
|
||||
monkeypatch,
|
||||
payload: dict[str, str],
|
||||
) -> None:
|
||||
config = build_config()
|
||||
logger = FakeLogger()
|
||||
create_usecase = FakeCreateSandboxUsecase(
|
||||
session=SandboxSession(
|
||||
session_id=SESSION_ID,
|
||||
chat_id=CHAT_ID,
|
||||
container_id='container-123',
|
||||
status=SandboxStatus.RUNNING,
|
||||
created_at=datetime(2026, 4, 2, 12, 0, tzinfo=UTC),
|
||||
expires_at=datetime(2026, 4, 2, 12, 5, tzinfo=UTC),
|
||||
agent_id=AGENT_ID,
|
||||
volume_host_path=VOLUME_HOST_PATH,
|
||||
endpoint=ENDPOINT,
|
||||
)
|
||||
)
|
||||
cleanup_usecase = FakeCleanupExpiredSandboxes()
|
||||
docker_client = FakeDockerClient()
|
||||
container = build_container(
|
||||
config,
|
||||
create_usecase,
|
||||
cleanup_usecase,
|
||||
logger,
|
||||
docker_client,
|
||||
)
|
||||
monkeypatch.setattr(app_module, 'build_container', lambda **kwargs: container)
|
||||
monkeypatch.setattr(
|
||||
app_module.FastAPIInstrumentor, 'instrument_app', lambda *args, **kwargs: None
|
||||
)
|
||||
|
||||
app = app_module.create_app(config=config)
|
||||
|
||||
status_code, response = asyncio.run(exercise_create_request(app, payload))
|
||||
|
||||
assert status_code == 422
|
||||
assert 'detail' in response
|
||||
assert create_usecase.commands == []
|
||||
assert docker_client.close_calls == 1
|
||||
|
||||
|
||||
def test_post_create_maps_conflict_to_conflict_response(monkeypatch) -> None:
|
||||
config = build_config()
|
||||
logger = FakeLogger()
|
||||
create_usecase = FakeCreateSandboxUsecase(
|
||||
error=SandboxConflictError(str(CHAT_ID))
|
||||
)
|
||||
cleanup_usecase = FakeCleanupExpiredSandboxes()
|
||||
docker_client = FakeDockerClient()
|
||||
container = build_container(
|
||||
config,
|
||||
create_usecase,
|
||||
cleanup_usecase,
|
||||
logger,
|
||||
docker_client,
|
||||
)
|
||||
monkeypatch.setattr(app_module, 'build_container', lambda **kwargs: container)
|
||||
monkeypatch.setattr(
|
||||
app_module.FastAPIInstrumentor, 'instrument_app', lambda *args, **kwargs: None
|
||||
)
|
||||
|
||||
app = app_module.create_app(config=config)
|
||||
|
||||
status_code, response = asyncio.run(
|
||||
exercise_create_request(
|
||||
app,
|
||||
{
|
||||
'chat_id': str(CHAT_ID),
|
||||
'agent_id': AGENT_ID,
|
||||
'volume_host_path': VOLUME_HOST_PATH,
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
assert status_code == 409
|
||||
assert response == {'detail': 'sandbox_conflict'}
|
||||
assert docker_client.close_calls == 1
|
||||
|
||||
|
||||
def test_post_create_maps_start_errors_to_service_unavailable(monkeypatch) -> None:
|
||||
config = build_config()
|
||||
logger = FakeLogger()
|
||||
|
|
@ -558,7 +775,14 @@ def test_post_create_maps_start_errors_to_service_unavailable(monkeypatch) -> No
|
|||
app = app_module.create_app(config=config)
|
||||
|
||||
status_code, response = asyncio.run(
|
||||
exercise_create_request(app, {'chat_id': str(CHAT_ID)})
|
||||
exercise_create_request(
|
||||
app,
|
||||
{
|
||||
'chat_id': str(CHAT_ID),
|
||||
'agent_id': AGENT_ID,
|
||||
'volume_host_path': VOLUME_HOST_PATH,
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
assert status_code == 503
|
||||
|
|
@ -587,7 +811,14 @@ def test_post_create_maps_generic_sandbox_errors_to_internal_error(monkeypatch)
|
|||
app = app_module.create_app(config=config)
|
||||
|
||||
status_code, response = asyncio.run(
|
||||
exercise_create_request(app, {'chat_id': str(CHAT_ID)})
|
||||
exercise_create_request(
|
||||
app,
|
||||
{
|
||||
'chat_id': str(CHAT_ID),
|
||||
'agent_id': AGENT_ID,
|
||||
'volume_host_path': VOLUME_HOST_PATH,
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
assert status_code == 500
|
||||
|
|
@ -595,6 +826,89 @@ def test_post_create_maps_generic_sandbox_errors_to_internal_error(monkeypatch)
|
|||
assert docker_client.close_calls == 1
|
||||
|
||||
|
||||
def test_delete_sandbox_endpoint_returns_deleted(monkeypatch) -> None:
|
||||
config = build_config()
|
||||
logger = FakeLogger()
|
||||
create_usecase = FakeCreateSandboxUsecase(error=AssertionError('unused'))
|
||||
cleanup_usecase = FakeCleanupExpiredSandboxes()
|
||||
delete_usecase = FakeDeleteSandboxUsecase(
|
||||
DeleteSandboxResult(
|
||||
chat_id=CHAT_ID,
|
||||
result='deleted',
|
||||
session_id=SESSION_ID,
|
||||
container_id='container-123',
|
||||
)
|
||||
)
|
||||
docker_client = FakeDockerClient()
|
||||
container = build_container(
|
||||
config,
|
||||
create_usecase,
|
||||
cleanup_usecase,
|
||||
logger,
|
||||
docker_client,
|
||||
delete_sandbox_usecase=delete_usecase,
|
||||
)
|
||||
monkeypatch.setattr(app_module, 'build_container', lambda **kwargs: container)
|
||||
monkeypatch.setattr(
|
||||
app_module.FastAPIInstrumentor, 'instrument_app', lambda *args, **kwargs: None
|
||||
)
|
||||
|
||||
app = app_module.create_app(config=config)
|
||||
|
||||
status_code, response = asyncio.run(
|
||||
exercise_delete_request(app, f'/api/v1/sandboxes/{CHAT_ID}')
|
||||
)
|
||||
|
||||
assert status_code == 200
|
||||
assert response == {
|
||||
'chat_id': str(CHAT_ID),
|
||||
'result': 'deleted',
|
||||
'session_id': str(SESSION_ID),
|
||||
'container_id': 'container-123',
|
||||
}
|
||||
assert delete_usecase.commands == [DeleteSandboxCommand(chat_id=CHAT_ID)]
|
||||
assert docker_client.close_calls == 1
|
||||
|
||||
|
||||
def test_delete_sandbox_endpoint_returns_not_found(monkeypatch) -> None:
|
||||
config = build_config()
|
||||
logger = FakeLogger()
|
||||
create_usecase = FakeCreateSandboxUsecase(error=AssertionError('unused'))
|
||||
cleanup_usecase = FakeCleanupExpiredSandboxes()
|
||||
delete_usecase = FakeDeleteSandboxUsecase(
|
||||
DeleteSandboxResult(chat_id=CHAT_ID, result='not_found')
|
||||
)
|
||||
docker_client = FakeDockerClient()
|
||||
container = build_container(
|
||||
config,
|
||||
create_usecase,
|
||||
cleanup_usecase,
|
||||
logger,
|
||||
docker_client,
|
||||
delete_sandbox_usecase=delete_usecase,
|
||||
)
|
||||
monkeypatch.setattr(app_module, 'build_container', lambda **kwargs: container)
|
||||
monkeypatch.setattr(
|
||||
app_module.FastAPIInstrumentor, 'instrument_app', lambda *args, **kwargs: None
|
||||
)
|
||||
|
||||
app = app_module.create_app(config=config)
|
||||
|
||||
status_code, response = asyncio.run(
|
||||
exercise_delete_request(app, f'/api/v1/sandboxes/{CHAT_ID}')
|
||||
)
|
||||
|
||||
assert status_code == 200
|
||||
assert response == {
|
||||
'chat_id': str(CHAT_ID),
|
||||
'result': 'not_found',
|
||||
'session_id': None,
|
||||
'container_id': None,
|
||||
}
|
||||
assert delete_usecase.commands == [DeleteSandboxCommand(chat_id=CHAT_ID)]
|
||||
assert docker_client.close_calls == 1
|
||||
|
||||
|
||||
def test_startup_reconciliation_reuses_existing_container_after_restart(
|
||||
monkeypatch,
|
||||
) -> None:
|
||||
|
|
@ -607,6 +921,9 @@ def test_startup_reconciliation_reuses_existing_container_after_restart(
|
|||
status=SandboxStatus.RUNNING,
|
||||
created_at=created_at,
|
||||
expires_at=created_at + timedelta(minutes=5),
|
||||
agent_id=AGENT_ID,
|
||||
volume_host_path=VOLUME_HOST_PATH,
|
||||
endpoint=ENDPOINT,
|
||||
)
|
||||
logger = FakeLogger()
|
||||
docker_client = FakeDockerClient()
|
||||
|
|
@ -618,6 +935,7 @@ def test_startup_reconciliation_reuses_existing_container_after_restart(
|
|||
tracer=NoopTracer(),
|
||||
)
|
||||
repositories = AppRepositories(sandbox_session=repository)
|
||||
locker = ProcessLocalSandboxLifecycleLocker()
|
||||
reconciler = SandboxSessionReconciler(
|
||||
state_source=runtime,
|
||||
registry=repository,
|
||||
|
|
@ -628,7 +946,7 @@ def test_startup_reconciliation_reuses_existing_container_after_restart(
|
|||
usecases = AppUsecases(
|
||||
create_sandbox=CreateSandbox(
|
||||
repository=repository,
|
||||
locker=ProcessLocalSandboxLifecycleLocker(),
|
||||
locker=locker,
|
||||
runtime=runtime,
|
||||
clock=FakeClock(created_at),
|
||||
logger=logger,
|
||||
|
|
@ -638,13 +956,21 @@ def test_startup_reconciliation_reuses_existing_container_after_restart(
|
|||
),
|
||||
cleanup_expired_sandboxes=CleanupExpiredSandboxes(
|
||||
repository=repository,
|
||||
locker=ProcessLocalSandboxLifecycleLocker(),
|
||||
locker=locker,
|
||||
runtime=runtime,
|
||||
clock=FakeClock(created_at),
|
||||
logger=logger,
|
||||
metrics=NoopMetrics(),
|
||||
tracer=NoopTracer(),
|
||||
),
|
||||
delete_sandbox=DeleteSandbox(
|
||||
repository=repository,
|
||||
locker=locker,
|
||||
runtime=runtime,
|
||||
logger=logger,
|
||||
metrics=NoopMetrics(),
|
||||
tracer=NoopTracer(),
|
||||
),
|
||||
)
|
||||
container = AppContainer(
|
||||
config=config,
|
||||
|
|
@ -662,14 +988,24 @@ def test_startup_reconciliation_reuses_existing_container_after_restart(
|
|||
app = app_module.create_app(config=config)
|
||||
|
||||
status_code, response = asyncio.run(
|
||||
exercise_create_request(app, {'chat_id': str(CHAT_ID)})
|
||||
exercise_create_request(
|
||||
app,
|
||||
{
|
||||
'chat_id': str(CHAT_ID),
|
||||
'agent_id': AGENT_ID,
|
||||
'volume_host_path': VOLUME_HOST_PATH,
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
assert status_code == 200
|
||||
assert response == {
|
||||
'session_id': str(SESSION_ID),
|
||||
'chat_id': str(CHAT_ID),
|
||||
'agent_id': AGENT_ID,
|
||||
'volume_host_path': VOLUME_HOST_PATH,
|
||||
'container_id': 'container-123',
|
||||
'endpoint': {'ip': '172.20.0.8', 'port': 8000},
|
||||
'status': 'running',
|
||||
'expires_at': '2026-04-02T12:05:00Z',
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue