Fix mypy errors and add network timeout handler for flaky CI tests

Mypy fixes (no-any-return errors from external libraries):
- litellm_pricing.py: cast litellm.model_cost
- anthropic.py: cast litellm cost returns
- cohere.py: cast litellm info/cost returns
- compressor.py: explicit int() for PIL size calculations
- sqlite.py: explicit bytes() for numpy tobytes()
- universal.py: explicit str() for CCR store key
- direct_mem0.py: explicit list() for OpenAI embedding
- langchain/agents.py: explicit str() for result
- server.py: explicit str() for httpx response.text
- runner_v2/v3.py: add hasattr check for backend.close()

Test fixes (flaky network timeouts in CI):
- Add network_timeout_handler decorator to skip on httpx.ReadTimeout
- Applied to test_close_idempotent, test_save_with_entities, test_add_batch_basic

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
chopratejas 2026-01-27 19:39:33 -08:00
parent 67631fecfc
commit 52da662979
13 changed files with 57 additions and 16 deletions

View file

@ -375,7 +375,7 @@ class UniversalCompressor:
original_tokens=self._estimate_tokens(original),
compressed_tokens=self._estimate_tokens(compressed),
)
return key
return str(key) if key else None
except ImportError:
logger.debug("CCR store not available")
return None

View file

@ -403,8 +403,8 @@ class LoCoMoEvaluatorV2:
finally:
# Clean up backend
if self._backend:
await self._backend.close()
if self._backend and hasattr(self._backend, "close"):
await self._backend.close() # type: ignore[union-attr]
# Calculate final metrics
duration = time.time() - start_time

View file

@ -277,8 +277,8 @@ class LoCoMoEvaluatorV3:
all_results.append(result)
finally:
if self._backend:
await self._backend.close()
if self._backend and hasattr(self._backend, "close"):
await self._backend.close() # type: ignore[union-attr]
# Aggregate results
duration = time.time() - start_time

View file

@ -241,7 +241,7 @@ class ImageCompressor:
# High detail: 85 tokens per 512x512 tile + 170 base
tiles_x = (width + 511) // 512
tiles_y = (height + 511) // 512
return 85 * tiles_x * tiles_y + 170
return int(85 * tiles_x * tiles_y + 170)
def _apply_compression(
self,

View file

@ -207,7 +207,7 @@ class HeadroomToolWrapper:
# Check if compression is needed
if len(result) < self.min_chars_to_compress:
self._record_metrics(result, result, was_compressed=False)
return result
return str(result)
# Try to compress
compressed = self._compress_output(result)

View file

@ -143,7 +143,7 @@ class SQLiteMemoryStore:
"""Serialize numpy array to bytes for BLOB storage."""
if embedding is None:
return None
return embedding.astype(np.float32).tobytes()
return bytes(embedding.astype(np.float32).tobytes())
def _deserialize_embedding(
self, data: bytes | None, dim: int | None = None

View file

@ -232,7 +232,7 @@ class DirectMem0Adapter:
input=text,
model=self._config.embedder_model,
)
return response.data[0].embedding
return list(response.data[0].embedding)
def _generate_id(self, content: str, user_id: str) -> str:
"""Generate a deterministic ID for a memory."""

View file

@ -37,7 +37,7 @@ def get_litellm_model_cost() -> dict[str, Any]:
Returns:
Dictionary mapping model names to their pricing/capability info.
"""
return litellm.model_cost
return litellm.model_cost # type: ignore[no-any-return]
def get_model_pricing(model: str) -> LiteLLMModelPricing | None:

View file

@ -579,7 +579,7 @@ class AnthropicProvider(Provider):
"cached_input", pricing["input"]
)
cost += cached_cost
return cost
return cost # type: ignore[no-any-return]
except Exception as e:
logger.debug(f"LiteLLM cost estimation failed for {model}: {e}")
@ -596,7 +596,7 @@ class AnthropicProvider(Provider):
+ (output_tokens / 1_000_000) * pricing["output"]
)
return cost
return cost # type: ignore[no-any-return]
def _get_pricing(self, model: str) -> dict[str, float] | None:
"""Get pricing for a model with fallback logic."""

View file

@ -271,11 +271,11 @@ class CohereProvider(Provider):
if info and "max_input_tokens" in info:
result = info["max_input_tokens"]
if result is not None:
return result
return int(result)
if info and "max_tokens" in info:
result = info["max_tokens"]
if result is not None:
return result
return int(result)
except Exception:
pass
@ -335,7 +335,7 @@ class CohereProvider(Provider):
completion_tokens=output_tokens,
)
if cost is not None:
return cost
return float(cost)
except Exception:
pass

View file

@ -4035,7 +4035,7 @@ class HeadroomProxy:
try:
response = await self.http_client.get(url, headers=headers) # type: ignore[union-attr]
if response.status_code == 200:
return response.text
return str(response.text)
logger.error(f"Failed to download file {file_id}: {response.status_code}")
return None
except Exception as e:

View file

@ -9,6 +9,9 @@ Tests cover:
- History chain traversal
- Deletion operations
- Convenience methods (remember, recall, get_user_memories, get_session_memories)
Note: These are integration tests that may hit external embedding APIs.
Tests are marked to skip on network timeouts (flaky CI).
"""
# CRITICAL: Must set TOKENIZERS_PARALLELISM before any imports
@ -16,9 +19,11 @@ import os
os.environ["TOKENIZERS_PARALLELISM"] = "false"
import functools
import tempfile
from pathlib import Path
import httpx
import pytest
from headroom.memory.config import MemoryConfig
@ -26,6 +31,20 @@ from headroom.memory.core import HierarchicalMemory
from headroom.memory.models import Memory, ScopeLevel
from headroom.memory.ports import MemoryFilter
def network_timeout_handler(func):
"""Decorator to skip tests on network timeouts (flaky CI)."""
@functools.wraps(func)
async def wrapper(*args, **kwargs):
try:
return await func(*args, **kwargs)
except httpx.ReadTimeout:
pytest.skip("Skipped due to network timeout (flaky CI)")
return wrapper
# =============================================================================
# Fixtures
# =============================================================================
@ -60,6 +79,7 @@ class TestAddBatch:
"""Tests for HierarchicalMemory.add_batch()."""
@pytest.mark.asyncio
@network_timeout_handler
async def test_add_batch_basic(self, memory_system):
"""Test basic batch addition."""
memories_data = [

View file

@ -7,6 +7,9 @@ Tests cover:
- Error handling and edge cases
- Backend type switching
- Resource cleanup
Note: These are integration tests that may hit external embedding APIs.
Tests are marked to skip on network timeouts (flaky CI).
"""
# CRITICAL: Must set TOKENIZERS_PARALLELISM before any imports that might
@ -18,10 +21,26 @@ os.environ["TOKENIZERS_PARALLELISM"] = "false"
import tempfile
from pathlib import Path
import httpx
import pytest
from headroom.memory.easy import Memory, MemoryResult
def network_timeout_handler(func):
"""Decorator to skip tests on network timeouts (flaky CI)."""
import functools
@functools.wraps(func)
async def wrapper(*args, **kwargs):
try:
return await func(*args, **kwargs)
except httpx.ReadTimeout:
pytest.skip("Skipped due to network timeout (flaky CI)")
return wrapper
# =============================================================================
# Fixtures
# =============================================================================
@ -165,6 +184,7 @@ class TestMemorySave:
assert memory_id is not None
@pytest.mark.asyncio
@network_timeout_handler
async def test_save_with_entities(self, memory_instance):
"""Test saving with pre-extracted entities."""
memory_id = await memory_instance.save(
@ -455,6 +475,7 @@ class TestMemoryClose:
assert mem._initialized is False
@pytest.mark.asyncio
@network_timeout_handler
async def test_close_idempotent(self, temp_db_path):
"""Test that close can be called multiple times."""
mem = Memory(backend="local", db_path=temp_db_path)