Files
cleveragents-core/robot/helper_plan_sync.py
freemo 8e9aa7af48 fix(client): address server client chain review findings
- Promote _request() to public method request() in ServerHttpClient
  to fix encapsulation violation across sync_client and remote_project
- Fix WebSocket connect() to raise NotImplementedError with clear TODO
  documenting that real websockets transport is not yet implemented
- Fix thread safety: protect last_event_id write under self._lock
  in ws_client.process_event()
- Broaden exception handling in sync() to catch ServerTimeoutError
  and A2aNotAvailableError in addition to ServerConnectionError
- Add has_next to PageResult in benchmark and test helpers
- Add comments explaining Retry-After header logging-only behavior
  and why blocking time.sleep is acceptable in sync client
- Update all test steps, robot helpers, and benchmarks to use the
  renamed public request() method and updated connect() behavior

Refs: #335, #336, #337, #338
2026-03-24 20:28:19 +00:00

141 lines
4.0 KiB
Python

"""Helper script for plan_sync.robot integration tests."""
from __future__ import annotations
import sys
from pathlib import Path
from unittest.mock import MagicMock
import httpx
_SRC = str(Path(__file__).resolve().parents[1] / "src")
if _SRC not in sys.path:
sys.path.insert(0, _SRC)
from cleveragents.client.http_client import ServerHttpClient # noqa: E402
from cleveragents.client.sync_client import ( # noqa: E402
ConflictPolicy,
ExecutionResult,
PlanSyncClient,
SyncScope,
SyncSummary,
)
def _mock_response(
status: int = 200, body: dict | list | None = None
) -> httpx.Response:
return httpx.Response(
status_code=status,
json=body,
headers={},
request=httpx.Request("GET", "http://mock"),
)
def scope_active() -> None:
scope = SyncScope(actions=True, skills=False, tools=True, projects=False)
types = scope.active_types()
assert types == ["actions", "tools"], f"Got {types}"
print("plan-sync-scope-ok")
def sync_create() -> None:
mock_http = MagicMock(spec=ServerHttpClient)
mock_http.request.return_value = _mock_response(200, {"id": "srv-new"})
client = PlanSyncClient(mock_http)
items = [{"id": "a1"}, {"id": "a2"}]
summary = client.sync(items, "actions")
assert summary.created == 2
print("plan-sync-create-ok")
def sync_dry_run() -> None:
mock_http = MagicMock(spec=ServerHttpClient)
client = PlanSyncClient(mock_http)
items = [{"id": "a1"}]
summary = client.sync(items, "actions", dry_run=True)
assert summary.dry_run is True
assert summary.created == 1
print("plan-sync-dry-run-ok")
def execute_plan() -> None:
mock_http = MagicMock(spec=ServerHttpClient)
mock_http.request.return_value = _mock_response(
200, {"server_plan_id": "srv-001", "status": "submitted"}
)
client = PlanSyncClient(mock_http)
result = client.execute_plan("plan-001")
assert result.plan_id == "plan-001"
assert result.status == "submitted"
print("plan-sync-execute-ok")
def apply_plan() -> None:
mock_http = MagicMock(spec=ServerHttpClient)
mock_http.request.return_value = _mock_response(
200, {"server_plan_id": "srv-002", "status": "applying"}
)
client = PlanSyncClient(mock_http)
result = client.apply_plan("plan-002")
assert result.status == "applying"
print("plan-sync-apply-ok")
def plan_status() -> None:
mock_http = MagicMock(spec=ServerHttpClient)
mock_http.request.return_value = _mock_response(
200, {"phase": "running", "progress": 50}
)
client = PlanSyncClient(mock_http)
status = client.get_plan_status("srv-001")
assert status["phase"] == "running"
print("plan-sync-status-ok")
def conflict_policy() -> None:
mock_http = MagicMock(spec=ServerHttpClient)
client = PlanSyncClient(mock_http, conflict_policy=ConflictPolicy.LOCAL_WINS)
assert client.conflict_policy == ConflictPolicy.LOCAL_WINS
client.conflict_policy = ConflictPolicy.SERVER_WINS
assert client.conflict_policy == ConflictPolicy.SERVER_WINS
print("plan-sync-conflict-policy-ok")
def summary_total() -> None:
s = SyncSummary(created=2, updated=1, skipped=1, errors=1)
assert s.total_processed == 5
print("plan-sync-summary-total-ok")
def exec_result_attrs() -> None:
r = ExecutionResult(plan_id="p1", server_plan_id="sp1", status="done", message="ok")
assert r.message == "ok"
assert r.server_plan_id == "sp1"
print("plan-sync-exec-result-ok")
_COMMANDS = {
"scope-active": scope_active,
"sync-create": sync_create,
"sync-dry-run": sync_dry_run,
"execute-plan": execute_plan,
"apply-plan": apply_plan,
"plan-status": plan_status,
"conflict-policy": conflict_policy,
"summary-total": summary_total,
"exec-result-attrs": exec_result_attrs,
}
def main() -> None:
if len(sys.argv) < 2 or sys.argv[1] not in _COMMANDS:
print(f"Usage: {sys.argv[0]} <{'|'.join(_COMMANDS)}>", file=sys.stderr)
sys.exit(1)
_COMMANDS[sys.argv[1]]()
if __name__ == "__main__":
main()