Sequential / Stateful Responses
Use sequenceIndex in fixture match criteria to return different responses for
the same query on each successive call. This enables testing multi-step agent
conversations and retry logic.
How It Works
- The router tracks how many times each unique match pattern has been hit
-
sequenceIndex: 0matches the first request,sequenceIndex: 1the second, etc. - Different match patterns have independent counters
- If a sequenceIndex fixture does not match the current count, routing falls through to the next fixture
-
Fixtures without
sequenceIndexmatch any occurrence (backward compatible) - Counters reset on
mock.reset()
Unit Test: 2-Step Sequence
const mock = new LLMock();
await mock.start();
mock.on({ userMessage: "plan", sequenceIndex: 0 }, { content: "Step 1: planning..." });
mock.on({ userMessage: "plan", sequenceIndex: 1 }, { content: "Step 2: done!" });
// First request → first response
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: "plan" }],
stream: false,
}),
});
const body1 = await res1.json();
expect(body1.choices[0].message.content).toBe("Step 1: planning...");
// Second request → second response
const res2 = await fetch(`${mock.url}/v1/chat/completions`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
model: "gpt-4",
messages: [{ role: "user", content: "plan" }],
stream: false,
}),
});
const body2 = await res2.json();
expect(body2.choices[0].message.content).toBe("Step 2: done!");
Fallback After Sequence Exhaustion
// First call matches sequenceIndex 0, subsequent calls fall through to fallback
mock.on({ userMessage: "once", sequenceIndex: 0 }, { content: "only-first-time" });
mock.on({ userMessage: "once" }, { content: "fallback" });
// Request 1 → "only-first-time" (sequenceIndex 0 matches)
// Request 2 → "fallback" (sequenceIndex 0 won't match, falls through)
JSON Fixture
{
"fixtures": [
{
"match": { "userMessage": "plan", "sequenceIndex": 0 },
"response": { "content": "Step 1: planning..." }
},
{
"match": { "userMessage": "plan", "sequenceIndex": 1 },
"response": { "content": "Step 2: done!" }
}
]
}
Sequence counters are per-fixture-match, not global. If you have fixtures matching "alpha" and "beta", their counters are tracked independently.
MCP tool sequences
MCP tool answers can be sequenced too. In a fixture file’s
mcpFakes block, each tool has an ordered calls list. Entries
with the same arguments are used in the order you declare them, once each. In this file,
the first create_ticket call gets a tool error the model can see and retry,
and the second call gets TICKET-42:
{
"fixtures": [
{
"match": { "userMessage": "open a refund ticket", "hasToolResult": false },
"response": {
"toolCalls": [{ "name": "create_ticket", "arguments": { "title": "Refund" } }]
}
}
],
"mcpFakes": {
"scope": { "testId": "tickets › retry on timeout" },
"tools": [
{
"name": "create_ticket",
"calls": [
{ "id": "first-try", "args": { "title": "Refund" }, "error": "upstream timeout" },
{ "id": "retry", "args": { "title": "Refund" }, "result": "TICKET-42" }
]
}
]
}
}
A third call with the same arguments fails with MCP_FAKE_EXHAUSTED. The
position is kept per test id (here tickets › retry on timeout) and rewinds on
mock.resetMatchCounts(). See
MCPMock: Scenario fakes.
Need stateless matching? sequenceIndex uses a server-side
counter that resets on mock.reset() and can drift when multiple clients
share an instance. If your use case is “different response based on conversation
depth” (not “different response for the Nth identical request”),
prefer the stateless turnIndex or hasToolResult fields
instead. See Multi-Turn Conversations.