03-oct: superguardado automático
This commit is contained in:
13
tests/conftest.py
Normal file
13
tests/conftest.py
Normal file
@@ -0,0 +1,13 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""conftest.py — Deja importable el paquete `monitor_estado` en los tests.
|
||||
|
||||
Anade INFRAESTRUCTURA/scripts al sys.path para poder hacer
|
||||
`import monitor_estado.<modulo>` sin instalar el paquete.
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
SCRIPTS_DIR = Path(__file__).resolve().parent.parent / "scripts"
|
||||
if str(SCRIPTS_DIR) not in sys.path:
|
||||
sys.path.insert(0, str(SCRIPTS_DIR))
|
||||
73
tests/test_archivo.py
Normal file
73
tests/test_archivo.py
Normal file
@@ -0,0 +1,73 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Tests de `monitor_estado.archivo` — ficheros del dia, reescritura y poda (RF-5,7,9,10,18)."""
|
||||
|
||||
from datetime import date, timedelta
|
||||
|
||||
import monitor_estado.archivo as archivo
|
||||
|
||||
|
||||
def _render(m):
|
||||
return f"- **{m['id']}**: {m['t']}"
|
||||
|
||||
|
||||
def test_archivo_reescribe_sin_duplicados(tmp_path, monkeypatch):
|
||||
"""El fichero del dia contiene todo lo de hoy y nada duplicado (RF-9)."""
|
||||
monkeypatch.setattr(archivo, "MONITOR_DIR", tmp_path)
|
||||
hoy = date(2026, 10, 3)
|
||||
|
||||
p = archivo.escribe_dia(
|
||||
"telegram/test", hoy,
|
||||
[{"id": 1, "t": "a"}, {"id": 2, "t": "b"}, {"id": 2, "t": "b"}],
|
||||
id_fn=lambda m: m["id"], render_fn=_render,
|
||||
)
|
||||
contenido = p.read_text(encoding="utf-8")
|
||||
assert contenido.count("- **1**") == 1
|
||||
assert contenido.count("- **2**") == 1
|
||||
|
||||
# Nuevo ciclo con lo del dia completo: el fichero se reescribe con el estado actual.
|
||||
archivo.escribe_dia(
|
||||
"telegram/test", hoy,
|
||||
[{"id": 1, "t": "a"}, {"id": 2, "t": "b"}, {"id": 3, "t": "c"}],
|
||||
id_fn=lambda m: m["id"], render_fn=_render,
|
||||
)
|
||||
contenido = p.read_text(encoding="utf-8")
|
||||
assert "- **3**" in contenido
|
||||
assert contenido.count("- **1**") == 1
|
||||
|
||||
|
||||
def test_archivo_poda_30_dias_y_conserva_recientes(tmp_path, monkeypatch):
|
||||
"""Se borran los ficheros de mas de 30 dias y se conservan los recientes (RF-10)."""
|
||||
monkeypatch.setattr(archivo, "MONITOR_DIR", tmp_path)
|
||||
hoy = date(2026, 10, 3)
|
||||
reciente = tmp_path / "telegram" / "test"
|
||||
reciente.mkdir(parents=True)
|
||||
f_hoy = reciente / f"{hoy.isoformat()}.md"
|
||||
f_10 = reciente / f"{(hoy - timedelta(days=10)).isoformat()}.md"
|
||||
f_40 = reciente / f"{(hoy - timedelta(days=40)).isoformat()}.md"
|
||||
for f in (f_hoy, f_10, f_40):
|
||||
f.write_text("x", encoding="utf-8")
|
||||
|
||||
borrados = archivo.poda(retencion_dias=30, hoy=hoy)
|
||||
|
||||
assert f_hoy.exists() and f_10.exists()
|
||||
assert not f_40.exists()
|
||||
assert f_40 in borrados
|
||||
|
||||
|
||||
def test_escribe_resumen(tmp_path, monkeypatch):
|
||||
"""El resumen se guarda en md y json consultables (RF-18)."""
|
||||
monkeypatch.setattr(archivo, "MONITOR_DIR", tmp_path)
|
||||
resumen = {"resumen_md": "Todo ok", "requiere_atencion": [], "errores": []}
|
||||
archivo.escribe_resumen(resumen)
|
||||
|
||||
assert (tmp_path / "resumen_actual.md").read_text(encoding="utf-8") == "Todo ok"
|
||||
assert '"resumen_md": "Todo ok"' in (tmp_path / "resumen_actual.json").read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_escribe_dia_de_fuente_sin_novedad(tmp_path, monkeypatch):
|
||||
"""Una fuente sin elementos genera un fichero vacio (no falla) (RF-5)."""
|
||||
monkeypatch.setattr(archivo, "MONITOR_DIR", tmp_path)
|
||||
p = archivo.escribe_dia(
|
||||
"correo", date(2026, 10, 3), [], id_fn=lambda m: m, render_fn=lambda m: str(m)
|
||||
)
|
||||
assert p.exists()
|
||||
32
tests/test_aviso.py
Normal file
32
tests/test_aviso.py
Normal file
@@ -0,0 +1,32 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Tests de `monitor_estado.aviso` — voz solo si requiere atencion (RF-14, RF-15)."""
|
||||
|
||||
import monitor_estado.aviso as aviso
|
||||
|
||||
|
||||
def test_aviso_dispara_con_atencion():
|
||||
llamadas = []
|
||||
r = {"fallo_ia": False, "requiere_atencion": [{"fuente": "jira", "texto": "caido"}]}
|
||||
assert aviso.avisar(r, cfg={"aviso": {"voz": True}}, _runner=lambda t: llamadas.append(t)) is True
|
||||
assert llamadas == ["fin"]
|
||||
|
||||
|
||||
def test_ciclo_sin_novedad_no_avisa():
|
||||
llamadas = []
|
||||
r = {"fallo_ia": False, "requiere_atencion": [], "resumen_md": "Sin novedades."}
|
||||
assert aviso.avisar(r, cfg={"aviso": {"voz": True}}, _runner=lambda t: llamadas.append(t)) is False
|
||||
assert llamadas == []
|
||||
|
||||
|
||||
def test_aviso_dispara_con_fallo_ia():
|
||||
llamadas = []
|
||||
r = {"fallo_ia": True, "requiere_atencion": []}
|
||||
assert aviso.avisar(r, cfg={"aviso": {"voz": True}}, _runner=lambda t: llamadas.append(t)) is True
|
||||
assert llamadas == ["error"]
|
||||
|
||||
|
||||
def test_aviso_desactivado_por_config():
|
||||
llamadas = []
|
||||
r = {"fallo_ia": False, "requiere_atencion": [{"texto": "x"}]}
|
||||
assert aviso.avisar(r, cfg={"aviso": {"voz": False}}, _runner=lambda t: llamadas.append(t)) is False
|
||||
assert llamadas == []
|
||||
81
tests/test_ciclo.py
Normal file
81
tests/test_ciclo.py
Normal file
@@ -0,0 +1,81 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Tests de `monitor_estado.ciclo` — aislamiento de fuentes y ciclo completo (RF-6, RF-9)."""
|
||||
|
||||
from datetime import date
|
||||
|
||||
import monitor_estado.archivo as archivo
|
||||
import monitor_estado.ciclo as ciclo
|
||||
import monitor_estado.vistos as vistos
|
||||
|
||||
|
||||
class _EmptyJira:
|
||||
def search(self, jql, fields=None, max_results=50):
|
||||
return {"issues": []}
|
||||
|
||||
|
||||
def _preparar_tmp(tmp_path, monkeypatch):
|
||||
monkeypatch.setattr(archivo, "MONITOR_DIR", tmp_path)
|
||||
monkeypatch.setattr(vistos, "VISTO_DIR", tmp_path / "visto")
|
||||
|
||||
|
||||
def test_aislamiento_fuente_fallida_continua(tmp_path, monkeypatch):
|
||||
"""Si todas las fuentes fallan, el ciclo termina y registra los errores (RF-6)."""
|
||||
_preparar_tmp(tmp_path, monkeypatch)
|
||||
|
||||
def bad_tg(cfg, hoy):
|
||||
raise RuntimeError("telegram caido")
|
||||
|
||||
def bad_correo(horas=24, limite=50):
|
||||
raise RuntimeError("gmail caido")
|
||||
|
||||
class BadJira:
|
||||
def search(self, *a, **k):
|
||||
raise RuntimeError("jira caido")
|
||||
|
||||
res = ciclo.ejecutar(
|
||||
cfg={"retencion_dias": 30, "correo": {"horas": 24}},
|
||||
deps={
|
||||
"telegram_recoger": bad_tg,
|
||||
"correo_recoger": bad_correo,
|
||||
"jira_cliente": BadJira(),
|
||||
"ia": lambda p: '{"resumen_md": "x", "requiere_atencion": []}',
|
||||
"avisar": lambda r, cfg=None, _runner=None: False,
|
||||
},
|
||||
)
|
||||
assert len(res["errores"]) == 3
|
||||
assert res["fallo_ia"] is False
|
||||
assert (tmp_path / "ultimo_ciclo.json").exists()
|
||||
|
||||
|
||||
def test_ciclo_archiva_y_avisa(tmp_path, monkeypatch):
|
||||
"""Un ciclo con novedad archiva el dia y avisa si hay atencion (RF-9, RF-14)."""
|
||||
_preparar_tmp(tmp_path, monkeypatch)
|
||||
emitidos = []
|
||||
|
||||
def tg(cfg, hoy):
|
||||
return ({"oscar_rodriguez": [{"id": 1, "fecha": "2026-10-03T09:00:00", "quien": "Oscar", "texto": "incidencia"}]}, [])
|
||||
|
||||
def correo(horas=24, limite=50):
|
||||
return []
|
||||
|
||||
def ia(prompt):
|
||||
return '{"resumen_md": "hay incidencia", "requiere_atencion": [{"fuente": "telegram", "texto": "incidencia"}]}'
|
||||
|
||||
def avisar(res, cfg=None, _runner=None):
|
||||
emitidos.append(res.get("requiere_atencion"))
|
||||
return True
|
||||
|
||||
res = ciclo.ejecutar(
|
||||
cfg={"retencion_dias": 30, "correo": {"horas": 24}},
|
||||
hoy=date(2026, 10, 3),
|
||||
deps={
|
||||
"telegram_recoger": tg,
|
||||
"correo_recoger": correo,
|
||||
"jira_cliente": _EmptyJira(),
|
||||
"ia": ia,
|
||||
"avisar": avisar,
|
||||
},
|
||||
)
|
||||
assert (tmp_path / "telegram" / "oscar_rodriguez" / "2026-10-03.md").exists()
|
||||
assert res["aviso_emitido"] is True
|
||||
assert emitidos and emitidos[0]
|
||||
74
tests/test_correo.py
Normal file
74
tests/test_correo.py
Normal file
@@ -0,0 +1,74 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Tests de `monitor_estado.fuentes.correo` — ventana 24h y solo lectura (RF-2, RF-25)."""
|
||||
|
||||
import monitor_estado.fuentes.correo as correo
|
||||
|
||||
|
||||
class _Req:
|
||||
def __init__(self, resp):
|
||||
self._resp = resp
|
||||
|
||||
def execute(self):
|
||||
return self._resp
|
||||
|
||||
|
||||
class _Messages:
|
||||
def __init__(self):
|
||||
self.queries = []
|
||||
self.gets = []
|
||||
self.modificados = []
|
||||
|
||||
def list(self, userId, q, maxResults):
|
||||
self.queries.append(q)
|
||||
return _Req({"messages": [{"id": "m1"}, {"id": "m2"}]})
|
||||
|
||||
def get(self, userId, id, format, metadataHeaders):
|
||||
self.gets.append(id)
|
||||
return _Req({
|
||||
"snippet": f"resumen de {id}",
|
||||
"payload": {"headers": [
|
||||
{"name": "Subject", "value": f"Asunto {id}"},
|
||||
{"name": "From", "value": "Remitente <r@b.com>"},
|
||||
{"name": "Date", "value": "Sat, 3 Oct 2026 09:00:00 +0200"},
|
||||
]},
|
||||
})
|
||||
|
||||
def modify(self, **kwargs): # no debe llamarse nunca (RF-25)
|
||||
self.modificados.append(kwargs)
|
||||
return _Req({})
|
||||
|
||||
|
||||
class _Users:
|
||||
def __init__(self):
|
||||
self._m = _Messages()
|
||||
|
||||
def messages(self):
|
||||
return self._m
|
||||
|
||||
|
||||
class FakeGmail:
|
||||
def __init__(self):
|
||||
self._u = _Users()
|
||||
|
||||
def users(self):
|
||||
return self._u
|
||||
|
||||
|
||||
def test_correo_ventana_24h():
|
||||
gmail = FakeGmail()
|
||||
correos = correo.recoger_recientes(gmail, horas=24)
|
||||
assert len(correos) == 2
|
||||
assert correos[0]["id"] == "m1"
|
||||
assert correos[0]["asunto"] == "Asunto m1"
|
||||
assert "newer_than:1d" in gmail.users().messages().queries[0]
|
||||
|
||||
|
||||
def test_correo_no_modifica_estado():
|
||||
gmail = FakeGmail()
|
||||
correo.recoger_recientes(gmail, horas=24)
|
||||
assert gmail.users().messages().modificados == [] # no marca leido (RF-25)
|
||||
|
||||
|
||||
def test_render_correo():
|
||||
txt = correo.render_correo({"fecha": "Sat, 3 Oct 2026 09:00:00 +0200", "de": "a@b.com", "asunto": "Hola", "snippet": "cuerpo"})
|
||||
assert "a@b.com" in txt and "Hola" in txt
|
||||
58
tests/test_jira.py
Normal file
58
tests/test_jira.py
Normal file
@@ -0,0 +1,58 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Tests de `monitor_estado.fuentes.jira` — DS nuevos y sin asignar, solo GET (RF-3, RF-4, RF-16)."""
|
||||
|
||||
import monitor_estado.fuentes.jira as jira
|
||||
|
||||
|
||||
def _issue(key, summary, status="Pendiente", created="2026-10-03T08:00:00.000+0200"):
|
||||
return {"key": key, "fields": {
|
||||
"summary": summary,
|
||||
"status": {"name": status},
|
||||
"created": created,
|
||||
"priority": {"name": "Media"},
|
||||
}}
|
||||
|
||||
|
||||
class FakeJira:
|
||||
"""Solo implementa `search` (lectura). Si el codigo intentara escribir, fallaria."""
|
||||
|
||||
def __init__(self):
|
||||
self.calls = []
|
||||
|
||||
def search(self, jql, fields=None, max_results=50):
|
||||
self.calls.append(jql)
|
||||
if "assignee = EMPTY" in jql:
|
||||
issues = [_issue("DS-100", "Ticket sin asignar")]
|
||||
elif "created >=" in jql:
|
||||
issues = [_issue("DS-200", "Ticket nuevo")]
|
||||
else:
|
||||
issues = []
|
||||
return {"issues": issues}
|
||||
|
||||
|
||||
def test_jira_jql_ds_sin_asignar():
|
||||
fake = FakeJira()
|
||||
out = jira.ds_sin_asignar(fake)
|
||||
assert any("assignee = EMPTY" in c and "project = DS" in c for c in fake.calls)
|
||||
assert out[0]["key"] == "DS-100"
|
||||
assert out[0]["estado"] == "Pendiente"
|
||||
|
||||
|
||||
def test_jira_ds_nuevos_ventana():
|
||||
fake = FakeJira()
|
||||
out = jira.ds_nuevos(fake, "2026-10-03 00:00")
|
||||
assert "created >=" in fake.calls[-1]
|
||||
assert out[0]["key"] == "DS-200"
|
||||
|
||||
|
||||
def test_jira_solo_lectura():
|
||||
"""El cliente usado solo expone `search` (no hay escritura) — RF-16."""
|
||||
fake = FakeJira()
|
||||
jira.ds_sin_asignar(fake)
|
||||
jira.ds_nuevos(fake, "2026-10-03 00:00")
|
||||
assert not hasattr(fake, "create_issue") and not hasattr(fake, "post")
|
||||
|
||||
|
||||
def test_render_jira():
|
||||
txt = jira.render_jira({"key": "DS-1", "resumen": "Algo", "estado": "Pendiente", "url": "u"})
|
||||
assert "DS-1" in txt and "Algo" in txt and "Pendiente" in txt
|
||||
40
tests/test_lock.py
Normal file
40
tests/test_lock.py
Normal file
@@ -0,0 +1,40 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Tests de `monitor_estado.lock` — instancia unica (RF-22)."""
|
||||
|
||||
import json
|
||||
|
||||
import monitor_estado.lock as lock
|
||||
|
||||
|
||||
def test_lock_impide_segunda_instancia(tmp_path, monkeypatch):
|
||||
"""Una segunda adquisicion con la primera viva debe fallar (RF-22)."""
|
||||
monkeypatch.setattr(lock, "LOCK_PATH", tmp_path / ".lock")
|
||||
|
||||
assert lock.adquirir() is True
|
||||
# Misma PID viva -> la segunda instancia no arranca.
|
||||
assert lock.adquirir() is False
|
||||
|
||||
lock.liberar()
|
||||
assert not (tmp_path / ".lock").exists()
|
||||
assert lock.adquirir() is True
|
||||
lock.liberar()
|
||||
|
||||
|
||||
def test_lock_recupera_lock_caducado(tmp_path, monkeypatch):
|
||||
"""Un lock de un PID muerto no debe bloquear (se recupera)."""
|
||||
ruta = tmp_path / ".lock"
|
||||
monkeypatch.setattr(lock, "LOCK_PATH", ruta)
|
||||
ruta.write_text(json.dumps({"pid": 999999, "arranque": "2026-01-01T00:00:00"}), encoding="utf-8")
|
||||
|
||||
assert lock.adquirir() is True
|
||||
lock.liberar()
|
||||
|
||||
|
||||
def test_lock_liberar_no_borra_lock_ajeno(tmp_path, monkeypatch):
|
||||
"""`liberar` solo elimina el lock si es de nuestro propio PID."""
|
||||
ruta = tmp_path / ".lock"
|
||||
monkeypatch.setattr(lock, "LOCK_PATH", ruta)
|
||||
ruta.write_text(json.dumps({"pid": 999999, "arranque": "x"}), encoding="utf-8")
|
||||
|
||||
lock.liberar()
|
||||
assert ruta.exists() # no era nuestro -> se respeta
|
||||
45
tests/test_resumen.py
Normal file
45
tests/test_resumen.py
Normal file
@@ -0,0 +1,45 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Tests de `monitor_estado.resumen` — resumen IA y clasificacion (RF-11, RF-12, RF-13)."""
|
||||
|
||||
import monitor_estado.resumen as resumen
|
||||
|
||||
|
||||
def test_resumen_clasifica_atencion():
|
||||
def fake_ia(prompt):
|
||||
return ('{"resumen_md": "Hay una incidencia de SQL.", '
|
||||
'"requiere_atencion": [{"fuente": "jira", "quien": "DS-3648", '
|
||||
'"texto": "PC526 sin conexion", "motivo": "ticket sin asignar"}]}')
|
||||
|
||||
r = resumen.generar({"jira": [{"key": "DS-3648", "resumen": "PC526 sin conexion"}]}, [], _ia=fake_ia)
|
||||
assert r["fallo_ia"] is False
|
||||
assert len(r["requiere_atencion"]) == 1
|
||||
assert r["requiere_atencion"][0]["quien"] == "DS-3648"
|
||||
assert "incidencia" in r["resumen_md"].lower()
|
||||
|
||||
|
||||
def test_resumen_fallo_ia_deja_constancia():
|
||||
def bad_ia(prompt):
|
||||
raise RuntimeError("OmniRoute caido")
|
||||
|
||||
r = resumen.generar({"jira": [{"key": "DS-1", "resumen": "x"}]}, [("correo", "token caducado")], _ia=bad_ia)
|
||||
assert r["fallo_ia"] is True
|
||||
assert r["requiere_atencion"] == []
|
||||
assert r["resumen_md"] # deja constancia del fallo
|
||||
assert r["errores"] # conserva los errores del ciclo
|
||||
|
||||
|
||||
def test_resumen_parsea_json_con_fences():
|
||||
def fake_ia(prompt):
|
||||
return '```json\n{"resumen_md": "ok", "requiere_atencion": []}\n```'
|
||||
|
||||
r = resumen.generar({"correo": [{"asunto": "hola"}]}, [], _ia=fake_ia)
|
||||
assert r["fallo_ia"] is False and r["resumen_md"] == "ok"
|
||||
|
||||
|
||||
def test_resumen_sin_novedades_no_llama_ia():
|
||||
def no_llamar(prompt):
|
||||
raise AssertionError("no deberia llamarse a la IA sin novedades")
|
||||
|
||||
r = resumen.generar({}, [], _ia=no_llamar)
|
||||
assert r["fallo_ia"] is False
|
||||
assert r["requiere_atencion"] == []
|
||||
91
tests/test_telegram.py
Normal file
91
tests/test_telegram.py
Normal file
@@ -0,0 +1,91 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Tests de `monitor_estado.fuentes.telegram` — nucleo y aislamiento (RF-1, RF-6)."""
|
||||
|
||||
from datetime import date, datetime, timezone
|
||||
from types import SimpleNamespace
|
||||
|
||||
import monitor_estado.fuentes.telegram as tg
|
||||
|
||||
|
||||
class FakeMsg:
|
||||
"""Mensaje crudo minimo compatible con lo que espera normalizar/recoger."""
|
||||
|
||||
def __init__(self, id, dt, text, sender_id=1, sender=None):
|
||||
self.id = id
|
||||
self.date = dt
|
||||
self.message = text
|
||||
self.sender_id = sender_id
|
||||
self._sender_name = sender
|
||||
self._sender = sender or "Anon"
|
||||
|
||||
async def get_sender(self):
|
||||
return SimpleNamespace(first_name=self._sender)
|
||||
|
||||
|
||||
class FakeClient:
|
||||
"""Cliente Telethon falso (sin red)."""
|
||||
|
||||
def __init__(self, por_id, malos=()):
|
||||
self.por_id = por_id
|
||||
self.malos = set(malos)
|
||||
self.conectado = False
|
||||
|
||||
async def connect(self):
|
||||
self.conectado = True
|
||||
|
||||
async def is_user_authorized(self):
|
||||
return True
|
||||
|
||||
async def disconnect(self):
|
||||
self.conectado = False
|
||||
|
||||
async def get_dialogs(self, limit=None):
|
||||
return []
|
||||
|
||||
async def get_entity(self, cid):
|
||||
if cid in self.malos:
|
||||
raise RuntimeError("sin acceso a la fuente")
|
||||
return cid
|
||||
|
||||
def iter_messages(self, entidad, limit=None):
|
||||
async def gen():
|
||||
for m in self.por_id.get(entidad, []):
|
||||
yield m
|
||||
return gen()
|
||||
|
||||
|
||||
def test_normalizar_filtra_dia_y_ordena():
|
||||
dia = date(2026, 10, 3)
|
||||
m1 = FakeMsg(2, datetime(2026, 10, 3, 15, 0, tzinfo=timezone.utc), "segundo", sender="Ana")
|
||||
m2 = FakeMsg(1, datetime(2026, 10, 3, 9, 0, tzinfo=timezone.utc), "primero", sender="Ana")
|
||||
m3 = FakeMsg(3, datetime(2026, 10, 2, 9, 0, tzinfo=timezone.utc), "ayer", sender="Ana")
|
||||
out = tg.normalizar_mensajes([m1, m2, m3], dia, "test")
|
||||
assert [r["id"] for r in out] == [1, 2] # solo hoy y ordenado
|
||||
assert out[0]["quien"] == "Ana" and out[0]["fuente"] == "test"
|
||||
|
||||
|
||||
def test_recoger_fuente_para_en_el_dia():
|
||||
dia = date(2026, 10, 3)
|
||||
hoy = FakeMsg(5, datetime(2026, 10, 3, 12, 0, tzinfo=timezone.utc), "hoy", sender="Ana")
|
||||
ayer = FakeMsg(4, datetime(2026, 10, 2, 12, 0, tzinfo=timezone.utc), "ayer", sender="Ana")
|
||||
client = FakeClient({7: [hoy, ayer]})
|
||||
import asyncio
|
||||
out = asyncio.run(tg.recoger_fuente(client, "oscar", 7, dia))
|
||||
assert [r["id"] for r in out] == [5]
|
||||
|
||||
|
||||
def test_recoger_todas_aisla_fuente_caida():
|
||||
dia = date(2026, 10, 3)
|
||||
ok = FakeMsg(1, datetime(2026, 10, 3, 12, 0, tzinfo=timezone.utc), "hola", sender="Ana")
|
||||
cfg = {"telegram": {"fuentes": [{"nombre": "buena", "id": 10}, {"nombre": "mala", "id": 20}]}}
|
||||
client = FakeClient({10: [ok]}, malos={20})
|
||||
|
||||
resultado, errores = tg.recoger_todas(cfg, dia, client_factory=lambda c: client)
|
||||
assert "buena" in resultado and len(resultado["buena"]) == 1
|
||||
assert len(errores) == 1 and errores[0][0] == "mala"
|
||||
|
||||
|
||||
def test_render_mensaje():
|
||||
m = {"fecha": "2026-10-03T09:12:00+02:00", "quien": "Oscar", "texto": "linea1\nlinea2"}
|
||||
txt = tg.render_mensaje(m)
|
||||
assert "[09:12] Oscar" in txt and "linea1" in txt and "linea2" in txt
|
||||
48
tests/test_vistos.py
Normal file
48
tests/test_vistos.py
Normal file
@@ -0,0 +1,48 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Tests de `monitor_estado.vistos` — dedup persistente (RF-8, RF-23)."""
|
||||
|
||||
import monitor_estado.vistos as vistos
|
||||
|
||||
|
||||
def test_vistos_filtra_nuevos(tmp_path, monkeypatch):
|
||||
"""Lo ya visto no se vuelve a contar; lo nuevo si (RF-8)."""
|
||||
monkeypatch.setattr(vistos, "VISTO_DIR", tmp_path)
|
||||
clave = "telegram_test"
|
||||
msgs = [{"id": 1, "t": "a"}, {"id": 2, "t": "b"}]
|
||||
|
||||
nuevos = vistos.filtrar_nuevos(clave, msgs, id_fn=lambda m: m["id"])
|
||||
assert [m["id"] for m in nuevos] == [1, 2]
|
||||
|
||||
# Segundo ciclo con los mismos mensajes -> nada nuevo.
|
||||
assert vistos.filtrar_nuevos(clave, msgs, id_fn=lambda m: m["id"]) == []
|
||||
|
||||
# Llega uno nuevo -> solo ese.
|
||||
mas = msgs + [{"id": 3, "t": "c"}]
|
||||
assert [m["id"] for m in vistos.filtrar_nuevos(clave, mas, id_fn=lambda m: m["id"])] == [3]
|
||||
|
||||
|
||||
def test_vistos_persiste_entre_llamadas(tmp_path, monkeypatch):
|
||||
"""El estado se guarda en disco y sobrevive a una nueva lectura (RF-23)."""
|
||||
monkeypatch.setattr(vistos, "VISTO_DIR", tmp_path)
|
||||
clave = "correo"
|
||||
vistos.filtrar_nuevos(clave, ["m1", "m2"], id_fn=lambda x: x)
|
||||
|
||||
assert (tmp_path / f"{clave}.json").exists()
|
||||
# Nueva lectura del fichero: m1/m2 ya no son nuevos.
|
||||
assert vistos.filtrar_nuevos(clave, ["m1", "m2"], id_fn=lambda x: x) == []
|
||||
|
||||
|
||||
def test_vistos_marcar_explicito(tmp_path, monkeypatch):
|
||||
"""`marcar` añade ids sin devolver nada."""
|
||||
monkeypatch.setattr(vistos, "VISTO_DIR", tmp_path)
|
||||
vistos.marcar("jira", ["DS-1", "DS-2"])
|
||||
assert vistos.filtrar_nuevos("jira", ["DS-1"], id_fn=lambda x: x) == []
|
||||
assert vistos.filtrar_nuevos("jira", ["DS-3"], id_fn=lambda x: x) == ["DS-3"]
|
||||
|
||||
|
||||
def test_vistos_idempotente_sin_marcar(tmp_path, monkeypatch):
|
||||
"""Con marcar_automatico=False, filtrar no altera el estado."""
|
||||
monkeypatch.setattr(vistos, "VISTO_DIR", tmp_path)
|
||||
a = vistos.filtrar_nuevos("k", [1, 2], id_fn=lambda x: x, marcar_automatico=False)
|
||||
b = vistos.filtrar_nuevos("k", [1, 2], id_fn=lambda x: x, marcar_automatico=False)
|
||||
assert a == [1, 2] and b == [1, 2]
|
||||
Reference in New Issue
Block a user