n4nAI

Testing whether agent memory actually persists

A practical how-to for testing AI agent memory persistence across sessions, with runnable code to verify state survives restarts and model swaps.

n4n Team3 min read669 words

Audio narration

Coming soon — every post will get a voice note here.

Most agents claim to remember, but few teams actually verify it. Testing AI agent memory persistence is less about unit tests for a database and more about end-to-end behavioral checks that survive process restarts and model swaps.

If you ship an agent that forgets a user’s name after a redeploy, that’s a regression no prompt tweak will fix. This guide walks through a concrete test harness you can drop into CI.

Why memory tests fail silently

Memory bugs hide because the happy path usually works. The agent talks to the same process, the same in-memory dict, and recall looks fine. Then you scale out, crash, or rotate the model, and the context is gone.

The fix is to treat persistence as a contract. You define what must survive, then write a test that breaks the process on purpose.

Step 1: Define the memory contract

Before writing tests, decide the state boundary. Is memory per-user, per-session, or per-thread? What keys are mandatory?

Write a minimal interface so the test doesn’t care about the backend.

from abc import ABC, abstractmethod

class MemoryStore(ABC):
    @abstractmethod
    def save(self, user_id: str, key: str, value: str) -> None: ...
    @abstractmethod
    def load(self, user_id: str, key: str) -> str | None: ...

A file-backed implementation makes “restart” trivial to simulate:

import json, os

class FileMemoryStore(MemoryStore):
    def __init__(self, path: str):
        self.path = path
        if not os.path.exists(path):
            json.dump({}, open(path, "w"))

    def save(self, user_id: str, key: str, value: str) -> None:
        data = json.load(open(self.path))
        data.setdefault(user_id, {})[key] = value
        json.dump(data, open(self.path, "w"))

    def load(self, user_id: str, key: str) -> str | None:
        data = json.load(open(self.path))
        return data.get(user_id, {}).get(key)

The contract is now: anything written via save with a user_id must be readable after a new process opens the same path.

Step 2: Give every test run a stable identity

Random UUIDs in tests create non-reproducible flake. Use a fixed user id scoped to the test name.

import pytest

@pytest.fixture
def user_id():
    return "test-user-persistence-001"

The agent wrapper should accept the store and an OpenAI-compatible client:

from openai import OpenAI

class Agent:
    def __init__(self, client: OpenAI, memory: MemoryStore):
        self.client = client
        self.memory = memory

    def respond(self, user_id: str, message: str) -> str:
        history = self.memory.load(user_id, "history") or []
        history.append({"role": "user", "content": message})
        resp = self.client.chat.completions.create(
            model="gpt-4o-mini",
            messages=history
        )
        content = resp.choices[0].message.content
        history.append({"role": "assistant", "content": content})
        self.memory.save(user_id, "history", history)
        return content

Step 3: Write a two-turn recall probe

This is the core of testing AI agent memory persistence. Turn 1 establishes a fact. You destroy the agent instance, create a new one against the same store, and ask a question that requires the fact.

def test_recall_across_instances(user_id, tmp_path):
    store_path = tmp_path / "mem.json"
    store = FileMemoryStore(str(store_path))
    client = OpenAI()  # point base_url at your gateway in real runs

    agent1 = Agent(client, store)
    agent1.respond(user_id, "My name is Ada and I like Rust.")

    # simulate process exit: drop agent1, keep file
    agent2 = Agent(client, FileMemoryStore(str(store_path)))
    answer = agent2.respond(user_id, "What programming language do I like?")

    assert "rust" in answer.lower()

Verify success: The assertion passes only if agent2 loaded history from disk. If you swap FileMemoryStore for a pure dict and skip the file, the test fails—that’s the point.

Step 4: Test provider/model rotation without losing context

Memory should not depend on the model that generated it. When testing AI agent memory persistence under model changes, point the client at one OpenAI-compatible endpoint that fronts multiple providers. n4n.ai does this and adds automatic fallback when a provider is rate-limited, so a 429 never masquerades as a memory failure.

Force a model switch between turns by setting the model explicitly:

def test_recall_across_models(user_id, tmp_path):
    store = FileMemoryStore(str(tmp_path / "m.json"))
    client = OpenAI(base_url="https://api.n4n.ai/v1")  # example gateway

    a1 = Agent(client, store)
    a1.client = OpenAI(base_url="https://api.n4n.ai/v1")
    # turn 1 with model A
    a1.respond(user_id, "Remember: ticket #42 is urgent.")

    a2 = Agent(client, FileMemoryStore(str(tmp_path / "m.json")))
    a2.model = "claude-3-5-sonnet"  # different backend, same gateway
    ans = a2.respond(user_id, "Which ticket is urgent?")
    assert "42" in ans

If the gateway honors client routing directives, you can also send a header to pin a provider and confirm the memory layer is untouched by the routing decision.

Step 5: Inject faults and confirm recovery

A real persistence layer faces corrupted files, locked databases, and timeouts. Write a test that truncates the store mid-flight.

def test_corrupt_store_recovers(user_id, tmp_path):
    path = tmp_path / "c.json"
    path.write_text("{}")
    store = FileMemoryStore(str(path))

    agent = Agent(OpenAI(), store)
    agent.respond(user_id, "I prefer dark mode.")

    # corrupt the file
    path.write_text("{ broken")
    # new agent should not crash on load; treat as empty
    recovered = Agent(OpenAI(), FileMemoryStore(str(path)))
    ans = recovered.respond(user_id, "What theme do I use?")
    # either answers plausibly or says unknown, but no exception
    assert ans is not None

Your FileMemoryStore.load should catch json.JSONDecodeError and return None. That behavior is part of the contract.

Step 6: Add a regression check for cache hints

Providers support cache-control to avoid re-paying for long system prompts. If your memory injects a static prefix, forward the hint. Testing AI agent memory persistence should include a check that the hint is present on the wire.

def test_cache_control_forwarded(user_id, tmp_path):
    store = FileMemoryStore(str(tmp_path / "cache.json"))
    client = OpenAI()

    agent = Agent(client, store)
    # monkeypatch create to capture kwargs
    captured = {}
    client.chat.completions.create = lambda **kw: captured.update(kw) or _fake_resp()
    agent.respond(user_id, "hi")
    assert captured.get("extra_body", {}).get("cache_control") == {"type": "ephemeral"}

If the gateway forwards provider cache-control hints, this ensures your memory prefix isn’t silently re-sent at full cost every turn.

Step 7: Run it in CI with token guards

Nightly memory tests can burn tokens if a loop regresses. Wrap the suite with a hard cap using per-token usage metering from your gateway, or simply mock the client in unit runs and use live calls only in a tagged job.

# .github/workflows/memory.yml
jobs:
  memory-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: pip install openai pytest
      - run: pytest tests/memory --tag live  # live only on schedule

For local dev, mock the model so the test runs in milliseconds:

class FakeCompletions:
    def create(self, **kw):
        return _fake_resp()

def _fake_resp():
    class M: content = "rust"
    class C: message = M()
    class R: choices = [C()]
    return R()

Verification checklist

After following these steps, you have a real persistence test if all of the following hold:

  • A new agent instance with the same store returns prior facts.
  • Swapping models behind one endpoint does not drop history.
  • Corrupting the backing file degrades gracefully.
  • Cache hints survive the agent-to-provider boundary.
  • The suite runs in CI without manual seeding.

Testing AI agent memory persistence is not glamorous, but it’s the difference between a demo and a product. Write the file-backed test once, and every future refactor will tell you exactly when memory broke.

Tagsagent-memorytestingstate-managementqa

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All testing & qa for ai agents posts →