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:
co-authored by
Claude Sonnet 5.5
parent
36564e3f2b
commit
fef5a9deab
@@ -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
|
||||
Reference in New Issue
Block a user