Files
smart-org/apps/api/tests/test_integration_thread_project.py
T
mjalmoudy94andClaude Sonnet 5.5 fef5a9deab 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>
2026-10-14 20:57:56 +03:30

89 lines
3.9 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.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"]
# 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
# without pipeline states (workforce_service._manager_turns), so it cannot show the state.
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