@avasapp/agent-bridge 0.4.2 → 0.6.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.
- package/README.md +36 -14
- package/dist/adapters/ably.cjs +79 -3
- package/dist/adapters/ably.d.cts +2 -1
- package/dist/adapters/async-storage.cjs +158 -0
- package/dist/adapters/async-storage.d.cts +14 -0
- package/dist/adapters/device.cjs +1110 -0
- package/dist/adapters/device.d.cts +49 -0
- package/dist/adapters/expo-router.cjs +112 -6
- package/dist/adapters/expo-router.d.cts +1 -1
- package/dist/adapters/jotai.cjs +266 -0
- package/dist/adapters/jotai.d.cts +19 -0
- package/dist/adapters/react-native-mmkv.cjs +80 -0
- package/dist/adapters/react-native-mmkv.d.cts +3 -1
- package/dist/adapters/react-native-webview.d.cts +1 -1
- package/dist/adapters/react-navigation.cjs +293 -0
- package/dist/adapters/react-navigation.d.cts +55 -0
- package/dist/adapters/redux.cjs +322 -0
- package/dist/adapters/redux.d.cts +28 -0
- package/dist/adapters/secure-store.cjs +169 -0
- package/dist/adapters/secure-store.d.cts +28 -0
- package/dist/adapters/socket-io.cjs +77 -3
- package/dist/adapters/socket-io.d.cts +2 -1
- package/dist/adapters/tanstack-query.cjs +3 -0
- package/dist/adapters/tanstack-query.d.cts +13 -4
- package/dist/adapters/zustand.cjs +193 -22
- package/dist/adapters/zustand.d.cts +11 -15
- package/dist/{chunk-TA2OLPPT.js → chunk-Y6QFU5ON.js} +477 -165
- package/dist/cli.js +2370 -103
- package/dist/client/index.cjs +441 -136
- package/dist/client/index.d.ts +67 -4
- package/dist/client/index.js +5 -3
- package/dist/expo/index.d.cts +1 -1
- package/dist/network/index.cjs +320 -73
- package/dist/network/index.d.cts +18 -3
- package/dist/noop/async-storage.cjs +26 -0
- package/dist/noop/device.cjs +26 -0
- package/dist/noop/jotai.cjs +26 -0
- package/dist/noop/react-navigation.cjs +26 -0
- package/dist/noop/realtime.cjs +6 -1
- package/dist/noop/redux.cjs +28 -0
- package/dist/noop/secure-store.cjs +26 -0
- package/dist/realtime/index.cjs +71 -4
- package/dist/realtime/index.d.cts +21 -2
- package/dist/runtime/index.cjs +838 -128
- package/dist/runtime/index.d.cts +4 -2
- package/dist/store-common-j0cHHvBr.d.cts +23 -0
- package/dist/{types-BmpykP1P.d.cts → types-qlwdcGjt.d.cts} +22 -0
- package/entries/async-storage.cjs +9 -0
- package/entries/device.cjs +9 -0
- package/entries/jotai.cjs +9 -0
- package/entries/react-navigation.cjs +9 -0
- package/entries/redux.cjs +9 -0
- package/entries/secure-store.cjs +9 -0
- package/package.json +34 -2
- package/skills/agent-bridge/SKILL.md +70 -22
- package/skills/agent-bridge/references/device.md +21 -0
- package/skills/agent-bridge/references/flows.md +19 -8
- package/skills/agent-bridge/references/large-data.md +6 -6
- package/skills/agent-bridge/references/network.md +25 -7
- package/skills/agent-bridge/references/realtime.md +5 -5
- package/skills/agent-bridge/references/restore.md +25 -7
- package/skills/agent-bridge/references/screen.md +25 -11
- package/skills/agent-bridge/references/state.md +31 -11
- package/skills/agent-bridge/references/webview.md +9 -9
- package/skills/agent-bridge/references/writing-scenarios.md +3 -3
package/dist/network/index.cjs
CHANGED
|
@@ -48,18 +48,23 @@ var signalMocksRemoved = (ids) => {
|
|
|
48
48
|
// src/network/api.ts
|
|
49
49
|
var escape = (s) => s.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&");
|
|
50
50
|
var ROUTE = /^(?:([A-Za-z]+)\s+)?(\/\S*)$/;
|
|
51
|
-
function compileRoute(baseUrl, route) {
|
|
51
|
+
function compileRoute(baseUrl, route, { loose = false } = {}) {
|
|
52
52
|
const parsed = ROUTE.exec(route.trim());
|
|
53
53
|
if (!parsed)
|
|
54
54
|
throw new Error(`Bad route "${route}": use "METHOD /path" or "/path"`);
|
|
55
|
-
const [, method,
|
|
55
|
+
const [, method, rawPath] = parsed;
|
|
56
|
+
const path = loose && rawPath.length > 1 ? rawPath.replace(/\/+$/, "") : rawPath;
|
|
56
57
|
const names = [];
|
|
57
|
-
const pattern = path.split(/(:[A-Za-z_][A-Za-z0-9_]*|\*)/).map((part) => {
|
|
58
|
+
const pattern = (path === "/" && loose ? "" : path).split(loose ? /(\/:[A-Za-z_][A-Za-z0-9_]*(?=\/|$)|\*)/ : /(:[A-Za-z_][A-Za-z0-9_]*|\*)/).map((part) => {
|
|
58
59
|
if (part === "*") {
|
|
59
60
|
names.push("*");
|
|
60
61
|
return "([^?#]*)";
|
|
61
62
|
}
|
|
62
|
-
if (part.startsWith("
|
|
63
|
+
if (loose && part.startsWith("/:")) {
|
|
64
|
+
names.push(part.slice(2));
|
|
65
|
+
return "/([^/?#]+)";
|
|
66
|
+
}
|
|
67
|
+
if (!loose && part.startsWith(":")) {
|
|
63
68
|
names.push(part.slice(1));
|
|
64
69
|
return "([^/?#]+)";
|
|
65
70
|
}
|
|
@@ -68,7 +73,7 @@ function compileRoute(baseUrl, route) {
|
|
|
68
73
|
const base = baseUrl ? escape(baseUrl.replace(/\/+$/, "")) : "(?:[a-z][a-z0-9+.-]*://[^/?#]*)?";
|
|
69
74
|
return {
|
|
70
75
|
method: method?.toUpperCase(),
|
|
71
|
-
regex: new RegExp(`^${base}${pattern}(?:[?#].*)?$`, "i"),
|
|
76
|
+
regex: new RegExp(`^${base}${pattern}${loose ? "/?" : ""}(?:[?#].*)?$`, "i"),
|
|
72
77
|
names
|
|
73
78
|
};
|
|
74
79
|
}
|
|
@@ -147,6 +152,154 @@ function errorMessage(error) {
|
|
|
147
152
|
return error instanceof Error ? error.message : String(error);
|
|
148
153
|
}
|
|
149
154
|
|
|
155
|
+
// src/network/headers.ts
|
|
156
|
+
var SECRET = /auth|cookie|token|secret|session|password|passwd|signature|api-?key|credential|jwt/i;
|
|
157
|
+
var lower = (headers) => Object.fromEntries(Object.entries(headers).map(([k, v]) => [k.toLowerCase(), v]));
|
|
158
|
+
function keepHeaders(headers) {
|
|
159
|
+
return headers && Object.keys(headers).length ? lower(headers) : void 0;
|
|
160
|
+
}
|
|
161
|
+
function redactHeaders(headers) {
|
|
162
|
+
const out = {};
|
|
163
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
164
|
+
if (!SECRET.test(name)) {
|
|
165
|
+
out[name] = value;
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
const scheme = /^authorization$|^proxy-authorization$/i.test(name) ? /^[A-Za-z]+(?= \S)/.exec(value)?.[0] : void 0;
|
|
169
|
+
const keep = scheme ? scheme.length + 1 : Math.min(4, Math.floor(value.length / 4));
|
|
170
|
+
out[name] = `${value.slice(0, keep)}\u2026 (+${value.length - keep} chars)`;
|
|
171
|
+
}
|
|
172
|
+
return out;
|
|
173
|
+
}
|
|
174
|
+
function parseHeaderBlock(block) {
|
|
175
|
+
const out = {};
|
|
176
|
+
for (const line of (block ?? "").split(/\r?\n/)) {
|
|
177
|
+
const at = line.indexOf(":");
|
|
178
|
+
if (at <= 0) continue;
|
|
179
|
+
const name = line.slice(0, at).trim().toLowerCase();
|
|
180
|
+
const value = line.slice(at + 1).trim();
|
|
181
|
+
out[name] = name in out ? `${out[name]}, ${value}` : value;
|
|
182
|
+
}
|
|
183
|
+
return out;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// src/network/match.ts
|
|
187
|
+
var KEYS = ["url", "path", "method", "body", "graphql"];
|
|
188
|
+
var NEED = "A mock needs a match: a URL substring or { url?, path?, method?, body?, graphql? } with one of url, path, body or graphql";
|
|
189
|
+
var own = (o, key) => Object.prototype.hasOwnProperty.call(o, key);
|
|
190
|
+
function subset(spec, actual) {
|
|
191
|
+
if (spec === null || typeof spec !== "object") return spec === actual;
|
|
192
|
+
if (Array.isArray(spec))
|
|
193
|
+
return Array.isArray(actual) && actual.length === spec.length && spec.every((item, i) => subset(item, actual[i]));
|
|
194
|
+
if (actual === null || typeof actual !== "object" || Array.isArray(actual)) return false;
|
|
195
|
+
return Object.entries(spec).every(
|
|
196
|
+
([key, value]) => own(actual, key) && subset(value, actual[key])
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
function operationName(probe) {
|
|
200
|
+
const json = probe.json();
|
|
201
|
+
if (json && typeof json === "object" && !Array.isArray(json)) {
|
|
202
|
+
const name = json.operationName;
|
|
203
|
+
if (typeof name === "string") return name;
|
|
204
|
+
}
|
|
205
|
+
return queryOf(probe.url).operationName;
|
|
206
|
+
}
|
|
207
|
+
function urlTest(url) {
|
|
208
|
+
if (typeof url === "string")
|
|
209
|
+
return { test: (u) => u.includes(url), shown: `URL containing ${JSON.stringify(url)}`, substring: url };
|
|
210
|
+
const isRegex = url instanceof RegExp;
|
|
211
|
+
if (!isRegex && !(url && typeof url === "object" && typeof url.regex === "string"))
|
|
212
|
+
throw new Error("url must be a string, or { regex, flags? }");
|
|
213
|
+
const regex = isRegex ? url : new RegExp(url.regex, url.flags);
|
|
214
|
+
return {
|
|
215
|
+
test: (u) => {
|
|
216
|
+
regex.lastIndex = 0;
|
|
217
|
+
return regex.test(u);
|
|
218
|
+
},
|
|
219
|
+
shown: `URL matching ${String(regex)}`
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
function compileMatch(match) {
|
|
223
|
+
const spec = typeof match === "string" ? { url: match } : match;
|
|
224
|
+
if (!spec || typeof spec !== "object" || Array.isArray(spec) || spec instanceof RegExp) throw new Error(NEED);
|
|
225
|
+
const unknown = Object.keys(spec).filter((k) => !KEYS.includes(k));
|
|
226
|
+
if (unknown.length)
|
|
227
|
+
throw new Error(`Unknown match key ${unknown.map((k) => `"${k}"`).join(", ")}; the keys are ${KEYS.join(", ")}`);
|
|
228
|
+
if (["url", "path", "body", "graphql"].every((k) => spec[k] === void 0)) throw new Error(NEED);
|
|
229
|
+
const parts = [];
|
|
230
|
+
const tests = [];
|
|
231
|
+
let method;
|
|
232
|
+
if (spec.method !== void 0) {
|
|
233
|
+
if (typeof spec.method !== "string") throw new Error('method must be a string such as "POST"');
|
|
234
|
+
method = spec.method.toUpperCase();
|
|
235
|
+
}
|
|
236
|
+
parts.push(method ?? "any method");
|
|
237
|
+
let substring;
|
|
238
|
+
if (spec.url !== void 0) {
|
|
239
|
+
const u = urlTest(spec.url);
|
|
240
|
+
substring = u.substring;
|
|
241
|
+
tests.push((_m, url) => u.test(url));
|
|
242
|
+
parts.push(u.shown);
|
|
243
|
+
}
|
|
244
|
+
if (spec.path !== void 0) {
|
|
245
|
+
const path = spec.path;
|
|
246
|
+
if (typeof path !== "string" || !path.startsWith("/"))
|
|
247
|
+
throw new Error('path is just the path, starting with "/", e.g. "/orders/:id"; use url to pin a host');
|
|
248
|
+
if (/[?#]/.test(path)) throw new Error("path has no query or hash; the query is ignored, use url to match it");
|
|
249
|
+
if (/\s/.test(path)) throw new Error("path has no spaces; write a space as %20");
|
|
250
|
+
const route = compileRoute("", path, { loose: true });
|
|
251
|
+
const decoded = (u) => {
|
|
252
|
+
try {
|
|
253
|
+
return decodeURI(u);
|
|
254
|
+
} catch {
|
|
255
|
+
return u;
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
tests.push((_m, url) => route.regex.test(url) || route.regex.test(decoded(url)));
|
|
259
|
+
parts.push(`path ${path} (whole path, any host)`);
|
|
260
|
+
}
|
|
261
|
+
const probeTests = [];
|
|
262
|
+
if (spec.body !== void 0) {
|
|
263
|
+
const body = spec.body;
|
|
264
|
+
const frozen = body && typeof body === "object" ? JSON.parse(JSON.stringify(body)) : void 0;
|
|
265
|
+
if (!frozen || typeof frozen !== "object" || !Array.isArray(frozen) && !Object.keys(frozen).length)
|
|
266
|
+
throw new Error("body must be a non-empty JSON object or an array to look for in the request body");
|
|
267
|
+
probeTests.push((p) => {
|
|
268
|
+
const json = p.json();
|
|
269
|
+
return json !== void 0 && subset(frozen, json);
|
|
270
|
+
});
|
|
271
|
+
parts.push(`JSON body containing ${truncateBody(JSON.stringify(frozen))}`);
|
|
272
|
+
}
|
|
273
|
+
if (spec.graphql !== void 0) {
|
|
274
|
+
const name = spec.graphql;
|
|
275
|
+
if (typeof name !== "string" || !name) throw new Error('graphql must be an operation name such as "GetUser"');
|
|
276
|
+
probeTests.push((p) => operationName(p) === name);
|
|
277
|
+
parts.push(`GraphQL operation ${JSON.stringify(name)}`);
|
|
278
|
+
}
|
|
279
|
+
return {
|
|
280
|
+
test: (m, u) => (!method || method === m) && tests.every((t) => t(m, u)),
|
|
281
|
+
needsBody: probeTests.length > 0,
|
|
282
|
+
testBody: (p) => {
|
|
283
|
+
try {
|
|
284
|
+
return probeTests.every((t) => t(p));
|
|
285
|
+
} catch {
|
|
286
|
+
return false;
|
|
287
|
+
}
|
|
288
|
+
},
|
|
289
|
+
matches: parts.join(", "),
|
|
290
|
+
key: JSON.stringify([
|
|
291
|
+
method,
|
|
292
|
+
spec.url instanceof RegExp ? String(spec.url) : spec.url,
|
|
293
|
+
spec.path,
|
|
294
|
+
spec.body,
|
|
295
|
+
spec.graphql
|
|
296
|
+
]),
|
|
297
|
+
substring,
|
|
298
|
+
plain: substring !== void 0 && spec.path === void 0 && spec.body === void 0 && spec.graphql === void 0,
|
|
299
|
+
method
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
150
303
|
// src/network/state.ts
|
|
151
304
|
var LOG_SIZE = 100;
|
|
152
305
|
var KEY2 = /* @__PURE__ */ Symbol.for("@avasapp/agent-bridge/network");
|
|
@@ -156,6 +309,7 @@ function networkState() {
|
|
|
156
309
|
installed: false,
|
|
157
310
|
log: [],
|
|
158
311
|
nextLogId: 1,
|
|
312
|
+
logSize: LOG_SIZE,
|
|
159
313
|
mocks: [],
|
|
160
314
|
nextSeq: 1,
|
|
161
315
|
insideFetch: 0,
|
|
@@ -251,21 +405,25 @@ function startEntry(state, fields) {
|
|
|
251
405
|
pending: true
|
|
252
406
|
};
|
|
253
407
|
keepRequestBody(entry, fields.requestBody);
|
|
408
|
+
const requestHeaders = keepHeaders(fields.requestHeaders);
|
|
409
|
+
if (requestHeaders) fullOf(entry).requestHeaders = requestHeaders;
|
|
254
410
|
state.log.push(entry);
|
|
255
411
|
signalRequestStarted(entry.id);
|
|
256
|
-
if (state.log.length >
|
|
257
|
-
state.log.splice(0, state.log.length -
|
|
412
|
+
if (state.log.length > state.logSize)
|
|
413
|
+
state.log.splice(0, state.log.length - state.logSize);
|
|
258
414
|
return entry;
|
|
259
415
|
}
|
|
260
416
|
function finishEntry(entry, fields) {
|
|
261
417
|
entry.ms = Date.now() - entry.startedAt;
|
|
262
418
|
delete entry.pending;
|
|
263
|
-
const { responseBody, contentType, ...rest } = fields;
|
|
419
|
+
const { responseBody, contentType, responseHeaders, ...rest } = fields;
|
|
420
|
+
const kept = keepHeaders(responseHeaders);
|
|
421
|
+
if (kept) fullOf(entry).responseHeaders = kept;
|
|
264
422
|
for (const [key, value] of Object.entries(rest))
|
|
265
423
|
if (value !== void 0) entry[key] = value;
|
|
266
424
|
if (responseBody !== void 0) setResponseBody(entry, responseBody, contentType);
|
|
267
425
|
}
|
|
268
|
-
function publicEntry(entry, full = false) {
|
|
426
|
+
function publicEntry(entry, full = false, headers) {
|
|
269
427
|
const out = entry.pending ? { ...entry, ms: Date.now() - entry.startedAt } : { ...entry };
|
|
270
428
|
if (full) {
|
|
271
429
|
const { requestBody, responseBody } = FULL.get(entry) ?? {};
|
|
@@ -275,13 +433,34 @@ function publicEntry(entry, full = false) {
|
|
|
275
433
|
const cutRequest = entry.requestBody !== void 0 && requestBody === void 0;
|
|
276
434
|
if (cut || cutRequest) out.truncated = true;
|
|
277
435
|
}
|
|
436
|
+
if (headers) {
|
|
437
|
+
const { requestHeaders, responseHeaders } = FULL.get(entry) ?? {};
|
|
438
|
+
const shown = (h) => h && (headers === "full" ? { ...h } : redactHeaders(h));
|
|
439
|
+
if (requestHeaders) out.requestHeaders = shown(requestHeaders);
|
|
440
|
+
if (responseHeaders) out.responseHeaders = shown(responseHeaders);
|
|
441
|
+
}
|
|
278
442
|
return out;
|
|
279
443
|
}
|
|
444
|
+
function setRequestBody(entry, text) {
|
|
445
|
+
if (entry.requestBody === void 0) keepRequestBody(entry, text);
|
|
446
|
+
}
|
|
447
|
+
function requestBodyForMatch(entry) {
|
|
448
|
+
const whole = FULL.get(entry)?.requestBody;
|
|
449
|
+
if (whole !== void 0) return whole;
|
|
450
|
+
const preview = entry.requestBody;
|
|
451
|
+
if (preview === void 0) return void 0;
|
|
452
|
+
try {
|
|
453
|
+
JSON.parse(preview);
|
|
454
|
+
return preview;
|
|
455
|
+
} catch {
|
|
456
|
+
return void 0;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
280
459
|
function fullResponse(entry) {
|
|
281
460
|
const full = FULL.get(entry);
|
|
282
461
|
return { state: full?.response ?? "none", body: full?.responseBody, contentType: full?.contentType };
|
|
283
462
|
}
|
|
284
|
-
function
|
|
463
|
+
function urlTest2(url) {
|
|
285
464
|
if (typeof url === "string") return (u) => u.includes(url);
|
|
286
465
|
const regex = url instanceof RegExp ? url : new RegExp(url.regex, url.flags);
|
|
287
466
|
return (u) => {
|
|
@@ -289,19 +468,12 @@ function urlTest(url) {
|
|
|
289
468
|
return regex.test(u);
|
|
290
469
|
};
|
|
291
470
|
}
|
|
292
|
-
function compileMatch(match) {
|
|
293
|
-
const spec = typeof match === "string" ? { url: match } : match;
|
|
294
|
-
if (!spec || spec.url === void 0)
|
|
295
|
-
throw new Error("A mock needs a match: a URL substring or { url, method? }");
|
|
296
|
-
const testUrl = urlTest(spec.url);
|
|
297
|
-
const method = spec.method?.toUpperCase();
|
|
298
|
-
return (m, u) => (!method || method === m) && testUrl(u);
|
|
299
|
-
}
|
|
300
471
|
function addMock(state, source, match, response, options = {}) {
|
|
301
472
|
if (!response || typeof response !== "object" && typeof response !== "function")
|
|
302
473
|
throw new Error("A mock needs a response: { status?, json?, body?, headers? }, { offline: true } or a handler");
|
|
303
474
|
if (options.priority !== void 0 && !Number.isFinite(options.priority))
|
|
304
475
|
throw new Error("priority must be a finite number");
|
|
476
|
+
const compiled = compileMatch(match);
|
|
305
477
|
const seq = state.nextSeq++;
|
|
306
478
|
const id = options.id ?? `${source}-${seq}`;
|
|
307
479
|
removeMocks(state, (m) => m.id === id);
|
|
@@ -315,7 +487,7 @@ function addMock(state, source, match, response, options = {}) {
|
|
|
315
487
|
priority: options.priority ?? 0,
|
|
316
488
|
hits: 0,
|
|
317
489
|
seq,
|
|
318
|
-
|
|
490
|
+
...(({ key, ...rest }) => ({ ...rest, matchKey: key }))(compiled)
|
|
319
491
|
};
|
|
320
492
|
state.mocks.push(mock2);
|
|
321
493
|
return mock2;
|
|
@@ -326,28 +498,27 @@ function removeMocks(state, which) {
|
|
|
326
498
|
return before - state.mocks.length;
|
|
327
499
|
}
|
|
328
500
|
var mockOrder = (a, b) => Number(b.source === "agent") - Number(a.source === "agent") || b.priority - a.priority || b.seq - a.seq;
|
|
329
|
-
var substringMatch = (match) => {
|
|
330
|
-
const spec = typeof match === "string" ? { url: match } : match;
|
|
331
|
-
return typeof spec.url === "string" ? { url: spec.url, method: spec.method?.toUpperCase() } : null;
|
|
332
|
-
};
|
|
333
501
|
function shadowedMocks(state, added) {
|
|
334
|
-
|
|
335
|
-
if (!next || typeof added.response === "function") return [];
|
|
502
|
+
if (typeof added.response === "function") return [];
|
|
336
503
|
return state.mocks.filter((old) => {
|
|
337
|
-
if (old === added || old.source !== "agent" || mockOrder(added, old) >= 0)
|
|
504
|
+
if (old === added || old.source !== "agent" || mockOrder(added, old) >= 0 || added.times !== void 0)
|
|
338
505
|
return false;
|
|
339
|
-
|
|
340
|
-
return
|
|
506
|
+
if (old.matchKey === added.matchKey) return true;
|
|
507
|
+
return added.plain && old.substring !== void 0 && old.substring.includes(added.substring) && (!added.method || added.method === old.method);
|
|
341
508
|
});
|
|
342
509
|
}
|
|
343
|
-
function matchingMocks(state, method, url) {
|
|
344
|
-
|
|
510
|
+
function matchingMocks(state, method, url, body) {
|
|
511
|
+
if (state.deviceOffline) return [];
|
|
512
|
+
const found = state.mocks.filter((m) => m.test(method, url));
|
|
513
|
+
const probe = body && found.some((m) => m.needsBody) ? mockRequest(method, url, {}, body.text) : null;
|
|
514
|
+
return found.filter((m) => !m.needsBody || !probe || m.testBody(probe)).sort(mockOrder);
|
|
345
515
|
}
|
|
346
|
-
async function answer(state, candidates, request) {
|
|
516
|
+
async function answer(state, candidates, request, bodyKnown = true) {
|
|
347
517
|
for (const mock2 of candidates) {
|
|
518
|
+
if (mock2.needsBody && !bodyKnown && !mock2.testBody(await request())) continue;
|
|
348
519
|
if (!state.mocks.includes(mock2)) continue;
|
|
349
520
|
const response = typeof mock2.response === "function" ? await mock2.response(await request()) : mock2.response;
|
|
350
|
-
if (!response) continue;
|
|
521
|
+
if (!response || !state.mocks.includes(mock2)) continue;
|
|
351
522
|
mock2.hits += 1;
|
|
352
523
|
if (mock2.times !== void 0 && mock2.hits >= mock2.times)
|
|
353
524
|
removeMocks(state, (m) => m === mock2);
|
|
@@ -356,17 +527,21 @@ async function answer(state, candidates, request) {
|
|
|
356
527
|
return null;
|
|
357
528
|
}
|
|
358
529
|
function mockRequest(method, url, headers, body) {
|
|
530
|
+
let parsed;
|
|
359
531
|
return {
|
|
360
532
|
method,
|
|
361
533
|
url,
|
|
362
534
|
headers,
|
|
363
535
|
body,
|
|
364
536
|
json: () => {
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
537
|
+
if (!parsed) {
|
|
538
|
+
try {
|
|
539
|
+
parsed = { value: body === void 0 ? void 0 : JSON.parse(body) };
|
|
540
|
+
} catch {
|
|
541
|
+
parsed = { value: void 0 };
|
|
542
|
+
}
|
|
369
543
|
}
|
|
544
|
+
return parsed.value;
|
|
370
545
|
}
|
|
371
546
|
};
|
|
372
547
|
}
|
|
@@ -387,7 +562,7 @@ function strictRule(options = {}) {
|
|
|
387
562
|
if (!Array.isArray(allow))
|
|
388
563
|
throw new Error("allow must be a list of URL substrings or regexes");
|
|
389
564
|
return {
|
|
390
|
-
allow: allow.map((rule) =>
|
|
565
|
+
allow: allow.map((rule) => urlTest2(rule)),
|
|
391
566
|
allowShown: allow.map(
|
|
392
567
|
(rule) => typeof rule === "string" ? rule : String(rule instanceof RegExp ? rule : new RegExp(rule.regex, rule.flags))
|
|
393
568
|
),
|
|
@@ -400,15 +575,19 @@ function strictRules(state) {
|
|
|
400
575
|
return state.strictAgent ? [state.strictAgent, ...state.strictApp] : state.strictApp;
|
|
401
576
|
}
|
|
402
577
|
var BLOCKED_HEADER = "x-agent-bridge";
|
|
403
|
-
function blockedResponse(state, method, url) {
|
|
578
|
+
function blockedResponse(state, method, url, body) {
|
|
579
|
+
if (state.deviceOffline) return { offline: true };
|
|
404
580
|
const rules = strictRules(state);
|
|
405
581
|
if (!rules.length || rules.some((r) => r.allow.some((test) => test(url))))
|
|
406
582
|
return null;
|
|
407
|
-
const
|
|
583
|
+
const operation = operationName(mockRequest(method, url, {}, body));
|
|
584
|
+
const key = operation ? `${method} ${url} (${operation})` : `${method} ${url}`;
|
|
585
|
+
const probe = mockRequest(method, url, {}, body);
|
|
586
|
+
const near = state.mocks.filter((m) => m.needsBody && m.test(method, url) && !m.testBody(probe)).map((m) => m.id);
|
|
408
587
|
const seen = state.blocked.get(key);
|
|
409
588
|
if (seen) seen.count += 1;
|
|
410
|
-
else state.blocked.set(key, { method, url, count: 1 });
|
|
411
|
-
const error = `agent-bridge strict network: no mock for ${key}`;
|
|
589
|
+
else state.blocked.set(key, { method, url, count: 1, ...operation && { operation } });
|
|
590
|
+
const error = `agent-bridge strict network: no mock for ${key}${near.length ? ` (${near.join(", ")} matched the URL but not the body)` : ""}`;
|
|
412
591
|
console.error(`${error}. Mock it, or allow it in strict mode.`);
|
|
413
592
|
if (rules[0].offline) return { offline: true };
|
|
414
593
|
return {
|
|
@@ -479,11 +658,12 @@ function patchFetch(state) {
|
|
|
479
658
|
const url = typeof input === "string" ? input : req.url ?? String(input);
|
|
480
659
|
if (isSkipped(state, url)) return callOriginal(input, init);
|
|
481
660
|
const method = String(init?.method ?? req.method ?? "GET").toUpperCase();
|
|
482
|
-
|
|
661
|
+
let requestBody = bodyText(init?.body);
|
|
483
662
|
const entry = startEntry(state, {
|
|
484
663
|
method,
|
|
485
664
|
url,
|
|
486
|
-
requestBody
|
|
665
|
+
requestBody,
|
|
666
|
+
requestHeaders: headersRecord(init?.headers ?? req.headers)
|
|
487
667
|
});
|
|
488
668
|
const real = () => {
|
|
489
669
|
let promise;
|
|
@@ -495,7 +675,7 @@ function patchFetch(state) {
|
|
|
495
675
|
}
|
|
496
676
|
return promise.then(
|
|
497
677
|
(res) => {
|
|
498
|
-
finishEntry(entry, { status: res.status });
|
|
678
|
+
finishEntry(entry, { status: res.status, responseHeaders: headersRecord(res.headers) });
|
|
499
679
|
const type = res.headers?.get?.("content-type") ?? void 0;
|
|
500
680
|
if (isTextual(type) && typeof res.clone === "function") {
|
|
501
681
|
markResponse(entry, "pending");
|
|
@@ -510,23 +690,27 @@ function patchFetch(state) {
|
|
|
510
690
|
);
|
|
511
691
|
};
|
|
512
692
|
const passOn = () => {
|
|
513
|
-
const blocked = blockedResponse(state, method, url);
|
|
693
|
+
const blocked = blockedResponse(state, method, url, requestBody);
|
|
514
694
|
if (!blocked) return real();
|
|
515
|
-
entry.blocked = true;
|
|
695
|
+
if (!state.deviceOffline) entry.blocked = true;
|
|
516
696
|
if (isOffline(blocked)) {
|
|
517
|
-
finishEntry(entry, { error: `${OFFLINE_MESSAGE} (strict network)` });
|
|
697
|
+
finishEntry(entry, { error: `${OFFLINE_MESSAGE} (${state.deviceOffline ? "device offline" : "strict network"})` });
|
|
518
698
|
return Promise.reject(new TypeError(OFFLINE_MESSAGE));
|
|
519
699
|
}
|
|
520
700
|
const { status, text, headers } = responseParts(blocked);
|
|
521
|
-
finishEntry(entry, { status, responseBody: text, contentType: contentTypeOf(headers) });
|
|
701
|
+
finishEntry(entry, { status, responseBody: text, contentType: contentTypeOf(headers), responseHeaders: headers });
|
|
522
702
|
return Promise.resolve(mockedResponse(blocked));
|
|
523
703
|
};
|
|
524
|
-
const
|
|
704
|
+
const bodyKnown = init?.body !== void 0 || !req.clone || method === "GET" || method === "HEAD" || !isTextual(headersRecord(init?.headers ?? req.headers)["content-type"]);
|
|
705
|
+
const candidates = matchingMocks(state, method, url, bodyKnown ? { text: requestBody } : void 0);
|
|
525
706
|
if (!candidates.length) return passOn();
|
|
526
|
-
|
|
707
|
+
let built;
|
|
708
|
+
const request = () => built ?? (built = (async () => {
|
|
527
709
|
let body = requestBody;
|
|
528
|
-
if (body === void 0 &&
|
|
529
|
-
body = await req.clone().text().catch(() => void 0);
|
|
710
|
+
if (body === void 0 && !bodyKnown) {
|
|
711
|
+
body = await Promise.resolve().then(() => req.clone().text()).catch(() => void 0);
|
|
712
|
+
setRequestBody(entry, body);
|
|
713
|
+
requestBody = body;
|
|
530
714
|
}
|
|
531
715
|
return mockRequest(
|
|
532
716
|
method,
|
|
@@ -534,8 +718,8 @@ function patchFetch(state) {
|
|
|
534
718
|
headersRecord(init?.headers ?? req.headers),
|
|
535
719
|
body || void 0
|
|
536
720
|
);
|
|
537
|
-
};
|
|
538
|
-
const answered = answer(state, candidates, request).catch((error) => {
|
|
721
|
+
})());
|
|
722
|
+
const answered = answer(state, candidates, request, bodyKnown).catch((error) => {
|
|
539
723
|
entry.mocked = true;
|
|
540
724
|
finishEntry(entry, { error: errorMessage(error) });
|
|
541
725
|
throw error;
|
|
@@ -554,7 +738,7 @@ function patchFetch(state) {
|
|
|
554
738
|
throw new TypeError(OFFLINE_MESSAGE);
|
|
555
739
|
}
|
|
556
740
|
const { status, text, headers } = responseParts(hit.response);
|
|
557
|
-
finishEntry(entry, { status, responseBody: text, contentType: contentTypeOf(headers) });
|
|
741
|
+
finishEntry(entry, { status, responseBody: text, contentType: contentTypeOf(headers), responseHeaders: headers });
|
|
558
742
|
return mockedResponse(hit.response);
|
|
559
743
|
});
|
|
560
744
|
};
|
|
@@ -586,7 +770,8 @@ function track(xhr, entry) {
|
|
|
586
770
|
status: error ? void 0 : xhr.status,
|
|
587
771
|
error,
|
|
588
772
|
responseBody: text,
|
|
589
|
-
contentType: xhr.getResponseHeader?.("content-type") ?? void 0
|
|
773
|
+
contentType: xhr.getResponseHeader?.("content-type") ?? void 0,
|
|
774
|
+
responseHeaders: error ? void 0 : parseHeaderBlock(xhr.getAllResponseHeaders?.())
|
|
590
775
|
});
|
|
591
776
|
if (!error && text === void 0) markResponse(entry, "none");
|
|
592
777
|
});
|
|
@@ -615,7 +800,7 @@ function respond(xhr, mocked, url) {
|
|
|
615
800
|
for (const [key, value] of Object.entries(values))
|
|
616
801
|
Object.defineProperty(xhr, key, { value, configurable: true });
|
|
617
802
|
};
|
|
618
|
-
const
|
|
803
|
+
const lower2 = Object.fromEntries(
|
|
619
804
|
Object.entries(headers).map(([k, v]) => [k.toLowerCase(), v])
|
|
620
805
|
);
|
|
621
806
|
let response = text;
|
|
@@ -633,9 +818,9 @@ function respond(xhr, mocked, url) {
|
|
|
633
818
|
responseURL: url,
|
|
634
819
|
responseText: offline ? "" : text,
|
|
635
820
|
response: offline ? "" : response,
|
|
636
|
-
getAllResponseHeaders: () => offline ? "" : Object.entries(
|
|
821
|
+
getAllResponseHeaders: () => offline ? "" : Object.entries(lower2).map(([k, v]) => `${k}: ${v}\r
|
|
637
822
|
`).join(""),
|
|
638
|
-
getResponseHeader: (name) =>
|
|
823
|
+
getResponseHeader: (name) => lower2[name.toLowerCase()] ?? null
|
|
639
824
|
});
|
|
640
825
|
fire(xhr, "readystatechange");
|
|
641
826
|
fire(xhr, offline ? "error" : "load");
|
|
@@ -670,30 +855,36 @@ function patchXhr(state) {
|
|
|
670
855
|
const meta = this[META];
|
|
671
856
|
if (!meta || meta.internal || isSkipped(state, meta.url))
|
|
672
857
|
return send.call(this, body);
|
|
858
|
+
meta.aborted = false;
|
|
673
859
|
const { method, url } = meta;
|
|
674
860
|
const text = bodyText(body);
|
|
675
861
|
const entry = startEntry(state, {
|
|
676
862
|
method,
|
|
677
863
|
url,
|
|
678
|
-
requestBody: text
|
|
864
|
+
requestBody: text,
|
|
865
|
+
requestHeaders: meta.headers
|
|
679
866
|
});
|
|
680
867
|
const passOn = () => {
|
|
681
|
-
|
|
868
|
+
if (meta.aborted) {
|
|
869
|
+
finishEntry(entry, { error: "aborted" });
|
|
870
|
+
return;
|
|
871
|
+
}
|
|
872
|
+
const blocked = blockedResponse(state, method, url, text);
|
|
682
873
|
if (!blocked) {
|
|
683
874
|
track(this, entry);
|
|
684
875
|
return send.call(this, body);
|
|
685
876
|
}
|
|
686
877
|
const { status, text: out, headers } = responseParts(blocked);
|
|
687
|
-
entry.blocked = true;
|
|
878
|
+
if (!state.deviceOffline) entry.blocked = true;
|
|
688
879
|
finishEntry(
|
|
689
880
|
entry,
|
|
690
|
-
isOffline(blocked) ? { error: `${OFFLINE_MESSAGE} (strict network)` } : { status, responseBody: out, contentType: contentTypeOf(headers) }
|
|
881
|
+
isOffline(blocked) ? { error: `${OFFLINE_MESSAGE} (${state.deviceOffline ? "device offline" : "strict network"})` } : { status, responseBody: out, contentType: contentTypeOf(headers), responseHeaders: headers }
|
|
691
882
|
);
|
|
692
883
|
void Promise.resolve().then(() => {
|
|
693
884
|
if (!meta.aborted) respond(this, blocked, url);
|
|
694
885
|
});
|
|
695
886
|
};
|
|
696
|
-
const candidates = matchingMocks(state, method, url);
|
|
887
|
+
const candidates = matchingMocks(state, method, url, { text });
|
|
697
888
|
if (!candidates.length) return passOn();
|
|
698
889
|
const request = async () => mockRequest(method, url, meta.headers, text);
|
|
699
890
|
answer(state, candidates, request).then(
|
|
@@ -710,7 +901,7 @@ function patchXhr(state) {
|
|
|
710
901
|
const { status, text: out, headers } = responseParts(hit.response);
|
|
711
902
|
finishEntry(
|
|
712
903
|
entry,
|
|
713
|
-
offline ? { error: `${OFFLINE_MESSAGE} (mocked offline)` } : { status, responseBody: out, contentType: contentTypeOf(headers) }
|
|
904
|
+
offline ? { error: `${OFFLINE_MESSAGE} (mocked offline)` } : { status, responseBody: out, contentType: contentTypeOf(headers), responseHeaders: headers }
|
|
714
905
|
);
|
|
715
906
|
respond(this, hit.response, url);
|
|
716
907
|
},
|
|
@@ -799,6 +990,7 @@ var describeMock = (m, full) => ({
|
|
|
799
990
|
id: m.id,
|
|
800
991
|
source: m.source,
|
|
801
992
|
match: describeMatch(m.match),
|
|
993
|
+
matches: m.matches,
|
|
802
994
|
...describeResponse(m.response, full),
|
|
803
995
|
times: m.times,
|
|
804
996
|
hits: m.hits,
|
|
@@ -809,6 +1001,11 @@ function networkTools(options = {}) {
|
|
|
809
1001
|
installNetwork();
|
|
810
1002
|
const state = networkState();
|
|
811
1003
|
if (options.skip) state.skip = options.skip;
|
|
1004
|
+
if (options.logSize !== void 0) {
|
|
1005
|
+
if (!Number.isInteger(options.logSize) || options.logSize < 1)
|
|
1006
|
+
throw new Error("logSize must be a whole number of at least 1");
|
|
1007
|
+
state.logSize = options.logSize;
|
|
1008
|
+
}
|
|
812
1009
|
const isAgent = (m) => m.source === "agent";
|
|
813
1010
|
const logged = (id) => {
|
|
814
1011
|
const entry = state.log.find((e) => e.id === id);
|
|
@@ -818,7 +1015,7 @@ function networkTools(options = {}) {
|
|
|
818
1015
|
const addAgentMock = (match, response, { times, delayMs, priority }) => {
|
|
819
1016
|
const added = addMock(state, "agent", match, response, { times, delayMs, priority });
|
|
820
1017
|
const shadows = shadowedMocks(state, added).map((m) => m.id);
|
|
821
|
-
return shadows.length ? { id: added.id, shadows } : { id: added.id };
|
|
1018
|
+
return shadows.length ? { id: added.id, matches: added.matches, shadows } : { id: added.id, matches: added.matches };
|
|
822
1019
|
};
|
|
823
1020
|
return {
|
|
824
1021
|
"net.log": {
|
|
@@ -835,19 +1032,55 @@ function networkTools(options = {}) {
|
|
|
835
1032
|
}
|
|
836
1033
|
},
|
|
837
1034
|
"net.entry": {
|
|
838
|
-
description: "One logged request by id, with its request and response bodies whole (not cut at ~2 KB). Ids come from net.log.",
|
|
839
|
-
run: (id) => publicEntry(logged(id), true)
|
|
1035
|
+
description: "One logged request by id, with its request and response bodies whole (not cut at ~2 KB) and its request and response headers (lower-case names). Authorization, Cookie, Set-Cookie, Proxy-Authorization and X-Api-Key values are cut to a short prefix; { full: true } as the second argument shows them whole. Ids come from net.log.",
|
|
1036
|
+
run: (id, options2 = {}) => publicEntry(logged(id), true, options2?.full ? "full" : "cut")
|
|
840
1037
|
},
|
|
841
1038
|
"net.mock": {
|
|
842
|
-
description: 'Answer matching requests with a canned response until unmocked. match: "/path" or { url
|
|
1039
|
+
description: 'Answer matching requests with a canned response until unmocked. match: "/path" (any URL containing it) or { url?: string | { regex }, path?: "/orders/:id" (the whole path on any host, query ignored; :name is one segment, * the rest), method?, body?: JSON subset the JSON request body must contain (objects by key, arrays by length and position), graphql?: "OperationName" (operationName in the JSON body or ?operationName=) }; every key given must match and one of url, path, body, graphql is required. The result has `matches`, which says in words what the mock matches. response: { status?, json?, body?, headers? } or { offline: true }. options: { times?, delayMs?, priority? }. The newest mock is tried first, so a later broad mock shadows an earlier specific one; give the specific one a higher priority (default 0, ties newest first; negative makes a fallback); agent mocks still come before the app mocks. The result lists `shadows` when the new mock will be tried before an agent mock it also matches.',
|
|
843
1040
|
run: (match, response, mockOptions = {}) => {
|
|
844
1041
|
if (typeof response !== "object" || response === null)
|
|
845
1042
|
throw new Error("response must be an object, e.g. { status: 500 }");
|
|
846
1043
|
return addAgentMock(match, response, mockOptions);
|
|
847
1044
|
}
|
|
848
1045
|
},
|
|
1046
|
+
"net.waitFor": {
|
|
1047
|
+
description: 'Wait until a request matching `match` (as net.mock: "/path" or { url?, path?, method?, body?, graphql? }) has finished, and return its entry like net.entry. Finished requests already in the log count, so pass { sinceId } (a log id from net.log) to ignore older ones. [match, { timeoutMs?: 5000, status?, sinceId? }]. A request that failed, was mocked or blocked still counts; status narrows it. Body and graphql checks need the request body: JSON up to 1 MB, and a Request object\'s body only when a mock read it. Times out with an error saying what it looked at.',
|
|
1048
|
+
run: (match, waitOptions = {}) => {
|
|
1049
|
+
const compiled = compileMatch(match);
|
|
1050
|
+
const { timeoutMs = 5e3, status, sinceId } = waitOptions ?? {};
|
|
1051
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0 || timeoutMs > 12e4)
|
|
1052
|
+
throw new Error("timeoutMs must be between 1 and 120000");
|
|
1053
|
+
const deadline = Date.now() + timeoutMs;
|
|
1054
|
+
let unreadable = 0;
|
|
1055
|
+
const find = () => {
|
|
1056
|
+
unreadable = 0;
|
|
1057
|
+
for (const e of state.log) {
|
|
1058
|
+
if (e.pending || fullResponse(e).state === "pending") continue;
|
|
1059
|
+
if (sinceId !== void 0 && e.id <= sinceId) continue;
|
|
1060
|
+
if (status !== void 0 && e.status !== status) continue;
|
|
1061
|
+
if (!compiled.test(e.method, e.url)) continue;
|
|
1062
|
+
if (!compiled.needsBody) return e;
|
|
1063
|
+
const body = requestBodyForMatch(e);
|
|
1064
|
+
if (body === void 0 && e.requestBody !== void 0) unreadable += 1;
|
|
1065
|
+
else if (compiled.testBody(mockRequest(e.method, e.url, {}, body))) return e;
|
|
1066
|
+
}
|
|
1067
|
+
return void 0;
|
|
1068
|
+
};
|
|
1069
|
+
return (async () => {
|
|
1070
|
+
for (; ; ) {
|
|
1071
|
+
const found = find();
|
|
1072
|
+
if (found) return publicEntry(found, true, "cut");
|
|
1073
|
+
if (Date.now() >= deadline) break;
|
|
1074
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
1075
|
+
}
|
|
1076
|
+
throw new Error(
|
|
1077
|
+
`Timed out after ${timeoutMs} ms waiting for a finished request matching ${compiled.matches}${status !== void 0 ? ` with status ${status}` : ""}${unreadable ? ` (${unreadable} matched the URL but their body was not kept)` : ""}; ${state.log.length} requests are logged`
|
|
1078
|
+
);
|
|
1079
|
+
})();
|
|
1080
|
+
}
|
|
1081
|
+
},
|
|
849
1082
|
"net.mockFromLog": {
|
|
850
|
-
description: "Mock the same method and URL as a logged request (id from net.log) with its status and whole response body, optionally changed by patch: JSON objects merge deeply, other values replace. [id, patch?, options?] options as net.mock. Returns { id, match, response }.",
|
|
1083
|
+
description: "Mock the same method and URL as a logged request (id from net.log) with its status and whole response body, optionally changed by patch: JSON objects merge deeply, other values replace. [id, patch?, options?] options as net.mock. Returns { id, matches, match, response }; on a GraphQL request the mock only matches that operationName; `warning` appears when a POST body was not kept and the mock may answer other operations too.",
|
|
851
1084
|
run: (id, patch, mockOptions = {}) => {
|
|
852
1085
|
const entry = logged(id);
|
|
853
1086
|
if (entry.status === void 0)
|
|
@@ -876,12 +1109,26 @@ function networkTools(options = {}) {
|
|
|
876
1109
|
response.body = body;
|
|
877
1110
|
if (contentType) response.headers = { "content-type": contentType };
|
|
878
1111
|
}
|
|
879
|
-
const
|
|
880
|
-
|
|
1112
|
+
const sent = requestBodyForMatch(entry);
|
|
1113
|
+
let graphql = operationName(mockRequest(entry.method, entry.url, {}, sent));
|
|
1114
|
+
if (!graphql && sent === void 0 && entry.requestBody !== void 0)
|
|
1115
|
+
graphql = /"operationName":"((?:[^"\\]|\\.)*)"/.exec(entry.requestBody)?.[1];
|
|
1116
|
+
const unread = !graphql && !["GET", "HEAD"].includes(entry.method) && sent === void 0 ? "This request's body was not kept, so the mock matches every request to that URL, whatever its GraphQL operation. Check it, or unmock and use net.mock with graphql." : void 0;
|
|
1117
|
+
const match = {
|
|
1118
|
+
url: { regex: `^${escapeRegex(entry.url)}$` },
|
|
1119
|
+
method: entry.method,
|
|
1120
|
+
...graphql && { graphql }
|
|
1121
|
+
};
|
|
1122
|
+
return {
|
|
1123
|
+
...addAgentMock(match, response, mockOptions),
|
|
1124
|
+
match: { url: entry.url, method: entry.method, ...graphql && { graphql } },
|
|
1125
|
+
response,
|
|
1126
|
+
...unread && { warning: unread }
|
|
1127
|
+
};
|
|
881
1128
|
}
|
|
882
1129
|
},
|
|
883
1130
|
"net.mocks": {
|
|
884
|
-
description: 'Active mocks, agent and app, in the order they are tried. Response json and body over ~2 KB are cut like net.log (json shown as its cut JSON text, "\u2026 (+N chars)"), with truncated: true; { full: true } returns them whole.',
|
|
1131
|
+
description: 'Active mocks, agent and app, in the order they are tried, each with `matches` in words. Response json and body over ~2 KB are cut like net.log (json shown as its cut JSON text, "\u2026 (+N chars)"), with truncated: true; { full: true } returns them whole.',
|
|
885
1132
|
run: (options2 = {}) => [...state.mocks].sort(mockOrder).map((m) => describeMock(m, options2.full))
|
|
886
1133
|
},
|
|
887
1134
|
"net.unmock": {
|