Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 18 additions & 59 deletions plugin/src/claude_smart/reflexio_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

from __future__ import annotations

import inspect
import logging
import os
from collections.abc import Sequence
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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",
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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 []
Expand All @@ -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 []
Expand Down Expand Up @@ -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:
Expand Down
132 changes: 15 additions & 117 deletions tests/test_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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

Expand Down Expand Up @@ -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()
Expand All @@ -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

Expand Down Expand Up @@ -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

Expand All @@ -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

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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=[])

Expand All @@ -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):
Expand Down Expand Up @@ -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]


Expand All @@ -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:
Expand Down Expand Up @@ -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"},
Expand Down
Loading