You cannot assert on the model.
Run the same prompt twice and get two different sentences, both correct. That is not a testability problem you can engineer away; it is the nature of the component. So the instinct that says "AI features can't really be tested" has the first half right and draws the wrong conclusion from it.
Everything around the model is ordinary, deterministic code with excellent testability. The loop, the caps, the parsing, the tools, the compensations, the rendering — all of it can be tested exactly as you test anything else, and all of it is where the bugs in this book actually lived. Not one chapter so far has described a bug that was the model's fault.
Fake at the Boundary You Own
Chapter 1 put the provider behind a driver so that exactly one seam exists. This is where that pays off:
beforeEach(function () {
Http::preventStrayRequests();
Http::fake(['*' => Http::response(['error' => 'boom'], 500)]);
});
Http::fake is the obvious half. preventStrayRequests is the important one.
Without it, a request your fake does not match goes to the real internet. In an AI codebase that means a test suite that quietly calls a paid API — slowly, non-deterministically, and on somebody's card. Worse, it means a test can pass because it accidentally reached production.
Turn it on globally, not per test file. The first time it fails a test you did not expect it to, it has already paid for itself.
Test the Loop, Not the Model
The agent loop from Chapter 8 is pure control flow over a value the provider sends. Feed it that value directly:
- A response with
stop_reason: end_turn→ the loop exits and publishes. - A response with
pause_turnand no tool blocks → the loop continues. This is the regression test for the bug that made a paused turn look finished, and it costs nothing to keep. - A response with
max_tokens→ an error message is appended andhasErroris set. - A response with
refusal→ a refusal message, and no retry. - Ten tool-using responses in a row → the loop stops at the iteration cap.
None of these call a model. All of them test the code where the interesting mistakes are. When a provider adds a new stop reason, you add a case and find out immediately what your loop does with it.
Tool Tests Are Feature Tests
Tools are endpoints, so test them through the server rather than by calling the class:
AgentServer::tool(CreateSiteTool::class, [
'url' => 'fails-to-provision',
'email' => 'owner@webplo.com',
'about' => 'Fail Site',
'verification_token' => $token,
]);
That exercises registration, schema, validation and handler together — the same path a model takes. Calling handle() directly skips the parts most likely to be misconfigured.
Which also makes registration testable, and that matters given Chapter 11: a test asserting that an internal tool is not in the published toolset is one line, and it is the only thing standing between "we decided not to publish this" and someone adding it to the array six months later.