"""Test de integración contra sesiones REALES de Redis (db 1). Valida el budget por-ventana y la compactación sobre las conversaciones reales del agentic (las que los usuarios mantienen abiertas), no sobre fixtures sintéticos. Es OPT-IN: se salta si no hay Redis disponible o no hay sesiones, para no acoplar la suite a datos de cliente ni romper en CI. Ejecutar contra el Redis real: docker run --rm --network acai-net \\ -v "$PWD/agenticSystem/src:/app/src" -v "$PWD/agenticSystem/tests:/app/tests" \\ -e AGENTIC_REDIS_HOST=redis -w /app acai-vscode-plugin-agentic \\ sh -lc "pip install -q pytest pytest-asyncio; python -m pytest tests/test_context_real_session.py -q" """ from __future__ import annotations import asyncio import enum import json import sys import types import pytest if not hasattr(enum, "StrEnum"): class _CompatStrEnum(str, enum.Enum): pass enum.StrEnum = _CompatStrEnum for _name, _attr in (("anthropic", "AsyncAnthropic"), ("openai", "AsyncOpenAI")): if _name not in sys.modules: _stub = types.ModuleType(_name) setattr(_stub, _attr, type("_Stub", (), {})) sys.modules[_name] = _stub from src.config import settings from src.context.compactor import estimate_tokens from src.context.engine import ContextEngine from src.models.agent import AgentProfile from src.models.session import SessionState def _load_largest_real_session(): """Mayor sesión real de Redis db 1, o None si no hay acceso/sesiones.""" try: import redis r = redis.Redis( host=settings.redis_host, port=settings.redis_port, db=1, password=settings.redis_password or None, decode_responses=True, socket_connect_timeout=2, ) keys = [ k for k in r.scan_iter("agentic:session:*") if not k.endswith((":events", ":artifacts")) ] if not keys: return None biggest = max(keys, key=lambda k: r.strlen(k)) raw = r.get(biggest) return json.loads(raw) if raw else None except Exception: return None def test_real_session_compacts_under_model_window(monkeypatch): data = _load_largest_real_session() if not data or not data.get("recent_messages"): pytest.skip("sin Redis/sesiones reales disponibles") rm = data["recent_messages"] raw_tokens = sum(estimate_tokens(json.dumps(m)) for m in rm) from src.orchestrator import cost async def _fake_window(model_id): return 32_000 monkeypatch.setattr(cost, "resolve_context_window", _fake_window) session = SessionState( immutable_rules=data.get("immutable_rules") or ["No romper"], project_profile=data.get("project_profile") or {}, task_history=data.get("task_history") or [], recent_messages=rm, ) session.begin_task("Sigamos con lo anterior") agent = AgentProfile( role="acai", name="Acai", system_prompt="Haz el trabajo.", context_sections=["immutable_rules", "task_state"], ) pkg = asyncio.run( ContextEngine().build_context( session=session, agent=agent, conversation=rm, model_id="openrouter/x" ) ) # Budget derivado de la ventana REAL del modelo (32k), no del fijo de 120k/200k. assert pkg.budget_tokens == settings.budget_for_window(32_000) # La sesión real se compactó de verdad (no se reenvía cruda). assert pkg.total_token_estimate < raw_tokens # Y el resultado cabe en el budget del modelo → no habría overflow. assert pkg.total_token_estimate <= pkg.budget_tokens