WalkXR-AI / tests / test_retrieve_context_node.py
test_retrieve_context_node.py
Raw

"""
Tests for the retrieve_context LangGraph node.

These tests focus on:
- Combining knowledge-base and user-memory context
- Retrieving user memory only on the first turn
- Normalizing both sources into the same retrieved_context shape

To run this test, enter the following code in the terminal:
    PYTHONPATH=src pytest tests/test_retrieve_context_node.py -q -s
"""

from __future__ import annotations
from walkxr_ai.core.nodes import build_retrieve_context_node


class FakeRetrievalEngine:
    """
    Fake knowledgebase retriever returning a fixed normalized result.
    """

    def retrieve(self, query: str) -> list[dict]:
        return [
            {
                "text": "Knowledge: grounding techniques help with anxiety.",
                "source": "knowledge_base",
                "score": 0.9,
                "metadata": {},
            }
        ]


class FakeNode:
    """
    Fake node that mimics the LlamaIndex interface used by the node.
    """

    def __init__(self, text: str, metadata: dict) -> None:
        self._text = text
        self.metadata = metadata

    def get_text(self) -> str:
        return self._text


class FakeResult:
    """
    Fake result that mimics a LlamaIndex retrieval result.
    """

    def __init__(self, text: str) -> None:
        self.node = FakeNode(text, {"user_id": "user_1"})
        self.score = 0.8


class FakeUserMemoryStore:
    """
    Fake user-memory store returning LlamaIndex-like result objects.
    """

    def query_memories(self, user_id: str, query: str):
        return [FakeResult("Memory: the user finds reflective walks relaxing.")]


def test_retrieve_context_combines_knowledge_and_memory_on_new_session() -> None:
    """
    Test #1:
    Test to verify that the knowledgebase (non-memory) context and user memory context 
    are combined on the first turn.
    """
    retrieve_context = build_retrieve_context_node(
        FakeRetrievalEngine(),
        FakeUserMemoryStore(),
    )

    state = {
        "user_input": "How can I relax?",
        "user_id": "user_1",
        "turn_index": 0,
    }

    result = retrieve_context(state)

    assert result["phase"] == "context_retrieved"                                           # Is the phase correct?
    assert any(item["source"] == "knowledge_base" for item in result["retrieved_context"])  # Is knowledgebase context included?
    assert any(item["source"] == "user_memory" for item in result["retrieved_context"])     # Is user memory context included?


def test_retrieve_context_skips_memory_after_first_turn() -> None:
    """
    Test #2:
    Test to verify that user-memory retrieval is skipped on subsequent turns after the first 
    turn of the session, even if user_id is present. This ensures that we only retrieve user 
    memory on the first turn, as intended.
    """
    retrieve_context = build_retrieve_context_node(
        FakeRetrievalEngine(),
        FakeUserMemoryStore(),
    )

    state = {
        "user_input": "How can I relax?",
        "user_id": "user_1",
        "turn_index": 1,
    }

    result = retrieve_context(state)

    assert result["phase"] == "context_retrieved"                                           # Is the phase correct?
    assert len(result["retrieved_context"]) == 1                                            # Is only one context item returned (the knowledgebase context)?
    assert all(item["source"] != "user_memory" for item in result["retrieved_context"])     # Is user memory context skipped?


def test_retrieve_context_skips_memory_if_no_user_id() -> None:
    """
    Test #3:
    Test to verify that user-memory retrieval is skipped if user_id is blank, even on the first turn. 
    This ensures that we only retrieve user memory when we have a valid user_id, as intended.
    """
    retrieve_context = build_retrieve_context_node(
        FakeRetrievalEngine(),
        FakeUserMemoryStore(),
    )

    state = {
        "user_input": "How can I relax?",
        "user_id": "",
        "turn_index": 0,
    }

    result = retrieve_context(state)

    assert result["phase"] == "context_retrieved"                                           # Is the phase correct?
    assert len(result["retrieved_context"]) == 1                                            # Is only one context item returned (the knowledgebase context)?
    assert all(item["source"] != "user_memory" for item in result["retrieved_context"])     # Is user memory context skipped?


def test_retrieve_context_returns_expected_structure() -> None:
    """
    Test #4:
    Test to verify that the retrieved context items from both knowledgebase and user memory 
    are returned in the expected normalized structure, with keys: text, source, score, and metadata. 
    This ensures that the node is correctly normalizing different context sources into a consistent 
    format for downstream nodes.
    """
    retrieve_context = build_retrieve_context_node(
        FakeRetrievalEngine(),
        FakeUserMemoryStore(),
    )

    state = {
        "user_input": "How can I relax?",
        "user_id": "user_1",
        "turn_index": 0,
    }

    result = retrieve_context(state)

    assert result["phase"] == "context_retrieved"   # Is the phase correct?
    for item in result["retrieved_context"]:        # Does each item have the expected keys?
        assert "text" in item
        assert "source" in item
        assert "score" in item
        assert "metadata" in item