mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-25 00:51:20 +00:00
fix: notify gateway users when updates finish or fail
This commit is contained in:
parent
cf3dceafe1
commit
3126c60885
2 changed files with 201 additions and 20 deletions
|
|
@ -133,6 +133,7 @@ class TestHandleUpdateCommand:
|
|||
assert data["platform"] == "telegram"
|
||||
assert data["chat_id"] == "99999"
|
||||
assert "timestamp" in data
|
||||
assert not (hermes_home / ".update_exit_code").exists()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_spawns_systemd_run(self, tmp_path):
|
||||
|
|
@ -160,6 +161,7 @@ class TestHandleUpdateCommand:
|
|||
call_args = mock_popen.call_args[0][0]
|
||||
assert call_args[0] == "/usr/bin/systemd-run"
|
||||
assert "--scope" in call_args
|
||||
assert ".update_exit_code" in call_args[-1]
|
||||
assert "Starting Hermes update" in result
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
@ -196,6 +198,7 @@ class TestHandleUpdateCommand:
|
|||
call_args = mock_popen.call_args[0][0]
|
||||
assert call_args[0] == "bash"
|
||||
assert "nohup" in call_args[2]
|
||||
assert ".update_exit_code" in call_args[2]
|
||||
assert "Starting Hermes update" in result
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
@ -222,6 +225,7 @@ class TestHandleUpdateCommand:
|
|||
assert "Failed to start update" in result
|
||||
# Pending file should be cleaned up
|
||||
assert not (hermes_home / ".update_pending.json").exists()
|
||||
assert not (hermes_home / ".update_exit_code").exists()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_returns_user_friendly_message(self, tmp_path):
|
||||
|
|
@ -266,6 +270,53 @@ class TestSendUpdateNotification:
|
|||
# Should not raise
|
||||
await runner._send_update_notification()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_defers_notification_while_update_still_running(self, tmp_path):
|
||||
"""Returns False and keeps marker files when the update has not exited yet."""
|
||||
runner = _make_runner()
|
||||
hermes_home = tmp_path / "hermes"
|
||||
hermes_home.mkdir()
|
||||
|
||||
pending_path = hermes_home / ".update_pending.json"
|
||||
pending_path.write_text(json.dumps({
|
||||
"platform": "telegram", "chat_id": "67890", "user_id": "12345",
|
||||
}))
|
||||
(hermes_home / ".update_output.txt").write_text("still running")
|
||||
|
||||
mock_adapter = AsyncMock()
|
||||
runner.adapters = {Platform.TELEGRAM: mock_adapter}
|
||||
|
||||
with patch("gateway.run._hermes_home", hermes_home):
|
||||
result = await runner._send_update_notification()
|
||||
|
||||
assert result is False
|
||||
mock_adapter.send.assert_not_called()
|
||||
assert pending_path.exists()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_recovers_from_claimed_pending_file(self, tmp_path):
|
||||
"""A claimed pending file from a crashed notifier is still deliverable."""
|
||||
runner = _make_runner()
|
||||
hermes_home = tmp_path / "hermes"
|
||||
hermes_home.mkdir()
|
||||
|
||||
claimed_path = hermes_home / ".update_pending.claimed.json"
|
||||
claimed_path.write_text(json.dumps({
|
||||
"platform": "telegram", "chat_id": "67890", "user_id": "12345",
|
||||
}))
|
||||
(hermes_home / ".update_output.txt").write_text("done")
|
||||
(hermes_home / ".update_exit_code").write_text("0")
|
||||
|
||||
mock_adapter = AsyncMock()
|
||||
runner.adapters = {Platform.TELEGRAM: mock_adapter}
|
||||
|
||||
with patch("gateway.run._hermes_home", hermes_home):
|
||||
result = await runner._send_update_notification()
|
||||
|
||||
assert result is True
|
||||
mock_adapter.send.assert_called_once()
|
||||
assert not claimed_path.exists()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sends_notification_with_output(self, tmp_path):
|
||||
"""Sends update output to the correct platform and chat."""
|
||||
|
|
@ -284,6 +335,7 @@ class TestSendUpdateNotification:
|
|||
(hermes_home / ".update_output.txt").write_text(
|
||||
"→ Found 3 new commit(s)\n✓ Code updated!\n✓ Update complete!"
|
||||
)
|
||||
(hermes_home / ".update_exit_code").write_text("0")
|
||||
|
||||
# Mock the adapter
|
||||
mock_adapter = AsyncMock()
|
||||
|
|
@ -310,6 +362,7 @@ class TestSendUpdateNotification:
|
|||
(hermes_home / ".update_output.txt").write_text(
|
||||
"\x1b[32m✓ Code updated!\x1b[0m\n\x1b[1mDone\x1b[0m"
|
||||
)
|
||||
(hermes_home / ".update_exit_code").write_text("0")
|
||||
|
||||
mock_adapter = AsyncMock()
|
||||
runner.adapters = {Platform.TELEGRAM: mock_adapter}
|
||||
|
|
@ -331,6 +384,7 @@ class TestSendUpdateNotification:
|
|||
pending = {"platform": "telegram", "chat_id": "111", "user_id": "222"}
|
||||
(hermes_home / ".update_pending.json").write_text(json.dumps(pending))
|
||||
(hermes_home / ".update_output.txt").write_text("x" * 5000)
|
||||
(hermes_home / ".update_exit_code").write_text("0")
|
||||
|
||||
mock_adapter = AsyncMock()
|
||||
runner.adapters = {Platform.TELEGRAM: mock_adapter}
|
||||
|
|
@ -344,6 +398,29 @@ class TestSendUpdateNotification:
|
|||
# Total message should not be absurdly long
|
||||
assert len(sent_text) < 4500
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sends_failure_message_when_update_fails(self, tmp_path):
|
||||
"""Non-zero exit codes produce a failure notification with captured output."""
|
||||
runner = _make_runner()
|
||||
hermes_home = tmp_path / "hermes"
|
||||
hermes_home.mkdir()
|
||||
|
||||
pending = {"platform": "telegram", "chat_id": "111", "user_id": "222"}
|
||||
(hermes_home / ".update_pending.json").write_text(json.dumps(pending))
|
||||
(hermes_home / ".update_output.txt").write_text("Traceback: boom")
|
||||
(hermes_home / ".update_exit_code").write_text("1")
|
||||
|
||||
mock_adapter = AsyncMock()
|
||||
runner.adapters = {Platform.TELEGRAM: mock_adapter}
|
||||
|
||||
with patch("gateway.run._hermes_home", hermes_home):
|
||||
result = await runner._send_update_notification()
|
||||
|
||||
assert result is True
|
||||
sent_text = mock_adapter.send.call_args[0][1]
|
||||
assert "update failed" in sent_text.lower()
|
||||
assert "Traceback: boom" in sent_text
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sends_generic_message_when_no_output(self, tmp_path):
|
||||
"""Sends a success message even if the output file is missing."""
|
||||
|
|
@ -354,6 +431,7 @@ class TestSendUpdateNotification:
|
|||
pending = {"platform": "telegram", "chat_id": "111", "user_id": "222"}
|
||||
(hermes_home / ".update_pending.json").write_text(json.dumps(pending))
|
||||
# No .update_output.txt created
|
||||
(hermes_home / ".update_exit_code").write_text("0")
|
||||
|
||||
mock_adapter = AsyncMock()
|
||||
runner.adapters = {Platform.TELEGRAM: mock_adapter}
|
||||
|
|
@ -362,7 +440,7 @@ class TestSendUpdateNotification:
|
|||
await runner._send_update_notification()
|
||||
|
||||
sent_text = mock_adapter.send.call_args[0][1]
|
||||
assert "restarted successfully" in sent_text
|
||||
assert "finished successfully" in sent_text
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_cleans_up_files_after_notification(self, tmp_path):
|
||||
|
|
@ -373,10 +451,12 @@ class TestSendUpdateNotification:
|
|||
|
||||
pending_path = hermes_home / ".update_pending.json"
|
||||
output_path = hermes_home / ".update_output.txt"
|
||||
exit_code_path = hermes_home / ".update_exit_code"
|
||||
pending_path.write_text(json.dumps({
|
||||
"platform": "telegram", "chat_id": "111", "user_id": "222",
|
||||
}))
|
||||
output_path.write_text("✓ Done")
|
||||
exit_code_path.write_text("0")
|
||||
|
||||
mock_adapter = AsyncMock()
|
||||
runner.adapters = {Platform.TELEGRAM: mock_adapter}
|
||||
|
|
@ -386,6 +466,7 @@ class TestSendUpdateNotification:
|
|||
|
||||
assert not pending_path.exists()
|
||||
assert not output_path.exists()
|
||||
assert not exit_code_path.exists()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_cleans_up_on_error(self, tmp_path):
|
||||
|
|
@ -396,10 +477,12 @@ class TestSendUpdateNotification:
|
|||
|
||||
pending_path = hermes_home / ".update_pending.json"
|
||||
output_path = hermes_home / ".update_output.txt"
|
||||
exit_code_path = hermes_home / ".update_exit_code"
|
||||
pending_path.write_text(json.dumps({
|
||||
"platform": "telegram", "chat_id": "111", "user_id": "222",
|
||||
}))
|
||||
output_path.write_text("✓ Done")
|
||||
exit_code_path.write_text("0")
|
||||
|
||||
# Adapter send raises
|
||||
mock_adapter = AsyncMock()
|
||||
|
|
@ -412,6 +495,7 @@ class TestSendUpdateNotification:
|
|||
# Files should still be cleaned up (finally block)
|
||||
assert not pending_path.exists()
|
||||
assert not output_path.exists()
|
||||
assert not exit_code_path.exists()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_handles_corrupt_pending_file(self, tmp_path):
|
||||
|
|
@ -440,8 +524,10 @@ class TestSendUpdateNotification:
|
|||
pending = {"platform": "discord", "chat_id": "111", "user_id": "222"}
|
||||
pending_path = hermes_home / ".update_pending.json"
|
||||
output_path = hermes_home / ".update_output.txt"
|
||||
exit_code_path = hermes_home / ".update_exit_code"
|
||||
pending_path.write_text(json.dumps(pending))
|
||||
output_path.write_text("Done")
|
||||
exit_code_path.write_text("0")
|
||||
|
||||
# Only telegram adapter available, but pending says discord
|
||||
mock_adapter = AsyncMock()
|
||||
|
|
@ -454,6 +540,7 @@ class TestSendUpdateNotification:
|
|||
mock_adapter.send.assert_not_called()
|
||||
# Files should still be cleaned up
|
||||
assert not pending_path.exists()
|
||||
assert not exit_code_path.exists()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue