@codeyam-editor/codeyam-editor 0.1.0 → 0.1.2

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 (47) hide show
  1. package/npm/cli.js +5 -2
  2. package/npm/cloud-branch-switch.js +442 -0
  3. package/npm/cloud.js +1521 -0
  4. package/npm/container.js +28 -5
  5. package/npm/editor-dev.js +378 -0
  6. package/npm/editor.js +199 -12
  7. package/npm/empty-project-dir.js +33 -0
  8. package/npm/fleet-auth.js +111 -0
  9. package/npm/fleet-cleanup.js +124 -0
  10. package/npm/fleet-env.js +125 -0
  11. package/npm/fleet-memory.js +127 -0
  12. package/npm/fleet-pool-policy.js +78 -0
  13. package/npm/fleet-pool.js +401 -0
  14. package/npm/fleet-queue.js +40 -0
  15. package/npm/fleet-reconcile.js +36 -0
  16. package/npm/fleet-reset.js +144 -0
  17. package/npm/fleet-roster.js +81 -0
  18. package/npm/launch-config.js +544 -0
  19. package/npm/preflight-guidance.js +68 -0
  20. package/npm/scenario-check.js +355 -290
  21. package/npm/scenario-handlers.js +82 -0
  22. package/npm/scenario-interactivity.js +199 -0
  23. package/npm/scenario-issues.js +35 -0
  24. package/npm/scenario-metrics.js +108 -0
  25. package/npm/scenario-mocks.js +107 -0
  26. package/npm/scenario-playwright.js +355 -0
  27. package/npm/utils.js +548 -22
  28. package/npm/vm-urls.js +117 -0
  29. package/package.json +13 -6
  30. package/ui/dist/assets/ContainerInspector-C8EkH4PP.js +2 -0
  31. package/ui/dist/assets/EditorShell-DWaeBxgj.js +186 -0
  32. package/ui/dist/assets/Markdown-BHBqz-Mj.js +1 -0
  33. package/ui/dist/assets/ScenarioDataPanel-DYP7pi_n.css +32 -0
  34. package/ui/dist/assets/ScenarioDataPanel-zJSXtXL4.js +5 -0
  35. package/ui/dist/assets/ScenarioDataPanelFullPage-C0YHQB-T.js +1 -0
  36. package/ui/dist/assets/ScenarioRenderer-CgDxuigm.js +1 -0
  37. package/ui/dist/assets/StepsSimulator-CRtmlyNZ.js +1 -0
  38. package/ui/dist/assets/index-BUYNFqKK.js +27 -0
  39. package/ui/dist/assets/index-DVr5rY2d.css +1 -0
  40. package/ui/dist/assets/markdown-v0F0UEt3.js +37 -0
  41. package/ui/dist/assets/react-CSS0HapR.js +28 -0
  42. package/ui/dist/assets/useEvents-DggWrtHe.js +1 -0
  43. package/ui/dist/assets/xterm--24IGk-x.js +9 -0
  44. package/ui/dist/favicon.ico +0 -0
  45. package/ui/dist/favicon.png +0 -0
  46. package/ui/dist/index.html +16 -0
  47. package/npm/postinstall.js +0 -95
@@ -0,0 +1,82 @@
1
+ const { createIssue } = require("./scenario-issues");
2
+
3
+ function getInitScript() {
4
+ return `
5
+ window.__codeyamUnhandledRejections = [];
6
+ window.addEventListener("unhandledrejection", (event) => {
7
+ const reason = event.reason;
8
+ const message =
9
+ reason instanceof Error ? reason.message : String(reason);
10
+ window.__codeyamUnhandledRejections.push(message);
11
+ });
12
+
13
+ // Stub WebSocket during capture to prevent terminal reconnection spam.
14
+ window.WebSocket = class StubWebSocket {
15
+ static CONNECTING = 0;
16
+ static OPEN = 1;
17
+ static CLOSING = 2;
18
+ static CLOSED = 3;
19
+ readyState = 3;
20
+ onopen = null;
21
+ onclose = null;
22
+ onerror = null;
23
+ onmessage = null;
24
+ send() {}
25
+ close() {}
26
+ addEventListener() {}
27
+ removeEventListener() {}
28
+ dispatchEvent() { return false; }
29
+ constructor() {
30
+ setTimeout(() => {
31
+ if (this.onerror) this.onerror(new Event("error"));
32
+ if (this.onclose) this.onclose(new CloseEvent("close"));
33
+ }, 0);
34
+ }
35
+ };
36
+ `;
37
+ }
38
+
39
+ function handleConsoleMessage(message) {
40
+ if (message.type() !== "error") return null;
41
+ const text = message.text();
42
+
43
+ // Ignore known dev-server WebSocket/HMR errors from Vite proxy
44
+ if (
45
+ text.includes("WebSocket connection to") ||
46
+ text.includes("Unsupported Media Type")
47
+ ) {
48
+ return null;
49
+ }
50
+
51
+ return createIssue("console", text);
52
+ }
53
+
54
+ function handlePageError(error) {
55
+ return createIssue("pageerror", error.message || String(error));
56
+ }
57
+
58
+ function handleRequestFailed(request) {
59
+ const errorText = request.failure()?.errorText || "Request failed";
60
+
61
+ // Filter benign request cancellations. `net::ERR_ABORTED` is what Playwright
62
+ // emits when a request is in-flight at the moment the page is closed (or the
63
+ // iframe is destroyed). For scenarios whose pages fetch large payloads (the
64
+ // editor's own EditorShell mounts a 2.2MB `/api/tests` fetch), the
65
+ // browser.close() at the end of capture races the fetch and produces this
66
+ // event AFTER the screenshot has already been taken — there is no real
67
+ // failure to surface. Genuine network failures arrive under different
68
+ // codes (net::ERR_CONNECTION_REFUSED, net::ERR_NAME_NOT_RESOLVED, etc.)
69
+ // and continue to be reported.
70
+ if (errorText.includes("net::ERR_ABORTED")) {
71
+ return null;
72
+ }
73
+
74
+ return createIssue("requestfailed", errorText, { url: request.url() });
75
+ }
76
+
77
+ module.exports = {
78
+ getInitScript,
79
+ handleConsoleMessage,
80
+ handlePageError,
81
+ handleRequestFailed,
82
+ };
@@ -0,0 +1,199 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const { createIssue } = require("./scenario-issues");
4
+
5
+ // Hydration / interactivity probe for the headless capture browser.
6
+ //
7
+ // Background: a page can return HTTP 200, render visible content, log no
8
+ // console errors, and still be completely dead — the client framework never
9
+ // hydrated, so every button and handler is inert. The status / render /
10
+ // console / image gates all pass and the broken page sails through to the
11
+ // user (the catalog whose filter buttons did nothing). This probe asserts the
12
+ // page actually became interactive before the capture gate reports success,
13
+ // so a non-hydrating page can no longer pass `client-errors`.
14
+ //
15
+ // Stack assumption: WHICH framework's hydration we look for is data-driven
16
+ // from `.codeyam/stack.json` (or an explicit override) — never hardcoded into
17
+ // the capture flow. Stacks with no client runtime (backend services, CLIs,
18
+ // static HTML) are a documented no-op pass. Frameworks for which we have no
19
+ // reliable in-page attachment signal are ALSO a conservative pass: we never
20
+ // flag a page we cannot prove is dead, so a healthy Svelte / Solid / vanilla
21
+ // page is never a false negative. A new framework detector is a new entry in
22
+ // `detectors` below plus a `KNOWN_FRAMEWORKS` mapping — not an edit here.
23
+
24
+ // Frameworks we have an in-page attachment detector for. Inference only
25
+ // resolves to one of these; an unrecognised framework yields `null`, which
26
+ // the caller treats as "cannot determine" (conservative pass).
27
+ const KNOWN_FRAMEWORKS = ["react", "vue"];
28
+
29
+ // Read `.codeyam/stack.json` relative to the capture script's cwd (the project
30
+ // dir — `scenario_check.rs` sets `.current_dir(project_dir)`). Never throws: a
31
+ // missing or malformed file yields `null` so the probe degrades to a no-op
32
+ // rather than breaking a capture.
33
+ function readStackJson() {
34
+ try {
35
+ const raw = fs.readFileSync(path.join(".codeyam", "stack.json"), "utf8");
36
+ return JSON.parse(raw);
37
+ } catch (_) {
38
+ return null;
39
+ }
40
+ }
41
+
42
+ // Scan a stack descriptor's identity fields for a framework we can probe.
43
+ // Pure — no I/O — so the matching rules are unit-tested without a stack.json.
44
+ function inferFramework(stack) {
45
+ if (!stack) return null;
46
+ const haystack = [
47
+ stack.id,
48
+ stack.name,
49
+ ...(Array.isArray(stack.technologies) ? stack.technologies : []),
50
+ ]
51
+ .filter((s) => typeof s === "string")
52
+ .join(" ")
53
+ .toLowerCase();
54
+ for (const fw of KNOWN_FRAMEWORKS) {
55
+ if (haystack.includes(fw)) return fw;
56
+ }
57
+ return null;
58
+ }
59
+
60
+ // Decide, from a stack descriptor, whether the capture should expect a
61
+ // hydrated client runtime and which framework to probe for. Pure.
62
+ //
63
+ // `capture.interactivity === false` is an explicit opt-out for stacks that
64
+ // render no client runtime. `capture.interactivity.framework` is an explicit
65
+ // override when inference can't see the framework in the identity fields.
66
+ // Otherwise we infer: a known client framework, or `routing.type ===
67
+ // "client-side"`, implies a runtime that must hydrate; backend / static / CLI
68
+ // stacks match neither and no-op.
69
+ function resolveInteractivityExpectation(stack) {
70
+ const capture = (stack && stack.capture) || {};
71
+ if (capture.interactivity === false) {
72
+ return { expectInteractive: false, framework: null };
73
+ }
74
+ const explicit =
75
+ capture.interactivity && typeof capture.interactivity === "object"
76
+ ? capture.interactivity.framework
77
+ : null;
78
+ if (typeof explicit === "string" && explicit.length > 0) {
79
+ return { expectInteractive: true, framework: explicit.toLowerCase() };
80
+ }
81
+ const framework = inferFramework(stack);
82
+ const routingType =
83
+ stack && stack.routing && typeof stack.routing.type === "string"
84
+ ? stack.routing.type
85
+ : null;
86
+ const expectInteractive = framework != null || routingType === "client-side";
87
+ return { expectInteractive, framework };
88
+ }
89
+
90
+ // Run the in-page detection inside the loaded frame. Read-only: it inspects
91
+ // DOM-node properties left by a framework's hydration but never clicks or
92
+ // mutates anything, so it is safe to run before the screenshot is taken.
93
+ //
94
+ // Returns `{ controlCount, frameworkAttached }` where `frameworkAttached` is
95
+ // `true` (runtime demonstrably attached), `false` (controls exist but no
96
+ // attachment signal — the dead-hydration case), or `null` (no detector for
97
+ // this framework → cannot judge).
98
+ async function collectHydrationState(frame, { framework } = {}) {
99
+ return frame.evaluate((fw) => {
100
+ const SELECTOR =
101
+ 'button, [role="button"], a[href], input:not([type="hidden"]), select, textarea, summary, [onclick]';
102
+ const controls = Array.from(document.querySelectorAll(SELECTOR));
103
+
104
+ // Per-framework attachment detectors. Each returns true when the framework
105
+ // has demonstrably attached its client runtime to a live interactive node.
106
+ // The presence of a detector for `fw` is itself the signal that we CAN
107
+ // judge this framework; the default (no detector) means "cannot determine".
108
+ const detectors = {
109
+ react: (els) =>
110
+ els.some(
111
+ (el) =>
112
+ !!el &&
113
+ Object.keys(el).some(
114
+ (k) =>
115
+ k.startsWith("__reactFiber$") ||
116
+ k.startsWith("__reactProps$") ||
117
+ k.startsWith("__reactContainer$"),
118
+ ),
119
+ ),
120
+ vue: (els) =>
121
+ els.some(
122
+ (el) =>
123
+ !!el &&
124
+ (el.__vue__ != null ||
125
+ el.__vnode != null ||
126
+ el.__vueParentComponent != null),
127
+ ) || !!document.querySelector("[data-v-app]"),
128
+ };
129
+
130
+ const detector = detectors[fw];
131
+ const frameworkAttached = detector ? detector(controls) : null;
132
+ return { controlCount: controls.length, frameworkAttached };
133
+ }, framework || null);
134
+ }
135
+
136
+ // Turn a collected state into a `hydration` issue, or `null` to pass. Pure, so
137
+ // every branch is unit-tested without a browser.
138
+ //
139
+ // Pass when: no client runtime expected (no-op stacks); no interactive control
140
+ // to probe (a static content page in a client app is legitimately inert); the
141
+ // framework attached; OR we couldn't determine attachment (`null` — never
142
+ // false-positive). Flag only the proven-dead case: controls exist AND the
143
+ // framework's runtime is demonstrably not attached.
144
+ function interpretHydration({
145
+ expectInteractive,
146
+ controlCount,
147
+ frameworkAttached,
148
+ framework,
149
+ url,
150
+ }) {
151
+ if (!expectInteractive) return null;
152
+ if (!(controlCount > 0)) return null;
153
+ if (frameworkAttached !== false) return null;
154
+ const fw = framework || "the client framework";
155
+ const plural = controlCount === 1 ? "" : "s";
156
+ return createIssue(
157
+ "hydration",
158
+ `Page rendered ${controlCount} interactive control${plural} but ${fw} never attached ` +
159
+ `event handlers — the page is not interactive (hydration did not run). Client JS may ` +
160
+ `not be executing; check the preview proxy and the browser console. Run ` +
161
+ "`codeyam-editor editor diagnose-preview --path <route>` to pinpoint a proxy " +
162
+ `HTML-injection blocker.`,
163
+ { url: url ?? null },
164
+ );
165
+ }
166
+
167
+ // Orchestrator called from the capture flow: resolve the expectation (from the
168
+ // project's stack.json unless the caller injects a `stack`), short-circuit when
169
+ // no client runtime is expected, collect the in-page state, and interpret it.
170
+ // Never throws — a probe failure must not break an otherwise-good capture.
171
+ async function probeInteractivity(frame, { url, stack } = {}) {
172
+ const descriptor = stack !== undefined ? stack : readStackJson();
173
+ const { expectInteractive, framework } =
174
+ resolveInteractivityExpectation(descriptor);
175
+ if (!expectInteractive) return null;
176
+ let state;
177
+ try {
178
+ state = await collectHydrationState(frame, { framework });
179
+ } catch (_) {
180
+ return null;
181
+ }
182
+ return interpretHydration({
183
+ expectInteractive: true,
184
+ controlCount: state.controlCount,
185
+ frameworkAttached: state.frameworkAttached,
186
+ framework,
187
+ url,
188
+ });
189
+ }
190
+
191
+ module.exports = {
192
+ KNOWN_FRAMEWORKS,
193
+ readStackJson,
194
+ inferFramework,
195
+ resolveInteractivityExpectation,
196
+ collectHydrationState,
197
+ interpretHydration,
198
+ probeInteractivity,
199
+ };
@@ -0,0 +1,35 @@
1
+ function createIssue(kind, message, extra = {}) {
2
+ const issue = {
3
+ kind,
4
+ message,
5
+ url: extra.url ?? null,
6
+ status: extra.status ?? null,
7
+ };
8
+ if (extra.matchedPattern != null) issue.matchedPattern = extra.matchedPattern;
9
+ if (extra.contextSnippet != null) issue.contextSnippet = extra.contextSnippet;
10
+ return issue;
11
+ }
12
+
13
+ function pushIssue(issues, issue) {
14
+ const key = JSON.stringify(issue);
15
+ if (!issues.some((existing) => JSON.stringify(existing) === key)) {
16
+ issues.push(issue);
17
+ }
18
+ }
19
+
20
+ function buildResult({ loaded, hasContent, issues, outputPath, url }) {
21
+ return {
22
+ ok: loaded && hasContent && issues.length === 0,
23
+ loaded,
24
+ hasContent,
25
+ url,
26
+ outputPath: outputPath ?? null,
27
+ issues,
28
+ };
29
+ }
30
+
31
+ module.exports = {
32
+ createIssue,
33
+ pushIssue,
34
+ buildResult,
35
+ };
@@ -0,0 +1,108 @@
1
+ const LOADING_MARKERS = [
2
+ "Loading scenario...",
3
+ "Loading tests...",
4
+ "Loading scenarios...",
5
+ "disconnected",
6
+ ];
7
+
8
+ // `extraMarkers` are project-supplied (from stack.json `capture.loadingMarkers`)
9
+ // because an app's own loading copy ("Loading…", "Please wait") is
10
+ // app-specific and must NOT be hardcoded into the shared harness — only the
11
+ // four codeyam-harness markers above are universal. Matching is
12
+ // case-insensitive so "Loading…" and "loading…" both count; without the
13
+ // project markers a stable app loading screen looks "ready" to
14
+ // waitForStablePage and gets captured mid-hydration.
15
+ function hasLoadingMarkers(text, extraMarkers = []) {
16
+ if (!text) return false;
17
+ const lower = text.toLowerCase();
18
+ const markers = LOADING_MARKERS.concat(
19
+ Array.isArray(extraMarkers) ? extraMarkers : [],
20
+ );
21
+ return markers.some(
22
+ (marker) => marker && lower.includes(String(marker).toLowerCase()),
23
+ );
24
+ }
25
+
26
+ function hasRenderableContent(state) {
27
+ if (!state) return false;
28
+ if (
29
+ state.rootChildCount > 0 ||
30
+ state.rootTextLength > 0 ||
31
+ state.bodyTextLength > 0
32
+ ) {
33
+ return true;
34
+ }
35
+ if ((state.loadedImageCount || 0) > 0) return true;
36
+ if ((state.mediaBboxCount || 0) > 0) return true;
37
+ return false;
38
+ }
39
+
40
+ function describeBlankReason(state) {
41
+ if (!state) return "no content state collected";
42
+ const parts = [];
43
+ if (!(state.bodyTextLength > 0)) {
44
+ parts.push("no text");
45
+ }
46
+ const imageCount = state.imageCount || 0;
47
+ const loadedImageCount = state.loadedImageCount || 0;
48
+ if (imageCount > 0 && loadedImageCount === 0) {
49
+ parts.push(`${imageCount} unloaded image${imageCount === 1 ? "" : "s"}`);
50
+ } else if (imageCount === 0) {
51
+ parts.push("no images");
52
+ }
53
+ if (!((state.mediaBboxCount || 0) > 0)) {
54
+ parts.push("no svg/canvas/video");
55
+ }
56
+ return parts.join(", ");
57
+ }
58
+
59
+ function shouldStopWaitingForImages(images, options = {}) {
60
+ const { elapsedMs = 0, overallTimeoutMs = 5000 } = options;
61
+ if (!Array.isArray(images) || images.length === 0) return true;
62
+ if (elapsedMs >= overallTimeoutMs) return true;
63
+ return images.every((img) => img && img.complete === true);
64
+ }
65
+
66
+ const ERROR_PATTERNS = [
67
+ "not found in registry",
68
+ "Component not found",
69
+ "Scenario Error",
70
+ ];
71
+
72
+ function hasErrorPatterns(text) {
73
+ return ERROR_PATTERNS.some((pattern) => text.includes(pattern));
74
+ }
75
+
76
+ function findErrorPattern(text) {
77
+ if (!text) return null;
78
+ for (const pattern of ERROR_PATTERNS) {
79
+ if (text.includes(pattern)) return pattern;
80
+ }
81
+ return null;
82
+ }
83
+
84
+ const ERROR_CONTEXT_RADIUS = 60;
85
+
86
+ function buildErrorContextSnippet(text, pattern) {
87
+ if (!text || !pattern) return null;
88
+ const index = text.indexOf(pattern);
89
+ if (index < 0) return null;
90
+ const start = Math.max(0, index - ERROR_CONTEXT_RADIUS);
91
+ const end = Math.min(text.length, index + pattern.length + ERROR_CONTEXT_RADIUS);
92
+ const slice = text.slice(start, end).replace(/\s+/g, " ").trim();
93
+ const prefix = start > 0 ? "…" : "";
94
+ const suffix = end < text.length ? "…" : "";
95
+ return `${prefix}${slice}${suffix}`;
96
+ }
97
+
98
+ module.exports = {
99
+ hasLoadingMarkers,
100
+ hasRenderableContent,
101
+ describeBlankReason,
102
+ shouldStopWaitingForImages,
103
+ hasErrorPatterns,
104
+ findErrorPattern,
105
+ buildErrorContextSnippet,
106
+ ERROR_PATTERNS,
107
+ ERROR_CONTEXT_RADIUS,
108
+ };
@@ -0,0 +1,107 @@
1
+ function normalizeMockCandidates(url) {
2
+ try {
3
+ const parsed = new URL(url);
4
+ return [url, `${parsed.pathname}${parsed.search}`, parsed.pathname];
5
+ } catch {
6
+ return [url];
7
+ }
8
+ }
9
+
10
+ function findHttpMock(httpMocks, request) {
11
+ const method = request.method().toUpperCase();
12
+ const candidates = normalizeMockCandidates(request.url());
13
+ for (const candidate of candidates) {
14
+ const key = `${method} ${candidate}`;
15
+ if (httpMocks[key]) {
16
+ return httpMocks[key];
17
+ }
18
+ }
19
+ return null;
20
+ }
21
+
22
+ // The set of path/URL targets declared by the mock keys. A key is
23
+ // `"<METHOD> <target>"` (e.g. `"GET /api/plans"`); we strip the method so the
24
+ // route matcher can decide whether a request *could* match any mock without
25
+ // knowing the method (the handler re-checks method via findHttpMock).
26
+ function mockedTargets(httpMocks) {
27
+ const targets = new Set();
28
+ for (const key of Object.keys(httpMocks)) {
29
+ const spaceIdx = key.indexOf(" ");
30
+ if (spaceIdx === -1) continue;
31
+ targets.add(key.slice(spaceIdx + 1));
32
+ }
33
+ return targets;
34
+ }
35
+
36
+ // True when a request URL matches one of the declared mock targets. Accepts
37
+ // either a string or a WHATWG URL (Playwright's URL-matcher passes a URL).
38
+ function requestTargetsMock(targets, url) {
39
+ const href = typeof url === "string" ? url : url.href;
40
+ return normalizeMockCandidates(href).some((candidate) =>
41
+ targets.has(candidate),
42
+ );
43
+ }
44
+
45
+ async function attachHttpMocks(page, httpMocks) {
46
+ if (!httpMocks || Object.keys(httpMocks).length === 0) return;
47
+
48
+ // Intercept ONLY requests whose path matches a declared mock — not every
49
+ // request. A blanket `page.route("**/*")` intercepts the dev server's ESM
50
+ // module/script/style requests too, and routing them through
51
+ // `route.continue()` breaks Vite dev-mode module loading: the lazy app
52
+ // chunks never resolve and the SPA renders blank. Scoping the matcher to
53
+ // mocked targets lets those requests load natively while still mocking the API.
54
+ const targets = mockedTargets(httpMocks);
55
+ await page.route(
56
+ (url) => requestTargetsMock(targets, url),
57
+ async (route) => {
58
+ const mock = findHttpMock(httpMocks, route.request());
59
+ if (!mock) {
60
+ await route.continue();
61
+ return;
62
+ }
63
+
64
+ const headers = { ...(mock.headers || {}) };
65
+ let body;
66
+ if (mock.body !== undefined) {
67
+ body =
68
+ typeof mock.body === "string"
69
+ ? mock.body
70
+ : JSON.stringify(mock.body);
71
+ const hasContentType = Object.keys(headers).some(
72
+ (key) => key.toLowerCase() === "content-type",
73
+ );
74
+ if (!hasContentType) {
75
+ headers["content-type"] = "application/json";
76
+ }
77
+ }
78
+
79
+ await route.fulfill({
80
+ status: mock.status || 200,
81
+ headers,
82
+ body,
83
+ });
84
+ },
85
+ );
86
+
87
+ // Disable the in-page fetch mock by returning an empty active-mocks.json.
88
+ // The HTML injects a script that synchronously loads this file and
89
+ // monkey-patches window.fetch, which would bypass Playwright's route
90
+ // interception. This route is registered AFTER the mock matcher so it takes
91
+ // priority for that path (Playwright uses LIFO ordering for route handlers).
92
+ await page.route("**/active-mocks.json", async (route) => {
93
+ await route.fulfill({
94
+ status: 200,
95
+ headers: { "content-type": "application/json" },
96
+ body: "[]",
97
+ });
98
+ });
99
+ }
100
+
101
+ module.exports = {
102
+ normalizeMockCandidates,
103
+ findHttpMock,
104
+ mockedTargets,
105
+ requestTargetsMock,
106
+ attachHttpMocks,
107
+ };