0ef5fcb1c5
Security / Dependency audit (pip-audit) (push) Has been cancelled
Security / CodeQL (javascript-typescript) (push) Has been cancelled
Security / CodeQL (python) (push) Has been cancelled
Security / Secret scan (gitleaks) (push) Has been cancelled
rust / test (ubuntu) (push) Has been cancelled
rust / simulator e2e (macos-latest) (push) Has been cancelled
rust / simulator e2e (ubuntu-latest) (push) Has been cancelled
rust / simulator e2e (windows-latest) (push) Has been cancelled
rust / wheels (aarch64-apple-darwin) (push) Has been cancelled
rust / wheels (x86_64-unknown-linux-gnu) (push) Has been cancelled
rust / wheels (x86_64-apple-darwin) (push) Has been cancelled
rust / audit (push) Has been cancelled
rust / parity (nightly, allowed to fail during Phase 0) (push) Has been cancelled
CI / commitlint (push) Has been skipped
Dev Containers / validate (.devcontainer/devcontainer.json, default) (push) Failing after 0s
Dev Containers / validate (.devcontainer/memory-stack/devcontainer.json, memory-stack) (push) Failing after 0s
Dev Containers / validate-worktree (push) Failing after 0s
CI / changes (push) Failing after 4s
Deploy Documentation / validate (push) Has been skipped
Deploy Documentation / deploy (push) Failing after 1s
Init Native E2E / init-native (ubuntu-latest, claude) (push) Failing after 1s
Init Native E2E / init-native (ubuntu-latest, codex) (push) Failing after 1s
Install Native E2E / install-native (ubuntu-latest) (push) Failing after 1s
OpenCode Plugin / typecheck + build + test (push) Failing after 1s
Init Native E2E / init-native (ubuntu-latest, copilot) (push) Failing after 1s
Release Please / release-please (push) Failing after 1s
Wrap E2E / docker-wrap-e2e (push) Failing after 1s
Wrap Native E2E / wrap-native (ubuntu-latest) (push) Failing after 1s
Init E2E / docker-init-e2e (push) Failing after 4s
Merge Conflicts / merge-conflicts (push) Failing after 4s
CI / lint (push) Has been cancelled
CI / build-wheel (push) Has been cancelled
CI / build-wheel-windows (push) Has been cancelled
CI / prefetch-model (push) Has been cancelled
CI / test-dashboard-ui (push) Has been cancelled
CI / test (1) (push) Has been cancelled
CI / test (2) (push) Has been cancelled
CI / test (3) (push) Has been cancelled
CI / test (4) (push) Has been cancelled
CI / test-extras (push) Has been cancelled
CI / test-agno (push) Has been cancelled
CI / build (push) Has been cancelled
CI / workflow-validation (push) Has been cancelled
CI / docker-native-e2e (push) Has been cancelled
CI / windows-native-wrapper (push) Has been cancelled
CI / macos-native-wrapper (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code-nonroot name:code-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code-slim name:code-slim]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code-slim-nonroot name:code-slim-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-nonroot name:nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-slim name:slim]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-slim-nonroot name:slim-nonroot]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime name:]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-code name:code]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-code-nonroot name:code-nonroot]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-code-slim name:code-slim]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-code-slim-nonroot name:code-slim-nonroot]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-nonroot name:nonroot]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-slim name:slim]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-slim-nonroot name:slim-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime name:]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code name:code]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code-nonroot name:code-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code-slim name:code-slim]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code-slim-nonroot name:code-slim-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-nonroot name:nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-slim name:slim]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-slim-nonroot name:slim-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime name:]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code name:code]) (push) Has been cancelled
Docker / promote-latest (push) Has been cancelled
Init Native E2E / init-native (macos-latest, claude) (push) Has been cancelled
Init Native E2E / init-native (macos-latest, codex) (push) Has been cancelled
Init Native E2E / init-native (macos-latest, copilot) (push) Has been cancelled
Install Native E2E / install-native (macos-latest) (push) Has been cancelled
Wrap Native E2E / wrap-native (macos-latest) (push) Has been cancelled
544 lines
18 KiB
Python
544 lines
18 KiB
Python
"""Tests for LangChain agent tool integration.
|
|
|
|
Tests cover:
|
|
1. ToolCompressionMetrics - Dataclass for tool compression metrics
|
|
2. ToolMetricsCollector - Collector for compression metrics
|
|
3. HeadroomToolWrapper - Wrapper for LangChain tools with compression
|
|
4. wrap_tools_with_headroom - Convenience function for wrapping multiple tools
|
|
5. get_tool_metrics / reset_tool_metrics - Global metrics access
|
|
"""
|
|
|
|
from datetime import datetime
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
# Check if LangChain is available
|
|
try:
|
|
from langchain_core.tools import BaseTool, StructuredTool
|
|
|
|
LANGCHAIN_AVAILABLE = True
|
|
except ImportError:
|
|
LANGCHAIN_AVAILABLE = False
|
|
|
|
# Skip all tests if LangChain not installed
|
|
pytestmark = pytest.mark.skipif(not LANGCHAIN_AVAILABLE, reason="LangChain not installed")
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_tool():
|
|
"""Create a mock LangChain tool."""
|
|
mock = MagicMock(spec=BaseTool)
|
|
mock.name = "test_tool"
|
|
mock.description = "A test tool"
|
|
mock.invoke = MagicMock(return_value="Tool result")
|
|
return mock
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_tool_with_large_output():
|
|
"""Create a mock tool that returns large output."""
|
|
mock = MagicMock(spec=BaseTool)
|
|
mock.name = "search_tool"
|
|
mock.description = "Search tool with large results"
|
|
# Return > 1000 chars to trigger compression
|
|
large_output = '{"items": [' + ",".join(f'{{"id": {i}}}' for i in range(200)) + "]}"
|
|
mock.invoke = MagicMock(return_value=large_output)
|
|
return mock
|
|
|
|
|
|
class TestToolCompressionMetrics:
|
|
"""Tests for ToolCompressionMetrics dataclass."""
|
|
|
|
def test_create_metrics(self):
|
|
"""Create metrics with all fields."""
|
|
from headroom.integrations.langchain.agents import ToolCompressionMetrics
|
|
|
|
metrics = ToolCompressionMetrics(
|
|
tool_name="search",
|
|
timestamp=datetime.now(),
|
|
chars_before=5000,
|
|
chars_after=2000,
|
|
chars_saved=3000,
|
|
compression_ratio=0.4,
|
|
was_compressed=True,
|
|
)
|
|
|
|
assert metrics.tool_name == "search"
|
|
assert metrics.chars_before == 5000
|
|
assert metrics.chars_after == 2000
|
|
assert metrics.chars_saved == 3000
|
|
assert metrics.compression_ratio == 0.4
|
|
assert metrics.was_compressed is True
|
|
|
|
def test_metrics_defaults(self):
|
|
"""Verify no default values (all required)."""
|
|
from headroom.integrations.langchain.agents import ToolCompressionMetrics
|
|
|
|
# All fields are required, should raise TypeError if missing
|
|
with pytest.raises(TypeError):
|
|
ToolCompressionMetrics() # type: ignore[call-arg]
|
|
|
|
|
|
class TestToolMetricsCollector:
|
|
"""Tests for ToolMetricsCollector."""
|
|
|
|
def test_init_empty(self):
|
|
"""Initialize with empty metrics list."""
|
|
from headroom.integrations.langchain.agents import ToolMetricsCollector
|
|
|
|
collector = ToolMetricsCollector()
|
|
|
|
assert collector.metrics == []
|
|
|
|
def test_add_metric(self):
|
|
"""Add a metric to the collector."""
|
|
from headroom.integrations.langchain.agents import (
|
|
ToolCompressionMetrics,
|
|
ToolMetricsCollector,
|
|
)
|
|
|
|
collector = ToolMetricsCollector()
|
|
metric = ToolCompressionMetrics(
|
|
tool_name="test",
|
|
timestamp=datetime.now(),
|
|
chars_before=100,
|
|
chars_after=80,
|
|
chars_saved=20,
|
|
compression_ratio=0.8,
|
|
was_compressed=True,
|
|
)
|
|
|
|
collector.add(metric)
|
|
|
|
assert len(collector.metrics) == 1
|
|
assert collector.metrics[0] is metric
|
|
|
|
def test_add_metric_limits_to_1000(self):
|
|
"""Metrics list is limited to 1000 entries."""
|
|
from headroom.integrations.langchain.agents import (
|
|
ToolCompressionMetrics,
|
|
ToolMetricsCollector,
|
|
)
|
|
|
|
collector = ToolMetricsCollector()
|
|
|
|
# Add 1100 metrics
|
|
for i in range(1100):
|
|
metric = ToolCompressionMetrics(
|
|
tool_name=f"tool_{i}",
|
|
timestamp=datetime.now(),
|
|
chars_before=100,
|
|
chars_after=80,
|
|
chars_saved=20,
|
|
compression_ratio=0.8,
|
|
was_compressed=True,
|
|
)
|
|
collector.add(metric)
|
|
|
|
assert len(collector.metrics) == 1000
|
|
# Should keep the last 1000 (most recent)
|
|
assert collector.metrics[0].tool_name == "tool_100"
|
|
assert collector.metrics[-1].tool_name == "tool_1099"
|
|
|
|
def test_get_summary_empty(self):
|
|
"""Get summary with no metrics."""
|
|
from headroom.integrations.langchain.agents import ToolMetricsCollector
|
|
|
|
collector = ToolMetricsCollector()
|
|
summary = collector.get_summary()
|
|
|
|
assert summary["total_invocations"] == 0
|
|
assert summary["total_compressions"] == 0
|
|
assert summary["total_chars_saved"] == 0
|
|
|
|
def test_get_summary_with_data(self):
|
|
"""Get summary with metrics."""
|
|
from headroom.integrations.langchain.agents import (
|
|
ToolCompressionMetrics,
|
|
ToolMetricsCollector,
|
|
)
|
|
|
|
collector = ToolMetricsCollector()
|
|
|
|
# Add compressed metric
|
|
collector.add(
|
|
ToolCompressionMetrics(
|
|
tool_name="search",
|
|
timestamp=datetime.now(),
|
|
chars_before=5000,
|
|
chars_after=2000,
|
|
chars_saved=3000,
|
|
compression_ratio=0.4,
|
|
was_compressed=True,
|
|
)
|
|
)
|
|
|
|
# Add uncompressed metric
|
|
collector.add(
|
|
ToolCompressionMetrics(
|
|
tool_name="simple",
|
|
timestamp=datetime.now(),
|
|
chars_before=100,
|
|
chars_after=100,
|
|
chars_saved=0,
|
|
compression_ratio=1.0,
|
|
was_compressed=False,
|
|
)
|
|
)
|
|
|
|
summary = collector.get_summary()
|
|
|
|
assert summary["total_invocations"] == 2
|
|
assert summary["total_compressions"] == 1
|
|
assert summary["total_chars_saved"] == 3000
|
|
assert summary["average_compression_ratio"] == 0.4 # Only compressed
|
|
|
|
def test_get_summary_by_tool(self):
|
|
"""Get per-tool statistics."""
|
|
from headroom.integrations.langchain.agents import (
|
|
ToolCompressionMetrics,
|
|
ToolMetricsCollector,
|
|
)
|
|
|
|
collector = ToolMetricsCollector()
|
|
|
|
# Add metrics for different tools
|
|
for _i in range(3):
|
|
collector.add(
|
|
ToolCompressionMetrics(
|
|
tool_name="search",
|
|
timestamp=datetime.now(),
|
|
chars_before=1000,
|
|
chars_after=500,
|
|
chars_saved=500,
|
|
compression_ratio=0.5,
|
|
was_compressed=True,
|
|
)
|
|
)
|
|
|
|
for _i in range(2):
|
|
collector.add(
|
|
ToolCompressionMetrics(
|
|
tool_name="database",
|
|
timestamp=datetime.now(),
|
|
chars_before=100,
|
|
chars_after=100,
|
|
chars_saved=0,
|
|
compression_ratio=1.0,
|
|
was_compressed=False,
|
|
)
|
|
)
|
|
|
|
summary = collector.get_summary()
|
|
|
|
assert "by_tool" in summary
|
|
assert summary["by_tool"]["search"]["invocations"] == 3
|
|
assert summary["by_tool"]["search"]["compressions"] == 3
|
|
assert summary["by_tool"]["search"]["chars_saved"] == 1500
|
|
assert summary["by_tool"]["database"]["invocations"] == 2
|
|
assert summary["by_tool"]["database"]["compressions"] == 0
|
|
|
|
|
|
class TestHeadroomToolWrapper:
|
|
"""Tests for HeadroomToolWrapper."""
|
|
|
|
def test_init_defaults(self, mock_tool):
|
|
"""Initialize with default settings."""
|
|
from headroom.integrations.langchain.agents import HeadroomToolWrapper
|
|
|
|
wrapper = HeadroomToolWrapper(mock_tool)
|
|
|
|
assert wrapper.tool is mock_tool
|
|
assert wrapper.name == "test_tool"
|
|
assert wrapper.description == "A test tool"
|
|
assert wrapper.min_chars_to_compress == 1000
|
|
|
|
def test_init_custom_threshold(self, mock_tool):
|
|
"""Initialize with custom compression threshold."""
|
|
from headroom.integrations.langchain.agents import (
|
|
HeadroomToolWrapper,
|
|
ToolMetricsCollector,
|
|
)
|
|
|
|
collector = ToolMetricsCollector()
|
|
wrapper = HeadroomToolWrapper(
|
|
mock_tool,
|
|
min_chars_to_compress=500,
|
|
metrics_collector=collector,
|
|
)
|
|
|
|
assert wrapper.min_chars_to_compress == 500
|
|
assert wrapper._metrics is collector
|
|
|
|
def test_call_small_output_no_compression(self, mock_tool):
|
|
"""Small outputs are not compressed."""
|
|
from headroom.integrations.langchain.agents import (
|
|
HeadroomToolWrapper,
|
|
ToolMetricsCollector,
|
|
)
|
|
|
|
collector = ToolMetricsCollector()
|
|
wrapper = HeadroomToolWrapper(
|
|
mock_tool,
|
|
min_chars_to_compress=1000,
|
|
metrics_collector=collector,
|
|
)
|
|
|
|
result = wrapper("input")
|
|
|
|
assert result == "Tool result"
|
|
assert len(collector.metrics) == 1
|
|
assert collector.metrics[0].was_compressed is False
|
|
|
|
def test_call_large_output_triggers_compression(self, mock_tool_with_large_output):
|
|
"""Large outputs trigger compression."""
|
|
from headroom.integrations.langchain.agents import (
|
|
HeadroomToolWrapper,
|
|
ToolMetricsCollector,
|
|
)
|
|
|
|
collector = ToolMetricsCollector()
|
|
wrapper = HeadroomToolWrapper(
|
|
mock_tool_with_large_output,
|
|
min_chars_to_compress=100,
|
|
metrics_collector=collector,
|
|
)
|
|
|
|
# Mock compress_tool_result to return compressed output
|
|
with patch("headroom.integrations.langchain.agents.compress_tool_result") as mock_compress:
|
|
mock_compress.return_value = '{"items": [...compressed...]}'
|
|
wrapper("query")
|
|
|
|
mock_compress.assert_called_once()
|
|
assert len(collector.metrics) == 1
|
|
assert collector.metrics[0].was_compressed is True
|
|
|
|
def test_call_converts_non_string_result(self, mock_tool):
|
|
"""Non-string results are converted to strings."""
|
|
from headroom.integrations.langchain.agents import HeadroomToolWrapper
|
|
|
|
mock_tool.invoke.return_value = {"key": "value"}
|
|
|
|
wrapper = HeadroomToolWrapper(mock_tool)
|
|
result = wrapper("input")
|
|
|
|
assert isinstance(result, str)
|
|
assert "key" in result
|
|
|
|
def test_invoke_alias(self, mock_tool):
|
|
"""invoke() is an alias for __call__()."""
|
|
from headroom.integrations.langchain.agents import HeadroomToolWrapper
|
|
|
|
wrapper = HeadroomToolWrapper(mock_tool)
|
|
|
|
result1 = wrapper("input")
|
|
mock_tool.invoke.reset_mock()
|
|
result2 = wrapper.invoke("input")
|
|
|
|
assert result1 == result2
|
|
|
|
def test_compression_failure_returns_original(self, mock_tool_with_large_output):
|
|
"""Compression failure returns original output."""
|
|
from headroom.integrations.langchain.agents import HeadroomToolWrapper
|
|
|
|
wrapper = HeadroomToolWrapper(
|
|
mock_tool_with_large_output,
|
|
min_chars_to_compress=100,
|
|
)
|
|
|
|
with patch("headroom.integrations.langchain.agents.compress_tool_result") as mock_compress:
|
|
mock_compress.side_effect = Exception("Compression error")
|
|
result = wrapper("query")
|
|
|
|
# Should return original output
|
|
assert "items" in result
|
|
assert "id" in result
|
|
|
|
def test_as_langchain_tool(self, mock_tool):
|
|
"""Convert wrapper to LangChain StructuredTool."""
|
|
from headroom.integrations.langchain.agents import HeadroomToolWrapper
|
|
|
|
wrapper = HeadroomToolWrapper(mock_tool)
|
|
lc_tool = wrapper.as_langchain_tool()
|
|
|
|
assert isinstance(lc_tool, StructuredTool)
|
|
assert lc_tool.name == "test_tool"
|
|
assert lc_tool.description == "A test tool"
|
|
|
|
def test_metrics_recorded_correctly(self, mock_tool_with_large_output):
|
|
"""Verify metrics are recorded correctly."""
|
|
from headroom.integrations.langchain.agents import (
|
|
HeadroomToolWrapper,
|
|
ToolMetricsCollector,
|
|
)
|
|
|
|
collector = ToolMetricsCollector()
|
|
wrapper = HeadroomToolWrapper(
|
|
mock_tool_with_large_output,
|
|
min_chars_to_compress=100,
|
|
metrics_collector=collector,
|
|
)
|
|
|
|
original_len = len(mock_tool_with_large_output.invoke.return_value)
|
|
|
|
with patch("headroom.integrations.langchain.agents.compress_tool_result") as mock_compress:
|
|
compressed_result = '{"items": [...]}'
|
|
mock_compress.return_value = compressed_result
|
|
wrapper("query")
|
|
|
|
metric = collector.metrics[0]
|
|
assert metric.tool_name == "search_tool"
|
|
assert metric.chars_before == original_len
|
|
assert metric.chars_after == len(compressed_result)
|
|
assert metric.chars_saved == original_len - len(compressed_result)
|
|
|
|
|
|
class TestWrapToolsWithHeadroom:
|
|
"""Tests for wrap_tools_with_headroom function."""
|
|
|
|
def test_wrap_single_tool(self, mock_tool):
|
|
"""Wrap a single tool."""
|
|
from headroom.integrations.langchain.agents import wrap_tools_with_headroom
|
|
|
|
wrapped = wrap_tools_with_headroom([mock_tool])
|
|
|
|
assert len(wrapped) == 1
|
|
assert isinstance(wrapped[0], StructuredTool)
|
|
assert wrapped[0].name == "test_tool"
|
|
|
|
def test_wrap_multiple_tools(self, mock_tool):
|
|
"""Wrap multiple tools."""
|
|
from headroom.integrations.langchain.agents import wrap_tools_with_headroom
|
|
|
|
tool2 = MagicMock(spec=BaseTool)
|
|
tool2.name = "tool_2"
|
|
tool2.description = "Second tool"
|
|
tool2.invoke = MagicMock(return_value="Result 2")
|
|
|
|
wrapped = wrap_tools_with_headroom([mock_tool, tool2])
|
|
|
|
assert len(wrapped) == 2
|
|
assert wrapped[0].name == "test_tool"
|
|
assert wrapped[1].name == "tool_2"
|
|
|
|
def test_wrap_with_custom_threshold(self, mock_tool):
|
|
"""Wrap with custom compression threshold."""
|
|
from headroom.integrations.langchain.agents import wrap_tools_with_headroom
|
|
|
|
wrapped = wrap_tools_with_headroom([mock_tool], min_chars_to_compress=500)
|
|
|
|
assert len(wrapped) == 1
|
|
# Invoke to verify wrapper is configured
|
|
# The wrapper should be invoked through the StructuredTool
|
|
assert wrapped[0].name == "test_tool"
|
|
|
|
def test_wrap_with_shared_collector(self, mock_tool):
|
|
"""Wrap with shared metrics collector."""
|
|
from headroom.integrations.langchain.agents import (
|
|
ToolMetricsCollector,
|
|
wrap_tools_with_headroom,
|
|
)
|
|
|
|
collector = ToolMetricsCollector()
|
|
|
|
tool2 = MagicMock(spec=BaseTool)
|
|
tool2.name = "tool_2"
|
|
tool2.description = "Second tool"
|
|
tool2.invoke = MagicMock(return_value="Result 2")
|
|
|
|
wrapped = wrap_tools_with_headroom(
|
|
[mock_tool, tool2],
|
|
metrics_collector=collector,
|
|
)
|
|
|
|
# Invoke both tools
|
|
wrapped[0].func("input1")
|
|
wrapped[1].func("input2")
|
|
|
|
# Both should use the same collector
|
|
assert len(collector.metrics) == 2
|
|
|
|
def test_wrap_empty_list(self):
|
|
"""Wrap empty list returns empty list."""
|
|
from headroom.integrations.langchain.agents import wrap_tools_with_headroom
|
|
|
|
wrapped = wrap_tools_with_headroom([])
|
|
|
|
assert wrapped == []
|
|
|
|
|
|
class TestGlobalMetrics:
|
|
"""Tests for global metrics functions."""
|
|
|
|
def test_get_tool_metrics(self):
|
|
"""get_tool_metrics returns the global collector."""
|
|
from headroom.integrations.langchain.agents import (
|
|
ToolMetricsCollector,
|
|
get_tool_metrics,
|
|
)
|
|
|
|
collector = get_tool_metrics()
|
|
|
|
assert isinstance(collector, ToolMetricsCollector)
|
|
|
|
def test_reset_tool_metrics(self):
|
|
"""reset_tool_metrics creates new collector."""
|
|
from headroom.integrations.langchain.agents import (
|
|
ToolCompressionMetrics,
|
|
get_tool_metrics,
|
|
reset_tool_metrics,
|
|
)
|
|
|
|
# Add a metric to the global collector
|
|
collector = get_tool_metrics()
|
|
collector.add(
|
|
ToolCompressionMetrics(
|
|
tool_name="test",
|
|
timestamp=datetime.now(),
|
|
chars_before=100,
|
|
chars_after=100,
|
|
chars_saved=0,
|
|
compression_ratio=1.0,
|
|
was_compressed=False,
|
|
)
|
|
)
|
|
|
|
# Reset
|
|
reset_tool_metrics()
|
|
|
|
# New collector should be empty
|
|
new_collector = get_tool_metrics()
|
|
assert len(new_collector.metrics) == 0
|
|
|
|
def test_wrapper_uses_global_metrics_by_default(self, mock_tool):
|
|
"""HeadroomToolWrapper uses global metrics by default."""
|
|
from headroom.integrations.langchain.agents import (
|
|
HeadroomToolWrapper,
|
|
get_tool_metrics,
|
|
reset_tool_metrics,
|
|
)
|
|
|
|
# Reset to start fresh
|
|
reset_tool_metrics()
|
|
|
|
wrapper = HeadroomToolWrapper(mock_tool)
|
|
wrapper("input")
|
|
|
|
global_collector = get_tool_metrics()
|
|
assert len(global_collector.metrics) == 1
|
|
|
|
|
|
class TestLangChainNotAvailable:
|
|
"""Tests for behavior when LangChain is not available."""
|
|
|
|
def test_check_raises_import_error(self):
|
|
"""_check_langchain_available raises ImportError when not available."""
|
|
from headroom.integrations.langchain.agents import _check_langchain_available
|
|
|
|
# When LangChain IS available, should not raise
|
|
try:
|
|
_check_langchain_available()
|
|
except ImportError:
|
|
pytest.fail("Should not raise when LangChain is available")
|