From 1541948e76775c33a8d4ea5f5166773db70e6295 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vitor=20Hugo=20Belorio=20Sim=C3=A3o?= Date: Thu, 26 Mar 2026 12:04:49 -0300 Subject: [PATCH] =?UTF-8?q?=F0=9F=A7=A9=20feat(shared):=20definir=20contra?= =?UTF-8?q?tos=20e=20deploy=20entre=20product=20e=20admin?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 50 +++++- .../orquestrador-admin.service.example | 20 +++ .../orquestrador-product.service.example | 20 +++ .../independent-deploy-strategy.md | 128 +++++++++++++++ .../architecture/monorepo-target-structure.md | 147 ++++++++++++++++++ .../shared-contracts-and-access-hierarchy.md | 94 +++++++++++ shared/__init__.py | 1 + shared/contracts/README.md | 27 ++++ shared/contracts/__init__.py | 31 ++++ shared/contracts/access_control.py | 88 +++++++++++ shared/contracts/tool_publication.py | 59 +++++++ tests/test_shared_contracts.py | 82 ++++++++++ 12 files changed, 744 insertions(+), 3 deletions(-) create mode 100644 deploy/systemd/orquestrador-admin.service.example create mode 100644 deploy/systemd/orquestrador-product.service.example create mode 100644 docs/architecture/independent-deploy-strategy.md create mode 100644 docs/architecture/monorepo-target-structure.md create mode 100644 docs/architecture/shared-contracts-and-access-hierarchy.md create mode 100644 shared/__init__.py create mode 100644 shared/contracts/README.md create mode 100644 shared/contracts/__init__.py create mode 100644 shared/contracts/access_control.py create mode 100644 shared/contracts/tool_publication.py create mode 100644 tests/test_shared_contracts.py diff --git a/README.md b/README.md index 9287c35..33ff038 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,6 @@ Capacidades de negocio ja implementadas: - consultar frota de aluguel - abrir locacao - registrar pagamento de aluguel -- registrar multa de aluguel - registrar devolucao de aluguel - responder consultas informativas sobre o aluguel atual, como contrato, placa, diaria, pagamento e data de devolucao @@ -87,7 +86,7 @@ Importante: ## Estrutura do Projeto ```text -app/ +app/ # runtime atual do produto main.py api/ schemas.py @@ -151,16 +150,56 @@ app/ user/ mock_customer_service.py user_service.py + +admin_app/ # novo runtime administrativo + app_factory.py + main.py + __main__.py + api/ + dependencies.py + router.py + routes/ + system.py + core/ + settings.py + db/ + models/ + repositories/ + services/ + +shared/ # contratos compartilhados entre servicos + contracts/ + access_control.py + tool_publication.py + scripts/ list_integration_deliveries.py list_integration_routes.py process_integration_deliveries.py upsert_integration_route.py stress_smoke.py + +docs/ + adr/ + architecture/ + tests/ ... ``` +## Monorepo Em Evolucao + +A partir da frente de auto-incremento, o repositorio passa a evoluir como monorepo com dois servicos: + +- `app/`: runtime de produto e atendimento +- `admin_app/`: runtime administrativo para auth interna, configuracao, relatorios e governanca de tools +- `shared/`: contratos compartilhados entre os dois servicos + +Nesta fase, o produto continua rodando a partir de `app/` sem migracao de import path. +A estrutura alvo detalhada esta em [docs/architecture/monorepo-target-structure.md](docs/architecture/monorepo-target-structure.md). +A hierarquia inicial de acesso e os primeiros contratos entre servicos estao em [docs/architecture/shared-contracts-and-access-hierarchy.md](docs/architecture/shared-contracts-and-access-hierarchy.md). +A estrategia de deploy independente entre os dois runtimes esta em [docs/architecture/independent-deploy-strategy.md](docs/architecture/independent-deploy-strategy.md). + ## Tools Disponiveis As definicoes padrao ficam em [app/db/tool_seed.py](app/db/tool_seed.py): @@ -180,7 +219,6 @@ As definicoes padrao ficam em [app/db/tool_seed.py](app/db/tool_seed.py): | `consultar_frota_aluguel` | lista frota disponivel para locacao | | `abrir_locacao_aluguel` | abre contrato de locacao | | `registrar_pagamento_aluguel` | registra pagamento vinculado ao contrato | -| `registrar_multa_aluguel` | registra multa vinculada ao contrato | | `registrar_devolucao_aluguel` | encerra locacao e calcula valor final | | `limpar_contexto_conversa` | zera contexto e fila | | `continuar_proximo_pedido` | retoma o proximo item da fila | @@ -398,6 +436,11 @@ No modelo atual: - o bootstrap pode ser rodado manualmente ou por uma unit separada; - em producao, Redis deve estar disponivel para o estado conversacional. +Para a topologia alvo com dois servicos, veja tambem: +- [docs/architecture/independent-deploy-strategy.md](docs/architecture/independent-deploy-strategy.md) +- [deploy/systemd/orquestrador-product.service.example](deploy/systemd/orquestrador-product.service.example) +- [deploy/systemd/orquestrador-admin.service.example](deploy/systemd/orquestrador-admin.service.example) + ## Arquivos Uteis - [TEST_CASES.md](TEST_CASES.md) @@ -413,3 +456,4 @@ Os proximos ganhos mais valiosos para o projeto sao: - evoluir a trilha de auditoria e consultas operacionais - criar uma camada de avaliacao semantica e replay de conversas - integrar o orquestrador com sistemas reais de operacao + diff --git a/deploy/systemd/orquestrador-admin.service.example b/deploy/systemd/orquestrador-admin.service.example new file mode 100644 index 0000000..4d69b42 --- /dev/null +++ b/deploy/systemd/orquestrador-admin.service.example @@ -0,0 +1,20 @@ +[Unit] +Description=AI Orquestrador Admin Runtime +After=network.target + +[Service] +Type=simple +User=vitor +Group=vitor +WorkingDirectory=/opt/orquestrador +EnvironmentFile=/opt/orquestrador/.env.admin +Environment=PATH=/opt/orquestrador/venv/bin +ExecStart=/opt/orquestrador/venv/bin/python -m uvicorn admin_app.main:app --host 127.0.0.1 --port 8081 +Restart=always +RestartSec=5 + +NoNewPrivileges=true +PrivateTmp=true + +[Install] +WantedBy=multi-user.target diff --git a/deploy/systemd/orquestrador-product.service.example b/deploy/systemd/orquestrador-product.service.example new file mode 100644 index 0000000..d72ee9e --- /dev/null +++ b/deploy/systemd/orquestrador-product.service.example @@ -0,0 +1,20 @@ +[Unit] +Description=AI Orquestrador Product Runtime +After=network.target + +[Service] +Type=simple +User=vitor +Group=vitor +WorkingDirectory=/opt/orquestrador +EnvironmentFile=/opt/orquestrador/.env.product +Environment=PATH=/opt/orquestrador/venv/bin +ExecStart=/opt/orquestrador/venv/bin/python -m app.integrations.telegram_satellite_service +Restart=always +RestartSec=5 + +NoNewPrivileges=true +PrivateTmp=true + +[Install] +WantedBy=multi-user.target diff --git a/docs/architecture/independent-deploy-strategy.md b/docs/architecture/independent-deploy-strategy.md new file mode 100644 index 0000000..e9e4bf9 --- /dev/null +++ b/docs/architecture/independent-deploy-strategy.md @@ -0,0 +1,128 @@ +# Estrategia De Deploy Independente Para Product E Admin + +Este documento define a estrategia de deploy para manter `orquestrador-product` +e `orquestrador-admin` como servicos distintos, porem ligados. + +## Objetivo + +Permitir que: + +- o atendimento continue estavel mesmo se o admin estiver fora do ar +- o admin evolua com login, painel, relatorios e geracao de tools sem impactar o hot path +- os dois servicos possam ser versionados e publicados com cadencias diferentes + +## Servico de produto + +Nome operacional sugerido: + +- `orquestrador-product` + +Responsabilidades: + +- Telegram +- orquestracao +- execucao de tools publicadas +- regras operacionais do atendimento + +Unit `systemd` sugerida: + +- `deploy/systemd/orquestrador-product.service.example` + +## Servico administrativo + +Nome operacional sugerido: + +- `orquestrador-admin` + +Responsabilidades: + +- autenticacao interna +- painel administrativo +- relatorios +- configuracao do sistema +- geracao, validacao, aprovacao e publicacao de tools + +Unit `systemd` sugerida: + +- `deploy/systemd/orquestrador-admin.service.example` + +## Principios + +1. Deploy do `admin` nao deve exigir restart do `product`. +2. Deploy do `product` nao deve depender do `admin` estar online. +3. Mudancas em `shared/contracts` devem ser compativeis para frente e para tras durante a janela de rollout. +4. O `product` consome somente estado publicado e aprovado. +5. O `admin` nao entra no hot path do atendimento. + +## Configuracao de ambiente + +Sugestao de arquivos distintos: + +- `.env.product` +- `.env.admin` + +### `product` + +Mantem: + +- Vertex do atendimento +- Redis do estado conversacional +- Telegram +- bancos do runtime operacional + +### `admin` + +Mantem: + +- credenciais do painel interno +- banco administrativo +- modelo de geracao de codigo +- configuracoes de relatorios e publicacao + +## Estrategia de rollout + +### Mudancas so no admin + +1. publicar codigo do `admin` +2. atualizar dependencias do `admin` +3. reiniciar apenas `orquestrador-admin` + +### Mudancas so no product + +1. publicar codigo do `product` +2. atualizar dependencias do `product` +3. reiniciar apenas `orquestrador-product` + +### Mudancas em contratos compartilhados + +1. publicar contrato novo de forma aditiva +2. subir primeiro o servico consumidor mais tolerante +3. subir o outro servico depois +4. so remover campos antigos numa fase posterior + +## Banco e publicacao de estado + +Nesta fase, a estrategia recomendada e: + +- `admin` grava seus proprios metadados e artefatos +- `product` consome somente dados publicados e estaveis +- nenhuma dependencia sincrona do `product` para consultar `admin` em tempo de atendimento + +## Observabilidade + +Cada servico deve ter: + +- logs proprios +- unit `systemd` propria +- variaveis de ambiente proprias +- healthcheck proprio + +## Situacao atual + +Hoje o runtime real em producao ainda e o de `product`. + +Esta estrategia ja prepara o caminho para: + +- manter o deploy atual do produto +- introduzir o `admin` como segundo servico +- fazer a transicao sem mover `app/` agora diff --git a/docs/architecture/monorepo-target-structure.md b/docs/architecture/monorepo-target-structure.md new file mode 100644 index 0000000..48ab962 --- /dev/null +++ b/docs/architecture/monorepo-target-structure.md @@ -0,0 +1,147 @@ +# Estrutura Alvo do Monorepo + +Este documento define a estrutura alvo do monorepo apos a decisao de separar o runtime de produto do servico administrativo. + +## Objetivo +Manter dois servicos distintos, mas ligados: + +- `orquestrador-product`: atendimento e operacao do produto +- `orquestrador-admin`: autenticacao interna, configuracao, relatorios e governanca de tools + +A prioridade desta fase e estrutural: +- preservar o runtime atual do produto +- introduzir o scaffold do servico administrativo +- criar um lugar claro para contratos compartilhados + +## Decisao de transicao +Nesta etapa, o codigo atual do produto permanece em `app/`. +Nao vamos mover o runtime de produto agora para evitar churn desnecessario, quebra de import e risco operacional. + +Portanto, a estrutura final de curto e medio prazo fica assim: + +```text +app/ # runtime atual do produto +admin_app/ # novo runtime administrativo +shared/ # contratos e artefatos compartilhados entre servicos + +docs/ + adr/ + architecture/ + +deploy/ + systemd/ + # evoluira para suportar servicos distintos +``` + +## Estrutura do servico de produto +O servico de produto continua centralizado em `app/`. + +Responsabilidades: +- atendimento conversacional +- integracoes com canais externos +- orquestracao em tempo de execucao +- leitura de tools publicadas +- regras operacionais de vendas, revisao e locacao + +## Estrutura do servico administrativo +O servico administrativo passa a nascer em `admin_app/`. + +Estrutura alvo inicial: + +```text +admin_app/ + app_factory.py + main.py + __main__.py + api/ + dependencies.py + router.py + routes/ + system.py + # auth.py + # staff_accounts.py + # tool_drafts.py + # reports.py + core/ + settings.py + # security.py + db/ + models/ + # staff_account.py + # tool_draft.py + # tool_generation_job.py + # tool_publication.py + # audit_log.py + repositories/ + # staff_account_repository.py + # tool_draft_repository.py + services/ + # auth_service.py + # tool_draft_service.py + # report_service.py +``` + +## Estrutura compartilhada +Tudo que for contrato entre servicos deve ficar em `shared/`. + +Regras: +- `shared/` nao deve conter regra de negocio de atendimento +- `shared/` nao deve conter dependencias do hot path do Telegram +- `shared/` deve armazenar apenas contratos, DTOs, enums, nomes de eventos e utilitarios realmente compartilhados + +Estrutura inicial: + +```text +shared/ + contracts/ + access_control.py + tool_publication.py + # settings_snapshot.py + # report_filters.py +``` + +## Regras de organizacao do monorepo + +1. `app/` continua sendo o produto ate eventual migracao planejada. +2. `admin_app/` nasce isolado e nao deve importar modulos internos de atendimento por conveniencia. +3. `shared/` e o unico lugar recomendado para contratos reutilizados por ambos os servicos. +4. O servico administrativo nao deve depender do runtime do Telegram para inicializar. +5. O servico de produto nao deve depender do servico administrativo no hot path. +6. Hierarquia de acesso administrativa deve nascer em shared/contracts/access_control.py. + +## Import boundaries + +### Permitido +- `admin_app` importar `shared` +- `app` importar `shared` + +### Nao permitido +- `admin_app` importar `app.services.orchestration` para executar atendimento +- `app` importar `admin_app.services` no fluxo de atendimento +- colocar metadados administrativos dentro de `app/services/orchestration` + +## Estrategia de evolucao + +### Fase atual +- criar scaffold do `admin_app` +- criar `shared/` +- documentar a topologia do monorepo + +### Fase seguinte +- implementar `StaffAccount` +- criar auth administrativa +- subir primeiras rotas internas no `admin_app` + +### Fase posterior +- publicar contratos compartilhados em `shared/` +- plugar pipeline de drafts, validacao e publicacao de tools + +## Impacto em deploy +Por enquanto, o deploy atual do produto permanece como esta. +Quando o admin ganhar runtime real, o deploy vai evoluir para dois servicos distintos: + +- `orquestrador-product` +- `orquestrador-admin` + +Sem mover o runtime atual de `app/` nesta etapa. + diff --git a/docs/architecture/shared-contracts-and-access-hierarchy.md b/docs/architecture/shared-contracts-and-access-hierarchy.md new file mode 100644 index 0000000..ac4c94c --- /dev/null +++ b/docs/architecture/shared-contracts-and-access-hierarchy.md @@ -0,0 +1,94 @@ +# Contratos Compartilhados E Hierarquia De Acesso + +Este documento define os primeiros contratos compartilhados entre `orquestrador-product` +e `orquestrador-admin`, com foco especial na hierarquia de acesso do runtime administrativo. + +## Objetivo + +Criar uma base comum para: + +- autenticacao e autorizacao administrativa +- publicacao de tools do `admin` para o `product` +- evolucao independente dos dois servicos sem acoplamento indevido + +## Hierarquia inicial de acesso + +Os papeis administrativos ficam centralizados em `shared/contracts/access_control.py`. + +Hierarquia: + +1. `viewer` +2. `staff` +3. `admin` + +### `viewer` + +Responsavel por leitura operacional. + +Permissoes iniciais: + +- `view_system` +- `view_reports` +- `view_audit_logs` + +### `staff` + +Responsavel por operacao interna e governanca de drafts. + +Permissoes iniciais: + +- todas as de `viewer` +- `manage_tool_drafts` +- `review_tool_generations` + +### `admin` + +Responsavel por configuracao, publicacao e gestao de acesso. + +Permissoes iniciais: + +- todas as de `staff` +- `publish_tools` +- `manage_settings` +- `manage_staff_accounts` + +## Regras de desenho + +1. Os papeis nascem em contrato compartilhado para que `admin` e `product` falem a mesma lingua. +2. O `product` nao usa essa hierarquia para atendimento ao cliente final. +3. O `admin` usa essa hierarquia para autenticacao, autorizacao e auditoria. +4. Toda evolucao deve ser additive-first para nao bloquear deploy independente. + +## Contrato de publicacao de tool + +O contrato inicial fica em `shared/contracts/tool_publication.py`. + +Ele cobre: + +- `ServiceName` +- `ToolLifecycleStatus` +- `ToolParameterType` +- `ToolParameterContract` +- `PublishedToolContract` +- `ToolPublicationEnvelope` + +## Como isso sera usado depois + +### No `orquestrador-admin` + +- criar `StaffAccount` +- associar `StaffAccount.role` +- controlar acesso a UI, as rotas e a aprovacao de tools +- emitir `ToolPublicationEnvelope` quando uma tool for publicada + +### No `orquestrador-product` + +- consumir apenas tools publicadas +- validar status e versao do contrato recebido +- evitar dependencia do runtime do admin no hot path + +## Proximos passos naturais + +- criar a entidade `StaffAccount` +- plugar a role do usuario interno ao contrato compartilhado +- modelar a persistencia de drafts/publicacoes de tool diff --git a/shared/__init__.py b/shared/__init__.py new file mode 100644 index 0000000..eb8230f --- /dev/null +++ b/shared/__init__.py @@ -0,0 +1 @@ +"""Contrato compartilhado entre servicos do monorepo.""" diff --git a/shared/contracts/README.md b/shared/contracts/README.md new file mode 100644 index 0000000..84df72d --- /dev/null +++ b/shared/contracts/README.md @@ -0,0 +1,27 @@ +# Shared Contracts + +Esta pasta existe para concentrar contratos e artefatos compartilhados entre: + +- `app/` (produto) +- `admin_app/` (administrativo) + +Ela nao deve receber regra de negocio do atendimento nem codigo acoplado ao hot path do produto. + +## Contratos iniciais + +Nesta fase, os primeiros contratos compartilhados sao: + +- `access_control.py` + - define a hierarquia inicial de acesso interno + - papeis: `viewer`, `staff`, `admin` + - permissoes iniciais para relatorios, configuracao, revisao e publicacao + +- `tool_publication.py` + - define o contrato minimo de publicacao de tools do `admin` para o `product` + - inclui envelope de publicacao, status de ciclo de vida e schema de parametros + +## Regras + +- `shared/contracts` deve guardar apenas contratos estaveis entre servicos +- nada aqui deve importar modulos internos de `app/` ou `admin_app/` +- as mudancas devem ser additive-first para permitir deploy independente entre `product` e `admin` diff --git a/shared/contracts/__init__.py b/shared/contracts/__init__.py new file mode 100644 index 0000000..d376904 --- /dev/null +++ b/shared/contracts/__init__.py @@ -0,0 +1,31 @@ +"""Contratos compartilhados entre product e admin.""" + +from shared.contracts.access_control import ( + AdminPermission, + StaffRole, + permissions_for_role, + role_has_permission, + role_includes, +) +from shared.contracts.tool_publication import ( + PublishedToolContract, + ServiceName, + ToolLifecycleStatus, + ToolParameterContract, + ToolParameterType, + ToolPublicationEnvelope, +) + +__all__ = [ + "AdminPermission", + "PublishedToolContract", + "ServiceName", + "StaffRole", + "ToolLifecycleStatus", + "ToolParameterContract", + "ToolParameterType", + "ToolPublicationEnvelope", + "permissions_for_role", + "role_has_permission", + "role_includes", +] diff --git a/shared/contracts/access_control.py b/shared/contracts/access_control.py new file mode 100644 index 0000000..136dcbd --- /dev/null +++ b/shared/contracts/access_control.py @@ -0,0 +1,88 @@ +from __future__ import annotations + +from enum import Enum + + +class StaffRole(str, Enum): + VIEWER = "viewer" + STAFF = "staff" + ADMIN = "admin" + + +class AdminPermission(str, Enum): + VIEW_SYSTEM = "view_system" + VIEW_REPORTS = "view_reports" + VIEW_AUDIT_LOGS = "view_audit_logs" + MANAGE_TOOL_DRAFTS = "manage_tool_drafts" + REVIEW_TOOL_GENERATIONS = "review_tool_generations" + PUBLISH_TOOLS = "publish_tools" + MANAGE_SETTINGS = "manage_settings" + MANAGE_STAFF_ACCOUNTS = "manage_staff_accounts" + + +_ROLE_HIERARCHY = { + StaffRole.VIEWER: 10, + StaffRole.STAFF: 20, + StaffRole.ADMIN: 30, +} + +_ROLE_PERMISSIONS = { + StaffRole.VIEWER: frozenset( + { + AdminPermission.VIEW_SYSTEM, + AdminPermission.VIEW_REPORTS, + AdminPermission.VIEW_AUDIT_LOGS, + } + ), + StaffRole.STAFF: frozenset( + { + AdminPermission.VIEW_SYSTEM, + AdminPermission.VIEW_REPORTS, + AdminPermission.VIEW_AUDIT_LOGS, + AdminPermission.MANAGE_TOOL_DRAFTS, + AdminPermission.REVIEW_TOOL_GENERATIONS, + } + ), + StaffRole.ADMIN: frozenset( + { + AdminPermission.VIEW_SYSTEM, + AdminPermission.VIEW_REPORTS, + AdminPermission.VIEW_AUDIT_LOGS, + AdminPermission.MANAGE_TOOL_DRAFTS, + AdminPermission.REVIEW_TOOL_GENERATIONS, + AdminPermission.PUBLISH_TOOLS, + AdminPermission.MANAGE_SETTINGS, + AdminPermission.MANAGE_STAFF_ACCOUNTS, + } + ), +} + + +def normalize_staff_role(role: StaffRole | str) -> StaffRole: + if isinstance(role, StaffRole): + return role + return StaffRole(str(role).strip().lower()) + + +def normalize_admin_permission(permission: AdminPermission | str) -> AdminPermission: + if isinstance(permission, AdminPermission): + return permission + return AdminPermission(str(permission).strip().lower()) + + +def permissions_for_role(role: StaffRole | str) -> frozenset[AdminPermission]: + normalized_role = normalize_staff_role(role) + return _ROLE_PERMISSIONS[normalized_role] + + +def role_includes(role: StaffRole | str, minimum_role: StaffRole | str) -> bool: + normalized_role = normalize_staff_role(role) + normalized_minimum = normalize_staff_role(minimum_role) + return _ROLE_HIERARCHY[normalized_role] >= _ROLE_HIERARCHY[normalized_minimum] + + +def role_has_permission( + role: StaffRole | str, permission: AdminPermission | str +) -> bool: + normalized_permission = normalize_admin_permission(permission) + return normalized_permission in permissions_for_role(role) diff --git a/shared/contracts/tool_publication.py b/shared/contracts/tool_publication.py new file mode 100644 index 0000000..31214a2 --- /dev/null +++ b/shared/contracts/tool_publication.py @@ -0,0 +1,59 @@ +from __future__ import annotations + +from datetime import datetime +from enum import Enum + +from pydantic import BaseModel, Field + + +class ServiceName(str, Enum): + PRODUCT = "product" + ADMIN = "admin" + + +class ToolLifecycleStatus(str, Enum): + DRAFT = "draft" + GENERATED = "generated" + VALIDATED = "validated" + APPROVED = "approved" + ACTIVE = "active" + FAILED = "failed" + ARCHIVED = "archived" + + +class ToolParameterType(str, Enum): + STRING = "string" + INTEGER = "integer" + NUMBER = "number" + BOOLEAN = "boolean" + OBJECT = "object" + ARRAY = "array" + + +class ToolParameterContract(BaseModel): + name: str + parameter_type: ToolParameterType + description: str + required: bool = True + + +class PublishedToolContract(BaseModel): + tool_name: str + display_name: str + description: str + version: int = Field(ge=1) + status: ToolLifecycleStatus + parameters: tuple[ToolParameterContract, ...] = () + implementation_module: str + implementation_callable: str + checksum: str | None = None + published_at: datetime | None = None + published_by: str | None = None + + +class ToolPublicationEnvelope(BaseModel): + source_service: ServiceName = ServiceName.ADMIN + target_service: ServiceName = ServiceName.PRODUCT + publication_id: str + published_tool: PublishedToolContract + emitted_at: datetime diff --git a/tests/test_shared_contracts.py b/tests/test_shared_contracts.py new file mode 100644 index 0000000..1a23921 --- /dev/null +++ b/tests/test_shared_contracts.py @@ -0,0 +1,82 @@ +import unittest +from datetime import datetime, timezone + +from shared.contracts import ( + AdminPermission, + PublishedToolContract, + ServiceName, + StaffRole, + ToolLifecycleStatus, + ToolParameterContract, + ToolParameterType, + ToolPublicationEnvelope, + permissions_for_role, + role_has_permission, + role_includes, +) + + +class AccessControlContractTests(unittest.TestCase): + def test_role_hierarchy_is_ordered(self): + self.assertTrue(role_includes(StaffRole.ADMIN, StaffRole.STAFF)) + self.assertTrue(role_includes(StaffRole.STAFF, StaffRole.VIEWER)) + self.assertFalse(role_includes(StaffRole.VIEWER, StaffRole.ADMIN)) + + def test_permissions_are_inherited_by_higher_roles(self): + self.assertIn( + AdminPermission.VIEW_REPORTS, + permissions_for_role(StaffRole.VIEWER), + ) + self.assertTrue( + role_has_permission(StaffRole.STAFF, AdminPermission.MANAGE_TOOL_DRAFTS) + ) + self.assertTrue( + role_has_permission(StaffRole.ADMIN, AdminPermission.MANAGE_SETTINGS) + ) + self.assertFalse( + role_has_permission(StaffRole.VIEWER, AdminPermission.PUBLISH_TOOLS) + ) + + +class ToolPublicationContractTests(unittest.TestCase): + def test_tool_publication_envelope_is_built_with_shared_contract(self): + published_tool = PublishedToolContract( + tool_name="consultar_financiamento", + display_name="Consultar Financiamento", + description="Consulta opcoes de financiamento.", + version=2, + status=ToolLifecycleStatus.APPROVED, + parameters=( + ToolParameterContract( + name="valor_veiculo", + parameter_type=ToolParameterType.NUMBER, + description="Valor do veiculo em reais.", + required=True, + ), + ), + implementation_module="generated_tools.consultar_financiamento", + implementation_callable="run", + checksum="sha256:abc123", + published_at=datetime(2026, 3, 26, 12, 0, tzinfo=timezone.utc), + published_by="staff:1", + ) + + envelope = ToolPublicationEnvelope( + source_service=ServiceName.ADMIN, + target_service=ServiceName.PRODUCT, + publication_id="pub_001", + published_tool=published_tool, + emitted_at=datetime(2026, 3, 26, 12, 1, tzinfo=timezone.utc), + ) + + self.assertEqual(envelope.source_service, ServiceName.ADMIN) + self.assertEqual(envelope.target_service, ServiceName.PRODUCT) + self.assertEqual(envelope.published_tool.tool_name, "consultar_financiamento") + self.assertEqual( + envelope.published_tool.parameters[0].parameter_type, + ToolParameterType.NUMBER, + ) + + +if __name__ == "__main__": + unittest.main()