feat(context): implement ContextStrategy protocol and plugin registration system

Implement strategy registry integration as spec §47561 by adding:

- load_strategies_from_config() in context_strategies.py for TOML-driven
  strategy bootstrapping (register builtins, discover custom plugins via
  module:ClassName, set enabled list)
- Re-exports of StrategyRegistry, StrategyConfig, StrategyRegistryEntry,
  StrategyNotFoundError, StrategyRegistrationError and ContextStrategy from
  strategy_registry and acms_service modules
- __all__ export list and DEFAULT_ENABLED_STRATEGIES constant

Fix StrategyRegistry.validate_registry() to skip resource_types check for
v1 pipeline strategies (context_strategies.py, acms_service.py) whose
StrategyCapabilities dataclass lacks the domain-model resource_types field,
preventing false-positive warnings.  Adds _is_v1_pipeline_caps() helper.

Auto-load strategy configuration from Settings in ACMSPipeline.__init__()
when a Settings object is provided, reading context.strategies config and
registering/ enabling strategies via the plugin loader.
This commit is contained in:
OpenCode AI
2026-05-14 08:05:57 +00:00
committed by Forgejo
parent fc7fed644d
commit cb6986c8f9
5 changed files with 969 additions and 2 deletions
@@ -821,6 +821,14 @@ class ACMSPipeline:
# can wire configuration (e.g. default budget, strategy allow-lists)
# and persistence without changing the constructor signature.
self._settings = settings
# Auto-load strategy configuration from Settings if provided.
# Per spec §42947-42980: when a Settings object is passed to the
# pipeline, its TOML-derived ``context`` configuration is used to
# populate and enable the StrategyRegistry. This eliminates the need
# for callers to manually register every strategy.
if settings is not None:
self._load_strategies_from_settings(settings)
self._unit_of_work = unit_of_work
self._strategies: dict[str, ContextStrategy] = {
@@ -874,6 +882,57 @@ class ACMSPipeline:
self._plugin_manager = plugin_manager
self._discover_context_extensions()
def _load_strategies_from_settings(self, settings: Settings) -> None:
"""Auto-load strategy configuration from a :class:`~cleveragents.config.settings.Settings` object.
Attempts to read ``context.strategies`` configuration from the TOML-based
Settings (typically loaded from a project config file via pydantic-settings).
If present, built-in strategies are registered as per default, custom plugins
from ``module:ClassName`` strings are discovered, and the enabled list is set.
Any errors are logged but do not prevent the pipeline from operating with
its core built-in strategies.
"""
try:
context_cfg = getattr(settings, "context", None)
if context_cfg is None:
return
# Extract the strategies sub-section as a dict.
strategy_cfg: dict[str, Any] = {}
if isinstance(context_cfg, dict):
strat_dict = context_cfg.get("strategies", {}) or {}
if isinstance(strat_dict, dict):
strategy_cfg = dict(strat_dict)
else:
return
# Import the lazy-load helper; avoid circular import at module level.
from cleveragents.application.services.context_strategies import (
load_strategies_from_config,
)
registry = load_strategies_from_config(strategy_cfg)
for name in registry.list_all():
if name not in self._strategies:
strat_inst = registry.get(name)
self.register_strategy(name=name, strategy=strat_inst)
# Sync the enabled list.
enabled = registry.list_enabled()
if enabled:
self._enforce_enabled_strategies(enabled)
except Exception as exc: # noqa: BLE001
self._logger.warning(
"strategy_config_load_failed",
error=str(exc),
)
def _enforce_enabled_strategies(self, enabled_names: list[str]) -> None:
"""Internal helper to filter ``_strategies`` to the enabled subset."""
enabled_set = set(enabled_names)
self._strategies = {
name: strat for name, strat in self._strategies.items() if name in enabled_set
}
# ------------------------------------------------------------------
# Plugin extension point discovery
# ------------------------------------------------------------------
@@ -20,7 +20,12 @@ All strategies implement the v1 ``ContextStrategy`` Protocol defined in
``acms_service.py`` and can be registered with ``ACMSPipeline`` via
``register_strategy()`` or added to ``BUILTIN_STRATEGIES``.
Based on ``docs/specification.md`` 2520-2521.
**Registry integration** (spec §47561): This module is the canonical home for
:py:class:`~cleveragents.application.services.strategy_registry.StrategyRegistry`.
The strategies here are registered by default when a registry is created, and
custom strategies can be loaded via :func:`load_strategies_from_config`.
Based on ``docs/specification.md`` §25207-25216.
"""
from __future__ import annotations
@@ -39,6 +44,41 @@ from cleveragents.domain.models.core.context_fragment import (
ContextFragment,
)
# ---------------------------------------------------------------------------
# Re-exports for registry integration (spec §47561)
# ---------------------------------------------------------------------------
from cleveragents.application.services.strategy_registry import (
StrategyConfig, # noqa: F401
StrategyNotFoundError, # noqa: F401
StrategyRegistrationError, # noqa: F401
StrategyRegistry, # noqa: F401
StrategyRegistryEntry, # noqa: F401
)
from cleveragents.domain.models.acms.strategy import (
ContextStrategy, # noqa: F401
StrategyCapabilities as _DomainStrategyCapabilities, # noqa: F401
)
__all__: list[str] = [
"SimpleKeywordStrategy",
"SemanticEmbeddingStrategy",
"BreadthDepthNavigatorStrategy",
"StrategyRegistry",
"StrategyConfig",
"StrategyRegistryEntry",
"StrategyNotFoundError",
"StrategyRegistrationError",
]
# Built-in strategy registry constants (used by load_strategies_from_config)
DEFAULT_ENABLED_STRATEGIES: tuple[str, ...] = (
"simple-keyword",
"semantic-embedding",
"breadth-depth-navigator",
)
logger = logging.getLogger(__name__)
_WORD_RE = re.compile(r"\w+", re.UNICODE)
@@ -532,3 +572,59 @@ def _max_proximity(node_uri: str, focus_nodes: list[str], max_hops: int) -> floa
best = max(best, proximity)
return best
# ---------------------------------------------------------------------------
# Helper: load_strategies_from_config (spec §42947-42980)
# ---------------------------------------------------------------------------
def load_strategies_from_config(config: dict[str, Any]) -> StrategyRegistry:
"""Populate and return a :class:`StrategyRegistry` from a TOML config dict.
Accepts a dict representing the parsed ``[context.strategies]`` section of
a TOML configuration file. The expected structure is::
{
"enabled": ["simple-keyword", "semantic-embedding"],
"custom": {
"my-strategy": "my_package.strategies:MyStrategy",
},
}
:param config: Dict with an optional ``"enabled"`` key and an optional
``"custom"`` dict mapping strategy names to ``"module:ClassName"``
strings.
:returns: A fully-populated :class:`StrategyRegistry` ready for use by
an ``ACMSPipeline`` or downstream component.
The built-in strategies from :const:`DEFAULT_ENABLED_STRATEGIES` are
always registered first (as built-ins). Custom strategies are discovered
and loaded via :meth:`StrategyRegistry.register_from_module`. Finally,
the enabled list is set so that only configured strategies are active.
"""
registry = StrategyRegistry()
# Register all built-in strategies from DEFAULT_ENABLED_STRATEGIES
for strategy_class_name in (
"SimpleKeywordStrategy",
"SemanticEmbeddingStrategy",
"BreadthDepthNavigatorStrategy",
):
cls = globals()[strategy_class_name]
instance = cls()
registry.register(
instance,
config=StrategyConfig(),
module_path="cleveragents.application.services.context_strategies",
is_builtin=True,
)
# Register custom strategies from module:ClassName strings
for name, module_path in (config.get("custom") or {}).items():
registry.register_from_module(name=name, module_path=module_path)
# Set the enabled strategy list
enabled = config.get("enabled", DEFAULT_ENABLED_STRATEGIES)
registry.set_enabled(list(enabled))
return registry
@@ -23,6 +23,9 @@ from typing import Any
import structlog
from cleveragents.application.services.acms_service import (
StrategyCapabilities as _V1StrategyCapabilities,
)
from cleveragents.domain.models.acms.strategy import (
ContextStrategy,
StrategyConfig,
@@ -32,6 +35,24 @@ from cleveragents.domain.models.acms.strategy import (
logger = structlog.get_logger(__name__)
def _is_v1_pipeline_caps(caps: object) -> bool:
"""Return True if *caps* is a v1-pipeline StrategyCapabilities dataclass.
V1 pipeline strategies (those defined in ``context_strategies.py`` and
``acms_service.py``) use a dataclass-style capabilities object that has
attributes like :attr:`~StrategyCapabilities.supports_semantic_search`,
rather than the domain-model ``StrategyCapabilities`` (from
``strategy.py``) which has :attr:`~StrategyCapabilities.uses_text`,
:attr:`~StrategyCapabilities.resource_types`, etc.
We detect v1-capabilities by checking whether the object is an instance
of the acms_service dataclass. This allows :meth:`validate_registry` to
skip the resource_types check for legacy strategies that do not declare
domain-model backend fields.
"""
return isinstance(caps, _V1StrategyCapabilities)
# ---------------------------------------------------------------------------
# Exceptions
# ---------------------------------------------------------------------------
@@ -489,7 +510,12 @@ class StrategyRegistry:
f"Strategy '{name}' declares no backend capabilities"
)
if not caps.resource_types:
# Skip the resource_types check for v1 pipeline strategies.
# Those use a dataclass-style StrategyCapabilities that lacks
# the domain-model ``resource_types`` field entirely, so all
# warnings about undeclared resource types would be false
# positives. See _is_v1_pipeline_caps().
if not _is_v1_pipeline_caps(caps) and not caps.resource_types:
warnings.append(
f"Strategy '{name}' does not declare supported "
f"resource types (capabilities.resource_types is empty)"
View File
+786
View File
@@ -0,0 +1,786 @@
"""Tests for the ContextStrategyRegistry and plugin loading system.
Covers:
- Strategy registration and retrieval
- Protocol conformance validation
- Plugin loading from ``module:ClassName`` strings
- TOML configuration loading via :func:`load_strategies_from_config`
- Thread safety of registry operations
- Fallback degradation chain verification
- Config-driven strategy enable/disable
"""
from __future__ import annotations
import threading
import time
from collections.abc import Sequence
from typing import Any
import pytest
# ---------------------------------------------------------------------------
# Test fixtures and helpers
# ---------------------------------------------------------------------------
class FakeStrategy:
"""Minimal strategy that satisfies the v1 ``ContextStrategy`` protocol."""
def __init__(self, name: str = "fake") -> None:
self._name = name
@property
def name(self) -> str: # type: ignore[override]
return self._name
@property
def capabilities(self):
from cleveragents.application.services.acms_service import (
StrategyCapabilities as _Caps,
)
return _Caps(supports_semantic_search=False)
def can_handle(self, request: dict[str, Any]) -> float: # type: ignore[override]
return 0.5
def assemble(
self,
fragments: Sequence[Any],
budget: Any,
) -> Sequence[Any]:
return list(fragments)
def explain(self) -> str:
return "Fake strategy for testing."
class FakeDomainStrategy:
"""Strategy that implements the domain-model ``ContextStrategy`` Protocol."""
def __init__(self, name: str = "domain-fake") -> None:
self._name = name
@property
def name(self) -> str: # type: ignore[override]
return self._name
@property
def capabilities(self):
from cleveragents.domain.models.acms.strategy import StrategyCapabilities
return StrategyCapabilities(uses_text=True, resource_types=("text",))
def can_handle(self, request: dict[str, Any], backends: Any) -> float: # type: ignore[override]
return 0.8
def assemble(
self, request: Any, backends: Any, budget: int, plan_context: Any
) -> list: # type: ignore[override]
return []
def explain(self) -> str:
return "Fake domain strategy for testing."
class FakeStrategyWithNoCaps:
"""Strategy with empty capabilities (used to trigger validate_registry warnings)."""
@property
def name(self) -> str:
return "noop-cap-fake"
@property
def capabilities(self):
from cleveragents.domain.models.acms.strategy import StrategyCapabilities
return StrategyCapabilities(uses_text=False, resource_types=()) # empty caps
def can_handle(self, request: dict[str, Any], backends: Any) -> float:
return 0.1
def assemble(self, request: Any, backends: Any, budget: int, plan_context: Any) -> list:
return []
def explain(self) -> str:
return "Strategy with empty capabilities"
# ---------------------------------------------------------------------------
# Tests: Strategy registration and retrieval
# ---------------------------------------------------------------------------
class TestRegistrationAndRetrieval:
"""Validate basic register/get/list operations."""
def test_register_and_get(self):
from cleveragents.application.services.strategy_registry import (
StrategyRegistry,
)
registry = StrategyRegistry()
strategy = FakeStrategy("my-strategy")
registry.register(strategy)
assert registry.get("my-strategy") is strategy
assert "my-strategy" in registry
def test_register_with_custom_name(self):
from cleveragents.application.services.strategy_registry import (
StrategyRegistry,
)
registry = StrategyRegistry()
strategy = FakeStrategy("original-name")
registry.register(strategy, name="override-name")
assert "override-name" in registry
assert "original-name" not in registry
def test_register_with_config(self):
from cleveragents.application.services.strategy_registry import (
StrategyConfig,
StrategyRegistry,
)
registry = StrategyRegistry()
strategy = FakeStrategy("config-strategy")
cfg = StrategyConfig(enabled=False)
registry.register(strategy, config=cfg)
# With enabled=False the strategy should be registered but not in _enabled_order
entry = registry.get_entry("config-strategy")
assert entry.config.enabled is False
def test_register_builtin_flag(self):
from cleveragents.application.services.strategy_registry import (
StrategyRegistry,
)
registry = StrategyRegistry()
strategy = FakeStrategy("built-in-1")
registry.register(strategy, is_builtin=True)
assert "built-in-1" in registry.list_builtin()
entry = registry.get_entry("built-in-1")
assert entry.is_builtin is True
def test_duplicate_registration_raises(self):
from cleveragents.application.services.strategy_registry import (
StrategyRegistrationError,
StrategyRegistry,
)
registry = StrategyRegistry()
s = FakeStrategy("dup")
registry.register(s)
with pytest.raises(StrategyRegistrationError):
registry.register(FakeStrategy("dup"))
def test_empty_name_raises(self):
from cleveragents.application.services.strategy_registry import (
StrategyRegistrationError,
StrategyRegistry,
)
registry = StrategyRegistry()
class Nameless:
@property
def name(self) -> str: # type: ignore[override]
return ""
@property
def capabilities(self):
from cleveragents.application.services.acms_service import (
StrategyCapabilities as _Caps,
)
return _Caps()
def can_handle(self, request: dict[str, Any]) -> float: # type: ignore[override]
return 0.3
def assemble(
self, fragments: Sequence[Any], budget: Any
) -> Sequence[Any]:
return []
def explain(self) -> str:
return ""
with pytest.raises(StrategyRegistrationError, match="empty"):
registry.register(Nameless())
def test_protocol_violation_raises(self):
from cleveragents.application.services.strategy_registry import (
StrategyRegistrationError,
StrategyRegistry,
)
registry = StrategyRegistry()
class NotStrategy: # No name/abilities etc. at all
pass
with pytest.raises(StrategyRegistrationError):
registry.register(NotStrategy()) # type: ignore[arg-type]
def test_list_all(self):
from cleveragents.application.services.strategy_registry import (
StrategyRegistry,
)
registry = StrategyRegistry()
for i in range(5):
entry_name = f"strat-{i}"
registry.register(FakeStrategy(entry_name))
all_names = registry.list_all()
assert sorted(all_names) == ["strat-0", "strat-1", "strat-2", "strat-3", "strat-4"]
def test_list_enabled_empty(self):
from cleveragents.application.services.strategy_registry import (
StrategyRegistry,
)
registry = StrategyRegistry()
assert registry.list_enabled() == []
def test_get_entry_and_config(self):
from cleveragents.application.services.strategy_registry import (
StrategyConfig,
StrategyNotFoundError,
StrategyRegistry,
)
registry = StrategyRegistry()
cfg = StrategyConfig(timeout_seconds=60)
registry.register(FakeStrategy("cfg-strat"), config=cfg)
entry = registry.get_entry("cfg-strat")
assert entry.name == "cfg-strat"
assert entry.config.timeout_seconds == 60
loaded_cfg = registry.get_config("cfg-strat")
assert loaded_cfg.timeout_seconds == 60
def test_get_raises_on_missing(self):
from cleveragents.application.services.strategy_registry import (
StrategyNotFoundError,
StrategyRegistry,
)
registry = StrategyRegistry()
with pytest.raises(StrategyNotFoundError, match="not found"):
registry.get("nonexistent")
def test_len_and_contains(self):
from cleveragents.application.services.strategy_registry import (
StrategyRegistry,
)
registry = StrategyRegistry()
assert len(registry) == 0
assert "empty" not in registry
registry.register(FakeStrategy("one"))
assert len(registry) == 1
assert "one" in registry
def test_unregister(self):
from cleveragents.application.services.strategy_registry import (
StrategyNotFoundError,
StrategyRegistry,
)
registry = StrategyRegistry()
registry.register(FakeStrategy("to-unregister"))
assert "to-unregister" in registry
registry.unregister("to-unregister")
assert "to-unregister" not in registry
def test_unregister_unknown_raises(self):
from cleveragents.application.services.strategy_registry import (
StrategyNotFoundError,
StrategyRegistry,
)
registry = StrategyRegistry()
with pytest.raises(StrategyNotFoundError):
registry.unregister("ghost")
def test_clear(self):
from cleveragents.application.services.strategy_registry import (
StrategyRegistry,
)
registry = StrategyRegistry()
for i in range(3):
registry.register(FakeStrategy(f"clear-{i}"))
assert len(registry) == 3
registry.clear()
assert len(registry) == 0
assert registry.list_all() == []
# ---------------------------------------------------------------------------
# Tests: Protocol conformance validation
# ---------------------------------------------------------------------------
class TestProtocolValidation:
"""Validate :meth:`StrategyRegistry.validate_registry` behavior."""
def test_validate_returns_no_warnings_empty(self):
from cleveragents.application.services.strategy_registry import (
StrategyRegistry,
)
registry = StrategyRegistry()
warnings = registry.validate_registry()
assert warnings == []
def test_validate_enabled_but_not_registered_warns(self):
from cleveragents.application.services.strategy_registry import (
StrategyRegistry,
)
registry = StrategyRegistry()
registry.inject_stale_enabled_entry("ghost-strategy")
warnings = registry.validate_registry()
assert any("ghost-strategy" in w for w in warnings)
def test_validate_domain_strategy_no_resource_types_warns(self):
"""Domain-model strategies must declare resource_types."""
from cleveragents.application.services.strategy_registry import (
StrategyRegistry,
)
registry = StrategyRegistry()
strat = FakeStrategyWithNoCaps() # has domain caps with empty resource_types
registry.register(strat)
warnings = registry.validate_registry()
assert any("resource types" in w for w in warnings)
def test_validate_domain_strategy_with_resource_types_ok(self):
"""Domain strategies that declare resource_types pass validation."""
from cleveragents.application.services.strategy_registry import (
StrategyRegistry,
)
registry = StrategyRegistry()
strat = FakeDomainStrategy("with-caps") # has resource_types=("text",)
registry.register(strat)
warnings = registry.validate_registry()
assert not any("resource types" in w for w in warnings)
def test_validate_v1_pipeline_strategies_skip_resource_types(self):
"""V1 pipeline strategies (with acms_service StrategyCapabilities) should NOT
get resource-types warnings because their capabilities object has no
such field. This was the fix introduced by #10590."""
from cleveragents.application.services.strategy_registry import (
StrategyRegistry,
)
registry = StrategyRegistry()
# SimpleKeywordStrategy uses v1 pipeline StrategyCapabilities (acms_service)
# which lacks the domain-model resource_types field.
from cleveragents.application.services.context_strategies import (
SimpleKeywordStrategy,
)
registry.register(SimpleKeywordStrategy(), is_builtin=True)
warnings = registry.validate_registry()
# Should NOT have a "resource types" warning for v1 pipeline strategies
resource_warnings = [w for w in warnings if "resource types" in w]
assert len(resource_warnings) == 0
# ---------------------------------------------------------------------------
# Tests: Plugin loading from module:ClassName strings
# ---------------------------------------------------------------------------
class TestPluginLoading:
"""Test :meth:`StrategyRegistry.register_from_module`."""
def test_register_from_module_success(self):
from cleveragents.application.services.context_strategies import (
StrategyConfig,
StrategyRegistry,
)
registry = StrategyRegistry()
registry.register_from_module(
name="simple-keyword",
module_path="cleveragents.application.services.context_strategies:SimpleKeywordStrategy",
config=StrategyConfig(enabled=True),
)
assert "simple-keyword" in registry
strategy = registry.get("simple-keyword")
assert strategy.name == "simple-keyword"
def test_register_from_module_invalid_format_raises(self):
from cleveragents.application.services.strategy_registry import (
StrategyRegistrationError,
StrategyRegistry,
)
registry = StrategyRegistry()
with pytest.raises(StrategyRegistrationError, match="module:ClassName"):
registry.register_from_module(name="bad", module_path="invalid_format") # type: ignore[arg-type]
def test_register_from_module_unauthorized_prefix_raises(self):
from cleveragents.application.services.strategy_registry import (
StrategyRegistrationError,
StrategyRegistry,
)
registry = StrategyRegistry()
with pytest.raises(StrategyRegistrationError):
registry.register_from_module(
name="evil",
module_path="os.path:join", # type: ignore[arg-type]
)
def test_register_from_module_nonexistent_class_raises(self):
from cleveragents.application.services.strategy_registry import (
StrategyRegistrationError,
StrategyRegistry,
)
registry = StrategyRegistry()
with pytest.raises(StrategyRegistrationError, match="not found"):
registry.register_from_module(
name="bogus",
module_path="cleveragents.application.services.context_strategies:"
"NoSuchClassDoesNotExist", # type: ignore[arg-type]
)
# ---------------------------------------------------------------------------
# Tests: TOML configuration loading
# ---------------------------------------------------------------------------
class TestTOMLConfigurationLoading:
"""Test :func:`load_strategies_from_config`.
This exercises the entire pipeline: register built-ins, optionally load
custom plugins from module:ClassName strings, and set the enabled list.
"""
def test_load_empty_config_registers_defaults(self):
from cleveragents.application.services.context_strategies import (
load_strategies_from_config,
)
registry = load_strategies_from_config({}) # {} = minimal config
names = registry.list_all()
assert "simple-keyword" in names
assert "semantic-embedding" in names
assert "breadth-depth-navigator" in names
# Only defaults should be enabled
enabled = registry.list_enabled()
assert set(enabled) == {
"simple-keyword",
"semantic-embedding",
"breadth-depth-navigator",
}
def test_load_config_with_custom_plugin(self):
from cleveragents.application.services.context_strategies import (
load_strategies_from_config,
)
registry = load_strategies_from_config(
{
"custom": {
"my-strategy": "cleveragents.application.services.context_strategies:"
"SemanticEmbeddingStrategy",
},
}
)
assert "my-strategy" in registry
assert "simple-keyword" in registry
def test_load_config_with_custom_and_custom_enabled(self):
from cleveragents.application.services.context_strategies import (
load_strategies_from_config,
)
registry = load_strategies_from_config(
{
"enabled": ["simple-keyword", "my-strategy"],
"custom": {
"my-strategy": "cleveragents.application.services.context_strategies:"
"SemanticEmbeddingStrategy", # type: ignore[dict-item]
},
}
)
enabled = registry.list_enabled()
assert set(enabled) == {"simple-keyword", "my-strategy"}
def test_load_config_all_builtins_registered_as_builtin(self):
from cleveragents.application.services.context_strategies import (
load_strategies_from_config,
)
registry = load_strategies_from_config({})
for name in ["simple-keyword", "semantic-embedding", "breadth-depth-navigator"]:
entry = registry.get_entry(name)
assert entry.is_builtin is True
# ---------------------------------------------------------------------------
# Tests: Thread safety of registry operations
# ---------------------------------------------------------------------------
class TestThreadSafety:
"""Verify that concurrent register/get/set_enabled calls don't corrupt state."""
def test_concurrent_register(self):
from cleveragents.application.services.strategy_registry import (
StrategyRegistry,
)
registry = StrategyRegistry()
errors: list[Exception] = [] # type: ignore[var-annotated]
def register_worker(worker_id: int) -> None:
try:
for _ in range(50):
name = f"concurrent-strat-{worker_id}"
registry.register(FakeStrategy(name))
except Exception as exc: # noqa: BLE001
errors.append(exc)
threads = [threading.Thread(target=register_worker, args=(i,)) for i in range(10)]
for t in threads:
t.start()
for t in threads:
t.join()
assert not errors, f"Errors during concurrent register: {errors}"
# Each worker registers once (deduplicated within its own run)
total = len(registry)
assert total > 0
def test_concurrent_get_and_register(self):
from cleveragents.application.services.strategy_registry import (
StrategyRegistry,
)
registry = StrategyRegistry()
registry.register(FakeStrategy("shared")) # Pre-register one strategy
errors: list[Exception] = [] # type: ignore[var-annotated]
reads: list[str] = [] # type: ignore[var-annotated]
def reader(count: int) -> None:
for _ in range(20):
try:
s = registry.get("shared")
assert s is not None
reads.append(s.name)
except Exception as exc: # noqa: BLE001
errors.append(exc)
def writer(worker_id: int) -> None:
for _ in range(20):
name = f"writer-{worker_id}"
registry.register(FakeStrategy(name))
threads: list[threading.Thread] = []
threads.append(threading.Thread(target=reader, args=(1,)))
for i in range(5):
threads.append(threading.Thread(target=writer, args=(i,)))
for t in threads:
t.start()
for t in threads:
t.join()
assert not errors
assert len(reads) == 20
# ---------------------------------------------------------------------------
# Tests: Fallback degradation chain verification
# ---------------------------------------------------------------------------
class TestFallbackDegradation:
"""Verify that SimpleKeywordStrategy is always available as a fallback."""
def test_simple_keyword_always_produces_results(self):
from cleveragents.application.services.context_strategies import (
SimpleKeywordStrategy,
)
from cleveragents.domain.models.core.context_fragment import ContextBudget
strat = SimpleKeywordStrategy()
budget = ContextBudget(max_tokens=1000)
# Even with empty fragments, should return an empty list (no crash)
result = strat.assemble([], budget)
assert result == []
# Create a fake fragment-like object
class FakeFrag: # pylint: disable=C0115
relevance_score: float = 0.5
token_count: int = 10
uko_node: str = "project://test/node"
content: str = "hello world test fragment"
fragments = [FakeFrag()] # type: ignore[list-item]
result = strat.assemble(fragments, budget)
assert len(result) == 1
def test_simple_keyword_confidence_is_universal(self):
from cleveragents.application.services.context_strategies import (
SimpleKeywordStrategy,
)
strat = SimpleKeywordStrategy()
# Without query in request
assert strat.can_handle({}) == 0.3
# Request is always handled with at least 0.3 confidence
assert strat.can_handle({"query": ""}) == 0.3
assert strat.can_handle({"anything": "irrelevant"}) == 0.3
# ---------------------------------------------------------------------------
# Tests: Config-driven strategy enable/disable
# ---------------------------------------------------------------------------
class TestEnableDisable:
"""Test :meth:`StrategyRegistry.set_enabled` and config updates."""
def test_set_enabled_updates_list(self):
from cleveragents.application.services.strategy_registry import (
StrategyConfig,
StrategyRegistry,
)
registry = StrategyRegistry()
for name in ["a", "b", "c"]:
registry.register(FakeStrategy(name), config=StrategyConfig(enabled=True))
# Only enable 'a' and 'c'
registry.set_enabled(["a", "c"])
enabled = registry.list_enabled()
assert enabled == ["a", "c"]
def test_set_unknown_strategy_raises(self):
from cleveragents.application.services.strategy_registry import (
StrategyNotFoundError,
StrategyRegistry,
)
registry = StrategyRegistry()
registry.register(FakeStrategy("exists"))
with pytest.raises(StrategyNotFoundError, match="unknown"):
registry.set_enabled(["nonexistent"])
def test_set_enabled_updates_config_flags(self):
from cleveragents.application.services.strategy_registry import (
StrategyConfig,
StrategyRegistry,
)
registry = StrategyRegistry()
for name in ["x", "y"]:
registry.register(FakeStrategy(name))
# Initially enabled by default (enable_by_default=True)
assert registry.get_config("x").enabled is True
registry.set_enabled(["x"]) # disable y only
# Check config entries: x stays enabled, y should be disabled
assert registry.get_config("x").enabled is True
assert registry.get_config("y").enabled is False
def test_update_config_individual_fields(self):
from cleveragents.application.services.strategy_registry import (
StrategyConfig,
StrategyRegistry,
)
registry = StrategyRegistry()
registry.register(FakeStrategy("updatable"))
registry.update_config(
"updatable",
enabled=False,
timeout_seconds=120,
max_fragments=50,
)
cfg = registry.get_config("updatable")
assert cfg.enabled is False
assert cfg.timeout_seconds == 120
assert cfg.max_fragments == 50
def test_update_config_enable_re_adds_to_enabled_order(self):
from cleveragents.application.services.strategy_registry import (
StrategyRegistry,
)
registry = StrategyRegistry()
registry.register(FakeStrategy("re-enable"))
registry.update_config("re-enable", enabled=True)
assert "re-enable" in registry._enabled_order # pylint: disable=protected-access
def test_update_config_disable_removes_from_enabled_order(self):
from cleveragents.application.services.strategy_registry import (
StrategyRegistry,
)
registry = StrategyRegistry()
registry.register(FakeStrategy("disable-me"))
assert "disable-me" in registry._enabled_order # pylint: disable=protected-access
registry.update_config("disable-me", enabled=False)
assert "disable-me" not in registry._enabled_order # pylint: disable=protected-access
# ---------------------------------------------------------------------------
# Tests: Integration with acms_service auto-loading
# ---------------------------------------------------------------------------
class TestACMSPipelineAutoLoading:
"""Verify that ACMSPipeline.__init__ loads strategies from Settings."""
def test_pipeline_auto_loads_strategies_from_settings(self):
from unittest.mock import MagicMock, PropertyMock
# Create a mock Settings object with context.strategies config.
settings = MagicMock()
settings.context = { # type: ignore[attr-defined]
"strategies": {
"enabled": ["simple-keyword", "semantic-embedding"],
}
}
from cleveragents.application.services.acms_service import ACMSPipeline
pipeline = ACMSPipeline(settings=settings)
strategies_in_pipeline = list(pipeline._strategies.keys()) # pylint: disable=protected-access
# Built-in relevance strategy is always registered
assert "relevance" in strategies_in_pipeline
# Simple-keyword should be auto-loaded
assert "simple-keyword" in strategies_in_pipeline
def test_pipeline_works_without_settings(self):
from cleveragents.application.services.acms_service import ACMSPipeline
pipeline = ACMSPipeline()
assert "relevance" in pipeline._strategies # pylint: disable=protected-access