Files
hermes-agent/tests/gateway/test_matrix_recovery_key_scope.py
teknium1 1f87fea541 test: trim salvaged test additions to two invariants each
#109036 added six TELEGRAM_REACTIONS cases and #110111 five recovery-key-path
cases; keep the two contracts per fix (explicit env beats YAML; a scoped miss
returns the default) and drop the change-detector permutations.
2026-09-13 15:39:11 -07:00

120 lines
5.4 KiB
Python

"""Regression test for #69090: MATRIX_RECOVERY_KEY must honor the active
profile's secret scope under ``gateway.multiplex_profiles`` so that a
secondary profile resolves its own recovery key (not the default profile's),
otherwise E2EE cross-signing verification fails with "Key MAC does not match".
The fix routes the recovery-key read through ``_scoped_recovery_key()``,
which uses :func:`agent.secret_scope.get_secret` (scope-aware) and only falls
back to ``os.getenv`` for an *unscoped* read under multiplex — mirroring the
established Slack app-token pattern (#59739).
``MATRIX_RECOVERY_KEY_OUTPUT_FILE`` is the sibling of the recovery key itself
(read by ``_recovery_key_output_path()``) and was missed by the #69090 fix:
it still used a bare ``os.getenv``, so a secondary profile's freshly
bootstrapped recovery key would either not be written at all, or be written
to the default profile's configured path.
"""
import pytest
from agent import secret_scope as ss
from plugins.platforms.matrix.adapter import _recovery_key_output_path, _scoped_recovery_key
@pytest.fixture(autouse=True)
def _reset_multiplex():
"""Ensure each test starts and ends with multiplexing off (it's a global)."""
ss.set_multiplex_active(False)
yield
ss.set_multiplex_active(False)
class TestScopedRecoveryKey:
def test_multiplex_inactive_reads_environ(self, monkeypatch):
"""Default deployment: get_secret transparently reads os.environ."""
monkeypatch.setenv("MATRIX_RECOVERY_KEY", "default-profile-key")
assert _scoped_recovery_key() == "default-profile-key"
def test_multiplex_active_scoped_uses_scope_not_environ(self, monkeypatch):
"""Secondary profile under multiplex must resolve its own key.
This is the core regression: ``os.getenv`` would have returned the
default profile's key (from os.environ), failing verification.
"""
monkeypatch.setenv("MATRIX_RECOVERY_KEY", "default-profile-key")
ss.set_multiplex_active(True)
token = ss.set_secret_scope({"MATRIX_RECOVERY_KEY": "secondary-profile-key"})
try:
assert _scoped_recovery_key() == "secondary-profile-key"
finally:
ss.reset_secret_scope(token)
def test_multiplex_active_unscoped_falls_back_to_environ(self, monkeypatch):
"""Default-profile startup loop under multiplex: unscoped read is fine.
An unscoped read raises ``UnscopedSecretError``; in that context
os.environ holds that profile's own value, so we fall back to it rather
than crashing startup. This matches the Slack adapter's behavior.
"""
monkeypatch.setenv("MATRIX_RECOVERY_KEY", "default-profile-key")
ss.set_multiplex_active(True)
# No secret scope installed -> get_secret raises UnscopedSecretError.
assert _scoped_recovery_key() == "default-profile-key"
def test_multiplex_active_scoped_missing_key_is_empty(self, monkeypatch):
"""A scope without the key must NOT fall through to another profile's env.
If the secondary profile hasn't configured a recovery key, the scope is
authoritative: we return empty rather than silently borrowing the
default profile's key (which would fail verification with a confusing
"Key MAC does not match").
"""
monkeypatch.setenv("MATRIX_RECOVERY_KEY", "default-profile-key")
ss.set_multiplex_active(True)
token = ss.set_secret_scope({"SOME_OTHER_KEY": "x"})
try:
assert _scoped_recovery_key() == ""
finally:
ss.reset_secret_scope(token)
def test_strips_whitespace(self, monkeypatch):
monkeypatch.setenv("MATRIX_RECOVERY_KEY", " padded-key \n")
assert _scoped_recovery_key() == "padded-key"
def test_unset_returns_empty(self, monkeypatch):
monkeypatch.delenv("MATRIX_RECOVERY_KEY", raising=False)
assert _scoped_recovery_key() == ""
class TestScopedRecoveryKeyOutputPath:
def test_multiplex_active_scoped_uses_scope_not_environ(self, monkeypatch, tmp_path):
"""Secondary profile under multiplex must resolve its own output path.
A bare ``os.getenv`` would have returned the default profile's path
(from os.environ), writing the secondary profile's freshly bootstrapped
recovery key to the wrong profile's file.
"""
default_path = tmp_path / "default-profile-key.txt"
secondary_path = tmp_path / "secondary-profile-key.txt"
monkeypatch.setenv("MATRIX_RECOVERY_KEY_OUTPUT_FILE", str(default_path))
ss.set_multiplex_active(True)
token = ss.set_secret_scope(
{"MATRIX_RECOVERY_KEY_OUTPUT_FILE": str(secondary_path)}
)
try:
assert _recovery_key_output_path() == secondary_path
finally:
ss.reset_secret_scope(token)
def test_multiplex_active_scoped_missing_key_is_none(self, monkeypatch, tmp_path):
"""A scope without the setting must NOT fall through to another
profile's env — the secondary profile's key silently goes unwritten
instead of landing in the default profile's file."""
default_path = tmp_path / "default-profile-key.txt"
monkeypatch.setenv("MATRIX_RECOVERY_KEY_OUTPUT_FILE", str(default_path))
ss.set_multiplex_active(True)
token = ss.set_secret_scope({"SOME_OTHER_KEY": "x"})
try:
assert _recovery_key_output_path() is None
finally:
ss.reset_secret_scope(token)