diff --git a/hermes_cli/profiles.py b/hermes_cli/profiles.py index 8f8d7729d2..6066beee1e 100644 --- a/hermes_cli/profiles.py +++ b/hermes_cli/profiles.py @@ -80,19 +80,21 @@ def _clone_all_copytree_ignore(source_dir: Path): if source_resolved == _get_default_hermes_home().resolve(): root_exclude |= _CLONE_ALL_DEFAULT_EXCLUDE_ROOT - def _ignore(directory: str, names: List[str]) -> List[str]: + def _ignore(directory: str, names: List[str]) -> set: try: at_root = Path(directory).resolve() == source_resolved except (OSError, ValueError): # resolve() can fail on odd FS layouts (broken symlinks, missing parents). # Fail open — better to over-copy than silently drop user data. at_root = False - return [ + # A live source profile carries gateway/agent-browser sockets that copytree + # cannot copy; _non_exportable_entries drops them (and __pycache__/*.sock/*.tmp). + ignored = _non_exportable_entries(directory, names) + ignored.update( entry for entry in names - if entry == "__pycache__" - or entry.endswith((".pyc", ".pyo", ".sock", ".tmp")) - or (at_root and entry in root_exclude) - ] + if entry.endswith((".pyc", ".pyo")) or (at_root and entry in root_exclude) + ) + return ignored return _ignore diff --git a/tests/hermes_cli/test_profiles.py b/tests/hermes_cli/test_profiles.py index d210e4a271..389385a48e 100644 --- a/tests/hermes_cli/test_profiles.py +++ b/tests/hermes_cli/test_profiles.py @@ -201,6 +201,7 @@ class TestCreateProfile: assert (profile_dir / ".env").read_text().strip() == "KEY=val" assert (profile_dir / "SOUL.md").read_text() == "Be helpful." + @pytest.mark.skipif(not hasattr(os, "mkfifo"), reason="special files need a POSIX filesystem") def test_clone_all_does_not_copy_cron_jobs(self, profile_env): # Cron jobs are scheduled work bound to the source profile + origin channel; a clone # that inherits jobs.json fires every job twice (two gateways, same job ids). @@ -209,12 +210,20 @@ class TestCreateProfile: (default_home / "cron").mkdir() (default_home / "cron" / "jobs.json").write_text(json.dumps({"jobs": [{"id": "abc123def456"}]})) (default_home / "cron" / "output").mkdir() + # A live source profile holds special files copytree cannot copy (e.g. a suffixless + # agent-browser control socket); one of them must not abort the whole clone. + browser_dir = default_home / "home" / ".agent-browser" + browser_dir.mkdir(parents=True) + (browser_dir / "state.json").write_text("{}") + os.mkfifo(browser_dir / "control") profile_dir = create_profile("coder", clone_all=True, no_alias=True) assert (profile_dir / "cron").is_dir() assert not any((profile_dir / "cron").iterdir()) assert yaml.safe_load((profile_dir / "config.yaml").read_text())["model"] == "test" + assert (profile_dir / "home" / ".agent-browser" / "state.json").is_file() + assert not (profile_dir / "home" / ".agent-browser" / "control").exists()