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
This commit is contained in:
@@ -24,7 +24,11 @@ import pytest_asyncio
|
||||
from httpx import AsyncClient
|
||||
|
||||
from app.domain.identity.models import PlatformRole
|
||||
from app.domain.workforce.pipeline import Stage
|
||||
from app.infrastructure.repositories.engine import PipelineStateRepository
|
||||
from tests.billing_helpers import fund_org
|
||||
from tests.conftest import auth_header, signup
|
||||
from tests.legacy_tasks import legacy_task
|
||||
|
||||
|
||||
@pytest_asyncio.fixture(autouse=True)
|
||||
@@ -33,7 +37,9 @@ async def _platform_seed(seeded):
|
||||
return seeded
|
||||
|
||||
|
||||
async def test_end_to_end_smoke_path(client: AsyncClient):
|
||||
async def test_end_to_end_smoke_path(client: AsyncClient, session_factory):
|
||||
"""Decision D-D: the task made over HTTP enters the engine and refuses the legacy run, so
|
||||
the run, its metering and its audit are walked on a pre-v2 task (see step 10)."""
|
||||
# -- 1. signup -------------------------------------------------------- #
|
||||
# The platform owner, not a plain manager: step 6 drives the sandbox
|
||||
# directly, and v2 makes container/command access owner-only
|
||||
@@ -184,6 +190,9 @@ async def test_end_to_end_smoke_path(client: AsyncClient):
|
||||
assert thread.status_code == 201, thread.text
|
||||
thread_id = thread.json()["id"]
|
||||
|
||||
# prepaid (D-B): the HTTP org starts at zero credit, and the chat costs model calls.
|
||||
await fund_org(session_factory, org_id)
|
||||
|
||||
message = await client.post(
|
||||
f"/api/organizations/{org_id}/threads/{thread_id}/messages",
|
||||
json={"content": "برای ماژول پرداخت تست بنویس.", "employee_id": employee_id},
|
||||
@@ -212,10 +221,31 @@ async def test_end_to_end_smoke_path(client: AsyncClient):
|
||||
)
|
||||
assert task.status_code == 201, task.text
|
||||
task_id = task.json()["id"]
|
||||
# Decision D-D: a task made over HTTP is the engine's from manager review, so it has a
|
||||
# pipeline state and the legacy run button refuses it.
|
||||
async with session_factory() as session:
|
||||
state = await PipelineStateRepository(session).get(task_id)
|
||||
assert state is not None and state.stage is Stage.PENDING_MANAGER_REVIEW
|
||||
refused = await client.post(
|
||||
f"/api/organizations/{org_id}/tasks/{task_id}/runs",
|
||||
json={"employee_id": employee_id},
|
||||
headers=headers,
|
||||
)
|
||||
assert refused.status_code == 409, refused.text
|
||||
|
||||
# -- 10. run task ------------------------------------------------------ #
|
||||
# The legacy run path serves pre-v2 tasks, which have no pipeline state, so the run, its
|
||||
# metering and its audit are checked on one of those (decision D-D).
|
||||
run_task_id = await legacy_task(
|
||||
session_factory,
|
||||
org_id,
|
||||
signup_body["user"]["id"],
|
||||
title="افزودن تست برای ماژول پرداخت",
|
||||
project_id=project_id,
|
||||
assigned_employee_id=employee_id,
|
||||
)
|
||||
run = await client.post(
|
||||
f"/api/organizations/{org_id}/tasks/{task_id}/runs",
|
||||
f"/api/organizations/{org_id}/tasks/{run_task_id}/runs",
|
||||
json={"employee_id": employee_id},
|
||||
headers=headers,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user