mirror of
https://github.com/agent0ai/agent-zero.git
synced 2026-07-09 17:08:29 +00:00
Skip usr/.time_travel during self-update user-data backups and add the same Time Travel shadow-history exclusion to default BackupService metadata.\n\nAdd focused regressions proving regular user files stay in backup coverage while Time Travel shadow repositories are omitted.
184 lines
5.8 KiB
Python
184 lines
5.8 KiB
Python
import json
|
|
import shutil
|
|
import zipfile
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from helpers.backup import BackupService
|
|
|
|
|
|
class UploadedBackup:
|
|
def __init__(self, path: Path):
|
|
self.path = path
|
|
|
|
def save(self, target: str) -> None:
|
|
shutil.copyfile(self.path, target)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_default_backup_patterns_exclude_time_travel_history(tmp_path):
|
|
root = tmp_path / "a0"
|
|
usr = root / "usr"
|
|
time_travel = usr / ".time_travel" / "workspaces" / "demo" / "repo.git"
|
|
time_travel.mkdir(parents=True)
|
|
(usr / "settings.json").write_text('{"ok": true}\n', encoding="utf-8")
|
|
(time_travel / "objects.pack").write_text("history\n", encoding="utf-8")
|
|
|
|
service = BackupService()
|
|
service.agent_zero_root = str(root)
|
|
service.base_paths = {str(root): str(root)}
|
|
metadata = service.get_default_backup_metadata()
|
|
|
|
files = await service.test_patterns(metadata, max_files=None)
|
|
paths = {item["real_path"] for item in files}
|
|
|
|
assert str(usr / "settings.json") in paths
|
|
assert str(time_travel / "objects.pack") not in paths
|
|
assert f"{root}/usr/.time_travel/**" in metadata["exclude_patterns"]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_pattern_scan_can_run_without_file_limit(tmp_path):
|
|
root = tmp_path / "a0"
|
|
usr = root / "usr"
|
|
usr.mkdir(parents=True)
|
|
for index in range(3):
|
|
(usr / f"file-{index}.txt").write_text(f"{index}\n", encoding="utf-8")
|
|
|
|
service = BackupService()
|
|
service.agent_zero_root = str(root)
|
|
service.base_paths = {str(root): str(root)}
|
|
metadata = {
|
|
"include_patterns": [f"{root}/usr/**"],
|
|
"exclude_patterns": [],
|
|
"include_hidden": True,
|
|
}
|
|
|
|
capped_files = await service.test_patterns(metadata, max_files=2)
|
|
all_files = await service.test_patterns(metadata, max_files=None)
|
|
|
|
assert len(capped_files) == 2
|
|
assert len(all_files) == 3
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_backup_uses_unlimited_pattern_scan(tmp_path, monkeypatch):
|
|
source_file = tmp_path / "source.txt"
|
|
source_file.write_text("payload\n", encoding="utf-8")
|
|
captured = {}
|
|
|
|
service = BackupService()
|
|
|
|
async def fake_test_patterns(metadata, max_files=1000):
|
|
captured["max_files"] = max_files
|
|
return [
|
|
{
|
|
"path": f"{service.agent_zero_root.rstrip('/')}/usr/file-{index}.txt",
|
|
"real_path": str(source_file),
|
|
"size": source_file.stat().st_size,
|
|
"modified": "2026-06-26T00:00:00+00:00",
|
|
"type": "file",
|
|
}
|
|
for index in range(3)
|
|
]
|
|
|
|
async def fake_info():
|
|
return {}
|
|
|
|
async def fake_author():
|
|
return "test"
|
|
|
|
monkeypatch.setattr(service, "test_patterns", fake_test_patterns)
|
|
monkeypatch.setattr(service, "_get_system_info", fake_info)
|
|
monkeypatch.setattr(service, "_get_environment_info", fake_info)
|
|
monkeypatch.setattr(service, "_get_backup_author", fake_author)
|
|
|
|
zip_path = await service.create_backup(
|
|
include_patterns=[f"{service.agent_zero_root}/usr/**"],
|
|
exclude_patterns=[],
|
|
include_hidden=True,
|
|
backup_name="large-backup",
|
|
)
|
|
|
|
assert captured["max_files"] is None
|
|
with zipfile.ZipFile(zip_path) as archive:
|
|
metadata = json.loads(archive.read("metadata.json").decode("utf-8"))
|
|
assert metadata["total_files"] == 3
|
|
assert (
|
|
f"{service.agent_zero_root.rstrip('/').lstrip('/')}/usr/file-2.txt"
|
|
in archive.namelist()
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_restore_can_reach_files_after_50000_archive_entries(tmp_path):
|
|
old_root = "/old-a0"
|
|
archive_root = old_root.lstrip("/")
|
|
file_count = 50_001
|
|
last_index = file_count - 1
|
|
zip_path = tmp_path / "large-backup.zip"
|
|
|
|
metadata = {
|
|
"environment_info": {"agent_zero_root": old_root},
|
|
"include_patterns": [f"{old_root}/usr/large/**"],
|
|
"exclude_patterns": [],
|
|
"include_hidden": True,
|
|
}
|
|
with zipfile.ZipFile(zip_path, "w", compression=zipfile.ZIP_STORED) as archive:
|
|
archive.writestr("metadata.json", json.dumps(metadata))
|
|
for index in range(file_count):
|
|
payload = "tail payload\n" if index == last_index else ""
|
|
archive.writestr(
|
|
f"{archive_root}/usr/large/file-{index:05d}.txt",
|
|
payload,
|
|
)
|
|
|
|
service = BackupService()
|
|
service.agent_zero_root = str(tmp_path / "restored-a0")
|
|
|
|
result = await service.restore_backup(
|
|
backup_file=UploadedBackup(zip_path),
|
|
restore_include_patterns=[
|
|
f"{old_root}/usr/large/file-{last_index:05d}.txt"
|
|
],
|
|
restore_exclude_patterns=[],
|
|
overwrite_policy="overwrite",
|
|
)
|
|
|
|
restored_path = (
|
|
Path(service.agent_zero_root)
|
|
/ "usr"
|
|
/ "large"
|
|
/ f"file-{last_index:05d}.txt"
|
|
)
|
|
assert len(result["restored_files"]) == 1
|
|
assert len(result["skipped_files"]) == file_count - 1
|
|
assert result["errors"] == []
|
|
assert restored_path.read_text(encoding="utf-8") == "tail payload\n"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_restore_clean_before_restore_uses_unlimited_pattern_scan(monkeypatch):
|
|
service = BackupService()
|
|
captured = {}
|
|
|
|
async def fake_test_patterns(metadata, max_files=1000):
|
|
captured["max_files"] = max_files
|
|
return []
|
|
|
|
monkeypatch.setattr(service, "test_patterns", fake_test_patterns)
|
|
|
|
result = await service._find_files_to_clean_with_user_metadata(
|
|
user_metadata={
|
|
"include_patterns": [f"{service.agent_zero_root}/usr/**"],
|
|
"exclude_patterns": [],
|
|
"include_hidden": True,
|
|
},
|
|
original_metadata={
|
|
"environment_info": {"agent_zero_root": service.agent_zero_root}
|
|
},
|
|
)
|
|
|
|
assert result == []
|
|
assert captured["max_files"] is None
|