Caching System

How Donobu caches AI-generated action sequences for fast test reruns, and strategies for managing the cache in CI.

Donobu caches the action sequences produced by autonomous page.ai() runs so that subsequent test runs are fast — no AI call required.

How it works

First run (AI)                   Subsequent runs (manual)
─────────────────────            ────────────────────────
page.ai('instruction')           page.ai('instruction')
        │                                │
        ▼                                ▼
  AI reasons about page          Cache lookup (instant)
        │                                │
        ▼                                ▼
  Executes tool calls            Replays same tool calls
        │                                │
        ▼                                ▼
  Writes to cache file           Returns result

On the first run, the AI executes the flow autonomously and the resulting sequence of tool calls is written to a cache file. On every subsequent run, Donobu reads the cache and replays those exact tool calls without calling the AI. This makes re-runs consistently fast — typically an order of magnitude faster than the first run.

Cache file location

Cache files live in a .cache-lock/ directory that sits alongside your test files:

tests/
  .cache-lock/
    checkout.test.ts.cache.js    ← generated automatically
    login.test.ts.cache.js
  checkout.test.ts
  login.test.ts

Cache files are plain JavaScript modules and are safe to commit to version control — committing them ensures every team member and every CI run starts with a warm cache.

Cache key

Each page.ai() call is identified by a cache key composed of:

  • Page hostname (e.g. app.example.com) — not the full URL, so the cache works across different paths on the same domain
  • Instruction text — the exact string passed to page.ai()
  • Schema shape — the Zod schema, if provided
  • allowedTools — the tool allow-list, if set
  • maxToolCalls — the tool call cap, if set
  • envVars — the list of exposed environment variable names

If any of these change, Donobu treats it as a new instruction and runs the AI again.

Cache invalidation

There are several ways to bypass the cache. Use the one with the desired granularity.

Invalidate all cache entries for a run

There are two equivalent ways to invalidate the entire cache for a run:

CLI flag (recommended for one-off runs):

npx donobu test --clear-ai-cache

Environment variable (useful in scripts or CI where you need more control):

DONOBU_PAGE_AI_CLEAR_CACHE=1 npx donobu test

Invalidate a single call

Pass cache: false to a specific page.ai() call to disable the cache every time that call is made:

await page.ai('Find the current pricing and click the Pro plan', {
  cache: false,
});

Invalidate manually

Delete the .cache-lock/ directory (or a specific .cache.js file) to force a full re-learn for the corresponding test:

rm tests/.cache-lock/checkout.test.ts.cache.js

Using the cache in CI

The recommended strategy depends on your team's workflow:

StrategyHowTrade-off
Commit the cacheAdd .cache-lock/ files to gitZero AI cost on every CI run; cache must be updated when the UI changes
Cache as CI artifactUpload .cache-lock/ as an artifact; restore on the next runCache persists across runs without being in git; first run after a cache miss costs AI tokens
Always re-run the AIPass --clear-ai-cache to npx donobu test (or set DONOBU_PAGE_AI_CLEAR_CACHE=1)Slowest, highest cost, but always exercises the latest page state

For most teams, committing the cache is the best starting point. Update the cache files whenever page.ai() calls break due to UI changes (or let self-healing update them automatically).

Tip: Do not use JavaScript string interpolation

There may be cases where you want a separate cache entry per value — for example, if you are testing multiple distinct user personas whose flows genuinely differ. However, if the value only changes between environments (staging vs. production credentials, for instance) or on every run (a timestamp), baking it directly into the instruction creates a unique cache key per value. This can result in an exceedingly large cache, or cache keys that will never be reused.

// ❌ Creates a new cache entry per user — fine for a small fixed set,
//    but problematic for credentials or timestamps that vary widely
await page.ai(`Log in as ${process.env.USERNAME}`);

// ✅ Keeps the cache key stable regardless of the username value
await page.ai('Log in as {{$.env.USERNAME}}', { envVars: ['USERNAME'] });

See Environment Variable Interpolation for the full syntax.