deer-flow/backend/tests/test_message_processing_signals.py
Eilen Shin aaca3eeaeb
refactor(agent): ground runtime decisions in structured evidence (#7986)
* refactor(agent): ground runtime decisions in structured evidence

Use producer status and structured errors for tool recovery, preserve memory context without lexical confirmation, and remove predetermined receipt and skill conclusions.

Require explicit MCP discovery and authenticated schedule trial actions. Preserve authorization, ownership, isolation, and bounded execution; update documentation and regression coverage.

* test(sandbox): expect read_file initialization error callback

* fix(agent): address evidence review follow-ups

* fix(agent): preserve retry budgets for OpenAI transport errors

* fix(agent): expose tool execution status in model requests

Project validated execution metadata and exact tool text into request-only JSON so status survives provider serialization. Preserve stored results and receipt hashes, cover SDK and graph boundaries, and migrate replay input hashes without changing recorded responses.

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-11-08 22:00:23 +08:00

134 lines
4.7 KiB
Python

"""Tests for message-processing signal detection and DeerMem preparation.
Pins that ``detect_signals`` recognizes all six signal classes (correction,
reinforcement, preference, identity, goal, decision), and ``_prepare_update``
returns the full signal set as a ``frozenset`` so each detected class reaches
the extraction prompt.
"""
from __future__ import annotations
from langchain_core.messages import AIMessage, HumanMessage
from deerflow.agents.memory.backends.deermem.deer_mem import DeerMem
from deerflow.agents.memory.backends.deermem.deermem.core.message_processing import (
detect_signals,
extract_message_text,
load_patterns,
)
def _human(text: str) -> HumanMessage:
return HumanMessage(content=text)
def _ai(text: str) -> AIMessage:
return AIMessage(content=text)
# ── detect_signals: the 6 signal classes ───────────────────────────────────
def test_detect_signals_correction() -> None:
assert "correction" in detect_signals([_human("That's wrong, use uv"), _ai("ok")])
def test_detect_signals_reinforcement() -> None:
assert "reinforcement" in detect_signals([_human("perfect, exactly right"), _ai("ok")])
def test_detect_signals_preference() -> None:
assert "preference" in detect_signals([_human("I prefer uv over pip"), _ai("ok")])
def test_detect_signals_identity() -> None:
assert "identity" in detect_signals([_human("I am an engineer"), _ai("ok")])
def test_detect_signals_goal() -> None:
assert "goal" in detect_signals([_human("I plan to migrate to uv"), _ai("ok")])
def test_detect_signals_decision() -> None:
assert "decision" in detect_signals([_human("let's go with uv"), _ai("ok")])
def test_detect_signals_none_for_substantive_turn() -> None:
assert detect_signals([_human("what is the weather"), _ai("sunny")]) == set()
def test_detect_signals_multiple_classes_in_one_turn() -> None:
# A turn that states both a preference and an identity surfaces both.
signals = detect_signals([_human("I am an engineer and I prefer uv"), _ai("ok")])
assert "identity" in signals
assert "preference" in signals
# ── _prepare_update: seam-stable 3-tuple projection ────────────────────────
def _make_deermem(tmp_path, **overrides) -> DeerMem:
cfg = {"storage_path": str(tmp_path)}
cfg.update(overrides)
return DeerMem(backend_config=cfg)
def test_prepare_update_keeps_short_turns(tmp_path) -> None:
m = _make_deermem(tmp_path)
messages = [_human("好的"), _ai("嗯")]
prepared = m._prepare_update(messages)
assert prepared is not None and prepared[0] == messages
def test_custom_patterns_dir_still_loads_signal_patterns(tmp_path) -> None:
pattern_file = tmp_path / "preference.yaml"
pattern_file.write_text("- 'I always prefer kiwi'\n", encoding="utf-8")
patterns = load_patterns("preference", patterns_dir=str(tmp_path))
assert len(patterns) == 1
assert patterns[0].search("I always prefer kiwi")
def test_prepare_update_returns_correction_signal(tmp_path) -> None:
m = _make_deermem(tmp_path)
r = m._prepare_update([_human("That's wrong, use uv"), _ai("ok")])
assert r is not None and len(r) == 2
_filtered, signals = r
assert "correction" in signals
def test_prepare_update_returns_reinforcement_signal(tmp_path) -> None:
m = _make_deermem(tmp_path)
r = m._prepare_update([_human("perfect, exactly right"), _ai("ok")])
assert r is not None and len(r) == 2
_filtered, signals = r
assert "reinforcement" in signals
def test_prepare_update_returns_new_signals_after_swap(tmp_path) -> None:
# After the signals-seam swap, the full signal set flows through (not just
# correction/reinforcement): a preference turn surfaces "preference".
m = _make_deermem(tmp_path)
r = m._prepare_update([_human("I prefer uv over pip"), _ai("ok")])
assert r is not None and len(r) == 2
_filtered, signals = r
assert "preference" in signals
assert len(_filtered) == 2 # not trivial -> kept
# ── extract_message_text: content shapes ───────────────────────────────────
def test_extract_message_text_treats_none_as_empty() -> None:
"""A content-less message must not become the literal string ``"None"``.
``str(None)`` is truthy, so the naive fallback would hand ``"None"`` to
signal detection and to the memory-update prompt as if it were real user
text. Mirrors ``utils/messages.py:message_content_to_text`` and
``test_mem0_memory_backend.py::test_extract_message_text_treats_none_as_empty``.
"""
msg = _human("")
msg.content = None
assert extract_message_text(msg) == ""