test(plugins): cover destructive update paths

(cherry picked from commit 7d5cfb5cff73661fc96a0737c1b187fc4594c8d6)
This commit is contained in:
JoaoMarcos44
2026-09-25 02:37:18 -03:00
committed by kshitij
parent 3e03da41e1
commit 23e678aaf9
2 changed files with 97 additions and 0 deletions

View File

@@ -204,6 +204,100 @@ def test_repin_keeps_local_files_backs_up_edits_and_follows_manifest_rename(worl
assert any("plugins-backup" in w for w in result["warnings"]) and any("renamed" in w for w in result["warnings"])
def test_carry_user_files_without_git_preserves_data_but_not_old_code_or_type_clashes(tmp_path):
"""No-git fallback keeps user state without resurrecting removed code or fighting new tree shapes."""
old = tmp_path / "old"
new = tmp_path / "new"
old.mkdir()
new.mkdir()
(old / "config.yaml").write_text("endpoint: mine\n")
(old / "data").mkdir()
(old / "data" / "state.db").write_text("user data")
(old / "legacy.py").write_text("OLD = True\n")
# old=file/new=dir: the new directory owns the path.
(old / "file-to-dir").write_text("old user file")
(new / "file-to-dir").mkdir()
(new / "file-to-dir" / "current.txt").write_text("new tree")
# old=dir/new=file: carrying a descendant must not make mkdir abort the update.
(old / "dir-to-file").mkdir()
(old / "dir-to-file" / "state.db").write_text("old nested data")
(new / "dir-to-file").write_text("new tree file")
cat._carry_user_files(old, new, None)
assert (new / "config.yaml").read_text() == "endpoint: mine\n"
assert (new / "data" / "state.db").read_text() == "user data"
assert not (new / "legacy.py").exists()
assert (new / "file-to-dir" / "current.txt").read_text() == "new tree"
assert sorted(path.name for path in (new / "file-to-dir").iterdir()) == ["current.txt"]
assert (new / "dir-to-file").read_text() == "new tree file"
@pytest.mark.parametrize("via", ["url", "catalog"])
def test_update_of_a_subdir_install_keeps_files_the_user_created_or_edited(world, tmp_path, monkeypatch, via):
"""A subdirectory install carries no ``.git``; both update paths must preserve user config/data."""
mono = tmp_path / "mono"
src = mono / "plugins" / "sub-plugin"
src.mkdir(parents=True)
(src / "plugin.yaml").write_text("name: sub-plugin\nversion: 1.0.0\ndescription: d\n")
(src / "__init__.py").write_text("def register(ctx):\n pass\n")
(src / "config.yaml.example").write_text("endpoint: default\n")
sp.run(["git", "init", "-q"], cwd=mono, check=True, env=_GIT_ENV)
pin = {"sha": _commit(mono, "v1")}
def entry():
return pc_cat.PluginCatalogEntry(
name="sub-plugin",
repo=mono.as_uri(),
sha=pin["sha"],
description="d",
maintainer="t",
subdir="plugins/sub-plugin",
)
monkeypatch.setattr(pc_cat, "load_catalog", lambda catalog_dir=None: [entry()])
if via == "catalog":
target = cat.install_catalog_entry(entry(), force=False)[0]
else:
target = pc._install_plugin_core(f"{mono.as_uri()}#plugins/sub-plugin", force=False)[0]
assert not (target / ".git").exists()
(target / "config.yaml").write_text("endpoint: mine\n")
(target / "data").mkdir()
(target / "data" / "state.json").write_text("{}")
(src / "plugin.yaml").write_text("name: sub-plugin\nversion: 2.0.0\ndescription: d\n")
(src / "config.yaml.example").write_text("endpoint: new-default\n")
pin["sha"] = _commit(mono, "v2")
assert pc.dashboard_update_user_plugin("sub-plugin")["ok"] is True
assert "version: 2.0.0" in (target / "plugin.yaml").read_text()
assert (target / "config.yaml").read_text() == "endpoint: mine\n"
assert (target / "data" / "state.json").read_text() == "{}"
def test_repin_keeps_a_wholly_ignored_data_dir_in_a_git_checkout(world):
"""A single ``!! data/`` status entry must preserve every file below that ignored directory."""
repo = world["repo"]
(repo / ".gitignore").write_text("data/\n")
world["state"]["pin"] = _commit(repo, "ignore data")
target = cat.install_catalog_entry(pc_cat.get_live_catalog_entry("cat-plugin"), force=False)[0]
assert (target / ".git").exists()
(target / "data" / "db").mkdir(parents=True)
(target / "data" / "db" / "index.db").write_text("user data")
(repo / "__init__.py").write_text("def register(ctx):\n pass # v3\n")
world["state"]["pin"] = _commit(repo, "v3")
assert pc.dashboard_update_user_plugin("cat-plugin")["unchanged"] is False
assert _head(target) == world["state"]["pin"]
assert (target / "data" / "db" / "index.db").read_text() == "user data"
def test_kill_list_covers_update_enable_and_load_of_an_installed_plugin(world, tmp_path, monkeypatch):
"""A URL install whose name lands on the kill list AFTER install must stop pulling, cannot be enabled
and is refused at load; an install made with --allow-removed keeps working."""

View File

@@ -196,6 +196,9 @@ catalog moved (via a reviewed PR), prepares and dependency-validates the new SHA
before publishing it. Your
enabled/disabled state is preserved, and so are files the plugin's repo does
not track (the `config.yaml` created from its `.example`, data files, `.env`).
For monorepo/subdirectory installs, which do not carry a local Git checkout,
update preserves non-Python files the new revision does not ship; removed Python
code is not carried forward because it can shadow the new plugin layout.
Edits you made to *tracked* files are not carried onto the new code; copies are
saved under `~/.hermes/plugins-backup/<name>-<sha>/` and the update warns you.
If the new pin renames the plugin's manifest, the old directory is removed and