feat(poc): rewind MCP, skill, and recover-ladder docs
Initial PoC: find_protection, tagged checkpoints, FLR, mutating catalog. Lab 10.9 status enums (0=Initializing, 1=MeetingSLA). Credentials stay in gitignored config.json.
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from zerto_rewind_mcp.catalog import CatalogEntry, MutatingCatalog, entry_from_dict
|
||||
|
||||
|
||||
def test_add_and_get(tmp_path: Path):
|
||||
path = tmp_path / "config.json"
|
||||
path.write_text(
|
||||
json.dumps({"zerto_url": "https://zvm", "mutating_tools": []}),
|
||||
encoding="utf-8",
|
||||
)
|
||||
cat = MutatingCatalog(path=path)
|
||||
cat.add(CatalogEntry(server="ssh", tool="exec", vm_arg="host", notes="guest shell"))
|
||||
found = cat.get("SSH", "Exec")
|
||||
assert found is not None
|
||||
assert found.vm_arg == "host"
|
||||
saved = json.loads(path.read_text(encoding="utf-8"))
|
||||
assert saved["zerto_url"] == "https://zvm"
|
||||
assert saved["mutating_tools"][0]["tool"] == "exec"
|
||||
|
||||
|
||||
def test_unlisted_is_missing():
|
||||
cat = MutatingCatalog()
|
||||
assert cat.get("foo", "bar") is None
|
||||
|
||||
|
||||
def test_entry_requires_fields():
|
||||
try:
|
||||
entry_from_dict({"server": "ssh"})
|
||||
raise AssertionError("expected ValueError")
|
||||
except ValueError:
|
||||
pass
|
||||
@@ -0,0 +1,24 @@
|
||||
from datetime import UTC, datetime
|
||||
|
||||
from zerto_rewind_mcp.checkpoints import checkpoint_id, checkpoint_tag, make_tag
|
||||
|
||||
|
||||
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_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_checkpoint_row_keys():
|
||||
row = {"CheckpointId": "cp-1", "Tag": "ai:x:y:z", "Timestamp": 1}
|
||||
assert checkpoint_id(row) == "cp-1"
|
||||
assert checkpoint_tag(row) == "ai:x:y:z"
|
||||
row2 = {"checkpointId": "cp-2", "tag": "t"}
|
||||
assert checkpoint_id(row2) == "cp-2"
|
||||
assert checkpoint_tag(row2) == "t"
|
||||
@@ -0,0 +1,103 @@
|
||||
from zerto_rewind_mcp.protection import find_from_rows, group_vm_rows
|
||||
from zerto_rewind_mcp.status import (
|
||||
BITMAP_SYNC,
|
||||
DELTA_SYNC,
|
||||
INITIAL_SYNC,
|
||||
INITIALIZING,
|
||||
MEETING_SLA,
|
||||
NOT_MEETING_SLA,
|
||||
)
|
||||
|
||||
|
||||
def _row(vm_id, vm_name, vpg_id, vpg_name, status=MEETING_SLA, sub=0):
|
||||
return {
|
||||
"VmIdentifier": vm_id,
|
||||
"VmName": vm_name,
|
||||
"VpgIdentifier": vpg_id,
|
||||
"VpgName": vpg_name,
|
||||
"Status": status,
|
||||
"SubStatus": sub,
|
||||
}
|
||||
|
||||
|
||||
def test_none():
|
||||
result = find_from_rows("web01", [])
|
||||
assert result.outcome == "none"
|
||||
assert "refuse" in result.message.lower()
|
||||
|
||||
|
||||
def test_unique_two_vpgs():
|
||||
rows = [
|
||||
_row("vm-1", "web01", "vpg-local", "local-bu"),
|
||||
_row("vm-1", "web01", "vpg-dr", "dr-remote"),
|
||||
]
|
||||
result = find_from_rows("web01", rows)
|
||||
assert result.outcome == "ok"
|
||||
assert result.vm is not None
|
||||
assert result.vm.vm_identifier == "vm-1"
|
||||
assert len(result.vm.vpgs) == 2
|
||||
assert len(result.taggable_vpgs) == 2
|
||||
|
||||
|
||||
def test_ambiguous_two_vms():
|
||||
rows = [
|
||||
_row("vm-1", "web01", "vpg-a", "a"),
|
||||
_row("vm-2", "web01", "vpg-b", "b"),
|
||||
]
|
||||
result = find_from_rows("web01", rows)
|
||||
assert result.outcome == "ambiguous"
|
||||
assert len(result.matches) == 2
|
||||
assert result.taggable_vpgs == []
|
||||
|
||||
|
||||
def test_skip_syncing_vpg():
|
||||
rows = [
|
||||
_row("vm-1", "app", "vpg-ok", "ok", status=MEETING_SLA, sub=0),
|
||||
_row("vm-1", "app", "vpg-sync", "syncing", status=MEETING_SLA, sub=INITIAL_SYNC),
|
||||
_row("vm-1", "app", "vpg-delta", "delta", status=MEETING_SLA, sub=DELTA_SYNC),
|
||||
_row("vm-1", "app", "vpg-bitmap", "bitmap", status=MEETING_SLA, sub=BITMAP_SYNC),
|
||||
]
|
||||
result = find_from_rows("app", rows)
|
||||
assert result.outcome == "ok"
|
||||
names = {v.vpg_name for v in result.taggable_vpgs}
|
||||
assert names == {"ok"}
|
||||
assert "InitialSync" in result.message
|
||||
|
||||
|
||||
def test_not_meeting_sla_is_taggable():
|
||||
rows = [_row("vm-1", "app", "vpg-a", "a", status=NOT_MEETING_SLA, sub=0)]
|
||||
result = find_from_rows("app", rows)
|
||||
assert result.outcome == "ok"
|
||||
assert len(result.taggable_vpgs) == 1
|
||||
|
||||
|
||||
def test_initializing_not_taggable():
|
||||
rows = [_row("vm-1", "app", "vpg-a", "a", status=INITIALIZING, sub=INITIAL_SYNC)]
|
||||
result = find_from_rows("app", rows)
|
||||
assert result.outcome == "ok"
|
||||
assert result.taggable_vpgs == []
|
||||
assert "refuse" in result.message.lower()
|
||||
|
||||
|
||||
def test_all_syncing_ok_but_no_taggable():
|
||||
rows = [_row("vm-1", "app", "vpg-a", "a", status=MEETING_SLA, sub=INITIAL_SYNC)]
|
||||
result = find_from_rows("app", rows)
|
||||
assert result.outcome == "ok"
|
||||
assert result.taggable_vpgs == []
|
||||
assert "refuse" in result.message.lower()
|
||||
|
||||
|
||||
def test_camel_case_keys():
|
||||
rows = [
|
||||
{
|
||||
"vmIdentifier": "id-9",
|
||||
"vmName": "db01",
|
||||
"vpgIdentifier": "v1",
|
||||
"vpgName": "db-vpg",
|
||||
"status": 1,
|
||||
"subStatus": 0,
|
||||
}
|
||||
]
|
||||
grouped = group_vm_rows(rows)
|
||||
assert grouped[0].vm_name == "db01"
|
||||
assert grouped[0].vpgs[0].can_tag
|
||||
@@ -0,0 +1,12 @@
|
||||
from zerto_rewind_mcp.recover import download_token_from, session_id_from
|
||||
|
||||
|
||||
def test_session_id_shapes():
|
||||
assert session_id_from("abc") == "abc"
|
||||
assert session_id_from({"sessionId": "s1"}) == "s1"
|
||||
assert session_id_from({"Identifier": "s2"}) == "s2"
|
||||
|
||||
|
||||
def test_download_token_shapes():
|
||||
assert download_token_from("tok") == "tok"
|
||||
assert download_token_from({"downloadToken": "t2"}) == "t2"
|
||||
Reference in New Issue
Block a user