Error Injection

Test your application's error handling with one-shot errors, stream truncation, and timed disconnects. aimock provides three mechanisms for simulating failures.

One-Shot Errors

Queue an error that fires on the next request and auto-removes itself. Useful for testing retry logic.

one-shot-error.test.ts ts
const mock = new LLMock();
await mock.start();
mock.onMessage("hello", { content: "Hi!" });

// Queue a 429 rate limit error for the next request
mock.nextRequestError(429, {
  message: "Rate limit exceeded",
  type: "rate_limit_error",
});

// First request → 429 error
const res1 = await fetch(`${mock.url}/v1/chat/completions`, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    model: "gpt-4",
    messages: [{ role: "user", content: "hello" }],
  }),
});
expect(res1.status).toBe(429);

// Second request → normal response (error auto-removed)
const res2 = await fetch(`${mock.url}/v1/chat/completions`, { /* same */ });
expect(res2.status).toBe(200);

Stream Truncation

Abort a streaming response after a specific number of SSE chunks. Tests that your application handles partial streams gracefully.

truncation.test.ts ts
mock.on(
  { userMessage: "long story" },
  { content: "This is a very long response that will be cut short" },
  { truncateAfterChunks: 3 }  // Abort after 3 SSE chunks
);

Model Length Stops

Model misbehavior tests malformed tool arguments, refusals, and other response failures on the openai-chat wire. The stop-length-mid-tool fault cuts the selected tool call's argument string and omits later tool calls.

In streaming mode, the response ends with finish_reason: "length", an optional usage chunk, and [DONE]. In non-streaming mode, the response contains the partial arguments and the same length finish reason. The HTTP response completes normally, but the tool arguments are incomplete.

Use truncateAfterChunks or disconnectAfterMs to test an interrupted connection. These settings can also interrupt a semantic fault before its clean terminal reaches the client. See chaos precedence when combining faults.

Timed Disconnect

Disconnect after a specified number of milliseconds. Simulates network timeouts and connection drops.

disconnect.test.ts ts
mock.on(
  { userMessage: "slow" },
  { content: "This response will never complete" },
  { disconnectAfterMs: 100 }  // Kill connection after 100ms
);

Error Fixtures in JSON

fixtures/errors.json json
{
  "fixtures": [
    {
      "match": { "userMessage": "error-test" },
      "response": {
        "error": {
          "message": "Rate limited",
          "type": "rate_limit_error"
        },
        "status": 429
      }
    },
    {
      "match": { "userMessage": "partial" },
      "response": { "content": "This gets cut off" },
      "truncateAfterChunks": 2
    },
    {
      "match": { "userMessage": "timeout" },
      "response": { "content": "Never finishes" },
      "disconnectAfterMs": 50
    }
  ]
}

Interruption Behavior

Client Disconnects

When the client hangs up before the reply is complete, aimock stops delivery. It sends no more chunks and skips the remaining waits. This applies to these endpoints, streaming and non-streaming:

On WebSocket, a client that closes its TCP connection without sending a close frame is also a disconnect. aimock closes the connection at once (code 1006, "Connection lost"), stops the turn and journals it.

The journal entry records how the reply ended:

nextRequestError() is one-shot: it fires once and auto-removes itself. For persistent error fixtures, use addFixture() with an error response.

Accepted status codes: integers 200–999. The same range applies to nextRequestError(), POST /__aimock/error and a fixture’s response.status. 600–999 is unassigned rather than invalid — Node sends it and a client sees it — so it is allowed on purpose. Outside the range the status could never reach the client: 1xx cannot terminate a response and would hang the request until the client times out, and anything below 100, above 999 or fractional makes Node throw, which loses the injected error and returns a generic 500. nextRequestError() throws a RangeError at the call site; the control API returns 400; a fixture fails validation.

See also: Chaos Testing — for probabilistic failure injection. Chaos testing adds configurable rates for dropped requests (500), malformed JSON responses, and disconnects that trigger based on probability rather than deterministic fixture matching. Use error injection for specific, reproducible failure scenarios; use chaos testing for resilience testing under unpredictable conditions.