from pathlib import Path import stat import uuid import httpx import pytest from flashops_agent.client import AgentUnauthorized, ControlPlaneClient from flashops_agent.state import AgentState, StateStore def test_control_plane_client_registers_and_sends_heartbeat(): requests = [] def handler(request: httpx.Request) -> httpx.Response: requests.append(request) if request.url.path == "/api/v1/agent/register": return httpx.Response( 201, json={ "agent_id": "agent_test", "host_id": "host_test", "token": "raw-token", "heartbeat_interval_s": 5, }, ) if request.url.path == "/api/v1/agent/agent_test/heartbeat": return httpx.Response( 200, json={ "ok": True, "server_time": "2026-07-27T00:00:00Z", "commands": [], }, ) return httpx.Response(404) with ControlPlaneClient( "http://control.test", enrollment_token="enroll-test", transport=httpx.MockTransport(handler), ) as client: registered = client.register( { "host_name": "pytest-host", "agent_version": "0.1.0", "os_family": "linux", } ) heartbeat = client.heartbeat( registered["agent_id"], registered["token"], {"healthy": True}, ) assert heartbeat["ok"] is True assert requests[0].headers["x-flashops-enrollment-token"] == "enroll-test" assert requests[1].headers["authorization"] == "Bearer raw-token" def test_control_plane_client_rejects_unauthorized_heartbeat(): def handler(_: httpx.Request) -> httpx.Response: return httpx.Response(401, json={"detail": "invalid"}) with ControlPlaneClient( "http://control.test", transport=httpx.MockTransport(handler), ) as client: with pytest.raises(AgentUnauthorized): client.heartbeat("agent_test", "bad-token", {"healthy": True}) def test_state_store_round_trip_uses_private_permissions(tmp_path: Path): path = tmp_path / "agent-state.json" store = StateStore(path) expected = AgentState( server="http://control.test", agent_id="agent_test", host_id="host_test", token="raw-token", heartbeat_interval_s=5.0, ) store.save(expected) assert store.load() == expected assert stat.S_IMODE(path.stat().st_mode) == 0o600 def test_control_plane_client_runs_step_lease_protocol(): requests = [] def handler(request: httpx.Request) -> httpx.Response: requests.append(request) path = request.url.path if path.endswith("/lease"): return httpx.Response( 200, json={"step_id": "step_test", "attempt": 1, "dry_run": True}, ) if path.endswith("/ack"): return httpx.Response(200, json={"step_id": "step_test", "state": "RUNNING"}) if path.endswith("/renew"): return httpx.Response(200, json={"step_id": "step_test", "state": "RUNNING"}) if path.endswith("/events"): return httpx.Response(200, json={"step_id": "step_test", "accepted": 1}) if path.endswith("/complete"): return httpx.Response(200, json={"step_id": "step_test", "state": "SUCCEEDED"}) return httpx.Response(404) token = "raw-token" keys = [str(uuid.uuid4()) for _ in range(4)] with ControlPlaneClient( "http://control.test", transport=httpx.MockTransport(handler) ) as client: lease = client.lease("agent_test", token) assert lease is not None client.ack("agent_test", token, "step_test", 1, keys[0]) client.renew("agent_test", token, "step_test", 1, keys[1]) client.report_events( "agent_test", token, "step_test", 1, [{"message": "hello"}], keys[2], ) client.complete( "agent_test", token, "step_test", 1, status="SUCCEEDED", exit_code=0, result={"simulated": True}, idempotency_key=keys[3], ) assert len(requests) == 5 assert requests[0].headers["authorization"] == "Bearer raw-token" assert requests[1].headers["idempotency-key"] == keys[0]