File size: 10,391 Bytes
108e227
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
"""Regression tests: supermemory + mem0 memory providers must lazy-install
their SDKs like honcho/hindsight.

Both providers ship a third-party SDK (``supermemory`` / ``mem0ai``) that is
NOT a core dependency. Before this fix they imported the SDK directly with no
``tools.lazy_deps.ensure()`` preflight and had no ``LAZY_DEPS`` allowlist
entry. On the published Docker image the agent venv is sealed
(``HERMES_DISABLE_LAZY_INSTALLS=1``) and lazy installs are redirected to a
writable durable target (``HERMES_LAZY_INSTALL_TARGET``). honcho/hindsight
route through ``ensure()`` and therefore install fine on a hosted instance;
supermemory/mem0 never called it, so the SDK was never installed there and
the provider silently reported itself unavailable.

These tests pin the contract:

1. Both features are in the ``LAZY_DEPS`` allowlist (without an entry,
   ``ensure()`` raises ``FeatureUnavailable`` — the original silent-dark bug).
2. Each provider's SDK-import chokepoint actually calls ``ensure(<feature>)``.
3. supermemory's ``is_available()`` no longer gates on the SDK being
   importable (the chicken-and-egg trap that stopped the provider loading at
   all on a sealed venv, so ``initialize()``/``ensure()`` never ran).
4. The real sealed-venv durable-target gate accepts the new features (the
   exact hosted-Fly condition the user hit).

The pip subprocess is never actually run — ``_venv_pip_install`` /
``_is_satisfied`` are stubbed so we exercise the real ``ensure()`` control
flow without touching PyPI.
"""

from __future__ import annotations

import os

import pytest

import tools.lazy_deps as ld


MEMORY_FEATURES = ("memory.supermemory", "memory.mem0")


# ---------------------------------------------------------------------------
# 1. Allowlist contract — the core regression.
# ---------------------------------------------------------------------------


class TestAllowlistEntries:
    @pytest.mark.parametrize("feature", MEMORY_FEATURES)
    def test_feature_is_allowlisted(self, feature):
        # Without an allowlist entry, ensure() raises FeatureUnavailable with
        # "not in LAZY_DEPS" — which is exactly why the SDK never installed on
        # a hosted instance before this fix.
        assert feature in ld.LAZY_DEPS, (
            f"{feature!r} missing from LAZY_DEPS — its SDK can never "
            f"lazy-install on a sealed Docker venv."
        )


    def test_supermemory_spec_package(self):
        specs = ld.LAZY_DEPS["memory.supermemory"]
        assert any(ld._pkg_name_from_spec(s) == "supermemory" for s in specs)

    def test_mem0_spec_package(self):
        # mem0's pip package is ``mem0ai`` (imports as ``mem0``).
        specs = ld.LAZY_DEPS["memory.mem0"]
        assert any(ld._pkg_name_from_spec(s) == "mem0ai" for s in specs)

    @pytest.mark.parametrize("feature", MEMORY_FEATURES)
    def test_unknown_feature_would_raise_without_entry(self, feature, monkeypatch):
        # Demonstrate the failure mode the allowlist entry prevents: a feature
        # NOT in LAZY_DEPS raises rather than installing.
        monkeypatch.setattr(ld, "_allow_lazy_installs", lambda: True)
        with pytest.raises(ld.FeatureUnavailable, match="not in LAZY_DEPS"):
            ld.ensure(feature + ".typo", prompt=False)


# ---------------------------------------------------------------------------
# 2. Import sites call ensure().
# ---------------------------------------------------------------------------


class TestSupermemoryEnsureCalled:
    def test_client_construction_calls_ensure(self, monkeypatch):
        """_SupermemoryClient.__init__ must call ensure('memory.supermemory')
        before importing the SDK."""
        from plugins.memory.supermemory import _SupermemoryClient

        calls = []
        monkeypatch.setattr(
            ld, "ensure",
            lambda feature, **kw: calls.append((feature, kw)),
        )

        # Stub the SDK so construction doesn't need the real package. The
        # client does ``from supermemory import Supermemory`` right after
        # ensure(); inject a fake module.
        import sys
        import types

        fake = types.ModuleType("supermemory")
        fake.Supermemory = lambda **kw: object()
        monkeypatch.setitem(sys.modules, "supermemory", fake)

        _SupermemoryClient(api_key="k", timeout=5.0, container_tag="hermes")

        assert ("memory.supermemory", {"prompt": False}) in calls, (
            "supermemory client did not call ensure('memory.supermemory', "
            f"prompt=False); calls={calls}"
        )


class TestMem0EnsureCalled:
    def test_create_backend_calls_ensure(self, monkeypatch):
        """SupermemoryMemoryProvider-style mem0 provider must call
        ensure('memory.mem0') in _create_backend before importing the SDK."""
        from plugins.memory.mem0 import Mem0MemoryProvider

        calls = []
        monkeypatch.setattr(
            ld, "ensure",
            lambda feature, **kw: calls.append((feature, kw)),
        )

        prov = Mem0MemoryProvider()
        # Platform mode is the default; force a known mode and stub the backend
        # import so we isolate the ensure() call.
        prov._mode = "platform"
        prov._api_key = "k"

        import sys
        import types

        fake = types.ModuleType("mem0")
        fake.MemoryClient = lambda **kw: object()
        fake.Memory = object
        monkeypatch.setitem(sys.modules, "mem0", fake)
        # _backend imports ``from mem0 import MemoryClient`` lazily inside
        # PlatformBackend.__init__, so the fake module satisfies it.

        prov._create_backend()

        assert ("memory.mem0", {"prompt": False}) in calls, (
            f"mem0 _create_backend did not call ensure('memory.mem0', "
            f"prompt=False); calls={calls}"
        )


# ---------------------------------------------------------------------------
# 3. supermemory is_available() chicken-and-egg fix.
# ---------------------------------------------------------------------------


class TestSupermemoryIsAvailable:
    def test_available_with_key_even_when_sdk_absent(self, monkeypatch):
        """With the key set but the SDK not importable, is_available() must
        still return True — otherwise the provider never loads on a sealed
        venv and ensure() (which installs the SDK) never runs."""
        from plugins.memory.supermemory import SupermemoryMemoryProvider
        import builtins

        monkeypatch.setenv("SUPERMEMORY_API_KEY", "sk-test")

        # Make any attempt to import the SDK fail, simulating the
        # not-yet-installed sealed-venv state.
        real_import = builtins.__import__

        def _no_supermemory(name, *args, **kwargs):
            if name == "supermemory" or name.startswith("supermemory."):
                raise ImportError("No module named 'supermemory'")
            return real_import(name, *args, **kwargs)

        monkeypatch.setattr(builtins, "__import__", _no_supermemory)

        prov = SupermemoryMemoryProvider()
        assert prov.is_available() is True

    def test_unavailable_without_key(self, monkeypatch):
        from plugins.memory.supermemory import SupermemoryMemoryProvider

        monkeypatch.delenv("SUPERMEMORY_API_KEY", raising=False)
        prov = SupermemoryMemoryProvider()
        assert prov.is_available() is False


# ---------------------------------------------------------------------------
# 4. Real sealed-venv durable-target gate accepts the new features.
#
# This is the exact hosted-Fly condition: HERMES_DISABLE_LAZY_INSTALLS=1 seals
# the venv, but HERMES_LAZY_INSTALL_TARGET redirects installs to a writable
# durable dir, so installs are still ALLOWED. We exercise the real
# _allow_lazy_installs() + ensure() flow end-to-end with only the pip
# subprocess stubbed.
# ---------------------------------------------------------------------------


class TestSealedVenvDurableTarget:
    @pytest.mark.parametrize("feature", MEMORY_FEATURES)
    def test_ensure_installs_into_durable_target_on_sealed_venv(
        self, feature, monkeypatch, tmp_path
    ):
        # Sealed venv + durable target = the published Docker image config.
        monkeypatch.setenv("HERMES_DISABLE_LAZY_INSTALLS", "1")
        monkeypatch.setenv("HERMES_LAZY_INSTALL_TARGET", str(tmp_path / "lazy"))
        # config.yaml kill-switch left at default (allow).
        monkeypatch.setattr(
            "hermes_cli.config.load_config",
            lambda: {"security": {"allow_lazy_installs": True}},
        )

        # Real gate must permit installs because a durable target is set.
        assert ld._allow_lazy_installs() is True, (
            "sealed venv WITH a durable target must allow installs — this is "
            "the path honcho/hindsight use on hosted Fly instances"
        )

        # Drive ensure(): missing first, satisfied after the (stubbed) install.
        states = iter([False, True])
        monkeypatch.setattr(ld, "_is_satisfied", lambda spec: next(states))

        captured = {}

        def fake_install(specs, **kw):
            captured["specs"] = specs
            captured["target_env"] = os.environ.get("HERMES_LAZY_INSTALL_TARGET")
            return ld._InstallResult(True, "ok", "")

        monkeypatch.setattr(ld, "_venv_pip_install", fake_install)

        ld.ensure(feature, prompt=False)  # must not raise

        assert captured.get("specs") == ld.LAZY_DEPS[feature]
        assert captured.get("target_env"), (
            "install ran without the durable target env set"
        )

    @pytest.mark.parametrize("feature", MEMORY_FEATURES)
    def test_sealed_venv_without_target_blocks(self, feature, monkeypatch):
        # Sealed venv and NO durable target → installs blocked (can't mutate
        # the sealed venv). Belt-and-suspenders: confirms the gate still
        # protects the seal for these features.
        monkeypatch.setenv("HERMES_DISABLE_LAZY_INSTALLS", "1")
        monkeypatch.delenv("HERMES_LAZY_INSTALL_TARGET", raising=False)
        monkeypatch.setattr(
            "hermes_cli.config.load_config",
            lambda: {"security": {"allow_lazy_installs": True}},
        )
        monkeypatch.setattr(ld, "_is_satisfied", lambda spec: False)

        with pytest.raises(ld.FeatureUnavailable, match="lazy installs disabled"):
            ld.ensure(feature, prompt=False)