@schmock/core 1.0.3 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/dist/builder.d.ts +13 -5
  2. package/dist/builder.d.ts.map +1 -1
  3. package/dist/builder.js +147 -60
  4. package/dist/constants.d.ts +6 -0
  5. package/dist/constants.d.ts.map +1 -0
  6. package/dist/constants.js +20 -0
  7. package/dist/errors.d.ts.map +1 -1
  8. package/dist/errors.js +3 -1
  9. package/dist/index.d.ts +3 -3
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +20 -11
  12. package/dist/parser.d.ts.map +1 -1
  13. package/dist/parser.js +2 -17
  14. package/dist/types.d.ts +17 -210
  15. package/dist/types.d.ts.map +1 -1
  16. package/dist/types.js +1 -0
  17. package/package.json +4 -4
  18. package/src/builder.test.ts +2 -2
  19. package/src/builder.ts +232 -108
  20. package/src/constants.test.ts +59 -0
  21. package/src/constants.ts +25 -0
  22. package/src/errors.ts +3 -1
  23. package/src/index.ts +41 -29
  24. package/src/namespace.test.ts +3 -2
  25. package/src/parser.property.test.ts +495 -0
  26. package/src/parser.ts +2 -20
  27. package/src/route-matching.test.ts +1 -1
  28. package/src/steps/async-support.steps.ts +101 -91
  29. package/src/steps/basic-usage.steps.ts +49 -36
  30. package/src/steps/developer-experience.steps.ts +110 -94
  31. package/src/steps/error-handling.steps.ts +90 -66
  32. package/src/steps/fluent-api.steps.ts +75 -72
  33. package/src/steps/http-methods.steps.ts +33 -33
  34. package/src/steps/performance-reliability.steps.ts +52 -88
  35. package/src/steps/plugin-integration.steps.ts +176 -176
  36. package/src/steps/request-history.steps.ts +333 -0
  37. package/src/steps/state-concurrency.steps.ts +418 -316
  38. package/src/steps/stateful-workflows.steps.ts +138 -136
  39. package/src/types.ts +20 -259
@@ -1,7 +1,70 @@
1
1
  import { describeFeature, loadFeature } from "@amiceli/vitest-cucumber";
2
2
  import { expect } from "vitest";
3
3
  import { schmock } from "../index";
4
- import type { CallableMockInstance } from "../types";
4
+ import type { CallableMockInstance, Plugin } from "../types";
5
+
6
+ // ---------- State shape interfaces for each scenario ----------
7
+ // Index signatures ensure compatibility with Record<string, unknown>
8
+
9
+ interface CounterState { counter: number; [k: string]: unknown }
10
+ interface MultiCounterState { users: number; posts: number; comments: number; [k: string]: unknown }
11
+ interface ValueState { value: number; [k: string]: unknown }
12
+ interface UpdateBody { newValue: number }
13
+ interface PluginState {
14
+ requestCount: number;
15
+ pluginData: Record<string, number>;
16
+ [k: string]: unknown;
17
+ }
18
+ interface SessionState {
19
+ sessions: Record<string, { user: string; loginTime: string }>;
20
+ activeUsers: number;
21
+ [k: string]: unknown;
22
+ }
23
+ interface LoginBody { username: string }
24
+ interface DataItem { id: number; value: number }
25
+ interface DataArrayState { data: DataItem[]; [k: string]: unknown }
26
+ interface DataUpdateBody { value: number }
27
+ interface CacheState {
28
+ cache: Record<string, { value: string; timestamp: number }>;
29
+ cacheSize: number;
30
+ maxCacheSize: number;
31
+ [k: string]: unknown;
32
+ }
33
+ interface CacheBody { value: string }
34
+ interface NestedUsersState {
35
+ users: {
36
+ profiles: Record<string, Record<string, unknown>>;
37
+ preferences: Record<string, Record<string, unknown>>;
38
+ activity: Record<string, Array<Record<string, unknown>>>;
39
+ };
40
+ [k: string]: unknown;
41
+ }
42
+ interface TransactionState {
43
+ transactions: Array<{ amount: number; balanceAfter: number; timestamp: number }>;
44
+ balance: number;
45
+ [k: string]: unknown;
46
+ }
47
+ interface TransactionBody { amount: number }
48
+
49
+ // ---------- Response body interfaces for assertions ----------
50
+
51
+ interface CounterBody { counter: number; previous: number }
52
+ interface TypeCountBody { type: string; count: number }
53
+ interface InstanceBody { instance: number; value?: number; updated?: number }
54
+ interface PluginResponseBody { plugin1Count: number; plugin2Count: number; base: string }
55
+ interface SessionResponseBody {
56
+ sessionId: string;
57
+ message: string;
58
+ activeUsers: number;
59
+ session?: { user: string; loginTime: string };
60
+ totalSessions?: number;
61
+ error?: string;
62
+ }
63
+ interface DataUpdateResponseBody { id: number; updated: number; total: number }
64
+ interface StatsResponseBody { total: number; average: number; items: number }
65
+ interface CacheResponseBody { key: string; cached: boolean; cacheSize: number }
66
+ interface ProfileResponseBody { userId: string; profile?: unknown; preferences?: unknown; activityCount?: number }
67
+ interface TransactionResponseBody { success: boolean; newBalance: number; transactionCount: number; error?: string; balance?: number }
5
68
 
6
69
  const feature = await loadFeature("../../features/state-concurrency.feature");
7
70
 
@@ -12,18 +75,19 @@ describeFeature(feature, ({ Scenario }) => {
12
75
  let responses: any[] = [];
13
76
 
14
77
  Scenario("Concurrent state updates with race conditions", ({ Given, When, Then, And }) => {
15
- Given("I create a mock with shared counter state:", (_, docString: string) => {
78
+ Given("I create a mock with a shared counter state", () => {
16
79
  mock = schmock({ state: { counter: 0 } });
17
- mock('POST /increment', ({ state }) => {
18
- const current = state.counter;
19
- state.counter = current + 1;
20
- return { counter: state.counter, previous: current };
80
+ mock("POST /increment", ({ state }) => {
81
+ const s = state as CounterState;
82
+ const current = s.counter;
83
+ s.counter = current + 1;
84
+ return { counter: s.counter, previous: current };
21
85
  });
22
86
  });
23
87
 
24
88
  When("I make 5 concurrent increment requests", async () => {
25
- const promises = Array.from({ length: 5 }, () =>
26
- mock.handle('POST', '/increment')
89
+ const promises = Array.from({ length: 5 }, () =>
90
+ mock.handle("POST", "/increment")
27
91
  );
28
92
  responses = await Promise.all(promises);
29
93
  });
@@ -32,86 +96,87 @@ describeFeature(feature, ({ Scenario }) => {
32
96
  expect(responses).toHaveLength(5);
33
97
  responses.forEach(response => {
34
98
  expect(response.status).toBe(200);
35
- expect(response.body).toHaveProperty('counter');
36
- expect(response.body).toHaveProperty('previous');
99
+ expect(response.body).toHaveProperty("counter");
100
+ expect(response.body).toHaveProperty("previous");
37
101
  });
38
102
  });
39
103
 
40
104
  And("the final counter should reflect all increments", () => {
41
- const finalCounters = responses.map(r => r.body.counter);
105
+ const finalCounters = responses.map(r => (r.body as CounterBody).counter);
42
106
  const maxCounter = Math.max(...finalCounters);
43
107
  expect(maxCounter).toBe(5);
44
108
  });
45
109
 
46
110
  And("each response should show sequential progression", () => {
47
- const previousValues = responses.map(r => r.body.previous);
48
- const counterValues = responses.map(r => r.body.counter);
49
-
111
+ const previousValues = responses.map(r => (r.body as CounterBody).previous);
112
+ const counterValues = responses.map(r => (r.body as CounterBody).counter);
113
+
50
114
  // All previous values should be unique (no duplicates due to race conditions)
51
115
  const uniquePrevious = new Set(previousValues);
52
116
  expect(uniquePrevious.size).toBe(5);
53
-
117
+
54
118
  // Counter values should be previous + 1
55
119
  responses.forEach(response => {
56
- expect(response.body.counter).toBe(response.body.previous + 1);
120
+ const body = response.body as CounterBody;
121
+ expect(body.counter).toBe(body.previous + 1);
57
122
  });
58
123
  });
59
124
  });
60
125
 
61
126
  Scenario("Concurrent access to different state properties", ({ Given, When, Then, And }) => {
62
- Given("I create a mock with multiple state properties:", (_, docString: string) => {
63
- mock = schmock({
64
- state: {
65
- users: 0,
66
- posts: 0,
67
- comments: 0
68
- }
127
+ Given("I create a mock with separate user, post, and comment counters", () => {
128
+ mock = schmock({
129
+ state: {
130
+ users: 0,
131
+ posts: 0,
132
+ comments: 0,
133
+ },
69
134
  });
70
-
71
- mock('POST /users', ({ state }) => {
72
- state.users++;
73
- return { type: 'user', count: state.users };
135
+ mock("POST /users", ({ state }) => {
136
+ const s = state as MultiCounterState;
137
+ s.users++;
138
+ return { type: "user", count: s.users };
74
139
  });
75
-
76
- mock('POST /posts', ({ state }) => {
77
- state.posts++;
78
- return { type: 'post', count: state.posts };
140
+ mock("POST /posts", ({ state }) => {
141
+ const s = state as MultiCounterState;
142
+ s.posts++;
143
+ return { type: "post", count: s.posts };
79
144
  });
80
-
81
- mock('POST /comments', ({ state }) => {
82
- state.comments++;
83
- return { type: 'comment', count: state.comments };
145
+ mock("POST /comments", ({ state }) => {
146
+ const s = state as MultiCounterState;
147
+ s.comments++;
148
+ return { type: "comment", count: s.comments };
84
149
  });
85
150
  });
86
151
 
87
152
  When("I make concurrent requests to different endpoints", async () => {
88
153
  const promises = [
89
- mock.handle('POST', '/users'),
90
- mock.handle('POST', '/users'),
91
- mock.handle('POST', '/posts'),
92
- mock.handle('POST', '/posts'),
93
- mock.handle('POST', '/comments'),
94
- mock.handle('POST', '/comments'),
95
- mock.handle('POST', '/users')
154
+ mock.handle("POST", "/users"),
155
+ mock.handle("POST", "/users"),
156
+ mock.handle("POST", "/posts"),
157
+ mock.handle("POST", "/posts"),
158
+ mock.handle("POST", "/comments"),
159
+ mock.handle("POST", "/comments"),
160
+ mock.handle("POST", "/users"),
96
161
  ];
97
162
  responses = await Promise.all(promises);
98
163
  });
99
164
 
100
165
  Then("each endpoint should maintain its own counter correctly", () => {
101
- const userResponses = responses.filter(r => r.body.type === 'user');
102
- const postResponses = responses.filter(r => r.body.type === 'post');
103
- const commentResponses = responses.filter(r => r.body.type === 'comment');
104
-
166
+ const userResponses = responses.filter(r => (r.body as TypeCountBody).type === "user");
167
+ const postResponses = responses.filter(r => (r.body as TypeCountBody).type === "post");
168
+ const commentResponses = responses.filter(r => (r.body as TypeCountBody).type === "comment");
169
+
105
170
  expect(userResponses).toHaveLength(3);
106
171
  expect(postResponses).toHaveLength(2);
107
172
  expect(commentResponses).toHaveLength(2);
108
173
  });
109
174
 
110
175
  And("the final state should show accurate counts for all properties", () => {
111
- const userCounts = responses.filter(r => r.body.type === 'user').map(r => r.body.count);
112
- const postCounts = responses.filter(r => r.body.type === 'post').map(r => r.body.count);
113
- const commentCounts = responses.filter(r => r.body.type === 'comment').map(r => r.body.count);
114
-
176
+ const userCounts = responses.filter(r => (r.body as TypeCountBody).type === "user").map(r => (r.body as TypeCountBody).count);
177
+ const postCounts = responses.filter(r => (r.body as TypeCountBody).type === "post").map(r => (r.body as TypeCountBody).count);
178
+ const commentCounts = responses.filter(r => (r.body as TypeCountBody).type === "comment").map(r => (r.body as TypeCountBody).count);
179
+
115
180
  expect(Math.max(...userCounts)).toBe(3);
116
181
  expect(Math.max(...postCounts)).toBe(2);
117
182
  expect(Math.max(...commentCounts)).toBe(2);
@@ -119,277 +184,300 @@ describeFeature(feature, ({ Scenario }) => {
119
184
  });
120
185
 
121
186
  Scenario("State isolation between different mock instances", ({ Given, When, Then, And }) => {
122
- Given("I create two separate mock instances with state:", (_, docString: string) => {
187
+ Given("I create two separate mock instances with independent state", () => {
123
188
  mock1 = schmock({ state: { value: 10 } });
124
189
  mock2 = schmock({ state: { value: 20 } });
125
-
126
- mock1('GET /value', ({ state }) => ({ instance: 1, value: state.value }));
127
- mock2('GET /value', ({ state }) => ({ instance: 2, value: state.value }));
128
-
129
- mock1('POST /update', ({ state, body }) => {
130
- state.value = body.newValue;
131
- return { instance: 1, updated: state.value };
190
+
191
+ mock1("GET /value", ({ state }) => ({ instance: 1, value: (state as ValueState).value }));
192
+ mock2("GET /value", ({ state }) => ({ instance: 2, value: (state as ValueState).value }));
193
+
194
+ mock1("POST /update", ({ state, body }) => {
195
+ const s = state as ValueState;
196
+ const b = body as UpdateBody;
197
+ s.value = b.newValue;
198
+ return { instance: 1, updated: s.value };
132
199
  });
133
-
134
- mock2('POST /update', ({ state, body }) => {
135
- state.value = body.newValue;
136
- return { instance: 2, updated: state.value };
200
+ mock2("POST /update", ({ state, body }) => {
201
+ const s = state as ValueState;
202
+ const b = body as UpdateBody;
203
+ s.value = b.newValue;
204
+ return { instance: 2, updated: s.value };
137
205
  });
138
206
  });
139
207
 
140
208
  When("I update state in both mock instances concurrently", async () => {
141
209
  const promises = [
142
- mock1.handle('POST', '/update', { body: { newValue: 100 } }),
143
- mock2.handle('POST', '/update', { body: { newValue: 200 } }),
144
- mock1.handle('GET', '/value'),
145
- mock2.handle('GET', '/value')
210
+ mock1.handle("POST", "/update", { body: { newValue: 100 } }),
211
+ mock2.handle("POST", "/update", { body: { newValue: 200 } }),
212
+ mock1.handle("GET", "/value"),
213
+ mock2.handle("GET", "/value"),
146
214
  ];
147
215
  responses = await Promise.all(promises);
148
216
  });
149
217
 
150
218
  Then("each mock instance should maintain its own isolated state", () => {
151
- const instance1Responses = responses.filter(r => r.body.instance === 1);
152
- const instance2Responses = responses.filter(r => r.body.instance === 2);
153
-
219
+ const instance1Responses = responses.filter(r => (r.body as InstanceBody).instance === 1);
220
+ const instance2Responses = responses.filter(r => (r.body as InstanceBody).instance === 2);
221
+
154
222
  expect(instance1Responses).toHaveLength(2);
155
223
  expect(instance2Responses).toHaveLength(2);
156
224
  });
157
225
 
158
226
  And("changes in one instance should not affect the other", () => {
159
- const instance1Get = responses.find(r => r.body.instance === 1 && r.body.value !== undefined);
160
- const instance2Get = responses.find(r => r.body.instance === 2 && r.body.value !== undefined);
161
-
162
- expect(instance1Get?.body.value).toBe(100);
163
- expect(instance2Get?.body.value).toBe(200);
227
+ const instance1Get = responses.find(r => (r.body as InstanceBody).instance === 1 && (r.body as InstanceBody).value !== undefined);
228
+ const instance2Get = responses.find(r => (r.body as InstanceBody).instance === 2 && (r.body as InstanceBody).value !== undefined);
229
+
230
+ expect((instance1Get?.body as InstanceBody | undefined)?.value).toBe(100);
231
+ expect((instance2Get?.body as InstanceBody | undefined)?.value).toBe(200);
164
232
  });
165
233
  });
166
234
 
167
235
  Scenario("Concurrent plugin state modifications", ({ Given, When, Then, And }) => {
168
- Given("I create a mock with stateful plugins:", (_, docString: string) => {
236
+ Given("I create a mock with stateful counter and tracker plugins", () => {
169
237
  mock = schmock({ state: { requestCount: 0, pluginData: {} } });
170
-
171
- const plugin1 = {
172
- name: 'counter-plugin',
173
- process: (ctx: any, response: any) => {
174
- ctx.state.requestCount++;
175
- ctx.state.pluginData.plugin1 = (ctx.state.pluginData.plugin1 || 0) + 1;
238
+
239
+ const plugin1: Plugin = {
240
+ name: "counter-plugin",
241
+ process: (ctx, response) => {
242
+ const rs = ctx.routeState! as PluginState;
243
+ rs.requestCount++;
244
+ rs.pluginData.plugin1 = (rs.pluginData.plugin1 || 0) + 1;
176
245
  return {
177
246
  context: ctx,
178
- response: { ...response, plugin1Count: ctx.state.pluginData.plugin1 }
247
+ response: { ...(response as Record<string, unknown>), plugin1Count: rs.pluginData.plugin1 },
179
248
  };
180
- }
249
+ },
181
250
  };
182
-
183
- const plugin2 = {
184
- name: 'tracker-plugin',
185
- process: (ctx: any, response: any) => {
186
- ctx.state.pluginData.plugin2 = (ctx.state.pluginData.plugin2 || 0) + 1;
251
+
252
+ const plugin2: Plugin = {
253
+ name: "tracker-plugin",
254
+ process: (ctx, response) => {
255
+ const rs = ctx.routeState! as PluginState;
256
+ rs.pluginData.plugin2 = (rs.pluginData.plugin2 || 0) + 1;
187
257
  return {
188
258
  context: ctx,
189
- response: { ...response, plugin2Count: ctx.state.pluginData.plugin2 }
259
+ response: { ...(response as Record<string, unknown>), plugin2Count: rs.pluginData.plugin2 },
190
260
  };
191
- }
261
+ },
192
262
  };
193
-
194
- mock('GET /data', { base: 'data' }).pipe(plugin1).pipe(plugin2);
263
+
264
+ mock("GET /data", { base: "data" }).pipe(plugin1).pipe(plugin2);
195
265
  });
196
266
 
197
267
  When("I make concurrent requests through the plugin pipeline", async () => {
198
- const promises = Array.from({ length: 4 }, () =>
199
- mock.handle('GET', '/data')
268
+ const promises = Array.from({ length: 4 }, () =>
269
+ mock.handle("GET", "/data")
200
270
  );
201
271
  responses = await Promise.all(promises);
202
272
  });
203
273
 
204
274
  Then("each plugin should correctly update its state counters", () => {
205
- // Just check that we got responses - the plugin behavior is complex with concurrency
206
275
  expect(responses).toHaveLength(4);
207
276
  responses.forEach(response => {
208
- expect(response).toBeDefined();
209
- expect(response.body).toBeDefined();
277
+ const body = response.body as PluginResponseBody;
278
+ expect(response.status).toBe(200);
279
+ expect(typeof body.plugin1Count).toBe("number");
280
+ expect(typeof body.plugin2Count).toBe("number");
210
281
  });
282
+
283
+ // Each plugin should have counted all 4 requests
284
+ const plugin1Counts = responses.map(r => (r.body as PluginResponseBody).plugin1Count);
285
+ const plugin2Counts = responses.map(r => (r.body as PluginResponseBody).plugin2Count);
286
+ expect(Math.max(...plugin1Counts)).toBe(4);
287
+ expect(Math.max(...plugin2Counts)).toBe(4);
211
288
  });
212
289
 
213
290
  And("the global request count should be accurate", () => {
214
- // Just verify all requests completed
291
+ // All 4 responses should have the base data preserved
215
292
  expect(responses).toHaveLength(4);
216
293
  responses.forEach(response => {
217
- expect(response).toBeDefined();
294
+ expect((response.body as PluginResponseBody).base).toBe("data");
218
295
  });
219
296
  });
220
297
 
221
298
  And("plugin state should not interfere with each other", () => {
222
- responses.forEach(response => {
223
- expect(response).toBeDefined();
224
- expect(response.body).toBeDefined();
225
- // Verify the base response structure is maintained
226
- });
299
+ // plugin1Count and plugin2Count should increment independently
300
+ // Both should reach 4 since there are 4 requests
301
+ const plugin1Max = Math.max(...responses.map(r => (r.body as PluginResponseBody).plugin1Count));
302
+ const plugin2Max = Math.max(...responses.map(r => (r.body as PluginResponseBody).plugin2Count));
303
+ expect(plugin1Max).toBe(plugin2Max);
227
304
  });
228
305
  });
229
306
 
230
307
  Scenario("State persistence across request contexts", ({ Given, When, Then, And }) => {
231
308
  let sessionIds: string[] = [];
232
309
 
233
- Given("I create a mock with persistent session state:", (_, docString: string) => {
234
- mock = schmock({
235
- state: {
310
+ Given("I create a mock with persistent session state", () => {
311
+ mock = schmock({
312
+ state: {
236
313
  sessions: {},
237
- activeUsers: 0
238
- }
239
- });
240
-
241
- mock('POST /login', ({ state, body }) => {
242
- const sessionId = 'session_' + Date.now() + '_' + Math.random().toString(36).substring(7);
243
- state.sessions[sessionId] = {
244
- user: body.username,
245
- loginTime: new Date().toISOString()
314
+ activeUsers: 0,
315
+ },
316
+ });
317
+
318
+ mock("POST /login", ({ state, body }) => {
319
+ const s = state as SessionState;
320
+ const b = body as LoginBody;
321
+ const sessionId = "session_" + Date.now();
322
+ s.sessions[sessionId] = {
323
+ user: b.username,
324
+ loginTime: new Date().toISOString(),
246
325
  };
247
- state.activeUsers++;
248
- return { sessionId, message: 'Logged in', activeUsers: state.activeUsers };
249
- });
250
-
251
- mock('GET /sessions/:sessionId', ({ state, params }) => {
252
- const session = state.sessions[params.sessionId];
253
- return session ? { session, totalSessions: Object.keys(state.sessions).length }
254
- : [404, { error: 'Session not found' }];
255
- });
256
-
257
- mock('DELETE /sessions/:sessionId', ({ state, params }) => {
258
- if (state.sessions[params.sessionId]) {
259
- delete state.sessions[params.sessionId];
260
- state.activeUsers--;
261
- return { message: 'Logged out', activeUsers: state.activeUsers };
326
+ s.activeUsers++;
327
+ return { sessionId, message: "Logged in", activeUsers: s.activeUsers };
328
+ });
329
+
330
+ mock("GET /sessions/:sessionId", ({ state, params }) => {
331
+ const s = state as SessionState;
332
+ const session = s.sessions[params.sessionId];
333
+ return session
334
+ ? { session, totalSessions: Object.keys(s.sessions).length }
335
+ : [404, { error: "Session not found" }];
336
+ });
337
+
338
+ mock("DELETE /sessions/:sessionId", ({ state, params }) => {
339
+ const s = state as SessionState;
340
+ if (s.sessions[params.sessionId]) {
341
+ delete s.sessions[params.sessionId];
342
+ s.activeUsers--;
343
+ return { message: "Logged out", activeUsers: s.activeUsers };
262
344
  }
263
- return [404, { error: 'Session not found' }];
345
+ return [404, { error: "Session not found" }];
264
346
  });
265
347
  });
266
348
 
267
349
  When("I simulate concurrent user login and logout operations", async () => {
268
350
  // First, login some users
269
351
  const loginPromises = [
270
- mock.handle('POST', '/login', { body: { username: 'user1' } }),
271
- mock.handle('POST', '/login', { body: { username: 'user2' } }),
272
- mock.handle('POST', '/login', { body: { username: 'user3' } })
352
+ mock.handle("POST", "/login", { body: { username: "user1" } }),
353
+ mock.handle("POST", "/login", { body: { username: "user2" } }),
354
+ mock.handle("POST", "/login", { body: { username: "user3" } }),
273
355
  ];
274
-
356
+
275
357
  const loginResponses = await Promise.all(loginPromises);
276
- sessionIds = loginResponses.map(r => r.body.sessionId);
277
-
358
+ sessionIds = loginResponses.map(r => (r.body as SessionResponseBody).sessionId);
359
+
278
360
  // Then make concurrent operations
279
361
  const promises = [
280
- mock.handle('GET', `/sessions/${sessionIds[0]}`),
281
- mock.handle('GET', `/sessions/${sessionIds[1]}`),
282
- mock.handle('DELETE', `/sessions/${sessionIds[0]}`),
283
- mock.handle('POST', '/login', { body: { username: 'user4' } })
362
+ mock.handle("GET", `/sessions/${sessionIds[0]}`),
363
+ mock.handle("GET", `/sessions/${sessionIds[1]}`),
364
+ mock.handle("DELETE", `/sessions/${sessionIds[0]}`),
365
+ mock.handle("POST", "/login", { body: { username: "user4" } }),
284
366
  ];
285
-
367
+
286
368
  responses = [...loginResponses, ...await Promise.all(promises)];
287
369
  });
288
370
 
289
371
  Then("session state should be maintained correctly across requests", () => {
290
- const sessionGets = responses.filter(r => r.body.session);
372
+ const sessionGets = responses.filter(r => (r.body as SessionResponseBody).session);
291
373
  expect(sessionGets.length).toBeGreaterThan(0);
292
-
374
+
293
375
  sessionGets.forEach(response => {
294
- expect(response.body.session).toHaveProperty('user');
295
- expect(response.body.session).toHaveProperty('loginTime');
376
+ const body = response.body as SessionResponseBody;
377
+ expect(body.session).toHaveProperty("user");
378
+ expect(body.session).toHaveProperty("loginTime");
296
379
  });
297
380
  });
298
381
 
299
382
  And("active user count should remain consistent", () => {
300
- const loginResponses = responses.filter(r => r.body.message === 'Logged in');
301
- const logoutResponses = responses.filter(r => r.body.message === 'Logged out');
302
-
383
+ const loginResponses = responses.filter(r => (r.body as SessionResponseBody).message === "Logged in");
384
+ const logoutResponses = responses.filter(r => (r.body as SessionResponseBody).message === "Logged out");
385
+
303
386
  expect(loginResponses).toHaveLength(4); // 3 initial + 1 concurrent
304
387
  expect(logoutResponses).toHaveLength(1);
305
-
388
+
306
389
  // Due to concurrent execution, the exact count might vary
307
- const finalActiveUsers = logoutResponses[0]?.body.activeUsers;
390
+ const finalActiveUsers = (logoutResponses[0]?.body as SessionResponseBody | undefined)?.activeUsers;
308
391
  expect(finalActiveUsers).toBeGreaterThan(0);
309
392
  expect(finalActiveUsers).toBeLessThan(4);
310
393
  });
311
394
 
312
395
  And("session cleanup should work properly", () => {
313
- const deleteResponse = responses.find(r => r.body.message === 'Logged out');
396
+ const deleteResponse = responses.find(r => (r.body as SessionResponseBody).message === "Logged out");
314
397
  expect(deleteResponse).toBeDefined();
315
- expect(deleteResponse?.body.activeUsers).toBeGreaterThanOrEqual(0);
398
+ expect((deleteResponse?.body as SessionResponseBody | undefined)?.activeUsers).toBeGreaterThanOrEqual(0);
316
399
  });
317
400
  });
318
401
 
319
402
  Scenario("Large state object concurrent modifications", ({ Given, When, Then, And }) => {
320
- Given("I create a mock with large state object:", (_, docString: string) => {
321
- mock = schmock({
322
- state: {
323
- data: Array.from({ length: 1000 }, (_, i) => ({ id: i, value: 0 }))
324
- }
325
- });
326
-
327
- mock('PATCH /data/:id', ({ state, params, body }) => {
403
+ Given("I create a mock with a large 1000-item data array", () => {
404
+ mock = schmock({
405
+ state: {
406
+ data: Array.from({ length: 1000 }, (_, i) => ({ id: i, value: 0 })),
407
+ },
408
+ });
409
+
410
+ mock("PATCH /data/:id", ({ state, params, body }) => {
411
+ const s = state as DataArrayState;
412
+ const b = body as DataUpdateBody;
328
413
  const id = parseInt(params.id);
329
- const item = state.data.find(d => d.id === id);
414
+ const item = s.data.find((d: DataItem) => d.id === id);
330
415
  if (item) {
331
- item.value = body.value;
332
- return { id, updated: item.value, total: state.data.length };
416
+ item.value = b.value;
417
+ return { id, updated: item.value, total: s.data.length };
333
418
  }
334
- return [404, { error: 'Item not found' }];
419
+ return [404, { error: "Item not found" }];
335
420
  });
336
-
337
- mock('GET /data/stats', ({ state }) => {
338
- const total = state.data.reduce((sum, item) => sum + item.value, 0);
339
- const average = total / state.data.length;
340
- return { total, average, items: state.data.length };
421
+
422
+ mock("GET /data/stats", ({ state }) => {
423
+ const s = state as DataArrayState;
424
+ const total = s.data.reduce((sum: number, item: DataItem) => sum + item.value, 0);
425
+ const average = total / s.data.length;
426
+ return { total, average, items: s.data.length };
341
427
  });
342
428
  });
343
429
 
344
430
  When("I make concurrent updates to different parts of large state", async () => {
345
431
  const updatePromises = [
346
- mock.handle('PATCH', '/data/0', { body: { value: 10 } }),
347
- mock.handle('PATCH', '/data/100', { body: { value: 20 } }),
348
- mock.handle('PATCH', '/data/500', { body: { value: 30 } }),
349
- mock.handle('PATCH', '/data/999', { body: { value: 40 } }),
350
- mock.handle('GET', '/data/stats')
432
+ mock.handle("PATCH", "/data/0", { body: { value: 10 } }),
433
+ mock.handle("PATCH", "/data/100", { body: { value: 20 } }),
434
+ mock.handle("PATCH", "/data/500", { body: { value: 30 } }),
435
+ mock.handle("PATCH", "/data/999", { body: { value: 40 } }),
436
+ mock.handle("GET", "/data/stats"),
351
437
  ];
352
-
438
+
353
439
  responses = await Promise.all(updatePromises);
354
440
  });
355
441
 
356
442
  Then("all updates should be applied correctly", () => {
357
- const updateResponses = responses.filter(r => r.body.updated !== undefined);
443
+ const updateResponses = responses.filter(r => (r.body as DataUpdateResponseBody).updated !== undefined);
358
444
  expect(updateResponses).toHaveLength(4);
359
-
445
+
360
446
  updateResponses.forEach((response, index) => {
361
447
  const expectedValues = [10, 20, 30, 40];
362
- expect(response.body.updated).toBe(expectedValues[index]);
448
+ expect((response.body as DataUpdateResponseBody).updated).toBe(expectedValues[index]);
363
449
  });
364
450
  });
365
451
 
366
452
  And("statistics should reflect the correct aggregated values", () => {
367
- const statsResponse = responses.find(r => r.body && r.body.total !== undefined);
453
+ const statsResponse = responses.find(r => r.body && (r.body as StatsResponseBody).total !== undefined);
368
454
  expect(statsResponse).toBeDefined();
369
-
455
+
370
456
  // Due to concurrent execution, the stats might be calculated before all updates complete
371
457
  // Just verify the structure and that the total makes sense
372
- if (statsResponse?.body.total !== undefined) {
373
- expect(statsResponse.body.total).toBeGreaterThanOrEqual(0);
458
+ const statsBody = statsResponse?.body as StatsResponseBody | undefined;
459
+ if (statsBody?.total !== undefined) {
460
+ expect(statsBody.total).toBeGreaterThanOrEqual(0);
374
461
  }
375
- if (statsResponse?.body.average !== undefined) {
376
- expect(statsResponse.body.average).toBeGreaterThanOrEqual(0);
462
+ if (statsBody?.average !== undefined) {
463
+ expect(statsBody.average).toBeGreaterThanOrEqual(0);
377
464
  }
378
- if (statsResponse?.body.items !== undefined) {
379
- expect(statsResponse.body.items).toBe(1000);
465
+ if (statsBody?.items !== undefined) {
466
+ expect(statsBody.items).toBe(1000);
380
467
  }
381
468
  });
382
469
 
383
470
  And("no data corruption should occur", () => {
384
471
  responses.forEach(response => {
472
+ const body = response.body as Record<string, unknown>;
385
473
  expect(response.status).toBe(200);
386
- if (response.body && response.body.total !== undefined && response.body.items !== undefined) {
387
- expect(response.body.items).toBe(1000); // Array length should remain unchanged
474
+ if (body && body.total !== undefined && body.items !== undefined) {
475
+ expect(body.items).toBe(1000); // Array length should remain unchanged
388
476
  }
389
- if (response.body && response.body.updated !== undefined) {
390
- expect(typeof response.body.updated).toBe('number');
391
- if (response.body.total !== undefined) {
392
- expect(response.body.total).toBe(1000);
477
+ if (body && body.updated !== undefined) {
478
+ expect(typeof body.updated).toBe("number");
479
+ if (body.total !== undefined) {
480
+ expect(body.total).toBe(1000);
393
481
  }
394
482
  }
395
483
  });
@@ -397,234 +485,248 @@ describeFeature(feature, ({ Scenario }) => {
397
485
  });
398
486
 
399
487
  Scenario("State cleanup and memory management", ({ Given, When, Then, And }) => {
400
- Given("I create a mock with temporary state management:", (_, docString: string) => {
401
- mock = schmock({
402
- state: {
488
+ Given("I create a mock with LRU cache state", () => {
489
+ mock = schmock({
490
+ state: {
403
491
  cache: {},
404
492
  cacheSize: 0,
405
- maxCacheSize: 5
406
- }
493
+ maxCacheSize: 5,
494
+ },
407
495
  });
408
-
409
- mock('POST /cache/:key', ({ state, params, body }) => {
496
+
497
+ mock("POST /cache/:key", ({ state, params, body }) => {
498
+ const s = state as CacheState;
499
+ const b = body as CacheBody;
410
500
  // Simple LRU-like cache with size limit
411
- if (state.cacheSize >= state.maxCacheSize) {
412
- const oldestKey = Object.keys(state.cache)[0];
413
- delete state.cache[oldestKey];
414
- state.cacheSize--;
501
+ if (s.cacheSize >= s.maxCacheSize) {
502
+ const oldestKey = Object.keys(s.cache)[0];
503
+ delete s.cache[oldestKey];
504
+ s.cacheSize--;
415
505
  }
416
-
417
- state.cache[params.key] = {
418
- value: body.value,
419
- timestamp: Date.now()
506
+
507
+ s.cache[params.key] = {
508
+ value: b.value,
509
+ timestamp: Date.now(),
420
510
  };
421
- state.cacheSize++;
422
-
423
- return {
424
- key: params.key,
425
- cached: true,
426
- cacheSize: state.cacheSize
511
+ s.cacheSize++;
512
+
513
+ return {
514
+ key: params.key,
515
+ cached: true,
516
+ cacheSize: s.cacheSize,
427
517
  };
428
518
  });
429
-
430
- mock('GET /cache/:key', ({ state, params }) => {
431
- const item = state.cache[params.key];
432
- return item ? { key: params.key, ...item }
433
- : [404, { error: 'Not found in cache' }];
519
+
520
+ mock("GET /cache/:key", ({ state, params }) => {
521
+ const s = state as CacheState;
522
+ const item = s.cache[params.key];
523
+ return item
524
+ ? { key: params.key, ...item }
525
+ : [404, { error: "Not found in cache" }];
434
526
  });
435
527
  });
436
528
 
437
529
  When("I add items to cache beyond the size limit concurrently", async () => {
438
- const promises = Array.from({ length: 8 }, (_, i) =>
439
- mock.handle('POST', `/cache/item${i}`, { body: { value: `value${i}` } })
530
+ const promises = Array.from({ length: 8 }, (_, i) =>
531
+ mock.handle("POST", `/cache/item${i}`, { body: { value: `value${i}` } })
440
532
  );
441
533
  responses = await Promise.all(promises);
442
534
  });
443
535
 
444
536
  Then("the cache should maintain its size limit", () => {
445
537
  responses.forEach(response => {
446
- expect(response.body.cacheSize).toBeLessThanOrEqual(5);
538
+ expect((response.body as CacheResponseBody).cacheSize).toBeLessThanOrEqual(5);
447
539
  });
448
540
  });
449
541
 
450
542
  And("old items should be evicted properly", () => {
451
- const finalCacheSize = Math.max(...responses.map(r => r.body.cacheSize));
543
+ const finalCacheSize = Math.max(...responses.map(r => (r.body as CacheResponseBody).cacheSize));
452
544
  expect(finalCacheSize).toBe(5);
453
545
  });
454
546
 
455
547
  And("cache size should remain consistent", () => {
456
548
  responses.forEach(response => {
457
- expect(response.body.cached).toBe(true);
458
- expect(typeof response.body.cacheSize).toBe('number');
459
- expect(response.body.cacheSize).toBeGreaterThan(0);
549
+ const body = response.body as CacheResponseBody;
550
+ expect(body.cached).toBe(true);
551
+ expect(typeof body.cacheSize).toBe("number");
552
+ expect(body.cacheSize).toBeGreaterThan(0);
460
553
  });
461
554
  });
462
555
  });
463
556
 
464
557
  Scenario("Nested state object concurrent access", ({ Given, When, Then, And }) => {
465
- Given("I create a mock with deeply nested state:", (_, docString: string) => {
466
- mock = schmock({
467
- state: {
558
+ Given("I create a mock with deeply nested user state", () => {
559
+ mock = schmock({
560
+ state: {
468
561
  users: {
469
562
  profiles: {},
470
563
  preferences: {},
471
- activity: {}
472
- }
473
- }
564
+ activity: {},
565
+ },
566
+ },
474
567
  });
475
-
476
- mock('PUT /users/:id/profile', ({ state, params, body }) => {
477
- if (!state.users.profiles[params.id]) {
478
- state.users.profiles[params.id] = {};
568
+
569
+ mock("PUT /users/:id/profile", ({ state, params, body }) => {
570
+ const s = state as NestedUsersState;
571
+ if (!s.users.profiles[params.id]) {
572
+ s.users.profiles[params.id] = {};
479
573
  }
480
- Object.assign(state.users.profiles[params.id], body);
481
- return { userId: params.id, profile: state.users.profiles[params.id] };
482
- });
483
-
484
- mock('PUT /users/:id/preferences', ({ state, params, body }) => {
485
- state.users.preferences[params.id] = { ...body, updatedAt: Date.now() };
486
- return { userId: params.id, preferences: state.users.preferences[params.id] };
487
- });
488
-
489
- mock('POST /users/:id/activity', ({ state, params, body }) => {
490
- if (!state.users.activity[params.id]) {
491
- state.users.activity[params.id] = [];
574
+ Object.assign(s.users.profiles[params.id], body);
575
+ return { userId: params.id, profile: s.users.profiles[params.id] };
576
+ });
577
+
578
+ mock("PUT /users/:id/preferences", ({ state, params, body }) => {
579
+ const s = state as NestedUsersState;
580
+ s.users.preferences[params.id] = { ...(body as Record<string, unknown>), updatedAt: Date.now() };
581
+ return { userId: params.id, preferences: s.users.preferences[params.id] };
582
+ });
583
+
584
+ mock("POST /users/:id/activity", ({ state, params, body }) => {
585
+ const s = state as NestedUsersState;
586
+ if (!s.users.activity[params.id]) {
587
+ s.users.activity[params.id] = [];
492
588
  }
493
- state.users.activity[params.id].push({ ...body, timestamp: Date.now() });
494
- return {
495
- userId: params.id,
496
- activityCount: state.users.activity[params.id].length
589
+ s.users.activity[params.id].push({ ...(body as Record<string, unknown>), timestamp: Date.now() });
590
+ return {
591
+ userId: params.id,
592
+ activityCount: s.users.activity[params.id].length,
497
593
  };
498
594
  });
499
595
  });
500
596
 
501
597
  When("I make concurrent updates to different nested state sections", async () => {
502
598
  const promises = [
503
- mock.handle('PUT', '/users/1/profile', { body: { name: 'User 1', age: 25 } }),
504
- mock.handle('PUT', '/users/1/preferences', { body: { theme: 'dark', language: 'en' } }),
505
- mock.handle('POST', '/users/1/activity', { body: { action: 'login' } }),
506
- mock.handle('PUT', '/users/2/profile', { body: { name: 'User 2', age: 30 } }),
507
- mock.handle('POST', '/users/2/activity', { body: { action: 'view_page' } }),
508
- mock.handle('POST', '/users/1/activity', { body: { action: 'logout' } })
599
+ mock.handle("PUT", "/users/1/profile", { body: { name: "User 1", age: 25 } }),
600
+ mock.handle("PUT", "/users/1/preferences", { body: { theme: "dark", language: "en" } }),
601
+ mock.handle("POST", "/users/1/activity", { body: { action: "login" } }),
602
+ mock.handle("PUT", "/users/2/profile", { body: { name: "User 2", age: 30 } }),
603
+ mock.handle("POST", "/users/2/activity", { body: { action: "view_page" } }),
604
+ mock.handle("POST", "/users/1/activity", { body: { action: "logout" } }),
509
605
  ];
510
-
606
+
511
607
  responses = await Promise.all(promises);
512
608
  });
513
609
 
514
610
  Then("each nested section should be updated independently", () => {
515
- const profileUpdates = responses.filter(r => r.body.profile);
516
- const preferenceUpdates = responses.filter(r => r.body.preferences);
517
- const activityUpdates = responses.filter(r => r.body.activityCount);
518
-
611
+ const profileUpdates = responses.filter(r => (r.body as ProfileResponseBody).profile);
612
+ const preferenceUpdates = responses.filter(r => (r.body as ProfileResponseBody).preferences);
613
+ const activityUpdates = responses.filter(r => (r.body as ProfileResponseBody).activityCount);
614
+
519
615
  expect(profileUpdates).toHaveLength(2);
520
616
  expect(preferenceUpdates).toHaveLength(1);
521
617
  expect(activityUpdates).toHaveLength(3);
522
618
  });
523
619
 
524
620
  And("no cross-contamination should occur between user data", () => {
525
- const user1Updates = responses.filter(r => r.body.userId === '1');
526
- const user2Updates = responses.filter(r => r.body.userId === '2');
527
-
621
+ const user1Updates = responses.filter(r => (r.body as ProfileResponseBody).userId === "1");
622
+ const user2Updates = responses.filter(r => (r.body as ProfileResponseBody).userId === "2");
623
+
528
624
  expect(user1Updates).toHaveLength(4); // profile, preferences, 2 activities
529
625
  expect(user2Updates).toHaveLength(2); // profile, activity
530
626
  });
531
627
 
532
628
  And("nested state structure should remain intact", () => {
533
629
  responses.forEach(response => {
534
- expect(response.body.userId).toBeDefined();
535
- expect(['1', '2']).toContain(response.body.userId);
630
+ const body = response.body as ProfileResponseBody;
631
+ expect(body.userId).toBeDefined();
632
+ expect(["1", "2"]).toContain(body.userId);
536
633
  });
537
-
538
- const activityUpdates = responses.filter(r => r.body.activityCount);
539
- const user1Activities = activityUpdates.filter(r => r.body.userId === '1');
634
+
635
+ const activityUpdates = responses.filter(r => (r.body as ProfileResponseBody).activityCount);
636
+ const user1Activities = activityUpdates.filter(r => (r.body as ProfileResponseBody).userId === "1");
540
637
  expect(user1Activities.length).toBe(2);
541
- expect(user1Activities[user1Activities.length - 1].body.activityCount).toBe(2);
638
+ expect((user1Activities[user1Activities.length - 1].body as ProfileResponseBody).activityCount).toBe(2);
542
639
  });
543
640
  });
544
641
 
545
642
  Scenario("State rollback on plugin errors", ({ Given, When, Then, And }) => {
546
- Given("I create a mock with error-prone stateful plugin:", (_, docString: string) => {
643
+ Given("I create a mock with an error-prone transaction plugin", () => {
547
644
  mock = schmock({ state: { transactions: [], balance: 100 } });
548
-
549
- const transactionPlugin = {
550
- name: 'transaction-plugin',
551
- process: (ctx: any, response: any) => {
552
- const amount = ctx.body.amount;
553
- const currentBalance = ctx.state.balance;
554
-
645
+
646
+ const transactionPlugin: Plugin = {
647
+ name: "transaction-plugin",
648
+ process: (ctx, response) => {
649
+ const b = ctx.body as TransactionBody;
650
+ const rs = ctx.routeState! as TransactionState;
651
+ const amount = b.amount;
652
+ const currentBalance = rs.balance;
653
+
555
654
  // Simulate transaction processing
556
655
  if (amount > currentBalance) {
557
- throw new Error('Insufficient funds');
656
+ throw new Error("Insufficient funds");
558
657
  }
559
-
560
- ctx.state.balance -= amount;
561
- ctx.state.transactions.push({
658
+
659
+ rs.balance -= amount;
660
+ rs.transactions.push({
562
661
  amount,
563
- balanceAfter: ctx.state.balance,
564
- timestamp: Date.now()
662
+ balanceAfter: rs.balance,
663
+ timestamp: Date.now(),
565
664
  });
566
-
665
+
567
666
  return {
568
667
  context: ctx,
569
668
  response: {
570
669
  success: true,
571
- newBalance: ctx.state.balance,
572
- transactionCount: ctx.state.transactions.length
573
- }
670
+ newBalance: rs.balance,
671
+ transactionCount: rs.transactions.length,
672
+ },
574
673
  };
575
674
  },
576
- onError: (error: Error, ctx: any) => {
675
+ onError: (error, ctx) => {
676
+ const rs = ctx.routeState! as TransactionState;
577
677
  // Don't modify state on error - let it rollback naturally
578
678
  return {
579
679
  status: 400,
580
- body: { error: error.message, balance: ctx.state.balance },
581
- headers: {}
680
+ body: { error: error.message, balance: rs.balance },
681
+ headers: {},
582
682
  };
583
- }
683
+ },
584
684
  };
585
-
586
- mock('POST /transaction', { initialBalance: 100 }).pipe(transactionPlugin);
685
+
686
+ mock("POST /transaction", { initialBalance: 100 }).pipe(transactionPlugin);
587
687
  });
588
688
 
589
689
  When("I make concurrent transactions including some that should fail", async () => {
590
690
  const promises = [
591
- mock.handle('POST', '/transaction', { body: { amount: 20 } }), // Should succeed
592
- mock.handle('POST', '/transaction', { body: { amount: 30 } }), // Should succeed
593
- mock.handle('POST', '/transaction', { body: { amount: 150 } }), // Should fail
594
- mock.handle('POST', '/transaction', { body: { amount: 25 } }), // Should succeed
595
- mock.handle('POST', '/transaction', { body: { amount: 100 } }) // May succeed or fail depending on order
691
+ mock.handle("POST", "/transaction", { body: { amount: 20 } }), // Should succeed
692
+ mock.handle("POST", "/transaction", { body: { amount: 30 } }), // Should succeed
693
+ mock.handle("POST", "/transaction", { body: { amount: 150 } }), // Should fail
694
+ mock.handle("POST", "/transaction", { body: { amount: 25 } }), // Should succeed
695
+ mock.handle("POST", "/transaction", { body: { amount: 100 } }), // May succeed or fail depending on order
596
696
  ];
597
-
697
+
598
698
  responses = await Promise.all(promises);
599
699
  });
600
700
 
601
701
  Then("successful transactions should update state correctly", () => {
602
- const successfulTransactions = responses.filter(r => r.status === 200 && r.body.success);
603
-
702
+ const successfulTransactions = responses.filter(r => r.status === 200 && (r.body as TransactionResponseBody).success);
703
+
604
704
  // Some transactions might succeed or fail depending on concurrency
605
705
  successfulTransactions.forEach(response => {
606
- expect(response.body.success).toBe(true);
607
- expect(response.body.newBalance).toBeDefined();
608
- expect(typeof response.body.newBalance).toBe('number');
706
+ const body = response.body as TransactionResponseBody;
707
+ expect(body.success).toBe(true);
708
+ expect(body.newBalance).toBeDefined();
709
+ expect(typeof body.newBalance).toBe("number");
609
710
  });
610
711
  });
611
712
 
612
713
  And("failed transactions should not modify state", () => {
613
714
  const failedTransactions = responses.filter(r => r.status >= 400);
614
-
715
+
615
716
  failedTransactions.forEach(response => {
717
+ const body = response.body as TransactionResponseBody;
616
718
  expect(response.status).toBeGreaterThanOrEqual(400);
617
719
  expect(response.body).toBeDefined();
618
- if (response.body.error) {
619
- expect(typeof response.body.error).toBe('string');
720
+ if (body.error) {
721
+ expect(typeof body.error).toBe("string");
620
722
  }
621
723
  });
622
724
  });
623
725
 
624
726
  And("final balance should reflect only successful transactions", () => {
625
- const successfulTransactions = responses.filter(r => r.body.success === true);
727
+ const successfulTransactions = responses.filter(r => (r.body as TransactionResponseBody).success === true);
626
728
  if (successfulTransactions.length > 0) {
627
- const balances = successfulTransactions.map(r => r.body.newBalance);
729
+ const balances = successfulTransactions.map(r => (r.body as TransactionResponseBody).newBalance);
628
730
  const finalBalance = Math.min(...balances); // Last successful transaction
629
731
  expect(finalBalance).toBeLessThan(100);
630
732
  expect(finalBalance).toBeGreaterThanOrEqual(0);
@@ -632,12 +734,12 @@ describeFeature(feature, ({ Scenario }) => {
632
734
  });
633
735
 
634
736
  And("transaction history should be consistent", () => {
635
- const successfulTransactions = responses.filter(r => r.body.success === true);
737
+ const successfulTransactions = responses.filter(r => (r.body as TransactionResponseBody).success === true);
636
738
  if (successfulTransactions.length > 0) {
637
- const transactionCounts = successfulTransactions.map(r => r.body.transactionCount);
739
+ const transactionCounts = successfulTransactions.map(r => (r.body as TransactionResponseBody).transactionCount);
638
740
  const maxCount = Math.max(...transactionCounts);
639
741
  expect(maxCount).toBe(successfulTransactions.length);
640
742
  }
641
743
  });
642
744
  });
643
- });
745
+ });