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:
@@ -0,0 +1,171 @@
|
||||
"""The manager chat does not call a model for an organization with no credit.
|
||||
|
||||
What these tests defend:
|
||||
|
||||
* a zero-credit organization gets the note as the manager's reply: the model is
|
||||
never called and the tags in the order are not executed (prepaid, decision D-B)
|
||||
* a missing wallet is no credit either
|
||||
* once the owner credits the organization, the same chat reaches the model
|
||||
* a manager built without a wallet (the direct construction the other manager
|
||||
tests use) keeps its old behaviour
|
||||
|
||||
The organization fixture and the manager's model profile come from
|
||||
``tests.test_engine_manager``.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from app.application.container_org import build_memory
|
||||
from app.application.engine.manager import CHAT_NO_CREDIT_MESSAGE, ManagerTurns
|
||||
from app.domain.workforce.models import MessageRole, RoleKey
|
||||
from app.infrastructure.db.models import CreditWalletRow, ThreadRow
|
||||
from app.infrastructure.repositories.ai_control import (
|
||||
AiModelRepository,
|
||||
ModelCredentialRepository,
|
||||
RateCardRepository,
|
||||
)
|
||||
from app.infrastructure.repositories.audit import UsageLedgerRepository
|
||||
from app.infrastructure.repositories.billing import CreditWalletRepository
|
||||
from app.infrastructure.repositories.engine import (
|
||||
EmployeeProfileRepository,
|
||||
PipelineStateRepository,
|
||||
)
|
||||
from app.infrastructure.repositories.workforce import (
|
||||
EmployeeRepository,
|
||||
MessageRepository,
|
||||
TaskRepository,
|
||||
ThreadRepository,
|
||||
)
|
||||
from tests.billing_helpers import fund_org
|
||||
from tests.engine_fakes import FakeGrants, ScriptedModel, ScriptedReply
|
||||
from tests.test_engine_manager import _with_model_profile
|
||||
from tests.test_engine_manager import world as manager_world
|
||||
|
||||
#: The engine manager tests' organization fixture. Binding it under its own name
|
||||
#: registers it for this module's tests (a bare import would be flagged as unused).
|
||||
world = manager_world
|
||||
|
||||
_ORDER = '[[CREATE_TASK title="ورود" kind="feature"]]ساخت فرم ورود[[/CREATE_TASK]]'
|
||||
_PLAIN_REPLY = "آمادهام."
|
||||
|
||||
|
||||
async def _thread_with_manager(world, session) -> str:
|
||||
"""A manager with a strong model profile, and a thread to talk in."""
|
||||
manager = await world.employee(session, "مدیر", RoleKey.MANAGER)
|
||||
await _with_model_profile(world, session, manager.id, world.strong_id)
|
||||
thread = await ThreadRepository(session).create(
|
||||
ThreadRow(organization_id=world.organization_id, title="گفتگو")
|
||||
)
|
||||
await session.commit()
|
||||
return thread.id
|
||||
|
||||
|
||||
async def _manager(session, *, scripted, grants, wired: bool) -> ManagerTurns:
|
||||
"""The engine manager on real repositories; ``wired`` adds the prepaid gate."""
|
||||
return ManagerTurns(
|
||||
provider=scripted,
|
||||
employees=EmployeeRepository(session),
|
||||
messages=MessageRepository(session),
|
||||
tasks=TaskRepository(session),
|
||||
threads=ThreadRepository(session),
|
||||
models=AiModelRepository(session),
|
||||
credentials=ModelCredentialRepository(session),
|
||||
rate_cards=RateCardRepository(session),
|
||||
ledger=UsageLedgerRepository(session),
|
||||
grants=grants,
|
||||
memory=build_memory(session),
|
||||
profiles=EmployeeProfileRepository(session),
|
||||
states=PipelineStateRepository(session),
|
||||
runner=None,
|
||||
wallets=CreditWalletRepository(session) if wired else None,
|
||||
)
|
||||
|
||||
|
||||
async def test_a_zero_credit_org_gets_the_note_and_no_model_call(world, session_factory):
|
||||
async with session_factory() as session:
|
||||
session.add(
|
||||
CreditWalletRow(organization_id=world.organization_id, balance=0.0, currency="USD")
|
||||
)
|
||||
thread_id = await _thread_with_manager(world, session)
|
||||
scripted = ScriptedModel([ScriptedReply(content=_ORDER)])
|
||||
grants = FakeGrants({world.strong_id: "strong"})
|
||||
|
||||
async with session_factory() as session:
|
||||
manager = await _manager(session, scripted=scripted, grants=grants, wired=True)
|
||||
user_message, assistant = await manager.chat(
|
||||
organization_id=world.organization_id,
|
||||
thread_id=thread_id,
|
||||
content="یک فرم ورود بساز.",
|
||||
user_id=world.user_id,
|
||||
)
|
||||
await session.commit()
|
||||
tasks = await TaskRepository(session).list_for_org(world.organization_id)
|
||||
|
||||
assert user_message.content == "یک فرم ورود بساز."
|
||||
assert assistant.role is MessageRole.ASSISTANT
|
||||
assert assistant.content == CHAT_NO_CREDIT_MESSAGE
|
||||
assert scripted.calls == 0
|
||||
# The order's CREATE_TASK tag was not executed.
|
||||
assert tasks == []
|
||||
|
||||
|
||||
async def test_a_missing_wallet_counts_as_no_credit(world, session_factory):
|
||||
async with session_factory() as session:
|
||||
thread_id = await _thread_with_manager(world, session)
|
||||
scripted = ScriptedModel([ScriptedReply(content=_PLAIN_REPLY)])
|
||||
grants = FakeGrants({world.strong_id: "strong"})
|
||||
|
||||
async with session_factory() as session:
|
||||
manager = await _manager(session, scripted=scripted, grants=grants, wired=True)
|
||||
_, assistant = await manager.chat(
|
||||
organization_id=world.organization_id,
|
||||
thread_id=thread_id,
|
||||
content="سلام",
|
||||
user_id=world.user_id,
|
||||
)
|
||||
await session.commit()
|
||||
|
||||
assert assistant.content == CHAT_NO_CREDIT_MESSAGE
|
||||
assert scripted.calls == 0
|
||||
|
||||
|
||||
async def test_after_the_owner_credits_the_org_the_model_answers(world, session_factory):
|
||||
async with session_factory() as session:
|
||||
thread_id = await _thread_with_manager(world, session)
|
||||
await fund_org(session_factory, world.organization_id, 50.0)
|
||||
scripted = ScriptedModel([ScriptedReply(content=_PLAIN_REPLY)])
|
||||
grants = FakeGrants({world.strong_id: "strong"})
|
||||
|
||||
async with session_factory() as session:
|
||||
manager = await _manager(session, scripted=scripted, grants=grants, wired=True)
|
||||
_, assistant = await manager.chat(
|
||||
organization_id=world.organization_id,
|
||||
thread_id=thread_id,
|
||||
content="سلام",
|
||||
user_id=world.user_id,
|
||||
)
|
||||
await session.commit()
|
||||
|
||||
assert assistant.content == _PLAIN_REPLY
|
||||
assert scripted.calls == 1
|
||||
|
||||
|
||||
async def test_a_manager_built_without_a_wallet_keeps_the_old_behaviour(world, session_factory):
|
||||
"""No ``wallets`` means no gate: the direct constructions in other tests rely on it."""
|
||||
async with session_factory() as session:
|
||||
thread_id = await _thread_with_manager(world, session)
|
||||
scripted = ScriptedModel([ScriptedReply(content=_PLAIN_REPLY)])
|
||||
grants = FakeGrants({world.strong_id: "strong"})
|
||||
|
||||
async with session_factory() as session:
|
||||
manager = await _manager(session, scripted=scripted, grants=grants, wired=False)
|
||||
_, assistant = await manager.chat(
|
||||
organization_id=world.organization_id,
|
||||
thread_id=thread_id,
|
||||
content="سلام",
|
||||
user_id=world.user_id,
|
||||
)
|
||||
await session.commit()
|
||||
|
||||
assert assistant.content == _PLAIN_REPLY
|
||||
assert scripted.calls == 1
|
||||
Reference in New Issue
Block a user