diff --git a/plugin/src/claude_smart/reflexio_adapter.py b/plugin/src/claude_smart/reflexio_adapter.py index c1f6836..1378836 100644 --- a/plugin/src/claude_smart/reflexio_adapter.py +++ b/plugin/src/claude_smart/reflexio_adapter.py @@ -6,7 +6,6 @@ from __future__ import annotations -import inspect import logging import os from collections.abc import Sequence @@ -95,18 +94,11 @@ def _get_client(self) -> Any | None: _LOGGER.debug("reflexio not importable: %s", exc) return None try: - try: - self._client = ReflexioClient( - url_endpoint=self.url, - api_key=self.api_key, - timeout=_HTTP_TIMEOUT_SECONDS, - ) - except TypeError: - # Pre-0.2.x reflexio releases didn't accept ``timeout``; - # fall back so hooks still work against older pinned clients. - self._client = ReflexioClient( - url_endpoint=self.url, api_key=self.api_key - ) + self._client = ReflexioClient( + url_endpoint=self.url, + api_key=self.api_key, + timeout=_HTTP_TIMEOUT_SECONDS, + ) except Exception as exc: # noqa: BLE001 — adapter must never raise. _LOGGER.warning("Failed to construct ReflexioClient: %s", exc) return None @@ -136,7 +128,6 @@ def publish( try: interaction_list = list(interactions) raw_request = getattr(client, "_make_request", None) - supports_source = _supports_keyword(client.publish_interaction, "source") if request_id is not None and callable(raw_request): payload: dict[str, Any] = { "request_id": request_id, @@ -148,9 +139,8 @@ def publish( "force_extraction": force_extraction, "evaluation_only": False, "override_learning_stall": override_learning_stall, + "source": _SOURCE, } - if supports_source: - payload["source"] = _SOURCE try: raw_request( "POST", @@ -194,15 +184,9 @@ def publish( "wait_for_response": False, "force_extraction": force_extraction, "skip_aggregation": skip_aggregation, + "source": _SOURCE, + "override_learning_stall": override_learning_stall, } - if supports_source: - kwargs["source"] = _SOURCE - if _supports_keyword(client.publish_interaction, "override_learning_stall"): - kwargs["override_learning_stall"] = override_learning_stall - elif override_learning_stall: - _LOGGER.debug( - "publish_interaction client does not support override_learning_stall" - ) response = client.publish_interaction(**kwargs) response_request_id = getattr(response, "request_id", None) if isinstance(response_request_id, str) and response_request_id: @@ -352,18 +336,11 @@ def fetch_user_playbooks(self, *, project_id: str, top_k: int = 10) -> list[Any] if client is None: return [] try: - if hasattr(client, "get_user_playbooks"): - response = client.get_user_playbooks( - user_id=project_id, - status_filter=[None], # None => CURRENT in reflexio's filter API - limit=top_k, - ) - else: - response = client.search_user_playbooks( - user_id=project_id, - status_filter=[None], - top_k=top_k, - ) + response = client.get_user_playbooks( + user_id=project_id, + status_filter=[None], # None => CURRENT in reflexio's filter API + limit=top_k, + ) except Exception as exc: # noqa: BLE001 self._record_read_error("fetch_user_playbooks", exc) return [] @@ -387,18 +364,11 @@ def fetch_agent_playbooks(self, top_k: int = 10) -> list[Any]: if client is None: return [] try: - if hasattr(client, "get_agent_playbooks"): - response = client.get_agent_playbooks( - agent_version=runtime.agent_version(), - status_filter=[None], - limit=top_k, - ) - else: - response = client.search_agent_playbooks( - agent_version=runtime.agent_version(), - status_filter=[None], - top_k=top_k, - ) + response = client.get_agent_playbooks( + agent_version=runtime.agent_version(), + status_filter=[None], + limit=top_k, + ) except Exception as exc: # noqa: BLE001 self._record_read_error("fetch_agent_playbooks", exc) return [] @@ -535,17 +505,6 @@ def _extract_items(response: Any, field: str) -> list[Any]: return list(value) if value else [] -def _supports_keyword(callable_obj: Any, keyword: str) -> bool: - try: - signature = inspect.signature(callable_obj) - except (TypeError, ValueError): - return False - for parameter in signature.parameters.values(): - if parameter.kind == inspect.Parameter.VAR_KEYWORD: - return True - return keyword in signature.parameters - - def _needs_raw_retrieved_learning_publish( interactions: Sequence[dict[str, Any]], ) -> bool: diff --git a/tests/test_adapter.py b/tests/test_adapter.py index 1bfeeeb..f76f9a6 100644 --- a/tests/test_adapter.py +++ b/tests/test_adapter.py @@ -39,10 +39,10 @@ def _make_request(self, method, path, **kwargs): if not self._publish_ok: raise RuntimeError("reflexio unreachable") - def search_user_playbooks(self, **_kw): + def get_user_playbooks(self, **_kw): return self._user_playbook_resp - def search_agent_playbooks(self, **_kw): + def get_agent_playbooks(self, **_kw): return self._agent_playbook_resp def get_profiles(self, **_kw): @@ -69,7 +69,7 @@ def test_adapter_constructs_client_with_env_url_and_api_key( seen: dict[str, str] = {} class FakeReflexioClient: - def __init__(self, *, url_endpoint: str, api_key: str): + def __init__(self, *, url_endpoint: str, api_key: str, timeout: int): seen["url_endpoint"] = url_endpoint seen["api_key"] = api_key @@ -113,31 +113,6 @@ def __init__(self, *, url_endpoint: str, api_key: str, timeout: int): assert 0 < reflexio_adapter._HTTP_TIMEOUT_SECONDS <= 30 -def test_adapter_falls_back_when_client_rejects_timeout_kwarg( - monkeypatch, tmp_path -) -> None: - """Older reflexio releases lack ``timeout``; constructor must still succeed.""" - monkeypatch.setattr( - reflexio_adapter.env_config, "CLAUDE_SMART_ENV_PATH", tmp_path / "missing.env" - ) - monkeypatch.setenv("REFLEXIO_URL", "https://x.example/") - monkeypatch.setenv("REFLEXIO_API_KEY", "k") - call_count = {"n": 0} - - class FakeReflexioClient: - def __init__(self, *, url_endpoint: str, api_key: str): - call_count["n"] += 1 - - monkeypatch.setitem( - sys.modules, - "reflexio", - SimpleNamespace(ReflexioClient=FakeReflexioClient), - ) - - assert reflexio_adapter.Adapter()._get_client() is not None - assert call_count["n"] == 1 - - def test_adapter_process_env_overrides_reflexio_env(monkeypatch, tmp_path) -> None: env_path = tmp_path / ".claude-smart" / ".env" env_path.parent.mkdir() @@ -150,7 +125,7 @@ def test_adapter_process_env_overrides_reflexio_env(monkeypatch, tmp_path) -> No seen: dict[str, str] = {} class FakeReflexioClient: - def __init__(self, *, url_endpoint: str, api_key: str): + def __init__(self, *, url_endpoint: str, api_key: str, timeout: int): seen["url_endpoint"] = url_endpoint seen["api_key"] = api_key @@ -179,7 +154,7 @@ def test_adapter_default_url_follows_backend_port(monkeypatch, tmp_path) -> None seen: dict[str, str] = {} class FakeReflexioClient: - def __init__(self, *, url_endpoint: str, api_key: str): + def __init__(self, *, url_endpoint: str, api_key: str, timeout: int): seen["url_endpoint"] = url_endpoint seen["api_key"] = api_key @@ -204,7 +179,7 @@ def test_adapter_local_default_file_url_follows_backend_port(monkeypatch, tmp_pa seen: dict[str, str] = {} class FakeReflexioClient: - def __init__(self, *, url_endpoint: str, api_key: str): + def __init__(self, *, url_endpoint: str, api_key: str, timeout: int): seen["url_endpoint"] = url_endpoint seen["api_key"] = api_key @@ -240,50 +215,6 @@ def test_publish_returns_true_on_success() -> None: assert client.published_kwargs["override_learning_stall"] is True -def test_publish_omits_override_learning_stall_when_client_lacks_keyword() -> None: - class OldPublishClient: - def __init__(self) -> None: - self.published_kwargs: dict[str, Any] = {} - - def publish_interaction( - self, - *, - user_id, - interactions, - agent_version, - session_id, - wait_for_response, - force_extraction, - skip_aggregation, - ): - self.published_kwargs = { - "user_id": user_id, - "interactions": interactions, - "agent_version": agent_version, - "session_id": session_id, - "wait_for_response": wait_for_response, - "force_extraction": force_extraction, - "skip_aggregation": skip_aggregation, - } - - client = OldPublishClient() - a = _adapter_with(client) - - ok = a.publish( - session_id="s1", - project_id="p1", - interactions=[{"role": "User", "content": "hi"}], - force_extraction=True, - override_learning_stall=True, - ) - - assert ok.ok is True - assert client.published_kwargs["user_id"] == "p1" - assert client.published_kwargs["force_extraction"] is True - assert "override_learning_stall" not in client.published_kwargs - assert "source" not in client.published_kwargs - - def test_link_publish_uses_stable_raw_request() -> None: client = _FakeClient() adapter = _adapter_with(client) @@ -370,39 +301,6 @@ def publish(request_id: str) -> None: } -def test_raw_publish_omits_source_for_older_client_contract() -> None: - class OldRawClient: - def __init__(self) -> None: - self.raw_payload = None - - def publish_interaction( - self, - *, - user_id, - interactions, - agent_version, - session_id, - wait_for_response, - force_extraction, - skip_aggregation, - ): - raise AssertionError("stable request IDs use the raw path") - - def _make_request(self, _method, _path, **kwargs): - self.raw_payload = kwargs["json"] - - client = OldRawClient() - result = _adapter_with(client).publish( - session_id="s1", - project_id="p1", - request_id="request-1", - interactions=[{"role": "User", "content": "hi"}], - ) - - assert result.request_id == "request-1" - assert "source" not in client.raw_payload - - def test_public_publish_records_server_generated_request_id() -> None: class PublicOnlyClient: def publish_interaction(self, **_kwargs): @@ -641,11 +539,11 @@ def search(self, **kwargs): self.search_call_count += 1 return self._unified_resp - def search_user_playbooks(self, **kwargs): + def get_user_playbooks(self, **kwargs): self.user_playbook_kwargs = kwargs return self._user_playbook_resp - def search_agent_playbooks(self, **kwargs): + def get_agent_playbooks(self, **kwargs): self.agent_playbook_kwargs = kwargs return self._agent_playbook_resp @@ -765,11 +663,11 @@ def test_fetch_all_runs_legs_in_parallel() -> None: """Serial would be ~0.6s across three legs; parallel should stay near 0.2s.""" class SlowClient: - def search_user_playbooks(self, **_kw): + def get_user_playbooks(self, **_kw): time.sleep(0.2) return SimpleNamespace(user_playbooks=[]) - def search_agent_playbooks(self, **_kw): + def get_agent_playbooks(self, **_kw): time.sleep(0.2) return SimpleNamespace(agent_playbooks=[]) @@ -786,10 +684,10 @@ def get_profiles(self, **_kw): def test_fetch_all_absorbs_per_leg_failure() -> None: class HalfBroken: - def search_user_playbooks(self, **_kw): + def get_user_playbooks(self, **_kw): raise RuntimeError("user playbook search down") - def search_agent_playbooks(self, **_kw): + def get_agent_playbooks(self, **_kw): return SimpleNamespace(agent_playbooks=[{"content": "a"}]) def get_profiles(self, **_kw): @@ -856,7 +754,7 @@ def test_fetch_user_playbooks_passes_project_id_and_default_top_k() -> None: a = _adapter_with(client) a.fetch_user_playbooks(project_id="proj") assert client.user_playbook_kwargs["user_id"] == "proj" - assert client.user_playbook_kwargs["top_k"] == 10 + assert client.user_playbook_kwargs["limit"] == 10 assert client.user_playbook_kwargs["status_filter"] == [None] @@ -867,7 +765,7 @@ def test_fetch_agent_playbooks_is_global() -> None: assert "user_id" not in client.agent_playbook_kwargs assert client.agent_playbook_kwargs["agent_version"] == "claude-code" assert client.agent_playbook_kwargs["status_filter"] == [None] - assert client.agent_playbook_kwargs["top_k"] == 10 + assert client.agent_playbook_kwargs["limit"] == 10 def test_search_all_filters_rejected_locally() -> None: @@ -896,7 +794,7 @@ def test_fetch_agent_playbooks_filters_rejected_locally() -> None: """Same defensive filter on the no-query /show path.""" class StubClient: - def search_agent_playbooks(self, **_kw): + def get_agent_playbooks(self, **_kw): return SimpleNamespace( agent_playbooks=[ {"content": "approved", "playbook_status": "approved"},