Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
name: CI

on:
workflow_dispatch:
push:
branches:
- main
Expand Down Expand Up @@ -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:
Expand All @@ -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
Expand Down
65 changes: 65 additions & 0 deletions TESTING.md
Original file line number Diff line number Diff line change
@@ -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.
55 changes: 55 additions & 0 deletions ai-service/tests/test_api.py
Original file line number Diff line number Diff line change
@@ -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)
37 changes: 37 additions & 0 deletions frontend/src/components/ui/index.test.tsx
Original file line number Diff line number Diff line change
@@ -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(<Button disabled onClick={action}>Salvar</Button>);
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(<Field label="Nome" error="Nome obrigatório"><input /></Field>);
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(<Progress value={value} label="Completude" />);
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(<AISuggestion onAccept={accept} onEdit={edit} onDiscard={discard}>Sugestão de requisito</AISuggestion>);
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);
});
Loading