# 🧪 Cómo generar nuevas baterías de test semántico
## 🤔 ¿Qué hago? ¿Cómo lo hago? ¿Y para qué lo hago?
**¿Qué hago?**
Creo una batería de tests que valida que la caché semántica de klaude-proxy distingue correctamente entre preguntas nuevas (MISS) y reformulaciones de preguntas ya cacheadas (HIT).
**¿Cómo lo hago?**
Diseño 3 preguntas originales sobre tópicos distintos, genero 7 variantes léxicas de esas preguntas preservando los tokens técnicos exactos, valido cada variante con `validate_variant.sh` y las ensamblo en un script bash siguiendo el template estándar.
**¿Para qué lo hago?**
Para aumentar la cobertura de validación de la caché semántica con nuevos dominios y tipos de pregunta, detectando regresiones en el threshold (0.87), el modelo de embeddings (`nomic-embed-text-v1.5`) o cambios en la lógica del proxy.
---
## 📐 Estructura obligatoria de una batería
```text
10 preguntas = 3 originales (MISS) + 7 variantes léxicas (HIT)
Q1 → MISS — tópico A (original)
Q2 → MISS — tópico B (original)
Q3 → MISS — tópico C (original)
Q4 → HIT — variante de Q1
Q5 → HIT — variante de Q1
Q6 → HIT — variante de Q2
Q7 → HIT — variante de Q2
Q8 → HIT — variante de Q3
Q9 → HIT — variante de Q3
Q10 → HIT — variante de Q3
```
**Distribución mínima de variantes:** 2 por tópico A y B, 3 por tópico C. Puede redistribuirse siempre que ningún tópico tenga menos de 2 variantes.
---
## 🔑 Regla de rephrasing léxico — la más importante
> **Mantener todos los tokens técnicos exactos. Solo variar la estructura gramatical.**
### Tokens que NUNCA se cambian
Estos términos son técnicos y su modificación reduce la similitud coseno por debajo del threshold 0.87:
| Categoría | Ejemplos de tokens intocables |
| --- | --- |
| Nombres de componentes | `klaude-proxy`, `Qdrant`, `FastAPI`, `asyncio`, `ONNX` |
| Variables de entorno | `QDRANT_URL`, `ANTHROPIC_API_KEY`, `SIMILARITY_THRESHOLD` |
| Parámetros técnicos | `768`, `coseno`, `0.87`, `tool_use`, `stop_reason` |
| Nombres de endpoint | `/v1/messages`, `/cache/flush`, `/analytics/history` |
| Nombres de campo/header | `x-api-key`, `x-cache`, `points_count` |
| Tipos y clases Python | `asyncio.Semaphore`, `set[asyncio.Task]`, `_background_tasks` |
### Qué SÍ se puede variar
Solo la envoltura gramatical alrededor de los tokens técnicos:
| Original | Variante válida |
| --- | --- |
| `¿Qué errores produce ...` | `¿Qué fallos genera ...` |
| `¿Qué causa un error 500 ...` | `¿Qué provoca un error 500 ...` |
| `¿Cuál es la razón por la que ...` | `¿Por qué ...` |
| `Genera el código Python de ...` | `Escribe el código Python de ...` |
| `Implementa la función Python que ...` | `Crea la función Python que ...` |
| `cuando X no está disponible` | `si X no responde` |
| `no puede cargarse` | `falla al cargarse` / `no se puede cargar` |
### Ejemplo completo
```text
✅ Original (MISS):
"¿Qué errores produce klaude-proxy cuando Qdrant no está disponible
en la URL configurada en QDRANT_URL?"
✅ Variante válida (HIT — solo verbo cambiado, tokens técnicos intactos):
"¿Qué fallos genera klaude-proxy cuando Qdrant no está disponible
en la URL configurada en QDRANT_URL?"
✅ Variante válida (HIT — estructura condicional cambiada):
"¿Qué errores produce klaude-proxy si Qdrant no responde
en la URL configurada en QDRANT_URL?"
❌ Variante inválida (FAIL — token técnico sustituido por su significado):
"¿Qué errores produce klaude-proxy cuando la base de datos vectorial
no está disponible en la dirección de conexión?"
```
---
## 🗂️ Cómo elegir tópicos para una nueva batería
### Regla de separación semántica
Los 3 tópicos de la batería deben ser **semánticamente distintos entre sí y con respecto a las baterías existentes**. Con threshold=0.87 y nomic-embed-text, dos preguntas sobre el mismo concepto con diferente formulación pueden colapsar en HIT.
### Tópicos ya cubiertos (no repetir)
| Batería | Dominio | Tópicos cubiertos |
| --- | --- | --- |
| v1 | Semántica básica | Dimensión embedding + modelo, columnas SQLite analytics.db, formato SSE |
| v2 | Threshold / exclusiones | Variable de umbral similitud, tipos de respuesta excluidos del caché, streaming en HIT |
| v3 | Concurrencia Python | asyncio.Semaphore ONNX, _background_tasks, exclusión tool_use |
| v4 | Generación de código | Inicialización colección Qdrant 768/coseno, handler /v1/messages + x-api-key, cálculo coste USD |
| v5 | Errores técnicos | Errores conexión Qdrant/QDRANT_URL, error 500 ANTHROPIC_API_KEY, fallo carga ONNX |
| v6 | Autenticación | /cache/flush requiere x-api-key → 401, reenvío x-api-key a Anthropic, ANTHROPIC_API_KEY obligatorio en Settings |
| v7 | Configuración | CACHE_TOOL_RESPONSES (default false), QDRANT_COLLECTION (default claude_cache), PROXY_PORT (default 8080) |
| v8 | Analytics avanzado | /analytics/stats in-memory vs /analytics/history SQLite, request_preview 400 chars, calculate_cost formula |
| v9 | Dashboard | /logs/stream keepalive 300ms, /logs?n parámetro, reconexión SSE setTimeout 3000ms |
| v10 | Rendimiento y memoria | preload_model doble sesión ONNX, serialize_request cap 2000 chars, mem_limit 2g compose |
| v11 | Logging y telemetría | Namespace klaude.proxy + log_buffer.install, color azul ANSI telemetría, buffer append-only |
| v12 | Deploy y contenedores | depends_on service_healthy, FASTEMBED_CACHE_PATH Dockerfile, healthcheck curl /health |
| v13 | Serialización y bypass | _SUBSYSTEM_RE subsistemas Claude Code, _INJECTED_BLOCK_RE limpia XML, serialize_store_key tool_result |
---
## 🌐 Taxonomía de dominios
Un **dominio** es el área funcional del proxy de la que se extraen los 3 tópicos de una batería. La relación jerárquica es:
```text
dominio (área funcional amplia)
└── tópico A → Q1 MISS + Q4, Q5 HIT
└── tópico B → Q2 MISS + Q6, Q7 HIT
└── tópico C → Q3 MISS + Q8, Q9, Q10 HIT
```
El dominio da **coherencia temática** a la batería. Los 3 tópicos deben ser conceptualmente distintos dentro del dominio: la similitud coseno entre dos preguntas de tópicos distintos debe quedar **por debajo de 0.87** para que Q1-Q3 sean MISS independientes.
### 📐 Dominios del proxy y su mapa de cobertura
| ID | Dominio | Área de código | Estado |
| --- | --- | --- | --- |
| D01 | Semántica básica | embeddings.py, analytics.py, log_buffer.py | ✅ v1 |
| D02 | Threshold y exclusiones | config.py, cache.py | ✅ v2 |
| D03 | Concurrencia Python | main.py (_embed_sem, _background_tasks) | ✅ v3 |
| D04 | Generación de código | cache.py, main.py, analytics.py | ✅ v4 |
| D05 | Errores técnicos | main.py, embeddings.py, config.py | ✅ v5 |
| D06 | Autenticación | main.py (/cache/flush), config.py (Settings) | ✅ v6 |
| D07 | Configuración del proxy | config.py (variables opcionales) | ✅ v7 |
| D08 | Analytics avanzado | analytics.py (stats/history, calculate_cost) | ✅ v8 |
| D09 | Dashboard | main.py (HTML, /logs/stream, /logs) | ✅ v9 |
| D10 | Rendimiento y memoria | embeddings.py (preload_model), compose.yaml | ✅ v10 |
| D11 | Logging y telemetría | log_buffer.py, main.py (_ColoredFormatter) | ✅ v11 |
| D12 | Deploy y contenedores | compose.yaml, Dockerfile | ✅ v12 |
| D13 | Serialización y bypass | embeddings.py (serialize_*, regex) | ✅ v13 |
| D14 | Modelo de datos Anthropic | models.py (si existe), estructuras JSON | ⬜ disponible |
| D15 | Cliente Anthropic | anthropic_client.py (forward, stream, SSE) | ⬜ disponible |
### 🔍 Cómo verificar que dos tópicos no colisionan
Antes de crear la batería, comprobar que Q1, Q2 y Q3 no se devuelven HIT entre sí:
```bash
# Paso 1: lanzar Q1 (MISS esperado — nuevo vector en caché)
curl -s -X POST http://192.168.1.50:8080/v1/messages \
-H "x-api-key: test" -H "content-type: application/json" \
-d '{"model":"claude-haiku-4-5-20251001","max_tokens":512,"messages":[{"role":"user","content":"[Q1]"}]}'
# Paso 2: lanzar Q2 — si devuelve HIT, los tópicos A y B son demasiado similares
# Paso 3: lanzar Q3 — si devuelve HIT, el tópico C solapa con A o B
# Si alguno devuelve HIT: elegir un tópico más específico o de otro sub-área del dominio
```
### 💡 Regla práctica para elegir tópicos dentro de un dominio
Dos tópicos son suficientemente distintos si responden a **preguntas de naturaleza diferente** dentro del mismo dominio:
| Naturaleza de pregunta | Ejemplo |
| --- | --- |
| ¿Qué valor tiene X? | Valor por defecto de `PROXY_PORT` |
| ¿Qué ocurre cuando Y? | Comportamiento al faltar `x-api-key` |
| ¿Cómo funciona Z? | Mecanismo de `serialize_store_key` |
| ¿Dónde está W? | Namespace del logger principal |
| ¿Por qué se hace V? | Razón del buffer append-only |
Mezclar naturalezas distintas dentro del mismo dominio garantiza separación semántica.
---
## 🔬 Validar variantes antes de incluirlas
Antes de añadir una variante al script, validarla individualmente con `validate_variant.sh`:
```bash
# Sintaxis
bash tests/validate_variant.sh "pregunta original exacta" "variante candidata"
# Ejemplo
bash tests/validate_variant.sh \
"¿Qué errores produce klaude-proxy cuando Qdrant no está disponible en la URL configurada en QDRANT_URL?" \
"¿Qué fallos genera klaude-proxy si Qdrant no responde en la URL configurada en QDRANT_URL?"
```
**Resultado esperado:**
```text
✅ PASS — Variante válida (similitud ≥ 0.87)
Puedes incluir esta variante en el test suite.
```
Si devuelve FAIL, la variante no alcanza el threshold. Reformular según las pistas del script y repetir.
---
## 📋 Template para una nueva batería
Copiar este template, rellenar los `[PLACEHOLDER]` y guardar como `tests/test_cache_semantic_vN.sh`:
```bash
#!/usr/bin/env bash
# [N]ª batería — caché semántica klaude-proxy — v[N] [TEMA]
# [Descripción de los tópicos cubiertos]
# Diseño: 3 preguntas originales (MISS) + 7 variantes léxicas (HIT)
# Portable: configurar con variables de entorno PROXY, MODEL, API_KEY
# Ejemplo: PROXY=http://192.168.1.50:8080 bash test_cache_semantic_v[N].sh
set -euo pipefail
PROXY="${PROXY:-http://192.168.1.50:8080}"
MODEL="${MODEL:-claude-haiku-4-5-20251001}"
API_KEY="${API_KEY:-test}"
PASS=0
FAIL=0
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
NC='\033[0m'
call_proxy() {
local label="$1"
local expected="$2"
local content="$3"
payload=$(printf '{"model":"%s","max_tokens":512,"messages":[{"role":"user","content":"%s"}]}' \
"$MODEL" "$(printf '%s' "$content" | sed 's/"/\\"/g')")
response=$(curl -s -D - \
-H "x-api-key: $API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "content-type: application/json" \
-d "$payload" \
"$PROXY/v1/messages" 2>&1)
cache_header=$(echo "$response" | grep -i "x-cache:" | tr -d '\r' | awk '{print $2}')
cache_header=${cache_header:-"UNKNOWN"}
if [[ "$cache_header" == "$expected" ]]; then
status="${GREEN}✅ PASS${NC}"
PASS=$((PASS + 1))
else
status="${RED}❌ FAIL (got $cache_header, expected $expected)${NC}"
FAIL=$((FAIL + 1))
fi
printf " %-6s %-10s $status\n" "[$label]" "[$cache_header]"
}
echo ""
echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
echo -e "${CYAN} klaude-proxy — Batería [TEMA] v[N] (Issue #[N])${NC}"
echo -e "${CYAN} [Descripción breve]${NC}"
echo -e "${CYAN} Proxy: $PROXY${NC}"
echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
echo ""
# ── Prerequisito: caché en estado limpio ──────────────────────────────────────
echo -e "${YELLOW}⚠️ AVISO: Este test asume que el caché de Qdrant está vacío.${NC}"
echo -e "${YELLOW} Q1-Q3 esperan MISS — si el caché tiene datos, devolverán HIT y el test fallará.${NC}"
echo -e "${YELLOW} Flush manual: curl -s -X DELETE -H \"x-api-key: \$API_KEY\" \$PROXY/cache/flush${NC}"
echo -e "${YELLOW} Ver: docs/cache_flush_manual.md${NC}"
echo ""
echo -e "${YELLOW}📊 Estado actual del caché (referencia):${NC}"
curl -s "$PROXY/cache/stats" | python3 -m json.tool 2>/dev/null || curl -s "$PROXY/cache/stats"
echo ""
# ── Tópicos cubiertos ─────────────────────────────────────────────────────────
# A. [Tópico A] → Q1 (MISS) + Q4, Q5 (HIT)
# B. [Tópico B] → Q2 (MISS) + Q6, Q7 (HIT)
# C. [Tópico C] → Q3 (MISS) + Q8, Q9, Q10 (HIT)
echo -e "${YELLOW}━━━ Fase 1: 3 preguntas originales → esperado MISS ━━━${NC}"
echo ""
call_proxy "Q1" "MISS" \
"[PREGUNTA ORIGINAL TÓPICO A]"
call_proxy "Q2" "MISS" \
"[PREGUNTA ORIGINAL TÓPICO B]"
call_proxy "Q3" "MISS" \
"[PREGUNTA ORIGINAL TÓPICO C]"
echo ""
echo -e "${YELLOW}━━━ Fase 2: 7 variantes léxicas → esperado HIT ━━━${NC}"
echo ""
# Variantes de Q1 — [Tópico A]
call_proxy "Q4" "HIT" \
"[VARIANTE 1 DE Q1]"
call_proxy "Q5" "HIT" \
"[VARIANTE 2 DE Q1]"
# Variantes de Q2 — [Tópico B]
call_proxy "Q6" "HIT" \
"[VARIANTE 1 DE Q2]"
call_proxy "Q7" "HIT" \
"[VARIANTE 2 DE Q2]"
# Variantes de Q3 — [Tópico C]
call_proxy "Q8" "HIT" \
"[VARIANTE 1 DE Q3]"
call_proxy "Q9" "HIT" \
"[VARIANTE 2 DE Q3]"
call_proxy "Q10" "HIT" \
"[VARIANTE 3 DE Q3]"
echo ""
echo -e "${YELLOW}━━━ Estado final del caché ━━━${NC}"
echo ""
curl -s "$PROXY/cache/stats" | python3 -m json.tool 2>/dev/null || curl -s "$PROXY/cache/stats"
echo ""
echo -e "${YELLOW}━━━ Analytics — histórico SQLite ━━━${NC}"
echo ""
curl -s "$PROXY/analytics/history" | python3 -m json.tool 2>/dev/null || curl -s "$PROXY/analytics/history"
echo ""
echo -e "${YELLOW}━━━ Analytics — sesión actual ━━━${NC}"
echo ""
curl -s "$PROXY/analytics/stats" | python3 -m json.tool 2>/dev/null || curl -s "$PROXY/analytics/stats"
echo ""
echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
TOTAL=$((PASS + FAIL))
if [[ $FAIL -eq 0 ]]; then
echo -e "${GREEN} RESULTADO: $PASS/$TOTAL PASS — Batería [TEMA] validada ✅${NC}"
else
echo -e "${RED} RESULTADO: $PASS/$TOTAL PASS — $FAIL fallos ❌${NC}"
fi
echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
echo ""
echo -e " Dashboard: ${CYAN}$PROXY/dashboard${NC}"
echo ""
```
---
## 🔄 Registrar la nueva batería en el wrapper
Tras crear `tests/test_cache_semantic_vN.sh`, añadirla a `tests/run_all_tests.sh`:
```bash
# En SUITE_FILES — añadir al final del array:
SUITE_FILES=(
...
"test_cache_semantic_vN.sh" # ← nueva suite
)
# En SUITE_NAMES — descripción corta:
SUITE_NAMES=(
...
"vN — [tema breve] ([tópico A], [tópico B], [tópico C])"
)
# En SUITE_EXPECTED — resultado esperado (normalmente 10):
SUITE_EXPECTED=(... "10")
# Si hay edge-cases conocidos, ajustar al número esperado real.
```
---
## 📊 Flujo completo de desarrollo de una batería
```mermaid
flowchart TD
A[🎯 Elegir 3 tópicos nuevos] --> B{¿Solapan con baterías existentes?}
B -- Sí --> A
B -- No --> C[✍️ Redactar 3 preguntas originales MISS]
C --> D[✍️ Redactar 7 variantes léxicas HIT]
D --> E[🔬 Validar cada variante con validate_variant.sh]
E --> F{¿Alguna variante falla?}
F -- Sí --> G[🔧 Reformular: mantener tokens técnicos,\ncambiar solo estructura gramatical]
G --> E
F -- No --> H[📝 Ensamblar script desde template]
H --> I[🧪 Ejecutar la suite nueva en aislamiento\nbash tests/test_cache_semantic_vN.sh]
I --> J{¿10/10 PASS?}
J -- No --> K{¿Edge-case conocido?}
K -- Sí --> L[📋 Documentar en SUITE_EXPECTED del wrapper]
K -- No --> G
J -- Sí --> L
L --> M[➕ Registrar en run_all_tests.sh]
M --> N[🔁 Ejecutar bash tests/run_all_tests.sh]
N --> O[✅ Abrir PR]
```
---
## 🔗 Documentos relacionados
- [cache.md](cache.md) — arquitectura del caché semántico, threshold, modelo de embeddings
- [cache_flush_manual.md](cache_flush_manual.md) — cuándo y cómo ejecutar el flush
- [proxy.md](proxy.md) — endpoints del proxy, headers de respuesta