fix(flr): partition-rooted FLR paths + agent/intent in checkpoint names (#2)
This commit was merged in pull request #2.
This commit is contained in:
@@ -1,18 +1,46 @@
|
||||
from datetime import UTC, datetime
|
||||
|
||||
from zerto_rewind_mcp.checkpoints import checkpoint_id, checkpoint_tag, make_tag
|
||||
from zerto_rewind_mcp.checkpoints import (
|
||||
TAG_MAX_LEN,
|
||||
checkpoint_id,
|
||||
checkpoint_tag,
|
||||
make_tag,
|
||||
)
|
||||
|
||||
WHEN = datetime(2026, 9, 21, 15, 4, 5, tzinfo=UTC)
|
||||
|
||||
|
||||
def test_tag_format():
|
||||
when = datetime(2026, 9, 21, 15, 4, 5, tzinfo=UTC)
|
||||
tag = make_tag("codex", "chg-99", when)
|
||||
assert tag == "ai:codex:chg-99:20260921T150405Z"
|
||||
def test_tag_describes_agent_and_action():
|
||||
tag = make_tag(
|
||||
"claude",
|
||||
"chg-99",
|
||||
WHEN,
|
||||
action="edit /home/justin/app-config.yaml",
|
||||
vm_name="jp-ubuntu",
|
||||
)
|
||||
assert tag == (
|
||||
"ai:claude | edit /home/justin/app-config.yaml | vm=jp-ubuntu "
|
||||
"| change=chg-99 | 20260921T150405Z"
|
||||
)
|
||||
|
||||
|
||||
def test_tag_strips_junk():
|
||||
tag = make_tag("agent one", "path/with spaces", datetime(2026, 1, 1, tzinfo=UTC))
|
||||
assert " " not in tag
|
||||
assert tag.startswith("ai:agent-one:path-with-spaces:")
|
||||
def test_tag_without_action_or_vm():
|
||||
assert make_tag("codex", "chg-99", WHEN) == "ai:codex | change=chg-99 | 20260921T150405Z"
|
||||
|
||||
|
||||
def test_tag_keeps_separators_unambiguous():
|
||||
# ';' is what Zerto appends to its own tags; '|' is our field separator.
|
||||
tag = make_tag("a;b", "c|d", WHEN, action="rm -rf /tmp;x", vm_name="v|m")
|
||||
assert tag.count(" | ") == 4
|
||||
assert ";" not in tag
|
||||
|
||||
|
||||
def test_tag_collapses_whitespace_and_caps_length():
|
||||
tag = make_tag("agent one", "chg 1", WHEN, action="do\n many things")
|
||||
assert "\n" not in tag
|
||||
assert "do many things" in tag
|
||||
long_tag = make_tag("a" * 200, "b" * 200, WHEN, action="c" * 400)
|
||||
assert len(long_tag) <= TAG_MAX_LEN
|
||||
|
||||
|
||||
def test_checkpoint_row_keys():
|
||||
|
||||
@@ -28,3 +28,52 @@ def test_flr_list_status():
|
||||
]
|
||||
row = flr_row(payload)
|
||||
assert flr_status(row).lower() == "mountcompletedsuccessfully"
|
||||
|
||||
|
||||
def test_path_items_shapes():
|
||||
from zerto_rewind_mcp.recover import path_items
|
||||
|
||||
assert path_items({"PathItems": [{"Path": "a"}]}) == [{"Path": "a"}]
|
||||
assert path_items([{"Path": "b"}]) == [{"Path": "b"}]
|
||||
assert path_items(None) == []
|
||||
|
||||
|
||||
def test_decode_flr_path():
|
||||
from zerto_rewind_mcp.recover import _decode_flr_path
|
||||
|
||||
assert _decode_flr_path("Volume2-Ext4%2fhome%2fjustin%2fapp-config.yaml") == (
|
||||
"Volume2-Ext4/home/justin/app-config.yaml"
|
||||
)
|
||||
|
||||
|
||||
def test_resolve_flr_path_picks_browsable_partition():
|
||||
import asyncio
|
||||
|
||||
from zerto_rewind_mcp.recover import resolve_flr_path
|
||||
|
||||
root = {
|
||||
"PathItems": [
|
||||
{"Path": "Volume1-Unknown", "IsBrowsable": False},
|
||||
{"Path": "Volume2-Ext4", "IsBrowsable": True},
|
||||
]
|
||||
}
|
||||
listing = {
|
||||
"PathItems": [
|
||||
{"Path": "Volume2-Ext4%2fhome%2fjustin%2f.bashrc", "Type": "File"},
|
||||
{"Path": "Volume2-Ext4%2fhome%2fjustin%2fapp-config.yaml", "Type": "File"},
|
||||
]
|
||||
}
|
||||
|
||||
class FakeClient:
|
||||
def __init__(self):
|
||||
self.seen = []
|
||||
|
||||
async def browse_flr(self, session_id, path="", recursive=False):
|
||||
self.seen.append(path)
|
||||
return root if path == "" else listing
|
||||
|
||||
client = FakeClient()
|
||||
got = asyncio.run(resolve_flr_path(client, "sess", "/home/justin/app-config.yaml"))
|
||||
assert got == "Volume2-Ext4/home/justin/app-config.yaml"
|
||||
# must not try the unrestorable partition
|
||||
assert "Volume1-Unknown/home/justin" not in client.seen
|
||||
|
||||
Reference in New Issue
Block a user