@codeyam-editor/codeyam-editor 0.1.3 → 0.1.5
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/npm/cloud-branch-switch.js +1 -1
- package/npm/cloud.js +1693 -96
- package/npm/container.js +14 -0
- package/npm/disk-pressure.js +167 -0
- package/npm/editor-dev.js +107 -15
- package/npm/editor.js +97 -37
- package/npm/fleet-auth-health.js +75 -0
- package/npm/fleet-auth.js +6 -1
- package/npm/fleet-build-data.js +130 -0
- package/npm/fleet-cleanup.js +145 -7
- package/npm/fleet-destroy-safety.js +55 -0
- package/npm/fleet-effect-status.js +33 -32
- package/npm/fleet-host.js +74 -0
- package/npm/fleet-job-badge.js +46 -2
- package/npm/fleet-launchd.js +171 -0
- package/npm/fleet-live-status.js +123 -0
- package/npm/fleet-plans.js +231 -0
- package/npm/fleet-prewarm.js +165 -0
- package/npm/fleet-public-url.js +160 -0
- package/npm/fleet-queue.js +38 -1
- package/npm/fleet-reconcile.js +100 -1
- package/npm/fleet-reset.js +298 -13
- package/npm/fleet-roster.js +6 -8
- package/npm/fleet-self-update.js +142 -0
- package/npm/fleet-setup-steps.js +105 -0
- package/npm/fleet-sleep-status.js +33 -0
- package/npm/fleet-strand.js +150 -0
- package/npm/fleet-stuck-status.js +111 -0
- package/npm/fleet-vm-access.js +234 -0
- package/npm/fleet-vm-readiness.js +104 -0
- package/npm/fleet-vm-slugs.js +116 -0
- package/npm/google-ingress.js +211 -0
- package/npm/ingress-invoke.js +292 -0
- package/npm/launch-config.js +310 -4
- package/npm/scenario-check.js +684 -33
- package/npm/scenario-handlers.js +94 -12
- package/npm/scenario-metrics.js +116 -1
- package/npm/scenario-mocks.js +20 -0
- package/npm/scenario-playwright.js +366 -29
- package/npm/script-drift.js +81 -0
- package/npm/utils.js +453 -43
- package/npm/vm-urls.js +69 -8
- package/package.json +5 -5
- package/ui/dist/assets/ContainerInspector-D7VZkkaj.js +2 -0
- package/ui/dist/assets/EditorShell-DHhIaotd.css +1 -0
- package/ui/dist/assets/EditorShell-r-796N_W.js +191 -0
- package/ui/dist/assets/{Markdown-BHBqz-Mj.js → Markdown-BoLHHFXH.js} +1 -1
- package/ui/dist/assets/ProjectLauncherScreen-Ck-XWkQN.js +1 -0
- package/ui/dist/assets/ScenarioDataPanel-B9iDSUpA.js +5 -0
- package/ui/dist/assets/ScenarioDataPanelFullPage-CBLTGC4-.js +1 -0
- package/ui/dist/assets/ScenarioRenderer-BhLfT-nX.js +21 -0
- package/ui/dist/assets/StepsSimulator-BFldWyoX.js +1 -0
- package/ui/dist/assets/index-CIXZkwfB.css +1 -0
- package/ui/dist/assets/{index-KDFAgt2e.js → index-QsOiOG9x.js} +9 -9
- package/ui/dist/assets/useEvents-DHkLJZR4.js +1 -0
- package/ui/dist/codeyam-mark.svg +4 -0
- package/ui/dist/codeyam-wordmark.svg +5 -0
- package/ui/dist/index.html +2 -2
- package/ui/dist/assets/ContainerInspector-C8EkH4PP.js +0 -2
- package/ui/dist/assets/EditorShell-CGsBSA6u.js +0 -186
- package/ui/dist/assets/ScenarioDataPanel-nuLXJzLf.js +0 -5
- package/ui/dist/assets/ScenarioDataPanelFullPage-Bnghtj8a.js +0 -1
- package/ui/dist/assets/ScenarioRenderer-B_Za9jL8.js +0 -1
- package/ui/dist/assets/StepsSimulator-CRtmlyNZ.js +0 -1
- package/ui/dist/assets/index-Bih6HJ_u.css +0 -1
- package/ui/dist/assets/useEvents-DggWrtHe.js +0 -1
package/npm/scenario-handlers.js
CHANGED
|
@@ -1,15 +1,59 @@
|
|
|
1
1
|
const { createIssue } = require("./scenario-issues");
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
3
|
+
// Known substrings that mean the app refused to initialize because it was loaded
|
|
4
|
+
// from an *insecure context* — a bare dotted-quad IP rather than HTTPS or the
|
|
5
|
+
// hostname `localhost`. This is a whole CLASS of failure (Sveltia CMS's
|
|
6
|
+
// "only works with HTTPS or localhost", anything gating on `isSecureContext`,
|
|
7
|
+
// `crypto.subtle`, service workers, `Secure` cookies, WebAuthn), not one app.
|
|
8
|
+
// Matching reclassifies the error as an actionable `insecure-host` advisory
|
|
9
|
+
// instead of a generic console/page failure that surfaces as a bare
|
|
10
|
+
// `screenshot=null` with no explanation. Substrings are matched
|
|
11
|
+
// case-insensitively; keep the set small and well-known.
|
|
12
|
+
const INSECURE_CONTEXT_SIGNATURES = [
|
|
13
|
+
"only works with https or localhost",
|
|
14
|
+
"issecurecontext",
|
|
15
|
+
"secure context",
|
|
16
|
+
"requires https",
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
// The single actionable message the `insecure-host` advisory carries. Names the
|
|
20
|
+
// class of failure and the fix (the preview origin is `localhost` by default;
|
|
21
|
+
// stubborn apps can opt into HTTPS) rather than echoing one app's raw error.
|
|
22
|
+
const INSECURE_HOST_ADVISORY_MESSAGE =
|
|
23
|
+
"The app refused to run because it was loaded from an insecure context " +
|
|
24
|
+
"(a bare IP). It requires a secure context — HTTPS or the hostname " +
|
|
25
|
+
"`localhost`. The preview origin is `localhost` by default; if this persists, " +
|
|
26
|
+
"the app may require HTTPS even on localhost — enable `proxy.httpsPreview` in " +
|
|
27
|
+
".codeyam/editor.json.";
|
|
28
|
+
|
|
29
|
+
// Reclassify an error string as an `insecure-host` advisory when it matches a
|
|
30
|
+
// known insecure-context signature, else null. Pure; the original text is kept
|
|
31
|
+
// as the `contextSnippet` so the operator can still see what the app logged.
|
|
32
|
+
function insecureContextAdvisory(text) {
|
|
33
|
+
if (typeof text !== "string" || text.length === 0) return null;
|
|
34
|
+
const lower = text.toLowerCase();
|
|
35
|
+
const matched = INSECURE_CONTEXT_SIGNATURES.some((sig) => lower.includes(sig));
|
|
36
|
+
if (!matched) return null;
|
|
37
|
+
return createIssue("insecure-host", INSECURE_HOST_ADVISORY_MESSAGE, {
|
|
38
|
+
contextSnippet: text,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
12
41
|
|
|
42
|
+
// `allowWebSocket` keeps the real `WebSocket` for scenarios that script a
|
|
43
|
+
// `/ws/terminal` transcript (or a WS stream): those captures NEED the socket to
|
|
44
|
+
// connect so the server replays the scripted agent state into the frame. The
|
|
45
|
+
// stub below exists to silence the live terminal's reconnect loop on EVERY
|
|
46
|
+
// OTHER capture (a component that opens `/ws/terminal` with no scripted
|
|
47
|
+
// playback would otherwise screenshot the "Reconnecting…" overlay); for a
|
|
48
|
+
// scripted scenario the server holds the socket open after replay, so there is
|
|
49
|
+
// no reconnect spam to silence and stubbing only hides the very state we are
|
|
50
|
+
// trying to capture. The flag is computed by the capture orchestrator from the
|
|
51
|
+
// scenario's `mocks.transcripts` / `mocks.streams` — see
|
|
52
|
+
// `scenarioScriptsLiveSocket` in scenario-check.js.
|
|
53
|
+
function getInitScript(allowWebSocket = false) {
|
|
54
|
+
const webSocketStub = allowWebSocket
|
|
55
|
+
? ""
|
|
56
|
+
: `
|
|
13
57
|
// Stub WebSocket during capture to prevent terminal reconnection spam.
|
|
14
58
|
window.WebSocket = class StubWebSocket {
|
|
15
59
|
static CONNECTING = 0;
|
|
@@ -32,14 +76,28 @@ function getInitScript() {
|
|
|
32
76
|
if (this.onclose) this.onclose(new CloseEvent("close"));
|
|
33
77
|
}, 0);
|
|
34
78
|
}
|
|
35
|
-
}
|
|
79
|
+
};`;
|
|
80
|
+
return `
|
|
81
|
+
window.__codeyamUnhandledRejections = [];
|
|
82
|
+
window.addEventListener("unhandledrejection", (event) => {
|
|
83
|
+
const reason = event.reason;
|
|
84
|
+
const message =
|
|
85
|
+
reason instanceof Error ? reason.message : String(reason);
|
|
86
|
+
window.__codeyamUnhandledRejections.push(message);
|
|
87
|
+
});
|
|
88
|
+
${webSocketStub}
|
|
36
89
|
`;
|
|
37
90
|
}
|
|
38
91
|
|
|
39
92
|
function handleConsoleMessage(message) {
|
|
40
93
|
if (message.type() !== "error") return null;
|
|
41
94
|
const text = message.text();
|
|
42
|
-
|
|
95
|
+
|
|
96
|
+
// An insecure-context refusal wins over the generic `console` classification:
|
|
97
|
+
// it is the targeted, actionable signal for the secure-context app class.
|
|
98
|
+
const advisory = insecureContextAdvisory(text);
|
|
99
|
+
if (advisory) return advisory;
|
|
100
|
+
|
|
43
101
|
// Ignore known dev-server WebSocket/HMR errors from Vite proxy
|
|
44
102
|
if (
|
|
45
103
|
text.includes("WebSocket connection to") ||
|
|
@@ -48,11 +106,32 @@ function handleConsoleMessage(message) {
|
|
|
48
106
|
return null;
|
|
49
107
|
}
|
|
50
108
|
|
|
109
|
+
// Ignore the browser's blocked-script warning for sandboxed mockup-preview
|
|
110
|
+
// frames. Mockup previews render untrusted AI-generated HTML inside a
|
|
111
|
+
// `sandbox=""` iframe; the HTML-injection proxy injects an error-capture
|
|
112
|
+
// <script> tag, which the browser then refuses to run, emitting
|
|
113
|
+
// "Blocked script execution ... because the frame is sandboxed". That block
|
|
114
|
+
// is the capture's own injected script being denied — benign for capture
|
|
115
|
+
// purposes. Match narrowly on BOTH the block phrase and the "sandboxed"
|
|
116
|
+
// signature so a genuine non-sandbox CSP block ("Blocked script execution"
|
|
117
|
+
// without "sandboxed") still surfaces as a real issue.
|
|
118
|
+
if (
|
|
119
|
+
text.includes("Blocked script execution") &&
|
|
120
|
+
text.includes("sandboxed")
|
|
121
|
+
) {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
|
|
51
125
|
return createIssue("console", text);
|
|
52
126
|
}
|
|
53
127
|
|
|
54
128
|
function handlePageError(error) {
|
|
55
|
-
|
|
129
|
+
const text = error.message || String(error);
|
|
130
|
+
// A secure-context guard often throws at boot rather than logging — surface
|
|
131
|
+
// the same actionable advisory for the pageerror path.
|
|
132
|
+
const advisory = insecureContextAdvisory(text);
|
|
133
|
+
if (advisory) return advisory;
|
|
134
|
+
return createIssue("pageerror", text);
|
|
56
135
|
}
|
|
57
136
|
|
|
58
137
|
function handleRequestFailed(request) {
|
|
@@ -79,4 +158,7 @@ module.exports = {
|
|
|
79
158
|
handleConsoleMessage,
|
|
80
159
|
handlePageError,
|
|
81
160
|
handleRequestFailed,
|
|
161
|
+
insecureContextAdvisory,
|
|
162
|
+
INSECURE_CONTEXT_SIGNATURES,
|
|
163
|
+
INSECURE_HOST_ADVISORY_MESSAGE,
|
|
82
164
|
};
|
package/npm/scenario-metrics.js
CHANGED
|
@@ -25,6 +25,34 @@ function hasLoadingMarkers(text, extraMarkers = []) {
|
|
|
25
25
|
|
|
26
26
|
function hasRenderableContent(state) {
|
|
27
27
|
if (!state) return false;
|
|
28
|
+
|
|
29
|
+
// Reveal-suppression guard. A scroll-gated entrance animation starts its
|
|
30
|
+
// content at `opacity:0` and reveals it via an IntersectionObserver wired
|
|
31
|
+
// from the app shell. Captured in isolation (no shell), the observer never
|
|
32
|
+
// fires, so the text renders into the DOM — `bodyTextLength`/`rootTextLength`
|
|
33
|
+
// are non-zero — yet stays visually invisible and the screenshot is blank.
|
|
34
|
+
// `visibleTextLength` is the length of text actually PAINTED to the frame
|
|
35
|
+
// (computed-style opacity/visibility aware). When the DOM carries text but
|
|
36
|
+
// none of it is visible AND there is no visible media, the frame is
|
|
37
|
+
// near-blank: report no content so the blank gate rejects it instead of
|
|
38
|
+
// waving through an empty shell. `visibleTextLength` is optional — older
|
|
39
|
+
// capture configs (and stubbed test targets) omit it, in which case the
|
|
40
|
+
// original DOM-presence behavior below is preserved byte-for-byte.
|
|
41
|
+
const domTextLength = Math.max(
|
|
42
|
+
state.bodyTextLength || 0,
|
|
43
|
+
state.rootTextLength || 0,
|
|
44
|
+
);
|
|
45
|
+
const hasVisibleMedia =
|
|
46
|
+
(state.loadedImageCount || 0) > 0 || (state.mediaBboxCount || 0) > 0;
|
|
47
|
+
if (
|
|
48
|
+
typeof state.visibleTextLength === "number" &&
|
|
49
|
+
domTextLength > 0 &&
|
|
50
|
+
state.visibleTextLength === 0 &&
|
|
51
|
+
!hasVisibleMedia
|
|
52
|
+
) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
|
|
28
56
|
if (
|
|
29
57
|
state.rootChildCount > 0 ||
|
|
30
58
|
state.rootTextLength > 0 ||
|
|
@@ -40,7 +68,17 @@ function hasRenderableContent(state) {
|
|
|
40
68
|
function describeBlankReason(state) {
|
|
41
69
|
if (!state) return "no content state collected";
|
|
42
70
|
const parts = [];
|
|
43
|
-
|
|
71
|
+
// Distinguish "no text in the DOM at all" from "text rendered but nothing is
|
|
72
|
+
// visible" — the latter is the reveal-suppression symptom (opacity:0
|
|
73
|
+
// entrance content whose observer never fired in isolation), which points
|
|
74
|
+
// the author at a different fix than a genuinely empty page.
|
|
75
|
+
if (
|
|
76
|
+
typeof state.visibleTextLength === "number" &&
|
|
77
|
+
state.bodyTextLength > 0 &&
|
|
78
|
+
state.visibleTextLength === 0
|
|
79
|
+
) {
|
|
80
|
+
parts.push("text rendered but not visible (reveal-suppressed?)");
|
|
81
|
+
} else if (!(state.bodyTextLength > 0)) {
|
|
44
82
|
parts.push("no text");
|
|
45
83
|
}
|
|
46
84
|
const imageCount = state.imageCount || 0;
|
|
@@ -69,6 +107,16 @@ const ERROR_PATTERNS = [
|
|
|
69
107
|
"Scenario Error",
|
|
70
108
|
];
|
|
71
109
|
|
|
110
|
+
// The DOM attribute codeyam's own ScenarioRenderer stamps on its error and
|
|
111
|
+
// seed-error fallback frames. Error detection anchors on this marker rather
|
|
112
|
+
// than scanning the whole page body for ERROR_PATTERNS: a legitimately-mocked
|
|
113
|
+
// scenario whose *content* quotes one of those harness phrases (a journal entry
|
|
114
|
+
// describing the error component, say) must not be flagged as a failed capture.
|
|
115
|
+
// Arbitrary client apps that never render a codeyam ScenarioRenderer never emit
|
|
116
|
+
// the marker, so their pages are treated as healthy regardless of body text —
|
|
117
|
+
// these patterns were always codeyam-harness-specific, never generic.
|
|
118
|
+
const SCENARIO_ERROR_MARKER = "data-codeyam-scenario-error";
|
|
119
|
+
|
|
72
120
|
function hasErrorPatterns(text) {
|
|
73
121
|
return ERROR_PATTERNS.some((pattern) => text.includes(pattern));
|
|
74
122
|
}
|
|
@@ -95,14 +143,81 @@ function buildErrorContextSnippet(text, pattern) {
|
|
|
95
143
|
return `${prefix}${slice}${suffix}`;
|
|
96
144
|
}
|
|
97
145
|
|
|
146
|
+
// Classify a scenario-error fallback into a `{ matchedPattern, contextSnippet }`
|
|
147
|
+
// from the marker info collected off the page. `marker` is the
|
|
148
|
+
// `{ reason, text }` read from the `[data-codeyam-scenario-error]` element, or
|
|
149
|
+
// null when no such element is present. Returns null when there is no marked
|
|
150
|
+
// fallback — the page is healthy and no error-content failure is raised.
|
|
151
|
+
//
|
|
152
|
+
// When the marked fallback text contains a known ERROR_PATTERN (the render-error
|
|
153
|
+
// path renders `Component "X" not found in registry`), that pattern is named so
|
|
154
|
+
// the downstream Rust classifier can still extract the component; otherwise the
|
|
155
|
+
// marker's `reason` attribute classifies it (the seed-error fallback, whose
|
|
156
|
+
// "Seed Error" copy is intentionally not an ERROR_PATTERN). Because the text is
|
|
157
|
+
// scoped to the fallback element — not the whole body — content elsewhere on the
|
|
158
|
+
// page can quote these phrases freely without tripping detection.
|
|
159
|
+
function findScenarioError(marker) {
|
|
160
|
+
// The capture passes the `frame.evaluate` result of querying the marker
|
|
161
|
+
// element: either null (no fallback rendered) or a `{reason, text}` object.
|
|
162
|
+
// Anything else (a bare string, a primitive) is not a marked error — guard
|
|
163
|
+
// so a non-object never reads as a present marker and false-flags the page.
|
|
164
|
+
if (!marker || typeof marker !== "object") return null;
|
|
165
|
+
const text = marker.text || "";
|
|
166
|
+
const reason = marker.reason || "";
|
|
167
|
+
const matchedPattern = findErrorPattern(text) || reason || "scenario error";
|
|
168
|
+
const contextSnippet =
|
|
169
|
+
buildErrorContextSnippet(text, matchedPattern) ||
|
|
170
|
+
(text ? text.replace(/\s+/g, " ").trim().slice(0, 200) : null);
|
|
171
|
+
return { matchedPattern, contextSnippet };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Build the non-blocking "this page looks client-fetched" advisory from the two
|
|
175
|
+
// settle signals the capture already computes: `stableOutcome` from
|
|
176
|
+
// waitForStablePage (did the page settle, and was a loading marker still up at
|
|
177
|
+
// the cap?) and `networkOutcome` from waitForNetworkQuiet (did in-flight network
|
|
178
|
+
// go quiet, or did the bounded wait cap out?). Captures settle within a bounded
|
|
179
|
+
// window so a never-idle stream can't hang them — which means a slower
|
|
180
|
+
// client-side fetch lands AFTER the window and the screenshot catches the
|
|
181
|
+
// loading/initial state instead of the populated one. When either signal fires,
|
|
182
|
+
// name the cause and the two reliable fixes (server-render the data, or drive a
|
|
183
|
+
// props-driven isolated component scenario) so the agent reaches for those up
|
|
184
|
+
// front instead of rediscovering the constraint by trial and error. Returns the
|
|
185
|
+
// advisory string, or null when the page settled cleanly (the SSR / props-driven
|
|
186
|
+
// happy path — no advisory, so a healthy capture stays noise-free).
|
|
187
|
+
function buildSettleAdvisory(stableOutcome, networkOutcome) {
|
|
188
|
+
const causes = [];
|
|
189
|
+
if (
|
|
190
|
+
stableOutcome &&
|
|
191
|
+
stableOutcome.stabilized === false &&
|
|
192
|
+
stableOutcome.hadLoadingMarkers === true
|
|
193
|
+
) {
|
|
194
|
+
causes.push("a loading marker was still on the page");
|
|
195
|
+
}
|
|
196
|
+
if (networkOutcome && networkOutcome.quiet === false) {
|
|
197
|
+
causes.push("network requests were still in flight");
|
|
198
|
+
}
|
|
199
|
+
if (causes.length === 0) return null;
|
|
200
|
+
return (
|
|
201
|
+
`When this page was captured, ${causes.join(" and ")} — it likely fetches ` +
|
|
202
|
+
`its data on the client. Captures settle within a bounded window, so data ` +
|
|
203
|
+
`that arrives via a client-side fetch after that window renders as the ` +
|
|
204
|
+
`loading/initial state, not the populated state. To capture a populated ` +
|
|
205
|
+
`state, server-render the data (SSR/RSC) or drive a props-driven isolated ` +
|
|
206
|
+
`component scenario.`
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
98
210
|
module.exports = {
|
|
99
211
|
hasLoadingMarkers,
|
|
100
212
|
hasRenderableContent,
|
|
213
|
+
buildSettleAdvisory,
|
|
101
214
|
describeBlankReason,
|
|
102
215
|
shouldStopWaitingForImages,
|
|
103
216
|
hasErrorPatterns,
|
|
104
217
|
findErrorPattern,
|
|
218
|
+
findScenarioError,
|
|
105
219
|
buildErrorContextSnippet,
|
|
106
220
|
ERROR_PATTERNS,
|
|
221
|
+
SCENARIO_ERROR_MARKER,
|
|
107
222
|
ERROR_CONTEXT_RADIUS,
|
|
108
223
|
};
|
package/npm/scenario-mocks.js
CHANGED
|
@@ -98,10 +98,30 @@ async function attachHttpMocks(page, httpMocks) {
|
|
|
98
98
|
});
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
|
|
102
|
+
// True when a console "Failed to load resource" error at `url` corresponds to
|
|
103
|
+
// a mock this scenario DECLARED with an error status (>= 400). An intentional
|
|
104
|
+
// error-state scenario (e.g. a History tab mocking `GET /api/history` -> 500)
|
|
105
|
+
// must not fail its own capture on the console noise its mock deliberately
|
|
106
|
+
// produces. Console errors carry no HTTP method, so any method's mock on a
|
|
107
|
+
// matching target counts.
|
|
108
|
+
function isDeclaredErrorMock(httpMocks, url) {
|
|
109
|
+
const candidates = normalizeMockCandidates(url);
|
|
110
|
+
for (const [key, mock] of Object.entries(httpMocks || {})) {
|
|
111
|
+
const spaceIdx = key.indexOf(" ");
|
|
112
|
+
if (spaceIdx === -1) continue;
|
|
113
|
+
const target = key.slice(spaceIdx + 1);
|
|
114
|
+
if (!candidates.includes(target)) continue;
|
|
115
|
+
if ((mock.status || 200) >= 400) return true;
|
|
116
|
+
}
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
|
|
101
120
|
module.exports = {
|
|
102
121
|
normalizeMockCandidates,
|
|
103
122
|
findHttpMock,
|
|
104
123
|
mockedTargets,
|
|
105
124
|
requestTargetsMock,
|
|
106
125
|
attachHttpMocks,
|
|
126
|
+
isDeclaredErrorMock,
|
|
107
127
|
};
|