@codeyam-editor/codeyam-editor 0.1.7 → 0.1.9

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 (45) hide show
  1. package/npm/cloud.js +477 -41
  2. package/npm/editor-dev.js +24 -2
  3. package/npm/editor.js +15 -5
  4. package/npm/fleet-add-failure.js +406 -25
  5. package/npm/fleet-branch.js +124 -1
  6. package/npm/fleet-build-data.js +12 -1
  7. package/npm/fleet-cleanup.js +84 -3
  8. package/npm/fleet-datastore-health.js +107 -0
  9. package/npm/fleet-memory.js +29 -4
  10. package/npm/fleet-queue.js +141 -0
  11. package/npm/fleet-revision.js +108 -0
  12. package/npm/fleet-run-setup.js +66 -0
  13. package/npm/fleet-session-activity.js +162 -0
  14. package/npm/fleet-vm-access.js +162 -0
  15. package/npm/fleet-vm-readiness.js +57 -16
  16. package/npm/ingress-invoke.js +46 -2
  17. package/npm/launch-config.js +169 -2
  18. package/npm/scenario-check.js +708 -35
  19. package/npm/scenario-handlers.js +18 -0
  20. package/npm/scenario-interactivity.js +477 -23
  21. package/npm/scenario-issues.js +72 -0
  22. package/npm/scenario-mocks.js +245 -39
  23. package/npm/scenario-playwright.js +137 -26
  24. package/npm/utils.js +262 -22
  25. package/package.json +5 -5
  26. package/ui/dist/assets/{ContainerInspector-Cu1tSN-o.js → ContainerInspector-C7I9wctz.js} +1 -1
  27. package/ui/dist/assets/EditorShell-9aYMeOdZ.js +330 -0
  28. package/ui/dist/assets/EditorShell-CE8iG_EM.css +1 -0
  29. package/ui/dist/assets/{ProjectLauncherScreen-Dmm6Z0mv.js → ProjectLauncherScreen-E0NYYiwy.js} +1 -1
  30. package/ui/dist/assets/ScenarioDataPanel-CLwkrex0.js +18 -0
  31. package/ui/dist/assets/{ScenarioDataPanelFullPage-BqMxWVKW.js → ScenarioDataPanelFullPage-DT0TZC4M.js} +1 -1
  32. package/ui/dist/assets/ScenarioRenderer-Bsarhcrl.js +21 -0
  33. package/ui/dist/assets/index-BRoAenc1.css +1 -0
  34. package/ui/dist/assets/index-iMR5siiG.js +27 -0
  35. package/ui/dist/assets/useServerIdentity-CVuHTzjg.js +1 -0
  36. package/ui/dist/index.html +2 -2
  37. package/ui/dist/preview-subdir-demo/index.html +62 -0
  38. package/ui/dist/preview-subdir-demo/sibling.html +48 -0
  39. package/ui/dist/assets/EditorShell--V2ExYQ6.js +0 -260
  40. package/ui/dist/assets/EditorShell-DP5UY20G.css +0 -1
  41. package/ui/dist/assets/ScenarioDataPanel-DjprZq2L.js +0 -9
  42. package/ui/dist/assets/ScenarioRenderer-DqLVVMYS.js +0 -21
  43. package/ui/dist/assets/index-CjxWhkSy.css +0 -1
  44. package/ui/dist/assets/index-VuyqBbGJ.js +0 -27
  45. package/ui/dist/assets/useServerIdentity-BdXy8TQv.js +0 -1
@@ -7,6 +7,19 @@ function createIssue(kind, message, extra = {}) {
7
7
  };
8
8
  if (extra.matchedPattern != null) issue.matchedPattern = extra.matchedPattern;
9
9
  if (extra.contextSnippet != null) issue.contextSnippet = extra.contextSnippet;
10
+ // The hydration gate's cross-origin ATTRIBUTION, carried as data rather than
11
+ // only as prose. A consumer that must act on the verdict — escalating the
12
+ // preview off the subpath when the counterpart origin hydrates — would
13
+ // otherwise have to regex the English message, which breaks the moment the
14
+ // wording is improved. Same conditional shape as the two fields above so an
15
+ // issue that carries no attribution serializes exactly as it did before.
16
+ if (extra.crossOrigin != null) issue.crossOrigin = extra.crossOrigin;
17
+ if (extra.counterpartUrl != null) issue.counterpartUrl = extra.counterpartUrl;
18
+ // The dev-server-down verdict, carried as data for the same reason as the
19
+ // attribution fields above: a consumer that must distinguish "the app served
20
+ // an error" from "the app was not serving at all" should branch on a field,
21
+ // not on the prose of the message.
22
+ if (extra.devServerDown != null) issue.devServerDown = extra.devServerDown;
10
23
  return issue;
11
24
  }
12
25
 
@@ -17,6 +30,42 @@ function pushIssue(issues, issue) {
17
30
  }
18
31
  }
19
32
 
33
+ // Derive which origin a capture actually loaded from, and whether that differs
34
+ // from the origin the VIEWER's browser is served.
35
+ //
36
+ // The two can legitimately differ: the capture browser runs where the app is,
37
+ // the viewer may not, and aiming them independently is what makes a capture
38
+ // work on a cloud VM at all. A capture that renders perfectly from the app's
39
+ // own origin while the user's pane sits on `/__codeyam_preview` is a USEFUL
40
+ // result — but only if the difference travels with it instead of having to be
41
+ // inferred from a URL the reader must parse themselves.
42
+ //
43
+ // Both unknowns resolve toward "no split", never toward a guess. A caller that
44
+ // supplies no `viewerOrigin` (every pre-existing call site) and a `url` with no
45
+ // parseable origin both yield `false`, so no existing consumer sees a changed
46
+ // verdict and an unclassifiable capture is never reported as diverging.
47
+ //
48
+ // Deliberately not reusing `isCrossOriginRequest` from scenario-check.js: that
49
+ // module imports THIS one, so depending on it would invert the dependency, and
50
+ // its fail-safe answers a different question (never strip markers) than this
51
+ // one does (never claim a split).
52
+ function deriveOriginSplit(url, viewerOrigin) {
53
+ let captureOrigin = null;
54
+ try {
55
+ captureOrigin = new URL(url).origin;
56
+ } catch (_) {
57
+ /* a malformed or relative capture URL has no origin to report */
58
+ }
59
+ return {
60
+ captureOrigin,
61
+ viewerOrigin: viewerOrigin ?? null,
62
+ captureOriginDiffersFromViewer:
63
+ captureOrigin != null &&
64
+ viewerOrigin != null &&
65
+ captureOrigin !== viewerOrigin,
66
+ };
67
+ }
68
+
20
69
  function buildResult({
21
70
  loaded,
22
71
  hasContent,
@@ -24,7 +73,15 @@ function buildResult({
24
73
  outputPath,
25
74
  url,
26
75
  unmockedRoutes = [],
76
+ mockUsage = { used: [], unused: [] },
77
+ externalRequests = [],
78
+ viewerOrigin = null,
27
79
  }) {
80
+ const { captureOrigin, captureOriginDiffersFromViewer } = deriveOriginSplit(
81
+ url,
82
+ viewerOrigin,
83
+ );
84
+
28
85
  return {
29
86
  ok: loaded && hasContent && issues.length === 0,
30
87
  loaded,
@@ -37,6 +94,20 @@ function buildResult({
37
94
  // the actionable route list the failure message and `stub-unmocked-routes`
38
95
  // consume. Defaults to `[]` so callers that omit it are unchanged.
39
96
  unmockedRoutes,
97
+ // Diagnostic-only, and deliberately NOT part of `ok`: an unused mock is not
98
+ // automatically an error (a scenario may legitimately declare a mock for a
99
+ // request the page makes only on interaction). Reporting it turns a silent
100
+ // inertness into a visible fact; failing on it would break working scenarios
101
+ // for a heuristic.
102
+ mockUsage,
103
+ // Requests grouped by origin, split mocked/unmocked and same/cross-origin.
104
+ // The line that ends a "why is this page blank" misdiagnosis.
105
+ externalRequests,
106
+ // The two hops, named. See `deriveOriginSplit` for why they can differ and
107
+ // why reporting the difference is not optional.
108
+ captureOrigin,
109
+ viewerOrigin,
110
+ captureOriginDiffersFromViewer,
40
111
  };
41
112
  }
42
113
 
@@ -44,4 +115,5 @@ module.exports = {
44
115
  createIssue,
45
116
  pushIssue,
46
117
  buildResult,
118
+ deriveOriginSplit,
47
119
  };
@@ -1,22 +1,46 @@
1
- // Route matcher shared, semantically, with the injected live-preview shim
2
- // (crates/proxy-http/src/fetch_patch.js::matchUrl / matchPath / matchQuerySpec).
3
- // This is a VERBATIM port of that shim's matcher — the two interception paths
4
- // MUST agree on which requests a mock route covers. When they drifted (the
5
- // shim gained `:param` + query-spec parity but this capture-harness matcher was
6
- // left doing exact string matching), a parameterized or query-spec'd mock
7
- // rendered in the Live Preview but escaped Playwright's `page.route` during
8
- // capture and rendered an empty list. `scenario-mocks.test.js` pins the two to
9
- // identical verdicts on a shared fixture table so a future divergence is a
10
- // test failure, not another lost build session.
1
+ // Route matcher for the capture harness — one of THREE implementations of a
2
+ // single semantic. The authority is the Rust engine
3
+ // (crates/mock-engine/src/route_parser.rs), where the contract is written down;
4
+ // this file and the injected live-preview shim
5
+ // (crates/proxy-http/src/fetch_patch.js) are ports of it.
11
6
  //
12
- // `:param` path segments match any single non-empty segment; a `?k=v` / `?k=*`
13
- // query-spec suffix constrains the query as a subset match (`k=*` matches any
14
- // value; an empty spec matches any query). Full-URL patterns (`http://…`) match
15
- // by prefix on the raw URL. Mirrors crates/mock-engine's server matcher.
7
+ // This is NOT a verbatim copy of the shim, and describing it as one is what let
8
+ // the drift hide: the two JS matchers agreed with EACH OTHER while both
9
+ // disagreed with the server (they prefix-matched full-URL patterns and returned
10
+ // before applying any query spec, where the server anchored). All three are now
11
+ // pinned against one shared fixture table,
12
+ // crates/mock-engine/fixtures/route-match-fixtures.json, asserted with its
13
+ // expected verdicts by both `scenario-mocks.test.js` and the Rust tests — so a
14
+ // change to one implementation the others do not follow is a test failure, and
15
+ // two matchers agreeing on a wrong answer no longer passes.
16
+ //
17
+ // The semantic, in short (see route_parser.rs for the full contract): `:name`
18
+ // and `*` each match one non-empty path segment; a TRAILING `**` matches the
19
+ // remaining path including `/` and including an empty remainder; a `?k=v` /
20
+ // `?k=*` suffix constrains the query as a subset match. A full-URL pattern
21
+ // (`http://…`) with no wildcard matches by PREFIX — the compatibility rule
22
+ // every shipped external mock relies on — while a wildcarded one matches by the
23
+ // wildcard rules.
16
24
  function matchUrl(pattern, url) {
17
- // Full-URL patterns (external APIs) match by prefix on the raw URL.
18
- if (pattern.startsWith("http://") || pattern.startsWith("https://")) {
19
- return url.indexOf(pattern) === 0;
25
+ // Split the query spec off FIRST — for full-URL and path-only patterns
26
+ // alike. Returning early for full URLs (as this did) meant a spec like
27
+ // `?k=*` on an external mock was silently ignored.
28
+ const pqi = pattern.indexOf("?");
29
+ const patPath = pqi >= 0 ? pattern.slice(0, pqi) : pattern;
30
+ const patSpec = pqi >= 0 ? pattern.slice(pqi + 1) : "";
31
+
32
+ // Full-URL patterns (external APIs) match against the whole URL, with its
33
+ // own query stripped before the compare.
34
+ if (patPath.startsWith("http://") || patPath.startsWith("https://")) {
35
+ const [rawPath, rawQuery] = splitUrlQuery(url);
36
+ // Compatibility rule: a wildcard-free full-URL pattern prefix-matches,
37
+ // which is what every external mock relies on today. A wildcarded one
38
+ // matches by the wildcard rules, with no prefix fallback.
39
+ const urlMatched = hasWildcard(patPath)
40
+ ? matchPath(patPath, rawPath)
41
+ : rawPath.indexOf(patPath) === 0;
42
+ if (!urlMatched) return false;
43
+ return matchQuerySpec(patSpec, rawQuery);
20
44
  }
21
45
 
22
46
  let reqPath = url;
@@ -26,35 +50,71 @@ function matchUrl(pattern, url) {
26
50
  reqPath = u.pathname;
27
51
  reqQuery = u.search.replace(/^\?/, "");
28
52
  } catch {
29
- const qi = url.indexOf("?");
30
- if (qi >= 0) {
31
- reqPath = url.slice(0, qi);
32
- reqQuery = url.slice(qi + 1);
33
- }
53
+ [reqPath, reqQuery] = splitUrlQuery(url);
34
54
  }
35
55
 
36
- const patParts = pattern.split("?");
37
- if (!matchPath(patParts[0], reqPath)) return false;
38
- return matchQuerySpec(patParts.length > 1 ? patParts[1] : "", reqQuery);
56
+ if (!matchPath(patPath, reqPath)) return false;
57
+ return matchQuerySpec(patSpec, reqQuery);
58
+ }
59
+
60
+ // Split a raw URL into [everything-before-the-query, query]. The `#fragment`
61
+ // is dropped from both halves, mirroring the server's strip_url_query.
62
+ function splitUrlQuery(url) {
63
+ const qi = url.indexOf("?");
64
+ const hi = url.indexOf("#");
65
+ const cut = qi < 0 ? hi : hi < 0 ? qi : Math.min(qi, hi);
66
+ const head = cut < 0 ? url : url.slice(0, cut);
67
+ if (qi < 0) return [head, ""];
68
+ const query = url.slice(qi + 1);
69
+ const fi = query.indexOf("#");
70
+ return [head, fi >= 0 ? query.slice(0, fi) : query];
71
+ }
72
+
73
+ // True when a segment is a valid `:name` param. A `:`-prefixed segment with
74
+ // an empty or punctuated name is matched literally, same as the server.
75
+ function isParamSegment(seg) {
76
+ return /^:[\p{L}\p{N}_]+$/u.test(seg);
39
77
  }
40
78
 
41
- // Segment-wise path match with `:param` wildcards. A `:name` segment matches
42
- // any single non-empty segment; every other segment must be equal, and the
43
- // segment counts must match — no implicit prefix match, same as the server.
79
+ // True when a segment is any wildcard: `:name`, `*`, or `**`.
80
+ function isWildcardSegment(seg) {
81
+ return seg === "*" || seg === "**" || isParamSegment(seg);
82
+ }
83
+
84
+ // True when the pattern contains any wildcard segment.
85
+ function hasWildcard(pattern) {
86
+ return pattern.split("/").some(isWildcardSegment);
87
+ }
88
+
89
+ // Segment-wise path match. `:name` and `*` each match any single non-empty
90
+ // segment; a TRAILING `**` matches the remaining path including `/` and
91
+ // including an empty remainder (`/api/**` covers `/api`); every other segment
92
+ // must be equal. No implicit prefix match, same as the server.
44
93
  function matchPath(pattern, path) {
45
94
  const pp = pattern.split("/");
46
95
  const rp = path.split("/");
96
+ if (pp[pp.length - 1] === "**") {
97
+ const head = pp.length - 1;
98
+ if (rp.length < head) return false;
99
+ for (let i = 0; i < head; i++) {
100
+ if (!matchSegment(pp[i], rp[i])) return false;
101
+ }
102
+ return true;
103
+ }
47
104
  if (pp.length !== rp.length) return false;
48
105
  for (let i = 0; i < pp.length; i++) {
49
- if (pp[i].charAt(0) === ":") {
50
- if (rp[i] === "") return false;
51
- continue;
52
- }
53
- if (pp[i] !== rp[i]) return false;
106
+ if (!matchSegment(pp[i], rp[i])) return false;
54
107
  }
55
108
  return true;
56
109
  }
57
110
 
111
+ // One segment. `:name`, `*`, and a non-final `**` each match any single
112
+ // non-empty segment; anything else must be equal.
113
+ function matchSegment(pat, seg) {
114
+ if (isWildcardSegment(pat)) return seg !== "";
115
+ return pat === seg;
116
+ }
117
+
58
118
  // Query-spec match. An empty spec matches any query (a path-only route still
59
119
  // matches a request that carries query params). Otherwise every `k=v` pair in
60
120
  // the spec must be present in the request query; `k=*` matches any value.
@@ -82,7 +142,11 @@ function splitMockKey(key) {
82
142
  return { method: key.slice(0, spaceIdx), route: key.slice(spaceIdx + 1) };
83
143
  }
84
144
 
85
- function findHttpMock(httpMocks, request) {
145
+ // The matching mock together with the KEY that declared it, or null. The key is
146
+ // what the usage tally counts — reporting "which declared mock fired" needs the
147
+ // declaration's identity, not just its body, since two keys can carry
148
+ // structurally identical mocks.
149
+ function findHttpMockEntry(httpMocks, request) {
86
150
  const method = request.method().toUpperCase();
87
151
  const url = request.url();
88
152
  for (const [key, mock] of Object.entries(httpMocks)) {
@@ -90,11 +154,16 @@ function findHttpMock(httpMocks, request) {
90
154
  if (!parsed) continue;
91
155
  // Method stays an exact check; only the route is matched by pattern.
92
156
  if (parsed.method.toUpperCase() !== method) continue;
93
- if (matchUrl(parsed.route, url)) return mock;
157
+ if (matchUrl(parsed.route, url)) return { key, mock };
94
158
  }
95
159
  return null;
96
160
  }
97
161
 
162
+ function findHttpMock(httpMocks, request) {
163
+ const entry = findHttpMockEntry(httpMocks, request);
164
+ return entry ? entry.mock : null;
165
+ }
166
+
98
167
  // The set of path/URL route patterns declared by the mock keys. A key is
99
168
  // `"<METHOD> <route>"` (e.g. `"GET /api/plans"`); we strip the method so the
100
169
  // route matcher can decide whether a request *could* match any mock without
@@ -121,8 +190,134 @@ function requestTargetsMock(targets, url) {
121
190
  return false;
122
191
  }
123
192
 
124
- async function attachHttpMocks(page, httpMocks) {
125
- if (!httpMocks || Object.keys(httpMocks).length === 0) return;
193
+ // The origin half of a URL, or the raw string when it will not parse. Grouping
194
+ // by origin is what turns twenty tile URLs into one readable line.
195
+ function originOf(url) {
196
+ try {
197
+ return new URL(url).origin;
198
+ } catch {
199
+ return url;
200
+ }
201
+ }
202
+
203
+ // Which declared mock keys actually served a response.
204
+ //
205
+ // This exists because a declared mock whose key never matches any request is
206
+ // silently inert: the capture still reports success, and the frame that lands
207
+ // is the healthy state under a name promising a degraded one. The declared keys
208
+ // were always known; nothing ever compared them against what fired.
209
+ function createMockUsageTally(httpMocks) {
210
+ const declared = Object.keys(httpMocks || {});
211
+ const usedKeys = new Set();
212
+ return {
213
+ recordFulfilled(key) {
214
+ usedKeys.add(key);
215
+ },
216
+ // Declared order is preserved in both lists so the reported set reads the
217
+ // same way the scenario's mock block does.
218
+ get used() {
219
+ return declared.filter((key) => usedKeys.has(key));
220
+ },
221
+ get unused() {
222
+ return declared.filter((key) => !usedKeys.has(key));
223
+ },
224
+ };
225
+ }
226
+
227
+ // Every request the page made, collapsed to one row per `(origin, mocked)` pair.
228
+ // A full URL list is noise (twenty tile URLs); origin + count + a representative
229
+ // URL is the line that ends a "why is this page blank" misdiagnosis.
230
+ function createRequestInventory(appOrigin) {
231
+ // Keyed by `origin|mocked` rather than origin alone: an origin serving both a
232
+ // mocked API call and unmocked sub-resources would otherwise collapse into one
233
+ // row whose `mocked` flag is a coin toss.
234
+ const groups = new Map();
235
+
236
+ function groupFor(url, mocked) {
237
+ const origin = originOf(url);
238
+ const groupKey = `${origin}|${mocked ? "mocked" : "unmocked"}`;
239
+ let row = groups.get(groupKey);
240
+ if (!row) {
241
+ row = {
242
+ origin,
243
+ count: 0,
244
+ sampleUrl: url,
245
+ mocked: !!mocked,
246
+ failed: false,
247
+ // `false` when the capture URL did not parse — the caller then has no
248
+ // origin to compare against, and treating everything as cross-origin
249
+ // over-reports rather than hiding a request.
250
+ sameOrigin: !!appOrigin && origin === appOrigin,
251
+ };
252
+ groups.set(groupKey, row);
253
+ }
254
+ return row;
255
+ }
256
+
257
+ return {
258
+ recordRequest(url, mocked) {
259
+ groupFor(url, mocked).count += 1;
260
+ },
261
+ recordFailure(url, mocked) {
262
+ groupFor(url, mocked).failed = true;
263
+ },
264
+ // Busiest origin first — the one worth explaining leads the report.
265
+ get groups() {
266
+ return [...groups.values()].sort(
267
+ (a, b) => b.count - a.count || a.origin.localeCompare(b.origin),
268
+ );
269
+ },
270
+ };
271
+ }
272
+
273
+ // Composes the two independent halves above into the single handle the capture
274
+ // harness threads through.
275
+ //
276
+ // Purely observational. Nothing here changes which requests are intercepted:
277
+ // `attachHttpMocks` scopes `page.route` to declared targets so unmocked requests
278
+ // load natively, and that scoping is load-bearing (a blanket catch-all route
279
+ // predicate breaks Vite ESM module loading and blanks the SPA).
280
+ function createMockObserver(httpMocks, appOrigin) {
281
+ const usage = createMockUsageTally(httpMocks);
282
+ const inventory = createRequestInventory(appOrigin);
283
+ return {
284
+ recordFulfilled: (key) => usage.recordFulfilled(key),
285
+ recordRequest: (url, mocked) => inventory.recordRequest(url, mocked),
286
+ recordFailure: (url, mocked) => inventory.recordFailure(url, mocked),
287
+ get used() {
288
+ return usage.used;
289
+ },
290
+ get unused() {
291
+ return usage.unused;
292
+ },
293
+ get externalRequests() {
294
+ return inventory.groups;
295
+ },
296
+ };
297
+ }
298
+
299
+ // Passive request inventory. `page.on` observes; it never routes, so attaching
300
+ // this cannot change what the page loads.
301
+ function attachRequestInventory(page, observer, httpMocks) {
302
+ const targets = mockedTargets(httpMocks || {});
303
+ page.on("request", (request) => {
304
+ const url = request.url();
305
+ observer.recordRequest(url, requestTargetsMock(targets, url));
306
+ });
307
+ page.on("requestfailed", (request) => {
308
+ const url = request.url();
309
+ observer.recordFailure(url, requestTargetsMock(targets, url));
310
+ });
311
+ }
312
+
313
+ // Returns the capture's mock observer (see `createMockObserver`). Always returns
314
+ // one, including for a scenario that declares no mocks at all — that is exactly
315
+ // the case where an unexplained third-party fetch is hardest to account for.
316
+ async function attachHttpMocks(page, httpMocks, options = {}) {
317
+ const observer = createMockObserver(httpMocks, options.appOrigin);
318
+ attachRequestInventory(page, observer, httpMocks);
319
+
320
+ if (!httpMocks || Object.keys(httpMocks).length === 0) return observer;
126
321
 
127
322
  // Intercept ONLY requests whose path matches a declared mock — not every
128
323
  // request. A blanket `page.route("**/*")` intercepts the dev server's ESM
@@ -134,11 +329,15 @@ async function attachHttpMocks(page, httpMocks) {
134
329
  await page.route(
135
330
  (url) => requestTargetsMock(targets, url),
136
331
  async (route) => {
137
- const mock = findHttpMock(httpMocks, route.request());
138
- if (!mock) {
332
+ const entry = findHttpMockEntry(httpMocks, route.request());
333
+ if (!entry) {
139
334
  await route.continue();
140
335
  return;
141
336
  }
337
+ const { key, mock } = entry;
338
+ // Counted at the fulfil branch, not at match time: a key is "used" only
339
+ // when it actually served a response.
340
+ observer.recordFulfilled(key);
142
341
 
143
342
  const headers = { ...(mock.headers || {}) };
144
343
  let body;
@@ -175,6 +374,8 @@ async function attachHttpMocks(page, httpMocks) {
175
374
  body: "[]",
176
375
  });
177
376
  });
377
+
378
+ return observer;
178
379
  }
179
380
 
180
381
 
@@ -222,8 +423,13 @@ module.exports = {
222
423
  matchPath,
223
424
  matchQuerySpec,
224
425
  findHttpMock,
426
+ findHttpMockEntry,
225
427
  mockedTargets,
226
428
  requestTargetsMock,
429
+ originOf,
430
+ createMockUsageTally,
431
+ createRequestInventory,
432
+ createMockObserver,
227
433
  attachHttpMocks,
228
434
  isDeclaredErrorMock,
229
435
  unmockedRouteFrom,