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:
@@ -17,6 +17,7 @@ import pytest_asyncio
|
||||
from httpx import AsyncClient
|
||||
|
||||
from tests.conftest import auth_header, signup
|
||||
from tests.legacy_tasks import legacy_task
|
||||
|
||||
|
||||
@pytest_asyncio.fixture(autouse=True)
|
||||
@@ -207,23 +208,24 @@ async def test_tools_catalog_requires_no_secrets(client: AsyncClient):
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
async def test_starting_a_run_produces_a_timeline(client: AsyncClient):
|
||||
"""Every run must be reconstructable from its steps."""
|
||||
token = (await signup(client, "wf-run@example.com"))["access_token"]
|
||||
async def test_starting_a_run_produces_a_timeline(client: AsyncClient, session_factory):
|
||||
"""Every run must be reconstructable from its steps.
|
||||
|
||||
The task is a pre-v2 row with no pipeline state, so it runs through the legacy path.
|
||||
A task made over HTTP now enters the engine and is refused here (decision D-D).
|
||||
"""
|
||||
signed = await signup(client, "wf-run@example.com")
|
||||
token = signed["access_token"]
|
||||
org_id = await _org(client, token)
|
||||
employee = await _employee(client, token, org_id)
|
||||
|
||||
task = await client.post(
|
||||
f"/api/organizations/{org_id}/tasks",
|
||||
json={
|
||||
"title": "افزودن تست برای ماژول پرداخت",
|
||||
"description": "پوشش تست واحد برای محاسبهٔ تخفیف",
|
||||
"assigned_employee_id": employee["id"],
|
||||
},
|
||||
headers=auth_header(token),
|
||||
task_id = await legacy_task(
|
||||
session_factory,
|
||||
org_id,
|
||||
signed["user"]["id"],
|
||||
title="افزودن تست برای ماژول پرداخت",
|
||||
assigned_employee_id=employee["id"],
|
||||
)
|
||||
assert task.status_code == 201, task.text
|
||||
task_id = task.json()["id"]
|
||||
|
||||
run = await client.post(
|
||||
f"/api/organizations/{org_id}/tasks/{task_id}/runs",
|
||||
@@ -241,17 +243,20 @@ async def test_starting_a_run_produces_a_timeline(client: AsyncClient):
|
||||
)
|
||||
|
||||
|
||||
async def test_run_payload_never_leaks_model_identity(client: AsyncClient):
|
||||
token = (await signup(client, "wf-runsecret@example.com"))["access_token"]
|
||||
async def test_run_payload_never_leaks_model_identity(client: AsyncClient, session_factory):
|
||||
"""The legacy run's payload names no model (a pre-v2 task, decision D-D)."""
|
||||
signed = await signup(client, "wf-runsecret@example.com")
|
||||
token = signed["access_token"]
|
||||
org_id = await _org(client, token)
|
||||
employee = await _employee(client, token, org_id)
|
||||
|
||||
task = await client.post(
|
||||
f"/api/organizations/{org_id}/tasks",
|
||||
json={"title": "کار برای بررسی نشت", "assigned_employee_id": employee["id"]},
|
||||
headers=auth_header(token),
|
||||
task_id = await legacy_task(
|
||||
session_factory,
|
||||
org_id,
|
||||
signed["user"]["id"],
|
||||
title="کار برای بررسی نشت",
|
||||
assigned_employee_id=employee["id"],
|
||||
)
|
||||
task_id = task.json()["id"]
|
||||
run = await client.post(
|
||||
f"/api/organizations/{org_id}/tasks/{task_id}/runs",
|
||||
json={"employee_id": employee["id"]},
|
||||
@@ -263,17 +268,20 @@ async def test_run_payload_never_leaks_model_identity(client: AsyncClient):
|
||||
assert forbidden not in payload, f"{forbidden} leaked in run payload"
|
||||
|
||||
|
||||
async def test_task_detail_lists_its_runs(client: AsyncClient):
|
||||
token = (await signup(client, "wf-taskdetail@example.com"))["access_token"]
|
||||
async def test_task_detail_lists_its_runs(client: AsyncClient, session_factory):
|
||||
"""A pre-v2 task (no pipeline state, decision D-D) keeps its runs on its detail."""
|
||||
signed = await signup(client, "wf-taskdetail@example.com")
|
||||
token = signed["access_token"]
|
||||
org_id = await _org(client, token)
|
||||
employee = await _employee(client, token, org_id)
|
||||
|
||||
task = await client.post(
|
||||
f"/api/organizations/{org_id}/tasks",
|
||||
json={"title": "کار با چند اجرا", "assigned_employee_id": employee["id"]},
|
||||
headers=auth_header(token),
|
||||
task_id = await legacy_task(
|
||||
session_factory,
|
||||
org_id,
|
||||
signed["user"]["id"],
|
||||
title="کار با چند اجرا",
|
||||
assigned_employee_id=employee["id"],
|
||||
)
|
||||
task_id = task.json()["id"]
|
||||
|
||||
run = await client.post(
|
||||
f"/api/organizations/{org_id}/tasks/{task_id}/runs",
|
||||
@@ -292,19 +300,24 @@ async def test_task_detail_lists_its_runs(client: AsyncClient):
|
||||
assert runs[0]["task_id"] == task_id
|
||||
|
||||
|
||||
async def test_a_closed_task_cannot_be_run_again(client: AsyncClient):
|
||||
async def test_a_closed_task_cannot_be_run_again(client: AsyncClient, session_factory):
|
||||
"""A successful run closes its task; re-running it must be refused, not
|
||||
silently create a second run against a task that is already done."""
|
||||
token = (await signup(client, "wf-closedtask@example.com"))["access_token"]
|
||||
silently create a second run against a task that is already done.
|
||||
|
||||
The task is a pre-v2 row (no pipeline state, decision D-D), so this is the legacy path.
|
||||
"""
|
||||
signed = await signup(client, "wf-closedtask@example.com")
|
||||
token = signed["access_token"]
|
||||
org_id = await _org(client, token)
|
||||
employee = await _employee(client, token, org_id)
|
||||
|
||||
task = await client.post(
|
||||
f"/api/organizations/{org_id}/tasks",
|
||||
json={"title": "کار بستهشده", "assigned_employee_id": employee["id"]},
|
||||
headers=auth_header(token),
|
||||
task_id = await legacy_task(
|
||||
session_factory,
|
||||
org_id,
|
||||
signed["user"]["id"],
|
||||
title="کار بستهشده",
|
||||
assigned_employee_id=employee["id"],
|
||||
)
|
||||
task_id = task.json()["id"]
|
||||
|
||||
first = await client.post(
|
||||
f"/api/organizations/{org_id}/tasks/{task_id}/runs",
|
||||
|
||||
Reference in New Issue
Block a user