diff --git a/gateway/slash_commands.py b/gateway/slash_commands.py index e8c43052bbc5..6649848a3401 100644 --- a/gateway/slash_commands.py +++ b/gateway/slash_commands.py @@ -330,12 +330,33 @@ class GatewaySlashCommandsMixin: return EphemeralReply(f"{header}{_tip_line}") async def _handle_profile_command(self, event: MessageEvent) -> str: - """Handle /profile — show active profile name and home directory.""" + """Handle /profile — show the profile serving this source and its home. + + On a multiplexed gateway the process-level active profile is always + the multiplexer's own (usually ``default``), so reporting it would + answer "default" in every chat regardless of which profile actually + serves the room/channel (``source.profile`` — stamped by the + ``/p//`` URL prefix, a per-credential adapter, or a room→ + profile map). Report the stamped profile and, like the scoped /reset + banner (#59003), resolve the displayed home under that profile's + runtime scope. Single-profile gateways are unchanged: an unstamped + source falls back to the active profile and the default home. + """ from hermes_constants import display_hermes_home from hermes_cli.profiles import get_active_profile_name - display = display_hermes_home() - profile_name = get_active_profile_name() + source = getattr(event, "source", None) + profile_name = ( + getattr(source, "profile", "") or "" + ).strip() or get_active_profile_name() + try: + from gateway.run import _profile_runtime_scope + + profile_home = self._resolve_profile_home_for_source(source) + with _profile_runtime_scope(profile_home): + display = display_hermes_home() + except Exception: + display = display_hermes_home() lines = [ t("gateway.profile.header", profile=profile_name), diff --git a/tests/gateway/test_status_command.py b/tests/gateway/test_status_command.py index cadeb9ca7068..3d5e15d079e5 100644 --- a/tests/gateway/test_status_command.py +++ b/tests/gateway/test_status_command.py @@ -672,6 +672,60 @@ async def test_profile_command_reports_custom_root_profile(monkeypatch, tmp_path assert f"**Home:** `{profile_home}`" in result +@pytest.mark.asyncio +async def test_profile_command_reports_source_stamped_profile(monkeypatch, tmp_path): + """On a multiplexed gateway, /profile reports the profile SERVING the + source (source.profile — URL prefix / per-credential adapter / room map), + not the multiplexer's active profile, which is always the default and + made /profile answer "default" in every persona chat.""" + hermes_home = tmp_path / ".hermes" + profile_home = hermes_home / "profiles" / "milo" + profile_home.mkdir(parents=True) + + session_entry = SessionEntry( + session_key=build_session_key(_make_source()), + session_id="sess-1", + created_at=datetime.now(), + updated_at=datetime.now(), + platform=Platform.TELEGRAM, + chat_type="dm", + ) + runner = _make_runner(session_entry) + monkeypatch.setenv("HERMES_HOME", str(hermes_home)) + + event = _make_event("/profile") + event.source.profile = "milo" + + result = await runner._handle_profile_command(event) + + assert "**Profile:** `milo`" in result + assert f"**Home:** `{profile_home}`" in result + + +@pytest.mark.asyncio +async def test_profile_command_unstamped_source_unchanged(monkeypatch, tmp_path): + """Single-profile behavior is untouched: an unstamped source reports the + active profile and the default home.""" + hermes_home = tmp_path / ".hermes" + hermes_home.mkdir() + + session_entry = SessionEntry( + session_key=build_session_key(_make_source()), + session_id="sess-1", + created_at=datetime.now(), + updated_at=datetime.now(), + platform=Platform.TELEGRAM, + chat_type="dm", + ) + runner = _make_runner(session_entry) + monkeypatch.setenv("HERMES_HOME", str(hermes_home)) + + result = await runner._handle_profile_command(_make_event("/profile")) + + assert "**Profile:** `default`" in result + assert f"**Home:** `{hermes_home}`" in result + + @pytest.mark.asyncio async def test_post_delivery_callback_generation_snapshot_happens_after_bind(): """Regression: the callback_generation snapshot in _process_message_background