📚 Pré-requisitos Teóricos: este projeto aplica conceitos ensinados em Módulo 02: Lógica de Programação e Algoritmos. Recomendado revisar antes de começar.

🌱 Miniprojeto 05: Empréstimo de Livros da Biblioteca Comunitária

Guia Autoguiado Passo a Passo: Do Controle em Arquivo TXT à Interface Desktop com Abas em Tkinter e SQLite Relacional

Trilha de Especialização — Nível Introdutório (Curso 25: Sistemas Desktop com Python)


🎯 Objetivo de Aprendizagem

Em bibliotecas escolares, comunitárias ou de igrejas em pequenas cidades, a gestão de quais livros estão na estante e quais estão emprestados é essencial para evitar o extravio de exemplares.

Neste projeto, o estudante aprende como:

  1. Controlar estoque de exemplares totais e disponíveis em arquivos .txt delimitados.
  2. Criar as classes Livro e Biblioteca para encapsular regras de empréstimo (bloqueio quando disponíveis == 0) e devolução.
  3. Modelar duas tabelas com integridade referencial no SQLite3 (livros 1:N emprestimos) executando transações atômicas de saída e retorno de exemplares.
  4. Construir uma interface gráfica desktop com abas (ttk.Notebook), permitindo gerenciar o acervo, realizar empréstimos via combobox e registrar devoluções com um clique.

🪜 O Caminho Didático dos 4 Degraus

flowchart LR
    D1["🪜 DEGRAU 1\nImperativo & .txt\nMenu while + livros.txt\ncom controle de saldo"] --> D2["🪜 DEGRAU 2\nPrimeira Classe POO\nclass Livro &\nclass Biblioteca"]
    D2 --> D3["🪜 DEGRAU 3\nSQLite Relacional (1:N)\nTabelas livros e\nemprestimos com FK"]
    D3 --> D4["🪜 DEGRAU 4\nInterface Desktop\nAbas ttk.Notebook:\nAcervo, Empréstimo e Devolução"]

    style D1 fill:#fffde7,stroke:#fbc02d,stroke-width:2px
    style D2 fill:#e3f2fd,stroke:#1976d2,stroke-width:2px
    style D3 fill:#e8f5e9,stroke:#388e3c,stroke-width:2px
    style D4 fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px

📂 Estrutura de Pastas e Arquivos

sistemas_python_intro_05_biblioteca_livros/
├── degrau_1_imperativo_txt/
│   ├── main.py              # Script imperativo com menu e arquivo livros.txt
│   └── test_degrau_1.py     # Testes automatizados de empréstimo e devolução
├── degrau_2_primeira_classe/
│   ├── main.py              # Classes POO (class Livro e class Biblioteca)
│   └── test_degrau_2.py     # Testes unitários de regras de negócio
├── degrau_3_sqlite/
│   ├── main.py              # SQLite3 relacional: 2 tabelas com chave estrangeira (1:N)
│   └── test_degrau_3.py     # Testes unitários de transações e JOIN
├── degrau_4_interface_tkinter/
│   └── main.py              # Aplicação Desktop com abas (Acervo, Novo Empréstimo, Devoluções)
├── main.py                  # Hub Central / Launcher interativo
└── index.md                 # Este guia didático autoguiado

🚀 Como Executar no Laboratório

1. Abra o terminal na pasta deste projeto

No VS Code / PyCharm, abra a pasta deste projeto (File > Open Folder) ou navegue via terminal:

cd sistemas_python_intro_05_biblioteca_livros

2. Execute o Hub Central ou um Degrau Específico

# Executar o Hub Central (Menu interativo com todos os degraus):
python main.py

# Ou executar um degrau individualmente:
python degrau_1_imperativo_txt/main.py
python degrau_2_primeira_classe/main.py
python degrau_3_sqlite/main.py
python degrau_4_interface_tkinter/main.py

[!TIP] Dica para execução a partir da raiz do repositório: Se você abriu o repositório completo no VS Code, basta executar: python catalogo_projetos/sistemas_python_intro_05_biblioteca_livros/main.py


📖 Roteiro Passo a Passo & Gabarito Completo


🪜 Degrau 1: Programação Imperativa com Arquivo de Texto (.txt)

No primeiro degrau, gravamos cada obra no arquivo livros.txt no formato titulo,autor,totais,disponiveis.

Código-Fonte Completo (degrau_1_imperativo_txt/main.py):

import os

ARQUIVO_PADRAO = "livros.txt"

def carregar_livros(caminho=ARQUIVO_PADRAO):
    livros = []
    if not os.path.exists(caminho):
        return livros
    with open(caminho, "r", encoding="utf-8") as f:
        for linha in f:
            partes = linha.strip().split(",")
            if len(partes) >= 4:
                livros.append({"titulo": partes[0], "autor": partes[1], "totais": int(partes[2]), "disponiveis": int(partes[3])})
    return livros

def salvar_livros(livros, caminho=ARQUIVO_PADRAO):
    with open(caminho, "w", encoding="utf-8") as f:
        for l in livros:
            f.write(f"{l['titulo']},{l['autor']},{l['totais']},{l['disponiveis']}\n")

def emprestar_livro(livros, titulo):
    for l in livros:
        if l["titulo"].lower() == titulo.lower():
            if l["disponiveis"] <= 0:
                print(f"[ERRO] Todos os {l['totais']} exemplares estao emprestados!")
                return False
            l["disponiveis"] -= 1
            print(f"[OK] Emprestimo realizado! Disponiveis: {l['disponiveis']}/{l['totais']}.")
            return True
    return False

def devolver_livro(livros, titulo):
    for l in livros:
        if l["titulo"].lower() == titulo.lower():
            if l["disponiveis"] >= l["totais"]:
                return False
            l["disponiveis"] += 1
            return True
    return False

Testes Automatizados (degrau_1_imperativo_txt/test_degrau_1.py):

"""
Testes Unitários para o Degrau 1 do Projeto 05 (Imperativo & Arquivo TXT)
"""

import os
import unittest
from main import (
    cadastrar_livro,
    carregar_livros,
    devolver_livro,
    emprestar_livro,
    salvar_livros
)

ARQUIVO_TESTE = "teste_livros.txt"


class TestDegrau1Biblioteca(unittest.TestCase):

    def setUp(self):
        if os.path.exists(ARQUIVO_TESTE):
            os.remove(ARQUIVO_TESTE)

    def tearDown(self):
        if os.path.exists(ARQUIVO_TESTE):
            os.remove(ARQUIVO_TESTE)

    def test_cadastrar_e_emprestar(self):
        livros = []
        cadastrar_livro(livros, "Dom Casmurro", "Machado de Assis", 2)
        self.assertEqual(len(livros), 1)
        self.assertEqual(livros[0]["disponiveis"], 2)

        # Empréstimo 1
        emp1 = emprestar_livro(livros, "Dom Casmurro")
        self.assertTrue(emp1)
        self.assertEqual(livros[0]["disponiveis"], 1)

        # Empréstimo 2
        emp2 = emprestar_livro(livros, "Dom Casmurro")
        self.assertTrue(emp2)
        self.assertEqual(livros[0]["disponiveis"], 0)

        # Empréstimo 3 (Esgotado)
        emp3 = emprestar_livro(livros, "Dom Casmurro")
        self.assertFalse(emp3)
        self.assertEqual(livros[0]["disponiveis"], 0)

        # Devolução
        dev = devolver_livro(livros, "Dom Casmurro")
        self.assertTrue(dev)
        self.assertEqual(livros[0]["disponiveis"], 1)

    def test_persistencia_arquivo_txt(self):
        livros = []
        cadastrar_livro(livros, "O Alquimista", "Paulo Coelho", 5)
        salvar_livros(livros, caminho=ARQUIVO_TESTE)

        carregados = carregar_livros(caminho=ARQUIVO_TESTE)
        self.assertEqual(len(carregados), 1)
        self.assertEqual(carregados[0]["titulo"], "O Alquimista")
        self.assertEqual(carregados[0]["totais"], 5)


if __name__ == "__main__":
    unittest.main()

🪜 Degrau 2: Primeiro Contato com POO (Classes Livro e Biblioteca)

No segundo degrau, a classe Livro encapsula o estado do exemplar e seus comportamentos (emprestar(), devolver()).

Código-Fonte Completo (degrau_2_primeira_classe/main.py):

class Livro:
    def __init__(self, titulo, autor, totais, disponiveis=None):
        self.titulo = titulo.strip()
        self.autor = autor.strip() if autor else "Autor Desconhecido"
        self.totais = int(totais)
        self.disponiveis = int(disponiveis) if disponiveis is not None else int(totais)

    def esta_disponivel(self):
        return self.disponiveis > 0

    def emprestar(self):
        if self.disponiveis <= 0:
            return False, "Todos os exemplares estão emprestados."
        self.disponiveis -= 1
        return True, f"Empréstimo realizado! Restam {self.disponiveis}/{self.totais}."

    def devolver(self):
        if self.disponiveis >= self.totais:
            return False, "Todos os exemplares já se encontram na biblioteca."
        self.disponiveis += 1
        return True, f"Devolução confirmada! Disponíveis: {self.disponiveis}/{self.totais}."

Testes Automatizados (degrau_2_primeira_classe/test_degrau_2.py):

"""
Testes Unitários para o Degrau 2 do Projeto 05 (Primeira Classe POO)
"""

import os
import unittest
from main import Biblioteca, Livro

ARQUIVO_TESTE = "teste_livros_poo.txt"


class TestDegrau2POO(unittest.TestCase):

    def setUp(self):
        if os.path.exists(ARQUIVO_TESTE):
            os.remove(ARQUIVO_TESTE)

    def tearDown(self):
        if os.path.exists(ARQUIVO_TESTE):
            os.remove(ARQUIVO_TESTE)

    def test_classe_livro_metodos(self):
        l = Livro("Vidas Secas", "Graciliano Ramos", 1)
        self.assertTrue(l.esta_disponivel())

        # Empresta o único exemplar
        sucesso, _ = l.emprestar()
        self.assertTrue(sucesso)
        self.assertFalse(l.esta_disponivel())

        # Tenta emprestar novamente
        falha, _ = l.emprestar()
        self.assertFalse(falha)

        # Devolve
        dev, _ = l.devolver()
        self.assertTrue(dev)
        self.assertTrue(l.esta_disponivel())

    def test_biblioteca_persistencia(self):
        bib1 = Biblioteca()
        bib1.adicionar(Livro("Capitaes da Areia", "Jorge Amado", 3))
        bib1.salvar(caminho=ARQUIVO_TESTE)

        bib2 = Biblioteca()
        bib2.carregar(caminho=ARQUIVO_TESTE)
        self.assertEqual(len(bib2.acervo), 1)
        self.assertEqual(bib2.acervo[0].titulo, "Capitaes da Areia")
        self.assertEqual(bib2.acervo[0].disponiveis, 3)


if __name__ == "__main__":
    unittest.main()

🪜 Degrau 3: Banco de Dados Relacional com SQLite3 Nativo (1:N)

No terceiro degrau, criamos as tabelas livros e emprestimos com FOREIGN KEY, realizando consultas com JOIN e baixas transacionais.

Código-Fonte Completo (degrau_3_sqlite/main.py):

import sqlite3

BANCO_PADRAO = "biblioteca.db"

def inicializar_tabelas(caminho=BANCO_PADRAO):
    conn = sqlite3.connect(caminho)
    conn.execute("PRAGMA foreign_keys = ON;")
    cursor = conn.cursor()
    cursor.execute("""
        CREATE TABLE IF NOT EXISTS livros (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            titulo TEXT NOT NULL UNIQUE,
            autor TEXT,
            total_exemplares INTEGER NOT NULL,
            disponiveis INTEGER NOT NULL
        )
    """)
    cursor.execute("""
        CREATE TABLE IF NOT EXISTS emprestimos (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            livro_id INTEGER NOT NULL,
            nome_leitor TEXT NOT NULL,
            data_emprestimo TEXT DEFAULT (date('now', 'localtime')),
            status TEXT DEFAULT 'ATIVO' CHECK(status IN ('ATIVO', 'DEVOLVIDO')),
            FOREIGN KEY (livro_id) REFERENCES livros(id) ON DELETE CASCADE
        )
    """)
    conn.commit()
    conn.close()

Testes Automatizados (degrau_3_sqlite/test_degrau_3.py):

"""
Testes Unitários para o Degrau 3 do Projeto 05 (SQLite3 Relacional com Foreign Key)
"""

import os
import unittest
from main import (
    cadastrar_livro,
    inicializar_tabelas,
    listar_acervo,
    listar_emprestimos_ativos,
    realizar_emprestimo,
    registrar_devolucao
)

BANCO_TESTE = "teste_biblioteca_degrau3.db"


class TestDegrau3SQLite(unittest.TestCase):

    def setUp(self):
        if os.path.exists(BANCO_TESTE):
            os.remove(BANCO_TESTE)
        inicializar_tabelas(caminho=BANCO_TESTE)

    def tearDown(self):
        if os.path.exists(BANCO_TESTE):
            os.remove(BANCO_TESTE)

    def test_fluxo_emprestimo_e_devolucao(self):
        # 1. Cadastra livro com 1 exemplar
        livro_id = cadastrar_livro("Grande Sertao: Veredas", "Guimaraes Rosa", 1, caminho=BANCO_TESTE)
        self.assertIsNotNone(livro_id)

        # 2. Empresta o exemplar
        emp_ok = realizar_emprestimo(livro_id, "Maria Clara", caminho=BANCO_TESTE)
        self.assertTrue(emp_ok)

        # 3. Verifica acervo zerado
        acervo = listar_acervo(caminho=BANCO_TESTE)
        self.assertEqual(acervo[0][4], 0)  # disponiveis = 0

        # 4. Tenta emprestar novamente (deve falhar)
        emp_bloqueado = realizar_emprestimo(livro_id, "Joao Pedro", caminho=BANCO_TESTE)
        self.assertFalse(emp_bloqueado)

        # 5. Lista empréstimos ativos com JOIN
        ativos = listar_emprestimos_ativos(caminho=BANCO_TESTE)
        self.assertEqual(len(ativos), 1)
        emp_id = ativos[0][0]

        # 6. Devolve o livro
        dev_ok = registrar_devolucao(emp_id, caminho=BANCO_TESTE)
        self.assertTrue(dev_ok)

        # 7. Verifica acervo restaurado
        acervo_depois = listar_acervo(caminho=BANCO_TESTE)
        self.assertEqual(acervo_depois[0][4], 1)
        self.assertEqual(len(listar_emprestimos_ativos(caminho=BANCO_TESTE)), 0)


if __name__ == "__main__":
    unittest.main()

🪜 Degrau 4: Interface Desktop com Abas em Tkinter (ttk.Notebook)

No quarto degrau, o aluno constrói uma aplicação desktop moderna dividida em 3 abas temáticas com o widget ttk.Notebook.

Código-Fonte Completo (degrau_4_interface_tkinter/main.py):

import tkinter as tk
from tkinter import ttk

class AppBiblioteca:
    def __init__(self, root):
        self.root = root
        self.root.title("📚 Biblioteca Comunitária (Degrau 4)")
        self.root.geometry("680x560")

        self.notebook = ttk.Notebook(self.root)
        self.notebook.pack(fill="both", expand=True, padx=15, pady=10)

        # Abas
        self.tab_acervo = tk.Frame(self.notebook, bg="#FFFFFF")
        self.notebook.add(self.tab_acervo, text=" 📖 Acervo de Livros ")

        self.tab_emprestimo = tk.Frame(self.notebook, bg="#FFFFFF")
        self.notebook.add(self.tab_emprestimo, text=" 🤝 Novo Empréstimo ")

        self.tab_devolucao = tk.Frame(self.notebook, bg="#FFFFFF")
        self.notebook.add(self.tab_devolucao, text=" ↩️ Devoluções / Ativos ")

🎛️ Hub Central: Launcher Interativo (main.py)

O arquivo main.py na raiz do projeto funciona como um painel de controle: permite executar qualquer um dos 4 degraus (ou toda a suíte de testes) a partir de um único menu no terminal, sem precisar navegar manualmente entre as pastas.

Código-Fonte Completo (main.py):

"""
Hub Central / Launcher — Miniprojeto 05: Empréstimo de Livros da Biblioteca Comunitária
Permite executar de forma autoguiada qualquer um dos 4 degraus pedagógicos ou rodar a suíte de testes.
"""

import os
import subprocess
import sys

BASE_DIR = os.path.dirname(os.path.abspath(__file__))


def rodar_script(caminho_relativo):
    caminho_absoluto = os.path.join(BASE_DIR, caminho_relativo)
    diretorio = os.path.dirname(caminho_absoluto)
    print(f"\n🚀 Executando: {caminho_relativo}...\n")
    try:
        subprocess.run([sys.executable, caminho_absoluto], cwd=diretorio, check=True)
    except KeyboardInterrupt:
        print("\n\n⏹️ Execucao interrompida pelo usuario.")
    except Exception as e:
        print(f"\n❌ Erro durante a execucao: {e}")


def rodar_todos_os_testes():
    print("\n🧪 ========================================================")
    print("   RODANDO A SUÍTE DE TESTES DOS DEGRAUS 1, 2 E 3")
    print("========================================================\n")
    testes = [
        ("Degrau 1 (Imperativo & Arquivo TXT)", "degrau_1_imperativo_txt/test_degrau_1.py"),
        ("Degrau 2 (Primeira Classe POO)", "degrau_2_primeira_classe/test_degrau_2.py"),
        ("Degrau 3 (Banco SQLite3 Relacional)", "degrau_3_sqlite/test_degrau_3.py"),
    ]
    for nome, arq in testes:
        print(f"▶️ Testando {nome}...")
        caminho_abs = os.path.join(BASE_DIR, arq)
        diretorio = os.path.dirname(caminho_abs)
        res = subprocess.run([sys.executable, caminho_abs], cwd=diretorio)
        if res.returncode == 0:
            print(f"✅ {nome}: PASSOU!\n")
        else:
            print(f"❌ {nome}: FALHOU!\n")


def menu_hub():
    while True:
        print("\n========================================================")
        print("📚 MINIPROJETO 05: BIBLIOTECA COMUNITÁRIA (HUB)")
        print("========================================================")
        print("1. 🪜 Executar Degrau 1 (Imperativo & Arquivo .txt)")
        print("2. 🪜 Executar Degrau 2 (Primeira Classe POO: class Livro)")
        print("3. 🪜 Executar Degrau 3 (Banco de Dados SQLite3 Relacional)")
        print("4. 🪜 Executar Degrau 4 (Interface Desktop com Abas Tkinter)")
        print("5. 🧪 Rodar Todos os Testes Automatizados (Degraus 1, 2 e 3)")
        print("6. 🚪 Sair")
        print("========================================================")
        opcao = input("Escolha o degrau que deseja executar (1-6): ").strip()

        if opcao == "1":
            rodar_script("degrau_1_imperativo_txt/main.py")
        elif opcao == "2":
            rodar_script("degrau_2_primeira_classe/main.py")
        elif opcao == "3":
            rodar_script("degrau_3_sqlite/main.py")
        elif opcao == "4":
            rodar_script("degrau_4_interface_tkinter/main.py")
        elif opcao == "5":
            rodar_todos_os_testes()
        elif opcao == "6":
            print("👋 Encerrando o launcher. Bons estudos!")
            break
        else:
            print("❌ Opcao invalida. Digite de 1 a 6.")


if __name__ == "__main__":
    menu_hub()

🧪 Validação com Testes Automatizados

# Executar a suíte de testes do Miniprojeto 05:
python -c "import subprocess, sys; [subprocess.run([sys.executable, p], check=True) for p in ['catalogo_projetos/sistemas_python_intro_05_biblioteca_livros/degrau_1_imperativo_txt/test_degrau_1.py', 'catalogo_projetos/sistemas_python_intro_05_biblioteca_livros/degrau_2_primeira_classe/test_degrau_2.py', 'catalogo_projetos/sistemas_python_intro_05_biblioteca_livros/degrau_3_sqlite/test_degrau_3.py']]"

# Ou, de forma ainda mais simples, use o Hub Central (opção 5 do menu):
python main.py

Resultado obtido:

Ran 2 tests in 0.004s OK (Degrau 1)
Ran 2 tests in 0.005s OK (Degrau 2)
Ran 1 test in 0.068s OK (Degrau 3)


🏛️ Arquitetura Visual & Engenharia do Sistema

A imagem abaixo ilustra a esteira evolutiva dos 4 degraus didáticos do miniprojeto:

Arquitetura da Aplicação em 4 Degraus

flowchart TD
    subgraph S1["🪜 Degrau 1: Script Imperativo"]
        A1["Menu Terminal while"] --> A2["Arquivo de Texto/JSON"]
    end
    subgraph S2["🪜 Degrau 2: Orientação a Objetos"]
        B1["Modelo de Domínio POO"] --> B2["Encapsulamento de Regras"]
    end
    subgraph S3["🪜 Degrau 3: Banco Relacional"]
        C1["SQLite3 Local"] --> C2["Transações ACID & Agregações"]
    end
    subgraph S4["🪜 Degrau 4: Interface Visual"]
        D1["Aplicação Desktop / Web"] --> D2["UX Interativa Final"]
    end

    S1 --> S2
    S2 --> S3
    S3 --> S4

    style S1 fill:#fffde7,stroke:#fbc02d,stroke-width:1.5px
    style S2 fill:#e3f2fd,stroke:#1976d2,stroke-width:1.5px
    style S3 fill:#e8f5e9,stroke:#388e3c,stroke-width:1.5px
    style S4 fill:#f3e5f5,stroke:#7b1fa2,stroke-width:1.5px

🎯 Engenharia de Requisitos & Modelagem Formal

👤 User Stories (Histórias de Usuário)

ID Persona História de Usuário Critério de Aceite Prioridade
US-01 Bibliotecário Voluntário Desejo catalogar títulos e autores em arquivo texto simples Persistir acervo em livros.txt com status de disponibilidade Alta
US-02 Atendente da Biblioteca Desejo gerenciar operações de empréstimo e devolução em classe POO Classe Livro com métodos emprestar() e devolver() com validações Alta
US-03 Gestor do Acervo Desejo controlar datas limite e leitores com SQLite relacional Tabela de empréstimos com histórico de devoluções no SQLite3 Alta
US-04 Operador do Balcão Desejo janela desktop com busca rápida por título e botões de ação Interface Tkinter com campo de busca dinâmica e tabela de acervo Alta
US-05 Auditor de Acervo Desejo garantir que um livro já emprestado não possa ser emprestado de novo Testes unitários impedindo empréstimos duplicados do mesmo exemplar Média

⚙️ Requisitos Funcionais (RF)

ID Descrição do Requisito Criticidade
RF-01 Cadastro e recuperação de obras literárias em arquivo texto delimitado Alta
RF-02 Encapsulamento de estado (Disponível/Emprestado) na classe de domínio Livro Alta
RF-03 Schema SQLite para controle de obras, leitores e histórico de movimentações Alta
RF-04 Aplicação gráfica Tkinter com formulário de empréstimo e painel de status Alta
RF-05 Launcher unificado com execução de suíte de testes de integridade Média

🛡️ Requisitos Não-Funcionais (RNF)

ID Especificação Arquitetural Criticidade
RNF-01 Biblioteca padrão sem dependências de pacotes externos do pip Alta
RNF-02 Operações de busca em catálogo com latência inferior a 100ms Alta
RNF-03 Mensagens de erro instrutivas e amigáveis ao usuário não técnico Média
RNF-04 Conformidade com padrões PEP 8 de estilo e tipagem em Python Média
RNF-05 Execução consistente em qualquer sistema operacional suportado Alta

🔗 Matriz de Rastreabilidade (RTM)

User Story Requisito Funcional Requisito Não-Funcional Módulo / Artefato de Código Status de Homologação
US-01 RF-01 RNF-01, RNF-04 degrau_1_imperativo_txt/main.py Testado
US-02 RF-02 RNF-01, RNF-03 degrau_2_primeira_classe/main.py Testado
US-03 RF-03 RNF-02, RNF-05 degrau_3_sqlite/main.py Testado
US-04 RF-04 RNF-01, RNF-03 degrau_4_interface_tkinter/main.py Testado
US-05 RF-05 RNF-01, RNF-05 main.py & test_degrau_*.py Testado

🏁 Checkpoints de Qualidade & Homologação

Checkpoint Escopo da Validação Artefato Verificado Status
CP-01 Script Imperativo & Persistência Inicial degrau_1_* PASS ✅
CP-02 Modelagem Orientada a Objetos degrau_2_* PASS ✅
CP-03 Integridade Relacional no SQLite degrau_3_* PASS ✅
CP-04 Interface Visual & Usabilidade degrau_4_* PASS ✅
CP-05 Hub Launcher Central & Suíte de Testes main.py + test_*.py PASS ✅