مخزن اختصاصی برنامهٔ وب؛ محتوای پوشهٔ web در ریشهٔ مخزن قرار گرفت. تاریخچهٔ پروژهٔ ربات (agy-bot) در این مخزن وجود ندارد و هیچ وابستگی runtime به آن نیست.
220 lines
7.7 KiB
Python
220 lines
7.7 KiB
Python
"""§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"
|