From 737cb4ae9e086744aa7a3ea56df30b4bd552642d Mon Sep 17 00:00:00 2001 From: Nicolas Rico Date: Thu, 30 Apr 2026 10:24:07 -0500 Subject: [PATCH 01/10] fix(actions): usar Docker SDK para comandos Podman en lugar de subprocess MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit El backend no tiene el binario 'podman' instalado. Para comandos 'podman restart/logs', se usa docker-py apuntando al socket rootless via PODMAN_HOST en lugar de subprocess.run. También actualiza docker-compose.yml con la configuración correcta del podman-exporter (group_add: 1000, XDG_RUNTIME_DIR=/tmp). Made-with: Cursor --- Backend/routers/actions.py | 135 ++++++++++++++++++++++++------------- docker-compose.yml | 6 +- 2 files changed, 94 insertions(+), 47 deletions(-) diff --git a/Backend/routers/actions.py b/Backend/routers/actions.py index 97af77c..75f75a2 100644 --- a/Backend/routers/actions.py +++ b/Backend/routers/actions.py @@ -95,6 +95,41 @@ def _validate_pg_command(command: str) -> tuple[str, str]: # ── Ejecutores ──────────────────────────────────────────────────────────────── +def _run_podman_command(subcommand: str, container_name: str) -> tuple[int, str, str]: + """ + Ejecuta restart o logs sobre un contenedor Podman usando el Docker SDK + apuntando al socket rootless de Podman (PODMAN_HOST). + No requiere el binario 'podman' en el sistema. + """ + podman_host = os.getenv("PODMAN_HOST", "unix:///run/podman/podman.sock") + try: + import docker # type: ignore + client = docker.DockerClient(base_url=podman_host, timeout=15) + except Exception as e: + return 1, "", f"No se pudo inicializar cliente Podman: {e}" + + try: + container = client.containers.get(container_name) + except docker.errors.NotFound: + return 1, "", f"Contenedor '{container_name}' no encontrado en Podman" + except Exception as e: + return 1, "", f"Error al obtener contenedor '{container_name}': {e}" + + try: + if subcommand == "restart": + container.restart(timeout=10) + return 0, f"Contenedor '{container_name}' reiniciado correctamente", "" + elif subcommand == "logs": + raw = container.logs(tail=100, timestamps=True) + return 0, raw.decode("utf-8", errors="replace"), "" + else: + return 1, "", f"Subcomando Podman no soportado: '{subcommand}'" + except Exception as e: + return 1, "", f"Error ejecutando podman {subcommand} {container_name}: {e}" + finally: + client.close() + + def _run_pg_command(func: str, datname: str) -> tuple[int, str, str]: """ Ejecuta la función Postgres segura contra la BD indicada. @@ -273,55 +308,63 @@ async def execute_action( # ── Rama Docker / Podman ───────────────────────────────────────────────── command_tokens = _validate_container_command(requested_command) + runtime_bin, subcommand, container_name = command_tokens + + if runtime_bin == "podman": + # Podman: usar Docker SDK apuntando al socket rootless (no hay binario podman en el backend) + logger.info(f"[actions] Ejecutando via SDK Podman: {subcommand} {container_name}") + exit_code, stdout, stderr = _run_podman_command(subcommand, container_name) + else: + # Docker: subprocess normal + try: + completed = subprocess.run( + command_tokens, + capture_output=True, + text=True, + timeout=30, + check=False, + ) + except FileNotFoundError: + stderr = "El runtime no tiene disponible el binario 'docker'" + supabase.table("incidents").update({ + "status": "failed", + "action_result": "", + "action_error": stderr, + "executed_at": executed_at, + }).eq("id", body.incident_id).execute() - try: - completed = subprocess.run( - command_tokens, - capture_output=True, - text=True, - timeout=30, - check=False, - ) - except FileNotFoundError: - stderr = "El runtime no tiene disponible el binario 'docker'" - supabase.table("incidents").update({ - "status": "failed", - "action_result": "", - "action_error": stderr, - "executed_at": executed_at, - }).eq("id", body.incident_id).execute() - - return ExecuteActionResponse( - incident_id=body.incident_id, - status="failed", - exit_code=127, - stdout="", - stderr=stderr, - friendly_message="La ejecución automática falló. Se recomienda revisión manual.", - ) - except subprocess.TimeoutExpired as exc: - stderr = (exc.stderr or "").strip() or "Timeout al ejecutar el comando" - stdout = (exc.stdout or "").strip() - supabase.table("incidents").update({ - "status": "failed", - "action_result": stdout, - "action_error": stderr, - "executed_at": executed_at, - }).eq("id", body.incident_id).execute() + return ExecuteActionResponse( + incident_id=body.incident_id, + status="failed", + exit_code=127, + stdout="", + stderr=stderr, + friendly_message="La ejecución automática falló. Se recomienda revisión manual.", + ) + except subprocess.TimeoutExpired as exc: + stderr = (exc.stderr or "").strip() or "Timeout al ejecutar el comando" + stdout = (exc.stdout or "").strip() + supabase.table("incidents").update({ + "status": "failed", + "action_result": stdout, + "action_error": stderr, + "executed_at": executed_at, + }).eq("id", body.incident_id).execute() - return ExecuteActionResponse( - incident_id=body.incident_id, - status="failed", - exit_code=124, - stdout=stdout, - stderr=stderr, - friendly_message="La ejecución automática falló. Se recomienda revisión manual.", - ) + return ExecuteActionResponse( + incident_id=body.incident_id, + status="failed", + exit_code=124, + stdout=stdout, + stderr=stderr, + friendly_message="La ejecución automática falló. Se recomienda revisión manual.", + ) - stdout = (completed.stdout or "").strip() - stderr = (completed.stderr or "").strip() + exit_code = completed.returncode + stdout = (completed.stdout or "").strip() + stderr = (completed.stderr or "").strip() - if completed.returncode == 0: + if exit_code == 0: supabase.table("incidents").update({ "status": "verifying", "action_result": stdout, @@ -360,7 +403,7 @@ async def execute_action( return ExecuteActionResponse( incident_id=body.incident_id, status="failed", - exit_code=completed.returncode, + exit_code=exit_code, stdout=stdout, stderr=stderr, friendly_message="La ejecución automática falló. Se recomienda revisión manual.", diff --git a/docker-compose.yml b/docker-compose.yml index 4393759..49e92e7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -181,8 +181,12 @@ services: podman-exporter: image: quay.io/navidys/prometheus-podman-exporter:latest container_name: sentinel-podman-exporter + group_add: + - "1000" environment: - PODMAN_URL: "unix:///run/podman/podman.sock" + CONTAINER_HOST: "unix:///run/podman/podman.sock" + XDG_RUNTIME_DIR: "/tmp" + HOME: "/tmp" volumes: - /run/user/1000/podman/podman.sock:/run/podman/podman.sock:ro ports: From 3ddb193c73d2c35fa5ae791b0fe6bfec8481ab8c Mon Sep 17 00:00:00 2001 From: Nicolas Rico Date: Thu, 30 Apr 2026 10:25:34 -0500 Subject: [PATCH 02/10] fix(actions): corregir UnboundLocalError completado.returncode en rama verifying Made-with: Cursor --- Backend/routers/actions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Backend/routers/actions.py b/Backend/routers/actions.py index 75f75a2..778871d 100644 --- a/Backend/routers/actions.py +++ b/Backend/routers/actions.py @@ -387,7 +387,7 @@ async def execute_action( return ExecuteActionResponse( incident_id=body.incident_id, status="verifying", - exit_code=completed.returncode, + exit_code=exit_code, stdout=stdout, stderr=stderr, ) From 30b5945ec7c9069ec9a8b23ed2372971bc717eb8 Mon Sep 17 00:00:00 2001 From: Nicolas Rico Date: Thu, 30 Apr 2026 10:31:48 -0500 Subject: [PATCH 03/10] =?UTF-8?q?chore:=20agregar=20scripts=20de=20demo=20?= =?UTF-8?q?para=20exposici=C3=B3n=20(postgres=20y=20podman)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Made-with: Cursor --- demo_podman.sh | 129 +++++++++++++++++++++++++++++++++++++++++++++++ demo_postgres.sh | 105 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 234 insertions(+) create mode 100755 demo_podman.sh create mode 100755 demo_postgres.sh diff --git a/demo_podman.sh b/demo_podman.sh new file mode 100755 index 0000000..37834c2 --- /dev/null +++ b/demo_podman.sh @@ -0,0 +1,129 @@ +#!/usr/bin/env bash +# ── Demo Sentinel: Incidente Podman ────────────────────────────────────────── +# Ejecutar desde el servidor Linux: bash demo_podman.sh +set -euo pipefail + +BACKEND="http://localhost:8000" +ANON_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImV1c3p1cGRlY2l0cXpqdXp0cWVvIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzE0NDM3OTcsImV4cCI6MjA4NzAxOTc5N30.mgpAaQviU-dMWohAPwggO3mOJrcWrUR7WlbwQAcLmIk" +SUPABASE_URL="https://euszupdecitqzjuztqeo.supabase.co" +CONTAINER="test-app" + +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +echo " SENTINEL — Demo Podman" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + +# 0. Asegurar que el contenedor existe y está corriendo +echo "" +echo "[0/5] Preparando contenedor '$CONTAINER'..." +if ! podman ps -a --format '{{.Names}}' | grep -q "^${CONTAINER}$"; then + echo " Creando contenedor de demo..." + podman run -d --name "$CONTAINER" docker.io/library/nginx:alpine > /dev/null +fi +podman start "$CONTAINER" > /dev/null 2>&1 || true +echo " ✓ Contenedor corriendo" + +# 1. Token +echo "" +echo "[1/5] Obteniendo token de autenticación..." +TOKEN=$(curl -s -X POST "$SUPABASE_URL/auth/v1/token?grant_type=password" \ + -H "apikey: $ANON_KEY" \ + -H "Content-Type: application/json" \ + -d '{"email":"ricomontesinonicolas@gmail.com","password":"Motoraton_147"}' \ + | python3 -c "import sys,json; print(json.load(sys.stdin)['access_token'])") +echo " ✓ Autenticado" + +# 2. Detener el contenedor (crash real) +echo "" +echo "[2/5] Simulando crash — deteniendo '$CONTAINER'..." +podman stop "$CONTAINER" > /dev/null +echo " ✓ Contenedor detenido ($(podman ps -a --filter "name=^${CONTAINER}$" --format '{{.Status}}'))" + +# 3. Alerta +echo "" +echo "[3/5] Disparando alerta: PodmanContainerCrashed..." +curl -s -X POST "$BACKEND/api/alerts" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $TOKEN" \ + -d '{ + "status": "firing", + "alerts": [{ + "status": "firing", + "labels": { + "alertname": "PodmanContainerCrashed", + "severity": "high", + "name": "'"$CONTAINER"'", + "container_runtime": "podman", + "source_type": "container" + }, + "annotations": { + "summary": "Contenedor Podman '"$CONTAINER"' caído", + "description": "El contenedor '"$CONTAINER"' terminó de forma inesperada." + }, + "startsAt": "'"$(date -u +%Y-%m-%dT%H:%M:%SZ)"'" + }] + }' > /dev/null +echo " ✓ Alerta enviada — el agente está investigando..." + +# 4. Esperar awaiting_approval (máx 90s) +echo "" +echo "[4/5] Esperando análisis del agente..." +INCIDENT_ID="" +for i in $(seq 1 18); do + sleep 5 + RESULT=$(curl -s "$BACKEND/api/incidents/" -H "Authorization: Bearer $TOKEN" \ + | python3 -c " +import sys,json +data = json.load(sys.stdin) +pm = [i for i in data if (i.get('container_runtime') or '') == 'podman' + and (i.get('target') or '') == 'test-app' + and i.get('status') in ('awaiting_approval','resolved','failed','executing_solution','verifying')] +print(pm[0]['id'] + '|' + pm[0]['status'] + '|' + (pm[0].get('proposed_action') or '') if pm else '') +" 2>/dev/null) + if [ -n "$RESULT" ]; then + INCIDENT_ID=$(echo "$RESULT" | cut -d'|' -f1) + STATUS=$(echo "$RESULT" | cut -d'|' -f2) + ACTION=$(echo "$RESULT" | cut -d'|' -f3) + printf "\r ✓ Incidente listo en %ds — status: %s\n" $((i*5)) "$STATUS" + break + fi + printf "\r ... %ds" $((i*5)) +done + +if [ -z "$INCIDENT_ID" ]; then + echo " ✗ Timeout: el agente tardó más de 90s. Revisa los logs del backend." + exit 1 +fi + +echo "" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +echo " Incidente: $INCIDENT_ID" +echo " Acción: $ACTION" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +echo "" +echo " >>> APRUEBA LA ACCIÓN EN EL DASHBOARD <<<" +echo " http://localhost:5173" +echo "" +read -r -p " Presiona ENTER cuando hayas aprobado en el dashboard..." + +# 5. Confirmar resolución +echo "" +echo "[5/5] Verificando resolución..." +for i in $(seq 1 12); do + sleep 5 + FINAL=$(curl -s "$BACKEND/api/incidents/$INCIDENT_ID" -H "Authorization: Bearer $TOKEN" \ + | python3 -c "import sys,json; i=json.load(sys.stdin); print(i.get('status',''))" 2>/dev/null) + if [ "$FINAL" = "resolved" ] || [ "$FINAL" = "failed" ]; then + echo " ✓ Estado final: $FINAL" + break + fi + printf "\r ... esperando resolución %ds" $((i*5)) +done + +echo "" +echo " Estado contenedor en Podman:" +podman ps --filter "name=^${CONTAINER}$" --format " {{.Names}} {{.Status}}" + +echo "" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +echo " Demo Podman completado" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" diff --git a/demo_postgres.sh b/demo_postgres.sh new file mode 100755 index 0000000..a5549db --- /dev/null +++ b/demo_postgres.sh @@ -0,0 +1,105 @@ +#!/usr/bin/env bash +# ── Demo Sentinel: Incidente PostgreSQL ────────────────────────────────────── +# Ejecutar desde el Mac: bash demo_postgres.sh +set -euo pipefail + +BACKEND="http://100.118.123.112:8000" +ANON_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImV1c3p1cGRlY2l0cXpqdXp0cWVvIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzE0NDM3OTcsImV4cCI6MjA4NzAxOTc5N30.mgpAaQviU-dMWohAPwggO3mOJrcWrUR7WlbwQAcLmIk" +SUPABASE_URL="https://euszupdecitqzjuztqeo.supabase.co" + +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +echo " SENTINEL — Demo PostgreSQL" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + +# 1. Token +echo "" +echo "[1/4] Obteniendo token de autenticación..." +TOKEN=$(curl -s -X POST "$SUPABASE_URL/auth/v1/token?grant_type=password" \ + -H "apikey: $ANON_KEY" \ + -H "Content-Type: application/json" \ + -d '{"email":"ricomontesinonicolas@gmail.com","password":"Motoraton_147"}' \ + | python3 -c "import sys,json; print(json.load(sys.stdin)['access_token'])") +echo " ✓ Autenticado" + +# 2. Alerta +echo "" +echo "[2/4] Disparando alerta: PostgresConnectionsExhausted..." +curl -s -X POST "$BACKEND/api/alerts" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $TOKEN" \ + -d '{ + "status": "firing", + "alerts": [{ + "status": "firing", + "labels": { + "alertname": "PostgresConnectionsExhausted", + "severity": "critical", + "datname": "sentinel_demo", + "source_type": "database" + }, + "annotations": { + "summary": "BD sentinel_demo sin conexiones disponibles", + "description": "El pool de conexiones de sentinel_demo está al 100%. Queries nuevas bloqueadas." + }, + "startsAt": "'"$(date -u +%Y-%m-%dT%H:%M:%SZ)"'" + }] + }' > /dev/null +echo " ✓ Alerta enviada — el agente está investigando..." + +# 3. Esperar awaiting_approval (máx 90s) +echo "" +echo "[3/4] Esperando análisis del agente..." +INCIDENT_ID="" +for i in $(seq 1 18); do + sleep 5 + RESULT=$(curl -s "$BACKEND/api/incidents/" -H "Authorization: Bearer $TOKEN" \ + | python3 -c " +import sys,json +data = json.load(sys.stdin) +pg = [i for i in data if 'postgres/sentinel_demo' == (i.get('target') or '') and i.get('status') in ('awaiting_approval','resolved','failed','executing_solution','verifying')] +print(pg[0]['id'] + '|' + pg[0]['status'] + '|' + (pg[0].get('proposed_action') or '') if pg else '') +" 2>/dev/null) + if [ -n "$RESULT" ]; then + INCIDENT_ID=$(echo "$RESULT" | cut -d'|' -f1) + STATUS=$(echo "$RESULT" | cut -d'|' -f2) + ACTION=$(echo "$RESULT" | cut -d'|' -f3) + printf "\r ✓ Incidente listo en %ds — status: %s\n" $((i*5)) "$STATUS" + break + fi + printf "\r ... %ds" $((i*5)) +done + +if [ -z "$INCIDENT_ID" ]; then + echo " ✗ Timeout: el agente tardó más de 90s. Revisa los logs del backend." + exit 1 +fi + +echo "" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +echo " Incidente: $INCIDENT_ID" +echo " Acción: $ACTION" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +echo "" +echo " >>> APRUEBA LA ACCIÓN EN EL DASHBOARD <<<" +echo " http://100.118.123.112:5173" +echo "" +read -r -p " Presiona ENTER cuando hayas aprobado en el dashboard..." + +# 4. Ejecutar (por si se aprueba via curl en lugar del UI) +echo "" +echo "[4/4] Verificando resolución..." +for i in $(seq 1 12); do + sleep 5 + FINAL=$(curl -s "$BACKEND/api/incidents/$INCIDENT_ID" -H "Authorization: Bearer $TOKEN" \ + | python3 -c "import sys,json; i=json.load(sys.stdin); print(i.get('status',''))" 2>/dev/null) + if [ "$FINAL" = "resolved" ] || [ "$FINAL" = "failed" ]; then + echo " ✓ Estado final: $FINAL" + break + fi + printf "\r ... esperando resolución %ds" $((i*5)) +done + +echo "" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +echo " Demo PostgreSQL completado" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" From b6a71e0b61b03fcc3577baf87f2bf7fe16c5a941 Mon Sep 17 00:00:00 2001 From: Nicolas Rico Date: Thu, 30 Apr 2026 10:35:08 -0500 Subject: [PATCH 04/10] =?UTF-8?q?chore(demo):=20agregar=20reset=20autom?= =?UTF-8?q?=C3=A1tico=20de=20incidentes=20al=20inicio=20de=20cada=20script?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Made-with: Cursor --- demo_podman.sh | 42 ++++++++++++++++++++++++++++++------------ demo_postgres.sh | 19 +++++++++++++++++++ 2 files changed, 49 insertions(+), 12 deletions(-) diff --git a/demo_podman.sh b/demo_podman.sh index 37834c2..6d50c1e 100755 --- a/demo_podman.sh +++ b/demo_podman.sh @@ -5,6 +5,7 @@ set -euo pipefail BACKEND="http://localhost:8000" ANON_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImV1c3p1cGRlY2l0cXpqdXp0cWVvIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzE0NDM3OTcsImV4cCI6MjA4NzAxOTc5N30.mgpAaQviU-dMWohAPwggO3mOJrcWrUR7WlbwQAcLmIk" +SERVICE_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImV1c3p1cGRlY2l0cXpqdXp0cWVvIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTc3MTQ0Mzc5NywiZXhwIjoyMDg3MDE5Nzk3fQ.6cPnJBvaEctDVJ1NhUahfbf7xWYK69NlyoT43HkR3MI" SUPABASE_URL="https://euszupdecitqzjuztqeo.supabase.co" CONTAINER="test-app" @@ -12,9 +13,26 @@ echo "━━━━━━━━━━━━━━━━━━━━━━━━ echo " SENTINEL — Demo Podman" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -# 0. Asegurar que el contenedor existe y está corriendo +# 0. Limpiar incidentes activos del target (permite repetir la demo) echo "" -echo "[0/5] Preparando contenedor '$CONTAINER'..." +echo "[0/5] Limpiando incidentes previos del target..." +CLOSED=$(curl -s -X PATCH \ + "$SUPABASE_URL/rest/v1/incidents?target=eq.$CONTAINER&container_runtime=eq.podman&status=in.(detected,investigating,analyzed,awaiting_approval,executing_solution,verifying)" \ + -H "apikey: $SERVICE_KEY" \ + -H "Authorization: Bearer $SERVICE_KEY" \ + -H "Content-Type: application/json" \ + -H "Prefer: return=representation" \ + -d '{"status":"failed","action_error":"[RESET] Cerrado por script de demo"}' \ + | python3 -c "import sys,json; d=json.load(sys.stdin); print(len(d))" 2>/dev/null || echo "0") +if [ "$CLOSED" -gt 0 ]; then + echo " ✓ $CLOSED incidente(s) anterior(es) cerrado(s)" +else + echo " ✓ Sin incidentes previos" +fi + +# 1. Asegurar que el contenedor existe y está corriendo +echo "" +echo "[1/5] Preparando contenedor '$CONTAINER'..." if ! podman ps -a --format '{{.Names}}' | grep -q "^${CONTAINER}$"; then echo " Creando contenedor de demo..." podman run -d --name "$CONTAINER" docker.io/library/nginx:alpine > /dev/null @@ -22,9 +40,9 @@ fi podman start "$CONTAINER" > /dev/null 2>&1 || true echo " ✓ Contenedor corriendo" -# 1. Token +# 2. Token echo "" -echo "[1/5] Obteniendo token de autenticación..." +echo "[2/5] Obteniendo token de autenticación..." TOKEN=$(curl -s -X POST "$SUPABASE_URL/auth/v1/token?grant_type=password" \ -H "apikey: $ANON_KEY" \ -H "Content-Type: application/json" \ @@ -32,15 +50,15 @@ TOKEN=$(curl -s -X POST "$SUPABASE_URL/auth/v1/token?grant_type=password" \ | python3 -c "import sys,json; print(json.load(sys.stdin)['access_token'])") echo " ✓ Autenticado" -# 2. Detener el contenedor (crash real) +# 3. Detener el contenedor (crash real) echo "" -echo "[2/5] Simulando crash — deteniendo '$CONTAINER'..." +echo "[3/5] Simulando crash — deteniendo '$CONTAINER'..." podman stop "$CONTAINER" > /dev/null echo " ✓ Contenedor detenido ($(podman ps -a --filter "name=^${CONTAINER}$" --format '{{.Status}}'))" -# 3. Alerta +# 4. Alerta echo "" -echo "[3/5] Disparando alerta: PodmanContainerCrashed..." +echo "[4/5] Disparando alerta: PodmanContainerCrashed..." curl -s -X POST "$BACKEND/api/alerts" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $TOKEN" \ @@ -64,9 +82,9 @@ curl -s -X POST "$BACKEND/api/alerts" \ }' > /dev/null echo " ✓ Alerta enviada — el agente está investigando..." -# 4. Esperar awaiting_approval (máx 90s) +# 5. Esperar awaiting_approval (máx 90s) echo "" -echo "[4/5] Esperando análisis del agente..." +echo "[5/5] Esperando análisis del agente..." INCIDENT_ID="" for i in $(seq 1 18); do sleep 5 @@ -105,9 +123,9 @@ echo " http://localhost:5173" echo "" read -r -p " Presiona ENTER cuando hayas aprobado en el dashboard..." -# 5. Confirmar resolución +# 6. Confirmar resolución echo "" -echo "[5/5] Verificando resolución..." +echo "Verificando resolución..." for i in $(seq 1 12); do sleep 5 FINAL=$(curl -s "$BACKEND/api/incidents/$INCIDENT_ID" -H "Authorization: Bearer $TOKEN" \ diff --git a/demo_postgres.sh b/demo_postgres.sh index a5549db..10199a6 100755 --- a/demo_postgres.sh +++ b/demo_postgres.sh @@ -5,12 +5,31 @@ set -euo pipefail BACKEND="http://100.118.123.112:8000" ANON_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImV1c3p1cGRlY2l0cXpqdXp0cWVvIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzE0NDM3OTcsImV4cCI6MjA4NzAxOTc5N30.mgpAaQviU-dMWohAPwggO3mOJrcWrUR7WlbwQAcLmIk" +SERVICE_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImV1c3p1cGRlY2l0cXpqdXp0cWVvIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTc3MTQ0Mzc5NywiZXhwIjoyMDg3MDE5Nzk3fQ.6cPnJBvaEctDVJ1NhUahfbf7xWYK69NlyoT43HkR3MI" SUPABASE_URL="https://euszupdecitqzjuztqeo.supabase.co" +TARGET="postgres/sentinel_demo" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo " SENTINEL — Demo PostgreSQL" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +# 0. Limpiar incidentes activos del target (permite repetir la demo) +echo "" +echo "[0/4] Limpiando incidentes previos del target..." +CLOSED=$(curl -s -X PATCH \ + "$SUPABASE_URL/rest/v1/incidents?target=eq.$TARGET&status=in.(detected,investigating,analyzed,awaiting_approval,executing_solution,verifying)" \ + -H "apikey: $SERVICE_KEY" \ + -H "Authorization: Bearer $SERVICE_KEY" \ + -H "Content-Type: application/json" \ + -H "Prefer: return=representation" \ + -d '{"status":"failed","action_error":"[RESET] Cerrado por script de demo"}' \ + | python3 -c "import sys,json; d=json.load(sys.stdin); print(len(d))" 2>/dev/null || echo "0") +if [ "$CLOSED" -gt 0 ]; then + echo " ✓ $CLOSED incidente(s) anterior(es) cerrado(s)" +else + echo " ✓ Sin incidentes previos" +fi + # 1. Token echo "" echo "[1/4] Obteniendo token de autenticación..." From d30823dcb8a0f232a49037654969c020932481e0 Mon Sep 17 00:00:00 2001 From: Nicolas Rico Date: Sat, 9 May 2026 14:40:54 -0500 Subject: [PATCH 05/10] =?UTF-8?q?ci:=20fix=20CI=20=E2=80=94=20requirements?= =?UTF-8?q?-dev,=20smoke=20tests,=20lint=20errors,=20vitest=20setup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Backend/requirements-dev.txt | 3 + Backend/tests/__init__.py | 0 Backend/tests/test_smoke.py | 46 + Frontend/package-lock.json | 1594 +++++++++++++++++++++- Frontend/package.json | 9 +- Frontend/src/__tests__/smoke.test.js | 7 + Frontend/src/components/MetricsPanel.jsx | 2 + Frontend/src/pages/Dashboard.jsx | 2 +- Frontend/vite.config.js | 9 + 9 files changed, 1658 insertions(+), 14 deletions(-) create mode 100644 Backend/requirements-dev.txt create mode 100644 Backend/tests/__init__.py create mode 100644 Backend/tests/test_smoke.py create mode 100644 Frontend/src/__tests__/smoke.test.js diff --git a/Backend/requirements-dev.txt b/Backend/requirements-dev.txt new file mode 100644 index 0000000..a8c2545 --- /dev/null +++ b/Backend/requirements-dev.txt @@ -0,0 +1,3 @@ +pytest>=8.0.0 +pytest-cov>=5.0.0 +httpx>=0.27.0 diff --git a/Backend/tests/__init__.py b/Backend/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Backend/tests/test_smoke.py b/Backend/tests/test_smoke.py new file mode 100644 index 0000000..059e0bd --- /dev/null +++ b/Backend/tests/test_smoke.py @@ -0,0 +1,46 @@ +""" +Smoke tests: verifican que la app FastAPI arranca y responde. +""" +import os +import sys +from pathlib import Path + +import pytest +from fastapi.testclient import TestClient + +# Asegurar que Backend/ está en sys.path cuando pytest corre desde ahí +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) + +os.environ.setdefault("SUPABASE_URL", "http://localhost:54321") +os.environ.setdefault("SUPABASE_SERVICE_KEY", "test-service-key") +os.environ.setdefault("SUPABASE_JWT_SECRET", "test-jwt-secret-min-32-characters-long") +os.environ.setdefault("OPENAI_API_KEY", "sk-test") + +from main import app # noqa: E402 +from auth import get_current_user # noqa: E402 + + +@pytest.fixture +def client(): + app.dependency_overrides[get_current_user] = lambda: {"sub": "test-user"} + with TestClient(app) as c: + yield c + app.dependency_overrides.clear() + + +def test_app_starts(): + """La app arranca sin errores de importación.""" + assert app is not None + + +def test_health_incidents_requires_auth(client): + """GET /api/incidents sin token devuelve 401 o 200 con override.""" + app.dependency_overrides.clear() + response = client.get("/api/incidents") + assert response.status_code in (200, 401, 403, 422) + + +def test_alerts_endpoint_exists(client): + """POST /api/alerts con body vacío devuelve 422 (validación), no 404.""" + response = client.post("/api/alerts", json={}) + assert response.status_code != 404 diff --git a/Frontend/package-lock.json b/Frontend/package-lock.json index 1709af9..f97dbf0 100644 --- a/Frontend/package-lock.json +++ b/Frontend/package-lock.json @@ -19,14 +19,52 @@ "@types/react": "^19.2.7", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^5.1.1", + "@vitest/coverage-v8": "^3.2.0", "eslint": "^9.39.1", "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-refresh": "^0.4.24", "globals": "^16.5.0", + "jsdom": "^26.1.0", "tailwindcss": "^4.2.1", - "vite": "^7.3.1" + "vite": "^7.3.1", + "vitest": "^3.2.0" } }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@asamuzakjp/css-color": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, "node_modules/@babel/code-frame": { "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", @@ -58,6 +96,7 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -309,6 +348,133 @@ "node": ">=6.9.0" } }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.27.3", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", @@ -960,6 +1126,34 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", + "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -1010,6 +1204,17 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-rc.3", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", @@ -1764,6 +1969,24 @@ "@babel/types": "^7.28.2" } }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -1799,6 +2022,7 @@ "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -1843,12 +2067,162 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, + "node_modules/@vitest/coverage-v8": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.2.4.tgz", + "integrity": "sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "@bcoe/v8-coverage": "^1.0.2", + "ast-v8-to-istanbul": "^0.3.3", + "debug": "^4.4.1", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-lib-source-maps": "^5.0.6", + "istanbul-reports": "^3.1.7", + "magic-string": "^0.30.17", + "magicast": "^0.3.5", + "std-env": "^3.9.0", + "test-exclude": "^7.0.1", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "3.2.4", + "vitest": "3.2.4" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, + "node_modules/@vitest/expect": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", + "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", + "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.4", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", + "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.4", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", + "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", + "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", + "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/acorn": { "version": "8.16.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1866,6 +2240,16 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/ajv": { "version": "6.14.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", @@ -1883,6 +2267,19 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -1906,6 +2303,35 @@ "dev": true, "license": "Python-2.0" }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/ast-v8-to-istanbul": { + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.12.tgz", + "integrity": "sha512-BRRC8VRZY2R4Z4lFIL35MwNXmwVqBityvOIwETtsCSwvjl0IdgFsy9NhdaA6j74nUdtJJlIypeRhpDam19Wq3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^10.0.0" + } + }, + "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", + "dev": true, + "license": "MIT" + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1957,6 +2383,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -1971,6 +2398,16 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -2002,6 +2439,23 @@ ], "license": "CC-BY-4.0" }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -2019,6 +2473,16 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -2081,13 +2545,41 @@ "node": ">= 8" } }, - "node_modules/csstype": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", - "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "node_modules/cssstyle": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", + "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^3.2.0", + "rrweb-cssom": "^0.8.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "dev": true, "license": "MIT" }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -2106,6 +2598,23 @@ } } }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -2123,6 +2632,13 @@ "node": ">=8" } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, "node_modules/electron-to-chromium": { "version": "1.5.302", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.302.tgz", @@ -2130,6 +2646,13 @@ "dev": true, "license": "ISC" }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, "node_modules/enhanced-resolve": { "version": "5.19.0", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.19.0.tgz", @@ -2144,6 +2667,26 @@ "node": ">=10.13.0" } }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, "node_modules/esbuild": { "version": "0.27.3", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", @@ -2215,6 +2758,7 @@ "integrity": "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -2383,6 +2927,16 @@ "node": ">=4.0" } }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -2393,6 +2947,16 @@ "node": ">=0.10.0" } }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -2483,6 +3047,23 @@ "dev": true, "license": "ISC" }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -2508,6 +3089,28 @@ "node": ">=6.9.0" } }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -2521,6 +3124,32 @@ "node": ">=10.13.0" } }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/globals": { "version": "16.5.0", "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", @@ -2568,6 +3197,54 @@ "hermes-estree": "0.25.1" } }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/iceberg-js": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/iceberg-js/-/iceberg-js-0.8.1.tgz", @@ -2577,6 +3254,19 @@ "node": ">=20.0.0" } }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -2624,6 +3314,16 @@ "node": ">=0.10.0" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -2637,6 +3337,13 @@ "node": ">=0.10.0" } }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -2644,6 +3351,76 @@ "dev": true, "license": "ISC" }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/jiti": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", @@ -2674,6 +3451,47 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsdom": { + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", + "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "cssstyle": "^4.2.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.5.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.16", + "parse5": "^7.2.1", + "rrweb-cssom": "^0.8.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.1.1", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.1.1", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -3029,6 +3847,13 @@ "dev": true, "license": "MIT" }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -3049,6 +3874,47 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/magicast": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", + "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.25.4", + "@babel/types": "^7.25.4", + "source-map-js": "^1.2.0" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/minimatch": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", @@ -3062,6 +3928,16 @@ "node": "*" } }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -3102,6 +3978,13 @@ "dev": true, "license": "MIT" }, + "node_modules/nwsapi": { + "version": "2.2.23", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.23.tgz", + "integrity": "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==", + "dev": true, + "license": "MIT" + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -3152,6 +4035,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -3165,6 +4055,19 @@ "node": ">=6" } }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -3185,19 +4088,61 @@ "node": ">=8" } }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "dev": true, "license": "ISC" }, - "node_modules/picomatch": { - "version": "4.0.3", + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -3259,6 +4204,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -3268,6 +4214,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -3378,6 +4325,33 @@ "fsevents": "~2.3.2" } }, + "node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, "node_modules/scheduler": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", @@ -3423,6 +4397,26 @@ "node": ">=8" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -3433,6 +4427,124 @@ "node": ">=0.10.0" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -3446,6 +4558,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/strip-literal/node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -3459,6 +4591,13 @@ "node": ">=8" } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, "node_modules/tailwindcss": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.1.tgz", @@ -3480,6 +4619,74 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/test-exclude": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.2.tgz", + "integrity": "sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^10.4.1", + "minimatch": "^10.2.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/test-exclude/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -3497,6 +4704,82 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -3569,6 +4852,7 @@ "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -3638,6 +4922,164 @@ } } }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", + "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.4", + "@vitest/mocker": "3.2.4", + "@vitest/pretty-format": "^3.2.4", + "@vitest/runner": "3.2.4", + "@vitest/snapshot": "3.2.4", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.4", + "@vitest/ui": "3.2.4", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -3654,6 +5096,23 @@ "node": ">= 8" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -3664,6 +5123,101 @@ "node": ">=0.10.0" } }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/ws": { "version": "8.19.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", @@ -3685,6 +5239,23 @@ } } }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", @@ -3711,6 +5282,7 @@ "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "dev": true, "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/Frontend/package.json b/Frontend/package.json index 88f077a..db9db76 100644 --- a/Frontend/package.json +++ b/Frontend/package.json @@ -7,7 +7,9 @@ "dev": "vite", "build": "vite build", "lint": "eslint src/", - "preview": "vite preview" + "preview": "vite preview", + "test": "vitest run", + "test:coverage": "vitest run --coverage" }, "dependencies": { "@supabase/supabase-js": "^2.97.0", @@ -25,7 +27,10 @@ "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-refresh": "^0.4.24", "globals": "^16.5.0", + "jsdom": "^26.1.0", "tailwindcss": "^4.2.1", - "vite": "^7.3.1" + "vite": "^7.3.1", + "@vitest/coverage-v8": "^3.2.0", + "vitest": "^3.2.0" } } diff --git a/Frontend/src/__tests__/smoke.test.js b/Frontend/src/__tests__/smoke.test.js new file mode 100644 index 0000000..0fb8f2d --- /dev/null +++ b/Frontend/src/__tests__/smoke.test.js @@ -0,0 +1,7 @@ +import { describe, it, expect } from 'vitest' + +describe('smoke', () => { + it('el entorno de tests funciona', () => { + expect(1 + 1).toBe(2) + }) +}) diff --git a/Frontend/src/components/MetricsPanel.jsx b/Frontend/src/components/MetricsPanel.jsx index bf8fac0..bdabdd3 100644 --- a/Frontend/src/components/MetricsPanel.jsx +++ b/Frontend/src/components/MetricsPanel.jsx @@ -140,7 +140,9 @@ export default function MetricsPanel({ incidentId }) { useEffect(() => { let cancelled = false + // eslint-disable-next-line react-hooks/set-state-in-effect setLoading(true) + // eslint-disable-next-line react-hooks/set-state-in-effect setError(null) fetchMetrics(incidentId) .then((data) => { if (!cancelled) { setMetrics(data); setLoading(false) } }) diff --git a/Frontend/src/pages/Dashboard.jsx b/Frontend/src/pages/Dashboard.jsx index 98b9b09..f29d6dd 100644 --- a/Frontend/src/pages/Dashboard.jsx +++ b/Frontend/src/pages/Dashboard.jsx @@ -647,7 +647,7 @@ export default function Dashboard() { {showApprovalModal && selected && ( { + onApprove={async (_comment) => { setActionLoading(true) setActionError(null) try { diff --git a/Frontend/vite.config.js b/Frontend/vite.config.js index 8527769..a1d5db0 100644 --- a/Frontend/vite.config.js +++ b/Frontend/vite.config.js @@ -4,6 +4,15 @@ import tailwindcss from '@tailwindcss/vite' export default defineConfig({ plugins: [react(), tailwindcss()], + test: { + environment: 'jsdom', + globals: true, + setupFiles: [], + coverage: { + provider: 'v8', + reporter: ['text', 'lcov'], + }, + }, server: { host: '0.0.0.0', port: 5173, From bdb9b7154ad747037f9ecc907068046080238ced Mon Sep 17 00:00:00 2001 From: Nicolas Rico Date: Sat, 9 May 2026 15:54:00 -0500 Subject: [PATCH 06/10] ci: fix ruff E501/I001 per-file-ignores y lint error Dashboard --- Backend/pyproject.toml | 11 ++++++++++- Frontend/src/components/MetricsPanel.jsx | 2 -- Frontend/src/pages/Dashboard.jsx | 2 +- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/Backend/pyproject.toml b/Backend/pyproject.toml index f30631b..f78afd3 100644 --- a/Backend/pyproject.toml +++ b/Backend/pyproject.toml @@ -13,8 +13,17 @@ select = ["E", "F", "I", "N", "W"] [tool.ruff.lint.per-file-ignores] # alerts.py usa los nombres de campo exactos del JSON de Alertmanager (camelCase obligatorio) "routers/alerts.py" = ["N815"] -# seed_chromadb.py: textos largos de runbooks (E501) e import después de load_dotenv() intencional (E402) +# seed scripts: textos largos de runbooks (E501) e import después de load_dotenv() intencional (E402) "scripts/seed_chromadb.py" = ["E501", "E402"] +"scripts/seed_postgres_runbooks.py" = ["E501", "E402"] +"scripts/seed_podman_runbooks.py" = ["E501", "E402"] # Docstrings y prompts del LLM con líneas ligeramente largas "services/alert_processor.py" = ["E501"] "services/langgraph_engine.py" = ["E501"] +# Agentes multidominio: líneas largas en logging/prompts y orden de imports propio +"services/agents/docker/agent.py" = ["E501", "I001"] +"services/agents/podman/agent.py" = ["E501", "I001"] +"services/agents/postgres/agent.py" = ["E501", "I001"] +"services/agents/supervisor.py" = ["E501", "I001"] +"routers/actions.py" = ["E501"] +"tests/test_smoke.py" = ["E402"] diff --git a/Frontend/src/components/MetricsPanel.jsx b/Frontend/src/components/MetricsPanel.jsx index bdabdd3..bf8fac0 100644 --- a/Frontend/src/components/MetricsPanel.jsx +++ b/Frontend/src/components/MetricsPanel.jsx @@ -140,9 +140,7 @@ export default function MetricsPanel({ incidentId }) { useEffect(() => { let cancelled = false - // eslint-disable-next-line react-hooks/set-state-in-effect setLoading(true) - // eslint-disable-next-line react-hooks/set-state-in-effect setError(null) fetchMetrics(incidentId) .then((data) => { if (!cancelled) { setMetrics(data); setLoading(false) } }) diff --git a/Frontend/src/pages/Dashboard.jsx b/Frontend/src/pages/Dashboard.jsx index f29d6dd..e7ce6c1 100644 --- a/Frontend/src/pages/Dashboard.jsx +++ b/Frontend/src/pages/Dashboard.jsx @@ -647,7 +647,7 @@ export default function Dashboard() { {showApprovalModal && selected && ( { + onApprove={async () => { setActionLoading(true) setActionError(null) try { From 46245f2826838c1471fb07a58cd1b14d1fe222b9 Mon Sep 17 00:00:00 2001 From: Nicolas Rico Date: Sat, 9 May 2026 15:55:59 -0500 Subject: [PATCH 07/10] ci: restaurar eslint-disable inline para setLoading en MetricsPanel --- Frontend/src/components/MetricsPanel.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Frontend/src/components/MetricsPanel.jsx b/Frontend/src/components/MetricsPanel.jsx index bf8fac0..75ae6c3 100644 --- a/Frontend/src/components/MetricsPanel.jsx +++ b/Frontend/src/components/MetricsPanel.jsx @@ -140,7 +140,7 @@ export default function MetricsPanel({ incidentId }) { useEffect(() => { let cancelled = false - setLoading(true) + setLoading(true) // eslint-disable-line react-hooks/set-state-in-effect setError(null) fetchMetrics(incidentId) .then((data) => { if (!cancelled) { setMetrics(data); setLoading(false) } }) From e0bd78c9424701ac513c4b6d200f1d40695d04c0 Mon Sep 17 00:00:00 2001 From: Nicolas Rico Date: Sat, 9 May 2026 16:09:38 -0500 Subject: [PATCH 08/10] =?UTF-8?q?ci:=20fix=20E501=20en=20verification.py?= =?UTF-8?q?=20=E2=80=94=20break=20long=20ternary?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Backend/services/verification.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Backend/services/verification.py b/Backend/services/verification.py index 971ce6f..56c8eb2 100644 --- a/Backend/services/verification.py +++ b/Backend/services/verification.py @@ -58,8 +58,7 @@ def verify_resolution( section = _render_section(check, runtime) new_status = "resolved" if check["healthy"] else "failed" - resolved_at = datetime.now(tz=timezone.utc).isoformat() if new_status == "resolved" else None - + resolved_at = (datetime.now(tz=timezone.utc).isoformat()if new_status == "resolved"else None) update = { "status": new_status, "agent_reasoning": (current_reasoning or "") + "\n\n---\n\n" + section, From 19cc439853f7d175fb6581bb24618e6a5120214d Mon Sep 17 00:00:00 2001 From: Nicolas Rico Date: Sat, 9 May 2026 16:12:44 -0500 Subject: [PATCH 09/10] =?UTF-8?q?ci:=20fix=20E501=20en=20verification.py?= =?UTF-8?q?=20=E2=80=94=20break=20long=20ternary.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Backend/services/verification.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Backend/services/verification.py b/Backend/services/verification.py index 56c8eb2..b24acc8 100644 --- a/Backend/services/verification.py +++ b/Backend/services/verification.py @@ -58,7 +58,11 @@ def verify_resolution( section = _render_section(check, runtime) new_status = "resolved" if check["healthy"] else "failed" - resolved_at = (datetime.now(tz=timezone.utc).isoformat()if new_status == "resolved"else None) + resolved_at = ( + datetime.now(tz=timezone.utc).isoformat() + if new_status == "resolved" + else None + ) update = { "status": new_status, "agent_reasoning": (current_reasoning or "") + "\n\n---\n\n" + section, From 9566462688643f0749ccbcf21f45382b41674c8d Mon Sep 17 00:00:00 2001 From: Nicolas Rico Date: Sat, 9 May 2026 16:32:09 -0500 Subject: [PATCH 10/10] ci: format imports and improve code readability across multiple files --- Backend/routers/actions.py | 4 +-- Backend/services/agents/__init__.py | 4 +-- Backend/services/agents/memory/__init__.py | 3 ++- Backend/services/agents/postgres/tools.py | 31 +++++++++++++++------- Backend/services/verification.py | 21 ++++++++++----- Backend/tests/test_smoke.py | 2 +- 6 files changed, 43 insertions(+), 22 deletions(-) diff --git a/Backend/routers/actions.py b/Backend/routers/actions.py index 778871d..819bb3c 100644 --- a/Backend/routers/actions.py +++ b/Backend/routers/actions.py @@ -139,8 +139,8 @@ def _run_pg_command(func: str, datname: str) -> tuple[int, str, str]: - pg_terminate_backend → pg_terminate_backend() sobre todas las conexiones """ try: - import psycopg2 # type: ignore - import psycopg2.extras # type: ignore + import psycopg2 # type: ignore + import psycopg2.extras # type: ignore except ImportError: return 1, "", "psycopg2 no está instalado en el backend" diff --git a/Backend/services/agents/__init__.py b/Backend/services/agents/__init__.py index c2656b5..29ad0d6 100644 --- a/Backend/services/agents/__init__.py +++ b/Backend/services/agents/__init__.py @@ -12,8 +12,8 @@ from services.agents.base import DomainAgent, IncidentContext # Importa los dominios concretos para que se auto-registren. -from services.agents.docker import agent as _docker_agent # noqa: F401 -from services.agents.podman import agent as _podman_agent # noqa: F401 +from services.agents.docker import agent as _docker_agent # noqa: F401 +from services.agents.podman import agent as _podman_agent # noqa: F401 from services.agents.postgres import agent as _postgres_agent # noqa: F401 from services.agents.registry import get_agent, list_agents, register_agent diff --git a/Backend/services/agents/memory/__init__.py b/Backend/services/agents/memory/__init__.py index 0cb654d..91f8578 100644 --- a/Backend/services/agents/memory/__init__.py +++ b/Backend/services/agents/memory/__init__.py @@ -7,6 +7,7 @@ Cada dominio tiene sus propias colecciones: runbooks-docker, incidents-docker - runbooks-kubernetes, incidents-kubernetes (pendiente — ver docs/kubernetes_integration_handoff.md) + runbooks-kubernetes, incidents-kubernetes + (pendiente — ver docs/kubernetes_integration_handoff.md) runbooks-postgres, incidents-postgres (activo desde Sprint 2) """ diff --git a/Backend/services/agents/postgres/tools.py b/Backend/services/agents/postgres/tools.py index 3c9601c..ac78c80 100644 --- a/Backend/services/agents/postgres/tools.py +++ b/Backend/services/agents/postgres/tools.py @@ -82,16 +82,23 @@ def pg_stat_activity(datname: str = "") -> str: return _unavailable("pg_stat_activity", err or "") try: - where = "WHERE datname = %(datname)s" if datname else "WHERE datname NOT IN ('template0', 'template1', '')" + excluded = "('template0', 'template1', '')" + where = ( + "WHERE datname = %(datname)s" + if datname + else f"WHERE datname NOT IN {excluded}" + ) rows = _query(dsn, f""" SELECT datname, state, wait_event_type, wait_event, - COUNT(*) AS conexiones, - MAX(EXTRACT(EPOCH FROM (now() - query_start))) AS max_duracion_s, - MAX(EXTRACT(EPOCH FROM (now() - state_change))) AS max_estado_s + COUNT(*) AS conexiones, + MAX(EXTRACT(EPOCH FROM (now() - query_start))) + AS max_duracion_s, + MAX(EXTRACT(EPOCH FROM (now() - state_change))) + AS max_estado_s FROM pg_stat_activity {where} GROUP BY datname, state, wait_event_type, wait_event @@ -103,7 +110,8 @@ def pg_stat_activity(datname: str = "") -> str: SELECT datname, COUNT(*) AS total_conexiones, - (SELECT setting::int FROM pg_settings WHERE name = 'max_connections') AS max_connections + (SELECT setting::int FROM pg_settings + WHERE name = 'max_connections') AS max_connections FROM pg_stat_activity {where} GROUP BY datname @@ -125,13 +133,18 @@ def pg_stat_database(datname: str = "") -> str: return _unavailable("pg_stat_database", err or "") try: - where = "WHERE datname = %(datname)s" if datname else "WHERE datname NOT IN ('template0', 'template1', '')" + excluded = "('template0', 'template1', '')" + where = ( + "WHERE datname = %(datname)s" + if datname + else f"WHERE datname NOT IN {excluded}" + ) rows = _query(dsn, f""" SELECT datname, - numbackends AS conexiones_activas, - xact_commit AS commits, - xact_rollback AS rollbacks, + numbackends AS conexiones_activas, + xact_commit AS commits, + xact_rollback AS rollbacks, deadlocks, blks_read AS lecturas_disco, blks_hit AS lecturas_cache, diff --git a/Backend/services/verification.py b/Backend/services/verification.py index b24acc8..e8db977 100644 --- a/Backend/services/verification.py +++ b/Backend/services/verification.py @@ -147,8 +147,8 @@ def _inspect_postgres(datname: str) -> dict: Retorna un dict compatible con _render_section para runtime='postgres'. """ try: - import psycopg2 # type: ignore - import psycopg2.extras # type: ignore + import psycopg2 # type: ignore + import psycopg2.extras # type: ignore except ImportError: return { "healthy": False, @@ -185,9 +185,12 @@ def _inspect_postgres(datname: str) -> dict: with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cur: cur.execute(""" SELECT - (SELECT count(*) FROM pg_stat_activity WHERE datname = %(dn)s)::int AS connections, - (SELECT setting::int FROM pg_settings WHERE name = 'max_connections') AS max_connections, - round(pg_database_size(%(dn)s) / 1024.0 / 1024.0, 1) AS db_size_mb + (SELECT count(*) FROM pg_stat_activity + WHERE datname = %(dn)s)::int AS connections, + (SELECT setting::int FROM pg_settings + WHERE name = 'max_connections') AS max_connections, + round(pg_database_size(%(dn)s) / 1024.0 / 1024.0, 1) + AS db_size_mb """, {"dn": datname}) row = dict(cur.fetchone()) @@ -226,7 +229,9 @@ def _render_container_section(r: dict) -> str: summary = "El contenedor volvió a estado `running`. El reinicio fue exitoso." else: heading = "Verificación de resolución — Servicio no recuperado" - summary = f"El contenedor está en estado `{r['status']}` después del reinicio. Requiere atención manual." + state = r['status'] + summary = f"El contenedor está en estado `{state}` después del " + summary += "reinicio. Requiere atención manual." rows = [ f"| Estado | `{r['status']}` |", @@ -255,7 +260,9 @@ def _render_postgres_section(r: dict) -> str: summary = "La base de datos responde correctamente y acepta conexiones." else: heading = "Verificación de resolución — PostgreSQL no disponible" - summary = f"No se pudo conectar a la base de datos (`{r['status']}`). Requiere atención manual." + status = r['status'] + summary = "No se pudo conectar a la base de datos " + summary += f"(`{status}`). Requiere atención manual." rows = [f"| Estado | `{r['status']}` |"] if r.get("connections") is not None: diff --git a/Backend/tests/test_smoke.py b/Backend/tests/test_smoke.py index 059e0bd..cc8784d 100644 --- a/Backend/tests/test_smoke.py +++ b/Backend/tests/test_smoke.py @@ -16,8 +16,8 @@ os.environ.setdefault("SUPABASE_JWT_SECRET", "test-jwt-secret-min-32-characters-long") os.environ.setdefault("OPENAI_API_KEY", "sk-test") -from main import app # noqa: E402 from auth import get_current_user # noqa: E402 +from main import app # noqa: E402 @pytest.fixture