fix(acms): defer settings strategy auto-load past _strategies/_logger init

The previous ACMSPipeline.__init__ invoked _load_strategies_from_settings
at line 831 before self._strategies (line 834) and self._logger (line 864)
were initialized.  When a Settings whose context dict contained a
'strategies' key was passed in, the method body raised AttributeError on
'self._strategies', and the except handler then raised a second
AttributeError on 'self._logger' that propagated out of __init__ and
crashed pipeline construction entirely.

Move the auto-load call to after both attributes are bound.  Convert the
pytest-shaped tests/strategies/test_strategy_registry.py (which the
behave-based unit_tests gate never ran) into a behave feature file +
step definitions under features/, matching the project's BDD convention
and bringing the strategy auto-loading paths under actual CI coverage.

ISSUES CLOSED: #7527
This commit is contained in:
2026-06-18 00:56:39 -04:00
committed by Forgejo
parent 35ecfda7d4
commit ffa0a3b3bf
5 changed files with 300 additions and 809 deletions
@@ -0,0 +1,80 @@
@phase2 @acms @strategy
Feature: ACMS Pipeline Auto-Loading Strategies from Settings
As a CleverAgents developer
I want the ACMS pipeline to auto-load strategies from a Settings
object's context.strategies configuration
So that downstream code does not have to register every strategy by hand,
and so that a Settings carrying strategy config cannot crash __init__
with an AttributeError on _strategies / _logger.
# ---------------------------------------------------------------------------
# Initialization order regression (init-order AttributeError fix)
# ---------------------------------------------------------------------------
@auto_load @init_order
Scenario: Pipeline constructs cleanly when Settings carries an empty strategies sub-dict
Given a mock Settings whose context dict contains an empty strategies sub-dict
When I construct an ACMSPipeline with that Settings
Then construction should succeed without raising
And the resulting pipeline should expose its registered strategies
@auto_load @init_order
Scenario: Pipeline registers strategies declared in the Settings registry
Given a mock Settings whose context.strategies enables two built-ins
When I construct an ACMSPipeline with that Settings
Then construction should succeed without raising
And the pipeline strategies should include "simple-keyword"
@auto_load
Scenario: Pipeline tolerates a Settings without any context attribute
Given a mock Settings without any context attribute
When I construct an ACMSPipeline with that Settings
Then construction should succeed without raising
@auto_load
Scenario: Pipeline tolerates a Settings whose context is a non-dict object
Given a mock Settings whose context is a non-dict object
When I construct an ACMSPipeline with that Settings
Then construction should succeed without raising
@auto_load @error_handling
Scenario: Pipeline survives a malformed strategies config without crashing
Given a mock Settings whose context.strategies references an invalid custom strategy module
When I construct an ACMSPipeline with that Settings
Then construction should succeed without raising
# ---------------------------------------------------------------------------
# _enforce_enabled_strategies — filter pipeline strategy set to enabled subset
# ---------------------------------------------------------------------------
@auto_load @enforce
Scenario: Enabled list filters the pipeline strategy set
Given a mock Settings whose context.strategies enables only "simple-keyword"
When I construct an ACMSPipeline with that Settings
Then the pipeline strategies should contain "simple-keyword"
# ---------------------------------------------------------------------------
# load_strategies_from_config helper (context_strategies.py)
# ---------------------------------------------------------------------------
@load_helper
Scenario: load_strategies_from_config registers the three default built-ins
When I call load_strategies_from_config with an empty config dict
Then the resulting registry should contain "simple-keyword"
And the resulting registry should contain "semantic-embedding"
And the resulting registry should contain "breadth-depth-navigator"
@load_helper
Scenario: load_strategies_from_config honors an explicit enabled list
When I call load_strategies_from_config with an enabled list of only "simple-keyword"
Then the resulting registry should list only "simple-keyword" as enabled
@load_helper
Scenario: load_strategies_from_config defaults to DEFAULT_ENABLED_STRATEGIES when enabled is missing
When I call load_strategies_from_config with no enabled key
Then the resulting registry should list the default-enabled strategies
@load_helper @custom
Scenario: load_strategies_from_config registers a custom strategy from a module:ClassName string
When I call load_strategies_from_config with a custom strategy mapping "my-keyword" to SimpleKeywordStrategy
Then the resulting registry should contain "my-keyword"
@@ -0,0 +1,210 @@
"""Behave step definitions for ACMS pipeline strategy auto-loading.
Covers:
- The init-order regression where ``ACMSPipeline.__init__`` invoked
``_load_strategies_from_settings`` before ``self._strategies`` /
``self._logger`` existed, crashing construction with ``AttributeError``.
- The ``_load_strategies_from_settings`` body (early-return paths, dict
handling, registry merge, enabled-list enforcement, exception logging).
- The ``_enforce_enabled_strategies`` filter.
- The ``load_strategies_from_config`` helper (default built-in
registration, custom ``module:ClassName`` discovery, enabled-list set).
"""
from __future__ import annotations
from types import SimpleNamespace
from typing import Any
from behave import given, then, when
from behave.runner import Context
# ---------------------------------------------------------------------------
# Given — Settings shapes
# ---------------------------------------------------------------------------
@given("a mock Settings whose context dict contains an empty strategies sub-dict")
def step_settings_empty_strategies(context: Context) -> None:
context.settings = SimpleNamespace(context={"strategies": {}})
@given("a mock Settings whose context.strategies enables two built-ins")
def step_settings_enables_two(context: Context) -> None:
context.settings = SimpleNamespace(
context={
"strategies": {
"enabled": ["simple-keyword", "semantic-embedding"],
}
}
)
@given('a mock Settings whose context.strategies enables only "simple-keyword"')
def step_settings_enables_one(context: Context) -> None:
context.settings = SimpleNamespace(
context={
"strategies": {
"enabled": ["simple-keyword"],
}
}
)
@given("a mock Settings without any context attribute")
def step_settings_without_context(context: Context) -> None:
context.settings = SimpleNamespace()
@given("a mock Settings whose context is a non-dict object")
def step_settings_context_non_dict(context: Context) -> None:
context.settings = SimpleNamespace(context=SimpleNamespace(strategies={}))
@given(
"a mock Settings whose context.strategies references an invalid custom strategy module"
)
def step_settings_invalid_custom(context: Context) -> None:
context.settings = SimpleNamespace(
context={
"strategies": {
"custom": {
"broken": "nonexistent_module_for_bdd:NoSuchClass",
},
}
}
)
# ---------------------------------------------------------------------------
# When — pipeline construction and helper invocations
# ---------------------------------------------------------------------------
@when("I construct an ACMSPipeline with that Settings")
def step_construct_pipeline(context: Context) -> None:
from cleveragents.application.services.acms_service import ACMSPipeline
try:
context.pipeline = ACMSPipeline(settings=context.settings)
context.construct_error: Exception | None = None
except Exception as exc: # pragma: no cover - exercised via assertion below
context.pipeline = None
context.construct_error = exc
@when("I call load_strategies_from_config with an empty config dict")
def step_load_empty(context: Context) -> None:
from cleveragents.application.services.context_strategies import (
load_strategies_from_config,
)
context.registry = load_strategies_from_config({})
@when(
'I call load_strategies_from_config with an enabled list of only "simple-keyword"'
)
def step_load_enabled_one(context: Context) -> None:
from cleveragents.application.services.context_strategies import (
load_strategies_from_config,
)
context.registry = load_strategies_from_config({"enabled": ["simple-keyword"]})
@when("I call load_strategies_from_config with no enabled key")
def step_load_no_enabled(context: Context) -> None:
from cleveragents.application.services.context_strategies import (
load_strategies_from_config,
)
context.registry = load_strategies_from_config({"custom": {}})
@when(
'I call load_strategies_from_config with a custom strategy mapping "my-keyword" to SimpleKeywordStrategy'
)
def step_load_custom(context: Context) -> None:
from cleveragents.application.services.context_strategies import (
load_strategies_from_config,
)
config: dict[str, Any] = {
"custom": {
"my-keyword": (
"cleveragents.application.services.context_strategies"
":SimpleKeywordStrategy"
),
},
}
context.registry = load_strategies_from_config(config)
# ---------------------------------------------------------------------------
# Then — assertions
# ---------------------------------------------------------------------------
@then("construction should succeed without raising")
def step_check_construct_ok(context: Context) -> None:
err = getattr(context, "construct_error", None)
assert err is None, f"Expected clean construction, got: {err!r}"
assert context.pipeline is not None
@then("the resulting pipeline should expose its registered strategies")
def step_check_pipeline_has_strategies(context: Context) -> None:
assert context.pipeline is not None
strategies = list(context.pipeline._strategies.keys())
assert strategies, "Pipeline should have registered at least one strategy"
@then('the pipeline strategies should include "simple-keyword"')
def step_pipeline_includes_simple_keyword(context: Context) -> None:
assert context.pipeline is not None
assert "simple-keyword" in context.pipeline._strategies
@then('the pipeline strategies should contain "simple-keyword"')
def step_pipeline_contains_simple_keyword(context: Context) -> None:
assert context.pipeline is not None
assert "simple-keyword" in context.pipeline._strategies
@then('the resulting registry should contain "simple-keyword"')
def step_registry_has_sk(context: Context) -> None:
assert "simple-keyword" in context.registry.list_all()
@then('the resulting registry should contain "semantic-embedding"')
def step_registry_has_se(context: Context) -> None:
assert "semantic-embedding" in context.registry.list_all()
@then('the resulting registry should contain "breadth-depth-navigator"')
def step_registry_has_bdn(context: Context) -> None:
assert "breadth-depth-navigator" in context.registry.list_all()
@then('the resulting registry should contain "my-keyword"')
def step_registry_has_custom(context: Context) -> None:
assert "my-keyword" in context.registry.list_all()
@then('the resulting registry should list only "simple-keyword" as enabled')
def step_registry_enabled_only_sk(context: Context) -> None:
enabled = list(context.registry.list_enabled())
assert enabled == ["simple-keyword"], f"Expected ['simple-keyword'], got {enabled}"
@then("the resulting registry should list the default-enabled strategies")
def step_registry_enabled_default(context: Context) -> None:
from cleveragents.application.services.context_strategies import (
DEFAULT_ENABLED_STRATEGIES,
)
enabled = set(context.registry.list_enabled())
assert enabled == set(DEFAULT_ENABLED_STRATEGIES), (
f"Expected {set(DEFAULT_ENABLED_STRATEGIES)}, got {enabled}"
)
@@ -821,14 +821,6 @@ 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] = {
@@ -864,6 +856,16 @@ class ACMSPipeline:
self._logger = logger.bind(service="acms_pipeline")
self._enforcement_result_local = local()
# 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. Must run AFTER
# ``self._strategies`` and ``self._logger`` are initialized — the
# method body reads both.
if settings is not None:
self._load_strategies_from_settings(settings)
# All 10 pipeline components (default to pass-through stubs)
self._strategy_selector = strategy_selector or DefaultStrategySelector()
self._budget_allocator = budget_allocator or DefaultBudgetAllocator()
View File
-801
View File
@@ -1,801 +0,0 @@
"""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
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,
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 (
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:
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:
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 (
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 (
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
# 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