You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
319 lines
14 KiB
Python
319 lines
14 KiB
Python
import unittest
|
|
from datetime import datetime, timezone
|
|
|
|
from fastapi.testclient import TestClient
|
|
|
|
from admin_app.api.dependencies import get_current_staff_principal, get_tool_management_service
|
|
from admin_app.app_factory import create_app
|
|
from admin_app.core import AdminSettings, AuthenticatedStaffPrincipal
|
|
from admin_app.db.models import ToolDraft, ToolVersion
|
|
from admin_app.services import ToolManagementService
|
|
from shared.contracts import StaffRole, ToolLifecycleStatus
|
|
|
|
|
|
class _FakeToolDraftRepository:
|
|
def __init__(self):
|
|
self.drafts: list[ToolDraft] = []
|
|
self.next_id = 1
|
|
|
|
def list_drafts(self, *, statuses=None) -> list[ToolDraft]:
|
|
drafts = sorted(
|
|
self.drafts,
|
|
key=lambda draft: draft.updated_at or draft.created_at or datetime.min.replace(tzinfo=timezone.utc),
|
|
reverse=True,
|
|
)
|
|
if statuses:
|
|
allowed = set(statuses)
|
|
drafts = [draft for draft in drafts if draft.status in allowed]
|
|
return drafts
|
|
|
|
def get_by_tool_name(self, tool_name: str) -> ToolDraft | None:
|
|
normalized = str(tool_name or "").strip().lower()
|
|
for draft in self.drafts:
|
|
if draft.tool_name == normalized:
|
|
return draft
|
|
return None
|
|
|
|
def create(self, **kwargs) -> ToolDraft:
|
|
now = datetime(2026, 3, 31, 16, 0, tzinfo=timezone.utc)
|
|
draft = ToolDraft(
|
|
id=self.next_id,
|
|
draft_id=f"draft_api_{self.next_id}",
|
|
created_at=now,
|
|
updated_at=now,
|
|
status=ToolLifecycleStatus.DRAFT,
|
|
**kwargs,
|
|
)
|
|
self.next_id += 1
|
|
self.drafts.append(draft)
|
|
return draft
|
|
|
|
def update_submission(self, draft: ToolDraft, **kwargs) -> ToolDraft:
|
|
draft.display_name = kwargs["display_name"]
|
|
draft.domain = kwargs["domain"]
|
|
draft.description = kwargs["description"]
|
|
draft.business_goal = kwargs["business_goal"]
|
|
draft.summary = kwargs["summary"]
|
|
draft.parameters_json = kwargs["parameters_json"]
|
|
draft.required_parameter_count = kwargs["required_parameter_count"]
|
|
draft.current_version_number = kwargs["current_version_number"]
|
|
draft.version_count = kwargs["version_count"]
|
|
draft.requires_director_approval = kwargs["requires_director_approval"]
|
|
draft.owner_staff_account_id = kwargs["owner_staff_account_id"]
|
|
draft.owner_display_name = kwargs["owner_display_name"]
|
|
draft.updated_at = datetime(2026, 3, 31, 16, draft.current_version_number, tzinfo=timezone.utc)
|
|
return draft
|
|
|
|
|
|
class _FakeToolVersionRepository:
|
|
def __init__(self):
|
|
self.versions: list[ToolVersion] = []
|
|
self.next_id = 1
|
|
|
|
def list_versions(self, *, tool_name=None, draft_id=None, statuses=None) -> list[ToolVersion]:
|
|
versions = sorted(
|
|
self.versions,
|
|
key=lambda version: (version.version_number, version.updated_at or version.created_at or datetime.min.replace(tzinfo=timezone.utc)),
|
|
reverse=True,
|
|
)
|
|
if tool_name:
|
|
normalized = str(tool_name).strip().lower()
|
|
versions = [version for version in versions if version.tool_name == normalized]
|
|
if draft_id is not None:
|
|
versions = [version for version in versions if version.draft_id == draft_id]
|
|
if statuses:
|
|
allowed = set(statuses)
|
|
versions = [version for version in versions if version.status in allowed]
|
|
return versions
|
|
|
|
def get_next_version_number(self, tool_name: str) -> int:
|
|
versions = self.list_versions(tool_name=tool_name)
|
|
return (versions[0].version_number if versions else 0) + 1
|
|
|
|
def create(self, **kwargs) -> ToolVersion:
|
|
version_number = kwargs["version_number"]
|
|
now = datetime(2026, 3, 31, 17, version_number, tzinfo=timezone.utc)
|
|
version = ToolVersion(
|
|
id=self.next_id,
|
|
version_id=self.build_version_id(kwargs["tool_name"], version_number),
|
|
created_at=now,
|
|
updated_at=now,
|
|
**kwargs,
|
|
)
|
|
self.next_id += 1
|
|
self.versions.append(version)
|
|
return version
|
|
|
|
@staticmethod
|
|
def build_version_id(tool_name: str, version_number: int) -> str:
|
|
normalized = str(tool_name or "").strip().lower()
|
|
return f"tool_version::{normalized}::v{int(version_number)}"
|
|
|
|
|
|
class AdminToolsWebTests(unittest.TestCase):
|
|
def _build_client_with_role(
|
|
self,
|
|
role: StaffRole,
|
|
settings: AdminSettings | None = None,
|
|
) -> tuple[TestClient, object, _FakeToolDraftRepository, _FakeToolVersionRepository]:
|
|
app = create_app(
|
|
settings
|
|
or AdminSettings(
|
|
admin_auth_token_secret="test-secret",
|
|
admin_api_prefix="/admin",
|
|
)
|
|
)
|
|
draft_repository = _FakeToolDraftRepository()
|
|
version_repository = _FakeToolVersionRepository()
|
|
service = ToolManagementService(
|
|
settings=app.state.admin_settings,
|
|
draft_repository=draft_repository,
|
|
version_repository=version_repository,
|
|
)
|
|
app.dependency_overrides[get_current_staff_principal] = lambda: AuthenticatedStaffPrincipal(
|
|
id=11,
|
|
email="colaborador@empresa.com" if role == StaffRole.COLABORADOR else "diretor@empresa.com",
|
|
display_name="Equipe de Tools",
|
|
role=role,
|
|
is_active=True,
|
|
)
|
|
app.dependency_overrides[get_tool_management_service] = lambda: service
|
|
return TestClient(app), app, draft_repository, version_repository
|
|
|
|
def test_tools_overview_returns_metrics_workflow_and_actions_for_colaborador(self):
|
|
client, app, _, _ = self._build_client_with_role(StaffRole.COLABORADOR)
|
|
try:
|
|
response = client.get("/admin/tools/overview", headers={"Authorization": "Bearer token"})
|
|
finally:
|
|
app.dependency_overrides.clear()
|
|
|
|
self.assertEqual(response.status_code, 200)
|
|
payload = response.json()
|
|
self.assertEqual(payload["service"], "orquestrador-admin")
|
|
self.assertEqual(payload["mode"], "admin_tool_draft_governance")
|
|
self.assertEqual(payload["metrics"][0]["value"], "18")
|
|
self.assertIn("persisted_versions", [item["key"] for item in payload["metrics"]])
|
|
self.assertIn("active", [item["code"] for item in payload["workflow"]])
|
|
self.assertIn("/admin/tools/contracts", [item["href"] for item in payload["actions"]])
|
|
self.assertIn("/admin/tools/drafts/intake", [item["href"] for item in payload["actions"]])
|
|
self.assertIn("artefatos", payload["next_steps"][0].lower())
|
|
|
|
def test_tools_contracts_return_shared_contract_snapshot(self):
|
|
client, app, _, _ = self._build_client_with_role(StaffRole.COLABORADOR)
|
|
try:
|
|
response = client.get("/admin/tools/contracts", headers={"Authorization": "Bearer token"})
|
|
finally:
|
|
app.dependency_overrides.clear()
|
|
|
|
self.assertEqual(response.status_code, 200)
|
|
payload = response.json()
|
|
self.assertEqual(payload["publication_source_service"], "admin")
|
|
self.assertEqual(payload["publication_target_service"], "product")
|
|
self.assertIn("draft", [item["code"] for item in payload["lifecycle_statuses"]])
|
|
self.assertEqual(payload["lifecycle_statuses"][0]["order"], 1)
|
|
self.assertFalse(payload["lifecycle_statuses"][0]["terminal"])
|
|
self.assertTrue(payload["lifecycle_statuses"][-1]["terminal"])
|
|
self.assertIn("string", [item["code"] for item in payload["parameter_types"]])
|
|
self.assertIn("published_tool", payload["publication_fields"])
|
|
|
|
def test_tools_drafts_return_single_root_draft_with_current_version_after_reintake(self):
|
|
client, app, _, _ = self._build_client_with_role(StaffRole.COLABORADOR)
|
|
try:
|
|
first_response = client.post(
|
|
"/admin/tools/drafts/intake",
|
|
headers={"Authorization": "Bearer token"},
|
|
json={
|
|
"domain": "vendas",
|
|
"tool_name": "consultar_resumo_financeiro",
|
|
"display_name": "Consultar resumo financeiro",
|
|
"description": "Consulta o resumo financeiro consolidado para analise do time administrativo.",
|
|
"business_goal": "Ajudar a equipe interna a priorizar a leitura dos principais indicadores financeiros.",
|
|
"parameters": [],
|
|
},
|
|
)
|
|
second_response = client.post(
|
|
"/admin/tools/drafts/intake",
|
|
headers={"Authorization": "Bearer token"},
|
|
json={
|
|
"domain": "vendas",
|
|
"tool_name": "consultar_resumo_financeiro",
|
|
"display_name": "Consultar resumo financeiro",
|
|
"description": "Consulta o resumo financeiro consolidado com detalhamento adicional para analise administrativa.",
|
|
"business_goal": "Ajudar a equipe interna a priorizar indicadores financeiros com contexto extra e leitura mais acionavel.",
|
|
"parameters": [],
|
|
},
|
|
)
|
|
response = client.get("/admin/tools/drafts", headers={"Authorization": "Bearer token"})
|
|
finally:
|
|
app.dependency_overrides.clear()
|
|
|
|
self.assertEqual(first_response.status_code, 200)
|
|
self.assertEqual(second_response.status_code, 200)
|
|
self.assertEqual(response.status_code, 200)
|
|
payload = response.json()
|
|
self.assertEqual(payload["storage_status"], "admin_database")
|
|
self.assertEqual(len(payload["drafts"]), 1)
|
|
self.assertEqual(payload["drafts"][0]["tool_name"], "consultar_resumo_financeiro")
|
|
self.assertEqual(payload["drafts"][0]["current_version_number"], 2)
|
|
self.assertEqual(payload["drafts"][0]["version_count"], 2)
|
|
self.assertEqual(payload["supported_statuses"], ["draft"])
|
|
|
|
def test_tools_draft_intake_persists_admin_draft_with_version_metadata(self):
|
|
client, app, draft_repository, version_repository = self._build_client_with_role(StaffRole.COLABORADOR)
|
|
try:
|
|
response = client.post(
|
|
"/admin/tools/drafts/intake",
|
|
headers={"Authorization": "Bearer token"},
|
|
json={
|
|
"domain": "orquestracao",
|
|
"tool_name": "priorizar_contato_quente",
|
|
"display_name": "Priorizar contato quente",
|
|
"description": "Classifica contatos mais quentes para orientar o proximo passo do atendimento.",
|
|
"business_goal": "Dar mais foco comercial ao time interno ao identificar oportunidades mais urgentes.",
|
|
"parameters": [
|
|
{
|
|
"name": "score_interesse",
|
|
"parameter_type": "number",
|
|
"description": "Pontuacao atual de interesse do lead.",
|
|
"required": True,
|
|
}
|
|
],
|
|
},
|
|
)
|
|
finally:
|
|
app.dependency_overrides.clear()
|
|
|
|
self.assertEqual(response.status_code, 200)
|
|
payload = response.json()
|
|
self.assertEqual(payload["storage_status"], "admin_database")
|
|
self.assertEqual(payload["draft_preview"]["status"], "draft")
|
|
self.assertEqual(payload["draft_preview"]["domain"], "orquestracao")
|
|
self.assertEqual(payload["draft_preview"]["version_id"], "tool_version::priorizar_contato_quente::v1")
|
|
self.assertEqual(payload["draft_preview"]["version_number"], 1)
|
|
self.assertEqual(payload["draft_preview"]["version_count"], 1)
|
|
self.assertTrue(payload["draft_preview"]["requires_director_approval"])
|
|
self.assertEqual(payload["draft_preview"]["owner_name"], "Equipe de Tools")
|
|
self.assertGreaterEqual(len(payload["warnings"]), 1)
|
|
self.assertEqual(len(draft_repository.drafts), 1)
|
|
self.assertEqual(len(version_repository.versions), 1)
|
|
|
|
def test_tools_review_queue_requires_director_review_permission(self):
|
|
client, app, _, _ = self._build_client_with_role(StaffRole.COLABORADOR)
|
|
try:
|
|
response = client.get("/admin/tools/review-queue", headers={"Authorization": "Bearer token"})
|
|
finally:
|
|
app.dependency_overrides.clear()
|
|
|
|
self.assertEqual(response.status_code, 403)
|
|
self.assertEqual(
|
|
response.json()["detail"],
|
|
"Permissao administrativa insuficiente: 'review_tool_generations'.",
|
|
)
|
|
|
|
def test_tools_review_queue_is_available_for_diretor(self):
|
|
client, app, _, _ = self._build_client_with_role(StaffRole.DIRETOR)
|
|
try:
|
|
response = client.get("/admin/tools/review-queue", headers={"Authorization": "Bearer token"})
|
|
finally:
|
|
app.dependency_overrides.clear()
|
|
|
|
self.assertEqual(response.status_code, 200)
|
|
payload = response.json()
|
|
self.assertEqual(payload["queue_mode"], "bootstrap_empty_state")
|
|
self.assertEqual(payload["items"], [])
|
|
self.assertIn("validated", payload["supported_statuses"])
|
|
|
|
def test_tools_publications_require_director_publication_permission(self):
|
|
client, app, _, _ = self._build_client_with_role(StaffRole.COLABORADOR)
|
|
try:
|
|
response = client.get("/admin/tools/publications", headers={"Authorization": "Bearer token"})
|
|
finally:
|
|
app.dependency_overrides.clear()
|
|
|
|
self.assertEqual(response.status_code, 403)
|
|
self.assertEqual(
|
|
response.json()["detail"],
|
|
"Permissao administrativa insuficiente: 'publish_tools'.",
|
|
)
|
|
|
|
def test_tools_publications_return_bootstrap_catalog_for_diretor(self):
|
|
client, app, _, _ = self._build_client_with_role(StaffRole.DIRETOR)
|
|
try:
|
|
response = client.get("/admin/tools/publications", headers={"Authorization": "Bearer token"})
|
|
finally:
|
|
app.dependency_overrides.clear()
|
|
|
|
self.assertEqual(response.status_code, 200)
|
|
payload = response.json()
|
|
self.assertEqual(payload["source"], "bootstrap_catalog")
|
|
self.assertEqual(payload["target_service"], "product")
|
|
self.assertGreaterEqual(len(payload["publications"]), 10)
|
|
self.assertIn("consultar_estoque", [item["tool_name"] for item in payload["publications"]])
|
|
first = payload["publications"][0]
|
|
self.assertEqual(first["status"], "active")
|
|
self.assertEqual(first["implementation_module"], "app.services.tools.handlers")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|