نسخهٔ وب مستقل — پلتفرم نیروی کار هوش مصنوعی

مخزن اختصاصی برنامهٔ وب؛ محتوای پوشهٔ web در ریشهٔ مخزن قرار گرفت.
تاریخچهٔ پروژهٔ ربات (agy-bot) در این مخزن وجود ندارد و هیچ وابستگی runtime به آن نیست.
This commit is contained in:
mjalmoudy94
2026-10-13 00:39:28 +03:30
commit 717e53a7ce
185 changed files with 39910 additions and 0 deletions
+248
View File
@@ -0,0 +1,248 @@
"""Shared test fixtures.
Tests run against an in-memory SQLite database and the ``mock`` adapters, so the
suite needs no Postgres, no Redis and no credentials.
"""
from __future__ import annotations
import pytest
import pytest_asyncio
from httpx import ASGITransport, AsyncClient
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
from sqlalchemy.pool import StaticPool
# Importing the ORM package registers every table on ``Base.metadata``. Bound to
# a private name on purpose: ``app`` is also the FastAPI instance imported below,
# and binding the package to the same name would shadow it.
import app.infrastructure.db.models as _orm_models # noqa: F401
from app.core.config import settings
from app.core.database import Base, get_session
from app.domain.base import new_id
from app.infrastructure.db.models import UserRow
from app.main import app
@pytest.fixture(scope="session", autouse=True)
def _production_shaped_jwt_secret():
"""Sign test tokens with a 32+ byte key.
The development default is ``change-me`` (9 bytes), so PyJWT emits an
``InsecureKeyLengthWarning`` for every token signed or verified — hundreds of
lines per run, which buries genuine warnings. Production refuses a short
secret outright; that guard is covered by ``tests/test_config_guard.py``.
"""
settings.jwt_secret = "test-only-jwt-secret-with-more-than-32-bytes"
@pytest_asyncio.fixture
async def engine():
# StaticPool keeps one connection alive so the in-memory schema survives
# across the sessions a single request opens.
test_engine = create_async_engine(
"sqlite+aiosqlite:///:memory:",
connect_args={"check_same_thread": False},
poolclass=StaticPool,
)
async with test_engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
yield test_engine
await test_engine.dispose()
@pytest_asyncio.fixture
async def session_factory(engine):
return async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
@pytest_asyncio.fixture
async def redirect_error_log(engine, monkeypatch):
"""Point the error recorder and the workers at the in-memory database.
Neither goes through the ``get_session`` dependency override that ``client``
installs — the recorder deliberately opens its own session, and the workers
are not HTTP requests at all. Without this redirect, any test that triggers a
failure path would write into the developer's real ``agy_web.db``.
Not autouse: only the modules that exercise failure paths need it, and the
``engine`` it depends on builds the whole schema.
"""
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
import app.infrastructure.error_log as error_log
from app.workers import base as worker_base
factory = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
monkeypatch.setattr(error_log, "SessionFactory", factory)
monkeypatch.setattr(worker_base, "SessionFactory", factory)
return factory
@pytest_asyncio.fixture
async def client(engine, session_factory):
async def _override_session():
async with session_factory() as session:
try:
yield session
await session.commit()
except Exception:
await session.rollback()
raise
app.dependency_overrides[get_session] = _override_session
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as test_client:
yield test_client
app.dependency_overrides.clear()
async def signup(
client: AsyncClient, email: str, password: str = "password123", name: str = ""
) -> dict:
response = await client.post(
"/api/auth/signup", json={"email": email, "password": password, "name": name}
)
assert response.status_code == 201, response.text
return response.json()
def auth_header(token: str) -> dict[str, str]:
return {"Authorization": f"Bearer {token}"}
@pytest_asyncio.fixture
async def platform_owner_token(client: AsyncClient, session_factory) -> str:
"""A signed-up user promoted to platform owner.
The promotion is done directly in the database because there is deliberately
no API that can grant platform-owner status.
"""
# ``example.com`` is a documentation domain and passes email validation.
# Do NOT use ``.local``/``.test``: those are special-use TLDs and newer
# ``email-validator`` releases reject them, which fails the fixture.
email = "platform-owner@example.com"
body = await signup(client, email, name="مالک سامانه")
async with session_factory() as session:
row = (
await session.execute(select(UserRow).where(UserRow.email == email))
).scalar_one()
row.is_platform_owner = True
await session.commit()
return body["access_token"]
#: The system agent templates ``app/seed.py`` installs. Mirrored here so the test
#: suite exercises the same employee-creation path a seeded install uses.
SYSTEM_TEMPLATES: list[tuple[str, str, str, list[str]]] = [
("مدیر سازمان", "manager", "کارها را تقسیم می‌کند.", ["knowledge.search"]),
(
"برنامه‌نویس",
"developer",
"کد می‌نویسد و تست می‌کند.",
["sandbox.run_command", "sandbox.write_file"],
),
]
#: The routing policies ``app/seed.py`` installs.
SYSTEM_ROUTING_POLICIES: list[tuple[str, str, bool, str]] = [
("سریع و ارزان", "cheap", True, "برای کارهای روزمره."),
("استدلال عمیق", "reasoning", False, "برای تحلیل پیچیده."),
]
#: The mock models ``app/seed.py`` installs: (internal_name, display, capabilities).
SYSTEM_MODELS: list[tuple[str, str, list[str]]] = [
("mock-fast", "Mock Fast", ["text"]),
("mock-reasoning", "Mock Reasoning", ["text", "reasoning", "code"]),
]
#: The rate cards ``app/seed.py`` installs: (unit, customer_price, internal_cost).
SYSTEM_RATE_CARDS: list[tuple[str, float, float]] = [
("model_input_tokens", 0.0000004, 0.0000002),
("model_output_tokens", 0.0000016, 0.0000008),
("tool_call", 0.0005, 0.0001),
("premium_employee_run", 0.02, 0.008),
]
#: The sandbox templates ``app/seed.py`` installs: (name, image, description).
SYSTEM_SANDBOX_TEMPLATES: list[tuple[str, str, str]] = [
("python-3.13", "python:3.13-slim", "محیط پایتون برای اجرای تست و اسکریپت."),
("node-22", "node:22-slim", "محیط Node برای build و اجرای پروژه‌های فرانت‌اند."),
]
@pytest_asyncio.fixture
async def seeded(session_factory):
"""Install the platform-level seed rows a fresh install would have.
Agent templates, routing policies, the mock provider/models, the rate cards
and the sandbox templates are all *platform* data (no organization owns
them), so without this an employee cannot be created, a run cannot resolve a
model to route to, usage cannot be priced and no sandbox can be started —
none of which the real seeded install lacks.
"""
from app.infrastructure.db.models import (
AgentTemplateRow,
AiModelRow,
AiProviderRow,
RateCardRow,
RoutingPolicyRow,
SandboxTemplateRow,
)
async with session_factory() as session:
for name, role_key, description, tools in SYSTEM_TEMPLATES:
session.add(
AgentTemplateRow(
name=name,
role_key=role_key,
description=description,
default_prompt=f"تو {name} هستی. وظیفه‌ات: {description}",
default_tools_json=tools,
is_system=True,
)
)
for name, mode, is_default, description in SYSTEM_ROUTING_POLICIES:
session.add(
RoutingPolicyRow(
name=name, description=description, mode=mode,
is_default=is_default, is_active=True,
)
)
# A run cannot reach a model without an active provider + model, and
# ``pick_for_mode`` returns ``None`` when the catalogue is empty — which
# surfaces as a ``ProviderUnavailableError`` and a *failed* run.
provider_id = new_id()
session.add(
AiProviderRow(id=provider_id, name="mock", type="mock", base_url="", is_active=True)
)
for internal, display, capabilities in SYSTEM_MODELS:
session.add(
AiModelRow(
provider_id=provider_id,
internal_name=internal,
display_name_for_admin=display,
capabilities=capabilities,
input_price_per_1m=0.3,
output_price_per_1m=1.2,
is_active=True,
)
)
for unit, customer_price, internal_cost in SYSTEM_RATE_CARDS:
session.add(
RateCardRow(
name=unit.replace("_", " "), unit=unit,
customer_price=customer_price,
internal_cost_estimate=internal_cost, is_active=True,
)
)
for name, image, description in SYSTEM_SANDBOX_TEMPLATES:
session.add(
SandboxTemplateRow(
name=name, provider="mock", image=image, description=description,
default_cpu=1.0, default_memory_mb=1024, default_disk_mb=2048,
is_system=True,
)
)
await session.commit()
return True