feat(flr): make FLR session lifecycle visible and reapable (#3)

This commit was merged in pull request #3.
This commit is contained in:
2026-09-21 13:43:05 -04:00
parent 108e919dcb
commit 59d1f71617
7 changed files with 288 additions and 18 deletions
+82
View File
@@ -77,3 +77,85 @@ def test_resolve_flr_path_picks_browsable_partition():
assert got == "Volume2-Ext4/home/justin/app-config.yaml"
# must not try the unrestorable partition
assert "Volume1-Unknown/home/justin" not in client.seen
def test_session_rows_and_id_shapes():
from zerto_rewind_mcp.recover import session_id_of, session_rows
assert session_rows([{"a": 1}]) == [{"a": 1}]
assert session_rows({"a": 1}) == [{"a": 1}]
assert session_rows(None) == []
assert session_id_of({"FlrSessionIdentifier": "s1"}) == "s1"
assert session_id_of({"sessionId": "s2"}) == "s2"
assert session_id_of({}) == ""
def test_is_live_session():
from zerto_rewind_mcp.recover import is_live_session
assert is_live_session({"FlrSessionStatus": "MountCompletedSuccessfully"})
assert is_live_session({"FlrSessionStatus": "MountInProgress"})
# unmounted/ended/failed sessions linger as history and hold nothing
assert not is_live_session({"FlrSessionStatus": "UnmountCompletedSuccessfully"})
assert not is_live_session({"FlrSessionStatus": "MountFailed"})
assert not is_live_session({})
class _FakeClient:
"""Stands in for ZertoClient in teardown tests."""
def __init__(self, sessions=None, fail_end=False):
self._sessions = sessions or []
self.fail_end = fail_end
self.ended: list[str] = []
async def list_flrs(self):
return self._sessions
async def end_flr(self, session_id):
from zerto_rewind_mcp.client import ZertoError
if self.fail_end:
raise ZertoError("unmount refused")
self.ended.append(session_id)
def test_teardown_reports_failure_instead_of_swallowing():
import asyncio
from zerto_rewind_mcp.server import _teardown_flr
client = _FakeClient(fail_end=True)
out = asyncio.run(_teardown_flr(client, "sess-1", set()))
assert out["ok"] is False
assert out["failed"][0]["session_id"] == "sess-1"
assert "still be mounted" in out["message"]
def test_teardown_reaps_orphan_when_session_id_never_parsed():
import asyncio
from zerto_rewind_mcp.server import _teardown_flr
# start_flr succeeded on the ZVM but the response did not parse, so the
# caller never learned the id. The new live session must still be reaped.
live = [{"FlrSessionIdentifier": "new-1", "FlrSessionStatus": "MountCompletedSuccessfully"}]
client = _FakeClient(sessions=live)
out = asyncio.run(_teardown_flr(client, None, set()))
assert out["ended"] == ["new-1"]
assert out["orphans_reaped"] == ["new-1"]
assert client.ended == ["new-1"]
def test_teardown_leaves_pre_existing_sessions_alone():
import asyncio
from zerto_rewind_mcp.server import _teardown_flr
live = [
{"FlrSessionIdentifier": "someone-else", "FlrSessionStatus": "MountCompletedSuccessfully"}
]
client = _FakeClient(sessions=live)
out = asyncio.run(_teardown_flr(client, None, {"someone-else"}))
assert out["ended"] == []
assert client.ended == []