mirror of
https://github.com/rookiestar28/ComfyUI-OpenClaw.git
synced 2026-08-14 00:48:07 +00:00
772 lines
29 KiB
Python
772 lines
29 KiB
Python
import hashlib
|
|
import io
|
|
import json
|
|
import os
|
|
import tempfile
|
|
import threading
|
|
import time
|
|
import unittest
|
|
from pathlib import Path, PurePosixPath
|
|
from unittest.mock import patch
|
|
|
|
from services.model_manager import (
|
|
MODEL_TYPE_EXCLUSION_REASONS,
|
|
MODEL_TYPE_TO_SUBDIR,
|
|
DownloadCancelled,
|
|
DownloadTask,
|
|
ModelManager,
|
|
ModelManagerError,
|
|
_model_type_exclusion_reason,
|
|
_norm_model_type,
|
|
)
|
|
from services.model_manager_transfer import (
|
|
_absolute_bounded_install_path,
|
|
_resolve_bounded_relative_install_path,
|
|
)
|
|
from services.safe_io import PathTraversalError
|
|
|
|
|
|
class _FakeResponse:
|
|
def __init__(self, *, code: int, body: bytes, headers: dict[str, str]):
|
|
self._code = int(code)
|
|
self._body = io.BytesIO(body)
|
|
self.headers = headers
|
|
|
|
def getcode(self):
|
|
return self._code
|
|
|
|
def read(self, size: int = -1) -> bytes:
|
|
return self._body.read(size)
|
|
|
|
def __enter__(self):
|
|
return self
|
|
|
|
def __exit__(self, exc_type, exc, tb):
|
|
return False
|
|
|
|
|
|
class _FakeOpener:
|
|
def __init__(self, mapping):
|
|
self._mapping = mapping
|
|
|
|
def open(self, req, timeout=0): # pragma: no cover - exercised via service
|
|
range_header = req.headers.get("Range") or req.headers.get("range") or ""
|
|
factory = self._mapping.get(range_header) or self._mapping.get("__default__")
|
|
if factory is None:
|
|
raise AssertionError(f"unexpected range header: {range_header!r}")
|
|
return factory()
|
|
|
|
|
|
class TestModelManagerService(unittest.TestCase):
|
|
def setUp(self):
|
|
self.tmp = tempfile.TemporaryDirectory(prefix="openclaw_model_manager_service_")
|
|
self.state_root = Path(self.tmp.name) / "state"
|
|
self.install_root = Path(self.tmp.name) / "install"
|
|
self.manager = ModelManager(
|
|
state_root=self.state_root, install_root=self.install_root
|
|
)
|
|
self.manager.allow_any_public = True
|
|
|
|
def tearDown(self):
|
|
self.tmp.cleanup()
|
|
|
|
def _wait_terminal(self, task_id: str, timeout: float = 3.0):
|
|
end = time.time() + timeout
|
|
while time.time() < end:
|
|
task = self.manager.get_download_task(task_id)
|
|
if task["state"] in {"completed", "failed", "cancelled"}:
|
|
return task
|
|
time.sleep(0.02)
|
|
self.fail(f"Task {task_id} did not reach terminal state")
|
|
|
|
def test_search_filters_and_deterministic_order(self):
|
|
self.manager._save_installations(
|
|
[
|
|
{
|
|
"id": "rec-a",
|
|
"model_id": "installed-a",
|
|
"name": "Installed A",
|
|
"model_type": "checkpoint",
|
|
"source": "manual",
|
|
"source_label": "Manual",
|
|
"sha256": "a" * 64,
|
|
"tenant_id": "default",
|
|
"installed_at": 10,
|
|
},
|
|
{
|
|
"id": "rec-b",
|
|
"model_id": "installed-b",
|
|
"name": "Installed B",
|
|
"model_type": "lora",
|
|
"source": "manual",
|
|
"source_label": "Manual",
|
|
"sha256": "b" * 64,
|
|
"tenant_id": "default",
|
|
"installed_at": 9,
|
|
},
|
|
]
|
|
)
|
|
catalog_dir = self.state_root / "catalog"
|
|
catalog_dir.mkdir(parents=True, exist_ok=True)
|
|
(catalog_dir / "test.json").write_text(
|
|
"""
|
|
{
|
|
"source": "catalog",
|
|
"source_label": "Catalog",
|
|
"items": [
|
|
{
|
|
"id": "catalog-a",
|
|
"name": "Catalog A",
|
|
"model_type": "checkpoint",
|
|
"download_url": "https://example.com/catalog-a.safetensors",
|
|
"sha256": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
|
|
}
|
|
]
|
|
}
|
|
""".strip(),
|
|
encoding="utf-8",
|
|
)
|
|
result = self.manager.search_models(limit=10, offset=0)
|
|
self.assertEqual(result["pagination"]["total"], 3)
|
|
names = [item["name"] for item in result["items"]]
|
|
self.assertEqual(names, ["Installed A", "Installed B", "Catalog A"])
|
|
|
|
lora_only = self.manager.search_models(model_type="lora")
|
|
self.assertEqual(lora_only["pagination"]["total"], 1)
|
|
self.assertEqual(lora_only["items"][0]["id"], "installed-b")
|
|
|
|
def test_norm_model_type_tracks_current_comfyui_folder_keys(self):
|
|
self.assertEqual(_norm_model_type("diffusion_models"), "diffusion_models")
|
|
self.assertEqual(_norm_model_type("text_encoders"), "text_encoders")
|
|
self.assertEqual(_norm_model_type("audio_encoders"), "audio_encoders")
|
|
self.assertEqual(_norm_model_type("background_removal"), "background_removal")
|
|
self.assertEqual(_norm_model_type("frame_interpolation"), "frame_interpolation")
|
|
self.assertEqual(_norm_model_type("gligen"), "gligen")
|
|
self.assertEqual(
|
|
_norm_model_type("latent_upscale_models"), "latent_upscale_models"
|
|
)
|
|
self.assertEqual(_norm_model_type("hypernetworks"), "hypernetworks")
|
|
self.assertEqual(_norm_model_type("photomaker"), "photomaker")
|
|
self.assertEqual(_norm_model_type("model_patches"), "model_patches")
|
|
self.assertEqual(_norm_model_type("geometry_estimation"), "geometry_estimation")
|
|
self.assertEqual(_norm_model_type("optical_flow"), "optical_flow")
|
|
self.assertEqual(_norm_model_type("detection"), "detection")
|
|
self.assertEqual(_norm_model_type("unet"), "diffusion_models")
|
|
self.assertEqual(_norm_model_type("clip"), "text_encoders")
|
|
self.assertEqual(
|
|
_norm_model_type("latent_upscale_model"), "latent_upscale_models"
|
|
)
|
|
self.assertEqual(_norm_model_type("hypernetwork"), "hypernetworks")
|
|
self.assertEqual(_norm_model_type("model_patch"), "model_patches")
|
|
self.assertEqual(_norm_model_type("geometry"), "geometry_estimation")
|
|
self.assertEqual(_norm_model_type("detector"), "detection")
|
|
self.assertEqual(_norm_model_type("diffusers"), "other")
|
|
|
|
def test_current_comfyui_model_type_support_and_exclusions_are_explicit(self):
|
|
supported = {
|
|
"checkpoint": "checkpoints",
|
|
"lora": "loras",
|
|
"vae": "vae",
|
|
"controlnet": "controlnet",
|
|
"embedding": "embeddings",
|
|
"text_encoders": "text_encoders",
|
|
"diffusion_models": "diffusion_models",
|
|
"clip_vision": "clip_vision",
|
|
"style_models": "style_models",
|
|
"upscale_models": "upscale_models",
|
|
"vae_approx": "vae_approx",
|
|
"gligen": "gligen",
|
|
"latent_upscale_models": "latent_upscale_models",
|
|
"hypernetworks": "hypernetworks",
|
|
"photomaker": "photomaker",
|
|
"model_patches": "model_patches",
|
|
"audio_encoders": "audio_encoders",
|
|
"background_removal": "background_removal",
|
|
"frame_interpolation": "frame_interpolation",
|
|
"geometry_estimation": "geometry_estimation",
|
|
"optical_flow": "optical_flow",
|
|
"detection": "detection",
|
|
}
|
|
|
|
for model_type, subdir in supported.items():
|
|
with self.subTest(model_type=model_type):
|
|
self.assertEqual(MODEL_TYPE_TO_SUBDIR[model_type], subdir)
|
|
self.assertEqual(_norm_model_type(model_type), model_type)
|
|
|
|
for excluded in ("configs", "diffusers", "classifiers", "custom_nodes"):
|
|
with self.subTest(excluded=excluded):
|
|
self.assertIn(excluded, MODEL_TYPE_EXCLUSION_REASONS)
|
|
self.assertEqual(_norm_model_type(excluded), "other")
|
|
self.assertTrue(_model_type_exclusion_reason(excluded))
|
|
|
|
@patch("services.model_manager.validate_outbound_url")
|
|
def test_create_download_task_rejects_known_excluded_current_folder_keys(
|
|
self, mock_validate
|
|
):
|
|
payload = {
|
|
"model_id": "excluded-model",
|
|
"name": "Excluded Model",
|
|
"source": "catalog",
|
|
"source_label": "Catalog",
|
|
"download_url": "https://example.com/excluded.safetensors",
|
|
"expected_sha256": "a" * 64,
|
|
"provenance": {
|
|
"publisher": "OpenClaw",
|
|
"license": "OpenRAIL",
|
|
"source_url": "https://example.com/excluded",
|
|
},
|
|
}
|
|
|
|
for model_type in ("configs", "diffusers", "classifiers", "custom_nodes"):
|
|
with self.subTest(model_type=model_type):
|
|
with self.assertRaises(ModelManagerError) as ctx:
|
|
self.manager.create_download_task(
|
|
model_type=model_type,
|
|
**payload,
|
|
)
|
|
self.assertEqual(ctx.exception.code, "unsupported_model_type")
|
|
self.assertIn(model_type, ctx.exception.detail)
|
|
|
|
self.assertFalse(mock_validate.called)
|
|
self.assertEqual(self.manager._tasks, {})
|
|
|
|
@patch(
|
|
"services.model_manager.validate_outbound_url",
|
|
return_value=("https", "example.com", 443, ["1.1.1.1"]),
|
|
)
|
|
def test_create_download_and_import_success(self, _mock_validate):
|
|
payload = b"model-bytes"
|
|
digest = hashlib.sha256(payload).hexdigest()
|
|
|
|
def fake_download(task, _cancel_event):
|
|
stage = self.manager.staging_dir / task.task_id
|
|
stage.mkdir(parents=True, exist_ok=True)
|
|
final = stage / task.filename
|
|
final.write_bytes(payload)
|
|
return str(final), digest
|
|
|
|
self.manager._download = fake_download # type: ignore[assignment]
|
|
task = self.manager.create_download_task(
|
|
model_id="model-a",
|
|
name="Model A",
|
|
model_type="checkpoint",
|
|
source="catalog",
|
|
source_label="Catalog",
|
|
download_url="https://example.com/model-a.safetensors",
|
|
expected_sha256=digest,
|
|
provenance={
|
|
"publisher": "OpenClaw",
|
|
"license": "OpenRAIL",
|
|
"source_url": "https://example.com/model-a",
|
|
},
|
|
)
|
|
done = self._wait_terminal(task["task_id"])
|
|
self.assertEqual(done["state"], "completed")
|
|
rec = self.manager.import_downloaded_model(task_id=task["task_id"])
|
|
self.assertEqual(rec["model_id"], "model-a")
|
|
installed = self.install_root / rec["installation_path"]
|
|
self.assertTrue(installed.exists())
|
|
self.assertEqual(installed.read_bytes(), payload)
|
|
|
|
@patch(
|
|
"services.model_manager.validate_outbound_url",
|
|
return_value=("https", "example.com", 443, ["1.1.1.1"]),
|
|
)
|
|
def test_cancel_running_task(self, _mock_validate):
|
|
digest = "d" * 64
|
|
|
|
def slow_download(_task, cancel_event):
|
|
for _ in range(60):
|
|
if cancel_event.is_set():
|
|
raise DownloadCancelled()
|
|
time.sleep(0.01)
|
|
raise AssertionError("expected cancellation")
|
|
|
|
self.manager._download = slow_download # type: ignore[assignment]
|
|
task = self.manager.create_download_task(
|
|
model_id="model-cancel",
|
|
name="Model Cancel",
|
|
model_type="checkpoint",
|
|
source="catalog",
|
|
source_label="Catalog",
|
|
download_url="https://example.com/model-cancel.safetensors",
|
|
expected_sha256=digest,
|
|
provenance={
|
|
"publisher": "OpenClaw",
|
|
"license": "OpenRAIL",
|
|
"source_url": "https://example.com/model-cancel",
|
|
},
|
|
)
|
|
self.manager.cancel_download_task(task["task_id"])
|
|
done = self._wait_terminal(task["task_id"])
|
|
self.assertEqual(done["state"], "cancelled")
|
|
|
|
def test_import_fails_on_hash_mismatch(self):
|
|
staged_dir = self.manager.staging_dir / "task-hash"
|
|
staged_dir.mkdir(parents=True, exist_ok=True)
|
|
staged_file = staged_dir / "model.safetensors"
|
|
staged_file.write_bytes(b"bad")
|
|
task = DownloadTask(
|
|
task_id="task-hash",
|
|
model_id="model-hash",
|
|
name="Model Hash",
|
|
model_type="checkpoint",
|
|
source="catalog",
|
|
source_label="Catalog",
|
|
download_url="https://example.com/model.safetensors",
|
|
destination_subdir="checkpoints",
|
|
filename="model.safetensors",
|
|
expected_sha256="a" * 64,
|
|
provenance={
|
|
"publisher": "OpenClaw",
|
|
"license": "OpenRAIL",
|
|
"source_url": "https://example.com/model",
|
|
},
|
|
tenant_id="default",
|
|
state="completed",
|
|
staged_path=str(staged_file),
|
|
computed_sha256="a" * 64,
|
|
)
|
|
self.manager._tasks[task.task_id] = task
|
|
with self.assertRaises(ModelManagerError) as ctx:
|
|
self.manager.import_downloaded_model(task_id=task.task_id)
|
|
self.assertEqual(ctx.exception.code, "sha256_mismatch")
|
|
|
|
def test_resolve_install_target_rejects_escape(self):
|
|
with self.assertRaises(PathTraversalError):
|
|
self.manager._resolve_install_target(
|
|
str(self.install_root), "../escape.bin"
|
|
)
|
|
|
|
def test_bounded_relative_install_path_rebuilds_safe_absolute_target(self):
|
|
safe_rel = _resolve_bounded_relative_install_path(
|
|
install_root=self.install_root,
|
|
relative_target=PurePosixPath("checkpoints/nested/model.safetensors"),
|
|
)
|
|
|
|
self.assertEqual(safe_rel.as_posix(), "checkpoints/nested/model.safetensors")
|
|
rebuilt = _absolute_bounded_install_path(
|
|
install_root=self.install_root,
|
|
safe_relative_target=safe_rel,
|
|
)
|
|
self.assertEqual(
|
|
rebuilt,
|
|
(
|
|
self.install_root / "checkpoints" / "nested" / "model.safetensors"
|
|
).resolve(),
|
|
)
|
|
|
|
def test_import_records_resolved_relative_installation_path(self):
|
|
payload = b"model-bytes"
|
|
digest = hashlib.sha256(payload).hexdigest()
|
|
staged_dir = self.manager.staging_dir / "task-safe-path"
|
|
staged_dir.mkdir(parents=True, exist_ok=True)
|
|
staged_file = staged_dir / "model.safetensors"
|
|
staged_file.write_bytes(payload)
|
|
task = DownloadTask(
|
|
task_id="task-safe-path",
|
|
model_id="model-safe-path",
|
|
name="Model Safe Path",
|
|
model_type="checkpoint",
|
|
source="catalog",
|
|
source_label="Catalog",
|
|
download_url="https://example.com/model.safetensors",
|
|
destination_subdir="checkpoints//nested",
|
|
filename="model.safetensors",
|
|
expected_sha256=digest,
|
|
provenance={
|
|
"publisher": "OpenClaw",
|
|
"license": "OpenRAIL",
|
|
"source_url": "https://example.com/model",
|
|
},
|
|
tenant_id="default",
|
|
state="completed",
|
|
staged_path=str(staged_file),
|
|
computed_sha256=digest,
|
|
)
|
|
self.manager._tasks[task.task_id] = task
|
|
rec = self.manager.import_downloaded_model(task_id=task.task_id)
|
|
self.assertEqual(
|
|
rec["installation_path"], "checkpoints/nested/model.safetensors"
|
|
)
|
|
self.assertTrue((self.install_root / rec["installation_path"]).exists())
|
|
|
|
def test_import_cleanup_remains_bounded_on_copy_failure(self):
|
|
payload = b"model-bytes"
|
|
digest = hashlib.sha256(payload).hexdigest()
|
|
staged_dir = self.manager.staging_dir / "task-bounded-cleanup"
|
|
staged_dir.mkdir(parents=True, exist_ok=True)
|
|
staged_file = staged_dir / "model.safetensors"
|
|
staged_file.write_bytes(payload)
|
|
task = DownloadTask(
|
|
task_id="task-bounded-cleanup",
|
|
model_id="model-bounded-cleanup",
|
|
name="Model Bounded Cleanup",
|
|
model_type="checkpoint",
|
|
source="catalog",
|
|
source_label="Catalog",
|
|
download_url="https://example.com/model.safetensors",
|
|
destination_subdir="checkpoints/nested",
|
|
filename="model.safetensors",
|
|
expected_sha256=digest,
|
|
provenance={
|
|
"publisher": "OpenClaw",
|
|
"license": "OpenRAIL",
|
|
"source_url": "https://example.com/model",
|
|
},
|
|
tenant_id="default",
|
|
state="completed",
|
|
staged_path=str(staged_file),
|
|
computed_sha256=digest,
|
|
)
|
|
self.manager._tasks[task.task_id] = task
|
|
|
|
with patch(
|
|
"services.model_manager_transfer.shutil.copy2",
|
|
side_effect=OSError("copy failed"),
|
|
):
|
|
with self.assertRaises(OSError):
|
|
self.manager.import_downloaded_model(task_id=task.task_id)
|
|
|
|
target_dir = self.install_root / "checkpoints" / "nested"
|
|
if target_dir.exists():
|
|
leftovers = [path.name for path in target_dir.iterdir()]
|
|
else:
|
|
leftovers = []
|
|
self.assertEqual(leftovers, [])
|
|
|
|
def test_list_download_tasks_delta_cursor_contract(self):
|
|
first = DownloadTask(
|
|
task_id="task-1",
|
|
model_id="model-1",
|
|
name="Model 1",
|
|
model_type="checkpoint",
|
|
source="catalog",
|
|
source_label="Catalog",
|
|
download_url="https://example.com/model-1.safetensors",
|
|
destination_subdir="checkpoints",
|
|
filename="model-1.safetensors",
|
|
expected_sha256="a" * 64,
|
|
provenance={
|
|
"publisher": "OpenClaw",
|
|
"license": "OpenRAIL",
|
|
"source_url": "https://example.com/model-1",
|
|
},
|
|
tenant_id="default",
|
|
created_at=10.0,
|
|
change_seq=4,
|
|
)
|
|
second = DownloadTask(
|
|
task_id="task-2",
|
|
model_id="model-2",
|
|
name="Model 2",
|
|
model_type="checkpoint",
|
|
source="catalog",
|
|
source_label="Catalog",
|
|
download_url="https://example.com/model-2.safetensors",
|
|
destination_subdir="checkpoints",
|
|
filename="model-2.safetensors",
|
|
expected_sha256="b" * 64,
|
|
provenance={
|
|
"publisher": "OpenClaw",
|
|
"license": "OpenRAIL",
|
|
"source_url": "https://example.com/model-2",
|
|
},
|
|
tenant_id="default",
|
|
created_at=11.0,
|
|
change_seq=5,
|
|
)
|
|
self.manager._tasks[first.task_id] = first
|
|
self.manager._tasks[second.task_id] = second
|
|
self.manager._task_change_seq = 5
|
|
|
|
result = self.manager.list_download_tasks(limit=10, since_seq=4)
|
|
self.assertEqual([row["task_id"] for row in result["tasks"]], ["task-2"])
|
|
self.assertEqual(result["delta"]["requested_since_seq"], 4)
|
|
self.assertEqual(result["delta"]["effective_since_seq"], 4)
|
|
self.assertEqual(result["delta"]["next_since_seq"], 5)
|
|
self.assertEqual(result["delta"]["cursor_status"], "ok")
|
|
self.assertFalse(result["delta"]["truncated"])
|
|
|
|
def test_list_download_tasks_delta_resets_stale_cursor(self):
|
|
first = DownloadTask(
|
|
task_id="task-1",
|
|
model_id="model-1",
|
|
name="Model 1",
|
|
model_type="checkpoint",
|
|
source="catalog",
|
|
source_label="Catalog",
|
|
download_url="https://example.com/model-1.safetensors",
|
|
destination_subdir="checkpoints",
|
|
filename="model-1.safetensors",
|
|
expected_sha256="a" * 64,
|
|
provenance={
|
|
"publisher": "OpenClaw",
|
|
"license": "OpenRAIL",
|
|
"source_url": "https://example.com/model-1",
|
|
},
|
|
tenant_id="default",
|
|
created_at=10.0,
|
|
change_seq=7,
|
|
)
|
|
second = DownloadTask(
|
|
task_id="task-2",
|
|
model_id="model-2",
|
|
name="Model 2",
|
|
model_type="checkpoint",
|
|
source="catalog",
|
|
source_label="Catalog",
|
|
download_url="https://example.com/model-2.safetensors",
|
|
destination_subdir="checkpoints",
|
|
filename="model-2.safetensors",
|
|
expected_sha256="b" * 64,
|
|
provenance={
|
|
"publisher": "OpenClaw",
|
|
"license": "OpenRAIL",
|
|
"source_url": "https://example.com/model-2",
|
|
},
|
|
tenant_id="default",
|
|
created_at=11.0,
|
|
change_seq=8,
|
|
)
|
|
self.manager._tasks[first.task_id] = first
|
|
self.manager._tasks[second.task_id] = second
|
|
self.manager._task_change_seq = 8
|
|
|
|
result = self.manager.list_download_tasks(limit=1, since_seq=1)
|
|
self.assertEqual([row["task_id"] for row in result["tasks"]], ["task-1"])
|
|
self.assertEqual(result["delta"]["effective_since_seq"], 6)
|
|
self.assertEqual(result["delta"]["next_since_seq"], 7)
|
|
self.assertEqual(result["delta"]["cursor_status"], "stale_cursor_reset")
|
|
self.assertTrue(result["delta"]["truncated"])
|
|
|
|
@patch(
|
|
"services.model_manager.validate_outbound_url",
|
|
return_value=("https", "example.com", 443, ["1.1.1.1"]),
|
|
)
|
|
@patch("services.model_manager._build_pinned_opener")
|
|
def test_resume_download_with_http_range(self, mock_opener, _mock_validate):
|
|
payload = b"123456789"
|
|
digest = hashlib.sha256(payload).hexdigest()
|
|
task = DownloadTask(
|
|
task_id="task-resume",
|
|
model_id="model-resume",
|
|
name="Model Resume",
|
|
model_type="checkpoint",
|
|
source="catalog",
|
|
source_label="Catalog",
|
|
download_url="https://example.com/model-resume.safetensors",
|
|
destination_subdir="checkpoints",
|
|
filename="model-resume.safetensors",
|
|
expected_sha256=digest,
|
|
provenance={
|
|
"publisher": "OpenClaw",
|
|
"license": "OpenRAIL",
|
|
"source_url": "https://example.com/model-resume",
|
|
},
|
|
tenant_id="default",
|
|
state="running",
|
|
)
|
|
self.manager._tasks[task.task_id] = task
|
|
self.manager._cancel_events[task.task_id] = threading.Event()
|
|
|
|
stage_dir = self.manager.staging_dir / task.task_id
|
|
stage_dir.mkdir(parents=True, exist_ok=True)
|
|
part = stage_dir / f"{task.filename}.part"
|
|
part.write_bytes(payload[:4])
|
|
checkpoint = self.manager._checkpoint_path(part)
|
|
checkpoint.write_text(
|
|
json.dumps(
|
|
{
|
|
"version": 1,
|
|
"task_id": task.task_id,
|
|
"download_url": task.download_url,
|
|
"expected_sha256": task.expected_sha256,
|
|
"filename": task.filename,
|
|
"bytes_downloaded": 4,
|
|
"etag": "etag-1",
|
|
"last_modified": "lm-1",
|
|
}
|
|
),
|
|
encoding="utf-8",
|
|
)
|
|
|
|
mock_opener.return_value = _FakeOpener(
|
|
{
|
|
"bytes=4-": lambda: _FakeResponse(
|
|
code=206,
|
|
body=payload[4:],
|
|
headers={
|
|
"Content-Range": "bytes 4-8/9",
|
|
"Content-Length": "5",
|
|
"ETag": "etag-1",
|
|
"Last-Modified": "lm-1",
|
|
},
|
|
)
|
|
}
|
|
)
|
|
|
|
final_path, got = self.manager._download(task, threading.Event())
|
|
self.assertEqual(got, digest)
|
|
self.assertEqual(Path(final_path).read_bytes(), payload)
|
|
self.assertFalse(checkpoint.exists())
|
|
self.assertEqual(
|
|
self.manager._tasks[task.task_id].resume_status, "resumed_partial"
|
|
)
|
|
|
|
@patch(
|
|
"services.model_manager.validate_outbound_url",
|
|
return_value=("https", "example.com", 443, ["1.1.1.1"]),
|
|
)
|
|
@patch("services.model_manager._build_pinned_opener")
|
|
def test_resume_fallback_when_range_not_supported(
|
|
self, mock_opener, _mock_validate
|
|
):
|
|
payload = b"abcdefghij"
|
|
digest = hashlib.sha256(payload).hexdigest()
|
|
task = DownloadTask(
|
|
task_id="task-resume-fallback",
|
|
model_id="model-resume-fallback",
|
|
name="Model Resume Fallback",
|
|
model_type="checkpoint",
|
|
source="catalog",
|
|
source_label="Catalog",
|
|
download_url="https://example.com/model-resume-fallback.safetensors",
|
|
destination_subdir="checkpoints",
|
|
filename="model-resume-fallback.safetensors",
|
|
expected_sha256=digest,
|
|
provenance={
|
|
"publisher": "OpenClaw",
|
|
"license": "OpenRAIL",
|
|
"source_url": "https://example.com/model-resume-fallback",
|
|
},
|
|
tenant_id="default",
|
|
state="running",
|
|
)
|
|
self.manager._tasks[task.task_id] = task
|
|
self.manager._cancel_events[task.task_id] = threading.Event()
|
|
|
|
stage_dir = self.manager.staging_dir / task.task_id
|
|
stage_dir.mkdir(parents=True, exist_ok=True)
|
|
part = stage_dir / f"{task.filename}.part"
|
|
part.write_bytes(payload[:3])
|
|
checkpoint = self.manager._checkpoint_path(part)
|
|
checkpoint.write_text(
|
|
json.dumps(
|
|
{
|
|
"version": 1,
|
|
"task_id": task.task_id,
|
|
"download_url": task.download_url,
|
|
"expected_sha256": task.expected_sha256,
|
|
"filename": task.filename,
|
|
"bytes_downloaded": 3,
|
|
"etag": "etag-1",
|
|
"last_modified": "lm-1",
|
|
}
|
|
),
|
|
encoding="utf-8",
|
|
)
|
|
|
|
mock_opener.return_value = _FakeOpener(
|
|
{
|
|
"bytes=3-": lambda: _FakeResponse(
|
|
code=200,
|
|
body=payload,
|
|
headers={
|
|
"Content-Length": str(len(payload)),
|
|
"ETag": "etag-1",
|
|
"Last-Modified": "lm-1",
|
|
},
|
|
),
|
|
"__default__": lambda: _FakeResponse(
|
|
code=200,
|
|
body=payload,
|
|
headers={
|
|
"Content-Length": str(len(payload)),
|
|
"ETag": "etag-1",
|
|
"Last-Modified": "lm-1",
|
|
},
|
|
),
|
|
}
|
|
)
|
|
|
|
final_path, got = self.manager._download(task, threading.Event())
|
|
self.assertEqual(got, digest)
|
|
self.assertEqual(Path(final_path).read_bytes(), payload)
|
|
self.assertFalse(checkpoint.exists())
|
|
self.assertEqual(
|
|
self.manager._tasks[task.task_id].resume_status,
|
|
"resume_fallback_range_not_supported",
|
|
)
|
|
|
|
def test_restart_recovery_replay_limit(self):
|
|
state_root = Path(self.tmp.name) / "recover-state"
|
|
install_root = Path(self.tmp.name) / "recover-install"
|
|
state_root.mkdir(parents=True, exist_ok=True)
|
|
|
|
t1 = DownloadTask(
|
|
task_id="recover-1",
|
|
model_id="m1",
|
|
name="Recover 1",
|
|
model_type="checkpoint",
|
|
source="catalog",
|
|
source_label="Catalog",
|
|
download_url="https://example.com/m1.safetensors",
|
|
destination_subdir="checkpoints",
|
|
filename="m1.safetensors",
|
|
expected_sha256="a" * 64,
|
|
provenance={
|
|
"publisher": "OpenClaw",
|
|
"license": "OpenRAIL",
|
|
"source_url": "https://example.com/m1",
|
|
},
|
|
tenant_id="default",
|
|
state="running",
|
|
)
|
|
t2 = DownloadTask(
|
|
task_id="recover-2",
|
|
model_id="m2",
|
|
name="Recover 2",
|
|
model_type="checkpoint",
|
|
source="catalog",
|
|
source_label="Catalog",
|
|
download_url="https://example.com/m2.safetensors",
|
|
destination_subdir="checkpoints",
|
|
filename="m2.safetensors",
|
|
expected_sha256="b" * 64,
|
|
provenance={
|
|
"publisher": "OpenClaw",
|
|
"license": "OpenRAIL",
|
|
"source_url": "https://example.com/m2",
|
|
},
|
|
tenant_id="default",
|
|
state="queued",
|
|
)
|
|
(state_root / "download_tasks.json").write_text(
|
|
json.dumps([t1.to_dict(), t2.to_dict()]), encoding="utf-8"
|
|
)
|
|
|
|
with patch.object(ModelManager, "_run_task", return_value=None):
|
|
with patch.dict(
|
|
os.environ,
|
|
{"OPENCLAW_MODEL_DOWNLOAD_RECOVERY_REPLAY_LIMIT": "1"},
|
|
clear=False,
|
|
):
|
|
manager = ModelManager(state_root=state_root, install_root=install_root)
|
|
r1 = manager.get_download_task("recover-1")
|
|
r2 = manager.get_download_task("recover-2")
|
|
manager._executor.shutdown(wait=True)
|
|
|
|
states = {r1["state"], r2["state"]}
|
|
self.assertIn("queued", states)
|
|
self.assertIn("failed", states)
|
|
failed = r1 if r1["state"] == "failed" else r2
|
|
replay = r1 if r1["state"] == "queued" else r2
|
|
self.assertEqual(failed["error"], "recovery_replay_limit_exceeded")
|
|
self.assertEqual(replay["resume_status"], "restart_replay_queued")
|
|
self.assertEqual(replay["recovery_attempts"], 1)
|
|
|
|
|
|
if __name__ == "__main__": # pragma: no cover
|
|
unittest.main()
|