Skip to content

Commit af5a157

Browse files
committed
feat(waitforpendingrequests): add waitForPendingRequests api
1 parent 8798fa1 commit af5a157

File tree

3 files changed

+78
-2
lines changed

3 files changed

+78
-2
lines changed

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,14 @@ await page.route("**/api/users", async (route) => {
189189

190190
Batch behaviour stays automatic: additional `requestMock` calls issued in the same macrotask are grouped, forwarded, and resolved together.
191191

192+
Need to pause the test until everything in-flight resolves? Call `waitForPendingRequests` to block on the current set of pending requests (anything started after the call is not included):
193+
194+
```ts
195+
// After routing a few requests
196+
await mockClient.waitForPendingRequests();
197+
// Safe to assert on the results produced by the mocked responses
198+
```
199+
192200
## Describe Requests with Metadata
193201

194202
`requestMock` accepts an optional third argument (`RequestMockOptions`) that is forwarded without modification to the MCP server. The most important field in that object is `metadata`, which lets the test process describe each request with the exact OpenAPI/JSON Schema fragment, sample payloads, or test context that the AI client needs to build a response.
@@ -261,6 +269,7 @@ The library exports primitives so you can embed the workflow inside bespoke runn
261269

262270
- `TestMockMCPServer` starts and stops the WebSocket plus MCP tooling bridge programmatically.
263271
- `BatchMockCollector` provides a low-level batching client used directly inside test environments.
272+
- `BatchMockCollector.waitForPendingRequests()` waits for the currently pending mock requests to settle (resolves when all finish, rejects if any fail).
264273
- `connect(options)` instantiates `BatchMockCollector` and waits for the WebSocket connection to open.
265274

266275
Each class accepts logger overrides, timeout tweaks, and other ergonomics surfaced in the technical design.

src/client/batch-mock-collector.ts

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ interface PendingRequest {
5656
resolve: (data: unknown) => void;
5757
reject: (error: Error) => void;
5858
timeoutId: NodeJS.Timeout;
59+
completion: Promise<PromiseSettledResult<void>>;
5960
}
6061

6162
const DEFAULT_TIMEOUT = 60_000;
@@ -132,10 +133,15 @@ export class BatchMockCollector {
132133
metadata: options.metadata,
133134
};
134135

136+
let settleCompletion!: (result: PromiseSettledResult<void>) => void;
137+
const completion = new Promise<PromiseSettledResult<void>>((resolve) => {
138+
settleCompletion = resolve;
139+
});
140+
135141
return new Promise<T>((resolve, reject) => {
136142
const timeoutId = setTimeout(() => {
137-
this.pendingRequests.delete(requestId);
138-
reject(
143+
this.rejectRequest(
144+
requestId,
139145
new Error(
140146
`Mock request timed out after ${this.timeout}ms: ${method} ${endpoint}`
141147
)
@@ -145,18 +151,40 @@ export class BatchMockCollector {
145151
this.pendingRequests.set(requestId, {
146152
request,
147153
resolve: (data) => {
154+
settleCompletion({ status: "fulfilled", value: undefined });
148155
resolve(data as T);
149156
},
150157
reject: (error) => {
158+
settleCompletion({ status: "rejected", reason: error });
151159
reject(error);
152160
},
153161
timeoutId,
162+
completion,
154163
});
155164

156165
this.enqueueRequest(requestId);
157166
});
158167
}
159168

169+
/**
170+
* Wait for all requests that are currently pending to settle. Requests
171+
* created after this method is called are not included.
172+
*/
173+
async waitForPendingRequests(): Promise<void> {
174+
const pendingCompletions = Array.from(this.pendingRequests.values()).map(
175+
(pending) => pending.completion
176+
);
177+
178+
const results = await Promise.all(pendingCompletions);
179+
const rejected = results.find(
180+
(result): result is PromiseRejectedResult => result.status === "rejected"
181+
);
182+
183+
if (rejected) {
184+
throw rejected.reason;
185+
}
186+
}
187+
160188
/**
161189
* Close the underlying connection and fail all pending requests.
162190
*/

test/index.test.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,45 @@ describe("BatchMockCollector", () => {
122122

123123
await collector.close();
124124
});
125+
126+
it("waits for pending requests to settle", async () => {
127+
wss = new WebSocketServer({ port: 0 });
128+
const address = wss.address() as AddressInfo;
129+
130+
wss.on("connection", (socket) => {
131+
socket.on("message", (data) => {
132+
const message = JSON.parse(data.toString()) as BatchMockRequestMessage;
133+
setTimeout(() => {
134+
const response: BatchMockResponseMessage = {
135+
type: BATCH_MOCK_RESPONSE,
136+
batchId: "batch-test",
137+
mocks: message.requests.map((request) => ({
138+
requestId: request.requestId,
139+
data: { endpoint: request.endpoint },
140+
})),
141+
};
142+
socket.send(JSON.stringify(response));
143+
}, 5);
144+
});
145+
});
146+
147+
const collector = await connect({
148+
port: address.port,
149+
timeout: 1_000,
150+
batchDebounceMs: 0,
151+
});
152+
153+
const usersPromise = collector.requestMock("/api/users", "GET");
154+
const ordersPromise = collector.requestMock("/api/orders", "GET");
155+
156+
await collector.waitForPendingRequests();
157+
const [users, orders] = await Promise.all([usersPromise, ordersPromise]);
158+
159+
expect(users).toEqual({ endpoint: "/api/users" });
160+
expect(orders).toEqual({ endpoint: "/api/orders" });
161+
162+
await collector.close();
163+
});
125164
});
126165

127166
describe("TestMockMCPServer", () => {

0 commit comments

Comments
 (0)