Files
smart-org/apps/api/tests/test_direct_tasks_engine.py
mjalmoudy94 d532929189 feat(integration): prepaid billing, engine UI, credits console, direct tasks, v2 final
- Prepaid credit system: org wallet, debit on usage, chat gate, admin credit console
- Engine UI: /manager, /tasks, /tasks/[id], /workforce, /dashboard, /traces pages
- Direct task creation (HTTP POST /tasks, MCP tasks_create) at pending_manager_review
- Pause/resume/cancel/answer actions in engine API and UI
- Employee cap (7 per org default), review pipeline guard (active reviewer required)
- Scheduler: wide window prevents org starvation; archived employees excluded from cap
- Tenant isolation: project-id cross-org 404, engine never uses another tenant's sandbox
- Order route fix: manager built with pipeline states + employee profiles
- Contract extension: ModelGrantPort.get_org_policy with DefaultOrgPolicy
- Engine event types in @agy/contracts (task.step, task.gate, run.log, run.tool, team.changed)
- api-engine.ts typed client; api-credit.ts client
- Alembic migration c4d5e6f7a8b9: prepaid credit + engine UI tables
- 859 tests passed, 1 skipped; ruff/mypy/tsc/eslint/vitest all green

Refs: PLAN-v2.md §9 (T1–T43), todo-v2-final.md
2026-10-14 23:19:05 +03:30

170 lines
6.8 KiB
Python

"""Direct tasks enter the engine, and the legacy "run" button refuses them (decision D-D).
What these tests defend:
* a task made over HTTP ``POST /tasks`` gets a pipeline state at ``pending_manager_review``
in the organization's project, so the scheduler's manager takes it from there
* ``POST /tasks/{id}/runs`` on such a task is refused with 409: the engine owns it, and
nothing runs outside the engine
* a task made through the MCP gateway (``tasks_create``) gets the same state
* a pre-v2 task with no state still runs through the legacy path
"""
from __future__ import annotations
from httpx import AsyncClient
from app.application.services.mcp_service import McpService, TokenRateLimiter
from app.domain.base import new_id
from app.domain.identity.models import PlatformRole, Role
from app.domain.workforce.pipeline import Stage
from app.infrastructure.db.models import MembershipRow, OrganizationRow, UserRow
from app.infrastructure.repositories.engine import PipelineStateRepository
from app.infrastructure.repositories.workforce import TaskRepository
from app.interfaces.mcp.gateway import McpGateway
from tests.conftest import auth_header, signup
from tests.legacy_tasks import legacy_task
#: Part of the Persian refusal shown when the legacy run button is pressed on an engine task.
ENGINE_OWNS = "موتور خودکار"
def _payload(name: str, slug: str) -> dict:
return {
"org_name": name,
"slug": slug,
"app_name": f"app-{slug}",
"app_type": "web_app",
"language": "node",
"framework": "next",
"git_source": "platform",
}
async def _wizard_org(client: AsyncClient, token: str, name: str, slug: str) -> dict:
"""The wizard's answer: the organization, its project and its team."""
response = await client.post(
"/api/setup/complete", json=_payload(name, slug), headers=auth_header(token)
)
assert response.status_code == 201, response.text
return response.json()
async def _state(session_factory, task_id: str):
async with session_factory() as session:
return await PipelineStateRepository(session).get(task_id)
async def _org_with_owner(session_factory, slug: str) -> tuple[str, str]:
"""An organization and its owner, made directly (the MCP gateway needs no wizard)."""
async with session_factory() as session:
user = UserRow(
id=new_id(),
email=f"{slug}@example.com",
password_hash="x",
name=slug,
platform_role=PlatformRole.MANAGER.value,
)
session.add(user)
await session.flush()
org = OrganizationRow(id=new_id(), name=slug, slug=slug, owner_user_id=user.id)
session.add(org)
await session.flush()
session.add(MembershipRow(organization_id=org.id, user_id=user.id, role=Role.OWNER.value))
await session.commit()
return org.id, user.id
async def _mcp_token(session_factory, organization_id: str, user_id: str) -> str:
async with session_factory() as session:
issued = await McpService(session).issue(
organization_id=organization_id,
name="agent",
scopes=["tasks:write", "tasks:read"],
created_by_user_id=user_id,
)
await session.commit()
return issued.token
async def test_a_task_made_over_http_enters_the_engine(client, session_factory, seeded):
manager = await signup(client, "direct-http@example.com")
body = await _wizard_org(client, manager["access_token"], "سازمان الف", "direct-http")
org_id = body["organization"]["id"]
project_id = body["project"]["id"]
created = await client.post(
f"/api/organizations/{org_id}/tasks",
json={"title": "صفحهٔ ورود"},
headers=auth_header(manager["access_token"]),
)
assert created.status_code == 201, created.text
task_id = created.json()["id"]
state = await _state(session_factory, task_id)
assert state is not None, "a direct task must get a pipeline state"
assert state.organization_id == org_id
assert state.project_id == project_id
assert state.stage is Stage.PENDING_MANAGER_REVIEW
async with session_factory() as session:
task = await TaskRepository(session).get(task_id)
assert task is not None and task.project_id == project_id
async def test_running_a_direct_task_is_refused_with_409(client, session_factory, seeded):
manager = await signup(client, "direct-run@example.com")
headers = auth_header(manager["access_token"])
body = await _wizard_org(client, manager["access_token"], "سازمان ب", "direct-run")
org_id = body["organization"]["id"]
created = await client.post(
f"/api/organizations/{org_id}/tasks", json={"title": "کار مستقیم"}, headers=headers
)
assert created.status_code == 201, created.text
task_id = created.json()["id"]
run = await client.post(
f"/api/organizations/{org_id}/tasks/{task_id}/runs", json={}, headers=headers
)
assert run.status_code == 409, run.text
assert ENGINE_OWNS in run.json()["error"]["message"]
# Nothing ran outside the engine: the task still has no run.
detail = await client.get(f"/api/organizations/{org_id}/tasks/{task_id}", headers=headers)
assert detail.status_code == 200, detail.text
assert detail.json()["runs"] == []
async def test_an_mcp_task_enters_the_engine(session_factory, seeded):
org_id, user_id = await _org_with_owner(session_factory, "direct-mcp")
token = await _mcp_token(session_factory, org_id, user_id)
gateway = McpGateway(
session_factory=session_factory, rate_limiter=TokenRateLimiter(limit_per_minute=None)
)
created = await gateway.call(
tool="tasks_create", arguments={"title": "کار از راه MCP"}, token=token
)
assert created.ok is True, created.error
task_id = created.payload["task"]["id"]
state = await _state(session_factory, task_id)
assert state is not None, "an MCP task must get a pipeline state too"
assert state.organization_id == org_id
assert state.stage is Stage.PENDING_MANAGER_REVIEW
async def test_a_pre_v2_task_still_runs_through_the_legacy_path(client, session_factory, seeded):
manager = await signup(client, "direct-legacy@example.com")
headers = auth_header(manager["access_token"])
body = await _wizard_org(client, manager["access_token"], "سازمان پ", "direct-legacy")
org_id = body["organization"]["id"]
task_id = await legacy_task(session_factory, org_id, manager["user"]["id"], title="کار قدیمی")
assert await _state(session_factory, task_id) is None
run = await client.post(
f"/api/organizations/{org_id}/tasks/{task_id}/runs", json={}, headers=headers
)
assert run.status_code == 201, run.text
assert run.json()["status"] == "completed", run.text