diff --git a/scripts/release.py b/scripts/release.py index ce8018f99..76460d26b 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -234,6 +234,7 @@ AUTHOR_MAP = { "157689911+itsflownium@users.noreply.github.com": "itsflownium", "dirtyren@users.noreply.github.com": "dirtyren", "153708448+hunjaiboy@users.noreply.github.com": "yyzquwu", # PR #47567 salvage (Matrix: register inbound handlers with wait_sync=True so _dispatch_sync's gather awaits them; without it mautrix fire-and-forgets and inbound intake has no completion point) + "jearnest@velocityenergy.com": "jearnest11", # PR #48700 salvage (multi-profile gateway flap: use node symlink's own parent, not .resolve() target, when building systemd/launchd service PATH so one profile's node path can't leak into every unit and force a perpetual daemon-reload restart loop) "tgmerritt@gmail.com": "tgmerritt", # PR #43553 salvage (parse vLLM's token-based output-cap error format so over-cap max_tokens 400s reduce the output cap instead of death-looping into compression) "13277570+justin-cyhuang@users.noreply.github.com": "justin-cyhuang", "agent@tranquil-flow.dev": "Tranquil-Flow", diff --git a/tests/hermes_cli/test_gateway_service.py b/tests/hermes_cli/test_gateway_service.py index 9ac0baf5f..747016592 100644 --- a/tests/hermes_cli/test_gateway_service.py +++ b/tests/hermes_cli/test_gateway_service.py @@ -447,6 +447,48 @@ class TestGeneratedSystemdUnits: assert "/home/test/.nvm/versions/node/v24.14.0/bin" in unit + def test_user_unit_does_not_leak_profile_node_symlink_target(self, tmp_path, monkeypatch): + # Regression for the multi-profile gateway restart-loop flap (#48700): + # ~/.local/bin/node is often a symlink into a *specific* profile's node + # install. The generated unit's PATH must contain the symlink's own + # directory (~/.local/bin), NOT the resolved profile target — otherwise + # one profile's node path leaks into every profile's unit, making + # systemd_unit_is_current() perpetually false and forcing a + # daemon-reload restart loop on every boot. + local_bin = tmp_path / ".local" / "bin" + profile_node_bin = tmp_path / ".hermes" / "profiles" / "jarvis" / "node" / "bin" + local_bin.mkdir(parents=True) + profile_node_bin.mkdir(parents=True) + real_node = profile_node_bin / "node" + real_node.write_text("#!/bin/sh\n") + link_node = local_bin / "node" + link_node.symlink_to(real_node) + + monkeypatch.setattr(gateway_cli.shutil, "which", lambda cmd: str(link_node) if cmd == "node" else None) + + unit = gateway_cli.generate_systemd_unit(system=False) + + assert str(local_bin) in unit + assert str(profile_node_bin) not in unit + + def test_launchd_plist_does_not_leak_profile_node_symlink_target(self, tmp_path, monkeypatch): + # Same #48700 regression for the macOS twin generate_launchd_plist(). + local_bin = tmp_path / ".local" / "bin" + profile_node_bin = tmp_path / ".hermes" / "profiles" / "jarvis" / "node" / "bin" + local_bin.mkdir(parents=True) + profile_node_bin.mkdir(parents=True) + real_node = profile_node_bin / "node" + real_node.write_text("#!/bin/sh\n") + link_node = local_bin / "node" + link_node.symlink_to(real_node) + + monkeypatch.setattr(gateway_cli.shutil, "which", lambda cmd: str(link_node) if cmd == "node" else None) + + plist = gateway_cli.generate_launchd_plist() + + assert str(local_bin) in plist + assert str(profile_node_bin) not in plist + def test_user_unit_includes_wsl_windows_interop_paths(self, monkeypatch): monkeypatch.setattr(gateway_cli, "is_wsl", lambda: True) monkeypatch.setenv(