At 2 a.m., my QA colleague spammed the group chat: “The memory decay algorithm you shipped yesterday seems broken again. Similarity search ranking is completely off — the top results are all old data.” I groggily opened the admin dashboard, copied in a few memories, then stared at the screen for an hour waiting for the decay to kick in. After that, I manually compared rankings… and debugged until dawn, only to discover the decay formula was missing a time coefficient. This kind of manual regression testing — waiting for real time and verifying rankings by hand — is painfully dumb. And honestly, this wasn’t the first time.
Are you still manually validating memory time decay?
“Memory storage” for large language models sounds fancy, but when you break it down, it’s essentially two things: semantic similarity ranking via vector search, and time decay that gives recent memories higher weight. Unit tests can guarantee the decay function itself isn’t broken, but end-to-end regression is a nightmare. You need to actually observe that one hour after a memory is written, its similarity score drops to the expected 0.8×, and after 24 hours it falls to 0.2×. If you rely on real time, testing a full decay curve can take days. And it’s not just the algorithm — the UI often has its own bugs, like pagination, highlighting, or timestamp display glitching after you adjust the decay coefficient.
The usual workaround is mocking time: freezegun on the backend, jest.useFakeTimers() on the frontend. But testing them separately never catches “backend and front end clocks out of sync” bugs. We even tried running Selenium for the UI and using backdoors to tweak database timestamps — to keep everything in sync we ended up writing a pile of hacky code that was a nightmare to maintain.
Why we chose Playwright — it gave the browser a “time accelerator”
When evaluating tools, we had a few hard requirements:
- Operate both the browser UI and call backend APIs, so one regression run can validate the admin dashboard and create test data via API.
-
Accelerate the entire browser’s timeline — speed up
Date.now(),setTimeout,requestAnimationFrame, and everything time-related together, not just mock a few APIs. - Maintainable scripts that a new teammate can pick up within an hour.
Cypress has cy.clock(), but it’s built on Sinon fake timers and can’t truly drive the whole browser timeline (WebSocket heartbeats, CSS animation timers, etc., go out of sync). Selenium offers almost no primitives for this. Playwright’s clock API, however, intercepts time at the browser process level. After you call page.clock.install(), a subsequent page.clock.fastForward('01:00:00') makes everything inside the browser believe an hour has passed — new Date(), setTimeout, and any third-party time library. That means you can turn a “wait 1 hour” test into a 1-second test without touching a line of frontend code.
The architecture is simple: Playwright script → concurrently calls API (to seed memory data) and drives the dashboard page → backend service → vector DB (Qdrant/Chroma) + decay calculation. The entire end-to-end verification runs inside a single process.
Core implementation: three steps to a time-accelerated regression test
Step 1: Launch a browser with a controlled clock
This snippet shows how to make the browser live on our timeline. The key is installing the fake timer before the page loads — otherwise the page initializes with real time, and subsequent fast-forwards drift.
# test_memory.py
from playwright.sync_api import sync_playwright, expect
def test_memory_time_decay():
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
context = browser.new_context()
page = context.new_page()
# Install fake timer before page load so all time operations run on simulated clock
page.clock.install(time="2025-01-01T00:00:00Z")
# Now navigate, the page's Date.now() starts from 2025-01-01
page.goto("https://memory-dashboard.example.com")
# Login and other setup omitted...
# The dashboard will now display our simulated time
Step 2: Seed a memory via API and verify the initial ranking on the UI
This snippet shows how to mix API and UI operations for a complete decay-validation loop. We deliberately make the new memory’s vector slightly more similar to the query, but the old memory is “fresher” and gets a time-decay boost — so initially the old memory should rank first.
# Use API to add two memories: one from 1 hour ago, one just created
api_context = context.request
# Add older memory (backend writes it with created_at = current simulated time - 1 hour)
api_context.post("https://api.example.com/memories", json={
"content": "User likes latte",
"created_at": "2024-12-31T23:00:00Z", # simulated time is 2025-01-01T00:00, so 1 hour ago
"vector": [0.1, 0.9, 0.05] # vector related to "latte"
})
# Add a newer memory with similar content but more recent timestamp
api_context.post("https://api.example.com/memories", json={
"content": "User often orders latte coffee",
"created_at": "2025-01-01T00:00:00Z",
"vector": [0.12, 0.88, 0.06]
})
# Go back to dashboard and search for "coffee preference"
page.fill('input[placeholder="Search memories"]', "coffee preference")
page.click('button:has-text("Search")')
# The older memory still has high time-decay weight, so it should rank first
first_result = page.locator(".memory-item").nth(0)
expect(first_result).to_contain_text("User likes latte")
Step 3: Fast-forward time and verify the ranking flips
This is the soul of the whole approach — fastForward makes the decay effective instantly.
# Fast-forward 1 hour to let decay take effect
page.clock.fastForward("01:00:00")
# Search again, now the newer memory should rank higher due to decay
page.click('button:has-text("Search")')
first_result = page.locator(".memory-item").nth(0)
expect(first_result).to_contain_text("User often orders latte coffee")
With this setup, a full decay-curve regression that used to take hours (or even days) now completes in minutes — and you can test edge cases around clock boundaries without any real wait. Playwright’s clock API turned a maintenance headache into a reliable, readable test that any team member can extend. No more 2 a.m. bug hunts over a missing coefficient.
Top comments (1)
I appreciate how you highlighted the challenges of testing time-based functionality, particularly with memory decay algorithms. The example you provided, where a 4-hour test was condensed to 10 minutes using Playwright's Clock API, is quite impressive. I've had similar experiences with Cypress's
cy.clock()and can see how Playwright's approach offers more comprehensive control over the browser's timeline. By leveragingpage.clock.install()andpage.clock.fastForward(), you can indeed synchronize the entire browser environment, including WebSockets and CSS animations, which is a significant advantage. Have you considered exploring other use cases for this time acceleration feature, such as testing scheduling or cron job-like functionality in your application?