From fd3440bcc4291d80a892ec8ff361f362b7344c63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Henrique?= Date: Wed, 9 Sep 2026 12:04:28 -0400 Subject: [PATCH] =?UTF-8?q?feat:=20corrigida=20a=20resolu=C3=A7=C3=A3o=20d?= =?UTF-8?q?o=20Node.js=20ao=20executar=20planos=20Python=20p?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - corrigida a resolução do Node.js ao executar planos Python pelo painel CEP Resumo: - 2 arquivos alterados - 1 novos - 1 modificados - 0 removidos 1 file changed, 38 insertions(+), 1 deletion(-) Arquivos: - code/engine/aplicar_plano_de_edicao.py - code/engine/testes/test_aplicar_plano_de_edicao.py --- code/engine/aplicar_plano_de_edicao.py | 39 ++++++++++++++++++- .../testes/test_aplicar_plano_de_edicao.py | 39 +++++++++++++++++++ 2 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 code/engine/testes/test_aplicar_plano_de_edicao.py diff --git a/code/engine/aplicar_plano_de_edicao.py b/code/engine/aplicar_plano_de_edicao.py index 875de4e..cb1bb9d 100644 --- a/code/engine/aplicar_plano_de_edicao.py +++ b/code/engine/aplicar_plano_de_edicao.py @@ -18,6 +18,8 @@ troca de sequência. from __future__ import annotations import json +import os +import shutil import sys from pathlib import Path @@ -41,6 +43,40 @@ from engine.integracoes.premiere.sessao_mcp import SessaoMCP CAMINHO_DO_SERVIDOR_MCP = CAMINHO_DO_CODIGO / "dist" / "index.js" +def resolver_caminho_do_node() -> str: + """Resolve o executável Node mesmo quando o painel CEP possui um PATH reduzido. + + A variável ``PREMIERE_NODE_BIN`` permite que o painel ou o sistema informe + um caminho explícito. Em seguida, a resolução tenta o ``PATH`` e os + caminhos comuns de instalação local. Levanta :class:`FileNotFoundError` + com orientação acionável quando o Node não está disponível. + """ + caminho_configurado = os.environ.get("PREMIERE_NODE_BIN", "").strip() + if caminho_configurado: + if os.path.isfile(caminho_configurado) and os.access(caminho_configurado, os.X_OK): + return caminho_configurado + raise FileNotFoundError( + f"Node.js configurado em PREMIERE_NODE_BIN não é executável: {caminho_configurado}" + ) + + encontrado_no_path = shutil.which("node.exe" if os.name == "nt" else "node") + if encontrado_no_path: + return encontrado_no_path + + caminhos_conhecidos = ( + ("C:/Program Files/nodejs/node.exe", "C:/Program Files/nodejs/nodejs.exe") + if os.name == "nt" + else ("/opt/homebrew/bin/node", "/usr/local/bin/node", "/usr/bin/node") + ) + for caminho in caminhos_conhecidos: + if os.path.isfile(caminho) and os.access(caminho, os.X_OK): + return caminho + raise FileNotFoundError( + "Node.js não encontrado. Instale o Node.js ou configure PREMIERE_NODE_BIN " + "com o caminho completo do executável." + ) + + def executar(caminho_do_plano: Path) -> dict: """Lê o plano em ``caminho_do_plano`` e o aplica na sequência ativa do Premiere. @@ -52,7 +88,8 @@ def executar(caminho_do_plano: Path) -> dict: # A pausa é aplicada no cliente, e não dentro do aplicador, para valer # igualmente nas leituras da timeline e nas escritas — são todas # round-trips síncronos no processo principal do Premiere. - cliente = ClienteMCPComPausa(ClienteMCPPorStdio(["node", str(CAMINHO_DO_SERVIDOR_MCP)])) + caminho_do_node = resolver_caminho_do_node() + cliente = ClienteMCPComPausa(ClienteMCPPorStdio([caminho_do_node, str(CAMINHO_DO_SERVIDOR_MCP)])) with SessaoMCP(cliente): acesso_ao_editor = AcessoAoEditor(AcessoATimeline(cliente)) conversor_de_timeline = ConversorDeTimeline() diff --git a/code/engine/testes/test_aplicar_plano_de_edicao.py b/code/engine/testes/test_aplicar_plano_de_edicao.py new file mode 100644 index 0000000..263fcd7 --- /dev/null +++ b/code/engine/testes/test_aplicar_plano_de_edicao.py @@ -0,0 +1,39 @@ +"""Testes da resolução do executável Node usada pelo aplicador de planos.""" + +import os +import unittest +from unittest.mock import patch + +from engine import aplicar_plano_de_edicao + + +class TesteResolucaoDoNode(unittest.TestCase): + """Garante que a engine não dependa do PATH herdado pelo painel CEP.""" + + def test_prioriza_executavel_configurado_no_ambiente(self): + """Usa o caminho explícito quando o painel ou sistema o fornece.""" + with patch.dict(os.environ, {"PREMIERE_NODE_BIN": "/custom/node"}, clear=False), patch( + "engine.aplicar_plano_de_edicao.os.path.isfile", return_value=True + ), patch("engine.aplicar_plano_de_edicao.os.access", return_value=True): + self.assertEqual(aplicar_plano_de_edicao.resolver_caminho_do_node(), "/custom/node") + + def test_usa_caminho_conhecido_quando_node_nao_esta_no_path(self): + """Encontra a instalação Homebrew mesmo com PATH incompleto.""" + with patch.dict(os.environ, {}, clear=True), patch( + "engine.aplicar_plano_de_edicao.shutil.which", return_value=None + ), patch("engine.aplicar_plano_de_edicao.os.path.isfile", side_effect=lambda caminho: caminho == "/opt/homebrew/bin/node"), patch( + "engine.aplicar_plano_de_edicao.os.access", return_value=True + ): + self.assertEqual(aplicar_plano_de_edicao.resolver_caminho_do_node(), "/opt/homebrew/bin/node") + + def test_informa_instalacao_ausente(self): + """Levanta um erro explicativo quando nenhum executável existe.""" + with patch.dict(os.environ, {}, clear=True), patch( + "engine.aplicar_plano_de_edicao.shutil.which", return_value=None + ), patch("engine.aplicar_plano_de_edicao.os.path.isfile", return_value=False): + with self.assertRaisesRegex(FileNotFoundError, "Node.js não encontrado"): + aplicar_plano_de_edicao.resolver_caminho_do_node() + + +if __name__ == "__main__": + unittest.main()