mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
Make on-demand AI feature-bundle installs reliable and self-healing, closing the failure modes behind most "some tool doesn't work" reports. Multi-bundle installs: tools needing more than one bundle (Passport Photo, Enhance Faces) install every required bundle from one action and stay not-installed until all are present. Verified across all 19 AI tools. Downloads: self-heal the accelerated Hugging Face (Xet) client so an upgraded venv no longer silently falls back to slow urllib; restart instead of corrupting a resumed partial when a proxy ignores Range and returns 200; verify the completed size; fail fast on disk-full and HTTP 4xx; retry transient errors five times; add hf_transfer fallback and document Xet egress. Install integrity: crash-atomic venv writes so a killed or out-of-space install can no longer tear the shared venv and break other tools; a boot breadcrumb reseeds a torn venv to a clean state automatically; a post-install smoke import test refuses to record a bundle whose libraries cannot load; an install watchdog stops a wedged installer that would otherwise hold the venv writer lock forever. Adds unit and end-to-end tests for every failure mode above.
135 lines
4.2 KiB
Python
135 lines
4.2 KiB
Python
"""Crash-atomicity tests for move_tree, which writes into the SHARED venv.
|
|
|
|
The invariant under test: a crash (or ENOSPC) partway through move_tree must
|
|
never leave a destination entry missing. Because the venv is shared by every AI
|
|
tool, a half-replaced package is what tears the venv and breaks unrelated tools.
|
|
"""
|
|
|
|
import errno
|
|
import importlib.util
|
|
import os
|
|
|
|
import pytest
|
|
|
|
|
|
def load_installer():
|
|
script_path = os.path.join(os.path.dirname(__file__), "..", "install_feature.py")
|
|
spec = importlib.util.spec_from_file_location("install_feature_move_under_test", script_path)
|
|
module = importlib.util.module_from_spec(spec)
|
|
assert spec.loader is not None
|
|
spec.loader.exec_module(module)
|
|
return module
|
|
|
|
|
|
def test_file_over_file_is_replaced_without_a_delete_window(monkeypatch, tmp_path):
|
|
installer = load_installer()
|
|
src = tmp_path / "src"
|
|
dst = tmp_path / "dst"
|
|
src.mkdir()
|
|
dst.mkdir()
|
|
(dst / "pkg.py").write_text("OLD")
|
|
(src / "pkg.py").write_text("NEW")
|
|
|
|
removed = []
|
|
real_remove = os.remove
|
|
monkeypatch.setattr(installer.os, "remove", lambda p: removed.append(p) or real_remove(p))
|
|
|
|
installer.move_tree(str(src), str(dst))
|
|
|
|
assert (dst / "pkg.py").read_text() == "NEW"
|
|
# The old file was atomically replaced, never deleted-then-rewritten.
|
|
assert str(dst / "pkg.py") not in removed
|
|
|
|
|
|
def test_crash_mid_move_leaves_every_dest_old_or_new_never_missing(monkeypatch, tmp_path):
|
|
installer = load_installer()
|
|
src = tmp_path / "src"
|
|
dst = tmp_path / "dst"
|
|
src.mkdir()
|
|
dst.mkdir()
|
|
for name in ("a.py", "b.py", "c.py"):
|
|
(dst / name).write_text(f"OLD_{name}")
|
|
(src / name).write_text(f"NEW_{name}")
|
|
|
|
real_replace = os.replace
|
|
state = {"n": 0}
|
|
|
|
def flaky_replace(s, d):
|
|
state["n"] += 1
|
|
if state["n"] == 2:
|
|
raise OSError("simulated crash mid-move")
|
|
return real_replace(s, d)
|
|
|
|
monkeypatch.setattr(installer.os, "replace", flaky_replace)
|
|
|
|
with pytest.raises(OSError):
|
|
installer.move_tree(str(src), str(dst))
|
|
|
|
# Regardless of listdir order, every destination file must still exist and
|
|
# hold either its old or its new content -- never a torn/missing entry.
|
|
for name in ("a.py", "b.py", "c.py"):
|
|
assert (dst / name).exists()
|
|
assert (dst / name).read_text() in (f"OLD_{name}", f"NEW_{name}")
|
|
|
|
|
|
def test_merges_directories_and_overwrites_files(tmp_path):
|
|
installer = load_installer()
|
|
src = tmp_path / "src"
|
|
dst = tmp_path / "dst"
|
|
(src / "pkg").mkdir(parents=True)
|
|
(dst / "pkg").mkdir(parents=True)
|
|
(dst / "pkg" / "keep.py").write_text("KEEP")
|
|
(src / "pkg" / "keep.py").write_text("UPDATED")
|
|
(src / "pkg" / "new.py").write_text("NEW")
|
|
|
|
installer.move_tree(str(src), str(dst))
|
|
|
|
assert (dst / "pkg" / "keep.py").read_text() == "UPDATED"
|
|
assert (dst / "pkg" / "new.py").read_text() == "NEW"
|
|
|
|
|
|
def test_type_mismatch_dir_replaces_file(tmp_path):
|
|
installer = load_installer()
|
|
src = tmp_path / "src"
|
|
dst = tmp_path / "dst"
|
|
src.mkdir()
|
|
dst.mkdir()
|
|
(dst / "x").write_text("i-am-a-file")
|
|
(src / "x").mkdir()
|
|
(src / "x" / "inner.py").write_text("dir-content")
|
|
|
|
installer.move_tree(str(src), str(dst))
|
|
|
|
assert (dst / "x").is_dir()
|
|
assert (dst / "x" / "inner.py").read_text() == "dir-content"
|
|
|
|
|
|
def test_exdev_file_copies_via_temp_then_atomic_replace(monkeypatch, tmp_path):
|
|
installer = load_installer()
|
|
src = tmp_path / "src"
|
|
dst = tmp_path / "dst"
|
|
src.mkdir()
|
|
dst.mkdir()
|
|
(dst / "f.py").write_text("OLD")
|
|
(src / "f.py").write_text("NEW")
|
|
|
|
real_replace = os.replace
|
|
seen = {"exdev": False, "part": False}
|
|
|
|
def exdev_for_direct_move(s, d):
|
|
if s == str(src / "f.py"):
|
|
seen["exdev"] = True
|
|
raise OSError(errno.EXDEV, "cross-device link")
|
|
if s.endswith(".part"):
|
|
seen["part"] = True
|
|
return real_replace(s, d)
|
|
|
|
monkeypatch.setattr(installer.os, "replace", exdev_for_direct_move)
|
|
|
|
installer.move_tree(str(src), str(dst))
|
|
|
|
assert (dst / "f.py").read_text() == "NEW"
|
|
assert seen["exdev"] and seen["part"]
|
|
# No leftover temp file.
|
|
assert not (dst / "f.py.part").exists()
|