-
Notifications
You must be signed in to change notification settings - Fork 55
feat: Add batch heartbeat submission support (#106) #162
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -329,6 +329,70 @@ def heartbeat( | |||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| def batch_heartbeat( | ||||||||||||||||||||||||||||||||||||||||||
| self, | ||||||||||||||||||||||||||||||||||||||||||
| heartbeats: List[Dict[str, Any]], | ||||||||||||||||||||||||||||||||||||||||||
| default_token: Optional[str] = None, | ||||||||||||||||||||||||||||||||||||||||||
| ) -> Dict[str, Any]: | ||||||||||||||||||||||||||||||||||||||||||
| """Process a batched relay heartbeat. | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| Args: | ||||||||||||||||||||||||||||||||||||||||||
| heartbeats: List of dicts, each with 'agent_id', 'status', and optionally 'token', 'health'. | ||||||||||||||||||||||||||||||||||||||||||
| default_token: Token to use if a heartbeat doesn't provide one. | ||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||
| results = {} | ||||||||||||||||||||||||||||||||||||||||||
| agents = self._load_agents() | ||||||||||||||||||||||||||||||||||||||||||
| now = int(time.time()) | ||||||||||||||||||||||||||||||||||||||||||
| dirty = False | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| for hb in heartbeats: | ||||||||||||||||||||||||||||||||||||||||||
| agent_id = hb.get("agent_id") | ||||||||||||||||||||||||||||||||||||||||||
| token = hb.get("token") or default_token | ||||||||||||||||||||||||||||||||||||||||||
| status = hb.get("status", "alive") | ||||||||||||||||||||||||||||||||||||||||||
| health = hb.get("health") | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| if not agent_id: | ||||||||||||||||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| agent_data = agents.get(agent_id) | ||||||||||||||||||||||||||||||||||||||||||
| if not agent_data: | ||||||||||||||||||||||||||||||||||||||||||
| results[agent_id] = {"error": "Agent not registered", "code": "NOT_FOUND"} | ||||||||||||||||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| if agent_data.get("relay_token") != token: | ||||||||||||||||||||||||||||||||||||||||||
| results[agent_id] = {"error": "Invalid relay token", "code": "AUTH_FAILED"} | ||||||||||||||||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| if agent_data.get("token_expires", 0) < now: | ||||||||||||||||||||||||||||||||||||||||||
| results[agent_id] = {"error": "Token expired — re-register", "code": "TOKEN_EXPIRED"} | ||||||||||||||||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| # Update heartbeat state | ||||||||||||||||||||||||||||||||||||||||||
| agent_data["last_heartbeat"] = now | ||||||||||||||||||||||||||||||||||||||||||
| agent_data["beat_count"] = agent_data.get("beat_count", 0) + 1 | ||||||||||||||||||||||||||||||||||||||||||
| agent_data["status"] = status | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| # Refresh token TTL | ||||||||||||||||||||||||||||||||||||||||||
| agent_data["token_expires"] = now + RELAY_TOKEN_TTL_S | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| if health: | ||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||
| if health: | |
| if health is not None: |
Copilot
AI
Mar 21, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The per-agent success payload differs from heartbeat() in a way that’s likely to complicate clients: heartbeat() returns token_expires (absolute) and assessment, while batch_heartbeat() returns only expires_in. Consider returning token_expires (and optionally assessment/status) per agent for consistency with the single-heartbeat API.
| "ok": True, | |
| "expires_in": RELAY_TOKEN_TTL_S, | |
| "ok": True, | |
| "expires_in": RELAY_TOKEN_TTL_S, | |
| "token_expires": agent_data["token_expires"], |
Copilot
AI
Mar 21, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unlike heartbeat(), batch_heartbeat() doesn’t emit any relay log entries. This reduces auditability/diagnostics, especially when batch is expected to become the common path at scale. Consider logging a single aggregated event (e.g., action batch_heartbeat with count and maybe ok/error totals) to preserve observability without per-agent log spam.
| # Log a single aggregated event for observability without per-agent spam. | |
| ok_count = 0 | |
| error_counts: Dict[str, int] = {} | |
| for _agent_id, res in results.items(): | |
| if res.get("ok"): | |
| ok_count += 1 | |
| else: | |
| code = res.get("code", "UNKNOWN") | |
| error_counts[code] = error_counts.get(code, 0) + 1 | |
| log_entry = { | |
| "ts": now, | |
| "action": "batch_heartbeat", | |
| "total": len(heartbeats), | |
| "ok": ok_count, | |
| "errors": error_counts, | |
| } | |
| append_jsonl(_dir(RELAY_LOG_FILE), log_entry) |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,47 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import pytest | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import time | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from beacon_skill.relay import RelayManager | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from beacon_skill.transports.relay import RelayClient | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def test_relay_batch_heartbeat(tmp_path): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| mgr = RelayManager(data_dir=tmp_path) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| now = int(time.time()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Register a few agents directly into the manager | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| agents = { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "agent_1": { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "relay_token": "token1", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "token_expires": now + 86400, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "status": "dormant" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "agent_2": { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "relay_token": "token2", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "token_expires": now + 86400, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "status": "dormant" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| mgr._save_agents(agents) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| heartbeats = [ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {"agent_id": "agent_1", "status": "active", "token": "token1"}, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {"agent_id": "agent_2", "status": "idle", "token": "token2"}, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {"agent_id": "agent_3", "status": "busy", "token": "wrong"} # not registered | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # We patch RELAY_TOKEN_TTL_S to be accessible in globals if needed, or it's built-in | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+1
to
+32
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| result = mgr.batch_heartbeat(heartbeats) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert "batch_results" in result | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert "agent_1" in result["batch_results"] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert "agent_2" in result["batch_results"] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert "agent_3" in result["batch_results"] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert result["batch_results"]["agent_1"].get("ok") is True | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert result["batch_results"]["agent_2"].get("ok") is True | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert result["batch_results"]["agent_3"].get("code") == "NOT_FOUND" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| updated = mgr._load_agents() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert updated["agent_1"]["status"] == "active" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert updated["agent_2"]["status"] == "idle" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+3
to
+47
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from beacon_skill.relay import RelayManager | |
| from beacon_skill.transports.relay import RelayClient | |
| def test_relay_batch_heartbeat(tmp_path): | |
| mgr = RelayManager(data_dir=tmp_path) | |
| now = int(time.time()) | |
| # Register a few agents directly into the manager | |
| agents = { | |
| "agent_1": { | |
| "relay_token": "token1", | |
| "token_expires": now + 86400, | |
| "status": "dormant" | |
| }, | |
| "agent_2": { | |
| "relay_token": "token2", | |
| "token_expires": now + 86400, | |
| "status": "dormant" | |
| } | |
| } | |
| mgr._save_agents(agents) | |
| heartbeats = [ | |
| {"agent_id": "agent_1", "status": "active", "token": "token1"}, | |
| {"agent_id": "agent_2", "status": "idle", "token": "token2"}, | |
| {"agent_id": "agent_3", "status": "busy", "token": "wrong"} # not registered | |
| ] | |
| # We patch RELAY_TOKEN_TTL_S to be accessible in globals if needed, or it's built-in | |
| result = mgr.batch_heartbeat(heartbeats) | |
| assert "batch_results" in result | |
| assert "agent_1" in result["batch_results"] | |
| assert "agent_2" in result["batch_results"] | |
| assert "agent_3" in result["batch_results"] | |
| assert result["batch_results"]["agent_1"].get("ok") is True | |
| assert result["batch_results"]["agent_2"].get("ok") is True | |
| assert result["batch_results"]["agent_3"].get("code") == "NOT_FOUND" | |
| updated = mgr._load_agents() | |
| assert updated["agent_1"]["status"] == "active" | |
| assert updated["agent_2"]["status"] == "idle" | |
| from beacon_skill.relay import RelayManager, RELAY_TOKEN_TTL_S | |
| from beacon_skill.transports.relay import RelayClient | |
| def test_relay_batch_heartbeat(tmp_path): | |
| mgr = RelayManager(data_dir=tmp_path) | |
| now = int(time.time()) | |
| # Register a few agents directly into the manager with expired tokens | |
| expired_time = now - 10 | |
| agents = { | |
| "agent_1": { | |
| "relay_token": "token1", | |
| "token_expires": expired_time, | |
| "status": "dormant", | |
| "beat_count": 0, | |
| }, | |
| "agent_2": { | |
| "relay_token": "token2", | |
| "token_expires": expired_time, | |
| "status": "dormant", | |
| "beat_count": 0, | |
| }, | |
| # Registered agent with wrong token in heartbeat to cover AUTH_FAILED | |
| "agent_3": { | |
| "relay_token": "token3", | |
| "token_expires": expired_time, | |
| "status": "dormant", | |
| "beat_count": 0, | |
| }, | |
| } | |
| original_expires = { | |
| agent_id: data["token_expires"] for agent_id, data in agents.items() | |
| } | |
| original_beats = { | |
| agent_id: data["beat_count"] for agent_id, data in agents.items() | |
| } | |
| mgr._save_agents(agents) | |
| heartbeats = [ | |
| {"agent_id": "agent_1", "status": "active", "token": "token1"}, | |
| {"agent_id": "agent_2", "status": "idle", "token": "token2"}, | |
| # Registered but wrong token -> AUTH_FAILED | |
| {"agent_id": "agent_3", "status": "busy", "token": "wrong"}, | |
| # Not registered at all -> NOT_FOUND | |
| {"agent_id": "agent_4", "status": "busy", "token": "wrong"}, | |
| ] | |
| result = mgr.batch_heartbeat(heartbeats) | |
| assert "batch_results" in result | |
| assert "agent_1" in result["batch_results"] | |
| assert "agent_2" in result["batch_results"] | |
| assert "agent_3" in result["batch_results"] | |
| assert "agent_4" in result["batch_results"] | |
| # Successful heartbeats | |
| assert result["batch_results"]["agent_1"].get("ok") is True | |
| assert result["batch_results"]["agent_2"].get("ok") is True | |
| # Auth failure for registered agent with wrong token | |
| assert result["batch_results"]["agent_3"].get("code") == "AUTH_FAILED" | |
| # Not found for completely unknown agent | |
| assert result["batch_results"]["agent_4"].get("code") == "NOT_FOUND" | |
| updated = mgr._load_agents() | |
| # Status updates for successful agents | |
| assert updated["agent_1"]["status"] == "active" | |
| assert updated["agent_2"]["status"] == "idle" | |
| # TTL should be refreshed for successful agents | |
| for agent_id in ("agent_1", "agent_2"): | |
| assert updated[agent_id]["token_expires"] > original_expires[agent_id] | |
| assert updated[agent_id]["token_expires"] >= now | |
| # beat_count should increment on each successful heartbeat | |
| assert updated[agent_id]["beat_count"] == original_beats[agent_id] + 1 | |
| # For AUTH_FAILED, TTL and beat_count should not change | |
| assert updated["agent_3"]["token_expires"] == original_expires["agent_3"] | |
| assert updated["agent_3"]["beat_count"] == original_beats["agent_3"] | |
| assert updated["agent_3"]["status"] == agents["agent_3"]["status"] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In
batch_heartbeat, heartbeats missingagent_idare silently skipped (continue), so the caller gets no indication that an input item was invalid. Consider recording an explicit error result for invalid items (e.g., under a synthetic key like an index) or returning a top-levelerrorslist so clients can reconcile inputs to outputs.