repositories
loading repo index
repositories
loading repo index
repository
loading code, commits, and activity
public Clawd ADK gateway launch mirror
stars
latest
clone command
git clone gitlawb://did:key:z6Mkq5mY...iFZ5/my-project-publ...git clone gitlawb://did:key:z6Mkq5mY.../my-project-publ...2fa351d6docs: add automaton and perps launch sources15d ago| #1 | """Tests for MnemosyneMemoryProvider host-LLM lifecycle hooks. |
| #2 | |
| #3 | Covers decisions A6 (bounded on_session_end), C7 (shutdown unregisters |
| #4 | the host backend), and the registration flow added to initialize(). |
| #5 | """ |
| #6 | |
| #7 | from __future__ import annotations |
| #8 | |
| #9 | import json |
| #10 | import time |
| #11 | from unittest.mock import MagicMock, patch |
| #12 | |
| #13 | import pytest |
| #14 | |
| #15 | from hermes_memory_provider import MnemosyneMemoryProvider |
| #16 | from mnemosyne.core.llm_backends import get_host_llm_backend |
| #17 | |
| #18 | |
| #19 | # --------------------------------------------------------------------------- |
| #20 | # initialize() registration |
| #21 | # --------------------------------------------------------------------------- |
| #22 | |
| #23 | def test_initialize_registers_host_llm_when_register_returns_true(monkeypatch): |
| #24 | provider = MnemosyneMemoryProvider() |
| #25 | # Stub BeamMemory so we don't touch the filesystem. |
| #26 | monkeypatch.setattr("hermes_memory_provider._get_beam_class", lambda: lambda **kwargs: MagicMock()) |
| #27 | # Stub the registration call so the test does not depend on the real |
| #28 | # adapter behavior — we only verify the hook is invoked and survives. |
| #29 | with patch("hermes_memory_provider.hermes_llm_adapter.register_hermes_host_llm", return_value=True) as mock_reg: |
| #30 | provider.initialize(session_id="test-session") |
| #31 | mock_reg.assert_called_once() |
| #32 | |
| #33 | |
| #34 | def test_initialize_does_not_fail_when_register_raises(monkeypatch): |
| #35 | provider = MnemosyneMemoryProvider() |
| #36 | monkeypatch.setattr("hermes_memory_provider._get_beam_class", lambda: lambda **kwargs: MagicMock()) |
| #37 | with patch( |
| #38 | "hermes_memory_provider.hermes_llm_adapter.register_hermes_host_llm", |
| #39 | side_effect=RuntimeError("boom"), |
| #40 | ): |
| #41 | # Must not raise. |
| #42 | provider.initialize(session_id="test-session") |
| #43 | # initialize() is allowed to leave _beam set even when registration explodes. |
| #44 | assert provider._beam is not None |
| #45 | |
| #46 | |
| #47 | def test_initialize_does_not_fail_when_register_returns_false(monkeypatch): |
| #48 | provider = MnemosyneMemoryProvider() |
| #49 | monkeypatch.setattr("hermes_memory_provider._get_beam_class", lambda: lambda **kwargs: MagicMock()) |
| #50 | with patch("hermes_memory_provider.hermes_llm_adapter.register_hermes_host_llm", return_value=False): |
| #51 | provider.initialize(session_id="test-session") |
| #52 | assert provider._beam is not None |
| #53 | |
| #54 | |
| #55 | def test_initialize_skips_for_non_primary_context(monkeypatch): |
| #56 | """REGRESSION: subagent/cron/flush contexts still skip initialization entirely.""" |
| #57 | provider = MnemosyneMemoryProvider() |
| #58 | with patch("hermes_memory_provider.hermes_llm_adapter.register_hermes_host_llm") as mock_reg: |
| #59 | provider.initialize(session_id="x", agent_context="cron") |
| #60 | mock_reg.assert_not_called() |
| #61 | assert provider._beam is None |
| #62 | |
| #63 | |
| #64 | # --------------------------------------------------------------------------- |
| #65 | # shutdown() unregistration (decision C7) |
| #66 | # --------------------------------------------------------------------------- |
| #67 | |
| #68 | def test_shutdown_clears_host_backend(monkeypatch): |
| #69 | """After shutdown(), the host LLM backend must be unregistered.""" |
| #70 | from hermes_memory_provider import hermes_llm_adapter |
| #71 | |
| #72 | provider = MnemosyneMemoryProvider() |
| #73 | # Manually register to simulate a live session. |
| #74 | hermes_llm_adapter.register_hermes_host_llm() |
| #75 | assert get_host_llm_backend() is not None |
| #76 | |
| #77 | provider.shutdown() |
| #78 | assert get_host_llm_backend() is None |
| #79 | assert provider._beam is None |
| #80 | |
| #81 | |
| #82 | def test_shutdown_swallows_unregister_failure(monkeypatch): |
| #83 | """If unregistering raises, shutdown() must still complete.""" |
| #84 | provider = MnemosyneMemoryProvider() |
| #85 | with patch( |
| #86 | "hermes_memory_provider.hermes_llm_adapter.unregister_hermes_host_llm", |
| #87 | side_effect=RuntimeError("boom"), |
| #88 | ): |
| #89 | provider.shutdown() # must not raise |
| #90 | assert provider._beam is None |
| #91 | |
| #92 | |
| #93 | # --------------------------------------------------------------------------- |
| #94 | # on_session_end() bounded daemon thread (decision A6) |
| #95 | # --------------------------------------------------------------------------- |
| #96 | |
| #97 | def _make_provider_with_blocking_sleep(sleep_duration: float, timeout: float = 0.5): |
| #98 | """Build a provider whose _beam.sleep() blocks for `sleep_duration` seconds. |
| #99 | |
| #100 | The provider's join timeout is shortened to keep the test suite fast. |
| #101 | """ |
| #102 | beam = MagicMock() |
| #103 | beam.sleep.side_effect = lambda: time.sleep(sleep_duration) |
| #104 | provider = MnemosyneMemoryProvider() |
| #105 | provider._beam = beam |
| #106 | provider.SESSION_END_SLEEP_TIMEOUT_SECONDS = timeout |
| #107 | return provider, beam |
| #108 | |
| #109 | |
| #110 | def test_on_session_end_returns_within_timeout_when_sleep_blocks(): |
| #111 | """A6 contract: blocking sleep must not block on_session_end past the join cap.""" |
| #112 | # Production timeout is 15s; test uses 0.5s for speed and a 5s outer ceiling. |
| #113 | provider, beam = _make_provider_with_blocking_sleep(sleep_duration=5.0, timeout=0.5) |
| #114 | |
| #115 | start = time.monotonic() |
| #116 | provider.on_session_end(messages=[]) |
| #117 | elapsed = time.monotonic() - start |
| #118 | |
| #119 | # 0.5s join cap + slack. A regression making on_session_end synchronous |
| #120 | # would take ~5s here. |
| #121 | assert elapsed < 2.0, f"on_session_end took {elapsed:.2f}s, expected <2s" |
| #122 | beam.sleep.assert_called_once() |
| #123 | |
| #124 | |
| #125 | def test_on_session_end_logs_warning_on_timeout(caplog): |
| #126 | provider, _ = _make_provider_with_blocking_sleep(sleep_duration=5.0, timeout=0.5) |
| #127 | with caplog.at_level("WARNING", logger="hermes_memory_provider"): |
| #128 | provider.on_session_end(messages=[]) |
| #129 | msgs = [r.getMessage() for r in caplog.records] |
| #130 | assert any("timed out" in m for m in msgs), msgs |
| #131 | |
| #132 | |
| #133 | def test_session_end_timeout_default_matches_design(): |
| #134 | """The production default should remain 15s (decision A6).""" |
| #135 | assert MnemosyneMemoryProvider.SESSION_END_SLEEP_TIMEOUT_SECONDS == 15 |
| #136 | |
| #137 | |
| #138 | def test_on_session_end_completes_when_sleep_is_fast(): |
| #139 | """Fast sleep must be allowed to finish; no warning emitted.""" |
| #140 | beam = MagicMock() |
| #141 | # No-op sleep returns immediately. |
| #142 | beam.sleep.return_value = None |
| #143 | provider = MnemosyneMemoryProvider() |
| #144 | provider._beam = beam |
| #145 | |
| #146 | provider.on_session_end(messages=[]) |
| #147 | beam.sleep.assert_called_once() |
| #148 | |
| #149 | |
| #150 | def test_on_session_end_no_op_without_beam(): |
| #151 | """REGRESSION: on_session_end skips work entirely when not initialized.""" |
| #152 | provider = MnemosyneMemoryProvider() |
| #153 | provider._beam = None |
| #154 | # Must not raise. |
| #155 | provider.on_session_end(messages=[]) |
| #156 | |
| #157 | |
| #158 | def test_on_session_end_logs_when_sleep_raises_in_daemon_thread(caplog): |
| #159 | """Codex finding 3: exceptions from beam.sleep() now happen in the daemon |
| #160 | thread; the wrapper must catch them and log at DEBUG instead of letting |
| #161 | the traceback escape uncaught.""" |
| #162 | beam = MagicMock() |
| #163 | beam.sleep.side_effect = RuntimeError("synthetic explosion") |
| #164 | provider = MnemosyneMemoryProvider() |
| #165 | provider._beam = beam |
| #166 | provider.SESSION_END_SLEEP_TIMEOUT_SECONDS = 1.0 # plenty of time for the raise to happen |
| #167 | |
| #168 | with caplog.at_level("DEBUG", logger="hermes_memory_provider"): |
| #169 | provider.on_session_end(messages=[]) |
| #170 | # Wait for daemon thread to fully run the wrapper. |
| #171 | if provider._session_end_thread is not None: |
| #172 | provider._session_end_thread.join(timeout=2.0) |
| #173 | msgs = [r.getMessage() for r in caplog.records] |
| #174 | assert any("session-end sleep failed" in m and "synthetic explosion" in m for m in msgs), msgs |
| #175 | |
| #176 | |
| #177 | def test_shutdown_drains_in_flight_session_end_thread(caplog): |
| #178 | """Codex finding 4: shutdown() must briefly wait for an in-flight |
| #179 | session_end thread before clearing the host backend, otherwise the |
| #180 | daemon thread's late host call sees backend=None and degrades to remote.""" |
| #181 | from hermes_memory_provider import hermes_llm_adapter |
| #182 | |
| #183 | # Make the session_end thread block for ~0.4s — longer than the |
| #184 | # session_end timeout (0.1s) but well within the shutdown drain. |
| #185 | beam = MagicMock() |
| #186 | sleep_started = [] |
| #187 | sleep_finished = [] |
| #188 | |
| #189 | def slow_sleep(): |
| #190 | sleep_started.append(True) |
| #191 | time.sleep(0.4) |
| #192 | sleep_finished.append(True) |
| #193 | |
| #194 | beam.sleep.side_effect = slow_sleep |
| #195 | provider = MnemosyneMemoryProvider() |
| #196 | provider._beam = beam |
| #197 | provider.SESSION_END_SLEEP_TIMEOUT_SECONDS = 0.1 |
| #198 | provider.SHUTDOWN_DRAIN_TIMEOUT_SECONDS = 1.0 |
| #199 | |
| #200 | # Start session_end (returns after 0.1s; daemon keeps running) |
| #201 | provider.on_session_end(messages=[]) |
| #202 | assert provider._session_end_thread is not None |
| #203 | assert provider._session_end_thread.is_alive(), "daemon should still be running" |
| #204 | |
| #205 | # Register the host backend so we can observe it being cleared |
| #206 | hermes_llm_adapter.register_hermes_host_llm() |
| #207 | assert get_host_llm_backend() is not None |
| #208 | |
| #209 | # Shutdown should drain the in-flight thread BEFORE clearing the backend |
| #210 | provider.shutdown() |
| #211 | |
| #212 | # By now the daemon thread should have finished |
| #213 | assert sleep_started, "daemon should have started" |
| #214 | assert sleep_finished, "shutdown should have drained the in-flight daemon" |
| #215 | assert get_host_llm_backend() is None # cleared after drain |
| #216 | |
| #217 | |
| #218 | def test_shutdown_proceeds_when_drain_times_out(caplog): |
| #219 | """If the drain takes longer than SHUTDOWN_DRAIN_TIMEOUT_SECONDS, shutdown |
| #220 | proceeds (we don't want shutdown to block indefinitely either).""" |
| #221 | from hermes_memory_provider import hermes_llm_adapter |
| #222 | |
| #223 | beam = MagicMock() |
| #224 | beam.sleep.side_effect = lambda: time.sleep(5.0) |
| #225 | provider = MnemosyneMemoryProvider() |
| #226 | provider._beam = beam |
| #227 | provider.SESSION_END_SLEEP_TIMEOUT_SECONDS = 0.05 |
| #228 | provider.SHUTDOWN_DRAIN_TIMEOUT_SECONDS = 0.2 |
| #229 | |
| #230 | provider.on_session_end(messages=[]) |
| #231 | hermes_llm_adapter.register_hermes_host_llm() |
| #232 | |
| #233 | start = time.monotonic() |
| #234 | with caplog.at_level("DEBUG", logger="hermes_memory_provider"): |
| #235 | provider.shutdown() |
| #236 | elapsed = time.monotonic() - start |
| #237 | |
| #238 | # Total shutdown should be bounded by drain timeout + small slack. |
| #239 | assert elapsed < 1.0, f"shutdown took {elapsed:.2f}s, expected <1s" |
| #240 | assert get_host_llm_backend() is None |
| #241 | msgs = [r.getMessage() for r in caplog.records] |
| #242 | assert any("session-end thread still running" in m for m in msgs), msgs |
| #243 | |
| #244 | |
| #245 | def test_shutdown_drain_default_matches_design(): |
| #246 | """Production drain default should remain 2s.""" |
| #247 | assert MnemosyneMemoryProvider.SHUTDOWN_DRAIN_TIMEOUT_SECONDS == 2 |
| #248 | |
| #249 | |
| #250 | # --------------------------------------------------------------------------- |
| #251 | # C12.b — REMEMBER_SCHEMA + _handle_remember per-call kwargs parity |
| #252 | # --------------------------------------------------------------------------- |
| #253 | # |
| #254 | # BeamMemory.remember() accepts extract, metadata, veracity per call. The |
| #255 | # plugin's REMEMBER_SCHEMA used to only expose content/importance/source/ |
| #256 | # scope/valid_until/extract_entities, so callers passing any of the missing |
| #257 | # fields had them silently stripped: |
| #258 | # - extract=True (LLM fact-triple extraction): facts never extracted |
| #259 | # - metadata={...} (source/tag tracking): provenance lost |
| #260 | # - veracity="stated"/"tool"/...: every plugin memory defaulted to "unknown", |
| #261 | # defeating the veracity boost in recall |
| #262 | # These tests lock the schema → handler → beam wiring. |
| #263 | |
| #264 | def test_remember_schema_advertises_extract_and_metadata_and_veracity(): |
| #265 | """[C12.b] REMEMBER_SCHEMA must advertise the per-call kwargs that |
| #266 | beam.remember() actually supports, so Hermes' tool-arg validator |
| #267 | accepts them instead of stripping them as unknown fields.""" |
| #268 | from hermes_memory_provider import REMEMBER_SCHEMA |
| #269 | |
| #270 | props = REMEMBER_SCHEMA["parameters"]["properties"] |
| #271 | assert "extract" in props, ( |
| #272 | "REMEMBER_SCHEMA missing 'extract' — LLM fact-triple extraction " |
| #273 | "is unreachable through the plugin" |
| #274 | ) |
| #275 | assert "metadata" in props, ( |
| #276 | "REMEMBER_SCHEMA missing 'metadata' — caller-supplied tags / " |
| #277 | "source-doc IDs get silently dropped" |
| #278 | ) |
| #279 | assert "veracity" in props, ( |
| #280 | "REMEMBER_SCHEMA missing 'veracity' — every plugin-stored memory " |
| #281 | "defaults to 'unknown', defeating recall's veracity weighting" |
| #282 | ) |
| #283 | # Sanity-check the advertised types so a typo doesn't slip in. |
| #284 | assert props["extract"]["type"] == "boolean" |
| #285 | assert props["metadata"]["type"] == "object" |
| #286 | assert props["veracity"]["type"] == "string" |
| #287 | |
| #288 | |
| #289 | def test_handle_remember_passes_extract_metadata_veracity_to_beam(monkeypatch): |
| #290 | """[C12.b] _handle_remember must forward extract / metadata / veracity |
| #291 | to beam.remember(). Pre-fix the args were either ignored (no .get()) |
| #292 | or never wired into the beam call.""" |
| #293 | from hermes_memory_provider import MnemosyneMemoryProvider |
| #294 | |
| #295 | provider = MnemosyneMemoryProvider() |
| #296 | beam = MagicMock() |
| #297 | beam.remember.return_value = "mem-123" |
| #298 | provider._beam = beam |
| #299 | |
| #300 | args = { |
| #301 | "content": "Sarah leads Project Falcon, started 2026-04-01.", |
| #302 | "extract": True, |
| #303 | "metadata": {"source_doc": "kickoff-deck.pdf", "page": 3}, |
| #304 | "veracity": "stated", |
| #305 | } |
| #306 | provider._handle_remember(args) |
| #307 | |
| #308 | beam.remember.assert_called_once() |
| #309 | kwargs = beam.remember.call_args.kwargs |
| #310 | assert kwargs.get("extract") is True, ( |
| #311 | "extract=True was not forwarded to beam.remember — LLM fact " |
| #312 | "extraction is unreachable through the plugin tool" |
| #313 | ) |
| #314 | assert kwargs.get("metadata") == {"source_doc": "kickoff-deck.pdf", "page": 3}, ( |
| #315 | f"metadata not forwarded to beam.remember; got {kwargs.get('metadata')!r}" |
| #316 | ) |
| #317 | assert kwargs.get("veracity") == "stated", ( |
| #318 | f"veracity not forwarded to beam.remember; got {kwargs.get('veracity')!r}" |
| #319 | ) |
| #320 | |
| #321 | |
| #322 | def test_handle_remember_defaults_when_new_kwargs_omitted(monkeypatch): |
| #323 | """[C12.b] Pre-existing callers that don't pass the new kwargs must not |
| #324 | break: extract defaults False, metadata defaults None, veracity defaults |
| #325 | 'unknown'. Verifies the schema bump is backward-compatible.""" |
| #326 | from hermes_memory_provider import MnemosyneMemoryProvider |
| #327 | |
| #328 | provider = MnemosyneMemoryProvider() |
| #329 | beam = MagicMock() |
| #330 | beam.remember.return_value = "mem-456" |
| #331 | provider._beam = beam |
| #332 | |
| #333 | provider._handle_remember({"content": "minimal call"}) |
| #334 | |
| #335 | kwargs = beam.remember.call_args.kwargs |
| #336 | assert kwargs.get("extract", False) is False |
| #337 | # metadata may be None or absent; both are acceptable as "not set" |
| #338 | assert kwargs.get("metadata") in (None, {}), kwargs.get("metadata") |
| #339 | # veracity may be "unknown" (passed through) or absent (beam default) |
| #340 | assert kwargs.get("veracity", "unknown") == "unknown" |
| #341 | |
| #342 | |
| #343 | def test_handle_remember_clamps_unknown_veracity_to_unknown(monkeypatch, caplog): |
| #344 | """[C12.b — adversarial review] An LLM typo or a caller passing a |
| #345 | non-canonical veracity label (e.g. 'STATED' capitalization, 'state' |
| #346 | truncation, 'random_garbage') must be clamped to 'unknown' at the |
| #347 | trust boundary. Beam itself does not validate; the row would persist |
| #348 | with the junk label and pollute the contamination filter |
| #349 | (`veracity != 'stated'`). Locks the handler-side allowlist.""" |
| #350 | from hermes_memory_provider import MnemosyneMemoryProvider |
| #351 | |
| #352 | provider = MnemosyneMemoryProvider() |
| #353 | beam = MagicMock() |
| #354 | beam.remember.return_value = "mem-789" |
| #355 | provider._beam = beam |
| #356 | |
| #357 | # 'STATED' (capitalization) — should normalize to 'stated', not clamp. |
| #358 | provider._handle_remember({"content": "x", "veracity": "STATED"}) |
| #359 | assert beam.remember.call_args.kwargs.get("veracity") == "stated" |
| #360 | |
| #361 | # 'state' (truncated) — not in allowlist, must clamp to 'unknown'. |
| #362 | beam.remember.reset_mock() |
| #363 | with caplog.at_level("WARNING", logger="hermes_memory_provider"): |
| #364 | provider._handle_remember({"content": "y", "veracity": "state"}) |
| #365 | assert beam.remember.call_args.kwargs.get("veracity") == "unknown" |
| #366 | assert any("unknown veracity" in r.getMessage() for r in caplog.records), ( |
| #367 | "handler should log a warning when clamping a bad veracity label" |
| #368 | ) |
| #369 | |
| #370 | # 'random_garbage' — not in allowlist, must clamp to 'unknown'. |
| #371 | beam.remember.reset_mock() |
| #372 | provider._handle_remember({"content": "z", "veracity": "random_garbage"}) |
| #373 | assert beam.remember.call_args.kwargs.get("veracity") == "unknown" |
| #374 | |
| #375 | |
| #376 | def test_handle_remember_response_echoes_metadata(monkeypatch): |
| #377 | """[C12.b — adversarial review] The response JSON echoes extract / |
| #378 | extract_entities / veracity already; metadata should be in the same |
| #379 | surface for symmetry so callers can confirm what got applied.""" |
| #380 | from hermes_memory_provider import MnemosyneMemoryProvider |
| #381 | |
| #382 | provider = MnemosyneMemoryProvider() |
| #383 | beam = MagicMock() |
| #384 | beam.remember.return_value = "mem-meta" |
| #385 | provider._beam = beam |
| #386 | |
| #387 | payload = {"content": "x", "metadata": {"source_doc": "deck.pdf", "page": 7}} |
| #388 | response = provider._handle_remember(payload) |
| #389 | parsed = json.loads(response) |
| #390 | assert parsed.get("metadata") == {"source_doc": "deck.pdf", "page": 7}, ( |
| #391 | f"response missing metadata echo: {parsed!r}" |
| #392 | ) |
| #393 | |
| #394 | |
| #395 | # --------------------------------------------------------------------------- |
| #396 | # Issue #45 followup — RECALL_SCHEMA + _handle_recall scoring weight forwarding |
| #397 | # --------------------------------------------------------------------------- |
| #398 | # |
| #399 | # Adversarial review of issue #45's PR caught that the Hermes-side recall |
| #400 | # surface here also drops vec_weight / fts_weight / importance_weight. The |
| #401 | # RECALL_SCHEMA's description literally says "50% vector + 30% FTS5 + 20% |
| #402 | # importance" but never lets clients tune those weights. Same shape as the |
| #403 | # C12.b REMEMBER fix in this same file — schema mismatch with what |
| #404 | # BeamMemory.recall actually accepts. |
| #405 | |
| #406 | def test_recall_schema_advertises_scoring_weights(): |
| #407 | """[issue #45 followup] RECALL_SCHEMA must advertise the per-call scoring |
| #408 | weights that BeamMemory.recall accepts (beam.py:1296-1298) so Hermes' |
| #409 | tool-arg validator accepts them instead of stripping as unknown fields.""" |
| #410 | from hermes_memory_provider import RECALL_SCHEMA |
| #411 | |
| #412 | props = RECALL_SCHEMA["parameters"]["properties"] |
| #413 | for key in ("vec_weight", "fts_weight", "importance_weight"): |
| #414 | assert key in props, ( |
| #415 | f"RECALL_SCHEMA missing {key!r} — schema description claims " |
| #416 | f"'50% vector + 30% FTS5 + 20% importance' but never lets the " |
| #417 | f"client tune those weights" |
| #418 | ) |
| #419 | assert props[key]["type"] == "number" |
| #420 | |
| #421 | |
| #422 | def test_handle_recall_forwards_scoring_weights_to_beam(monkeypatch): |
| #423 | """[issue #45 followup] _handle_recall must forward vec_weight / |
| #424 | fts_weight / importance_weight when the caller supplies them, so the |
| #425 | schema-advertised tuning actually takes effect on ranking.""" |
| #426 | from hermes_memory_provider import MnemosyneMemoryProvider |
| #427 | |
| #428 | provider = MnemosyneMemoryProvider() |
| #429 | beam = MagicMock() |
| #430 | beam.recall.return_value = [] |
| #431 | provider._beam = beam |
| #432 | |
| #433 | provider._handle_recall({ |
| #434 | "query": "anything", |
| #435 | "limit": 3, |
| #436 | "vec_weight": 0.55, |
| #437 | "fts_weight": 0.25, |
| #438 | "importance_weight": 0.20, |
| #439 | }) |
| #440 | |
| #441 | kwargs = beam.recall.call_args.kwargs |
| #442 | assert kwargs.get("vec_weight") == 0.55, ( |
| #443 | f"_handle_recall did not forward vec_weight; kwargs={kwargs!r}" |
| #444 | ) |
| #445 | assert kwargs.get("fts_weight") == 0.25 |
| #446 | assert kwargs.get("importance_weight") == 0.20 |
| #447 | |
| #448 | |
| #449 | def test_handle_recall_omits_weights_when_caller_does_not_supply(): |
| #450 | """[issue #45 followup] When caller omits the weight kwargs, the handler |
| #451 | must NOT pass spurious values to beam.recall — beam treats None as |
| #452 | "fall back to env var or default" via _normalize_weights, and forcing |
| #453 | 0.0 / 0.5 / etc. would override that resolution.""" |
| #454 | from hermes_memory_provider import MnemosyneMemoryProvider |
| #455 | |
| #456 | provider = MnemosyneMemoryProvider() |
| #457 | beam = MagicMock() |
| #458 | beam.recall.return_value = [] |
| #459 | provider._beam = beam |
| #460 | |
| #461 | provider._handle_recall({"query": "anything", "limit": 3}) |
| #462 | |
| #463 | kwargs = beam.recall.call_args.kwargs |
| #464 | # Acceptable: the kwarg is not in beam.recall's call OR is explicitly None. |
| #465 | # Failing path: a numeric default like 0.5 / 0.0 leaked through. |
| #466 | for key in ("vec_weight", "fts_weight", "importance_weight"): |
| #467 | val = kwargs.get(key, "OMITTED") |
| #468 | assert val in (None, "OMITTED"), ( |
| #469 | f"_handle_recall forwarded {key}={val!r} when caller omitted it; " |
| #470 | f"this overrides beam's env/default resolution. Either pass None " |
| #471 | f"or omit the kwarg entirely." |
| #472 | ) |
| #473 |