feat(integration): wire v2 workstreams together (engine policy/budget, sandbox, tenant checks, order route, docs)

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>
This commit is contained in:
mjalmoudy94
2026-10-14 20:57:56 +03:30
co-authored by Claude Sonnet 5.5
parent 36564e3f2b
commit fef5a9deab
43 changed files with 3024 additions and 221 deletions
@@ -0,0 +1,233 @@
"""A review pipeline needs an active reviewer, or the task waits forever in a review stage.
``resolve_pipeline`` already falls back from ``plan_code_review_test`` to ``plan_code_test``
when it is told there is no active reviewer. The manager used to leave that flag at its
default, so a refactor in an organization without a reviewer went to a stage nobody could
act on. The manager now looks the reviewer up in the organization, for the chat order and
for triage. The bootstrap team still has no reviewer: a fresh organization gets the plain
pipeline, and only a hired, active reviewer gets the review one.
"""
from __future__ import annotations
import pytest_asyncio
from sqlalchemy import select
from app.application.container_org import build_memory
from app.application.engine.manager import ManagerTurns
from app.application.engine.runner import TurnRunner
from app.core.config import settings
from app.domain.base import new_id
from app.domain.workforce.models import Employee, EmployeeStatus, RoleKey
from app.domain.workforce.pipeline import PipelineKey, Stage, TaskKind, TaskPipelineState
from app.infrastructure.db.models import (
AiModelRow,
OrganizationRow,
RunRow,
TaskRow,
ThreadRow,
UserRow,
)
from app.infrastructure.repositories.ai_control import (
AiModelRepository,
ModelCredentialRepository,
RateCardRepository,
)
from app.infrastructure.repositories.audit import UsageLedgerRepository
from app.infrastructure.repositories.engine import (
EmployeeProfileRepository,
PipelineStateRepository,
)
from app.infrastructure.repositories.workforce import (
EmployeeRepository,
MessageRepository,
RunRepository,
RunStepRepository,
TaskRepository,
ThreadRepository,
ToolCallRepository,
)
from tests.engine_fakes import FakeGrants, ScriptedModel, ScriptedReply
#: One refactor, which the task-kind default sends to the review pipeline.
REFACTOR_ORDER = (
'[[CREATE_TASK title="بازنویسی ورود" kind="refactor"]]بازنویسی ماژول ورود[[/CREATE_TASK]]'
)
class _Org:
def __init__(self, **kwargs) -> None:
self.__dict__.update(kwargs)
@pytest_asyncio.fixture
async def org(session_factory, seeded):
"""An organization and its owner, with one reasoning and one cheap catalog model."""
async with session_factory() as session:
user = UserRow(email=f"rev-{new_id()[:8]}@example.com", password_hash="x")
session.add(user)
await session.flush()
organization = OrganizationRow(
name="سازمان بازبین", slug=f"rev-{user.id[:6]}", owner_user_id=user.id
)
session.add(organization)
await session.commit()
models = list(
(await session.execute(select(AiModelRow).order_by(AiModelRow.internal_name)))
.scalars()
.all()
)
strong = next(m for m in models if "reasoning" in m.capabilities)
fast = next(m for m in models if "reasoning" not in m.capabilities)
yield _Org(
id=organization.id,
user_id=user.id,
strong_id=strong.id,
fast_id=fast.id,
)
def _grants(org: _Org) -> FakeGrants:
return FakeGrants({org.strong_id: "strong", org.fast_id: "fast"})
def _manager(session, *, scripted, grants) -> ManagerTurns:
"""A manager service with real repositories and a scripted model."""
return ManagerTurns(
provider=scripted,
employees=EmployeeRepository(session),
messages=MessageRepository(session),
tasks=TaskRepository(session),
threads=ThreadRepository(session),
models=AiModelRepository(session),
credentials=ModelCredentialRepository(session),
rate_cards=RateCardRepository(session),
ledger=UsageLedgerRepository(session),
grants=grants,
memory=build_memory(session),
profiles=EmployeeProfileRepository(session),
states=PipelineStateRepository(session),
runner=None,
)
async def _order_pipeline(
org: _Org, session_factory, *, reviewer_status: EmployeeStatus | None
) -> PipelineKey:
"""Bootstrap the team, optionally hire a reviewer, then send one refactor order."""
scripted = ScriptedModel([ScriptedReply(content=REFACTOR_ORDER)])
async with session_factory() as session:
manager = _manager(session, scripted=scripted, grants=_grants(org))
await manager.bootstrap_team(organization_id=org.id, project_id=None)
if reviewer_status is not None:
await EmployeeRepository(session).create(
Employee(
organization_id=org.id,
name="رضا",
role_key=RoleKey.REVIEWER,
status=reviewer_status,
)
)
thread = await ThreadRepository(session).create(
ThreadRow(organization_id=org.id, title="گفتگو")
)
await session.commit()
await manager.chat(
organization_id=org.id,
thread_id=thread.id,
content="این ماژول را بازنویسی کن.",
user_id=org.user_id,
)
await session.commit()
task = (await TaskRepository(session).list_for_org(org.id))[0]
state = await PipelineStateRepository(session).get(task.id)
assert state is not None
assert state.kind is TaskKind.REFACTOR
return state.pipeline
async def test_a_refactor_order_without_a_reviewer_skips_the_review_stage(org, session_factory):
pipeline = await _order_pipeline(org, session_factory, reviewer_status=None)
assert pipeline is PipelineKey.PLAN_CODE_TEST
async def test_a_refactor_order_with_an_active_reviewer_gets_the_review_stage(
org, session_factory
):
pipeline = await _order_pipeline(
org, session_factory, reviewer_status=EmployeeStatus.ACTIVE
)
assert pipeline is PipelineKey.PLAN_CODE_REVIEW_TEST
async def test_a_paused_reviewer_does_not_count(org, session_factory):
pipeline = await _order_pipeline(
org, session_factory, reviewer_status=EmployeeStatus.PAUSED
)
assert pipeline is PipelineKey.PLAN_CODE_TEST
async def _refactor_task(session, organization_id: str) -> tuple[str, str]:
task = TaskRow(organization_id=organization_id, title="بازنویسی")
session.add(task)
await session.flush()
run = RunRow(task_id=task.id)
session.add(run)
await session.flush()
# The state starts on the review pipeline, as a refactor order would leave it, so the
# assertion below can only pass if triage moved it back to the plain pipeline.
await PipelineStateRepository(session).create(
TaskPipelineState(
task_id=task.id,
organization_id=organization_id,
pipeline=PipelineKey.PLAN_CODE_REVIEW_TEST,
kind=TaskKind.REFACTOR,
stage=Stage.PENDING_MANAGER_REVIEW,
)
)
await session.flush()
return task.id, run.id
async def test_triage_of_a_refactor_without_a_reviewer_skips_the_review_stage(
org, session_factory, monkeypatch
):
monkeypatch.setattr(settings, "engine_max_tool_calls_per_turn", 40)
scripted = ScriptedModel([ScriptedReply(content='[[ASSIGN employee=""/]]')])
grants = _grants(org)
async with session_factory() as session:
manager = _manager(session, scripted=scripted, grants=grants)
await manager.bootstrap_team(organization_id=org.id, project_id=None)
manager_row = next(
e
for e in await EmployeeRepository(session).list_for_org(org.id)
if e.role_key is RoleKey.MANAGER
)
task_id, run_id = await _refactor_task(session, org.id)
await session.commit()
manager.runner = TurnRunner(
provider=scripted,
models=AiModelRepository(session),
credentials=ModelCredentialRepository(session),
rate_cards=RateCardRepository(session),
ledger=UsageLedgerRepository(session),
grants=grants,
memory=build_memory(session),
turns=None,
run_steps=RunStepRepository(session),
tool_calls=ToolCallRepository(session),
runs=RunRepository(session),
states=PipelineStateRepository(session),
)
task = await TaskRepository(session).get(task_id)
outcome = await manager.triage(task=task, run_id=run_id, employee=manager_row)
await session.commit()
state = await PipelineStateRepository(session).get(task_id)
assert outcome.acted is True
assert outcome.pipeline is PipelineKey.PLAN_CODE_TEST
assert state is not None
assert state.pipeline is PipelineKey.PLAN_CODE_TEST