Files
smart-org/apps/api/tests/test_workforce.py
T
mjalmoudy94 717e53a7ce نسخهٔ وب مستقل — پلتفرم نیروی کار هوش مصنوعی
مخزن اختصاصی برنامهٔ وب؛ محتوای پوشهٔ web در ریشهٔ مخزن قرار گرفت.
تاریخچهٔ پروژهٔ ربات (agy-bot) در این مخزن وجود ندارد و هیچ وابستگی runtime به آن نیست.
2026-10-13 00:39:28 +03:30

380 lines
15 KiB
Python

"""AI Workforce: employees, runs, tool permissions and approvals.
The invariants these tests defend:
* a customer never sees a model, provider or routing *mode* — only a named policy
* tool permission is deny-by-default, so a tool the platform has never heard of
is not silently allowed
* every run leaves a timeline, because that timeline is the audit trail
* approving is a human decision; nothing dangerous executes without it
"""
from __future__ import annotations
import json
import pytest_asyncio
from httpx import AsyncClient
from tests.conftest import auth_header, signup
@pytest_asyncio.fixture(autouse=True)
async def _platform_seed(seeded):
"""Every test here needs the platform-level seed rows.
Agent templates and routing policies are installed by ``app/seed.py`` and are
not owned by any organization, so a test that creates an employee would
otherwise find no template to start from.
"""
return seeded
async def _org(client: AsyncClient, token: str, name: str = "شرکت نیروی کار") -> str:
response = await client.post(
"/api/organizations", json={"name": name}, headers=auth_header(token)
)
assert response.status_code == 201, response.text
return response.json()["id"]
async def _employee(
client: AsyncClient, token: str, org_id: str, *, name: str = "برنامه‌نویس یک"
) -> dict:
"""Create an employee from the first available template."""
templates = await client.get(
f"/api/organizations/{org_id}/agent-templates", headers=auth_header(token)
)
assert templates.status_code == 200, templates.text
template = templates.json()[0]
response = await client.post(
f"/api/organizations/{org_id}/employees",
json={"name": name, "role_key": template["role_key"], "template_id": template["id"]},
headers=auth_header(token),
)
assert response.status_code == 201, response.text
return response.json()
# ---------------------------------------------------------------------------
# Employees
# ---------------------------------------------------------------------------
async def test_creating_an_employee_seeds_a_prompt_and_tool_permissions(client: AsyncClient):
"""A template must produce a usable employee, not an empty shell."""
token = (await signup(client, "wf-seed@example.com"))["access_token"]
org_id = await _org(client, token)
employee = await _employee(client, token, org_id)
detail = await client.get(
f"/api/organizations/{org_id}/employees/{employee['id']}", headers=auth_header(token)
)
assert detail.status_code == 200, detail.text
body = detail.json()
# Prompt version 1 exists and carries the template's default prompt.
assert len(body["prompt_versions"]) >= 1
assert body["prompt_versions"][0]["version"] == 1
assert body["prompt_versions"][0]["content"].strip()
# Tool permissions were seeded from the template.
assert isinstance(body["tool_permissions"], list)
assert all("tool_key" in p and "allowed" in p for p in body["tool_permissions"])
async def test_employee_payload_never_leaks_model_identity(client: AsyncClient):
"""The customer buys capacity, not a model. Nothing may name one."""
token = (await signup(client, "wf-secret@example.com"))["access_token"]
org_id = await _org(client, token)
employee = await _employee(client, token, org_id)
detail = await client.get(
f"/api/organizations/{org_id}/employees/{employee['id']}", headers=auth_header(token)
)
payload = json.dumps(detail.json()).lower()
for forbidden in ("provider", "api_key", "internal_name", "openai", "anthropic"):
assert forbidden not in payload, f"{forbidden} leaked in employee payload"
async def test_routing_choices_expose_a_name_but_not_the_mode(client: AsyncClient):
"""`mode` is an internal routing hint and must not be serialised."""
token = (await signup(client, "wf-routing@example.com"))["access_token"]
org_id = await _org(client, token)
response = await client.get(
f"/api/organizations/{org_id}/routing-choices", headers=auth_header(token)
)
assert response.status_code == 200, response.text
choices = response.json()
assert choices, "seed data should provide routing policies"
for choice in choices:
assert set(choice) <= {"id", "name", "description"}, choice
assert "mode" not in choice
async def test_prompt_version_is_appended_with_an_incrementing_number(client: AsyncClient):
token = (await signup(client, "wf-prompt@example.com"))["access_token"]
org_id = await _org(client, token)
employee = await _employee(client, token, org_id)
for content in ("نسخهٔ دوم پرامپت", "نسخهٔ سوم پرامپت"):
response = await client.post(
f"/api/organizations/{org_id}/employees/{employee['id']}/prompt-versions",
json={"content": content},
headers=auth_header(token),
)
assert response.status_code in (200, 201), response.text
detail = await client.get(
f"/api/organizations/{org_id}/employees/{employee['id']}", headers=auth_header(token)
)
versions = sorted(v["version"] for v in detail.json()["prompt_versions"])
assert versions == [1, 2, 3]
# ---------------------------------------------------------------------------
# Tool permissions — deny by default
# ---------------------------------------------------------------------------
async def test_unknown_tool_is_denied_by_default(client: AsyncClient):
"""The platform must fail closed for a tool it has never heard of.
A permission row for a tool that does not exist would be a silent no-op that
hides a typo, so the request is rejected outright (422) rather than stored.
"""
token = (await signup(client, "wf-tools@example.com"))["access_token"]
org_id = await _org(client, token)
employee = await _employee(client, token, org_id)
# Explicitly grant a tool that is not in the catalog.
response = await client.put(
f"/api/organizations/{org_id}/employees/{employee['id']}/tools/evil.exfiltrate",
json={"allowed": False},
headers=auth_header(token),
)
assert response.status_code == 422, response.text
assert response.json()["error"]["code"] == "validation_error"
# And nothing was persisted: the employee's permission set stays clean.
detail = await client.get(
f"/api/organizations/{org_id}/employees/{employee['id']}",
headers=auth_header(token),
)
assert detail.status_code == 200, detail.text
granted = {p["tool_key"] for p in detail.json()["tool_permissions"] if p["allowed"]}
assert "evil.exfiltrate" not in granted, "an unknown tool must never be granted"
async def test_tool_permission_can_be_toggled(client: AsyncClient):
token = (await signup(client, "wf-toggle@example.com"))["access_token"]
org_id = await _org(client, token)
employee = await _employee(client, token, org_id)
base = f"/api/organizations/{org_id}/employees/{employee['id']}/tools"
granted = await client.put(
f"{base}/knowledge.search", json={"allowed": True}, headers=auth_header(token)
)
assert granted.status_code == 200, granted.text
assert granted.json()["allowed"] is True
revoked = await client.put(
f"{base}/knowledge.search", json={"allowed": False}, headers=auth_header(token)
)
assert revoked.status_code == 200, revoked.text
assert revoked.json()["allowed"] is False
async def test_tools_catalog_requires_no_secrets(client: AsyncClient):
"""The catalog describes capabilities; it must not carry endpoints or keys."""
token = (await signup(client, "wf-catalog@example.com"))["access_token"]
org_id = await _org(client, token)
response = await client.get(
f"/api/organizations/{org_id}/tools", headers=auth_header(token)
)
assert response.status_code == 200, response.text
payload = json.dumps(response.json()).lower()
for forbidden in ("api_key", "secret", "token", "password"):
assert forbidden not in payload
# ---------------------------------------------------------------------------
# Tasks and runs
# ---------------------------------------------------------------------------
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"]
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),
)
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",
json={"employee_id": employee["id"]},
headers=auth_header(token),
)
assert run.status_code in (200, 201), run.text
run_body = run.json()
assert run_body["task_id"] == task_id
assert run_body["status"] in {"queued", "planning", "running", "completed"}
assert len(run_body["steps"]) >= 1, "a run must record at least one step"
assert [s["index"] for s in run_body["steps"]] == sorted(
s["index"] for s in run_body["steps"]
)
async def test_run_payload_never_leaks_model_identity(client: AsyncClient):
token = (await signup(client, "wf-runsecret@example.com"))["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 = task.json()["id"]
run = await client.post(
f"/api/organizations/{org_id}/tasks/{task_id}/runs",
json={"employee_id": employee["id"]},
headers=auth_header(token),
)
payload = json.dumps(run.json()).lower()
for forbidden in ("provider", "api_key", "internal_name", "model"):
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"]
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 = task.json()["id"]
run = await client.post(
f"/api/organizations/{org_id}/tasks/{task_id}/runs",
json={"employee_id": employee["id"]},
headers=auth_header(token),
)
assert run.status_code == 201, run.text
detail = await client.get(
f"/api/organizations/{org_id}/tasks/{task_id}", headers=auth_header(token)
)
assert detail.status_code == 200, detail.text
runs = detail.json()["runs"]
assert len(runs) == 1
assert runs[0]["id"] == run.json()["id"]
assert runs[0]["task_id"] == task_id
async def test_a_closed_task_cannot_be_run_again(client: AsyncClient):
"""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"]
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 = task.json()["id"]
first = await client.post(
f"/api/organizations/{org_id}/tasks/{task_id}/runs",
json={"employee_id": employee["id"]},
headers=auth_header(token),
)
assert first.status_code == 201, first.text
assert first.json()["status"] == "completed", first.text
second = await client.post(
f"/api/organizations/{org_id}/tasks/{task_id}/runs",
json={"employee_id": employee["id"]},
headers=auth_header(token),
)
assert second.status_code == 409, second.text
# ---------------------------------------------------------------------------
# Tenant isolation
# ---------------------------------------------------------------------------
async def test_employee_of_another_tenant_is_not_readable(client: AsyncClient):
token_a = (await signup(client, "wf-iso-a@example.com"))["access_token"]
token_b = (await signup(client, "wf-iso-b@example.com"))["access_token"]
org_a = await _org(client, token_a, "سازمان الف")
employee = await _employee(client, token_a, org_a)
# B is authenticated and knows the employee id, but has no membership in A.
response = await client.get(
f"/api/organizations/{org_a}/employees/{employee['id']}", headers=auth_header(token_b)
)
# 404, not 403: the API must not confirm that the employee exists.
assert response.status_code == 404, response.text
async def test_task_of_another_tenant_is_not_readable(client: AsyncClient):
token_a = (await signup(client, "wf-tiso-a@example.com"))["access_token"]
token_b = (await signup(client, "wf-tiso-b@example.com"))["access_token"]
org_a = await _org(client, token_a, "سازمان ب")
task = await client.post(
f"/api/organizations/{org_a}/tasks",
json={"title": "کار محرمانهٔ الف"},
headers=auth_header(token_a),
)
task_id = task.json()["id"]
response = await client.get(
f"/api/organizations/{org_a}/tasks/{task_id}", headers=auth_header(token_b)
)
assert response.status_code == 404, response.text
async def test_pending_approvals_are_org_scoped(client: AsyncClient):
"""A tenant must never see another tenant's approval queue."""
token_a = (await signup(client, "wf-appr-a@example.com"))["access_token"]
token_b = (await signup(client, "wf-appr-b@example.com"))["access_token"]
org_a = await _org(client, token_a, "سازمان پ")
org_b = await _org(client, token_b, "سازمان ت")
response = await client.get(
f"/api/organizations/{org_b}/approvals/pending", headers=auth_header(token_a)
)
# Not a member of B at all.
assert response.status_code == 404, response.text
own = await client.get(
f"/api/organizations/{org_a}/approvals/pending", headers=auth_header(token_a)
)
assert own.status_code == 200, own.text
assert own.json() == []