import asyncio import unittest from unittest.mock import patch from datetime import datetime, timezone from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker from admin_app.core import AdminSettings from admin_app.db.database import AdminBase from admin_app.db.models import StaffAccount, ToolArtifact, ToolDraft, ToolMetadata, ToolVersion from admin_app.db.models.tool_artifact import ToolArtifactKind from admin_app.repositories.tool_artifact_repository import ToolArtifactRepository from admin_app.repositories.tool_draft_repository import ToolDraftRepository from admin_app.repositories.tool_metadata_repository import ToolMetadataRepository from admin_app.repositories.tool_version_repository import ToolVersionRepository from admin_app.services.tool_management_service import ToolManagementService from shared.contracts import ( AdminPermission, GENERATED_TOOL_ENTRYPOINT, GENERATED_TOOLS_PACKAGE, StaffRole, ToolLifecycleStatus, ToolParameterType, build_generated_tool_module_name, build_generated_tool_module_path, ) 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, *, tool_name: str, display_name: str, domain: str, description: str, business_goal: str, summary: str, parameters_json: list[dict], required_parameter_count: int, current_version_number: int, version_count: int, owner_staff_account_id: int, owner_display_name: str, generation_model: str | None = None, requires_director_approval: bool = True, commit: bool = True, ) -> ToolDraft: now = datetime(2026, 3, 31, 15, 0, tzinfo=timezone.utc) draft = ToolDraft( id=self.next_id, draft_id=f"draft_fake_{self.next_id}", tool_name=tool_name, display_name=display_name, domain=domain, description=description, business_goal=business_goal, status=ToolLifecycleStatus.DRAFT, summary=summary, parameters_json=parameters_json, required_parameter_count=required_parameter_count, current_version_number=current_version_number, version_count=version_count, generation_model=generation_model, requires_director_approval=requires_director_approval, owner_staff_account_id=owner_staff_account_id, owner_display_name=owner_display_name, created_at=now, updated_at=now, ) self.next_id += 1 self.drafts.append(draft) return draft def update_submission( self, draft: ToolDraft, *, display_name: str, domain: str, description: str, business_goal: str, summary: str, parameters_json: list[dict], required_parameter_count: int, current_version_number: int, version_count: int, owner_staff_account_id: int, owner_display_name: str, generation_model: str | None = None, requires_director_approval: bool = True, commit: bool = True, ) -> ToolDraft: draft.display_name = display_name draft.domain = domain draft.description = description draft.business_goal = business_goal draft.status = ToolLifecycleStatus.DRAFT draft.summary = summary draft.parameters_json = parameters_json draft.required_parameter_count = required_parameter_count draft.current_version_number = current_version_number draft.version_count = version_count draft.generation_model = generation_model draft.requires_director_approval = requires_director_approval draft.owner_staff_account_id = owner_staff_account_id draft.owner_display_name = owner_display_name draft.updated_at = datetime(2026, 3, 31, 15, current_version_number, tzinfo=timezone.utc) return draft def update_status(self, draft: ToolDraft, *, status: ToolLifecycleStatus, commit: bool = True) -> ToolDraft: draft.status = status draft.updated_at = datetime(2026, 3, 31, 15, draft.current_version_number, 30, 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 get_by_version_id(self, version_id: str) -> ToolVersion | None: normalized = str(version_id or "").strip().lower() for version in self.versions: if version.version_id == normalized: return version return None def create( self, *, draft_id: int, tool_name: str, version_number: int, summary: str, description: str, business_goal: str, parameters_json: list[dict], required_parameter_count: int, owner_staff_account_id: int, owner_display_name: str, generation_model: str | None = None, status: ToolLifecycleStatus = ToolLifecycleStatus.DRAFT, requires_director_approval: bool = True, commit: bool = True, ) -> ToolVersion: now = datetime(2026, 3, 31, 16, version_number, tzinfo=timezone.utc) version = ToolVersion( id=self.next_id, version_id=self.build_version_id(tool_name, version_number), draft_id=draft_id, tool_name=tool_name, version_number=version_number, status=status, summary=summary, description=description, business_goal=business_goal, parameters_json=parameters_json, required_parameter_count=required_parameter_count, generation_model=generation_model, requires_director_approval=requires_director_approval, owner_staff_account_id=owner_staff_account_id, owner_display_name=owner_display_name, created_at=now, updated_at=now, ) self.next_id += 1 self.versions.append(version) return version def update_status(self, version: ToolVersion, *, status: ToolLifecycleStatus, commit: bool = True) -> ToolVersion: version.status = status version.updated_at = datetime(2026, 3, 31, 16, version.version_number, 30, tzinfo=timezone.utc) 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 _FakeToolMetadataRepository: def __init__(self): self.metadata_entries: list[ToolMetadata] = [] self.next_id = 1 def list_metadata(self, *, tool_name=None, statuses=None) -> list[ToolMetadata]: metadata_entries = sorted( self.metadata_entries, key=lambda metadata: ( metadata.version_number, metadata.updated_at or metadata.created_at or datetime.min.replace(tzinfo=timezone.utc), ), reverse=True, ) if tool_name: normalized = str(tool_name).strip().lower() metadata_entries = [metadata for metadata in metadata_entries if metadata.tool_name == normalized] if statuses: allowed = set(statuses) metadata_entries = [metadata for metadata in metadata_entries if metadata.status in allowed] return metadata_entries def get_by_tool_version_id(self, tool_version_id: int) -> ToolMetadata | None: for metadata in self.metadata_entries: if metadata.tool_version_id == tool_version_id: return metadata return None def create(self, **kwargs) -> ToolMetadata: version_number = kwargs["version_number"] now = datetime(2026, 3, 31, 17, version_number, tzinfo=timezone.utc) metadata = ToolMetadata( id=self.next_id, metadata_id=self.build_metadata_id(kwargs["tool_name"], version_number), created_at=now, updated_at=now, **kwargs, ) self.next_id += 1 self.metadata_entries.append(metadata) return metadata def update_metadata(self, metadata: ToolMetadata, **kwargs) -> ToolMetadata: metadata.display_name = kwargs["display_name"] metadata.domain = kwargs["domain"] metadata.description = kwargs["description"] metadata.parameters_json = kwargs["parameters_json"] metadata.status = kwargs["status"] metadata.author_staff_account_id = kwargs["author_staff_account_id"] metadata.author_display_name = kwargs["author_display_name"] metadata.updated_at = datetime(2026, 3, 31, 17, metadata.version_number, tzinfo=timezone.utc) return metadata def update_status(self, metadata: ToolMetadata, *, status: ToolLifecycleStatus, commit: bool = True) -> ToolMetadata: metadata.status = status metadata.updated_at = datetime(2026, 3, 31, 17, metadata.version_number, 30, tzinfo=timezone.utc) return metadata def upsert_version_metadata(self, **kwargs) -> ToolMetadata: existing = self.get_by_tool_version_id(kwargs["tool_version_id"]) if existing is None: return self.create(**kwargs) return self.update_metadata(existing, **kwargs) @staticmethod def build_metadata_id(tool_name: str, version_number: int) -> str: normalized = str(tool_name or "").strip().lower() return f"tool_metadata::{normalized}::v{int(version_number)}" class _FakeToolArtifactRepository: def __init__(self): self.artifacts: list[ToolArtifact] = [] self.next_id = 1 def list_artifacts(self, *, tool_name=None, tool_version_id=None, artifact_stage=None, artifact_kind=None) -> list[ToolArtifact]: artifacts = sorted( self.artifacts, key=lambda artifact: ( artifact.version_number, artifact.updated_at or artifact.created_at or datetime.min.replace(tzinfo=timezone.utc), ), reverse=True, ) if tool_name: normalized = str(tool_name).strip().lower() artifacts = [artifact for artifact in artifacts if artifact.tool_name == normalized] if tool_version_id is not None: artifacts = [artifact for artifact in artifacts if artifact.tool_version_id == tool_version_id] if artifact_stage: artifacts = [artifact for artifact in artifacts if artifact.artifact_stage == artifact_stage] if artifact_kind: artifacts = [artifact for artifact in artifacts if artifact.artifact_kind == artifact_kind] return artifacts def get_by_tool_version_and_kind(self, tool_version_id: int, artifact_kind) -> ToolArtifact | None: for artifact in self.artifacts: if artifact.tool_version_id == tool_version_id and artifact.artifact_kind == artifact_kind: return artifact return None def create(self, **kwargs) -> ToolArtifact: version_number = kwargs["version_number"] now = datetime(2026, 3, 31, 18, version_number, self.next_id, tzinfo=timezone.utc) artifact = ToolArtifact( id=self.next_id, artifact_id=self.build_artifact_id(kwargs["tool_name"], version_number, kwargs["artifact_kind"]), created_at=now, updated_at=now, checksum=kwargs.get("checksum") or f"fake-checksum-{self.next_id}", **kwargs, ) self.next_id += 1 self.artifacts.append(artifact) return artifact def update_artifact(self, artifact: ToolArtifact, **kwargs) -> ToolArtifact: artifact.artifact_status = kwargs["artifact_status"] artifact.storage_kind = kwargs.get("storage_kind", artifact.storage_kind) artifact.summary = kwargs["summary"] artifact.payload_json = kwargs["payload_json"] artifact.checksum = kwargs.get("checksum", artifact.checksum) artifact.author_staff_account_id = kwargs["author_staff_account_id"] artifact.author_display_name = kwargs["author_display_name"] artifact.updated_at = datetime(2026, 3, 31, 18, artifact.version_number, tzinfo=timezone.utc) return artifact def upsert_version_artifact(self, **kwargs) -> ToolArtifact: existing = self.get_by_tool_version_and_kind(kwargs["tool_version_id"], kwargs["artifact_kind"]) if existing is None: return self.create(**kwargs) return self.update_artifact(existing, **kwargs) @staticmethod def build_artifact_id(tool_name: str, version_number: int, artifact_kind) -> str: normalized = str(tool_name or "").strip().lower() return f"tool_artifact::{normalized}::v{int(version_number)}::{artifact_kind.value}" class _FailingToolArtifactRepository(ToolArtifactRepository): def __init__(self, db, *, fail_on_call: int): super().__init__(db) self.fail_on_call = fail_on_call self.calls = 0 def upsert_version_artifact(self, **kwargs): self.calls += 1 if self.calls == self.fail_on_call: raise RuntimeError("artifact persistence failure") return super().upsert_version_artifact(**kwargs) class AdminToolManagementServiceTests(unittest.TestCase): def setUp(self): self.draft_repository = _FakeToolDraftRepository() self.version_repository = _FakeToolVersionRepository() self.metadata_repository = _FakeToolMetadataRepository() self.artifact_repository = _FakeToolArtifactRepository() self.service = ToolManagementService( settings=AdminSettings(admin_api_prefix="/admin"), draft_repository=self.draft_repository, version_repository=self.version_repository, metadata_repository=self.metadata_repository, artifact_repository=self.artifact_repository, ) def _run_async(self, awaitable): return asyncio.run(awaitable) def test_create_draft_submission_persists_initial_tool_version_metadata_and_artifacts(self): payload = self.service.create_draft_submission( { "domain": "vendas", "tool_name": "consultar_vendas_periodo", "display_name": "Consultar vendas por periodo", "description": "Consulta vendas consolidadas por periodo informado no painel.", "business_goal": "Ajudar o time interno a acompanhar o desempenho comercial com mais agilidade.", "generation_model": "gemini-2.5-pro", "parameters": [ { "name": "periodo_inicio", "parameter_type": "string", "description": "Data inicial usada no filtro.", "required": True, }, { "name": "periodo_fim", "parameter_type": "string", "description": "Data final usada no filtro.", "required": True, }, ], }, owner_staff_account_id=7, owner_name="Equipe Interna", owner_role=StaffRole.COLABORADOR, ) self.assertEqual(payload["storage_status"], "admin_database") self.assertEqual(payload["submission_policy"]["mode"], "draft_only") self.assertEqual(payload["submission_policy"]["submitter_role"], StaffRole.COLABORADOR) self.assertFalse(payload["submission_policy"]["submitter_can_publish_now"]) self.assertTrue(payload["submission_policy"]["direct_publication_blocked"]) self.assertEqual(payload["submission_policy"]["required_approver_role"], StaffRole.DIRETOR) self.assertEqual(payload["submission_policy"]["required_publish_permission"], AdminPermission.PUBLISH_TOOLS) self.assertEqual(payload["draft_preview"]["draft_id"], "draft_fake_1") self.assertEqual(payload["draft_preview"]["version_id"], "tool_version::consultar_vendas_periodo::v1") self.assertEqual(payload["draft_preview"]["version_number"], 1) self.assertEqual(payload["draft_preview"]["generation_model"], "gemini-2.5-pro") self.assertEqual(payload["draft_preview"]["version_count"], 1) self.assertEqual(payload["draft_preview"]["status"], ToolLifecycleStatus.DRAFT) self.assertEqual(payload["draft_preview"]["owner_name"], "Equipe Interna") self.assertEqual(len(self.draft_repository.drafts), 1) self.assertEqual(len(self.version_repository.versions), 1) self.assertEqual(self.draft_repository.drafts[0].generation_model, "gemini-2.5-pro") self.assertEqual(self.version_repository.versions[0].generation_model, "gemini-2.5-pro") self.assertEqual(len(self.metadata_repository.metadata_entries), 1) self.assertEqual(self.metadata_repository.metadata_entries[0].author_display_name, "Equipe Interna") self.assertEqual(self.metadata_repository.metadata_entries[0].version_number, 1) self.assertEqual(len(self.artifact_repository.artifacts), 2) artifacts_by_kind = { artifact.artifact_kind: artifact for artifact in self.artifact_repository.artifacts } generation_artifact = artifacts_by_kind[ToolArtifactKind.GENERATION_REQUEST] validation_artifact = artifacts_by_kind[ToolArtifactKind.VALIDATION_REPORT] self.assertEqual(generation_artifact.tool_name, "consultar_vendas_periodo") self.assertEqual(generation_artifact.payload_json["target_package"], GENERATED_TOOLS_PACKAGE) self.assertEqual( generation_artifact.payload_json["target_module"], build_generated_tool_module_name("consultar_vendas_periodo"), ) self.assertEqual( generation_artifact.payload_json["target_file_path"], build_generated_tool_module_path("consultar_vendas_periodo"), ) self.assertEqual(generation_artifact.payload_json["target_callable"], GENERATED_TOOL_ENTRYPOINT) self.assertEqual(generation_artifact.payload_json["reserved_lifecycle_target"], "generated") self.assertEqual(generation_artifact.payload_json["parameters"][0]["name"], "periodo_inicio") self.assertEqual(validation_artifact.payload_json["validation_status"], "passed") self.assertEqual(validation_artifact.payload_json["parameter_count"], 2) self.assertEqual(validation_artifact.author_display_name, "Equipe Interna") self.assertTrue(generation_artifact.checksum) self.assertTrue(validation_artifact.checksum) def test_create_draft_submission_blocks_tool_name_reserved_by_core_catalog(self): with self.assertRaisesRegex(ValueError, "catalogo core do sistema"): self.service.create_draft_submission( { "domain": "vendas", "tool_name": "consultar_estoque", "display_name": "Consultar estoque paralelo", "description": "Tentativa de sobrescrever a tool core de estoque com uma versao administrativa.", "business_goal": "Validar que o admin nao permite reutilizar nomes reservados do runtime core.", "parameters": [], }, owner_staff_account_id=11, owner_name="Equipe Interna", ) self.assertEqual(len(self.draft_repository.drafts), 0) self.assertEqual(len(self.version_repository.versions), 0) self.assertEqual(len(self.metadata_repository.metadata_entries), 0) self.assertEqual(len(self.artifact_repository.artifacts), 0) def test_create_draft_submission_reuses_root_draft_and_increments_version(self): self.service.create_draft_submission( { "domain": "locacao", "tool_name": "emitir_resumo_locacao", "display_name": "Emitir resumo de locacao", "description": "Resume o contrato atual de locacao para consulta administrativa.", "business_goal": "Dar visibilidade rapida ao status do contrato e dos dados principais.", "parameters": [], }, owner_staff_account_id=3, owner_name="Analista de Locacao", ) payload = self.service.create_draft_submission( { "domain": "locacao", "tool_name": "emitir_resumo_locacao", "display_name": "Emitir resumo de locacao", "description": "Resume o contrato atual de locacao e os principais eventos administrativos.", "business_goal": "Dar visibilidade rapida ao status do contrato, do pagamento e dos dados principais.", "parameters": [ { "name": "contrato_id", "parameter_type": "string", "description": "Identificador do contrato consultado.", "required": True, } ], }, owner_staff_account_id=4, owner_name="Coordenacao de Locacao", ) self.assertEqual(payload["draft_preview"]["version_id"], "tool_version::emitir_resumo_locacao::v2") self.assertEqual(payload["draft_preview"]["version_number"], 2) self.assertEqual(payload["draft_preview"]["version_count"], 2) self.assertEqual(len(self.draft_repository.drafts), 1) self.assertEqual(len(self.version_repository.versions), 2) self.assertEqual(len(self.metadata_repository.metadata_entries), 2) self.assertEqual(len(self.artifact_repository.artifacts), 6) self.assertEqual(self.draft_repository.drafts[0].current_version_number, 2) self.assertEqual(self.draft_repository.drafts[0].version_count, 2) self.assertEqual(self.draft_repository.drafts[0].owner_display_name, "Coordenacao de Locacao") def test_build_drafts_payload_returns_versioned_draft_summaries(self): self.service.create_draft_submission( { "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": [], }, owner_staff_account_id=5, owner_name="Equipe de Tools", ) self.service.create_draft_submission( { "domain": "orquestracao", "tool_name": "priorizar_contato_quente", "display_name": "Priorizar contato quente", "description": "Classifica contatos mais quentes com sinais adicionais para orientar o atendimento.", "business_goal": "Dar mais foco comercial ao time interno ao identificar oportunidades quentes com mais contexto.", "parameters": [], }, owner_staff_account_id=6, owner_name="Diretoria Comercial", ) payload = self.service.build_drafts_payload() self.assertEqual(payload["storage_status"], "admin_database") self.assertEqual(len(payload["drafts"]), 1) self.assertEqual(payload["drafts"][0]["tool_name"], "priorizar_contato_quente") self.assertEqual(payload["drafts"][0]["current_version_number"], 2) self.assertEqual(payload["drafts"][0]["version_count"], 2) self.assertEqual(payload["drafts"][0]["owner_name"], "Diretoria Comercial") self.assertEqual(payload["supported_statuses"], [ToolLifecycleStatus.DRAFT]) def test_build_publications_payload_keeps_bootstrap_catalog_for_draft_metadata(self): self.service.create_draft_submission( { "domain": "revisao", "tool_name": "consultar_revisao_aberta", "display_name": "Consultar revisao aberta", "description": "Consulta revisoes abertas com filtros administrativos para a oficina.", "business_goal": "Ajudar o time a localizar revisoes abertas com mais contexto operacional.", "parameters": [ { "name": "placa", "parameter_type": "string", "description": "Placa usada na busca da revisao.", "required": True, } ], }, owner_staff_account_id=8, owner_name="Operacao de Oficina", ) payload = self.service.build_publications_payload() self.assertEqual(payload["source"], "bootstrap_catalog") self.assertEqual(payload["target_service"], "product") self.assertGreaterEqual(len(payload["publications"]), 10) self.assertNotIn("consultar_revisao_aberta", [item["tool_name"] for item in payload["publications"]]) def test_build_publications_payload_merges_active_governed_publications_with_bootstrap_catalog(self): self.service.create_draft_submission( { "domain": "revisao", "tool_name": "consultar_revisao_aberta", "display_name": "Consultar revisao aberta", "description": "Consulta revisoes abertas com filtros administrativos para a oficina.", "business_goal": "Ajudar o time a localizar revisoes abertas com mais contexto operacional.", "parameters": [ { "name": "placa", "parameter_type": "string", "description": "Placa usada na busca da revisao.", "required": True, } ], }, owner_staff_account_id=8, owner_name="Operacao de Oficina", ) self.metadata_repository.metadata_entries[0].status = ToolLifecycleStatus.ACTIVE payload = self.service.build_publications_payload() self.assertEqual(payload["source"], "hybrid_runtime_catalog") self.assertEqual(payload["target_service"], "product") self.assertGreaterEqual(len(payload["publications"]), 11) self.assertIn("consultar_estoque", [item["tool_name"] for item in payload["publications"]]) publication = next( item for item in payload["publications"] if item["tool_name"] == "consultar_revisao_aberta" ) self.assertEqual(publication["publication_id"], "tool_metadata::consultar_revisao_aberta::v1") self.assertEqual(publication["version"], 1) self.assertEqual(publication["status"], ToolLifecycleStatus.ACTIVE) self.assertEqual(publication["author_name"], "Operacao de Oficina") self.assertEqual(publication["implementation_module"], build_generated_tool_module_name("consultar_revisao_aberta")) self.assertEqual(publication["implementation_callable"], GENERATED_TOOL_ENTRYPOINT) self.assertEqual(publication["parameter_count"], 1) self.assertEqual(publication["parameters"][0]["parameter_type"], ToolParameterType.STRING) def test_build_review_queue_payload_returns_latest_version_pending_director_action(self): intake_payload = self.service.create_draft_submission( { "domain": "revisao", "tool_name": "consultar_revisao_aberta", "display_name": "Consultar revisao aberta", "description": "Consulta revisoes abertas com filtros administrativos para a oficina.", "business_goal": "Ajudar o time a localizar revisoes abertas com mais contexto operacional.", "parameters": [ { "name": "placa", "parameter_type": "string", "description": "Placa usada na busca da revisao.", "required": True, } ], }, owner_staff_account_id=8, owner_name="Operacao de Oficina", ) payload = self.service.build_review_queue_payload() self.assertEqual(payload["queue_mode"], "governed_admin_queue") self.assertEqual(len(payload["items"]), 1) self.assertEqual(payload["items"][0]["version_id"], intake_payload["draft_preview"]["version_id"]) self.assertEqual(payload["items"][0]["version_number"], 1) self.assertEqual(payload["items"][0]["status"], ToolLifecycleStatus.DRAFT) self.assertEqual(payload["items"][0]["gate"], "generation_pipeline_required") self.assertIn(ToolLifecycleStatus.APPROVED, payload["supported_statuses"]) def test_collaborator_draft_requires_director_authorization_before_generation(self): intake_payload = self.service.create_draft_submission( { "domain": "revisao", "tool_name": "consultar_revisao_aberta", "display_name": "Consultar revisao aberta", "description": "Consulta revisoes abertas com filtros administrativos para a oficina.", "business_goal": "Ajudar o time a localizar revisoes abertas com mais contexto operacional.", "parameters": [], }, owner_staff_account_id=8, owner_name="Operacao de Oficina", owner_role=StaffRole.COLABORADOR, ) payload = self.service.build_review_queue_payload() self.assertEqual(payload["items"][0]["version_id"], intake_payload["draft_preview"]["version_id"]) self.assertEqual(payload["items"][0]["gate"], "generation_decision_required") detail = self.service.build_review_detail_payload(intake_payload["draft_preview"]["version_id"]) self.assertEqual(detail["human_gate"]["current_gate"], "generation_decision_required") self.assertTrue(detail["human_gate"]["authorize_generation_action_available"]) self.assertFalse(detail["human_gate"]["run_pipeline_action_available"]) transition = self.service.authorize_generation( intake_payload["draft_preview"]["version_id"], actor_staff_account_id=99, actor_name="Diretoria", actor_role=StaffRole.DIRETOR, decision_notes="A proposta esta alinhada e pode consumir a etapa de geracao governada.", ) self.assertEqual(transition["status"], ToolLifecycleStatus.DRAFT) self.assertEqual(transition["queue_entry"]["gate"], "generation_pipeline_required") detail_after_authorization = self.service.build_review_detail_payload(intake_payload["draft_preview"]["version_id"]) self.assertTrue(detail_after_authorization["human_gate"]["run_pipeline_action_available"]) def test_request_changes_returns_generated_version_to_draft_with_changes_gate(self): intake_payload = self.service.create_draft_submission( { "domain": "revisao", "tool_name": "consultar_revisao_aberta", "display_name": "Consultar revisao aberta", "description": "Consulta revisoes abertas com filtros administrativos para a oficina.", "business_goal": "Ajudar o time a localizar revisoes abertas com mais contexto operacional.", "parameters": [], }, owner_staff_account_id=8, owner_name="Operacao de Oficina", ) version_id = intake_payload["draft_preview"]["version_id"] self.service.run_generation_pipeline( version_id, runner_staff_account_id=8, runner_name="Operacao de Oficina", runner_role=StaffRole.COLABORADOR, ) payload = self.service.request_changes( version_id, actor_staff_account_id=99, actor_name="Diretoria", actor_role=StaffRole.DIRETOR, decision_notes="A proposta precisa ajustar o codigo e a estrutura antes de nova validacao.", ) self.assertEqual(payload["status"], ToolLifecycleStatus.DRAFT) self.assertEqual(payload["queue_entry"]["gate"], "changes_requested") detail = self.service.build_review_detail_payload(version_id) self.assertEqual(detail["human_gate"]["current_gate"], "changes_requested") self.assertTrue(detail["human_gate"]["run_pipeline_action_available"]) self.assertEqual(detail["decision_history"][-1]["action_key"], ToolArtifactKind.GENERATION_CHANGE_REQUEST.value) def test_close_proposal_archives_generated_version_without_notes(self): intake_payload = self.service.create_draft_submission( { "domain": "revisao", "tool_name": "consultar_revisao_aberta", "display_name": "Consultar revisao aberta", "description": "Consulta revisoes abertas com filtros administrativos para a oficina.", "business_goal": "Ajudar o time a localizar revisoes abertas com mais contexto operacional.", "parameters": [], }, owner_staff_account_id=8, owner_name="Operacao de Oficina", ) version_id = intake_payload["draft_preview"]["version_id"] self.service.run_generation_pipeline( version_id, runner_staff_account_id=8, runner_name="Operacao de Oficina", runner_role=StaffRole.COLABORADOR, ) payload = self.service.close_proposal( version_id, actor_staff_account_id=99, actor_name="Diretoria", actor_role=StaffRole.DIRETOR, decision_notes=None, ) self.assertEqual(payload["status"], ToolLifecycleStatus.ARCHIVED) self.assertEqual(payload["queue_entry"]["gate"], "archived_history") detail = self.service.build_review_detail_payload(version_id) self.assertFalse(detail["human_gate"]["close_proposal_action_available"]) self.assertEqual(detail["decision_history"][-1]["action_key"], ToolArtifactKind.PROPOSAL_CLOSURE.value) self.assertIsNone(detail["decision_history"][-1]["decision_notes"]) def test_rerun_generation_after_change_request_tracks_new_iteration_without_new_version(self): intake_payload = self.service.create_draft_submission( { "domain": "revisao", "tool_name": "consultar_revisao_aberta", "display_name": "Consultar revisao aberta", "description": "Consulta revisoes abertas com filtros administrativos para a oficina.", "business_goal": "Ajudar o time a localizar revisoes abertas com mais contexto operacional.", "parameters": [ { "name": "placa", "parameter_type": "string", "description": "Placa usada na busca da revisao.", "required": True, } ], }, owner_staff_account_id=8, owner_name="Operacao de Oficina", owner_role=StaffRole.COLABORADOR, ) version_id = intake_payload["draft_preview"]["version_id"] version_number = intake_payload["draft_preview"]["version_number"] self.service.authorize_generation( version_id, actor_staff_account_id=99, actor_name="Diretoria", actor_role=StaffRole.DIRETOR, decision_notes="A proposta pode consumir a primeira geracao governada.", ) self.service.run_generation_pipeline( version_id, runner_staff_account_id=8, runner_name="Operacao de Oficina", runner_role=StaffRole.COLABORADOR, ) feedback_notes = "Ajuste o retorno para destacar o resumo operacional e preserve a assinatura atual." self.service.request_changes( version_id, actor_staff_account_id=99, actor_name="Diretoria", actor_role=StaffRole.DIRETOR, decision_notes=feedback_notes, ) second_generation = self.service.run_generation_pipeline( version_id, runner_staff_account_id=8, runner_name="Operacao de Oficina", runner_role=StaffRole.COLABORADOR, ) self.assertEqual(second_generation["version_id"], version_id) self.assertEqual(second_generation["version_number"], version_number) self.assertEqual(second_generation["status"], ToolLifecycleStatus.GENERATED) generation_artifact = next( artifact for artifact in self.artifact_repository.artifacts if artifact.artifact_kind == ToolArtifactKind.GENERATION_REQUEST ) self.assertEqual(generation_artifact.payload_json["generation_iteration"], 2) self.assertEqual(generation_artifact.payload_json["generation_mode"], "change_request_refinement") self.assertEqual(generation_artifact.payload_json["feedback_notes"], feedback_notes) self.assertEqual(len(generation_artifact.payload_json["generation_iterations"]), 2) self.assertEqual( generation_artifact.payload_json["generation_iterations"][-1]["generation_iteration"], 2, ) validation_artifact = next( artifact for artifact in self.artifact_repository.artifacts if artifact.artifact_kind == ToolArtifactKind.VALIDATION_REPORT ) self.assertEqual(validation_artifact.payload_json["generation_iteration"], 2) self.assertEqual(validation_artifact.payload_json["generation_mode"], "change_request_refinement") self.assertEqual(validation_artifact.payload_json["change_request_notes"], feedback_notes) self.assertEqual(len(validation_artifact.payload_json["validation_iterations"]), 2) self.assertEqual( validation_artifact.payload_json["validation_iterations"][-1]["generation_iteration"], 2, ) def test_review_detail_and_runtime_snapshot_recover_legacy_generated_source_from_metadata(self): intake_payload = self.service.create_draft_submission( { "domain": "locacao", "tool_name": "emitir_resumo_locacao", "display_name": "Emitir resumo de locacao", "description": "Resume contratos de locacao com filtros operacionais para o time interno.", "business_goal": "Dar visibilidade rapida aos contratos e aos principais dados da locacao.", "parameters": [ { "name": "contrato_id", "parameter_type": "string", "description": "Identificador do contrato consultado.", "required": True, } ], }, owner_staff_account_id=7, owner_name="Equipe Interna", owner_role=StaffRole.COLABORADOR, ) version_id = intake_payload["draft_preview"]["version_id"] self.service.authorize_generation( version_id, actor_staff_account_id=99, actor_name="Diretoria", actor_role=StaffRole.DIRETOR, decision_notes="A proposta foi autorizada para gerar codigo governado.", ) self.service.run_generation_pipeline( version_id, runner_staff_account_id=7, runner_name="Equipe Interna", runner_role=StaffRole.COLABORADOR, ) self.service.review_version( version_id, reviewer_staff_account_id=99, reviewer_name="Diretoria", reviewer_role=StaffRole.DIRETOR, decision_notes="Revisao humana concluida para publicar a iteracao legada.", reviewed_generated_code=True, ) self.service.approve_version( version_id, approver_staff_account_id=99, approver_name="Diretoria", approver_role=StaffRole.DIRETOR, decision_notes="Aprovacao final registrada para publicacao governada.", ) version = self.version_repository.get_by_version_id(version_id) validation_artifact = self.artifact_repository.get_by_tool_version_and_kind( version.id, ToolArtifactKind.VALIDATION_REPORT, ) generation_artifact = self.artifact_repository.get_by_tool_version_and_kind( version.id, ToolArtifactKind.GENERATION_REQUEST, ) validation_payload = dict(validation_artifact.payload_json or {}) validation_payload.pop("generated_source_code", None) validation_payload.pop("generated_source_checksum", None) validation_artifact.payload_json = validation_payload generation_payload = dict(generation_artifact.payload_json or {}) generation_payload.pop("generation_iterations", None) generation_payload.pop("latest_generation", None) generation_payload.pop("generation_mode", None) generation_artifact.payload_json = generation_payload detail = self.service.build_review_detail_payload(version_id) self.assertIn("async def run", detail["generated_source_code"]) self.assertEqual(detail["generation_context"]["latest_generation_mode"], "legacy_generation") self.assertTrue(self.service._version_has_generated_source(version_id)) import shutil from pathlib import Path sandbox_root = Path.cwd() / ".tmp_test_admin_legacy_runtime_snapshot" shutil.rmtree(sandbox_root, ignore_errors=True) package_dir = sandbox_root / GENERATED_TOOLS_PACKAGE manifest_path = package_dir / "published_runtime_tools.json" def build_file_path(tool_name: str): return package_dir / f"{tool_name}.py" try: with patch( "admin_app.services.tool_management_service.get_generated_tools_runtime_dir", return_value=package_dir, ), patch( "admin_app.services.tool_management_service.get_generated_tool_publication_manifest_path", return_value=manifest_path, ), patch( "admin_app.services.tool_management_service.build_generated_tool_file_path", side_effect=build_file_path, ): payload = self.service.publish_version( version_id, publisher_staff_account_id=99, publisher_name="Diretoria", publisher_role=StaffRole.DIRETOR, ) self.assertEqual(payload["status"], ToolLifecycleStatus.ACTIVE) self.assertTrue(build_file_path("emitir_resumo_locacao").exists()) self.assertIn( "async def run", build_file_path("emitir_resumo_locacao").read_text(encoding="utf-8"), ) finally: shutil.rmtree(sandbox_root, ignore_errors=True) def test_failed_generation_without_validation_does_not_reconstruct_legacy_source(self): class _FailingGenerationService: async def generate_tool_source(self, **kwargs): return { "generated_source_code": None, "generation_model_used": "gemini-3-pro-preview", "issues": ["Vertex AI indisponivel para gerar o codigo nesta tentativa."], "prompt_rendered": "prompt de geracao com falha", "elapsed_ms": 17.0, } self.service.tool_generation_service = _FailingGenerationService() intake_payload = self.service.create_draft_submission( { "domain": "revisao", "tool_name": "consultar_revisao_aberta", "display_name": "Consultar revisao aberta", "description": "Consulta revisoes abertas com filtros administrativos para a oficina.", "business_goal": "Ajudar o time a localizar revisoes abertas com mais contexto operacional.", "parameters": [], }, owner_staff_account_id=8, owner_name="Operacao de Oficina", owner_role=StaffRole.COLABORADOR, ) version_id = intake_payload["draft_preview"]["version_id"] self.service.authorize_generation( version_id, actor_staff_account_id=99, actor_name="Diretoria", actor_role=StaffRole.DIRETOR, decision_notes="A proposta pode seguir para geracao governada apos triagem inicial.", ) payload = self.service.run_generation_pipeline( version_id, runner_staff_account_id=8, runner_name="Operacao de Oficina", runner_role=StaffRole.COLABORADOR, ) self.assertEqual(payload["status"], ToolLifecycleStatus.FAILED) detail = self.service.build_review_detail_payload(version_id) self.assertEqual(detail["generated_source_code"], "") self.assertFalse(detail["generation_context"]["has_previous_generation"]) self.assertFalse(self.service._version_has_generated_source(version_id)) version = self.version_repository.get_by_version_id(version_id) with self.assertRaisesRegex(RuntimeError, "codigo gerado"): self.service._get_generated_source_code_for_version(version.id) def test_publish_allows_legacy_single_iteration_review_and_approval_metadata(self): intake_payload = self.service.create_draft_submission( { "domain": "revisao", "tool_name": "consultar_revisao_aberta", "display_name": "Consultar revisao aberta", "description": "Consulta revisoes abertas com filtros administrativos para a oficina.", "business_goal": "Ajudar o time a localizar revisoes abertas com mais contexto operacional.", "parameters": [], }, owner_staff_account_id=99, owner_name="Diretoria", owner_role=StaffRole.DIRETOR, ) version_id = intake_payload["draft_preview"]["version_id"] version = self.version_repository.get_by_version_id(version_id) self.service.run_generation_pipeline( version_id, runner_staff_account_id=99, runner_name="Diretoria", runner_role=StaffRole.DIRETOR, ) self.service.review_version( version_id, reviewer_staff_account_id=99, reviewer_name="Diretoria", reviewer_role=StaffRole.DIRETOR, decision_notes="Revisao humana registrada antes da migracao do contexto de iteracao.", reviewed_generated_code=True, ) self.service.approve_version( version_id, approver_staff_account_id=99, approver_name="Diretoria", approver_role=StaffRole.DIRETOR, decision_notes="Aprovacao humana registrada antes da migracao do contexto de iteracao.", ) generation_artifact = self.artifact_repository.get_by_tool_version_and_kind(version.id, ToolArtifactKind.GENERATION_REQUEST) validation_artifact = self.artifact_repository.get_by_tool_version_and_kind(version.id, ToolArtifactKind.VALIDATION_REPORT) review_artifact = self.artifact_repository.get_by_tool_version_and_kind(version.id, ToolArtifactKind.DIRECTOR_REVIEW) approval_artifact = self.artifact_repository.get_by_tool_version_and_kind(version.id, ToolArtifactKind.DIRECTOR_APPROVAL) generation_payload = dict(generation_artifact.payload_json or {}) generation_payload.pop("generation_iterations", None) generation_payload.pop("latest_generation", None) generation_payload.pop("generation_mode", None) generation_artifact.payload_json = generation_payload validation_payload = dict(validation_artifact.payload_json or {}) validation_payload.pop("validation_iterations", None) validation_payload.pop("latest_validation", None) validation_payload.pop("generation_mode", None) validation_artifact.payload_json = validation_payload review_payload = dict(review_artifact.payload_json or {}) review_payload.pop("reviewed_generation_iteration", None) review_payload.pop("reviewed_generation_mode", None) review_payload.pop("reviewed_generation_checksum", None) review_artifact.payload_json = review_payload approval_payload = dict(approval_artifact.payload_json or {}) approval_payload.pop("approved_generation_iteration", None) approval_payload.pop("approved_generation_mode", None) approval_payload.pop("approved_generation_checksum", None) approval_artifact.payload_json = approval_payload payload = self.service.publish_version( version_id, publisher_staff_account_id=99, publisher_name="Diretoria", publisher_role=StaffRole.DIRETOR, ) self.assertEqual(payload["status"], ToolLifecycleStatus.ACTIVE) self.assertEqual(payload["version_id"], version_id) def test_rollback_allows_legacy_single_iteration_review_and_approval_metadata(self): first_intake = self.service.create_draft_submission( { "domain": "vendas", "tool_name": "consultar_funil_comercial", "display_name": "Consultar funil comercial", "description": "Consulta o funil comercial consolidado para acompanhamento administrativo.", "business_goal": "Dar visibilidade ao time interno sobre os principais gargalos do funil.", "parameters": [], }, owner_staff_account_id=7, owner_name="Equipe Interna", ) first_version_id = first_intake["draft_preview"]["version_id"] self.service.run_generation_pipeline(first_version_id, runner_staff_account_id=7, runner_name="Equipe Interna", runner_role=StaffRole.COLABORADOR) self.service.review_version( first_version_id, reviewer_staff_account_id=99, reviewer_name="Diretoria", reviewer_role=StaffRole.DIRETOR, decision_notes="Primeira versao revisada antes da migracao do contexto de iteracao.", reviewed_generated_code=True, ) self.service.approve_version( first_version_id, approver_staff_account_id=99, approver_name="Diretoria", approver_role=StaffRole.DIRETOR, decision_notes="Primeira versao aprovada antes da migracao do contexto de iteracao.", ) self.service.publish_version(first_version_id, publisher_staff_account_id=99, publisher_name="Diretoria", publisher_role=StaffRole.DIRETOR) first_version = self.version_repository.get_by_version_id(first_version_id) generation_artifact = self.artifact_repository.get_by_tool_version_and_kind(first_version.id, ToolArtifactKind.GENERATION_REQUEST) validation_artifact = self.artifact_repository.get_by_tool_version_and_kind(first_version.id, ToolArtifactKind.VALIDATION_REPORT) review_artifact = self.artifact_repository.get_by_tool_version_and_kind(first_version.id, ToolArtifactKind.DIRECTOR_REVIEW) approval_artifact = self.artifact_repository.get_by_tool_version_and_kind(first_version.id, ToolArtifactKind.DIRECTOR_APPROVAL) generation_payload = dict(generation_artifact.payload_json or {}) generation_payload.pop("generation_iterations", None) generation_payload.pop("latest_generation", None) generation_payload.pop("generation_mode", None) generation_artifact.payload_json = generation_payload validation_payload = dict(validation_artifact.payload_json or {}) validation_payload.pop("validation_iterations", None) validation_payload.pop("latest_validation", None) validation_payload.pop("generation_mode", None) validation_artifact.payload_json = validation_payload review_payload = dict(review_artifact.payload_json or {}) review_payload.pop("reviewed_generation_iteration", None) review_payload.pop("reviewed_generation_mode", None) review_payload.pop("reviewed_generation_checksum", None) review_artifact.payload_json = review_payload approval_payload = dict(approval_artifact.payload_json or {}) approval_payload.pop("approved_generation_iteration", None) approval_payload.pop("approved_generation_mode", None) approval_payload.pop("approved_generation_checksum", None) approval_artifact.payload_json = approval_payload second_intake = self.service.create_draft_submission( { "domain": "vendas", "tool_name": "consultar_funil_comercial", "display_name": "Consultar funil comercial", "description": "Consulta o funil comercial consolidado com novos filtros administrativos.", "business_goal": "Dar visibilidade ao time interno sobre gargalos do funil com cortes adicionais.", "parameters": [], }, owner_staff_account_id=7, owner_name="Equipe Interna", ) second_version_id = second_intake["draft_preview"]["version_id"] self.service.run_generation_pipeline(second_version_id, runner_staff_account_id=7, runner_name="Equipe Interna", runner_role=StaffRole.COLABORADOR) self.service.review_version( second_version_id, reviewer_staff_account_id=99, reviewer_name="Diretoria", reviewer_role=StaffRole.DIRETOR, decision_notes="Segunda versao revisada para substituir a publicacao anterior.", reviewed_generated_code=True, ) self.service.approve_version( second_version_id, approver_staff_account_id=99, approver_name="Diretoria", approver_role=StaffRole.DIRETOR, decision_notes="Segunda versao aprovada para substituir a publicacao anterior.", ) self.service.publish_version(second_version_id, publisher_staff_account_id=99, publisher_name="Diretoria", publisher_role=StaffRole.DIRETOR) payload = self.service.rollback_version( second_version_id, actor_staff_account_id=99, actor_name="Diretoria", actor_role=StaffRole.DIRETOR, decision_notes="Rollback compativel com artefatos legados de revisao e aprovacao.", ) self.assertEqual(payload["status"], ToolLifecycleStatus.ACTIVE) self.assertEqual(payload["version_id"], first_version_id) def test_publish_requires_review_and_approval_for_latest_generation_iteration(self): intake_payload = self.service.create_draft_submission( { "domain": "revisao", "tool_name": "consultar_revisao_aberta", "display_name": "Consultar revisao aberta", "description": "Consulta revisoes abertas com filtros administrativos para a oficina.", "business_goal": "Ajudar o time a localizar revisoes abertas com mais contexto operacional.", "parameters": [ { "name": "placa", "parameter_type": "string", "description": "Placa usada na busca da revisao.", "required": True, } ], }, owner_staff_account_id=99, owner_name="Diretoria", owner_role=StaffRole.DIRETOR, ) version_id = intake_payload["draft_preview"]["version_id"] version = self.version_repository.get_by_version_id(version_id) draft = self.draft_repository.get_by_tool_name("consultar_revisao_aberta") metadata = self.metadata_repository.get_by_tool_version_id(version.id) self.service.run_generation_pipeline( version_id, runner_staff_account_id=99, runner_name="Diretoria", runner_role=StaffRole.DIRETOR, ) review_payload = self.service.review_version( version_id, reviewer_staff_account_id=99, reviewer_name="Diretoria", reviewer_role=StaffRole.DIRETOR, decision_notes="Revisao inicial completa do codigo governado.", reviewed_generated_code=True, ) approve_payload = self.service.approve_version( version_id, approver_staff_account_id=99, approver_name="Diretoria", approver_role=StaffRole.DIRETOR, decision_notes="Aprovacao inicial completa da versao governada.", ) self.assertEqual(review_payload["status"], ToolLifecycleStatus.VALIDATED) self.assertEqual(approve_payload["status"], ToolLifecycleStatus.APPROVED) self.service._persist_generation_pipeline_artifact( draft=draft, version=version, actor_staff_account_id=99, actor_name="Diretoria", actor_role=StaffRole.DIRETOR, llm_generated_source=None, llm_generation_model=None, llm_generation_issues=[], generation_iteration=2, generation_mode="change_request_refinement", feedback_notes="Foi identificada uma nova iteracao tecnica apos a aprovacao inicial.", previous_source_checksum=self.service._compute_source_checksum( self.service._get_generated_source_code_for_version(version.id) ), prompt_rendered="prompt de refatoracao tecnica", generation_elapsed_ms=10.0, commit=None, ) self.service._execute_automated_contract_validation( draft=draft, version=version, metadata=metadata, actor_staff_account_id=99, actor_name="Diretoria", llm_generated_source=None, generation_iteration=2, generation_mode="change_request_refinement", change_request_notes="Foi identificada uma nova iteracao tecnica apos a aprovacao inicial.", previous_source_checksum=self.service._compute_source_checksum( self.service._get_generated_source_code_for_version(version.id) ), commit=None, ) with self.assertRaisesRegex(ValueError, "iteracao mais recente"): self.service.publish_version( version_id, publisher_staff_account_id=99, publisher_name="Diretoria", publisher_role=StaffRole.DIRETOR, ) def test_run_generation_pipeline_promotes_version_to_generated(self): intake_payload = self.service.create_draft_submission( { "domain": "revisao", "tool_name": "consultar_revisao_aberta", "display_name": "Consultar revisao aberta", "description": "Consulta revisoes abertas com filtros administrativos para a oficina.", "business_goal": "Ajudar o time a localizar revisoes abertas com mais contexto operacional.", "parameters": [ { "name": "placa", "parameter_type": "string", "description": "Placa usada na busca da revisao.", "required": True, } ], }, owner_staff_account_id=8, owner_name="Operacao de Oficina", owner_role=StaffRole.COLABORADOR, ) self.service.authorize_generation( intake_payload["draft_preview"]["version_id"], actor_staff_account_id=99, actor_name="Diretoria", actor_role=StaffRole.DIRETOR, decision_notes="A proposta pode seguir para geracao governada apos triagem inicial.", ) payload = self.service.run_generation_pipeline( intake_payload["draft_preview"]["version_id"], runner_staff_account_id=8, runner_name="Operacao de Oficina", runner_role=StaffRole.COLABORADOR, ) self.assertEqual(payload["status"], ToolLifecycleStatus.GENERATED) self.assertEqual(payload["current_step"], "validation") self.assertEqual(payload["queue_entry"]["gate"], "validation_required") self.assertEqual(payload["queue_entry"]["automated_validation_status"], "passed") self.assertEqual(payload["queue_entry"]["automated_validation_summary"], "4/4 validacoes automaticas passaram antes da revisao humana.") self.assertEqual(len(payload["automated_validations"]), 4) automated_checks = {check["key"]: check for check in payload["automated_validations"]} self.assertEqual(automated_checks["tool_contract"]["status"], "passed") self.assertEqual(automated_checks["tool_signature_schema"]["status"], "passed") self.assertEqual(automated_checks["tool_import_loading"]["status"], "passed") self.assertEqual(automated_checks["tool_smoke_tests"]["status"], "passed") steps_by_key = {step["key"]: step for step in payload["steps"]} self.assertEqual(steps_by_key["manual_intake"]["state"], "completed") self.assertEqual(steps_by_key["generation"]["state"], "completed") self.assertEqual(steps_by_key["validation"]["state"], "current") self.assertEqual(self.draft_repository.drafts[0].status, ToolLifecycleStatus.GENERATED) self.assertEqual(self.version_repository.versions[0].status, ToolLifecycleStatus.GENERATED) self.assertEqual(self.metadata_repository.metadata_entries[0].status, ToolLifecycleStatus.GENERATED) generation_artifact = next( artifact for artifact in self.artifact_repository.artifacts if artifact.artifact_kind == ToolArtifactKind.GENERATION_REQUEST ) self.assertEqual(generation_artifact.payload_json["pipeline_status"], "completed") self.assertEqual(generation_artifact.payload_json["triggered_by_role"], StaffRole.COLABORADOR.value) validation_artifact = next( artifact for artifact in self.artifact_repository.artifacts if artifact.artifact_kind == ToolArtifactKind.VALIDATION_REPORT ) self.assertEqual(validation_artifact.artifact_status.value, "succeeded") self.assertEqual(validation_artifact.payload_json["validation_scope"], "tool_contract") automated_checks = {check["key"]: check for check in validation_artifact.payload_json["automated_checks"]} self.assertEqual(automated_checks["tool_contract"]["status"], "passed") self.assertEqual(automated_checks["tool_signature_schema"]["status"], "passed") self.assertEqual(automated_checks["tool_import_loading"]["status"], "passed") self.assertEqual(automated_checks["tool_smoke_tests"]["status"], "passed") self.assertEqual(validation_artifact.payload_json["signature_schema"]["callable_name"], GENERATED_TOOL_ENTRYPOINT) self.assertEqual(validation_artifact.payload_json["signature_schema"]["signature"], "run(*, placa)") self.assertEqual(validation_artifact.payload_json["import_loading"]["loaded_callable"], GENERATED_TOOL_ENTRYPOINT) self.assertEqual(validation_artifact.payload_json["import_loading"]["loaded_signature"], "run(*, placa)") self.assertEqual(validation_artifact.payload_json["smoke_tests"]["direct_result_type"], "dict") self.assertEqual(validation_artifact.payload_json["smoke_tests"]["runtime_result_type"], "dict") self.assertEqual(validation_artifact.payload_json["smoke_tests"]["invocation_arguments"]["placa"], "sample_placa") self.assertEqual(validation_artifact.payload_json["publication_envelope"]["target_service"], "product") def test_run_generation_pipeline_marks_version_failed_when_contract_validation_fails(self): intake_payload = self.service.create_draft_submission( { "domain": "revisao", "tool_name": "consultar_revisao_aberta", "display_name": "Consultar revisao aberta", "description": "Consulta revisoes abertas com filtros administrativos para a oficina.", "business_goal": "Ajudar o time a localizar revisoes abertas com mais contexto operacional.", "parameters": [], }, owner_staff_account_id=8, owner_name="Operacao de Oficina", owner_role=StaffRole.COLABORADOR, ) self.metadata_repository.metadata_entries[0].display_name = "No" self.service.authorize_generation( intake_payload["draft_preview"]["version_id"], actor_staff_account_id=99, actor_name="Diretoria", actor_role=StaffRole.DIRETOR, decision_notes="A proposta pode seguir para geracao governada apos triagem inicial.", ) payload = self.service.run_generation_pipeline( intake_payload["draft_preview"]["version_id"], runner_staff_account_id=8, runner_name="Operacao de Oficina", runner_role=StaffRole.COLABORADOR, ) self.assertEqual(payload["status"], ToolLifecycleStatus.FAILED) self.assertEqual(payload["current_step"], "generation") self.assertEqual(payload["queue_entry"]["gate"], "pipeline_retry_required") failed_detail = self.service.build_review_detail_payload(intake_payload["draft_preview"]["version_id"]) self.assertTrue(failed_detail["human_gate"]["run_pipeline_action_available"]) self.assertEqual(payload["queue_entry"]["automated_validation_status"], "failed") self.assertIn("revisar contrato da tool", payload["queue_entry"]["automated_validation_summary"].lower()) automated_checks = {check["key"]: check for check in payload["automated_validations"]} self.assertEqual(automated_checks["tool_contract"]["status"], "failed") self.assertEqual(automated_checks["tool_signature_schema"]["status"], "passed") self.assertEqual(automated_checks["tool_import_loading"]["status"], "passed") self.assertEqual(automated_checks["tool_smoke_tests"]["status"], "passed") self.assertTrue(automated_checks["tool_contract"]["blocking_issues"]) self.assertEqual(self.draft_repository.drafts[0].status, ToolLifecycleStatus.FAILED) self.assertEqual(self.version_repository.versions[0].status, ToolLifecycleStatus.FAILED) self.assertEqual(self.metadata_repository.metadata_entries[0].status, ToolLifecycleStatus.FAILED) validation_artifact = next( artifact for artifact in self.artifact_repository.artifacts if artifact.artifact_kind == ToolArtifactKind.VALIDATION_REPORT ) self.assertEqual(validation_artifact.artifact_status.value, "failed") self.assertIn("display_name", validation_artifact.payload_json["blocking_issues"][0]) def test_run_generation_pipeline_marks_version_failed_when_signature_schema_validation_fails(self): intake_payload = self.service.create_draft_submission( { "domain": "revisao", "tool_name": "consultar_revisao_aberta", "display_name": "Consultar revisao aberta", "description": "Consulta revisoes abertas com filtros administrativos para a oficina.", "business_goal": "Ajudar o time a localizar revisoes abertas com mais contexto operacional.", "parameters": [ { "name": "user_id", "parameter_type": "string", "description": "Identificador do usuario usado no filtro administrativo.", "required": True, } ], }, owner_staff_account_id=8, owner_name="Operacao de Oficina", owner_role=StaffRole.COLABORADOR, ) self.service.authorize_generation( intake_payload["draft_preview"]["version_id"], actor_staff_account_id=99, actor_name="Diretoria", actor_role=StaffRole.DIRETOR, decision_notes="A proposta pode seguir para geracao governada apos triagem inicial.", ) payload = self.service.run_generation_pipeline( intake_payload["draft_preview"]["version_id"], runner_staff_account_id=8, runner_name="Operacao de Oficina", runner_role=StaffRole.COLABORADOR, ) self.assertEqual(payload["status"], ToolLifecycleStatus.FAILED) self.assertEqual(payload["queue_entry"]["gate"], "pipeline_retry_required") automated_checks = {check["key"]: check for check in payload["automated_validations"]} self.assertEqual(automated_checks["tool_contract"]["status"], "passed") self.assertEqual(automated_checks["tool_signature_schema"]["status"], "failed") self.assertEqual(automated_checks["tool_import_loading"]["status"], "failed") self.assertEqual(automated_checks["tool_smoke_tests"]["status"], "failed") self.assertIn("user_id", automated_checks["tool_signature_schema"]["blocking_issues"][0]) validation_artifact = next( artifact for artifact in self.artifact_repository.artifacts if artifact.artifact_kind == ToolArtifactKind.VALIDATION_REPORT ) self.assertEqual(validation_artifact.artifact_status.value, "failed") self.assertIn("user_id", validation_artifact.payload_json["signature_schema"]["issues"][0]) def test_run_generation_pipeline_marks_version_failed_when_import_loading_validation_fails(self): intake_payload = self.service.create_draft_submission( { "domain": "revisao", "tool_name": "consultar_revisao_aberta", "display_name": "Consultar revisao aberta", "description": "Consulta revisoes abertas com filtros administrativos para a oficina.", "business_goal": "Ajudar o time a localizar revisoes abertas com mais contexto operacional.", "parameters": [ { "name": "placa", "parameter_type": "string", "description": "Placa usada na busca da revisao.", "required": True, } ], }, owner_staff_account_id=8, owner_name="Operacao de Oficina", owner_role=StaffRole.COLABORADOR, ) with patch.object( self.service, "_render_generated_tool_module_source", return_value="async def run(:\n pass\n", ): self.service.authorize_generation( intake_payload["draft_preview"]["version_id"], actor_staff_account_id=99, actor_name="Diretoria", actor_role=StaffRole.DIRETOR, decision_notes="A proposta pode seguir para geracao governada apos triagem inicial.", ) payload = self.service.run_generation_pipeline( intake_payload["draft_preview"]["version_id"], runner_staff_account_id=8, runner_name="Operacao de Oficina", runner_role=StaffRole.COLABORADOR, ) self.assertEqual(payload["status"], ToolLifecycleStatus.FAILED) self.assertEqual(payload["queue_entry"]["gate"], "pipeline_retry_required") automated_checks = {check["key"]: check for check in payload["automated_validations"]} self.assertEqual(automated_checks["tool_contract"]["status"], "passed") self.assertEqual(automated_checks["tool_signature_schema"]["status"], "passed") self.assertEqual(automated_checks["tool_import_loading"]["status"], "failed") self.assertEqual(automated_checks["tool_smoke_tests"]["status"], "failed") self.assertIn("SyntaxError", automated_checks["tool_import_loading"]["blocking_issues"][0]) validation_artifact = next( artifact for artifact in self.artifact_repository.artifacts if artifact.artifact_kind == ToolArtifactKind.VALIDATION_REPORT ) self.assertEqual(validation_artifact.artifact_status.value, "failed") self.assertIn("SyntaxError", validation_artifact.payload_json["import_loading"]["issues"][0]) self.assertIn("import/loading validation did not pass", validation_artifact.payload_json["smoke_tests"]["issues"][0]) def test_run_generation_pipeline_marks_version_failed_when_smoke_tests_fail(self): intake_payload = self.service.create_draft_submission( { "domain": "revisao", "tool_name": "consultar_revisao_aberta", "display_name": "Consultar revisao aberta", "description": "Consulta revisoes abertas com filtros administrativos para a oficina.", "business_goal": "Ajudar o time a localizar revisoes abertas com mais contexto operacional.", "parameters": [ { "name": "placa", "parameter_type": "string", "description": "Placa usada na busca da revisao.", "required": True, } ], }, owner_staff_account_id=8, owner_name="Operacao de Oficina", owner_role=StaffRole.COLABORADOR, ) with patch.object( self.service, "_render_generated_tool_module_source", return_value='async def run(*, placa):\n raise RuntimeError("smoke test boom")\n', ): self.service.authorize_generation( intake_payload["draft_preview"]["version_id"], actor_staff_account_id=99, actor_name="Diretoria", actor_role=StaffRole.DIRETOR, decision_notes="A proposta pode seguir para geracao governada apos triagem inicial.", ) payload = self.service.run_generation_pipeline( intake_payload["draft_preview"]["version_id"], runner_staff_account_id=8, runner_name="Operacao de Oficina", runner_role=StaffRole.COLABORADOR, ) self.assertEqual(payload["status"], ToolLifecycleStatus.FAILED) self.assertEqual(payload["queue_entry"]["gate"], "pipeline_retry_required") automated_checks = {check["key"]: check for check in payload["automated_validations"]} self.assertEqual(automated_checks["tool_contract"]["status"], "passed") self.assertEqual(automated_checks["tool_signature_schema"]["status"], "passed") self.assertEqual(automated_checks["tool_import_loading"]["status"], "passed") self.assertEqual(automated_checks["tool_smoke_tests"]["status"], "failed") self.assertIn("RuntimeError", automated_checks["tool_smoke_tests"]["blocking_issues"][0]) validation_artifact = next( artifact for artifact in self.artifact_repository.artifacts if artifact.artifact_kind == ToolArtifactKind.VALIDATION_REPORT ) self.assertEqual(validation_artifact.artifact_status.value, "failed") self.assertIn("RuntimeError", validation_artifact.payload_json["smoke_tests"]["issues"][0]) def test_review_requires_generation_pipeline_before_validation(self): intake_payload = self.service.create_draft_submission( { "domain": "locacao", "tool_name": "emitir_resumo_locacao", "display_name": "Emitir resumo de locacao", "description": "Resume contratos de locacao com filtros operacionais para o time interno.", "business_goal": "Dar visibilidade rapida aos contratos e aos principais dados da locacao.", "parameters": [], }, owner_staff_account_id=3, owner_name="Equipe Interna", ) with self.assertRaisesRegex(ValueError, "generated"): self.service.review_version( intake_payload["draft_preview"]["version_id"], reviewer_staff_account_id=99, reviewer_name="Diretoria", reviewer_role=StaffRole.DIRETOR, decision_notes="Aguardando a geracao controlada da funcao.", reviewed_generated_code=True, ) def test_director_must_review_approve_and_publish_before_activation(self): intake_payload = self.service.create_draft_submission( { "domain": "locacao", "tool_name": "emitir_resumo_locacao", "display_name": "Emitir resumo de locacao", "description": "Resume contratos de locacao com filtros operacionais para o time interno.", "business_goal": "Dar visibilidade rapida aos contratos e aos principais dados da locacao.", "parameters": [ { "name": "contrato_id", "parameter_type": "string", "description": "Identificador do contrato consultado.", "required": True, } ], }, owner_staff_account_id=3, owner_name="Equipe Interna", ) version_id = intake_payload["draft_preview"]["version_id"] pipeline_payload = self.service.run_generation_pipeline( version_id, runner_staff_account_id=3, runner_name="Equipe Interna", runner_role=StaffRole.COLABORADOR, ) review_payload = self.service.review_version( version_id, reviewer_staff_account_id=99, reviewer_name="Diretoria", reviewer_role=StaffRole.DIRETOR, decision_notes="Analisei o codigo completo gerado e a estrutura esta aderente ao fluxo governado.", reviewed_generated_code=True, ) approve_payload = self.service.approve_version( version_id, approver_staff_account_id=99, approver_name="Diretoria", approver_role=StaffRole.DIRETOR, decision_notes="Aprovacao formal registrada apos revisao tecnica e leitura integral do codigo.", ) publish_payload = self.service.publish_version( version_id, publisher_staff_account_id=99, publisher_name="Diretoria", publisher_role=StaffRole.DIRETOR, ) self.assertEqual(pipeline_payload["status"], ToolLifecycleStatus.GENERATED) self.assertEqual(len(pipeline_payload["automated_validations"]), 4) self.assertTrue(all(check["status"] == "passed" for check in pipeline_payload["automated_validations"])) self.assertEqual(review_payload["status"], ToolLifecycleStatus.VALIDATED) self.assertEqual(review_payload["queue_entry"]["gate"], "director_approval_required") self.assertEqual(approve_payload["status"], ToolLifecycleStatus.APPROVED) self.assertEqual(approve_payload["queue_entry"]["gate"], "director_publication_required") self.assertEqual(publish_payload["status"], ToolLifecycleStatus.ACTIVE) self.assertIsNone(publish_payload["queue_entry"]) self.assertEqual(publish_payload["publication"]["tool_name"], "emitir_resumo_locacao") self.assertEqual(self.draft_repository.drafts[0].status, ToolLifecycleStatus.ACTIVE) self.assertEqual(self.version_repository.versions[0].status, ToolLifecycleStatus.ACTIVE) self.assertEqual(self.metadata_repository.metadata_entries[0].status, ToolLifecycleStatus.ACTIVE) artifact_kinds = {artifact.artifact_kind for artifact in self.artifact_repository.artifacts} self.assertIn(ToolArtifactKind.DIRECTOR_REVIEW, artifact_kinds) self.assertIn(ToolArtifactKind.DIRECTOR_APPROVAL, artifact_kinds) self.assertIn(ToolArtifactKind.PUBLICATION_RELEASE, artifact_kinds) def test_publish_requires_prior_director_review_and_approval(self): intake_payload = self.service.create_draft_submission( { "domain": "revisao", "tool_name": "consultar_revisao_aberta", "display_name": "Consultar revisao aberta", "description": "Consulta revisoes abertas com filtros administrativos para a oficina.", "business_goal": "Ajudar o time a localizar revisoes abertas com mais contexto operacional.", "parameters": [], }, owner_staff_account_id=8, owner_name="Operacao de Oficina", ) with self.assertRaisesRegex(ValueError, "approved"): self.service.publish_version( intake_payload["draft_preview"]["version_id"], publisher_staff_account_id=99, publisher_name="Diretoria", publisher_role=StaffRole.DIRETOR, ) def test_build_review_detail_payload_exposes_generated_source_and_human_history(self): intake_payload = self.service.create_draft_submission( { "domain": "revisao", "tool_name": "consultar_revisao_aberta", "display_name": "Consultar revisao aberta", "description": "Consulta revisoes abertas com filtros administrativos para a oficina.", "business_goal": "Ajudar o time a localizar revisoes abertas com mais contexto operacional.", "parameters": [ { "name": "placa", "parameter_type": "string", "description": "Placa usada na busca da revisao.", "required": True, } ], }, owner_staff_account_id=8, owner_name="Operacao de Oficina", ) version_id = intake_payload["draft_preview"]["version_id"] self.service.run_generation_pipeline( version_id, runner_staff_account_id=8, runner_name="Operacao de Oficina", runner_role=StaffRole.COLABORADOR, ) self.service.review_version( version_id, reviewer_staff_account_id=99, reviewer_name="Diretoria", reviewer_role=StaffRole.DIRETOR, decision_notes="Analise completa do codigo gerado antes da validacao humana.", reviewed_generated_code=True, ) self.service.approve_version( version_id, approver_staff_account_id=99, approver_name="Diretoria", approver_role=StaffRole.DIRETOR, decision_notes="Aprovacao formal da versao apos revisao humana detalhada.", ) payload = self.service.build_review_detail_payload(version_id) self.assertEqual(payload["status"], ToolLifecycleStatus.APPROVED) self.assertEqual(payload["human_gate"]["publication_action_available"], True) self.assertEqual(payload["generation_context"]["latest_generation_iteration"], 1) self.assertEqual(payload["generation_context"]["generation_iterations_count"], 1) self.assertEqual(payload["generation_context"]["latest_generation_mode"], "initial_generation") self.assertTrue(payload["generation_context"]["has_previous_generation"]) self.assertIsNone(payload["generation_context"]["latest_change_request_notes"]) self.assertIn("async def run", payload["generated_source_code"]) self.assertEqual(len(payload["automated_validations"]), 4) self.assertEqual(len(payload["decision_history"]), 3) self.assertEqual(payload["decision_history"][0]["action_key"], ToolArtifactKind.GENERATION_AUTHORIZATION.value) self.assertEqual(payload["decision_history"][1]["action_key"], ToolArtifactKind.DIRECTOR_REVIEW.value) self.assertTrue(payload["decision_history"][1]["reviewed_generated_code"]) self.assertIn("aprovacao formal", payload["decision_history"][2]["decision_notes"].lower()) def test_publishing_new_version_archives_previous_active_version(self): first_intake = self.service.create_draft_submission( { "domain": "vendas", "tool_name": "consultar_funil_comercial", "display_name": "Consultar funil comercial", "description": "Consulta o funil comercial consolidado para acompanhamento administrativo.", "business_goal": "Dar visibilidade ao time interno sobre os principais gargalos do funil.", "parameters": [], }, owner_staff_account_id=7, owner_name="Equipe Interna", ) first_version_id = first_intake["draft_preview"]["version_id"] self.service.run_generation_pipeline(first_version_id, runner_staff_account_id=7, runner_name="Equipe Interna", runner_role=StaffRole.COLABORADOR) self.service.review_version( first_version_id, reviewer_staff_account_id=99, reviewer_name="Diretoria", reviewer_role=StaffRole.DIRETOR, decision_notes="Primeira versao revisada com leitura integral do codigo gerado.", reviewed_generated_code=True, ) self.service.approve_version( first_version_id, approver_staff_account_id=99, approver_name="Diretoria", approver_role=StaffRole.DIRETOR, decision_notes="Primeira versao aprovada para ativacao controlada.", ) self.service.publish_version(first_version_id, publisher_staff_account_id=99, publisher_name="Diretoria", publisher_role=StaffRole.DIRETOR) second_intake = self.service.create_draft_submission( { "domain": "vendas", "tool_name": "consultar_funil_comercial", "display_name": "Consultar funil comercial", "description": "Consulta o funil comercial consolidado com campos adicionais para acompanhamento administrativo.", "business_goal": "Dar visibilidade ao time interno sobre gargalos, volume e conversao do funil.", "parameters": [], }, owner_staff_account_id=7, owner_name="Equipe Interna", ) second_version_id = second_intake["draft_preview"]["version_id"] self.service.run_generation_pipeline(second_version_id, runner_staff_account_id=7, runner_name="Equipe Interna", runner_role=StaffRole.COLABORADOR) self.service.review_version( second_version_id, reviewer_staff_account_id=99, reviewer_name="Diretoria", reviewer_role=StaffRole.DIRETOR, decision_notes="Nova versao revisada com comparativo do codigo completo gerado.", reviewed_generated_code=True, ) self.service.approve_version( second_version_id, approver_staff_account_id=99, approver_name="Diretoria", approver_role=StaffRole.DIRETOR, decision_notes="Nova versao aprovada para substituir a publicacao anterior.", ) self.service.publish_version(second_version_id, publisher_staff_account_id=99, publisher_name="Diretoria", publisher_role=StaffRole.DIRETOR) versions_by_number = {version.version_number: version for version in self.version_repository.versions} metadata_by_number = {metadata.version_number: metadata for metadata in self.metadata_repository.metadata_entries} self.assertEqual(versions_by_number[1].status, ToolLifecycleStatus.ARCHIVED) self.assertEqual(metadata_by_number[1].status, ToolLifecycleStatus.ARCHIVED) self.assertEqual(versions_by_number[2].status, ToolLifecycleStatus.ACTIVE) self.assertEqual(metadata_by_number[2].status, ToolLifecycleStatus.ACTIVE) def test_deactivating_active_version_archives_publication_and_removes_tool_from_catalog(self): intake = self.service.create_draft_submission( { "domain": "locacao", "tool_name": "emitir_resumo_locacao", "display_name": "Emitir resumo de locacao", "description": "Resume contratos de locacao com filtros operacionais para o time interno.", "business_goal": "Dar visibilidade rapida aos contratos e aos principais dados da locacao.", "parameters": [], }, owner_staff_account_id=7, owner_name="Equipe Interna", ) version_id = intake["draft_preview"]["version_id"] self.service.run_generation_pipeline(version_id, runner_staff_account_id=7, runner_name="Equipe Interna", runner_role=StaffRole.COLABORADOR) self.service.review_version( version_id, reviewer_staff_account_id=99, reviewer_name="Diretoria", reviewer_role=StaffRole.DIRETOR, decision_notes="Analisei a versao ativa antes da desativacao controlada.", reviewed_generated_code=True, ) self.service.approve_version( version_id, approver_staff_account_id=99, approver_name="Diretoria", approver_role=StaffRole.DIRETOR, decision_notes="Aprovacao formal para ativar e depois validar a desativacao controlada.", ) self.service.publish_version(version_id, publisher_staff_account_id=99, publisher_name="Diretoria", publisher_role=StaffRole.DIRETOR) payload = self.service.deactivate_version( version_id, actor_staff_account_id=99, actor_name="Diretoria", actor_role=StaffRole.DIRETOR, decision_notes="Desativacao controlada da tool apos encerramento temporario do uso.", ) self.assertEqual(payload["status"], ToolLifecycleStatus.ARCHIVED) self.assertIsNone(payload["queue_entry"]) detail = self.service.build_review_detail_payload(version_id) self.assertEqual(detail["status"], ToolLifecycleStatus.ARCHIVED) self.assertFalse(detail["human_gate"]["deactivation_action_available"]) self.assertFalse(detail["human_gate"]["rollback_action_available"]) self.assertEqual(detail["decision_history"][-1]["action_key"], ToolArtifactKind.PUBLICATION_DEACTIVATION.value) publications = self.service.build_publications_payload() self.assertNotIn("emitir_resumo_locacao", [item["tool_name"] for item in publications["publications"]]) def test_rollback_restores_latest_archived_version_into_active_catalog(self): first_intake = self.service.create_draft_submission( { "domain": "vendas", "tool_name": "consultar_funil_comercial", "display_name": "Consultar funil comercial", "description": "Consulta o funil comercial consolidado para acompanhamento administrativo.", "business_goal": "Dar visibilidade ao time interno sobre os principais gargalos do funil.", "parameters": [], }, owner_staff_account_id=7, owner_name="Equipe Interna", ) first_version_id = first_intake["draft_preview"]["version_id"] self.service.run_generation_pipeline(first_version_id, runner_staff_account_id=7, runner_name="Equipe Interna", runner_role=StaffRole.COLABORADOR) self.service.review_version( first_version_id, reviewer_staff_account_id=99, reviewer_name="Diretoria", reviewer_role=StaffRole.DIRETOR, decision_notes="Primeira versao revisada antes da futura ativacao controlada.", reviewed_generated_code=True, ) self.service.approve_version( first_version_id, approver_staff_account_id=99, approver_name="Diretoria", approver_role=StaffRole.DIRETOR, decision_notes="Primeira versao aprovada para publicacao inicial.", ) self.service.publish_version(first_version_id, publisher_staff_account_id=99, publisher_name="Diretoria", publisher_role=StaffRole.DIRETOR) second_intake = self.service.create_draft_submission( { "domain": "vendas", "tool_name": "consultar_funil_comercial", "display_name": "Consultar funil comercial", "description": "Consulta o funil comercial consolidado com campos adicionais para acompanhamento administrativo.", "business_goal": "Dar visibilidade ao time interno sobre gargalos, volume e conversao do funil.", "parameters": [], }, owner_staff_account_id=7, owner_name="Equipe Interna", ) second_version_id = second_intake["draft_preview"]["version_id"] self.service.run_generation_pipeline(second_version_id, runner_staff_account_id=7, runner_name="Equipe Interna", runner_role=StaffRole.COLABORADOR) self.service.review_version( second_version_id, reviewer_staff_account_id=99, reviewer_name="Diretoria", reviewer_role=StaffRole.DIRETOR, decision_notes="Nova versao revisada com leitura integral antes da substituicao.", reviewed_generated_code=True, ) self.service.approve_version( second_version_id, approver_staff_account_id=99, approver_name="Diretoria", approver_role=StaffRole.DIRETOR, decision_notes="Nova versao aprovada para substituir a publicacao anterior.", ) self.service.publish_version(second_version_id, publisher_staff_account_id=99, publisher_name="Diretoria", publisher_role=StaffRole.DIRETOR) active_detail = self.service.build_review_detail_payload(second_version_id) self.assertTrue(active_detail["human_gate"]["deactivation_action_available"]) self.assertTrue(active_detail["human_gate"]["rollback_action_available"]) self.assertEqual(active_detail["human_gate"]["rollback_target_version_number"], 1) payload = self.service.rollback_version( second_version_id, actor_staff_account_id=99, actor_name="Diretoria", actor_role=StaffRole.DIRETOR, decision_notes="Rollback controlado para restaurar a versao anterior mais estavel.", ) versions_by_number = {version.version_number: version for version in self.version_repository.versions} metadata_by_number = {metadata.version_number: metadata for metadata in self.metadata_repository.metadata_entries} self.assertEqual(payload["status"], ToolLifecycleStatus.ACTIVE) self.assertEqual(payload["version_id"], first_version_id) self.assertEqual(versions_by_number[1].status, ToolLifecycleStatus.ACTIVE) self.assertEqual(metadata_by_number[1].status, ToolLifecycleStatus.ACTIVE) self.assertEqual(versions_by_number[2].status, ToolLifecycleStatus.ARCHIVED) self.assertEqual(metadata_by_number[2].status, ToolLifecycleStatus.ARCHIVED) restored_detail = self.service.build_review_detail_payload(first_version_id) self.assertEqual(restored_detail["decision_history"][-1]["action_key"], ToolArtifactKind.PUBLICATION_ROLLBACK.value) publications = self.service.build_publications_payload() restored_publication = next(item for item in publications["publications"] if item["tool_name"] == "consultar_funil_comercial") self.assertEqual(restored_publication["version_id"], first_version_id) self.assertTrue(restored_publication["deactivation_action_available"]) class AdminToolManagementTransactionalPersistenceTests(unittest.TestCase): def test_publish_and_deactivate_keep_local_runtime_snapshot_for_product(self): import json import shutil from pathlib import Path from unittest.mock import patch draft_repository = _FakeToolDraftRepository() version_repository = _FakeToolVersionRepository() metadata_repository = _FakeToolMetadataRepository() artifact_repository = _FakeToolArtifactRepository() service = ToolManagementService( settings=AdminSettings(admin_api_prefix="/admin"), draft_repository=draft_repository, version_repository=version_repository, metadata_repository=metadata_repository, artifact_repository=artifact_repository, ) intake_payload = service.create_draft_submission( { "domain": "locacao", "tool_name": "emitir_resumo_locacao", "display_name": "Emitir resumo de locacao", "description": "Resume uma locacao ativa com dados importantes para o atendimento.", "business_goal": "Permitir que a equipe gere um resumo operacional de locacao sem acessar o core.", "parameters": [ { "name": "reserva_id", "parameter_type": ToolParameterType.STRING, "description": "Identificador da reserva que sera resumida.", "required": True, } ], }, owner_staff_account_id=7, owner_name="Equipe Interna", owner_role=StaffRole.COLABORADOR, ) version_id = intake_payload["draft_preview"]["version_id"] service.authorize_generation( version_id, actor_staff_account_id=99, actor_name="Diretoria", actor_role=StaffRole.DIRETOR, decision_notes="A proposta foi autorizada pela diretoria antes da geracao governada.", ) service.run_generation_pipeline( version_id, runner_staff_account_id=7, runner_name="Equipe Interna", runner_role=StaffRole.COLABORADOR, ) service.review_version( version_id, reviewer_staff_account_id=99, reviewer_name="Diretoria", reviewer_role=StaffRole.DIRETOR, decision_notes="Revisao humana registrada para liberacao controlada.", reviewed_generated_code=True, ) service.approve_version( version_id, approver_staff_account_id=99, approver_name="Diretoria", approver_role=StaffRole.DIRETOR, decision_notes="Aprovacao formal liberada para publicacao governada.", ) sandbox_root = Path.cwd() / ".tmp_test_admin_runtime_snapshot" shutil.rmtree(sandbox_root, ignore_errors=True) package_dir = sandbox_root / GENERATED_TOOLS_PACKAGE manifest_path = package_dir / "published_runtime_tools.json" def build_file_path(tool_name: str): return package_dir / f"{tool_name}.py" try: with patch( "admin_app.services.tool_management_service.get_generated_tools_runtime_dir", return_value=package_dir, ), patch( "admin_app.services.tool_management_service.get_generated_tool_publication_manifest_path", return_value=manifest_path, ), patch( "admin_app.services.tool_management_service.build_generated_tool_file_path", side_effect=build_file_path, ): service.publish_version( version_id, publisher_staff_account_id=99, publisher_name="Diretoria", publisher_role=StaffRole.DIRETOR, ) manifest = json.loads(manifest_path.read_text(encoding="utf-8")) self.assertEqual(manifest["target_service"], "product") self.assertEqual(len(manifest["publications"]), 1) self.assertEqual( manifest["publications"][0]["published_tool"]["tool_name"], "emitir_resumo_locacao", ) self.assertEqual( manifest["publications"][0]["published_tool"]["status"], ToolLifecycleStatus.ACTIVE.value, ) self.assertTrue(build_file_path("emitir_resumo_locacao").exists()) self.assertIn( "async def run", build_file_path("emitir_resumo_locacao").read_text(encoding="utf-8"), ) service.deactivate_version( version_id, actor_staff_account_id=99, actor_name="Diretoria", actor_role=StaffRole.DIRETOR, decision_notes="Desativacao controlada para manter apenas o snapshot local anterior.", ) manifest_after = json.loads(manifest_path.read_text(encoding="utf-8")) self.assertEqual(manifest_after["publications"], []) finally: shutil.rmtree(sandbox_root, ignore_errors=True) def setUp(self): self.engine = create_engine("sqlite:///:memory:") AdminBase.metadata.create_all(bind=self.engine) session_local = sessionmaker(autocommit=False, autoflush=False, bind=self.engine) self.db = session_local() owner = StaffAccount( email="tools-admin@example.com", display_name="Equipe Interna", password_hash="hash", role=StaffRole.COLABORADOR, is_active=True, ) self.db.add(owner) self.db.commit() self.db.refresh(owner) self.owner = owner self.draft_repository = ToolDraftRepository(self.db) self.version_repository = ToolVersionRepository(self.db) self.metadata_repository = ToolMetadataRepository(self.db) self.artifact_repository = _FailingToolArtifactRepository(self.db, fail_on_call=2) self.service = ToolManagementService( settings=AdminSettings(admin_api_prefix="/admin"), draft_repository=self.draft_repository, version_repository=self.version_repository, metadata_repository=self.metadata_repository, artifact_repository=self.artifact_repository, ) def tearDown(self): self.db.close() self.engine.dispose() def test_create_draft_submission_rolls_back_all_rows_when_artifact_persistence_fails(self): with self.assertRaisesRegex(RuntimeError, "artifact persistence failure"): self.service.create_draft_submission( { "domain": "vendas", "tool_name": "consolidar_funil_interno", "display_name": "Consolidar funil interno", "description": "Consolida indicadores internos do funil comercial para acompanhamento administrativo.", "business_goal": "Permitir que o time acompanhe a saude do funil sem depender de consultas manuais repetidas.", "parameters": [ { "name": "periodo_inicio", "parameter_type": "string", "description": "Data inicial usada na consolidacao.", "required": True, } ], }, owner_staff_account_id=self.owner.id, owner_name=self.owner.display_name, ) self.assertEqual(self.draft_repository.list_drafts(), []) self.assertEqual(self.version_repository.list_versions(), []) self.assertEqual(self.metadata_repository.list_metadata(), []) self.assertEqual(self.artifact_repository.list_artifacts(), []) if __name__ == "__main__": unittest.main() class AdminToolManagementWorkerDispatchTests(unittest.TestCase): def test_run_generation_pipeline_in_worker_falls_back_to_inline_execution_metadata(self): service = ToolManagementService(settings=AdminSettings(admin_api_prefix="/admin")) expected_payload = { "message": "Pipeline executado.", "version_id": "tool_version::teste::v1", "tool_name": "teste", "version_number": 1, "status": ToolLifecycleStatus.GENERATED, "current_step": "validation", "steps": [], "queue_entry": { "entry_id": "tool_version::teste::v1", "version_id": "tool_version::teste::v1", "version_number": 1, "tool_name": "teste", "display_name": "Tool teste", "status": ToolLifecycleStatus.GENERATED, "gate": "validation_required", "summary": "Resumo", }, "automated_validations": [], "next_steps": [], } with patch.object(service, "run_generation_pipeline", return_value=expected_payload) as run_pipeline: payload = service.run_generation_pipeline_in_worker( "tool_version::teste::v1", runner_staff_account_id=7, runner_name="Equipe Interna", runner_role=StaffRole.COLABORADOR, ) run_pipeline.assert_called_once_with( "tool_version::teste::v1", runner_staff_account_id=7, runner_name="Equipe Interna", runner_role=StaffRole.COLABORADOR, ) self.assertEqual(payload["execution"]["mode"], "inline_admin_service") self.assertEqual(payload["execution"]["target"], "admin_inline_generation_pipeline")