Fix critical bugs from Deep Dive Audit (Phase 2)

**Porque foi feita essa alteração?**
Resolução de bugs críticos identificados na Segunda Passagem de Auditoria (Deep Dive):
1. 'rag_pipeline.py': Correção de SyntaxError (await em função síncrona) convertendo pipeline de ingestão para async.
2. 'pipeline.py': Remoção de campos duplicados na instanciação de 'AuditLog' que causavam erro de sintaxe/lógica.
3. 'zabbix_connector.py': Correção de query N+1 em 'get_neighbor_alerts' e adição de import faltante 'time'.
4. 'test_rag_pipeline.py': Atualização dos testes para suportar async e mocking correto do 'OllamaClient'.

**Quais testes foram feitos?**
- 'py_compile': Verificação de sintaxe em todos os arquivos modificados.
- 'flake8': Verificação de linting (apenas warnings de whitespace ignorados).
- 'pytest':
    - 'tests/test_rag_pipeline.py': Passou (13 testes).
    - 'tests/test_pipeline.py': Passou (6 testes).
    - 'tests/test_zabbix.py': Passou (9 testes).

**A alteração gerou um novo teste que precisa ser implementado no pipeline de testes?**
Sim, os testes do 'rag_pipeline' foram modernizados para 'asyncio' e devem ser mantidos no CI.
This commit is contained in:
João Pedro Toledo Goncalves 2026-02-01 14:44:02 -03:00
parent f69b990fa5
commit 3ad3161519
5 changed files with 41 additions and 29 deletions

View File

@ -108,8 +108,9 @@ Este documento serve como o roteiro técnico detalhado para a implementação do
- [x] **Baixa:** Validação dinâmica de domínios em `validators.py` - [x] **Baixa:** Validação dinâmica de domínios em `validators.py`
- [ ] **Refinamento e Correção:** - [ ] **Refinamento e Correção:**
- [x] Verificar todas as alterações - [x] Verificar todas as alterações
- [ ] **Segunda Passagem de Auditoria (Deep Dive)**: - [x] **Segunda Passagem de Auditoria (Deep Dive)**:
- [ ] Análise de regressão e pontos cegos pós-correção - [x] Análise de regressão e pontos cegos pós-correção
- Resultado: [AUDIT_DEEP_DIVE.md](file:///C:/Users/joao.goncalves/.gemini/antigravity/brain/0ae8ff87-2359-49bb-951c-6f6c593ee5db/AUDIT_DEEP_DIVE.md)
- [ ] Validar ausência de regressões - [ ] Validar ausência de regressões
## Fase 7: Homologação e Go-Live 🔄 ## Fase 7: Homologação e Go-Live 🔄

View File

@ -11,8 +11,8 @@ from typing import Optional
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime, timezone from datetime import datetime, timezone
from src.agents.triage_agent import TriageAgent, TriageResult, get_triage_agent from src.agents.triage_agent import TriageResult, get_triage_agent
from src.agents.specialist_agent import SpecialistAgent, SpecialistResponse, get_specialist_agent from src.agents.specialist_agent import SpecialistResponse, get_specialist_agent
from src.database.connection import get_db_manager from src.database.connection import get_db_manager
from src.models import AuditLog, ResolutionStatus, TicketContext from src.models import AuditLog, ResolutionStatus, TicketContext
from src.security import sanitize_text from src.security import sanitize_text
@ -276,9 +276,6 @@ class TicketPipeline:
# Create log # Create log
return AuditLog( return AuditLog(
ticket_id=ticket_id,
tenant_id=triage.tenant.id if triage.tenant else "UNKNOWN",
sender_email=sanitize_text(sender_email),
ticket_id=ticket_id, ticket_id=ticket_id,
tenant_id=triage.tenant.id if triage.tenant else "UNKNOWN", tenant_id=triage.tenant.id if triage.tenant else "UNKNOWN",
sender_email=sanitize_text(sender_email), sender_email=sanitize_text(sender_email),

View File

@ -6,7 +6,8 @@ infrastructure diagnostics and root cause analysis.
""" """
import logging import logging
from typing import Optional, Any import time
from typing import Optional
from dataclasses import dataclass from dataclasses import dataclass
from zabbix_utils import ZabbixAPI from zabbix_utils import ZabbixAPI
@ -284,8 +285,7 @@ class ZabbixConnector:
if not neighbor_ids: if not neighbor_ids:
return [] return []
# Get problems for neighbor hosts # Get problems for neighbor hosts (using selectHosts to avoid N+1)
import time
time_from = int(time.time()) - (time_window_minutes * 60) time_from = int(time.time()) - (time_window_minutes * 60)
problems = self._api.problem.get( problems = self._api.problem.get(
@ -294,13 +294,17 @@ class ZabbixConnector:
recent=True, recent=True,
sortfield="eventid", sortfield="eventid",
sortorder="DESC", sortorder="DESC",
selectHosts=["hostid", "host"], # Fetch host info in same query
output=["eventid", "objectid", "severity", "name", output=["eventid", "objectid", "severity", "name",
"acknowledged", "clock"] "acknowledged", "clock"]
) )
result = [] result = []
for p in problems: for p in problems:
host_info = self._get_host_for_trigger(p.get("objectid")) # Extract host info from payload (no extra API call)
hosts = p.get("hosts", [])
host_info = hosts[0] if hosts else {}
result.append(Problem( result.append(Problem(
event_id=p["eventid"], event_id=p["eventid"],
host_id=host_info.get("hostid", ""), host_id=host_info.get("hostid", ""),

View File

@ -5,7 +5,7 @@ Processes Markdown and PDF documents, extracts text,
generates embeddings and indexes in Qdrant. generates embeddings and indexes in Qdrant.
""" """
import os
import re import re
import hashlib import hashlib
import logging import logging
@ -94,7 +94,7 @@ class RAGIngestionPipeline:
self._qdrant = get_qdrant_client() self._qdrant = get_qdrant_client()
self._ollama = get_ollama_client() self._ollama = get_ollama_client()
def ingest_directory( async def ingest_directory(
self, self,
directory: str, directory: str,
tenant_id: str, tenant_id: str,
@ -137,7 +137,7 @@ class RAGIngestionPipeline:
logger.info(f"Found {len(files)} documents in {directory}") logger.info(f"Found {len(files)} documents in {directory}")
for filepath in files: for filepath in files:
result = self.ingest_file( result = await self.ingest_file(
filepath=str(filepath), filepath=str(filepath),
tenant_id=tenant_id, tenant_id=tenant_id,
doc_type=doc_type doc_type=doc_type
@ -146,7 +146,7 @@ class RAGIngestionPipeline:
return results return results
def ingest_file( async def ingest_file(
self, self,
filepath: str, filepath: str,
tenant_id: str, tenant_id: str,

View File

@ -8,7 +8,7 @@ import pytest
import tempfile import tempfile
import os import os
from pathlib import Path from pathlib import Path
from unittest.mock import Mock, patch from unittest.mock import Mock, patch, AsyncMock
from src.flywheel.rag_pipeline import ( from src.flywheel.rag_pipeline import (
RAGIngestionPipeline, RAGIngestionPipeline,
@ -42,10 +42,16 @@ class TestRAGPipeline:
@pytest.fixture @pytest.fixture
def pipeline(self): def pipeline(self):
"""Create pipeline with mocked Qdrant.""" """Create pipeline with mocked Qdrant and Ollama."""
with patch('src.flywheel.rag_pipeline.get_qdrant_client') as mock: with patch('src.flywheel.rag_pipeline.get_qdrant_client') as mock_qdrant, \
mock.return_value = Mock() patch('src.flywheel.rag_pipeline.get_ollama_client') as mock_ollama:
mock.return_value.upsert_document = Mock(return_value=True) mock_qdrant.return_value = Mock()
mock_qdrant.return_value.upsert_document = Mock(return_value=True)
# Mock Ollama client for embeddings
mock_ollama.return_value = Mock()
mock_ollama.return_value.get_embeddings = AsyncMock(return_value=[0.1] * 384)
return RAGIngestionPipeline() return RAGIngestionPipeline()
def test_sanitize_removes_scripts(self, pipeline): def test_sanitize_removes_scripts(self, pipeline):
@ -120,16 +126,19 @@ class TestRAGPipeline:
assert id1 != id2 assert id1 != id2
assert id1 == id3 # Same inputs should give same ID assert id1 == id3 # Same inputs should give same ID
def test_generate_embedding(self, pipeline): @pytest.mark.asyncio
"""Test embedding generation.""" async def test_generate_embedding(self, pipeline):
emb = pipeline._generate_embedding("test content") """Test embedding generation via Ollama."""
emb = await pipeline._generate_embedding("test content")
assert len(emb) == 384 # Embedding should be returned from mock
assert all(-1 <= v <= 1 for v in emb) assert isinstance(emb, list)
assert len(emb) > 0
def test_ingest_file_not_found(self, pipeline): @pytest.mark.asyncio
async def test_ingest_file_not_found(self, pipeline):
"""Test ingestion of non-existent file.""" """Test ingestion of non-existent file."""
result = pipeline.ingest_file( result = await pipeline.ingest_file(
filepath="/nonexistent/file.md", filepath="/nonexistent/file.md",
tenant_id="tenant-001" tenant_id="tenant-001"
) )
@ -137,14 +146,15 @@ class TestRAGPipeline:
assert result.success is False assert result.success is False
assert "not found" in result.error.lower() assert "not found" in result.error.lower()
def test_ingest_file_success(self, pipeline): @pytest.mark.asyncio
async def test_ingest_file_success(self, pipeline):
"""Test successful file ingestion.""" """Test successful file ingestion."""
with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False) as f: with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False) as f:
f.write("# Test Document\n\nThis is a test about Linux servers.") f.write("# Test Document\n\nThis is a test about Linux servers.")
filepath = f.name filepath = f.name
try: try:
result = pipeline.ingest_file( result = await pipeline.ingest_file(
filepath=filepath, filepath=filepath,
tenant_id="tenant-001" tenant_id="tenant-001"
) )