"""End-to-end smoke for the v2 seams: wizard, team, manager order, engine task, push, run. Only the model is fake: ``tests.engine_fakes.ScriptedModel`` answers every call, reached through ``app.application.container.get_model_provider``. Git is the default fake workspace (``GIT_WORKSPACE=fake``, the ``container_delivery._fake_workspace`` singleton), and the sandbox and domain router are the mock defaults. Everything else is the real code path: the owner grants the default models over HTTP, the manager completes the wizard over HTTP (which runs the real bootstrap and delivery hooks), the manager opens a thread over HTTP without a project as the web UI does, the order goes through the real ``ManagerTurns.chat``, and every pass is one real ``AiRuntimeWorker.drain()``. What it proves is that the seams line up. A fresh organization gets its team, its demo host and its granted models. A thread made without a project still yields a task in the organization's project, with a pipeline state. The engine starts the project's sandbox session on its own. The task walks to ``COMPLETED``, and the merge queues a ``dev`` pipeline run that names the pushed commit. No network, docker or real git. """ from __future__ import annotations from sqlalchemy import select from app.application.container_delivery import _fake_workspace from app.application.container_engine import build_scheduler from app.application.ports.repo_workspace import DiffResult from app.core.config import settings from app.domain.workforce.models import RoleKey, TaskStatus from app.domain.workforce.pipeline import Stage from app.infrastructure.db.models import ( AiModelRow, OrganizationRow, OrgModelGrantRow, SandboxSessionRow, WorkspaceRow, ) from app.infrastructure.db.models_delivery import AppRuntimeRow from app.infrastructure.repositories.delivery import PipelineRunRepository from app.infrastructure.repositories.engine import PipelineStateRepository from app.infrastructure.repositories.workforce import EmployeeRepository, TaskRepository from app.workers.ai_runtime import AiRuntimeWorker from tests.billing_helpers import fund_org from tests.conftest import auth_header from tests.engine_fakes import ScriptedModel, ScriptedReply from tests.test_org_wizard import _complete, _manager DEFAULTS = "/api/admin/default-model-grants" #: The manager's chat reply: one task tag, so the order becomes one task. ORDER_REPLY = ( "باشه، یک کار می‌سازم.\n" '[[CREATE_TASK title="صفحهٔ ورود" kind="feature"]]' "ساخت صفحهٔ ورود با اعتبارسنجی[[/CREATE_TASK]]" ) #: The replies that drive one task from triage to merge, as in test_engine_flow. The #: pipeline is ``plan_code_test``: the default for a feature, and the one the ASSIGN reply #: names. It has no review stage, so no review replies are needed. PIPELINE_REPLIES = [ '[[ASSIGN employee="" pipeline="plan_code_test"/]]', "[[PLAN_SPEC]]پلان کار[[/PLAN_SPEC]]", "[[WORK_REPORT]]پیاده‌سازی شد[[/WORK_REPORT]]", "[[TEST_PASS]]سبز است[[/TEST_PASS]]", "[[COMPLETE/]]", ] async def _sandboxes_of(session_factory, organization_id: str, project_id: str) -> list: """The sandbox sessions of one organization's project.""" async with session_factory() as session: stmt = ( select(SandboxSessionRow) .join(WorkspaceRow, SandboxSessionRow.workspace_id == WorkspaceRow.id) .where( WorkspaceRow.organization_id == organization_id, WorkspaceRow.project_id == project_id, ) ) return list((await session.execute(stmt)).scalars().all()) async def test_a_manager_order_ships_through_the_engine( client, session_factory, seeded, platform_owner_token, monkeypatch, redirect_error_log ): scripted = ScriptedModel( [ScriptedReply(content=ORDER_REPLY), *(ScriptedReply(content=r) for r in PIPELINE_REPLIES)] ) monkeypatch.setattr("app.application.container.get_model_provider", lambda: scripted) # The platform owner grants one reasoning model and one cheap model to the default set. async with session_factory() as session: 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) cheap = next(m for m in models if "reasoning" not in m.capabilities) owner = auth_header(platform_owner_token) for model in (strong, cheap): granted = await client.put( f"{DEFAULTS}/{model.id}", json={"is_active": True}, headers=owner ) assert granted.status_code == 200, granted.text # The manager completes the wizard over HTTP: org, project, team, runtime, grants. _, headers = await _manager(client) body = await _complete(client, headers) org_id = body["organization"]["id"] slug = body["organization"]["slug"] project_id = body["project"]["id"] # prepaid: the owner credits a new org before work await fund_org(session_factory, org_id, 1000.0) async with session_factory() as session: roles = {e.role_key for e in await EmployeeRepository(session).list_for_org(org_id)} runtime = await session.get(AppRuntimeRow, project_id) grants = ( await session.execute( select(OrgModelGrantRow).where(OrgModelGrantRow.organization_id == org_id) ) ).scalars().all() owner_user_id = (await session.get(OrganizationRow, org_id)).owner_user_id assert {RoleKey.MANAGER, RoleKey.PLANNER, RoleKey.DEVELOPER, RoleKey.TESTER} <= roles assert runtime is not None assert runtime.demo_host == f"{slug}.{settings.preview_root_domain}" assert grants # The manager opens a thread the way the web UI does: no project_id. created = await client.post( f"/api/organizations/{org_id}/threads", json={"title": "گفتگو"}, headers=headers ) assert created.status_code == 201, created.text assert created.json()["project_id"] is None thread_id = created.json()["id"] # A manager order in that thread becomes one task with a pipeline state. The order goes to # the engine's manager directly, only to read the reply and the states in this session: the # HTTP message route builds its manager with the pipeline states too # (test_integration_order_route.py). async with session_factory() as session: await build_scheduler(session).manager.chat( organization_id=org_id, thread_id=thread_id, content="یک صفحهٔ ورود بساز.", user_id=owner_user_id, ) await session.commit() tasks = await TaskRepository(session).list_for_org(org_id) states = await PipelineStateRepository(session).list_for_org(org_id) assert len(tasks) == 1 assert len(states) == 1 assert tasks[0].project_id == project_id assert states[0].project_id == project_id task_id = tasks[0].id # Nothing has started the project's sandbox yet; the engine must do it. assert await _sandboxes_of(session_factory, org_id, project_id) == [] # The engine walks the task through its stages. Each pass is one real worker drain (the # legacy drain, then one Scheduler.tick()); the merge pushes to dev. _fake_workspace.diffs[task_id] = DiffResult( changed_files=["app/main.py"], text="--- a\n+++ b\n@@\n+x\n" ) state = None for _ in range(10): await AiRuntimeWorker().drain() async with session_factory() as session: state = await PipelineStateRepository(session).get(task_id) if state is not None and state.stage is Stage.COMPLETED: break async with session_factory() as session: task = await TaskRepository(session).get(task_id) assert state is not None and state.stage is Stage.COMPLETED assert task is not None and task.status is TaskStatus.DONE # The engine started the project's sandbox session on its own; no API call did. assert await _sandboxes_of(session_factory, org_id, project_id) pushed_sha = _fake_workspace.heads[(project_id, "dev")] async with session_factory() as session: runs = await PipelineRunRepository(session).list_for_project( organization_id=org_id, project_id=project_id ) assert any( run.trigger == "engine" and run.branch == "dev" and run.sha == pushed_sha for run in runs )