<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: BAOFUFAN</title>
    <description>The latest articles on DEV Community by BAOFUFAN (@_eb7f2a654e97a60ae9f96e).</description>
    <link>https://dev.to/_eb7f2a654e97a60ae9f96e</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3903614%2F88f4214a-aed8-4e71-a7f1-a6aca8cfe579.jpg</url>
      <title>DEV Community: BAOFUFAN</title>
      <link>https://dev.to/_eb7f2a654e97a60ae9f96e</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/_eb7f2a654e97a60ae9f96e"/>
    <language>en</language>
    <item>
      <title>From 2 Hours to 3 Minutes: Automating LLM Memory Testing with Playwright + LangChain</title>
      <dc:creator>BAOFUFAN</dc:creator>
      <pubDate>Wed, 12 Aug 2026 01:04:06 +0000</pubDate>
      <link>https://dev.to/_eb7f2a654e97a60ae9f96e/from-2-hours-to-3-minutes-automating-llm-memory-testing-with-playwright-langchain-1c98</link>
      <guid>https://dev.to/_eb7f2a654e97a60ae9f96e/from-2-hours-to-3-minutes-automating-llm-memory-testing-with-playwright-langchain-1c98</guid>
      <description>&lt;p&gt;At 2 AM, my testing colleague’s WeChat voice call jolted me awake: “The conversation memory bug that drops context is back. I ran a full regression and still couldn’t capture a stable reproduction path.” Rubbing my eyes, I opened my laptop, manually launched five browser windows, typed “My name is Ming, remember that,” refreshed the page, asked “What’s my name?” and ticked off results against screenshots. That night I spent nearly two hours just to check whether a memory persistence feature had been broken again by a backend change. The thought that burned in my mind: &lt;strong&gt;Manually verifying memory in a browser is like mining with a hand shovel – painfully inefficient and borderline absurd.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Then I slammed the desk and wired up Playwright with LangChain, making the browser automatically chat, refresh, and ask questions, then letting an LLM decide “did it remember or not.” Regression time dropped from 2 hours to 3 minutes, while we managed to cover 30+ edge cases in the same run. This post shares the idea and complete code behind this “automated memory bodyguard.”&lt;/p&gt;




&lt;h2&gt;
  
  
  Breaking It Down: Why Manual Memory Persistence Testing Is a Nightmare
&lt;/h2&gt;

&lt;p&gt;The essence of “conversation memory persistence” in LLM applications is this: &lt;strong&gt;after the frontend is closed or refreshed, the session history and knowledge base remain alive.&lt;/strong&gt; The standard verification path is:  &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open the web page, chat with the bot for a few rounds, inject personal info (name, preferences, to-dos).
&lt;/li&gt;
&lt;li&gt;Close or refresh the page.
&lt;/li&gt;
&lt;li&gt;Send another message and visually check whether the response references the earlier information.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Running this manually takes at least 2–3 minutes per cycle. If the system under test uses a deeply customized LangChain Memory, a custom Redis store, or a conversation flow with complex routing, regression needs to cover 20+ conversation shapes (multi-turn, single-turn, mid-disconnect, memory after token trimming, etc.). Humans simply can’t keep up, and “eyeballing” whether the bot remembered is terribly unreliable – sometimes the bot just politely says “Hello,” and you have no idea if that reply came from memory or was randomly generated.&lt;/p&gt;

&lt;p&gt;The usual automation approaches are threefold:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Direct API calls&lt;/strong&gt;: bypasses the browser and can’t verify whether frontend cookies / localStorage / session data synced correctly to the backend.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Selenium + fixed assertions&lt;/strong&gt;: asserting “response contains ‘Ming’” misses tons of natural variations – the model might say “Your name is Ming, right?” Fixed pattern matching is practically blind.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fully manual&lt;/strong&gt;: slow, unrepeatable, error-prone.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This job requires &lt;strong&gt;a semantic judge that truly understands meaning, plus a robot that can drive a browser.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Solution Design: Playwright Drives the Browser, LangChain Judges
&lt;/h2&gt;

&lt;p&gt;I raided my toolbox:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why Playwright instead of Selenium?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Playwright’s auto-wait is far friendlier to modern SPAs – no more false timeouts when an element is in the DOM but not yet rendered. It also effortlessly manages multiple browser contexts, perfect for simulating “refresh and re-enter.” And its Python API mixes seamlessly with LangChain.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why LangChain for assertions?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Asserting “memory retained or not” is fundamentally a semantic judgment task. Instead of writing a hundred regular expressions, just hand it to ChatOpenAI (or any LLM) and let it respond “yes/no.” LangChain’s ChatModels and Prompt templates let you quickly build a reusable evaluator, and you can easily swap models for cost control.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Alternatives I deliberately skipped&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pure LLM API to compare texts: no coverage of the frontend link.&lt;/li&gt;
&lt;li&gt;Browser extension recording &amp;amp; playback: no intelligent assertions; any minor DOM change breaks everything.&lt;/li&gt;
&lt;li&gt;Robot Framework + custom keywords: high extension overhead; just using Python directly is more flexible.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The final architecture is dead simple: &lt;code&gt;Playwright script → browser simulates conversation &amp;amp; refresh → grabs the last reply → LangChain judge =&amp;gt; pass/fail&lt;/code&gt;, then collect results into a CLI report.&lt;/p&gt;


&lt;h2&gt;
  
  
  Core Implementation: Three Building Blocks That Form the Memory Bodyguard
&lt;/h2&gt;

&lt;p&gt;The runnable Python code below is split into three functions, tackling &lt;strong&gt;simulated conversation&lt;/strong&gt;, &lt;strong&gt;triggering the memory check flow&lt;/strong&gt;, and &lt;strong&gt;intelligent assertions&lt;/strong&gt; respectively.&lt;/p&gt;
&lt;h3&gt;
  
  
  1. Playwright Simulated Conversation – Chat the Bot into Position
&lt;/h3&gt;

&lt;p&gt;This code automatically inputs messages, clicks send, and waits for the assistant’s reply to appear. All selectors use common semantic placeholders; tweak them for your project and you’re good to go.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;playwright.sync_api&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sync_playwright&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;start_session_and_chat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;initial_messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    page is a Playwright Page object already on the chat page,
    initial_messages is a list of strings to send in order.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;initial_messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# Wait for input field to be interactive (SPA loading friendly)
&lt;/span&gt;        &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wait_for_selector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;textarea[placeholder=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;输入消息...&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;visible&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;textarea[placeholder=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;输入消息...&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;button:has-text(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;发送&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="c1"&gt;# Wait for at least one assistant message to appear before continuing, to avoid overlap
&lt;/span&gt;        &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wait_for_selector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.assistant-message:last-child&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Usage example: implant memory
&lt;/span&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;sync_playwright&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;pw&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;browser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chromium&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;launch&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new_page&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://localhost:3000/chat&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;start_session_and_chat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;你好&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;我叫小明，我最喜欢蓝色的跑鞋&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;wait_for_selector&lt;/code&gt; and &lt;code&gt;.assistant-message:last-child&lt;/code&gt; are key for handling asynchronous streaming replies. If your UI uses SSE, you might need additional DOM change listeners, but usually waiting for the latest message node is enough.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Memory Persistence Verification Flow – “Quiz” It after Refresh
&lt;/h3&gt;

&lt;p&gt;We deliberately inject a set of personal information, then refresh the page to simulate a disconnect, and ask a targeted question.&lt;/p&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
python
def verify_memory_after_reload(page, user_name: str, memory_keyword: str) -&amp;gt; bool:
    """
    Implant name and preference into the bot, refresh, then ask whether it remembers.
    user_name        : user name, used for questions like "What's my name?"
    memory_keyword   : expected keyword in the response (e.g., the name).
    """
    # Implant phase
    start_session_and_chat(page, [
        f"记住：我
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

</description>
      <category>python</category>
      <category>programming</category>
    </item>
    <item>
      <title>Automating LLM Agent Memory Testing with Playwright: 10x Efficiency Boost</title>
      <dc:creator>BAOFUFAN</dc:creator>
      <pubDate>Tue, 11 Aug 2026 01:04:14 +0000</pubDate>
      <link>https://dev.to/_eb7f2a654e97a60ae9f96e/automating-llm-agent-memory-testing-with-playwright-10x-efficiency-boost-2dno</link>
      <guid>https://dev.to/_eb7f2a654e97a60ae9f96e/automating-llm-agent-memory-testing-with-playwright-10x-efficiency-boost-2dno</guid>
      <description>&lt;p&gt;A while ago, our team took on a task: regression testing for an enterprise AI assistant, with a focus on its “memory” and context retention. In round 1 you tell it “My name is Zhang San and I work at ByteDance.” By round 5, when you ask “What’s my name? Which company do I work for?”, it must answer correctly. At first, three of us were doing this manually. We’d run at most 30 test cases a day, frequently miss subtle regressions when our attention drifted, and constantly worry about edge cases. Frustrated, I automated the entire workflow with &lt;strong&gt;Playwright&lt;/strong&gt; — now a single machine easily runs 300+ cases a day, and I no longer dread being woken up in the middle of the night to “put out fires”. This post is a recap of that overhaul, and all the code is ready to run.&lt;/p&gt;

&lt;h2&gt;
  
  
  Breaking down the problem
&lt;/h2&gt;

&lt;p&gt;Our test target is an in‑house &lt;strong&gt;LLM Agent&lt;/strong&gt; web app. Users access it through a browser, can have multi‑turn conversations, and the agent is expected to have “long‑term memory” — remembering user preferences, facts, and recalling them even after the conversation shifts to different topics.&lt;/p&gt;

&lt;p&gt;A typical manual test flow goes like this: open the page → type “My name is Zhang San and I work at ByteDance” → chat about unrelated stuff → suddenly ask “What’s my name?” → manually judge if the answer is correct. A few things drive you crazy:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Long context windows&lt;/strong&gt; — a single session often spans a dozen turns. By the later rounds, your brain is fried.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Blurred forgetting boundaries&lt;/strong&gt; — LLM “forgetting” is rarely a hard drop; it might misremember a single character (“Zhang Shan”), which is painfully easy to miss with the naked eye.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Combinatorial explosion of scenarios&lt;/strong&gt; — name, company, preferences, order history… the permutations quickly become impossible to cover manually.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Interleaved tool calls&lt;/strong&gt; — the agent supports in‑conversation interruptions like “Book a flight for me”. Crafting that test data by hand is a huge pain.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Classic API‑level testing (calling &lt;code&gt;/chat&lt;/code&gt; directly) has two fatal flaws: it skips the front‑end message‑history stitching logic (our app has its own message merging strategy), so you never test the full chain; and the API often returns streaming chunks, making assertions tricky, and you can’t reproduce the final rendered text the user actually sees. &lt;strong&gt;End‑to‑end browser automation&lt;/strong&gt; is the only reliable way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Design decisions
&lt;/h2&gt;

&lt;p&gt;When picking a tool, I ruled out Selenium immediately. Not because it’s bad — it’s just that our Agent’s front‑end relies heavily on WebSockets pushing streaming tokens. Waiting for dynamic content with Selenium feels clunky; you end up writing piles of &lt;code&gt;WebDriverWait&lt;/code&gt;. &lt;strong&gt;Playwright&lt;/strong&gt; natively supports waiting for network idle, text changes in elements, and can even intercept WebSocket frames — plus its Python async API is butter smooth.&lt;/p&gt;

&lt;p&gt;The architecture boils down to three simple rules:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Tests as configuration&lt;/strong&gt; – each memory test becomes a “dialogue sequence + final assertion”, managed in YAML, completely decoupled from code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One browser context per session&lt;/strong&gt; – each test case uses an isolated browser context, keeping localStorage/sessionStorage separate, avoiding cross‑contamination and allowing parallel execution.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Smart waiting&lt;/strong&gt; – no hardcoded &lt;code&gt;sleep(2)&lt;/code&gt;. Instead, we use Playwright’s &lt;code&gt;expect(page.locator(...)).to_contain_text(...)&lt;/code&gt; to wait for the AI’s complete response. More stable than any fixed delay.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Why not use an existing AI testing framework (like DeepEval’s E2E part)? Most of them are still API‑oriented, offer weak support for front‑end rendering and multi‑step interactions, and add extra dependencies that the team has to maintain. Wrapping Playwright ourselves took about 200 lines of code, and we have far more control.&lt;/p&gt;

&lt;h2&gt;
  
  
  Core implementation
&lt;/h2&gt;

&lt;p&gt;Let’s get to the good stuff. The three code blocks below make up a minimal, runnable memory test script.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;First block: page interaction helper.&lt;/strong&gt; It solves “how to reliably send a message and get back the AI’s complete response”.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;playwright.async_api&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;async_playwright&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expect&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;send_and_get_reply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;15000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    向聊天框发送 message，等待 AI 返回完整响应文本
    特别处理流式输出：等待&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;停止生成&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;按钮消失，表明回复结束
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="c1"&gt;# 定位输入框并填入文本，通常 chatbot 都有一个 textarea
&lt;/span&gt;    &lt;span class="n"&gt;input_box&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;textarea[placeholder*=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;输入消息&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;]&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;input_box&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# 点击发送按钮
&lt;/span&gt;    &lt;span class="n"&gt;send_btn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;button:has-text(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;发送&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;send_btn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="c1"&gt;# 关键：等待流式输出完成。我们前端在生成时会显示一个"停止"按钮，
&lt;/span&gt;    &lt;span class="c1"&gt;# 生成结束后按钮消失。也可以用其他页面标志，比如光标出现。
&lt;/span&gt;    &lt;span class="n"&gt;stop_btn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;button:has-text(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;停止生成&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stop_btn&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to_be_hidden&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# 等15秒
&lt;/span&gt;
    &lt;span class="c1"&gt;# 获取最后一条 AI 消息的完整文本
&lt;/span&gt;    &lt;span class="c1"&gt;# 假设消息列表最后一条 class="assistant-message"
&lt;/span&gt;    &lt;span class="n"&gt;last_message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.assistant-message&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;last&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;last_message&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to_be_visible&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;last_message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;inner_text&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Second block: memory test case runner.&lt;/strong&gt; It solves “how to execute a multi‑turn conversation and run assertions”.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run_memory_test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;case&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    case 格式:
    {
      &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;基础记忆-姓名公司&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;,
      &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;conversations&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: [
        {&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;我叫张三，我在字节跳动工作&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;},
        {&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;assistant&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ignore&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: True},  # 不验证
        {&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;今天天气真好&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;},
        {&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;assistant&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ignore&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: True},
        {&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;我叫什么？我在哪工作？&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;},
      ],
      &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;expected&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: [&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;张三&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;字节跳动&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;]  # 最后一条回复必须同时包含这些词
    }
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new_context&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Third block: parallel execution entry point.&lt;/strong&gt; Kick off all the test cases at once. (I’ll leave that as an exercise — it’s really just an &lt;code&gt;asyncio.gather&lt;/code&gt; loop over your YAML‑defined cases using the two helpers above.)&lt;/p&gt;

&lt;p&gt;With these pieces, we turned a tedious, error‑prone process into something reliable and massively scalable. The team now adds new memory scenarios by simply appending YAML, and the entire suite runs multiple times a day in CI. If you’re testing LLM‑powered chat interfaces, give Playwright end‑to‑end automation a try — your sleep schedule will thank you.&lt;/p&gt;

</description>
      <category>playwright</category>
      <category>大模型测试</category>
      <category>自动化测试</category>
      <category>python</category>
    </item>
    <item>
      <title>How I Spent 6 Hours Debugging LangChain Memory’s Silent Message Drop</title>
      <dc:creator>BAOFUFAN</dc:creator>
      <pubDate>Mon, 10 Aug 2026 12:04:49 +0000</pubDate>
      <link>https://dev.to/_eb7f2a654e97a60ae9f96e/how-i-spent-6-hours-debugging-langchain-memorys-silent-message-drop-1hpf</link>
      <guid>https://dev.to/_eb7f2a654e97a60ae9f96e/how-i-spent-6-hours-debugging-langchain-memorys-silent-message-drop-1hpf</guid>
      <description>&lt;p&gt;At 2 a.m. an alert yanked me out of bed. I pulled up the logs and my blood pressure went through the roof: a user asked “Where is my order?” and the AI agent replied “What’s your favorite color?” I had trained this agent myself, run over 20 rounds of dialogue before deployment, and everything looked flawless. Right then I knew—it had to be the memory system again. Six hours later I finally pinpointed the root cause, and it made me want to throw my keyboard out the window: &lt;strong&gt;LangChain’s Memory silently drops messages under certain edge cases, and manual testing never covered them&lt;/strong&gt;. In this post I’ll share the complete memory-consistency testing setup I built with Pytest—along with the debugging journey—so you can save those six hours.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem Breakdown: Why “20 rounds worked” still exploded
&lt;/h2&gt;

&lt;p&gt;Our scenario is an order-inquiry agent. Users provide their phone number and order ID over multiple turns, the agent calls a lookup tool, and the conversation might branch into phone-number changes, coupon inquiries, and the like. We used LangChain’s &lt;code&gt;ConversationBufferMemory&lt;/code&gt; to store history and injected it into the prompt via &lt;code&gt;{chat_history}&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;During the first week in production we caught occasional reports that “the agent seems to forget who I am,” but reproducing it was tough. That night we finally pulled the full-chain logs and spotted a pattern:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;When a tool returned really long content (e.g., a hundreds-of-characters coupon list), &lt;strong&gt;by the time the next user message arrived, only the last two messages survived in memory&lt;/strong&gt;. The phone number and order ID from earlier rounds simply evaporated. Deprived of context, the agent would blurt out a random fallback reply.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The root cause? We had set &lt;code&gt;k=3&lt;/code&gt; on the Memory to control token usage, meaning “keep only the last 3 turns of interaction.” But in &lt;code&gt;ConversationBufferWindowMemory&lt;/code&gt; with &lt;code&gt;return_messages=True&lt;/code&gt;, when the total token length exceeded the model limit, LangChain would perform a &lt;strong&gt;secondary trim—at a higher priority—that completely ignored the &lt;code&gt;k&lt;/code&gt; value&lt;/strong&gt;, chopping history arbitrarily. &lt;strong&gt;This behavior wasn’t documented anywhere&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Why did our regular tests miss it? We used to spin up a real LLM, run 20-scripted conversations, and spot-check outputs manually. Slow, expensive, and because of LLM randomness we couldn’t write assertions. Those tests could only catch “completely broken” bugs, never the silent memory corruption.&lt;/p&gt;

&lt;h2&gt;
  
  
  Solution Design: Make Pytest the Black-Box Judge of Memory
&lt;/h2&gt;

&lt;p&gt;The core idea is dead simple: &lt;strong&gt;mock all LLM calls and test only the Memory read/write behavior inside the Agent flow&lt;/strong&gt;. Tests execute in milliseconds, cost zero, and let you parametrize the heck out of edge cases.&lt;/p&gt;

&lt;p&gt;Technical choices:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pytest&lt;/strong&gt; — parametrization and fixtures are far more elegant than unittest. &lt;code&gt;@pytest.mark.parametrize&lt;/code&gt; lets a single function run dozens of cases.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;unittest.mock&lt;/strong&gt; — patch &lt;code&gt;ChatOpenAI&lt;/code&gt; and similar LLM calls to make the agent’s “thinking” fully deterministic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Why not LangSmith or other platforms?&lt;/strong&gt; — They’re great, but require internet access, introduce latency, and aren’t suitable for fast local TDD red-green loops. Budget was tight, too.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Why not integration tests?&lt;/strong&gt; — Integration tests ensure components interact correctly, but memory-logic edge cases need to be violently enumerated with unit tests. They complement each other.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Architecture-wise, we designed a set of fixtures: &lt;code&gt;mock_llm&lt;/code&gt; → inject a custom reply sequence → create &lt;code&gt;AgentExecutor&lt;/code&gt; or &lt;code&gt;ConversationChain&lt;/code&gt; → run multiple turns → assert the contents of &lt;code&gt;memory.chat_memory.messages&lt;/code&gt;. This puts the memory component in the interrogation room all by itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Core Implementation: Three Steps to a Memory-Consistency Test
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Build an obedient Mock LLM&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This snippet solves the “uncontrollable LLM” problem. The mock returns preset replies one by one, simulating an AI that always follows the script.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;unittest.mock&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;MagicMock&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;patch&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langchain.schema&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;AIMessage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;HumanMessage&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langchain.chat_models&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ChatOpenAI&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pytest&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_mock_llm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;responses&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;创建一个顺序返回指定回复的mock LLM&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;mock&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MagicMock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;spec&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ChatOpenAI&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# 让mock的invoke方法依次返回AIMessage
&lt;/span&gt;    &lt;span class="n"&gt;mock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;side_effect&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;AIMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;responses&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;mock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;return_value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mock&lt;/span&gt;  &lt;span class="c1"&gt;# 支持链式调用
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;mock&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 2: Wire up a Chain with Memory, run multiple turns&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This code verifies that under normal flow, memory accumulates correctly. A pytest fixture initializes the chain; the test function feeds three user messages back to back, then asserts the correct number of messages in memory.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langchain.chains&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ConversationChain&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langchain.memory&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ConversationBufferMemory&lt;/span&gt;

&lt;span class="nd"&gt;@pytest.fixture&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;chain_with_memory&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="c1"&gt;# 注意：return_messages=True 让memory存储消息对象，便于断言
&lt;/span&gt;    &lt;span class="n"&gt;memory&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ConversationBufferMemory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;return_messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;mock_llm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;create_mock_llm&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;你好！&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;你叫什么？&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;我叫小智&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;chain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ConversationChain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;llm&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;mock_llm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;memory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;memory&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;chain&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_memory_accumulates_correctly&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chain_with_memory&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;chain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;chain_with_memory&lt;/span&gt;
    &lt;span class="n"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;predict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;你好&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;predict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;我叫小明&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;predict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;帮我查订单&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;messages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;memory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chat_memory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;
    &lt;span class="c1"&gt;# 期望3轮对话 = 6条消息（Human/AI交替）
&lt;/span&gt;    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;
    &lt;span class="c1"&gt;# 第一条用户消息应始终保留
&lt;/span&gt;    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;你好&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="c1"&gt;# 最后一条AI消息应为我预设的"我叫小智"
&lt;/span&gt;    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;我叫小智&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 3: Reproduce the bug that cost me 6 hours&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This test reveals the boundary vulnerability in &lt;code&gt;k&lt;/code&gt; truncation. We deliberately set &lt;code&gt;k=2&lt;/code&gt; (keep only the last 2 rounds) and run three rounds of dialogue. If early information still exists, the configuration failed to take effect; if the third user message is present but the second is missing, it indicates the truncation logic has a bug—exactly the silent corruption we saw in production. By writing this as a fast unit test, we caught the exact scenario that took me all night to find in logs, and we can now run it in seconds on every commit.&lt;/p&gt;

</description>
      <category>python</category>
      <category>programming</category>
    </item>
    <item>
      <title>I Let an LLM Write My API Tests—Coverage Jumped from 41% to 98% and Caught a Hidden Bug</title>
      <dc:creator>BAOFUFAN</dc:creator>
      <pubDate>Mon, 10 Aug 2026 01:04:16 +0000</pubDate>
      <link>https://dev.to/_eb7f2a654e97a60ae9f96e/i-let-an-llm-write-my-api-tests-coverage-jumped-from-41-to-98-and-caught-a-hidden-bug-fie</link>
      <guid>https://dev.to/_eb7f2a654e97a60ae9f96e/i-let-an-llm-write-my-api-tests-coverage-jumped-from-41-to-98-and-caught-a-hidden-bug-fie</guid>
      <description>&lt;p&gt;Liquid syntax error: Unknown tag 'endraw'&lt;/p&gt;
</description>
      <category>python</category>
      <category>programming</category>
    </item>
    <item>
      <title>Automated AI Memory Testing with Pytest &amp; Redis: 10x Faster Validation and 3 Hidden Bugs Squashed</title>
      <dc:creator>BAOFUFAN</dc:creator>
      <pubDate>Sun, 09 Aug 2026 12:05:12 +0000</pubDate>
      <link>https://dev.to/_eb7f2a654e97a60ae9f96e/automated-ai-memory-testing-with-pytest-redis-10x-faster-validation-and-3-hidden-bugs-squashed-5445</link>
      <guid>https://dev.to/_eb7f2a654e97a60ae9f96e/automated-ai-memory-testing-with-pytest-redis-10x-faster-validation-and-3-hidden-bugs-squashed-5445</guid>
      <description>&lt;p&gt;Last Friday afternoon, I was sipping coffee and getting ready to wrap up the week when a message from QA landed in our group chat: “Users are complaining the AI can’t remember earlier conversations – check if the memory disappeared again.” My heart sank a little. I opened the logs – the Redis key still contained the conversation history, but the order was completely scrambled, with the latest message sitting at the front. What annoyed me even more was that this was already the third time this month we’d been bitten by an “invisible” memory storage bug.&lt;/p&gt;

&lt;p&gt;Every time we tried to reproduce the issue, it involved spinning up three terminals, manually sending messages like “Hi, my name is Alice” and “What’s my name?” to the AI, then visually comparing the responses. “Manual validation” sounds generous – it was basically crossing our fingers.&lt;/p&gt;

&lt;p&gt;I decided to put an end to the guesswork by building a Pytest + Redis test suite. The goal wasn’t just to check whether memories were saved, but to verify order, TTL expiry, and consistency under concurrent writes. After setting it up, a manual regression cycle that used to take 30 minutes now finishes in 3 minutes for 30+ test cases, and I caught three logical bugs that even code review had missed. This article reproduces the entire journey – all the code is ready to run.&lt;/p&gt;

&lt;h2&gt;
  
  
  Breaking down the problem: why your AI memory tests have gaps
&lt;/h2&gt;

&lt;p&gt;Here’s the typical setup: an AI chat system stores every user–assistant turn in Redis, probably under a key like &lt;code&gt;chat:memory:{session_id}&lt;/code&gt;. The value is a JSON array containing message objects in chronological order. When the user sends a new message, the backend fetches the history from Redis, prepends it to the prompt, and feeds everything to the LLM. Simple on paper, but riddled with traps in practice:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Ordering issues&lt;/strong&gt; – under concurrent requests, multiple writes can interleave, messing up the array order. A user asks “What’s the weather?” and then “What’s my name?”; the AI answers the name first and the weather later because in the stored memory the “name” message jumped ahead of the “weather” one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TTL boundaries&lt;/strong&gt; – the conversation memory is set to expire after 30 minutes, but if a new message is written at 29:59 and the &lt;code&gt;EXPIRE&lt;/code&gt; reset happens at the wrong moment, the memory vanishes in the middle of the chat.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Serialization inconsistencies&lt;/strong&gt; – you save with &lt;code&gt;json.dumps&lt;/code&gt; and load with &lt;code&gt;json.loads&lt;/code&gt;, but when multiple processes or services are involved, &lt;code&gt;datetime&lt;/code&gt; objects can serialize differently (one process includes timezone info, another doesn’t). This causes memory comparisons to fail or even breaks the conversation flow with an error.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Manual testing typically looks like this: open a Redis client, insert a record by hand, call the API with &lt;code&gt;curl&lt;/code&gt;, then reconnect to check if the data is still “correct”. This approach has two fatal flaws: when there are 50 turns in a conversation, no human can reliably verify the order and content of all messages. Moreover, TTL-dependent test cases require waiting 30 minutes – nobody actually waits that long in a test environment, so that logic is almost never exercised. And concurrency scenarios? Nearly impossible to simulate by hand.&lt;/p&gt;

&lt;p&gt;What we really need is a test setup that can &lt;strong&gt;generate data, assert correctness automatically, fast‑forward time, and run in parallel&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Design decisions: why we test against a real Redis instance instead of mocking it
&lt;/h2&gt;

&lt;p&gt;When testing external dependencies, people usually pick one of two extremes: mock everything (replace all Redis calls with fake objects) or connect directly to a remote test Redis. I tried mocking and hit several walls:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Advanced features of &lt;code&gt;redis-py&lt;/code&gt; like pipelines, Lua scripts, and blocking commands are extremely hard to fake realistically. For example, the timeout behavior of &lt;code&gt;brpop&lt;/code&gt; in a mock is nothing like the real thing.&lt;/li&gt;
&lt;li&gt;The very things we need to test are &lt;strong&gt;Redis’s real behaviors&lt;/strong&gt;: TTL expiry, type conversions, and reconnection after a disconnect. Mocking would turn the tests into “testing my own mock code”, which is pointless.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Using a shared remote Redis instance also has downsides: parallel tests pollute each other’s keys, requiring constant cleanup, and network latency can cause flakiness. The sweet spot is &lt;strong&gt;testcontainers&lt;/strong&gt; – we spin up a temporary Redis container that behaves exactly like a production instance but is fully isolated, and it’s destroyed automatically when the tests finish. Combined with Pytest fixtures and &lt;code&gt;freezegun&lt;/code&gt; to freeze time, we can compress hours of time‑dependent tests into a few seconds.&lt;/p&gt;

&lt;p&gt;The test strategy is layered into three levels:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Unit tests&lt;/strong&gt; – verify the CRUD operations and serialization logic of the memory handler.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Integration tests&lt;/strong&gt; – exercise the full request path (FastAPI + Redis).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consistency tests&lt;/strong&gt; – validate memory order and completeness under concurrent writes.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Core implementation: solving a real pain point at every step
&lt;/h2&gt;

&lt;p&gt;Let’s build the test suite step by step. All the code includes imports so you can drop it into a &lt;code&gt;test_memory.py&lt;/code&gt; and run it.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Redis container fixture – solving environmental pollution
&lt;/h3&gt;

&lt;p&gt;This snippet eliminates leftover data and instability issues. Each test class gets its own Redis container.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# conftest.py or top of test_memory.py
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pytest&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;testcontainers.redis&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;RedisContainer&lt;/span&gt;

&lt;span class="nd"&gt;@pytest.fixture&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;session&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;redis_container&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="c1"&gt;# session-scoped to boost speed – starts once
&lt;/span&gt;    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nc"&gt;RedisContainer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;redis:7-alpine&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;container&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;container&lt;/span&gt;

&lt;span class="nd"&gt;@pytest.fixture&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;redis_client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;redis_container&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;redis&lt;/span&gt;
    &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Redis&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;redis_container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_container_host_ip&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;redis_container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_exposed_port&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;6379&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;decode_responses&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;  &lt;span class="c1"&gt;# auto-decode for easy assertions
&lt;/span&gt;    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;
    &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;flushall&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;# clean after each test to avoid cross-contamination
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Storing and reading memory – verifying basic correctness
&lt;/h3&gt;

&lt;p&gt;We need to confirm that after saving conversation history, the retrieved list has the exact same length, content, and order, without any weird escaping.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timezone&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MemoryService&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;实际业务代码（简化版），你项目里可能更复杂&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;redis_client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ttl&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1800&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;redis&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;redis_client&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ttl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ttl&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;append_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chat:memory:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="c1"&gt;# 取出历史
&lt;/span&gt;        &lt;span class="n"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;red&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>python</category>
      <category>programming</category>
    </item>
    <item>
      <title>Debugging Playwright LocalStorage Persistence: A 3-Hour Puzzle</title>
      <dc:creator>BAOFUFAN</dc:creator>
      <pubDate>Sun, 09 Aug 2026 01:04:13 +0000</pubDate>
      <link>https://dev.to/_eb7f2a654e97a60ae9f96e/debugging-playwright-localstorage-persistence-a-3-hour-puzzle-ego</link>
      <guid>https://dev.to/_eb7f2a654e97a60ae9f96e/debugging-playwright-localstorage-persistence-a-3-hour-puzzle-ego</guid>
      <description>&lt;p&gt;It was 2 a.m. when the CI alert fired. A front-end login test case had suddenly started failing — it was perfectly fine yesterday. Bleary-eyed, I opened the Allure report and saw the failure screenshot: login succeeded, the token was saved into LocalStorage, but after a page reload, the token was gone and the user got kicked back to the login page.&lt;/p&gt;

&lt;p&gt;That makes no sense, I thought. The front-end colleague swore that localStorage is persistent. How could it disappear after a refresh? Even weirder, the flow worked flawlessly when I tested it manually in the browser, yet the automation script reproduced the issue consistently. At that moment I knew: I wasn't going to bed anytime soon.&lt;/p&gt;

&lt;h2&gt;
  
  
  Breaking Down the Problem
&lt;/h2&gt;

&lt;p&gt;Our scenario is quite typical: after a user logs in, the front end writes a JWT via &lt;code&gt;localStorage.setItem('token', xxx)&lt;/code&gt; and then navigates to the home page. To verify that "the user stays logged in after a page refresh," we wrote the following Playwright test:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Simulate login → assert that a token exists in &lt;code&gt;localStorage&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;page.reload()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Assert again that the token is still there and that the page doesn’t redirect to &lt;code&gt;/login&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Manual testing worked perfectly, yet the automation failed most of the time: after &lt;code&gt;reload&lt;/code&gt;, &lt;code&gt;localStorage&lt;/code&gt; was cleared. I initially suspected the front end was wiping it during a reload. I combed through the source code — no cleanup logic. Then I wondered if Playwright’s &lt;code&gt;page.reload&lt;/code&gt; behaved oddly, so I swapped it with &lt;code&gt;page.goto&lt;/code&gt; — still broken.&lt;/p&gt;

&lt;p&gt;The root cause gradually surfaced: &lt;strong&gt;&lt;code&gt;localStorage&lt;/code&gt; persistence is tied to the browser storage directory, and we were launching Playwright with the default launch parameters, without specifying a persistent directory.&lt;/strong&gt; This means that every &lt;code&gt;browser.new_context()&lt;/code&gt; gave us a temporary profile. Data wouldn’t get lost when you call &lt;code&gt;page.reload&lt;/code&gt; inside the &lt;em&gt;same&lt;/em&gt; context. But our test framework used &lt;code&gt;@pytest.fixture(scope="function")&lt;/code&gt; to create a brand-new context for each test case. In cross-context scenarios — such as recreating a context to simulate closing and reopening the browser — localStorage was gone.&lt;/p&gt;

&lt;p&gt;To make matters worse, CI container environments naturally create a fresh browser instance per job, and the behavior of &lt;code&gt;new_context&lt;/code&gt; can differ from a developer’s local machine (where caching and recycling mechanisms may kick in). This led to tests that occasionally passed locally but always failed in CI. Here lies the reason why "the conventional approach doesn't work": directly asserting on &lt;code&gt;localStorage&lt;/code&gt; only verifies the data living inside the current session’s memory, not actual "disk persistence."&lt;/p&gt;

&lt;h2&gt;
  
  
  Solution Design
&lt;/h2&gt;

&lt;p&gt;I outlined several possible verification paths:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Option A: Read localStorage directly via &lt;code&gt;page.evaluate&lt;/code&gt;&lt;/strong&gt; – Verifies the current session but cannot guarantee that the data has been flushed to disk and can survive a “close browser and reopen” scenario. Pass.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Option B: Selenium’s &lt;code&gt;local_storage&lt;/code&gt; interface&lt;/strong&gt; – Selenium 4 supports it, but it requires extra configuration for the Chrome user data directory. The maintenance cost is high, and since we’ve already fully migrated to Playwright, there’s no reason to go backwards.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Option C: Playwright’s &lt;code&gt;storageState&lt;/code&gt; snapshot + reload&lt;/strong&gt; – Playwright provides &lt;code&gt;context.storage_state()&lt;/code&gt; to export the entire storage state (cookies + LocalStorage and other origin storage), and allows you to hydrate it back via the &lt;code&gt;storage_state&lt;/code&gt; parameter when creating a new context. This precisely simulates the data recovery process of “disk persistence → next browser session.”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the final choice: &lt;strong&gt;Combine Playwright + pytest with the &lt;code&gt;storageState&lt;/code&gt; mechanism, and design a fixture to truly verify localStorage persistence.&lt;/strong&gt; This not only tests the front-end logic but also incidentally validates the correctness of storage recovery.&lt;/p&gt;

&lt;h2&gt;
  
  
  Core Implementation
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. A reusable helper: save and restore storage state
&lt;/h3&gt;

&lt;p&gt;This code addresses the problem of “accurately simulating closing and reopening the browser while preserving localStorage.”&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;playwright.sync_api&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BrowserContext&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;save_and_reload_storage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;BrowserContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;filepath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;state.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;BrowserContext&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    Serialize the current context&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s storageState to a file,
    then close the original context and create a brand new one
    using the saved state. This simulates closing the browser
    and opening it again.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="c1"&gt;# Save the complete storage state (cookies + localStorage)
&lt;/span&gt;    &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;storage_state&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filepath&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;write_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Close the original context, releasing resources
&lt;/span&gt;    &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pages&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="c1"&gt;# Rebuild context from the saved state
&lt;/span&gt;    &lt;span class="n"&gt;browser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;browser&lt;/span&gt;
    &lt;span class="c1"&gt;# Important: use the same browser instance, otherwise a new variable is introduced
&lt;/span&gt;    &lt;span class="n"&gt;new_context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new_context&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;storage_state&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;filepath&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;new_context&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Key points:&lt;/strong&gt; The &lt;code&gt;storage_state&lt;/code&gt; parameter must point to a file path (Playwright reads it internally); you can also pass a dict, but a file is easier to audit in logs. Also, be sure to &lt;code&gt;close&lt;/code&gt; the old context first before creating the new one to guarantee a fully fresh session simulation.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. pytest fixture: providing independent context and verification tools for tests
&lt;/h3&gt;

&lt;p&gt;This code addresses “managing the Playwright lifecycle uniformly inside pytest and offering persistent verification capabilities.”&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pytest&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;playwright.sync_api&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sync_playwright&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Browser&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BrowserContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Page&lt;/span&gt;

&lt;span class="nd"&gt;@pytest.fixture&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;function&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;persisted_context&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;BrowserContext&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    Each test case receives a BrowserContext equipped with
    persistent verification capabilities.
    Inside the test, you can simulate reloading state via reload_state()
    to model
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>python</category>
      <category>programming</category>
    </item>
    <item>
      <title>Pytest + Chroma: The 6‑Hour Bug That Erased AI’s Memory</title>
      <dc:creator>BAOFUFAN</dc:creator>
      <pubDate>Sat, 08 Aug 2026 12:04:37 +0000</pubDate>
      <link>https://dev.to/_eb7f2a654e97a60ae9f96e/pytest-chroma-the-6-hour-bug-that-erased-ais-memory-409</link>
      <guid>https://dev.to/_eb7f2a654e97a60ae9f96e/pytest-chroma-the-6-hour-bug-that-erased-ais-memory-409</guid>
      <description>&lt;p&gt;At 2:30 AM, I was jolted awake by a stream of PagerDuty alerts. Users were complaining that our “AI memory assistant” kept asking “Hello, who am I?” in a loop—despite having chatted the day before about a cat named Pudding. I groggily opened the Chroma console. All vector records were still there. I reran the Pytest suite. Twenty‑seven tests glowed green like a spring meadow. Data intact, tests passing—yet the AI had amnesia. That night I stared at Chroma’s source code and logs from 2:30 to 8:30 AM, finally smoking out a ghost hiding inside a testing blind spot: a boundary case that only real users triggered, rendering our automated verification completely useless.&lt;/p&gt;

&lt;h3&gt;
  
  
  When your tests become a placebo
&lt;/h3&gt;

&lt;p&gt;Our architecture was straightforward: LangChain’s &lt;code&gt;ConversationBufferMemory&lt;/code&gt; stored conversation summaries in Chroma. At query time, we used semantic search with the current user input to fetch relevant memories and inject them into the LLM. To guarantee storage reliability, we wrote a full Pytest suite: insert a memory, query with the &lt;em&gt;exact same text&lt;/em&gt;, assert that the returned document ID matched. It looked bulletproof—until users rephrased their sentences in production and the memories never came back.&lt;/p&gt;

&lt;p&gt;The root cause quickly surfaced: a missing &lt;strong&gt;distance threshold&lt;/strong&gt; on query results. Chroma returns results by &lt;code&gt;top_k&lt;/code&gt; and will happily give you garbage when similarity is extremely low. Because our test always queried with the identical insertion text, the cosine similarity was always 1.0, the result was always first, and the assertion always passed. In reality, users don’t repeat themselves like a broken record. They reword, add filler words, make typos. Once the vector distance between the rephrased query and the stored memory widened to 0.6 or lower, Chroma still returned &lt;em&gt;something&lt;/em&gt;—possibly completely irrelevant memory fragments with a miserable score of 0.4. We never validated the score, so the LLM swallowed that noise as real memory, producing incoherent replies that looked like amnesia.&lt;/p&gt;

&lt;p&gt;Standard solutions—like LangChain’s built‑in &lt;code&gt;Memory&lt;/code&gt; test utilities—failed here. LangChain’s testing wrappers are too opaque: you can only check whether a final string appears, but you cannot control the vector search distance threshold, the similarity metric, or run parametrized tests with borderline text variations. To truly guard Chroma’s quality, we had to tear into the testing logic ourselves at the Pytest level.&lt;/p&gt;

&lt;h3&gt;
  
  
  Turning Pytest into the vector‑DB coroner
&lt;/h3&gt;

&lt;p&gt;We ditched all high‑level wrappers and drove the &lt;code&gt;chromadb&lt;/code&gt; client directly, building three dimensions of integrity validation inside Pytest:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Content correctness&lt;/strong&gt;: don’t just check IDs; assert the returned &lt;code&gt;document&lt;/code&gt; text is genuinely relevant.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Distance reasonableness&lt;/strong&gt;: force an assertion that &lt;code&gt;distance&lt;/code&gt; (or &lt;code&gt;score&lt;/code&gt;) exceeds our business threshold. If it doesn’t, the retrieval is considered a failure even if something was returned.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Semantic robustness&lt;/strong&gt;: hammer the retrieval with dozens of semantically similar queries that use different wording—misspellings, synonyms, colloquial expressions—to guarantee the memory’s “fault tolerance.”&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Why not use Chroma’s own integration tests? They’re too coarse, and the default similarity metric even changed between versions (pitfall #1, for another day). We chose Pytest for brutally simple reasons: great ecosystem, effortless parametrization, and &lt;code&gt;fixture&lt;/code&gt; lifecycle management that isolates Chroma collections cleanly. We weren’t writing a one‑off script; we were building an automated gate that must turn green before any deployment can happen.&lt;/p&gt;

&lt;h3&gt;
  
  
  Core implementation: three building blocks for an amnesia‑proof test suite
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Block 1: a reusable Chroma fixture for perfect isolation
&lt;/h4&gt;

&lt;p&gt;This snippet solves the classic “one test pollutes the next” problem. We use &lt;code&gt;tmp_path&lt;/code&gt; to give each test its own persistent directory and explicitly &lt;code&gt;delete_collection&lt;/code&gt; during teardown—ephemeral mode alone wasn’t enough after a leftover bug we hit.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pytest&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;chromadb&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;chromadb.config&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Settings&lt;/span&gt;

&lt;span class="nd"&gt;@pytest.fixture&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;memory_collection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tmp_path&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    每个测试用例独享一个 Chroma 集合，彻底隔离。
    强制指定余弦相似度，避免默认度量因版本变化。
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;chromadb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Settings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;chroma_db_impl&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;duckdb+parquet&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;persist_directory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tmp_path&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chroma_test&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;collection&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_collection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;test_memory&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hnsw:space&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cosine&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;  &lt;span class="c1"&gt;# 强制余弦距离
&lt;/span&gt;    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;collection&lt;/span&gt;
    &lt;span class="c1"&gt;# 清理——官方文档没强调，但不删会导致多测试套件相互影响
&lt;/span&gt;    &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;delete_collection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;test_memory&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Block 2: dual assertions on content and score
&lt;/h4&gt;

&lt;p&gt;This is the core validation: insert a known memory, then query with &lt;strong&gt;differently worded&lt;/strong&gt; phrases, and simultaneously verify both the returned text and the distance score. Our previous mistake was checking only IDs and ignoring the score, which let low‑similarity “fake memories” slip through.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_memory_retrieval_with_score_threshold&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;memory_collection&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    验证：用语义相近但措辞不同的查询，仍能召回正确记忆，
    且余弦距离分数不低于 0.75（我们的业务安全阈值）。
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="c1"&gt;# Arrange：插入一段用户的记忆
&lt;/span&gt;    &lt;span class="n"&gt;memory_collection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;documents&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;用户养了一只名叫布丁的橘猫，今年三岁。&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;metadatas&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;u1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;session&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;s1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;
        &lt;span class="n"&gt;ids&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mem-1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# 用户真实场景可能说的话
&lt;/span&gt;    &lt;span class="n"&gt;queries&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;我家的猫叫什么来着？&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;我那只橘猫多大了？&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;布丁几岁了&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;           &lt;span class="c1"&gt;# 省略“猫”
&lt;/span&gt;        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;我的宠物是什么品种&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;    &lt;span class="c1"&gt;# 语义相关但不完全重叠
&lt;/span&gt;    &lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;queries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;memory_collection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;query_texts&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="n"&gt;n_results&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;documents&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;distances&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="c1"&gt;# 关键断言 1：必须返回至少一条结果
&lt;/span&gt;        &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;documents&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;查询 &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; 未返回任何结果&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For each query we also assert that the returned document contains a key piece of information (e.g., “布丁”) and that the distance stays below the 0.75 threshold—the full test goes on to catch exactly those low‑score poison pills. Without this, our AI would keep “remembering” things that never happened.&lt;/p&gt;

&lt;p&gt;With the fixture and the parametrized barrage of real‑world queries, the test suite finally became the brutal gatekeeper we needed. No green, no deployment—and no more 2 AM amnesia wake‑up calls.&lt;/p&gt;

</description>
      <category>python</category>
      <category>programming</category>
    </item>
    <item>
      <title>From 3 Hours to 5 Minutes: Automating AI Chatbot Memory Regression with Pytest &amp; Docker</title>
      <dc:creator>BAOFUFAN</dc:creator>
      <pubDate>Sat, 08 Aug 2026 01:03:34 +0000</pubDate>
      <link>https://dev.to/_eb7f2a654e97a60ae9f96e/from-3-hours-to-5-minutes-automating-ai-chatbot-memory-regression-with-pytest-docker-1c5a</link>
      <guid>https://dev.to/_eb7f2a654e97a60ae9f96e/from-3-hours-to-5-minutes-automating-ai-chatbot-memory-regression-with-pytest-docker-1c5a</guid>
      <description>&lt;p&gt;It was 1 AM when a Slack alert jolted me awake: a user complained that “it suddenly lost its memory”—I had just told the AI I'm from Hangzhou, but in the next turn when asked “Where is my hometown?”, the bot replied deadpan, “I don't know.” Checking the latest release, sure enough the memory storage module had changed its serialization logic, quietly breaking the path for loading conversation history.&lt;/p&gt;

&lt;p&gt;A manual regression pass took 3 hours: set up Redis and Postgres environments separately, simulate multi-turn conversations, switch users, check memory recall… After that night I decided: this grunt work had to be automated away, once and for all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Exactly Is the Pain?
&lt;/h2&gt;

&lt;p&gt;Memory storage in an AI chatbot isn't as simple as saving a single key-value pair. It involves:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Multiple storage backends: Redis for short‑term, Postgres for long‑term, sometimes a hybrid architecture
&lt;/li&gt;
&lt;li&gt;Memory lifecycle: reads/writes, expiration, merging, rollback
&lt;/li&gt;
&lt;li&gt;Context and user isolation: threading memories across multiple sessions of the same user, while strictly preventing cross‑user contamination
&lt;/li&gt;
&lt;li&gt;Sensitivity to release changes: a single change to serialization format, schema, or TTL logic can silently drop memories
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The typical approach is to mock the storage layer in unit tests, but &lt;strong&gt;mocks can't expose the real serialization/deserialization pitfalls&lt;/strong&gt;—many production incidents were cases where mocks passed with flying colors, only to fail miserably in production. A manual regression pass went like: spin up environments → fabricate conversation data → deploy the change → verify memories → fabricate more data → verify again. Half an hour was the bare minimum; a full‑scenario regression could easily take 3 hours and still miss bugs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Design: Turn Real Middleware into Disposable Test Environments
&lt;/h2&gt;

&lt;p&gt;The core idea: &lt;strong&gt;use Docker containers to provide real Redis/Postgres, drive tests with Pytest parametrization, and turn memory logic into repeatable regression test cases.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Why not other approaches?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Why not &lt;code&gt;fakeredis&lt;/code&gt; or &lt;code&gt;testcontainers-python&lt;/code&gt; with an in-memory fake Redis?&lt;/strong&gt; Because serialization/deserialization, Lua script behavior under cluster mode, and other nuances can't be replicated by a fake library—that only gives you false confidence.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Why not a shared remote environment integrated with CI?&lt;/strong&gt; Multiple tests running in parallel would pollute each other, plus it's expensive and slow.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What about Docker + the &lt;code&gt;pytest-docker&lt;/code&gt; plugin?&lt;/strong&gt; The plugin's &lt;code&gt;session&lt;/code&gt; scope turned out to be tricky (more on that later). Ultimately I chose &lt;strong&gt;pytest &lt;code&gt;fixture&lt;/code&gt; + manual lifecycle control with &lt;code&gt;docker-compose&lt;/code&gt;&lt;/strong&gt;, which proved flexible and stable.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Architecture: &lt;code&gt;docker-compose.yml&lt;/code&gt; defines the Redis and Postgres services; a &lt;code&gt;module&lt;/code&gt;-scoped fixture in &lt;code&gt;conftest.py&lt;/code&gt; handles &lt;code&gt;docker-compose up&lt;/code&gt; and tears it down after tests; test cases use &lt;code&gt;parametrize&lt;/code&gt; to cover different storage backends, conversation turns, and data sizes, verifying memory read/write, isolation, and migration at a click.&lt;/p&gt;

&lt;h2&gt;
  
  
  Core Implementation: Building the Regression Step by Step
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Use docker-compose to define disposable real middleware
&lt;/h3&gt;

&lt;p&gt;This config solves the problem of differing databases across environments—dev, CI, and local all use the same image versions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# docker-compose.yml&lt;/span&gt;
&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3.8"&lt;/span&gt;
&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;redis&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;redis:7-alpine&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;6379"&lt;/span&gt;
    &lt;span class="c1"&gt;# No fixed external port to avoid conflicts&lt;/span&gt;
  &lt;span class="na"&gt;postgres&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:15-alpine&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;test&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;test&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;memory_test&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;5432"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Use a fixture in conftest.py to manage container lifecycle and inject dynamic connection info
&lt;/h3&gt;

&lt;p&gt;This code eliminates the pain of manually starting services every time you run tests, and dynamically assigns ports to avoid conflicts during parallel runs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# conftest.py
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pytest&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;docker&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;

&lt;span class="c1"&gt;# Use module scope to balance speed and isolation
&lt;/span&gt;&lt;span class="nd"&gt;@pytest.fixture&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;module&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;docker_services&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;docker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_env&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="c1"&gt;# Start with docker-compose, isolated project name prevents clashes
&lt;/span&gt;    &lt;span class="n"&gt;project_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;memtest_&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getpid&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;compose_file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dirname&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;__file__&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;docker-compose.yml&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# Use the docker-compose CLI (or Compose API, but CLI is more universal)
&lt;/span&gt;    &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;system&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;docker-compose -p &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;project_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; -f &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;compose_file&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; up -d&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# Wait for Redis/Postgres to be ready (health check polling)
&lt;/span&gt;    &lt;span class="n"&gt;redis_container&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="n"&gt;pg_container&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;containers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filters&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;project_name&lt;/span&gt;&lt;span class="p"&gt;}):&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;redis&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;redis_container&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;
        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;postgres&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;pg_container&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;

    &lt;span class="c1"&gt;# Poll until Redis is ready
&lt;/span&gt;    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;exit_code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;redis_container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exec_run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;redis-cli ping&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PONG&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;exit_code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;
        &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Redis did not start&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Wait for Postgres to accept connections
&lt;/span&gt;    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;exit_code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pg_container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exec_run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pg_isready -U test&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;accepting&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;exit_code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;
        &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Postgres did not start&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# 拿到动态端口
&lt;/span&gt;    &lt;span class="n"&gt;redis_port&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;redis_container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;attrs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;NetworkSettings&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Ports&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;6379/tcp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Ho
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>python</category>
      <category>programming</category>
    </item>
    <item>
      <title>Redis Cache Inconsistency: My 48-Hour Debugging Nightmare &amp; The Automated Test That Saved Me</title>
      <dc:creator>BAOFUFAN</dc:creator>
      <pubDate>Fri, 07 Aug 2026 12:04:02 +0000</pubDate>
      <link>https://dev.to/_eb7f2a654e97a60ae9f96e/redis-cache-inconsistency-my-48-hour-debugging-nightmare-the-automated-test-that-saved-me-1909</link>
      <guid>https://dev.to/_eb7f2a654e97a60ae9f96e/redis-cache-inconsistency-my-48-hour-debugging-nightmare-the-automated-test-that-saved-me-1909</guid>
      <description>&lt;p&gt;At 1 a.m., an alert call yanked me out of sleep: users were seeing their old nicknames on the page, but after a few refreshes it would fix itself. First thought: the cache wasn’t invalidated. I logged into Redis — the key was still there, but the database already had the updated value. Classic cache-database inconsistency. I assumed it was a small bug. Instead, I spent the next two days hunting this ghost. It was intermittent and only appeared under concurrent updates. Frustrated with debugging by luck, I spent half a weekend building an automated consistency test suite with pytest + Docker. Now it runs before every release and I haven’t been bitten by stale cache data since.&lt;/p&gt;

&lt;p&gt;Let me break down the entire approach. If you’ve dealt with similar headaches, you might be able to adopt it directly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;The scenario isn’t complicated. A user updates an order status. The code writes to MySQL, then deletes the Redis cache entry. On the next read, the cache is rebuilt. Simple logic, but under concurrency something ugly happens: process A updates the DB and deletes the cache; process B, after A deletes the cache but before A’s transaction commits, reads the old value from the DB and populates it back into the cache. The result? The DB has the new value, the cache holds the old one — only to be fixed when the cache expires. For low-frequency data, this dirty window can stretch for hours.&lt;/p&gt;

&lt;p&gt;Standard local tests totally miss this: single-threaded serial execution is always correct. Load-testing tools can trigger concurrency, but they struggle to assert “is the cache consistent with the DB right now?”. You usually only see the final state, and the fleeting intermediate inconsistency slips through. Even worse, these bugs are often introduced after tweaking a SQL query or adding some caching logic — by the time you notice in production, it’s already too late.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Plan
&lt;/h2&gt;

&lt;p&gt;My core requirement: &lt;strong&gt;in a local/CI environment, precisely create cache–database inconsistency with controlled concurrent operations, and assert immediately.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Why I chose what I chose:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;pytest?&lt;/strong&gt; Flexible enough to write test cases, fixture management for resources is a breeze, and you can parameterize concurrency combos. Way better than shell scripts and duct-tape tools.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docker?&lt;/strong&gt; Every test run needs pristine Redis + MySQL instances. Docker Compose spins them up with a single command, locks the versions, and avoids the dreaded “works on my machine” dance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Why not Testcontainers?&lt;/strong&gt; Not that it’s bad; I just needed finer-grained control over container lifecycles. For example, I wanted to restart Redis mid-test or simulate network glitches. Invoking the Docker CLI directly from a pytest fixture gave me that control.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Why not rely on production monitoring?&lt;/strong&gt; Monitoring only tells you when something broke. I wanted to stop defects at the CI stage.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The overall idea: define the dependent services in &lt;code&gt;docker-compose&lt;/code&gt;, use pytest fixtures to start/stop containers, create tables, and seed data. Each test case simulates a specific concurrency model (write-then-delete, delayed double-delete, binlog-subscription refresh, etc.). Concurrency is orchestrated with &lt;code&gt;threading&lt;/code&gt; or &lt;code&gt;asyncio&lt;/code&gt;. Finally, read from both Redis and MySQL and assert they match.&lt;/p&gt;

&lt;h2&gt;
  
  
  Core Implementation
&lt;/h2&gt;

&lt;p&gt;The project layout looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.
├── docker-compose.yml
├── conftest.py
├── test_cache_consistency.py
└── requirements.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  1. One-Command Environment with Docker Compose
&lt;/h3&gt;

&lt;p&gt;This configuration solves the pain of manually setting up DB and Redis for every test run.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# docker-compose.yml&lt;/span&gt;
&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.8'&lt;/span&gt;
&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;mysql&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mysql:8.0&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;MYSQL_ROOT_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;testpwd&lt;/span&gt;
      &lt;span class="na"&gt;MYSQL_DATABASE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;testdb&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3306:3306"&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;--default-authentication-plugin=mysql_native_password&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mysqladmin"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ping"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-h"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;localhost"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;

  &lt;span class="na"&gt;redis&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;redis:7-alpine&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;6379:6379"&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;redis-cli"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ping"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Adding &lt;code&gt;healthcheck&lt;/code&gt; was the first trap — more on that later.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. pytest Fixtures: Initialize Connections, Create Tables, Tear Down Data
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# conftest.py
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pytest&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;redis&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pymysql&lt;/span&gt;

&lt;span class="nd"&gt;@pytest.fixture&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;session&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;docker_services&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="c1"&gt;# 启动 docker-compose
&lt;/span&gt;    &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;docker-compose&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;docker-compose.yml&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;down&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-v&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;check&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stdout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DEVNULL&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;docker-compose&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;docker-compose.yml&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;up&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-d&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;check&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# 等待健康检查全部通过，避免服务未就绪就开始测试
&lt;/span&gt;    &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;yield&lt;/span&gt;
    &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;docker-compose&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;docker-compose.yml&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;down&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-v&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;check&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stdout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DEVNULL&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@pytest.fixture&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;function&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;db_conn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;docker_services&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pymysql&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;127.0.0.1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3306&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;root&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;testpwd&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;database&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;testdb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;autocommit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;  &lt;span class="c1"&gt;# 后面踩坑会解释
&lt;/span&gt;    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
            CREATE TABLE IF NOT EXISTS users (
                id INT PRIMARY KEY,
                name VARCHAR(50)
            )
    # ... (the fixture continues to return the connection and clean up after the test)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this foundation in place, the actual concurrency tests become surprisingly straightforward — but that’s a story for the next section. The key takeaway: if you can reproduce a cache bug deterministically in CI, you’ve already won half the battle.&lt;/p&gt;

</description>
      <category>python</category>
      <category>programming</category>
    </item>
    <item>
      <title>From 200s to 15s: 13x Faster Tests with Pytest + Redis Memory Store</title>
      <dc:creator>BAOFUFAN</dc:creator>
      <pubDate>Fri, 07 Aug 2026 01:05:04 +0000</pubDate>
      <link>https://dev.to/_eb7f2a654e97a60ae9f96e/from-200s-to-15s-13x-faster-tests-with-pytest-redis-memory-store-b4b</link>
      <guid>https://dev.to/_eb7f2a654e97a60ae9f96e/from-200s-to-15s-13x-faster-tests-with-pytest-redis-memory-store-b4b</guid>
      <description>&lt;p&gt;At 2 a.m., PagerDuty woke me up not for a production outage, but because the CI pipeline had turned red again—the test stage hit a hard timeout of 600 seconds. That moment felt even more frustrating than fixing a midnight bug: the code itself was fine, but the tests were just too slow. Every single run recreated data, re-called external APIs, and recomputed expensive fixtures from scratch.&lt;/p&gt;

&lt;p&gt;The root cause was blunt: &lt;strong&gt;our tests had no memory&lt;/strong&gt;. The same fixture that ran yesterday would be recomputed from scratch today on a different machine or branch. Some team members used &lt;code&gt;.pytest_cache&lt;/code&gt; for local caching, but that can’t be shared across distributed CI nodes—a fresh machine still suffered the same painful first run. Even worse, we occasionally hit consistency failures where stale cache results didn’t reflect fresh data, and when a popular key expired, dozens of test workers simultaneously hammered the data source—classic cache stampede and penetration issues, all present.&lt;/p&gt;

&lt;p&gt;Finally, I spent half a day embedding Redis directly into the Pytest fixture machinery, building a caching layer with memory storage, anti-penetration, and anti-stampede safeguards. Our test pipeline dropped from &lt;strong&gt;200s to 15s&lt;/strong&gt;—a 13x improvement—and never timed out in CI again. Here’s the full breakdown of the approach.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem Dissection: It’s Not Slow Code, It’s Wheel Reinvention
&lt;/h2&gt;

&lt;p&gt;Our test suite had two typical characteristics:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Many fixtures depend on expensive operations: complex database queries, model inference, external API calls (e.g., fetching tokens).&lt;/li&gt;
&lt;li&gt;The test environment was &lt;strong&gt;dynamically scaling&lt;/strong&gt;: 5 concurrent nodes today, maybe 10 tomorrow. Each new runner started as a blank slate.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The usual approach was &lt;code&gt;@pytest.fixture(scope="session")&lt;/code&gt; for in-process reuse, but that only works within a single node’s session. What truly slowed us down was &lt;strong&gt;cross-node, cross-session repeated computation&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Why not rely on &lt;code&gt;.pytest_cache&lt;/code&gt; or local files? Because distributed CI doesn’t share file systems, and local caches can’t control expiration properly, leading to consistency pitfalls like “the code changed but the cache still returns yesterday’s result”. More critically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cache stampede&lt;/strong&gt;: When a hot key (e.g., a large query result) just expired, dozens of test workers simultaneously hit the source, overwhelming the database or external API.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cache penetration&lt;/strong&gt;: Test code passes nonexistent parameters (e.g., a wrong user ID) that constantly bypass the cache and hit the backend, causing a flood of empty queries.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Expiry consistency&lt;/strong&gt;: After the source data updates, the cache still holds the old value, making assertions falsely pass while production breaks.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To fix this fundamentally, we needed a &lt;strong&gt;distributed, penetration-proof, and actively expirable&lt;/strong&gt; centralized memory store. Redis fit that role perfectly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Solution Design: Turning Redis into the Tests’ “Shared Memory”
&lt;/h2&gt;

&lt;p&gt;My reasoning wasn’t “I want Redis.” It was “I need a cache that can be shared quickly across distributed test nodes and natively supports atomic operations.”&lt;/p&gt;

&lt;p&gt;Candidate approaches:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Local files + NFS&lt;/strong&gt;: passable sharing, but no atomic ops—building custom locks and expiration is too painful.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memcached&lt;/strong&gt;: pure cache with good expiry strategies, but weak data structures; implementing Bloom filters or null markers is awkward.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redis&lt;/strong&gt;: strings, hashes, SETNX locks, Lua scripts, expiration—everything we needed from a mature ecosystem. No reason not to pick it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Architecturally, I designed three layers of protection:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Use &lt;strong&gt;SETNX-based mutual exclusion&lt;/strong&gt; so that only the first process fetches the data for a given key while others wait—solving stampedes.&lt;/li&gt;
&lt;li&gt;For non-existent data, store a &lt;strong&gt;null marker with a short TTL&lt;/strong&gt; to prevent penetration.&lt;/li&gt;
&lt;li&gt;When generating cache entries, embed a version or fixed TTL and provide &lt;strong&gt;explicit invalidation commands&lt;/strong&gt;—developers or CI scripts can purge entries when the source data changes, solving consistency.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The final form: a lightweight Pytest-plugin-style &lt;code&gt;conftest.py&lt;/code&gt; where developers simply wrap a fixture’s return value with a function called &lt;code&gt;cached_fixture&lt;/code&gt; to gain memory storage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Core Implementation: A Memory Layer from Zero to Usable
&lt;/h2&gt;

&lt;p&gt;Here’s the runnable code step by step. &lt;strong&gt;You can drop it directly into your &lt;code&gt;conftest.py&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Redis Connection and Lock Utilities
&lt;/h3&gt;

&lt;p&gt;This block handles safe Redis communication and provides basic locking. We create one Redis connection per Pytest session and release it automatically when the session ends.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# conftest.py
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pytest&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;redis&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;contextlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;contextmanager&lt;/span&gt;

&lt;span class="n"&gt;REDIS_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;redis://localhost:6379/0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="nd"&gt;@pytest.fixture&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;session&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;redis_client&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Session‑level Redis connection, reused for the entire test lifecycle&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;REDIS_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;decode_responses&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;
    &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="nd"&gt;@contextmanager&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;redis_lock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Redis&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    Simple mutual exclusion lock based on SETNX.
    Serializes &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fetch from source&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; operations on the same key to prevent stampedes.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;lock_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lock:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;lock_acquired&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setnx&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lock_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;lock_acquired&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;expire&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lock_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;lock_acquired&lt;/span&gt;
    &lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;lock_acquired&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lock_key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2: Core Memory‑Store Function
&lt;/h3&gt;

&lt;p&gt;This section solves “how to store and retrieve fixture return values while handling penetration and expiration.” The function first checks the cache; on a miss, it applies null-marker protection, then uses the lock to prevent a stampede when fetching the real data.&lt;/p&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
python
NULL_MARKER = "__NULL__"          # Marker for null values, distinct from None
DEFAULT_TTL = 3600 * 24 * 7       # Normal cache lives 7 days
NULL_TTL = 60                     # Null marker only lives 60 seconds to allow quick correction

def _build_cache_key(func_name: str, args, kwargs) -&amp;gt; str:
    """Generate a unique cache key from function name and parameters, ensuring same inputs hit the same cache."""
    params = json.dumps({"args": args, "kwargs": kwargs}, sort_keys=True)
    raw = f"{func_name}:{params}"
    return hashlib.md5(ra
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

</description>
      <category>python</category>
      <category>programming</category>
    </item>
    <item>
      <title>Debugging an LLM Long-Term Memory Disaster: 2000 Misplaced Memories in 3 Hours</title>
      <dc:creator>BAOFUFAN</dc:creator>
      <pubDate>Thu, 06 Aug 2026 12:04:31 +0000</pubDate>
      <link>https://dev.to/_eb7f2a654e97a60ae9f96e/debugging-an-llm-long-term-memory-disaster-2000-misplaced-memories-in-3-hours-4h4g</link>
      <guid>https://dev.to/_eb7f2a654e97a60ae9f96e/debugging-an-llm-long-term-memory-disaster-2000-misplaced-memories-in-3-hours-4h4g</guid>
      <description>&lt;p&gt;At 2 AM, my QA colleague frantically @-ed me in the group chat: “The chatbot’s long-term memory is completely broken. A user mentioned ‘my cat is named Doubao’, but days later when asked about it, the bot recalled ‘your dog is called Xuebing’. Even creepier, when you flip to page 3 you see the same content as page 1. Right now, over 2000 users’ memories are misaligned — take a look.”&lt;/p&gt;

&lt;p&gt;Bleary‑eyed, I opened the monitoring dashboards. The tables housing vectors and metadata showed normal write QPS, APIs were returning 200, and there were zero errors. In that moment I knew — this wasn’t a simple code bug. We had shipped a long‑term memory system without any &lt;strong&gt;consistency regression testing&lt;/strong&gt;. Our trust in that “memory storage” black box was running naked from day one.&lt;/p&gt;




&lt;h2&gt;
  
  
  Breaking down the problem
&lt;/h2&gt;

&lt;p&gt;LLM long‑term memory stores (like Mem0, LangChain’s memory modules, or a home‑grown vector index) typically rely on a “memory pipeline”:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The user converses with the model and facts (memory facts) are created.&lt;/li&gt;
&lt;li&gt;Each fact is embedded and stored in a vector database (pgvector, Milvus, etc.), together with structured fields such as timestamp, user ID, and session ID.&lt;/li&gt;
&lt;li&gt;During future conversations, the top‑N memories are recalled by similarity plus time‑decay, then stitched together into pages for the model.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The third step is where things went wrong. To make every retrieval reproducible and pagination coherent, you must simultaneously satisfy &lt;strong&gt;sort determinism and no‑duplicate‑no‑missing pagination&lt;/strong&gt;. In practice, many teams just throw an &lt;code&gt;ORDER BY created_at&lt;/code&gt; with &lt;code&gt;LIMIT/OFFSET&lt;/code&gt; into production. Under concurrent writes, it falls apart immediately — insert order within the same timestamp is non‑deterministic, so &lt;code&gt;OFFSET&lt;/code&gt; can skip or repeat records. Additionally, some services cache metadata for performance; if the cache isn’t invalidated promptly, you get phantom reads where a just‑inserted memory “disappears” on query.&lt;/p&gt;

&lt;p&gt;Traditional CRUD backend testing falls flat here. You can’t simulate 2000 concurrent writes, page drifting, and cache anomalies with a few hand‑crafted timestamps. You need an &lt;strong&gt;automated consistency test suite&lt;/strong&gt; — one that kneads your memory system like dough until it submits.&lt;/p&gt;




&lt;h2&gt;
  
  
  Designing the solution
&lt;/h2&gt;

&lt;p&gt;The goal was to build a test suite that could run against any long‑term memory backend — Postgres, Milvus, even an in‑memory KV store. The core idea: &lt;strong&gt;generate a large volume of memories that mimic real distributions → batch‑insert them concurrently → fetch via different pagination/sort strategies → verify mathematical invariants&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Tech stack: Python + pytest, memory generation with Faker, storage abstracted behind an interface. &lt;em&gt;Why not just validate with the storage’s own SQL scripts?&lt;/em&gt; Because we need to test the final “memory view” that is handed to the LLM — the API’s paginated results — not the raw physical rows in the database. Querying the DB directly would test the wrong thing. &lt;em&gt;Why not Go/JMeter for stress testing?&lt;/em&gt; We need logical assertions (e.g., the union of all pages yields the same set), not throughput benchmarks. pytest’s parametrized fixtures and Allure reporting offer at least an order of magnitude higher development speed for this kind of scenario.&lt;/p&gt;

&lt;p&gt;The architecture is dead simple: define a &lt;code&gt;MemoryStore&lt;/code&gt; protocol with two methods — &lt;code&gt;insert(memories)&lt;/code&gt; and &lt;code&gt;recall(user_id, page, size, sort_order)&lt;/code&gt;. Write a set of pytest test cases around it, then inject the concrete database implementation via dependency injection. Even if you swap Postgres for a custom vector engine, the test suite stays untouched.&lt;/p&gt;




&lt;h2&gt;
  
  
  Core implementation
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Why does this code exist?
&lt;/h3&gt;

&lt;p&gt;First, define the memory data model and the storage abstraction interface. This decouples the test logic from any specific backend. When you change databases, you only need to implement two methods and the entire suite runs immediately.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dataclasses&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dataclass&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timezone&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Protocol&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;uuid4&lt;/span&gt;

&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Memory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;A single memory entity&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;memory_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;default_factory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;uuid4&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nb"&gt;hex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;
    &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;
    &lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;default_factory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# Crucial: must carry UTC timezone, otherwise sorting will bite you
&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MemoryStore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Protocol&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Memory storage protocol – every backend implements this&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;insert_batch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;memories&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Memory&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="bp"&gt;...&lt;/span&gt;

    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;recall_page&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;page_size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;sort_order&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;desc&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Memory&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next comes the core pagination‑consistency test function — it generates a flood of memories, pumps them into the store, then reads page by page and verifies mathematical invariants:&lt;/p&gt;

&lt;h3&gt;
  
  
  Why does this code exist?
&lt;/h3&gt;

&lt;p&gt;It simulates a real user with 2000 inserted memories, then traverses all pages with different page sizes to check for &lt;strong&gt;no duplicates, no missing entries, and strict sort consistency&lt;/strong&gt;. Any breach stops immediately with reproducible failure information.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pytest&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;faker&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Faker&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;collections&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OrderedDict&lt;/span&gt;

&lt;span class="n"&gt;fake&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Faker&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run_pagination_consistency&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;MemoryStore&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;page_size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# 1. Insert `total` memories, deliberately jittered timestamps to mimic concurrency
&lt;/span&gt;    &lt;span class="n"&gt;memories&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="nc"&gt;Memory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;fake&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sentence&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="c1"&gt;# Make some timestamps extremely close to trigger sort instability
&lt;/span&gt;            &lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="mi"&gt;2025&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tzinfo&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seconds&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.01&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="c1"&gt;# Randomly shuffle the insertion order to simulate out‑of‑order concurrent writes
&lt;/span&gt;    &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;shuffle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;memories&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert_batch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;memories&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# 2. Expected full set: sorted by created_at descending (newest first), with memory_id tie‑breaker
&lt;/span&gt;    &lt;span class="n"&gt;ex&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>python</category>
      <category>programming</category>
    </item>
    <item>
      <title>LangChain Memory in Production: 10 Edge Cases We Hit and the Automated Tests That Saved Us</title>
      <dc:creator>BAOFUFAN</dc:creator>
      <pubDate>Thu, 06 Aug 2026 01:04:00 +0000</pubDate>
      <link>https://dev.to/_eb7f2a654e97a60ae9f96e/langchain-memory-in-production-10-edge-cases-we-hit-and-the-automated-tests-that-saved-us-13ff</link>
      <guid>https://dev.to/_eb7f2a654e97a60ae9f96e/langchain-memory-in-production-10-edge-cases-we-hit-and-the-automated-tests-that-saved-us-13ff</guid>
      <description>&lt;p&gt;At 2:17 AM, my phone vibrated harder than the coffee machine. I opened the monitoring dashboard to find users complaining that “the bot was spouting nonsense.” The real cause: LangChain’s &lt;code&gt;ConversationBufferMemory&lt;/code&gt; had silently swallowed an exception after a Redis timeout, returned an empty &lt;code&gt;history&lt;/code&gt;, and the downstream Chain kept generating responses with the wrong context. After debugging until 4 AM, the root cause came down to one sentence: &lt;strong&gt;the memory component never accounted for the storage layer failing&lt;/strong&gt;. The next day, we decided not to fix the bug first – we fixed the testing. We designed an automated test suite that cages all 10 conceivable failure scenarios.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Memory Breakdowns Break Everything
&lt;/h2&gt;

&lt;p&gt;LangChain’s Memory looks like “just storing the conversation history,” but in production, the backend can be Redis, Postgres, or even a custom vector store. Failures in the storage layer are far more complex than what you see in a demo:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Network jitter causing connect or read timeouts&lt;/li&gt;
&lt;li&gt;Connection pool exhaustion – new requests throw instantly&lt;/li&gt;
&lt;li&gt;Serialization / deserialization failures (e.g., a message contains an unserializable object)&lt;/li&gt;
&lt;li&gt;Concurrent writes to the same &lt;code&gt;session_id&lt;/code&gt; creating a race-condition overwrite&lt;/li&gt;
&lt;li&gt;Oversized message bodies that blow up Redis memory or hit transfer timeouts&lt;/li&gt;
&lt;li&gt;Data corruption (e.g., someone manually tweaked a value in Redis and the checksum no longer matches)&lt;/li&gt;
&lt;li&gt;The storage backend becomes completely unavailable (Redis goes down)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The official docs only teach you &lt;code&gt;memory.chat_memory.add_user_message(...)&lt;/code&gt;, and for exceptions they throw in a single line: “we recommend wrapping with try/except.” When you test manually, you can’t just unplug the Redis network cable and plug it back in. Even if you could, a full regression takes at least two hours and at best covers one or two happy paths. We needed an automated test suite that can &lt;strong&gt;precisely inject faults, validate Memory degradation, retries, and data consistency&lt;/strong&gt;, and run the entire suite in under 3 minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test Strategy: Mock All the Things? Both Approaches Have Pitfalls
&lt;/h2&gt;

&lt;p&gt;First, let’s be clear: we want to test the anomalous behavior of “Memory + a real storage backend,” not just the in-memory logic of Memory itself. The storage layer must be forced to fail.&lt;/p&gt;

&lt;p&gt;Candidate approaches:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Integration tests + manually stopping services&lt;/strong&gt;: too slow, not repeatable, can’t run in CI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Locust / chaos engineering&lt;/strong&gt;: too heavy; the goal is unit-level reliability, not load testing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;pytest + fakeredis&lt;/strong&gt;: can simulate most Redis commands, and with &lt;code&gt;monkeypatch&lt;/code&gt; you can inject timeouts, connection refusals, etc. – fast and CI-friendly. But &lt;code&gt;fakeredis&lt;/code&gt; and real Redis have behavioral differences (more on that later).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;testcontainers + real Redis&lt;/strong&gt;: fully realistic, but slow to start; better as a safety-net integration test than the main workhorse.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Final decision: &lt;strong&gt;pytest + fakeredis as the daily test workhorse, with testcontainers as a gating integration check&lt;/strong&gt;. We also wrapped our own &lt;code&gt;RobustMemoryWrapper&lt;/code&gt; that encapsulates all exception handling, retries, and degradation logic – instead of scattering it across business code. The test layout:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;tests/
  unit/          # fast fakeredis tests covering 10 failure scenarios
  integration/   # testcontainers with real Redis for final verification
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The 10 failure scenarios:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Connection timeout (connect timeout)&lt;/li&gt;
&lt;li&gt;Read timeout (read timeout)&lt;/li&gt;
&lt;li&gt;Connection pool exhaustion&lt;/li&gt;
&lt;li&gt;Redis OOM on write&lt;/li&gt;
&lt;li&gt;Serialization failure (unserializable object)&lt;/li&gt;
&lt;li&gt;Deserialization failure (corrupted data)&lt;/li&gt;
&lt;li&gt;Concurrent write race condition&lt;/li&gt;
&lt;li&gt;Oversized messages (exceeding &lt;code&gt;max_chunk_size&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Storage backend completely unavailable&lt;/li&gt;
&lt;li&gt;Multi-key conflicts (different Memory types sharing the same key)&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Implementation: Build a Robust Wrapper First, Then Write the Tests
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Wrap Memory into a "drop-proof" component
&lt;/h3&gt;

&lt;p&gt;The following code addresses the problem of “the official Memory component throws exceptions straight up with no degradation strategy.” We add timeouts, retries, and fallback behavior to all read/write operations – on timeout, we return an empty history so the Chain won’t crash, but we log an alert.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;logging&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langchain.schema&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BaseMessage&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langchain.memory.chat_memory&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BaseChatMemory&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;redis.exceptions&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nb"&gt;TimeoutError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;ConnectionError&lt;/span&gt;

&lt;span class="n"&gt;logger&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getLogger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;__name__&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RobustMemoryWrapper&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;带重试与降级的 Memory 包装器，适用于 Redis 等不可靠后端&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;memory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;BaseChatMemory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_retries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_memory&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;memory&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;max_retries&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;max_retries&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;

    &lt;span class="c1"&gt;# ---------- 对外保持与 BaseChatMemory 兼容 ----------
&lt;/span&gt;    &lt;span class="nd"&gt;@property&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;chat_memory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_memory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chat_memory&lt;/span&gt;

    &lt;span class="nd"&gt;@property&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;memory_variables&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_memory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;memory_variables&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;load_memory_variables&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;max_retries&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="c1"&gt;# 假设底层存储操作会触发 Redis 访问
&lt;/span&gt;                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_memory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load_memory_variables&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="nf"&gt;except &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;TimeoutError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;ConnectionError&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warning&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Memory load failed (attempt &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;): &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;max_retries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="c1"&gt;# 最终降级：返回空历史，避免 Chain 中断
&lt;/span&gt;                    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;erro&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>python</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
