aida-api / testes /test_cache_analise.py
AIDAON's picture
Claude Opus 5.5
Valida dimensoes da imagem e versiona a suite de testes
5340e92
Raw History Blame Contribute Delete
5.41 kB
"""Cache de análises: gravação, leitura, invalidação e o que NÃO é persistido.
O cache é a peça de que o AIDA Forensics depende inteiramente. Se ele guardar
demais, o disco cresce sem controle; se guardar de menos ou entregar dados de
outra versão do modelo, o dossiê passa a contradizer o resultado que o originou.
"""
from __future__ import annotations
import json
import time
import pytest
def test_guarda_e_recupera(cache, payload_analise):
cache.guardar(payload_analise("ABC-1"))
lido = cache.recuperar_analise("ABC-1")
assert lido is not None
assert lido["resultado"] == "INCONCLUSIVO"
assert lido["scores_modulos"] == {"forense": 0.31, "clip": 0.55}
def test_nao_persiste_o_vetor_de_caracteristicas(cache, payload_analise):
"""659 colunas que nenhuma tela usa não podem ir para o disco."""
cache.guardar(payload_analise("ABC-2"))
lido = cache.recuperar_analise("ABC-2")
assert "principais_metricas" not in lido
# O que a tela usa continua lá.
assert lido["qualidade"]["qualidade_score"] == pytest.approx(0.62)
def test_nao_persiste_caminho_do_servidor(cache, payload_analise):
"""Caminho absoluto expõe a estrutura de diretórios da implantação."""
cache.guardar(payload_analise("ABC-3"))
lido = cache.recuperar_analise("ABC-3")
assert lido["caminho_imagem"] == ""
# Dos mapas sobra o nome do arquivo, não o caminho.
for nome in lido["evidencias"]["arquivos"].values():
assert "/" not in nome and "\\" not in nome
def test_tipos_do_numpy_sao_serializados(cache, payload_analise):
"""`np.float32` no payload não pode derrubar a gravação inteira."""
numpy = pytest.importorskip("numpy")
payload = payload_analise("ABC-4")
payload["scores_modulos"] = {"forense": numpy.float32(0.42)}
payload["duracao_s"] = numpy.float64(2.5)
payload["evidencias"]["perfil_radial"] = numpy.array([1.0, 2.0])
assert cache.guardar(payload) is not None
lido = cache.recuperar_analise("ABC-4")
assert lido["scores_modulos"]["forense"] == pytest.approx(0.42, abs=1e-6)
def test_identificador_invalido_nao_grava(cache, payload_analise):
"""`../` na chave escaparia do diretório de cache."""
assert cache.guardar(payload_analise("ABC-5"), id_analise="../fuga") is None
assert cache.recuperar_analise("../fuga") is None
def test_expira_por_tempo(cache, payload_analise):
cache.guardar(payload_analise("ABC-6"))
caminho = cache.caminho_da_analise("ABC-6")
registro = json.loads(caminho.read_text(encoding="utf-8"))
registro["gravado_em"] = time.time() - 3600
caminho.write_text(json.dumps(registro), encoding="utf-8")
assert cache.recuperar_analise("ABC-6", ttl_s=60) is None
assert cache.recuperar_analise("ABC-6", ttl_s=7200) is not None
def test_ttl_zero_desliga_expiracao(cache, payload_analise, monkeypatch):
monkeypatch.setenv("AIDA_CACHE_TTL_H", "0")
cache.guardar(payload_analise("ABC-7"))
caminho = cache.caminho_da_analise("ABC-7")
registro = json.loads(caminho.read_text(encoding="utf-8"))
registro["gravado_em"] = 0.0
caminho.write_text(json.dumps(registro), encoding="utf-8")
assert cache.recuperar_analise("ABC-7") is not None
def test_versao_de_modelo_diferente_invalida(cache, payload_analise):
"""Dossiê precisa corresponder ao modelo que produziu a decisão."""
cache.guardar(payload_analise("ABC-8"))
assert cache.recuperar_analise("ABC-8", versao_modelo="v-outra") is None
assert cache.recuperar_analise("ABC-8") is not None
def test_invalidacao_manual_remove_tambem_o_indice(cache, payload_analise):
cache.guardar(payload_analise("ABC-9"))
assert cache.recuperar_por_conteudo("a" * 64) is not None
assert cache.invalidar("ABC-9") is True
assert cache.recuperar_analise("ABC-9") is None
# Ponteiro órfão faria a consulta por conteúdo devolver um id inexistente.
assert cache.recuperar_por_conteudo("a" * 64) is None
assert cache.invalidar("ABC-9") is False
def test_indice_por_conteudo_encontra_analise_anterior(cache, payload_analise):
cache.guardar(payload_analise("ABC-10", hash_sha256="b" * 64))
registro = cache.recuperar_por_conteudo("b" * 64)
assert registro is not None
assert registro["id_analise"] == "ABC-10"
def test_arquivo_corrompido_e_descartado(cache, payload_analise):
cache.guardar(payload_analise("ABC-11"))
cache.caminho_da_analise("ABC-11").write_text("{ isto não é json", encoding="utf-8")
assert cache.recuperar_analise("ABC-11") is None
# Descartado no ato: manter o arquivo faria toda leitura futura repetir a
# tentativa e o log encher com o mesmo erro.
assert not cache.caminho_da_analise("ABC-11").exists()
def test_limpar_remove_apenas_o_que_expirou(cache, payload_analise):
cache.guardar(payload_analise("NOVA"))
cache.guardar(payload_analise("VELHA"))
velha = cache.caminho_da_analise("VELHA")
antigo = time.time() - 10 * 24 * 3600
import os
os.utime(velha, (antigo, antigo))
assert cache.limpar() == 1
assert cache.recuperar_analise("NOVA") is not None
assert cache.recuperar_analise("VELHA") is None
def test_listar_devolve_apenas_validas(cache, payload_analise):
cache.guardar(payload_analise("L-1"))
cache.guardar(payload_analise("L-2"))
assert sorted(cache.listar()) == ["L-1", "L-2"]