نسخهٔ وب مستقل — پلتفرم نیروی کار هوش مصنوعی
مخزن اختصاصی برنامهٔ وب؛ محتوای پوشهٔ web در ریشهٔ مخزن قرار گرفت. تاریخچهٔ پروژهٔ ربات (agy-bot) در این مخزن وجود ندارد و هیچ وابستگی runtime به آن نیست.
This commit is contained in:
@@ -0,0 +1,219 @@
|
||||
"""§17 — the seed produces a stack a developer can actually log into.
|
||||
|
||||
This file exists because of two real bugs, both found by trying to *use* a
|
||||
seeded install rather than by reading the code:
|
||||
|
||||
1. The seed created ``demo@agy.local`` by writing straight to the database,
|
||||
bypassing request validation — while the login endpoint rejects ``.local`` as
|
||||
a special-use TLD. The account existed and was unreachable.
|
||||
2. Even the account that *could* log in belonged to no organization, so every
|
||||
screen rendered empty and the demo looked broken.
|
||||
|
||||
Neither was caught by the rest of the suite, because every other test signs up a
|
||||
fresh user at ``@example.com`` and creates its own organization. The seed was
|
||||
simply never exercised end to end. These tests close that hole.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest_asyncio
|
||||
from httpx import AsyncClient
|
||||
from sqlalchemy import func, select
|
||||
|
||||
from tests.conftest import auth_header
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def seeded_install(monkeypatch, engine, session_factory):
|
||||
"""Run the real ``app.seed`` against the in-memory test database.
|
||||
|
||||
``app.seed`` opens its own engine and session factory, so both module-level
|
||||
names are swapped for the test ones. The seed code itself is untouched —
|
||||
testing a copy of it would prove nothing.
|
||||
"""
|
||||
import app.seed as seed_module
|
||||
|
||||
monkeypatch.setattr(seed_module, "engine", engine)
|
||||
monkeypatch.setattr(seed_module, "SessionFactory", session_factory)
|
||||
await seed_module.seed()
|
||||
return seed_module
|
||||
|
||||
|
||||
async def test_seeded_demo_user_can_log_in(client: AsyncClient, seeded_install):
|
||||
"""The credentials the seed prints must actually work."""
|
||||
response = await client.post(
|
||||
"/api/auth/login",
|
||||
json={
|
||||
"email": seeded_install.DEMO_EMAIL,
|
||||
"password": seeded_install.DEMO_PASSWORD,
|
||||
},
|
||||
)
|
||||
assert response.status_code == 200, (
|
||||
f"the seeded demo account cannot log in: {response.text}"
|
||||
)
|
||||
assert response.json()["access_token"]
|
||||
|
||||
|
||||
async def test_seeded_platform_owner_can_log_in(client: AsyncClient, seeded_install):
|
||||
response = await client.post(
|
||||
"/api/auth/login",
|
||||
json={
|
||||
"email": seeded_install.PLATFORM_OWNER_EMAIL,
|
||||
"password": seeded_install.PLATFORM_OWNER_PASSWORD,
|
||||
},
|
||||
)
|
||||
assert response.status_code == 200, response.text
|
||||
|
||||
me = await client.get(
|
||||
"/api/auth/me", headers=auth_header(response.json()["access_token"])
|
||||
)
|
||||
assert me.status_code == 200, me.text
|
||||
assert me.json()["user"]["is_platform_owner"] is True
|
||||
|
||||
|
||||
async def test_seeded_accounts_belong_to_the_sample_organization(
|
||||
client: AsyncClient, seeded_install
|
||||
):
|
||||
"""A login that lands on an empty dashboard is a broken demo.
|
||||
|
||||
Both seeded accounts must see the sample organization, otherwise every
|
||||
screen behind the login renders nothing.
|
||||
"""
|
||||
for email, password in (
|
||||
(seeded_install.DEMO_EMAIL, seeded_install.DEMO_PASSWORD),
|
||||
(seeded_install.PLATFORM_OWNER_EMAIL, seeded_install.PLATFORM_OWNER_PASSWORD),
|
||||
):
|
||||
login = await client.post("/api/auth/login", json={"email": email, "password": password})
|
||||
assert login.status_code == 200, login.text
|
||||
|
||||
me = await client.get(
|
||||
"/api/auth/me", headers=auth_header(login.json()["access_token"])
|
||||
)
|
||||
organizations = me.json()["organizations"]
|
||||
assert organizations, f"{email} belongs to no organization"
|
||||
assert organizations[0]["name"] == seeded_install.DEMO_ORG_NAME
|
||||
assert organizations[0]["role"] == "owner"
|
||||
|
||||
|
||||
async def test_seeded_install_can_use_the_product(
|
||||
client: AsyncClient, seeded_install
|
||||
):
|
||||
"""The seed's platform rows are enough to create a project and run a task.
|
||||
|
||||
A seed that satisfies the login test but leaves the catalogue empty would
|
||||
still fail the moment a developer tried anything real.
|
||||
"""
|
||||
login = await client.post(
|
||||
"/api/auth/login",
|
||||
json={
|
||||
"email": seeded_install.DEMO_EMAIL,
|
||||
"password": seeded_install.DEMO_PASSWORD,
|
||||
},
|
||||
)
|
||||
headers = auth_header(login.json()["access_token"])
|
||||
org_id = (await client.get("/api/auth/me", headers=headers)).json()["organizations"][0]["id"]
|
||||
|
||||
# The platform catalogue the seed installs must be visible to a tenant.
|
||||
templates = await client.get(
|
||||
f"/api/organizations/{org_id}/agent-templates", headers=headers
|
||||
)
|
||||
assert templates.status_code == 200, templates.text
|
||||
assert templates.json(), "the seed must install agent templates"
|
||||
|
||||
sandbox_templates = await client.get(
|
||||
f"/api/organizations/{org_id}/sandbox/templates", headers=headers
|
||||
)
|
||||
assert sandbox_templates.json(), "the seed must install sandbox templates"
|
||||
|
||||
project_id = (
|
||||
await client.post(
|
||||
f"/api/organizations/{org_id}/projects",
|
||||
json={"name": "پروژهٔ seed"},
|
||||
headers=headers,
|
||||
)
|
||||
).json()["id"]
|
||||
|
||||
template = templates.json()[0]
|
||||
employee_id = (
|
||||
await client.post(
|
||||
f"/api/organizations/{org_id}/employees",
|
||||
json={
|
||||
"name": "کارمند seed",
|
||||
"role_key": template["role_key"],
|
||||
"template_id": template["id"],
|
||||
},
|
||||
headers=headers,
|
||||
)
|
||||
).json()["id"]
|
||||
|
||||
task_id = (
|
||||
await client.post(
|
||||
f"/api/organizations/{org_id}/tasks",
|
||||
json={
|
||||
"title": "کار seed",
|
||||
"project_id": project_id,
|
||||
"assigned_employee_id": employee_id,
|
||||
},
|
||||
headers=headers,
|
||||
)
|
||||
).json()["id"]
|
||||
|
||||
run = await client.post(
|
||||
f"/api/organizations/{org_id}/tasks/{task_id}/runs",
|
||||
json={"employee_id": employee_id},
|
||||
headers=headers,
|
||||
)
|
||||
assert run.status_code in (200, 201), run.text
|
||||
assert run.json()["status"] == "completed", (
|
||||
"a seeded install must be able to route to a model and finish a run"
|
||||
)
|
||||
|
||||
|
||||
async def test_seed_is_idempotent(client: AsyncClient, seeded_install, session_factory):
|
||||
"""Re-running the seed must not duplicate anything.
|
||||
|
||||
The memberships and the wallet are now created by get-or-create rather than
|
||||
as a side effect of creating the organization, so this guards the parts that
|
||||
were easy to double up.
|
||||
"""
|
||||
import app.seed as seed_module
|
||||
from app.infrastructure.db.models import (
|
||||
AgentTemplateRow,
|
||||
CreditWalletRow,
|
||||
MembershipRow,
|
||||
OrganizationRow,
|
||||
UserRow,
|
||||
)
|
||||
|
||||
async def counts() -> dict[str, int]:
|
||||
async with session_factory() as session:
|
||||
return {
|
||||
"organizations": int(
|
||||
(await session.execute(select(func.count()).select_from(OrganizationRow)))
|
||||
.scalar_one()
|
||||
),
|
||||
"memberships": int(
|
||||
(await session.execute(select(func.count()).select_from(MembershipRow)))
|
||||
.scalar_one()
|
||||
),
|
||||
"wallets": int(
|
||||
(await session.execute(select(func.count()).select_from(CreditWalletRow)))
|
||||
.scalar_one()
|
||||
),
|
||||
"users": int(
|
||||
(await session.execute(select(func.count()).select_from(UserRow)))
|
||||
.scalar_one()
|
||||
),
|
||||
"templates": int(
|
||||
(await session.execute(select(func.count()).select_from(AgentTemplateRow)))
|
||||
.scalar_one()
|
||||
),
|
||||
}
|
||||
|
||||
before = await counts()
|
||||
await seed_module.seed()
|
||||
after = await counts()
|
||||
|
||||
assert after == before, f"re-seeding changed row counts: {before} -> {after}"
|
||||
assert after["organizations"] == 1
|
||||
assert after["memberships"] == 2, "one per seeded account, and no duplicates"
|
||||
Reference in New Issue
Block a user