diff --git a/tests/tui_gateway/test_hosted_room_service.py b/tests/tui_gateway/test_hosted_room_service.py index 0d1e3b731d..80596b14a0 100644 --- a/tests/tui_gateway/test_hosted_room_service.py +++ b/tests/tui_gateway/test_hosted_room_service.py @@ -2073,3 +2073,19 @@ def test_peer_recovery_replays_the_same_execution_generation(tmp_path: Path): assert recovered["task_id"] == "task-1" assert recovered["execution_generation"] == 1 assert recovered["prompt"] == "Recover the accepted review." + + +def test_local_profiles_skips_delete_tombstones_and_dot_dirs(tmp_path: Path): + """`hermes profile delete` leaves ``profiles/.deleted/``; neither the tombstone dir nor a + tombstoned profile is a roster member (#106847: ``.deleted`` failed validate_roster every cycle).""" + from hermes_constants import mark_named_profile_deleted + + profiles = tmp_path / "profiles" + (profiles / "ops").mkdir(parents=True) + (profiles / "gone").mkdir() + mark_named_profile_deleted(profiles / "gone") + assert (profiles / ".deleted").is_dir() + + service = HostedRoomService(_server(), db_path=tmp_path / "shared-state.db") + + assert service.local_profiles() == ("default", "ops") diff --git a/tui_gateway/hosted_room_service.py b/tui_gateway/hosted_room_service.py index ca23d955ed..a5d7e131a8 100644 --- a/tui_gateway/hosted_room_service.py +++ b/tui_gateway/hosted_room_service.py @@ -124,9 +124,15 @@ class HostedRoomService: return self.db_path.parent def local_profiles(self) -> tuple[str, ...]: + from hermes_constants import named_profile_is_deleted + profiles, profiles_dir = {"default"}, self.root / "profiles" if profiles_dir.is_dir(): - profiles.update(path.name for path in profiles_dir.iterdir() if path.is_dir()) + # ``profiles/.deleted/`` is the tombstone dir `hermes profile delete` leaves behind, not a + # profile: feeding it to validate_roster failed plan_next_task on every cycle (#106847). + profiles.update( + path.name for path in profiles_dir.iterdir() + if path.is_dir() and not path.name.startswith(".") and not named_profile_is_deleted(path)) return tuple(sorted(profiles)) def bindings(self) -> tuple[HostedRoomBinding, ...]: