Add comprehensive two-tier architecture where Steward analyzes requests and Tatlock executes with scoped tools. Includes full infrastructure for request preprocessing, tool tracking, benchmarking, and streaming. **Added:** - Steward agent for request analysis and capability recommendation - Household Registry for centralized capability management - Request preprocessing pipeline (Steward → Tatlock flow) - Tool usage tracking and benchmarking system - Streaming transparency (Steward reasoning visible in streams) - Structured logging with operation timing - Redis benchmark storage with 30-day expiry - Benchmark analysis CLI tools **Infrastructure:** - src/agents/steward/ - Steward agent implementation - src/agents/tatlock_core/ - Tatlock capability domain - src/core/preprocessing.py - Request preprocessing pipeline - src/core/tool_tracking.py - Tool call tracking - src/core/benchmarks.py - Benchmark recording system - src/core/household_registry.py - Capability registry - src/core/startup.py - Application startup coordination - src/core/logging_config.py - Structured logging setup **Integration:** - Responses API uses Steward for Tatlock requests - Chat Completions wraps Responses API for OpenAI compatibility - Streaming coordinator supports Steward + Tatlock flow - Tool scoping per request based on Steward recommendations **Testing:** - Integration tests for Steward-Tatlock flow - Benchmark and registry unit tests - Steward streaming tests See PHASE2_PLAN.md and PHASE2_COMPLETE.md for detailed documentation. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
255 lines
8.0 KiB
Python
255 lines
8.0 KiB
Python
"""
|
|
Tests for structured logging configuration.
|
|
|
|
Tests logging setup, context management, and FastAPI integration.
|
|
"""
|
|
import logging
|
|
from io import StringIO
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
import structlog
|
|
|
|
from src.core.logging_config import (
|
|
add_log_level,
|
|
add_timestamp,
|
|
get_logger,
|
|
get_uvicorn_log_config,
|
|
log_operation,
|
|
)
|
|
|
|
|
|
class TestLoggingProcessors:
|
|
"""Test logging processor functions."""
|
|
|
|
def test_add_timestamp(self):
|
|
"""Test timestamp processor adds ISO timestamp."""
|
|
event_dict = {}
|
|
result = add_timestamp(None, "info", event_dict)
|
|
|
|
assert "timestamp" in result
|
|
assert isinstance(result["timestamp"], str)
|
|
# Should be ISO 8601 format
|
|
assert "T" in result["timestamp"] or "-" in result["timestamp"]
|
|
|
|
def test_add_log_level(self):
|
|
"""Test log level processor."""
|
|
event_dict = {}
|
|
result = add_log_level(None, "info", event_dict)
|
|
|
|
assert result["level"] == "INFO"
|
|
|
|
result = add_log_level(None, "error", {})
|
|
assert result["level"] == "ERROR"
|
|
|
|
|
|
class TestGetLogger:
|
|
"""Test logger retrieval."""
|
|
|
|
def test_get_logger_returns_bound_logger(self):
|
|
"""Test get_logger returns structlog BoundLogger."""
|
|
logger = get_logger("test")
|
|
# Logger should have standard logging methods
|
|
assert hasattr(logger, 'info')
|
|
assert hasattr(logger, 'debug')
|
|
assert hasattr(logger, 'warning')
|
|
assert hasattr(logger, 'error')
|
|
|
|
def test_get_logger_with_module_name(self):
|
|
"""Test logger with module name."""
|
|
logger = get_logger(__name__)
|
|
assert logger is not None
|
|
|
|
def test_logger_has_standard_methods(self):
|
|
"""Test logger has standard logging methods."""
|
|
logger = get_logger("test")
|
|
assert hasattr(logger, "debug")
|
|
assert hasattr(logger, "info")
|
|
assert hasattr(logger, "warning")
|
|
assert hasattr(logger, "error")
|
|
assert hasattr(logger, "exception")
|
|
|
|
|
|
class TestLogOperation:
|
|
"""Test log_operation context manager."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_log_operation_success(self):
|
|
"""Test log_operation for successful operation."""
|
|
logger = get_logger("test")
|
|
|
|
async with log_operation("test_operation", {"user_id": "123"}) as ctx:
|
|
# Can update context during operation
|
|
ctx["result_count"] = 5
|
|
|
|
# Context should have been updated with success info
|
|
assert ctx["success"] is True
|
|
assert ctx["result_count"] == 5
|
|
assert "duration_seconds" in ctx
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_log_operation_failure(self):
|
|
"""Test log_operation for failed operation."""
|
|
logger = get_logger("test")
|
|
|
|
with pytest.raises(ValueError):
|
|
async with log_operation("test_operation") as ctx:
|
|
raise ValueError("Test error")
|
|
|
|
# Context should have failure info
|
|
assert ctx["success"] is False
|
|
assert ctx["error"] == "Test error"
|
|
assert ctx["error_type"] == "ValueError"
|
|
assert "duration_seconds" in ctx
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_log_operation_timing(self):
|
|
"""Test log_operation records duration."""
|
|
import asyncio
|
|
|
|
async with log_operation("test_operation") as ctx:
|
|
await asyncio.sleep(0.01) # Small delay
|
|
|
|
# Should have measurable duration
|
|
assert ctx["duration_seconds"] > 0
|
|
assert ctx["duration_seconds"] < 1.0 # Should be quick
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_log_operation_initial_context(self):
|
|
"""Test log_operation with initial context."""
|
|
initial = {"request_id": "abc123", "user": "test_user"}
|
|
|
|
async with log_operation("test_operation", initial) as ctx:
|
|
pass
|
|
|
|
# Initial context should be preserved
|
|
assert ctx["request_id"] == "abc123"
|
|
assert ctx["user"] == "test_user"
|
|
assert ctx["operation"] == "test_operation"
|
|
|
|
|
|
class TestUvicornLogConfig:
|
|
"""Test uvicorn logging configuration."""
|
|
|
|
def test_get_uvicorn_log_config_returns_dict(self):
|
|
"""Test uvicorn config returns valid dict."""
|
|
config = get_uvicorn_log_config()
|
|
|
|
assert isinstance(config, dict)
|
|
assert "version" in config
|
|
assert "formatters" in config
|
|
assert "handlers" in config
|
|
assert "loggers" in config
|
|
|
|
def test_uvicorn_log_config_has_required_loggers(self):
|
|
"""Test config includes uvicorn loggers."""
|
|
config = get_uvicorn_log_config()
|
|
|
|
loggers = config["loggers"]
|
|
assert "uvicorn" in loggers
|
|
assert "uvicorn.error" in loggers
|
|
assert "uvicorn.access" in loggers
|
|
|
|
def test_uvicorn_log_config_format_selection(self):
|
|
"""Test config format changes based on environment."""
|
|
# Just test that the config is valid, format is determined by environment
|
|
config = get_uvicorn_log_config()
|
|
# Should have required structure
|
|
assert "version" in config
|
|
assert "formatters" in config
|
|
assert "handlers" in config
|
|
assert "loggers" in config
|
|
|
|
|
|
class TestLoggingIntegration:
|
|
"""Test logging integration with standard library."""
|
|
|
|
def test_standard_logging_works(self):
|
|
"""Test standard logging.getLogger works."""
|
|
logger = logging.getLogger("test.standard")
|
|
# Should not raise
|
|
logger.info("Test message")
|
|
|
|
def test_structlog_and_stdlib_coexist(self):
|
|
"""Test structlog and stdlib can coexist."""
|
|
struct_logger = get_logger("test.struct")
|
|
std_logger = logging.getLogger("test.std")
|
|
|
|
# Both should work
|
|
struct_logger.info("Structured log")
|
|
std_logger.info("Standard log")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_logging_in_async_context(self):
|
|
"""Test logging works in async context."""
|
|
logger = get_logger("test.async")
|
|
|
|
async def async_function():
|
|
logger.info("Async log message", task="async_task")
|
|
|
|
await async_function()
|
|
|
|
|
|
class TestLoggingOutput:
|
|
"""Test actual logging output."""
|
|
|
|
def test_logger_outputs_structured_data(self):
|
|
"""Test logger can output structured data."""
|
|
logger = get_logger("test.output")
|
|
|
|
# Log with structured data
|
|
logger.info(
|
|
"user_action",
|
|
user_id="123",
|
|
action="login",
|
|
success=True,
|
|
)
|
|
# Should not raise, output tested in integration tests
|
|
|
|
def test_logger_handles_exceptions(self):
|
|
"""Test logger handles exception logging."""
|
|
logger = get_logger("test.exceptions")
|
|
|
|
try:
|
|
raise ValueError("Test error")
|
|
except ValueError:
|
|
logger.exception("Error occurred", extra_field="value")
|
|
# Should not raise
|
|
|
|
def test_different_log_levels(self):
|
|
"""Test different log levels."""
|
|
logger = get_logger("test.levels")
|
|
|
|
logger.debug("Debug message", level="debug")
|
|
logger.info("Info message", level="info")
|
|
logger.warning("Warning message", level="warning")
|
|
logger.error("Error message", level="error")
|
|
# Should not raise
|
|
|
|
|
|
class TestLoggingConfiguration:
|
|
"""Test logging configuration behavior."""
|
|
|
|
def test_logging_respects_environment(self):
|
|
"""Test logging format changes with environment."""
|
|
from src.core.config import Environment, config
|
|
|
|
# In development, should use console format
|
|
if config.ENVIRONMENT == Environment.DEVELOPMENT:
|
|
assert config.log_format == "console"
|
|
|
|
# Mock production environment
|
|
with patch.object(config, "ENVIRONMENT", Environment.PRODUCTION):
|
|
assert config.log_format == "json"
|
|
|
|
def test_multiple_loggers_independent(self):
|
|
"""Test multiple loggers are independent."""
|
|
logger1 = get_logger("test.logger1")
|
|
logger2 = get_logger("test.logger2")
|
|
|
|
assert logger1 is not logger2
|
|
|
|
# Both should work independently
|
|
logger1.info("Logger 1 message")
|
|
logger2.info("Logger 2 message")
|