How do you test an AI agent that calls internal APIs with no staging environment?
Rebuild the dependency instead of borrowing it. Record what the agent's tool calls send and receive, or read the OpenAPI spec, then stand up a stub service per tool with seeded fixtures and its own state. Point the agent at that and assert on the tool calls it makes, not just on the final text. Keep a small smoke test against the real system for the parts a copy cannot prove.
Last updated 2 August 2026
Why "just get a staging instance" usually fails
The systems an agent calls are rarely all yours. A support agent touches the orders service (another team, one shared staging box, permanently in a broken state), a payments API (a vendor, sandbox credentials on request, three weeks), an internal admin API (no test instance has ever existed, and asking produces a ticket rather than a URL), and a search index (a snapshot nobody refreshes).
Every one of those is a separate negotiation, and the test you want to write needs all of them at once, in a known state, repeatedly. That is why the practical answer is not to get an instance but to stand one up yourself, per tool, from something you already have.
Step 1: record what the tools actually do
You cannot rebuild what you have never observed. Instrument the agent so every tool call is captured with its arguments, its response, its latency, and its errors. OpenTelemetry is the portable way to do this and every observability platform in this category ingests it; the collector SDKs that ship with tracing vendors do the same job with less setup.
Capture failures deliberately, not just the happy path. A stub built only from successful calls will answer 200 to everything, and an agent that has never seen a 409 or a rate-limit response is an agent whose error handling has never been tested. If your traffic is too clean, provoke the errors once in a scratch environment and record those runs too.
Step 2: write a contract for each tool
Turn the recordings into something declarative: the operation, its parameters, the response schema, the error conditions, and the rules that are not visible in a payload. An OpenAPI spec is the ideal input if one exists, because it carries the prose rules recordings never show ("submit requires at least one line item", "a refund above the order total is a 422").
Where no spec exists, infer the schema from the recordings and write the rules down by hand as you learn them. This document is the asset. It outlives the stub, it is what a new engineer reads, and it is what makes the difference between a fake that returns plausible JSON and a fake that behaves like the system.
Step 3: stand up a stub with real state
The important word is state. A response-matching mock (Prism from an OpenAPI spec, WireMock, MSW in a TypeScript stack) is enough for a read-only tool, and if that is all you need, stop here: it is a day of work and it is free.
It stops being enough the moment the agent writes. A refund flow that reads a balance, issues a refund, and reads the balance again cannot be tested against fixed responses, because the second read has to reflect the write. For those tools, back the stub with a real store: SQLite or a container-local Postgres seeded from fixtures, with thin handlers implementing the contract from step 2. That is more work than a mock and it is the work that makes the test meaningful.
Step 4: seed it, and reset between runs
Seed with invented data that has the shape of the real thing: the same entities, the same relationships, realistic cardinality, and the awkward cases on purpose (the order with forty line items, the customer with no email, the refund already refunded). Do not copy production rows into a test fixture; you inherit a compliance problem to solve a data problem.
Seed deterministically from a fixed seed, and reset between runs. If run 3 starts from whatever run 2 left behind, a failure tells you nothing about the change you were testing.
Step 5: assert on the tool calls, not just the prose
The output of an agent run is not one string, it is a sequence of decisions. Assert on which tools were called, in what order, with what arguments, and on the state the run left behind: the refund exists, it is for $40.00, it is against order 1182, and no second refund was issued.
Keep judged assertions for the parts that are genuinely fuzzy, like tone or whether an explanation was given, and hold the judge to a threshold you have calibrated. A suite that only checks the final sentence will pass a run that did the wrong thing politely.
Step 6: measure the gap, and keep one real smoke test
A rebuilt system is a copy, and a copy that nobody checks drifts silently until the suite is green against a system that no longer exists. Diff recorded production responses against what the stub returns for the same call, per tool, and treat that agreement rate as a number you watch. When it drops, the contract changed and you now know which tool.
And keep one thin end-to-end test against the real systems, in a real environment, running rarely. It will not catch regressions (that is what the suite above is for), but it is what tells you the copy has stopped being a copy.
Where Mirrors fits
Steps 1 through 4 are the expensive part, and they are the part Mirrors does for you. It reads whatever you have (traces, tool code, MCP-style tool definitions, OpenAPI specs, or docs), works out what each tool does individually, and composes a runnable environment from the union of what those tools need: a schema covering every entity any tool touches, fabricated seed data, and each tool bound to real code where you provided it or to a synthesized implementation where you did not.
It also does step 6 as a first-class thing, which is the part hand-built fakes almost never get. Each tool carries a fidelity score against your recorded traces and an evidence grade, so a tool rebuilt from a spec with no recordings is never confused with one validated against production behavior, and the replay view diffs each recorded call against its counterpart in the environment.
Steps 5 stays yours either way. Assertions are about your business rules, and no tool can decide for you that a refund above the order total should never be issued.
What this does not solve
It cannot rebuild what you never recorded and never specified. A tool with no traces, no code, and no docs is a tool nobody can reconstruct, and a rebuilt environment inherits every blind spot in its inputs. If a code path only runs on the third Tuesday of the quarter, it is not in your traces and it will not be in the copy.
It does not test the things that live between systems rather than inside them: authentication against the real identity provider, rate limits, timeouts, network partitions, and the vendor whose sandbox behaves differently from their production. Those need the real thing, which is why the thin smoke test in step 6 stays.
And a rebuilt environment is a copy of the systems, not of your infrastructure. It says nothing about whether your deploy works, whether the queue is drained, or whether the agent has the right credentials in production.
Read next
- What an agent staging environment is →
- What a seeded environment is →
- How do you safely test an agent that issues refunds, deletes, or sends? →
- How Mirrors builds an environment