Files
hermes-agent/tests/hermes_cli/test_profile_export_default_path.py
ethernet 92686159d1 fix(pm): integrate audited runtime and lifecycle repairs
Prepare dependency generations before selecting them. Keep shipped tool
bytes separate from writable additions, and store facts beside their entries.
Validate proposed plugin sets before config publication. Restore the previous
config if the facts write fails.

Consolidate duplicate updater, backup, setup, and voice helpers. Repair
launcher selection, dependency consumers, download ownership, update feeds,
and native Windows process and file handling.

Verification: 206 changed/prior-failing Python files reported 4630 passed,
one failed, and 330 skipped. Fix the remaining Hindsight fixture boundary.
The final targeted rerun reported 234 passed and two skipped. The store
review regression batch reported 83 passed and one skipped. Desktop
TypeScript checks, 56 selected Electron tests, 24 release tests, and the
removed-import/compatibility guards passed.

This is an integration checkpoint, not full audit acceptance. The complete
Python suite has not run on this fixed tree. Crash-atomic plugin publication,
generation cleanup, receipt correlation, and packaged lifecycle acceptance
remain open in docs/pm-audit-status.md.
2026-09-05 22:36:48 -04:00

231 lines
8.2 KiB
Python

"""Regression coverage for safe automatic profile-export destinations."""
from __future__ import annotations
import importlib
from argparse import Namespace
from pathlib import Path
import pytest
from hermes_cli import profile_cmd
@pytest.fixture()
def profiles():
"""Resolve the live profiles module at call time.
Sibling test files reload ``hermes_cli.profiles`` / ``hermes_cli.main``;
a top-level ``from hermes_cli.profiles import ...`` here would bind the
pre-reload function objects and silently divorce this file's monkeypatches
from the code under test when the whole directory runs as one sweep.
"""
return importlib.import_module("hermes_cli.profiles")
def test_default_export_path_is_managed_and_outside_named_profiles(
tmp_path, monkeypatch, profiles
):
default_home = tmp_path / ".hermes"
default_home.mkdir()
monkeypatch.setattr(profiles, "_get_default_hermes_home", lambda: default_home)
result = profiles.get_profile_export_path(
"Research-Bot", timestamp="20260823-120000"
)
assert (
result
== default_home / "profile-exports" / "research-bot-20260823-120000.tar.gz"
)
assert result.parent.is_dir()
assert not (default_home / "profiles" / "research-bot" / result.name).exists()
def test_custom_hermes_home_inside_a_checkout_uses_a_sibling_store(
tmp_path, monkeypatch, profiles
):
checkout = tmp_path / "checkout"
checkout.mkdir()
(checkout / ".git").write_text("gitdir: ../git\n", encoding="utf-8")
monkeypatch.chdir(checkout)
monkeypatch.setattr(Path, "home", lambda: tmp_path / "home")
monkeypatch.setattr(profiles, "_get_default_hermes_home", lambda: checkout)
result = profiles.get_profile_export_path("default", timestamp="20260823-120000")
assert not result.resolve().is_relative_to(checkout.resolve())
def test_checkout_detection_does_not_depend_on_cwd(tmp_path, monkeypatch, profiles):
"""HERMES_HOME inside a checkout must be detected even when cwd is elsewhere.
Regression for the cwd-anchored bypass: cron/service-manager invocations
run from outside the checkout, and the original heuristic walked
``Path.cwd()`` — letting the export land inside the source tree again.
"""
checkout = tmp_path / "checkout"
checkout.mkdir()
(checkout / ".git").write_text("gitdir: ../git\n", encoding="utf-8")
hermes_home = checkout / "data_home"
hermes_home.mkdir()
elsewhere = tmp_path / "elsewhere"
elsewhere.mkdir()
monkeypatch.chdir(elsewhere) # cwd has no .git ancestor inside tmp_path
monkeypatch.setattr(Path, "home", lambda: tmp_path / "home")
monkeypatch.setattr(profiles, "_get_default_hermes_home", lambda: hermes_home)
result = profiles.get_profile_export_path("default", timestamp="20260823-120000")
assert not result.resolve().is_relative_to(checkout.resolve())
def test_cli_export_rejects_bad_profile_name_without_traceback(
tmp_path, monkeypatch, capsys, profiles
):
"""A bad name must print a clean error — the helper raises before export."""
main_mod = importlib.import_module("hermes_cli.main")
default_home = tmp_path / ".hermes"
default_home.mkdir()
monkeypatch.setattr(profiles, "_get_default_hermes_home", lambda: default_home)
with pytest.raises(SystemExit):
profile_cmd.cmd_profile(
Namespace(
profile_action="export",
profile_name="bad//name",
output=None,
)
)
out = capsys.readouterr().out
assert "Error:" in out
def test_cli_export_default_does_not_write_into_the_current_checkout(
tmp_path, monkeypatch, capsys, profiles
):
main_mod = importlib.import_module("hermes_cli.main")
default_home = tmp_path / ".hermes"
default_home.mkdir()
(default_home / "config.yaml").write_text("model: test\n", encoding="utf-8")
checkout = tmp_path / "checkout"
checkout.mkdir()
monkeypatch.chdir(checkout)
monkeypatch.setattr(profiles, "_get_default_hermes_home", lambda: default_home)
monkeypatch.setattr(
"hermes_constants.get_default_hermes_root", lambda: default_home
)
profile_cmd.cmd_profile(
Namespace(
profile_action="export",
profile_name="default",
output=None,
)
)
exported = list((default_home / "profile-exports").glob("default-*.tar.gz"))
assert len(exported) == 1
assert exported[0].parent == default_home / "profile-exports"
assert not (checkout / "default.tar.gz").exists()
assert str(exported[0]) in capsys.readouterr().out
def test_slash_export_uses_the_same_managed_destination(
tmp_path, monkeypatch, profiles
):
mixin_mod = importlib.import_module("hermes_cli.cli_commands_mixin")
default_home = tmp_path / ".hermes"
default_home.mkdir()
monkeypatch.setattr(profiles, "_get_default_hermes_home", lambda: default_home)
monkeypatch.setattr(profiles, "get_active_profile_name", lambda: "default")
calls = []
monkeypatch.setattr(
profiles,
"export_profile",
lambda name, output: calls.append((name, output)) or output,
)
mixin_mod.CLICommandsMixin()._handle_export_command("/export")
assert len(calls) == 1
assert calls[0][0] == "default"
assert Path(calls[0][1]).parent == default_home / "profile-exports"
assert not (tmp_path / "default.tar.gz").exists()
@pytest.mark.asyncio
async def test_profile_export_api_uses_the_shared_managed_destination(
tmp_path, monkeypatch, profiles
):
from hermes_cli.web_models import ProfileExport
router_mod = importlib.import_module("hermes_cli.web_routers.profiles")
managed = tmp_path / "profile-exports" / "default-20260823-120000.tar.gz"
monkeypatch.setattr(profiles, "get_profile_export_path", lambda name: managed)
monkeypatch.setattr(
profiles,
"export_profile",
lambda name, output, extra_files=None: output,
)
result = await router_mod.export_profile_endpoint("default", ProfileExport())
assert result == {"ok": True, "archive": str(managed)}
def test_cwd_in_unrelated_checkout_does_not_prove_safety(
tmp_path, monkeypatch, profiles
):
"""cwd inside unrelated checkout A must not stand in for the safety proof
of a HERMES_HOME inside checkout B."""
checkout_b = tmp_path / "checkout-b"
checkout_b.mkdir()
(checkout_b / ".git").mkdir()
checkout_a = tmp_path / "checkout-a"
checkout_a.mkdir()
(checkout_a / ".git").write_text("gitdir: ../git\n", encoding="utf-8")
monkeypatch.chdir(checkout_a)
monkeypatch.setattr(Path, "home", lambda: tmp_path / "home")
monkeypatch.setattr(profiles, "_get_default_hermes_home", lambda: checkout_b)
result = profiles.get_profile_export_path("default", timestamp="20260823-120000")
assert not result.resolve().is_relative_to(checkout_b.resolve())
assert not result.resolve().is_relative_to(checkout_a.resolve())
def test_every_candidate_inside_a_checkout_fails_closed(
tmp_path, monkeypatch, profiles
):
"""When home, sibling store, and tempdir all resolve inside checkouts the
helper must refuse — a warning would not stop a scripted export from
recreating the #92457 incident artifact."""
import tempfile
checkout = tmp_path / "checkout"
checkout.mkdir()
(checkout / ".git").mkdir()
monkeypatch.setattr(Path, "home", lambda: checkout / "home")
monkeypatch.setattr(profiles, "_get_default_hermes_home", lambda: checkout)
monkeypatch.setattr(tempfile, "gettempdir", lambda: str(checkout / "tmp"))
with pytest.raises(ValueError, match="No safe automatic export destination"):
profiles.get_profile_export_path("default")
@pytest.mark.platforms("posix")
def test_export_dir_symlink_is_rejected(tmp_path, monkeypatch, profiles):
"""A pre-created symlink at the managed export path (predictable-path
attack on shared hosts) must be refused, not silently followed."""
default_home = tmp_path / ".hermes"
default_home.mkdir()
elsewhere = tmp_path / "elsewhere"
elsewhere.mkdir()
(default_home / "profile-exports").symlink_to(elsewhere)
monkeypatch.setattr(profiles, "_get_default_hermes_home", lambda: default_home)
with pytest.raises(ValueError, match="symlink"):
profiles.get_profile_export_path("default")