Engine honours org policy and monthly budget, legacy runner skips engine tasks, sandbox session provisioned on demand, project fallback for threads, reviewer fallback, tenant checks on project ids, memory reflection respects grants, expiry worker, MCP pipeline_status, login redirect, sharing settings, e2e smoke. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
135 lines
5.1 KiB
Python
135 lines
5.1 KiB
Python
"""Integration: the engine starts the project's sandbox session when none is active.
|
|
|
|
A fresh organization cannot start a sandbox through the API, because sandbox
|
|
mutations are owner-only. Before this, the engine only reused a running session,
|
|
so every tool turn of a new app failed and the tests gate stayed red. The engine
|
|
now starts the project's single workspace session on demand. The tests use the
|
|
default mock provider and the seeded sandbox templates.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from sqlalchemy import select
|
|
|
|
from app.application.container_engine import _EngineSandboxSessions
|
|
from app.core.errors import ProviderUnavailableError
|
|
from app.domain.base import new_id
|
|
from app.domain.sandbox.models import SessionStatus
|
|
from app.infrastructure.db.models import (
|
|
ErrorEventRow,
|
|
OrganizationRow,
|
|
ProjectRow,
|
|
TaskRow,
|
|
UserRow,
|
|
)
|
|
from app.infrastructure.repositories.sandbox import (
|
|
SandboxSessionRepository,
|
|
WorkspaceRepository,
|
|
)
|
|
from app.infrastructure.repositories.workforce import TaskRepository
|
|
|
|
|
|
async def _seed_task(session_factory) -> str:
|
|
"""One organization with one project and one queued task. Returns the task id."""
|
|
async with session_factory() as session:
|
|
user = UserRow(email=f"sbx-{new_id()[:8]}@example.com", password_hash="x")
|
|
session.add(user)
|
|
await session.flush()
|
|
org = OrganizationRow(
|
|
name="سازمان سندباکس", slug=f"sbx-{user.id[:6]}", owner_user_id=user.id
|
|
)
|
|
session.add(org)
|
|
await session.flush()
|
|
project = ProjectRow(
|
|
id=new_id(), organization_id=org.id, name="app", slug="app", test_command="pytest -q"
|
|
)
|
|
session.add(project)
|
|
await session.flush()
|
|
task = TaskRow(
|
|
organization_id=org.id,
|
|
title="کار سندباکس",
|
|
project_id=project.id,
|
|
created_by_user_id=user.id,
|
|
)
|
|
session.add(task)
|
|
await session.commit()
|
|
return task.id
|
|
|
|
|
|
async def _sandbox_for(session, task_id: str) -> str:
|
|
task = await TaskRepository(session).get(task_id)
|
|
assert task is not None
|
|
return await _EngineSandboxSessions(session).for_task(task)
|
|
|
|
|
|
async def test_a_project_without_a_workspace_gets_one_running_session(session_factory, seeded):
|
|
task_id = await _seed_task(session_factory)
|
|
async with session_factory() as session:
|
|
sandbox_id = await _sandbox_for(session, task_id)
|
|
await session.commit()
|
|
task = await TaskRepository(session).get(task_id)
|
|
assert task is not None and task.project_id is not None
|
|
workspaces = await WorkspaceRepository(session).list_for_project(task.project_id)
|
|
sessions = await SandboxSessionRepository(session).list_for_workspace(workspaces[0].id)
|
|
|
|
assert sandbox_id
|
|
assert len(workspaces) == 1
|
|
assert [s.status for s in sessions] == [SessionStatus.RUNNING]
|
|
assert sessions[0].external_session_id == sandbox_id
|
|
|
|
|
|
async def test_a_second_call_reuses_the_running_session(session_factory, seeded):
|
|
task_id = await _seed_task(session_factory)
|
|
async with session_factory() as session:
|
|
first = await _sandbox_for(session, task_id)
|
|
second = await _sandbox_for(session, task_id)
|
|
await session.commit()
|
|
task = await TaskRepository(session).get(task_id)
|
|
assert task is not None and task.project_id is not None
|
|
workspaces = await WorkspaceRepository(session).list_for_project(task.project_id)
|
|
sessions = await SandboxSessionRepository(session).list_for_workspace(workspaces[0].id)
|
|
|
|
assert first
|
|
assert second == first
|
|
assert len(workspaces) == 1
|
|
assert len(sessions) == 1
|
|
|
|
|
|
async def test_without_a_template_nothing_starts_and_the_reason_is_recorded(
|
|
session_factory, redirect_error_log
|
|
):
|
|
"""No seeded sandbox templates (this test does not use ``seeded``)."""
|
|
task_id = await _seed_task(session_factory)
|
|
async with session_factory() as session:
|
|
sandbox_id = await _sandbox_for(session, task_id)
|
|
await session.commit()
|
|
|
|
async with session_factory() as session:
|
|
rows = (await session.execute(select(ErrorEventRow))).scalars().all()
|
|
|
|
assert sandbox_id == ""
|
|
assert any(row.kind == "engine.sandbox_provision_failed" for row in rows)
|
|
|
|
|
|
async def test_a_provider_failure_is_recorded_not_raised(
|
|
session_factory, seeded, redirect_error_log, monkeypatch
|
|
):
|
|
class _UnavailableService:
|
|
async def create_workspace(self, **_kwargs):
|
|
raise ProviderUnavailableError("ساخت محیط اجرای ایزوله ناموفق بود.")
|
|
|
|
monkeypatch.setattr(
|
|
"app.application.container.build_sandbox_service",
|
|
lambda _session: _UnavailableService(),
|
|
)
|
|
task_id = await _seed_task(session_factory)
|
|
async with session_factory() as session:
|
|
sandbox_id = await _sandbox_for(session, task_id)
|
|
await session.commit()
|
|
|
|
async with session_factory() as session:
|
|
rows = (await session.execute(select(ErrorEventRow))).scalars().all()
|
|
|
|
assert sandbox_id == ""
|
|
assert any(row.kind == "engine.sandbox_provision_failed" for row in rows)
|