diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 872edf2..30eed58 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,6 +1,7 @@ name: CI on: + workflow_dispatch: push: branches: - main @@ -111,7 +112,7 @@ jobs: run: npm run audit:security validate-frontend: - name: Build Frontend (React + Vite) + name: Test and Build Frontend (React + Vite) runs-on: ubuntu-latest steps: @@ -129,14 +130,14 @@ jobs: working-directory: frontend run: npm ci - - name: Build Production Assets - working-directory: frontend - run: npm run build - - name: Run frontend tests working-directory: frontend run: npm test + - name: Build Production Assets + working-directory: frontend + run: npm run build + validate-ai-service: name: Validate AI Service (Python / FastAPI) runs-on: ubuntu-latest diff --git a/TESTING.md b/TESTING.md new file mode 100644 index 0000000..4c48cc4 --- /dev/null +++ b/TESTING.md @@ -0,0 +1,65 @@ +# PRE-04 — Fundação de testes + +Os três serviços possuem suítes determinísticas executadas pela CI em +`.github/workflows/ci.yml`. Esta fundação não implementa funcionalidades pendentes +dos PBIs e não substitui testes ponta a ponta com serviços reais. + +## Execução local + +Use Node.js 20 e Python 3.11, as mesmas versões configuradas na CI. +Execute cada grupo no diretório indicado: + +| Diretório | Instalação | Testes | Compilação | +| --- | --- | --- | --- | +| `backend` | `npm ci` | `npm test` | `npm run build` | +| `frontend` | `npm ci` | `npm test` | `npm run build` | +| `ai-service` | `python -m pip install -r requirements.txt` | `python -m unittest discover -s tests -v` | `python -m py_compile main.py config.py services/__init__.py services/chunker.py services/ollama_client.py` | + +Para Python, recomenda-se um ambiente virtual local (`python -m venv .venv`). +Ative-o antes de instalar dependências e executar os comandos. + +## Cobertura inicial + +- Backend: testes de configuração, autenticação, autorização, seed e projetos, incluindo regras de + negócio e HTTP com repositório em memória. `scripts/test.mjs` descobre arquivos + `src/**/*.test.ts` recursivamente sem depender da expansão de globs do shell. + Nenhum arquivo encontrado ou falha em qualquer teste retorna código diferente de zero. +- Frontend: Vitest, Testing Library e jsdom; autenticação, projetos, navegação e ações explícitas sobre sugestões de IA, + botão desabilitado, erro acessível de formulário e limites do indicador de progresso. + Novos arquivos `*.test.ts`/`*.test.tsx` são descobertos pelo Vitest. O comando não + permanece em modo watch e falha se não encontrar testes. +- IA: testes existentes de chunking/metadados e novos testes HTTP de validação, + embeddings, contexto RAG e indisponibilidade do Ollama. Chamadas ao cliente de + modelos usam `AsyncMock`; não exigem GPU, Ollama, banco ou conexão externa. + +Dados de teste são sintéticos. Não adicionar tokens, senhas reais, documentos de +clientes, dumps de ambiente ou respostas privadas aos testes e logs. + +## Integração contínua + +Os jobs do backend, frontend e IA executam os mesmos comandos acima em pull requests +para `main`, `HT-setup` e `develop`, e em pushes nessas branches. O workflow também +permite execução manual (`workflow_dispatch`). Um comando de teste com falha reprova +seu job; não há `continue-on-error` nem tolerância automática por repetição. +Os checks existentes de n8n, Docker Compose e auditoria do backend foram mantidos. + +## Limites e conclusão + +Após integrar a main em 16/09/2026, a validação local em Windows passou com +52 testes de backend (1 teste de PostgreSQL ignorado localmente), 50 de frontend +e 9 de IA. Os builds TypeScript/Vite e a compilação Python passaram. A auditoria +das dependências de produção do backend não encontrou vulnerabilidades. +O teste de banco exige SEED_TEST_DATABASE_URL e é executado no job +validate-seed com PostgreSQL/pgvector; não deve ser contado como aprovado localmente. +A CI usa Node.js 20/Python 3.11; a validação local usa Node.js 22/Python 3.12. + +Os PBIs referenciados orientam a expansão da suíte: completude, busca semântica, +respostas fundamentadas e autenticação. A fundação não comprova que esses PBIs estão +concluídos. Em particular, os testes RAG verificam o transporte do contexto, não a +qualidade factual de um modelo real. Os testes de autenticação e projetos já integrados à main foram preservados, +assim como os scripts de seed e test:integration:s1. + +Nenhum endpoint foi alterado; não há alteração de OpenAPI nesta tarefa. +Para encerrar a PRE-04, confirmar uma execução remota verde dos três jobs no PR e +obter revisão por outra pessoa. Tornar os checks obrigatórios para merge depende +das regras de proteção da branch no GitHub, que não são alteradas por este commit. diff --git a/ai-service/tests/test_api.py b/ai-service/tests/test_api.py new file mode 100644 index 0000000..e4dde30 --- /dev/null +++ b/ai-service/tests/test_api.py @@ -0,0 +1,55 @@ +import unittest +from unittest.mock import AsyncMock, patch + +from fastapi.testclient import TestClient +from main import app, ollama_client + + +class ApiTests(unittest.TestCase): + def setUp(self): + self.client = self.enterContext(TestClient(app)) + # All model calls are mocked: tests never require a running Ollama. + self.embedding = self.enterContext(patch.object( + ollama_client, "get_embedding", new_callable=AsyncMock)) + self.generate = self.enterContext(patch.object( + ollama_client, "generate_response", new_callable=AsyncMock)) + self.health = self.enterContext(patch.object( + ollama_client, "check_health", new_callable=AsyncMock)) + + def test_health_reports_unavailable_dependency(self): + self.health.return_value = False + result = self.client.get("/health") + self.assertEqual(result.status_code, 200) + self.assertEqual(result.json()["status"], "degraded") + + def test_missing_embedding_text_is_rejected_before_model_call(self): + result = self.client.post("/embeddings", json={}) + self.assertEqual(result.status_code, 422) + self.embedding.assert_not_awaited() + + def test_embedding_returns_model_vector_and_dimension(self): + self.embedding.return_value = [0.25, 0.5] + result = self.client.post("/embeddings", json={"text": "Requisito", "model": "test-model"}) + self.assertEqual(result.status_code, 200) + self.assertEqual(result.json(), {"model": "test-model", "dimension": 2, "embedding": [0.25, 0.5]}) + self.embedding.assert_awaited_once_with("Requisito", model="test-model") + + def test_embedding_dependency_failure_returns_502(self): + self.embedding.side_effect = RuntimeError("test dependency unavailable") + self.assertEqual(self.client.post("/embeddings", json={"text": "Requisito"}).status_code, 502) + + def test_rag_preserves_context_and_project_in_response(self): + self.generate.return_value = "Resposta simulada [doc-1]" + result = self.client.post("/rag/query", json={ + "query": "Como funciona?", "project_id": "project-1", + "context_chunks": ["doc-1: evidência de teste"], + }) + self.assertEqual(result.status_code, 200) + self.assertEqual(result.json()["project_id"], "project-1") + self.assertEqual(result.json()["context_chunks_used"], 1) + self.assertEqual(result.json()["response"], "Resposta simulada [doc-1]") + self.assertIn("doc-1: evidência de teste", self.generate.await_args.args[0]) + + def test_rag_dependency_failure_returns_502(self): + self.generate.side_effect = RuntimeError("test dependency unavailable") + self.assertEqual(self.client.post("/rag/query", json={"query": "Pergunta"}).status_code, 502) diff --git a/frontend/src/components/ui/index.test.tsx b/frontend/src/components/ui/index.test.tsx new file mode 100644 index 0000000..39f505d --- /dev/null +++ b/frontend/src/components/ui/index.test.tsx @@ -0,0 +1,37 @@ +// @vitest-environment jsdom +import { afterEach, expect, it, vi } from "vitest"; +import { cleanup, fireEvent, render, screen } from "@testing-library/react"; +import { AISuggestion, Button, Field, Progress } from "./index"; + +afterEach(cleanup); + +it("botão desabilitado não executa a ação", () => { + const action = vi.fn(); + render(); + fireEvent.click(screen.getByRole("button", { name: "Salvar" })); + expect(action).not.toHaveBeenCalled(); +}); + +it("campo apresenta erro com alerta e mantém rótulo acessível", () => { + render(); + expect(screen.getByLabelText(/Nome/)).toBeTruthy(); + expect(screen.getByRole("alert").textContent).toBe("Nome obrigatório"); +}); + +it.each([[-20, 0], [45, 45], [150, 100]])("progresso %s fica no intervalo acessível", (value, expected) => { + render(); + expect(screen.getByRole("progressbar", { name: "Completude" }).getAttribute("aria-valuenow")).toBe(String(expected)); +}); + +it("sugestão de IA exige ação explícita e diferencia aceitar, editar e descartar", () => { + const accept = vi.fn(); const edit = vi.fn(); const discard = vi.fn(); + render(Sugestão de requisito); + expect(accept).not.toHaveBeenCalled(); + fireEvent.click(screen.getByRole("button", { name: "Editar antes de aceitar" })); + expect(edit).toHaveBeenCalledTimes(1); + expect(accept).not.toHaveBeenCalled(); + fireEvent.click(screen.getByRole("button", { name: "Descartar" })); + expect(discard).toHaveBeenCalledTimes(1); + fireEvent.click(screen.getByRole("button", { name: "Aceitar" })); + expect(accept).toHaveBeenCalledTimes(1); +});