- 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
95 lines
4.2 KiB
Python
95 lines
4.2 KiB
Python
"""A manager order from a thread without a project still gets the organization's project.
|
|
|
|
The web UI creates the manager thread without ``project_id`` (``manager/page.tsx``). Before
|
|
this fix such an order made a task with no project, so the engine gave it no worktree and
|
|
no sandbox. ``ManagerTurns`` now falls back to the organization's project, for the task and
|
|
for its pipeline state. This test drives the path: the owner grants the default models, the
|
|
manager completes the wizard over HTTP, a thread is created over HTTP without ``project_id``,
|
|
and the order goes to the engine's manager, as in ``test_engine_manager.py``.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from sqlalchemy import select
|
|
|
|
from app.application.container_engine import build_scheduler
|
|
from app.infrastructure.db.models import AiModelRow, OrganizationRow
|
|
from app.infrastructure.repositories.engine import PipelineStateRepository
|
|
from app.infrastructure.repositories.workforce import TaskRepository
|
|
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]]"
|
|
)
|
|
|
|
|
|
async def test_an_order_in_a_thread_without_a_project_gets_the_orgs_project(
|
|
client, session_factory, seeded, platform_owner_token, monkeypatch
|
|
):
|
|
scripted = ScriptedModel([ScriptedReply(content=ORDER_REPLY)])
|
|
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: org, project, team and grants.
|
|
_, headers = await _manager(client)
|
|
body = await _complete(client, headers)
|
|
org_id = body["organization"]["id"]
|
|
project_id = body["project"]["id"]
|
|
|
|
# prepaid (D-B): the wizard's org starts at zero credit, and an order costs model calls.
|
|
await fund_org(session_factory, org_id)
|
|
|
|
# The web UI's way: a thread with 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"]
|
|
|
|
# The order goes to the engine's manager. The HTTP message route builds its manager with
|
|
# the pipeline states too (workforce_service._manager_turns, covered by
|
|
# test_integration_order_route.py); this test calls the manager directly only to read the
|
|
# reply and the states in the same session.
|
|
async with session_factory() as session:
|
|
owner_user_id = (await session.get(OrganizationRow, org_id)).owner_user_id
|
|
_, assistant = 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 "کار ساخته شد: صفحهٔ ورود" in assistant.content
|
|
|
|
assert len(tasks) == 1
|
|
assert tasks[0].project_id == project_id
|
|
assert len(states) == 1
|
|
assert states[0].project_id == project_id
|