gipity 1.0.428 → 1.0.429
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/dist/commands/deploy.js +19 -1
- package/dist/commands/generate.js +61 -4
- package/dist/commands/page-eval.js +387 -50
- package/dist/commands/page-inspect.js +8 -2
- package/dist/commands/page-screenshot.js +209 -43
- package/dist/hooks/capture-runner.js +9 -1
- package/dist/index.js +1057 -488
- package/dist/page-fixtures.js +92 -3
- package/dist/provider-docs.js +10 -10
- package/dist/relay/daemon.js +16 -2
- package/dist/relay/device-http.js +22 -0
- package/dist/relay/media-upload.js +184 -0
- package/dist/setup.js +4 -1
- package/package.json +2 -2
|
@@ -5,7 +5,7 @@ import { brand, bold, muted, warning, success } from '../colors.js';
|
|
|
5
5
|
import { run } from '../helpers/index.js';
|
|
6
6
|
import { getAuth } from '../auth.js';
|
|
7
7
|
import { resolveProjectContext } from '../config.js';
|
|
8
|
-
import { uploadPublicFixture, deleteFixture } from '../page-fixtures.js';
|
|
8
|
+
import { uploadPublicFixture, uploadCameraFeed, assertCameraFile, deleteFixture } from '../page-fixtures.js';
|
|
9
9
|
// Shown when an eval runs cleanly but returns nothing serializable. Turns a
|
|
10
10
|
// bare/opaque `null` into a deterministic, actionable nudge so the agent shapes
|
|
11
11
|
// a returnable value instead of guessing and retrying.
|
|
@@ -44,6 +44,52 @@ export function normalizeEvalResult(raw) {
|
|
|
44
44
|
}
|
|
45
45
|
return { result: raw, noValue: false };
|
|
46
46
|
}
|
|
47
|
+
// Shown when a run returns a structurally empty value ({}, [], or an object whose
|
|
48
|
+
// every field is null/''). That is NOT the same as "no value": the script ran and
|
|
49
|
+
// read the page — it just read it at an instant where the state it wanted did not
|
|
50
|
+
// exist. A fixed --wait samples ONE moment, and transient state (a round result,
|
|
51
|
+
// a toast, a frame's detection) may have already been cleared by then, so the run
|
|
52
|
+
// looks like a pass and the agent has to reason its way to "I sampled too late".
|
|
53
|
+
export const EVAL_EMPTY_STATE_HINT = 'The eval returned an empty value — the script ran, but the state it read was not there at that instant. ' +
|
|
54
|
+
'A fixed --wait samples ONE moment; transient state (a round result, a toast, a detection) can be gone by then. ' +
|
|
55
|
+
'Poll INSIDE the body and return the moment the condition holds (raise --timeout if the sequence needs longer), ' +
|
|
56
|
+
"or gate the read on the app's own signal with --wait-for '<selector>'.";
|
|
57
|
+
// The one-run experiment that separates "the frame is unreadable" from "the app
|
|
58
|
+
// never routes frames to the model" — the ambiguity that otherwise costs a
|
|
59
|
+
// re-generate-and-re-run cycle per candidate image. Named once, reused by every
|
|
60
|
+
// camera-run message below so the caller is told the same thing wherever it
|
|
61
|
+
// notices something is off.
|
|
62
|
+
const CAMERA_FRAME_CHECK = "in the eval body, run the model on the frame YOURSELF (grab the app's <video> element, or the app's own "
|
|
63
|
+
+ "detector on it) and return its RAW output alongside the app's state. Model saw nothing => the frame is at fault: "
|
|
64
|
+
+ 'regenerate it (subject filling the frame, plain background, even light), do not re-run the same one. '
|
|
65
|
+
+ "Model saw it but the app's state is empty => the bug is in the app's wiring, not the picture.";
|
|
66
|
+
// A camera run that comes back empty must NOT be sent down the generic
|
|
67
|
+
// "raise --timeout and poll harder" path below: on a looped still frame that is
|
|
68
|
+
// a guaranteed-identical re-run, and that escalation (60s, then 70s, then 80s)
|
|
69
|
+
// is the single most expensive way a vision app gets verified. Give it the
|
|
70
|
+
// verdict and the disambiguating experiment instead.
|
|
71
|
+
export const EVAL_CAMERA_EMPTY_HINT = 'The eval returned an empty value on a --camera run. --camera loops ONE still image, and the model is deterministic '
|
|
72
|
+
+ 'on it, so a longer --wait/--timeout re-runs the identical inference and returns the identical nothing — do not escalate. '
|
|
73
|
+
+ `Two suspects: the frame, or the app's wiring. Tell them apart in one run: ${CAMERA_FRAME_CHECK} `
|
|
74
|
+
+ "(If the app only starts its pipeline on a click, a headless run never clicks — start it on load and gate this eval on "
|
|
75
|
+
+ "the app's own ready signal with --wait-for '<selector>'.)";
|
|
76
|
+
/** True when the eval came back with a structurally empty container: `{}`, `[]`,
|
|
77
|
+
* or an object whose every value is null/undefined/''. Exported for tests. */
|
|
78
|
+
export function isEmptyStateResult(result) {
|
|
79
|
+
let parsed;
|
|
80
|
+
try {
|
|
81
|
+
parsed = JSON.parse(result);
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
if (Array.isArray(parsed))
|
|
87
|
+
return parsed.length === 0;
|
|
88
|
+
if (!parsed || typeof parsed !== 'object')
|
|
89
|
+
return false;
|
|
90
|
+
const values = Object.values(parsed);
|
|
91
|
+
return values.every((v) => v === null || v === undefined || v === '');
|
|
92
|
+
}
|
|
47
93
|
// A one-line inline expr is worth echoing back — it's the thing you're asserting
|
|
48
94
|
// on. A multi-line driver script is not: echoing 30 lines of the caller's own
|
|
49
95
|
// source above the result buries the value, and an echo that lands right before
|
|
@@ -82,14 +128,158 @@ export function capWaitMs(rawWait, url) {
|
|
|
82
128
|
`To watch an app that keeps changing past 30s, cover the span with staggered windows in one command: gipity page test "${url}" --clients N --stagger S.`));
|
|
83
129
|
return MAX_WAIT_MS;
|
|
84
130
|
}
|
|
131
|
+
// How many --step expressions may ride on one page load (mirrors the server's
|
|
132
|
+
// MAX_EVAL_STEPS). Past this, the run is really two verifications.
|
|
133
|
+
export const MAX_EVAL_STEPS = 4;
|
|
134
|
+
// Ceiling the server accepts for the --wait-for gate (its WAIT_FOR_MAX_MS).
|
|
135
|
+
export const WAIT_FOR_MAX_MS = 30_000;
|
|
136
|
+
/** Parse --wait-timeout, clamping to the gate's server-side ceiling. Over the cap
|
|
137
|
+
* the server answers with a zod 400 that names neither the flag nor the limit, so
|
|
138
|
+
* clamp and say so on stderr (--json stdout stays clean). Exported for tests. */
|
|
139
|
+
export function capWaitForTimeoutMs(raw) {
|
|
140
|
+
const parsed = parseInt(raw, 10);
|
|
141
|
+
const ms = Number.isFinite(parsed) && parsed >= 0 ? parsed : 5_000;
|
|
142
|
+
if (ms <= WAIT_FOR_MAX_MS)
|
|
143
|
+
return ms;
|
|
144
|
+
console.error(warning(`--wait-timeout ${ms}ms exceeds the ${WAIT_FOR_MAX_MS}ms cap on the selector gate — using ${WAIT_FOR_MAX_MS}ms. ` +
|
|
145
|
+
`A state that takes longer than that to appear is not a settling delay: have the page reach it on load, ` +
|
|
146
|
+
`or watch for it INSIDE the script (--timeout ${EVAL_SCRIPT_BUDGET_MAX_MS}).`));
|
|
147
|
+
return WAIT_FOR_MAX_MS;
|
|
148
|
+
}
|
|
149
|
+
/** True when `s` is an expression (`document.title`, an IIFE) rather than a
|
|
150
|
+
* statement body (`const x = …; return x`). Mirrors the server's own parse
|
|
151
|
+
* choice: only an expression may be spliced into `return (…)`. Nothing is
|
|
152
|
+
* executed — Function() parses and throws on a statement body. */
|
|
153
|
+
export function parsesAsExpression(s) {
|
|
154
|
+
try {
|
|
155
|
+
// eslint-disable-next-line no-new-func
|
|
156
|
+
new Function(`return (${s}\n);`);
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/** Prepend a prelude (fixture bindings, a post-gate settle) to a caller script.
|
|
164
|
+
* A prelude makes the whole thing a statement body, so an expression script has
|
|
165
|
+
* to become `return (expr)` — a statement-body script is spliced as-is. */
|
|
166
|
+
export function withPrelude(script, prelude) {
|
|
167
|
+
if (!prelude)
|
|
168
|
+
return script;
|
|
169
|
+
return parsesAsExpression(script)
|
|
170
|
+
? `${prelude}\nreturn (${script});`
|
|
171
|
+
: `${prelude}\n${script}`;
|
|
172
|
+
}
|
|
173
|
+
// A camera app's first useful frame is never 500ms away: getUserMedia has to
|
|
174
|
+
// come up, then the vision pipeline downloads and initializes its model (WASM +
|
|
175
|
+
// weights) before it can label anything. Evaluating at the default --wait always
|
|
176
|
+
// reads an empty/"loading" DOM, and pushing that warm-up into the eval body
|
|
177
|
+
// instead blows the in-page execution budget. So --camera raises the pre-eval
|
|
178
|
+
// window (which is NOT on the eval's clock) to something a model load fits in.
|
|
179
|
+
export const CAMERA_DEFAULT_WAIT_MS = 15_000;
|
|
180
|
+
// Playing a real frame into the browser's webcam costs the server real time
|
|
181
|
+
// before the page is even navigated: it fetches the hosted feed and encodes it
|
|
182
|
+
// into the synthetic capture device. That is server-side setup the caller cannot
|
|
183
|
+
// shorten, so it has to be inside the client's patience — otherwise the CLI
|
|
184
|
+
// abandons a job that was always going to take this long.
|
|
185
|
+
const CAMERA_SETUP_BUDGET_MS = 30_000;
|
|
186
|
+
// How long the script may run INSIDE the page. The server enforces this (it is
|
|
187
|
+
// the `timeoutMs` on the eval request) and picks the roomier default on a
|
|
188
|
+
// synthetic-media run, where a WASM/model download IS the wait. These mirror the
|
|
189
|
+
// server's EVAL_SCRIPT_BUDGET_* constants; --timeout names any value up to MAX.
|
|
190
|
+
export const EVAL_SCRIPT_BUDGET_MS = 30_000;
|
|
191
|
+
export const EVAL_SCRIPT_BUDGET_CAMERA_MS = 45_000;
|
|
192
|
+
export const EVAL_SCRIPT_BUDGET_MAX_MS = 90_000;
|
|
193
|
+
/** The in-page budget this call gets: the caller's --timeout when given, else the
|
|
194
|
+
* default for the kind of run (roomier under --camera/--fake-media). Clamped to
|
|
195
|
+
* the server's accepted range — a value over the max is clamped and explained on
|
|
196
|
+
* stderr rather than bounced back as an opaque 400. Exported for tests. */
|
|
197
|
+
export function capScriptBudgetMs(rawTimeout, hasMedia) {
|
|
198
|
+
const fallback = hasMedia ? EVAL_SCRIPT_BUDGET_CAMERA_MS : EVAL_SCRIPT_BUDGET_MS;
|
|
199
|
+
if (rawTimeout === undefined)
|
|
200
|
+
return fallback;
|
|
201
|
+
const parsed = parseInt(rawTimeout, 10);
|
|
202
|
+
if (!Number.isFinite(parsed) || parsed <= 0)
|
|
203
|
+
return fallback;
|
|
204
|
+
if (parsed > EVAL_SCRIPT_BUDGET_MAX_MS) {
|
|
205
|
+
console.error(warning(`--timeout ${parsed}ms exceeds the ${EVAL_SCRIPT_BUDGET_MAX_MS}ms max in-page budget — using ${EVAL_SCRIPT_BUDGET_MAX_MS}ms. ` +
|
|
206
|
+
`A body that needs longer than that is doing its waiting in the wrong place: start the slow work on page load and ` +
|
|
207
|
+
`absorb it in the pre-eval window (--wait-for '<ready-selector>'), keeping the body to a quick read.`));
|
|
208
|
+
return EVAL_SCRIPT_BUDGET_MAX_MS;
|
|
209
|
+
}
|
|
210
|
+
return Math.max(1_000, parsed);
|
|
211
|
+
}
|
|
212
|
+
/** Upper bound on the server-side work this eval asked for. EVERY leg the caller
|
|
213
|
+
* can lengthen has to be in here, or the client gives up on a job that is still
|
|
214
|
+
* legitimately running and reports it as the caller's fault:
|
|
215
|
+
* - the pre-eval window (--wait, and --wait-for's own timeout)
|
|
216
|
+
* - every script leg (<expr> plus each --step), each allowed its full in-page
|
|
217
|
+
* budget (--timeout) — they share ONE page load but not one clock
|
|
218
|
+
* - the reload leg, which repeats settle + eval
|
|
219
|
+
* - --camera's feed fetch/encode, paid before the page loads
|
|
220
|
+
* Browser cold-start/open overhead is NOT here; it's the flat headroom in
|
|
221
|
+
* pollEvalResult. Exported for tests. */
|
|
222
|
+
export function evalWorkBudgetMs(o) {
|
|
223
|
+
const script = o.scriptBudgetMs ?? (o.hasCamera ? EVAL_SCRIPT_BUDGET_CAMERA_MS : EVAL_SCRIPT_BUDGET_MS);
|
|
224
|
+
const legs = Math.max(1, o.legs ?? 1);
|
|
225
|
+
const settle = o.waitMs + (o.waitForTimeoutMs ?? 0);
|
|
226
|
+
const firstPass = settle + script * legs;
|
|
227
|
+
return firstPass + (o.hasReload ? settle + script : 0) + (o.hasCamera ? CAMERA_SETUP_BUDGET_MS : 0);
|
|
228
|
+
}
|
|
229
|
+
/** The server refuses a script that outran its in-page budget with a message that
|
|
230
|
+
* names the budget and says to "raise the eval timeout" — but not the flag that
|
|
231
|
+
* does it. Name it, with the value this run actually used, so the fix is one
|
|
232
|
+
* edit away instead of a guess (or a trip through --help). */
|
|
233
|
+
export function budgetOverrunHint(reason, usedBudgetMs) {
|
|
234
|
+
if (!/in-page budget/i.test(reason))
|
|
235
|
+
return null;
|
|
236
|
+
if (usedBudgetMs >= EVAL_SCRIPT_BUDGET_MAX_MS) {
|
|
237
|
+
return `This run already used the maximum in-page budget (--timeout ${EVAL_SCRIPT_BUDGET_MAX_MS}). ` +
|
|
238
|
+
`More time is not the answer: have the page start its slow work on load, wait it out in the pre-eval window ` +
|
|
239
|
+
`(--wait-for '<ready-selector>' --wait-timeout ${MAX_WAIT_MS}), and keep the body to a quick read.`;
|
|
240
|
+
}
|
|
241
|
+
return `That budget was ${Math.round(usedBudgetMs / 1000)}s. Raise it with --timeout <ms> ` +
|
|
242
|
+
`(up to ${EVAL_SCRIPT_BUDGET_MAX_MS}), e.g. --timeout ${EVAL_SCRIPT_BUDGET_MAX_MS} — ` +
|
|
243
|
+
`or gate on a ready signal instead: --wait-for '<selector>' --wait-timeout ${MAX_WAIT_MS}.`;
|
|
244
|
+
}
|
|
245
|
+
/** The headless browser paints slowly, and what that MEANS depends on what the
|
|
246
|
+
* page is doing — so the one-size warning was actively misleading on a --camera
|
|
247
|
+
* run. A vision app has no loop to step: its pipeline is driven by camera
|
|
248
|
+
* frames, one inference per painted frame.
|
|
249
|
+
*
|
|
250
|
+
* The trap this message exists to close: a slow-render warning next to a "no
|
|
251
|
+
* detection" result reads as "the renderer starved you", so the caller re-runs
|
|
252
|
+
* with a bigger --wait/--timeout, several times, and gets the identical answer
|
|
253
|
+
* each time. It cannot be otherwise. A --camera still is played as a LOOPED
|
|
254
|
+
* feed — every painted frame is the same pixels — and a vision model is
|
|
255
|
+
* deterministic on the same pixels. Frame count therefore buys attempts, not
|
|
256
|
+
* information: once the model has run once, more time cannot change the verdict.
|
|
257
|
+
* So this says so, and names the two real suspects (the frame, the app) plus the
|
|
258
|
+
* deterministic way to wait (--wait-for) instead of inviting a wall-clock
|
|
259
|
+
* escalation that is guaranteed to be wasted. Exported for tests. */
|
|
260
|
+
export function slowRenderMessage(fps, o) {
|
|
261
|
+
if (o.camera) {
|
|
262
|
+
const frames = Math.max(1, Math.round(fps * (o.waitMs / 1000)));
|
|
263
|
+
return `${warning('⚠ Slow render:')} page painted at ${fps} fps, so the app's vision pipeline ran on roughly `
|
|
264
|
+
+ `${bold(`${frames} frame${frames === 1 ? '' : 's'}`)} during the ${Math.round(o.waitMs / 1000)}s before this eval `
|
|
265
|
+
+ `(it infers once per painted frame). ${bold('That is enough:')} --camera loops your still image, so every one of `
|
|
266
|
+
+ `those frames is the SAME pixels and the model returns the SAME answer on each — re-running with a bigger `
|
|
267
|
+
+ `--wait/--timeout cannot change the result. ${bold('Do not escalate the wait.')} If a detection landed, it is real. `
|
|
268
|
+
+ `If nothing was detected, the suspect is the ${bold('frame')} (a model needs the whole subject in shot — a tight crop, `
|
|
269
|
+
+ `an odd angle or a busy background reads as nothing) or the ${bold('app')} (frames never reach the model) — never the frame rate. `
|
|
270
|
+
+ `Settle it in ONE run: ${CAMERA_FRAME_CHECK}`;
|
|
271
|
+
}
|
|
272
|
+
return `${warning('⚠ Slow render:')} page painted at ${fps} fps. `
|
|
273
|
+
+ `Waiting on real time (setTimeout) advances animation/physics time far slower than it looks — `
|
|
274
|
+
+ `assertions after a wall-clock wait can report a false negative. `
|
|
275
|
+
+ `Step the app's own loop deterministically instead (3D templates: ${bold('core.advance(seconds)')}).`;
|
|
276
|
+
}
|
|
85
277
|
/** Poll the async eval job until it finishes. Eval runs server-side as a
|
|
86
278
|
* short-lived job (so a long --wait can't trip the gateway idle timeout);
|
|
87
279
|
* we submit, then poll the result out of the job store. `expectedWorkMs` is
|
|
88
|
-
* the time the server-side work is expected to take (
|
|
89
|
-
*
|
|
280
|
+
* the time the server-side work is expected to take (see evalWorkBudgetMs);
|
|
281
|
+
* the client budget is that plus 60s of headroom for browser open/cold start. */
|
|
90
282
|
export async function pollEvalResult(evalJobId, expectedWorkMs) {
|
|
91
|
-
// Generous client budget: the server work is bounded by --wait plus browser
|
|
92
|
-
// open/settle overhead; give it that plus headroom before giving up.
|
|
93
283
|
const deadline = Date.now() + expectedWorkMs + 60_000;
|
|
94
284
|
let missCount = 0;
|
|
95
285
|
while (Date.now() < deadline) {
|
|
@@ -112,20 +302,24 @@ export async function pollEvalResult(evalJobId, expectedWorkMs) {
|
|
|
112
302
|
throw new ApiError(rec.httpStatus, rec.code, rec.reason);
|
|
113
303
|
await sleep(1000);
|
|
114
304
|
}
|
|
115
|
-
|
|
305
|
+
// This is the CLIENT giving up, not the page failing — and the budget above
|
|
306
|
+
// already covers everything the caller asked for, so "your expression is too
|
|
307
|
+
// big" is the one thing it is NOT. Never advise lowering --wait: on a camera /
|
|
308
|
+
// vision run the wait is what lets the model load, so that "fix" swaps a
|
|
309
|
+
// timeout for a silently empty read, which is worse.
|
|
310
|
+
throw new ApiError(504, 'EVAL_TIMEOUT', `the browser did not report back within ${Math.round((expectedWorkMs + 60_000) / 1000)}s — that budget already ` +
|
|
311
|
+
`covers --wait, --wait-for, the eval body and any --camera setup, so this is the browser itself being slow or ` +
|
|
312
|
+
`stuck (a cold sandbox, a heavy page), not your expression being too big. Lowering --wait does NOT help and on ` +
|
|
313
|
+
`a --camera run actively hurts (the wait is what lets the vision model load — cut it and the eval reads an ` +
|
|
314
|
+
`empty page instead). Just run the same command again: the sandbox is warm on the second hit.`);
|
|
116
315
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
* back as a `{success:false, error:"CDP command timed out: Runtime.evaluate"}`
|
|
125
|
-
* envelope that the server surfaces verbatim as the eval `result` — opaque to
|
|
126
|
-
* the caller (no timeout named, no distinction from the page or --wait). Detect
|
|
127
|
-
* exactly that envelope and return an actionable message; null otherwise. */
|
|
128
|
-
export function evalExecTimeoutMessage(result) {
|
|
316
|
+
/** When the script's own runtime overruns the in-page budget, agent-browser can
|
|
317
|
+
* abort the `Runtime.evaluate` CDP call and the failure comes back as a
|
|
318
|
+
* `{success:false, error:"CDP command timed out: Runtime.evaluate"}` envelope
|
|
319
|
+
* that the server surfaces verbatim as the eval `result` — opaque to the caller
|
|
320
|
+
* (no budget named, no flag to raise it). Detect exactly that envelope and
|
|
321
|
+
* return an actionable message; null otherwise. */
|
|
322
|
+
export function evalExecTimeoutMessage(result, budgetMs) {
|
|
129
323
|
let parsed;
|
|
130
324
|
try {
|
|
131
325
|
parsed = JSON.parse(result);
|
|
@@ -137,11 +331,14 @@ export function evalExecTimeoutMessage(result) {
|
|
|
137
331
|
return null;
|
|
138
332
|
if (!/CDP command timed out:\s*Runtime\.evaluate/i.test(parsed.error))
|
|
139
333
|
return null;
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
`
|
|
143
|
-
|
|
144
|
-
`
|
|
334
|
+
const budget = Math.round(budgetMs / 1000);
|
|
335
|
+
return (`the script hit its ${budget}s in-page budget — the body (including its own await/setTimeout ` +
|
|
336
|
+
`pauses) ran longer than that. --wait sleeps BEFORE the script and does not extend it.\n` +
|
|
337
|
+
(budgetOverrunHint('in-page budget', budgetMs) ?? '') + '\n' +
|
|
338
|
+
`If the slow part is a ONE-TIME page init (a WASM/model download, a big asset, a first-frame ` +
|
|
339
|
+
`pipeline warm-up), do NOT split the body across several 'page eval' calls — every call is a fresh ` +
|
|
340
|
+
`page load that re-pays that init, so each one hits this same wall. Have the page kick it off on ` +
|
|
341
|
+
`load (not behind a click) and absorb it in the pre-eval window instead.`);
|
|
145
342
|
}
|
|
146
343
|
// Agents instinctively reach for a flag to pass the script (`--js`, `--script`,
|
|
147
344
|
// `--code`, …); the JS is actually the positional <expr> (or --file for a saved
|
|
@@ -163,14 +360,18 @@ const JS_DECOY_FLAGS = ['--js', '--javascript', '--script', '--code', '--expr',
|
|
|
163
360
|
export const pageEvalCommand = new Command('eval')
|
|
164
361
|
.description('Evaluate JS in a real browser on a page (DOM, computed styles, element rects; inline expr or --file script). ONE client per call - to verify realtime/presence across concurrent clients use `page test --observe` instead')
|
|
165
362
|
.argument('<url>', 'URL to load')
|
|
166
|
-
.argument('[expr]',
|
|
167
|
-
.option('--file <path>',
|
|
363
|
+
.argument('[expr]', `JavaScript to evaluate in page context (inline expression or statement body with return/await; result is JSON-serialized). Omit when using --file. Time budget: the body has ${EVAL_SCRIPT_BUDGET_MS / 1000}s to finish after page load (${EVAL_SCRIPT_BUDGET_CAMERA_MS / 1000}s with --camera) - raise it with --timeout, max ${EVAL_SCRIPT_BUDGET_MAX_MS / 1000}s.`)
|
|
364
|
+
.option('--file <path>', `Read the script body from a file instead of the inline <expr> arg (mutually exclusive). Runs as an async function body, so top-level return/await work. Same post-load budget as <expr> (--timeout).`)
|
|
365
|
+
.option('--step <expr>', `Run another expression against the SAME loaded page, after <expr> (repeat, max ${MAX_EVAL_STEPS}). Whatever that page load paid for — a vision model coming up, a game booting, a socket connecting — stays up for every step, so an N-part check costs ONE page load instead of N. Each step gets its own in-page budget and its own reported result.`, (val, prev) => [...prev, val], [])
|
|
168
366
|
.option('--fixture <path>', 'Host a local file and expose it to the eval as `fixtureUrl` (and under `fixtures` by basename) to fetch in-page. For verifying a render/parse path against a real binary (an MP3, an image) - no size limit, auto-deleted after the run. Repeat for several files (single-value so it never swallows the inline <expr>).', (val, prev) => [...prev, val], [])
|
|
169
367
|
.option('--reload <expr>', 'After the first eval, reload the page IN PLACE (localStorage/sessionStorage/cookies preserved) and evaluate this second expression against the post-reload DOM. One command verifies persisted state survives a reload: seed/assert state with <expr>, then assert the restored UI here.')
|
|
170
368
|
.option('--reload-file <path>', 'Read the post-reload expression from a file instead of inline --reload (mutually exclusive)')
|
|
171
|
-
.option('--
|
|
172
|
-
.option('--
|
|
173
|
-
.option('--wait
|
|
369
|
+
.option('--camera <path>', `Play a local image or video (.png/.jpg/.webp/.mp4/.webm/.y4m/.mjpeg) as the browser's WEBCAM feed, so a camera app's real pipeline (getUserMedia → MediaPipe/YOLOX → your app logic) runs headlessly on a frame you choose. Implies --fake-media and waits ${CAMERA_DEFAULT_WAIT_MS / 1000}s for the vision model to load. No frame handy? gipity generate image "a hand making a closed fist, palm to camera".`)
|
|
370
|
+
.option('--fake-media', 'Grant a synthetic microphone + camera and auto-accept the getUserMedia prompt, so a voice/camera app runs headlessly instead of hitting its no-camera path. The feed is a built-in test pattern (and a tone) — nothing a vision model can recognize; to drive a vision app use --camera <path> instead.')
|
|
371
|
+
.option('--wait <ms>', 'Sleep this many ms after DOMContentLoaded before evaluating (lets late async work settle; max 30000). With --wait-for, it elapses AFTER the selector appears - gate, then settle.', '500')
|
|
372
|
+
.option('--wait-for <selector>', `Wait until this CSS selector appears before evaluating, then evaluate (deterministic - beats guessing a --wait). Gate on the state you are ASSERTING, not just a ready flag: '#verdict:not(:empty)' returns the instant the round lands, where a fixed wait snapshots mid-sequence. Same flag on page inspect/screenshot. Max ${WAIT_FOR_MAX_MS}ms (--wait-timeout).`)
|
|
373
|
+
.option('--wait-timeout <ms>', `Max ms to wait for --wait-for before giving up (max ${WAIT_FOR_MAX_MS})`, '5000')
|
|
374
|
+
.option('--timeout <ms>', `How long the script itself may run IN the page - its own await/setTimeout pauses count (default ${EVAL_SCRIPT_BUDGET_MS}, ${EVAL_SCRIPT_BUDGET_CAMERA_MS} with --camera/--fake-media; max ${EVAL_SCRIPT_BUDGET_MAX_MS}). Raise it to trace a sequence that unfolds over time (a game round, an animation). Distinct from --wait, which only sleeps BEFORE the script.`)
|
|
174
375
|
.option('--auth', 'Evaluate signed in as you (your Gipity account), so a page behind a Sign-in-with-Gipity login is reachable. Only works for apps using Sign in with Gipity, hosted on *.gipity.ai.')
|
|
175
376
|
.option('--json', 'Output as JSON')
|
|
176
377
|
.action((url, exprArg, opts) => run('Page eval', async () => {
|
|
@@ -223,22 +424,68 @@ export const pageEvalCommand = new Command('eval')
|
|
|
223
424
|
pageEvalCommand.error(`error: Cannot read file: ${opts.reloadFile}`);
|
|
224
425
|
}
|
|
225
426
|
}
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
|
|
427
|
+
const steps = opts.step ?? [];
|
|
428
|
+
if (steps.length > MAX_EVAL_STEPS) {
|
|
429
|
+
pageEvalCommand.error(`error: at most ${MAX_EVAL_STEPS} --step expressions ride on one page load (got ${steps.length}) — ` +
|
|
430
|
+
`do more per step, or split this into two evals`);
|
|
431
|
+
}
|
|
432
|
+
let waitMs = capWaitMs(opts.wait, url);
|
|
433
|
+
const waitForTimeoutMs = capWaitForTimeoutMs(opts.waitTimeout);
|
|
434
|
+
// The in-page budget for the script itself. Always sent, so the caller (and
|
|
435
|
+
// every error message below) is talking about one known number rather than a
|
|
436
|
+
// server-side default nobody can see.
|
|
437
|
+
const hasMedia = !!opts.camera || !!opts.fakeMedia;
|
|
438
|
+
const scriptBudgetMs = capScriptBudgetMs(opts.timeout, hasMedia);
|
|
439
|
+
// Camera runs get a model-load-sized pre-eval window unless the caller chose
|
|
440
|
+
// their own wait (either flag). Doing this by default is the whole point: the
|
|
441
|
+
// eval body must not be where a vision app's warm-up happens.
|
|
442
|
+
const explicitWait = pageEvalCommand.getOptionValueSource('wait') !== 'default';
|
|
443
|
+
const chosenWait = explicitWait || !!opts.waitFor;
|
|
444
|
+
if (opts.camera && !chosenWait) {
|
|
445
|
+
waitMs = CAMERA_DEFAULT_WAIT_MS;
|
|
446
|
+
console.error(muted(`--camera: waiting ${CAMERA_DEFAULT_WAIT_MS / 1000}s before the eval so the camera and the app's ` +
|
|
447
|
+
`vision model finish loading (they cannot warm up inside the eval body — that has a ` +
|
|
448
|
+
`${scriptBudgetMs / 1000}s budget of its own, --timeout to change it). Override with --wait <ms>, or use ` +
|
|
449
|
+
`--wait-for '<ready-selector>' to stop as soon as the app is ready.`));
|
|
450
|
+
}
|
|
451
|
+
// The selector gate REPLACES the blind pre-eval sleep server-side, so a
|
|
452
|
+
// --wait passed alongside it was silently dropped: the script ran the instant
|
|
453
|
+
// the selector appeared and read a half-finished sequence (a round still
|
|
454
|
+
// counting down), which looks exactly like an app bug. Honour both, in the
|
|
455
|
+
// only order that means anything — gate, THEN settle — by moving the sleep to
|
|
456
|
+
// the head of the first script and paying for it out of that script's budget.
|
|
457
|
+
const settleAfterGateMs = opts.waitFor && explicitWait ? waitMs : 0;
|
|
458
|
+
const sentWaitMs = settleAfterGateMs > 0 ? 0 : waitMs;
|
|
459
|
+
const sentScriptBudgetMs = Math.min(scriptBudgetMs + settleAfterGateMs, EVAL_SCRIPT_BUDGET_MAX_MS);
|
|
229
460
|
// --fixture: host each file publicly, then splice `fixtures` / `fixtureUrl`
|
|
230
|
-
// into the eval scope so the page can fetch the bytes.
|
|
231
|
-
// body a statement (const/return), so
|
|
232
|
-
//
|
|
233
|
-
//
|
|
461
|
+
// into the eval scope so the page can fetch the bytes. A prelude makes the
|
|
462
|
+
// body a statement (const/return), so an expression script is spliced into
|
|
463
|
+
// `return (...)` and a statement body is kept as-is (withPrelude). Every leg
|
|
464
|
+
// (<expr> and each --step) gets the fixture bindings. Cleanup in `finally`.
|
|
234
465
|
const fixturePaths = opts.fixture ?? [];
|
|
235
466
|
const hosted = [];
|
|
467
|
+
// The webcam feed is hosted like a fixture but is NOT exposed to the eval
|
|
468
|
+
// body as `fixtures`/`fixtureUrl` — the page never fetches it; the browser
|
|
469
|
+
// plays it as the camera device. Tracked separately so it still gets
|
|
470
|
+
// cleaned up, without shifting the fixture map the caller indexes into.
|
|
471
|
+
let camera;
|
|
236
472
|
let projectGuid;
|
|
237
473
|
let sentExpr = expr;
|
|
474
|
+
let sentSteps = steps;
|
|
475
|
+
// Validate the camera file locally first: a wrong file type should cost one
|
|
476
|
+
// instant error, not an upload plus an opaque browser-side failure.
|
|
477
|
+
if (opts.camera)
|
|
478
|
+
assertCameraFile(opts.camera);
|
|
238
479
|
try {
|
|
239
|
-
if (fixturePaths.length) {
|
|
480
|
+
if (fixturePaths.length || opts.camera) {
|
|
240
481
|
const { config } = await resolveProjectContext({});
|
|
241
482
|
projectGuid = config.projectGuid;
|
|
483
|
+
}
|
|
484
|
+
if (opts.camera) {
|
|
485
|
+
console.log(muted(`Hosting camera feed ${opts.camera}…`));
|
|
486
|
+
camera = await uploadCameraFeed(projectGuid, opts.camera);
|
|
487
|
+
}
|
|
488
|
+
if (fixturePaths.length) {
|
|
242
489
|
for (const p of fixturePaths) {
|
|
243
490
|
console.log(muted(`Hosting fixture ${p}…`));
|
|
244
491
|
hosted.push(await uploadPublicFixture(projectGuid, p));
|
|
@@ -247,27 +494,72 @@ export const pageEvalCommand = new Command('eval')
|
|
|
247
494
|
for (const h of hosted)
|
|
248
495
|
map[h.name] = h.url;
|
|
249
496
|
const prelude = `const fixtures=${JSON.stringify(map)};const fixtureUrl=${JSON.stringify(hosted[0].url)};`;
|
|
250
|
-
sentExpr =
|
|
497
|
+
sentExpr = withPrelude(sentExpr, prelude);
|
|
498
|
+
sentSteps = sentSteps.map((s) => withPrelude(s, prelude));
|
|
499
|
+
}
|
|
500
|
+
// The post-gate settle rides at the head of the FIRST script only: the
|
|
501
|
+
// later steps run on a page the gate + settle already carried forward.
|
|
502
|
+
if (settleAfterGateMs > 0) {
|
|
503
|
+
sentExpr = withPrelude(sentExpr, `await new Promise((r) => setTimeout(r, ${settleAfterGateMs}));`);
|
|
251
504
|
}
|
|
252
505
|
const kickoff = await post('/tools/browser/eval', {
|
|
253
|
-
url, expr: sentExpr, waitMs,
|
|
506
|
+
url, expr: sentExpr, waitMs: sentWaitMs,
|
|
507
|
+
steps: sentSteps.length ? sentSteps : undefined,
|
|
254
508
|
reloadExpr,
|
|
255
509
|
waitForSelector: opts.waitFor || undefined,
|
|
256
510
|
waitForTimeoutMs: opts.waitFor ? waitForTimeoutMs : undefined,
|
|
511
|
+
timeoutMs: sentScriptBudgetMs,
|
|
257
512
|
auth: opts.auth || undefined,
|
|
513
|
+
// A camera feed needs the synthetic devices in place to be played into,
|
|
514
|
+
// so --camera implies --fake-media rather than failing on the pairing.
|
|
515
|
+
fakeMedia: opts.fakeMedia || !!camera || undefined,
|
|
516
|
+
cameraUrl: camera?.url,
|
|
258
517
|
});
|
|
259
|
-
|
|
260
|
-
|
|
518
|
+
let d;
|
|
519
|
+
try {
|
|
520
|
+
d = await pollEvalResult(kickoff.data.evalJobId, evalWorkBudgetMs({
|
|
521
|
+
waitMs: sentWaitMs,
|
|
522
|
+
waitForTimeoutMs: opts.waitFor ? waitForTimeoutMs : 0,
|
|
523
|
+
hasReload: reloadExpr !== undefined,
|
|
524
|
+
hasCamera: !!camera,
|
|
525
|
+
scriptBudgetMs: sentScriptBudgetMs,
|
|
526
|
+
legs: 1 + sentSteps.length,
|
|
527
|
+
}));
|
|
528
|
+
}
|
|
529
|
+
catch (err) {
|
|
530
|
+
// The server's overrun message names the budget but not the flag that
|
|
531
|
+
// raises it — and the flag is what the caller needs next. Name it, with
|
|
532
|
+
// the value THIS run used, so the retry is an edit rather than a guess.
|
|
533
|
+
const msg = err?.message ?? '';
|
|
534
|
+
const hint = budgetOverrunHint(msg, scriptBudgetMs);
|
|
535
|
+
if (!hint)
|
|
536
|
+
throw err;
|
|
537
|
+
throw new Error(`${msg}\n${hint}`);
|
|
538
|
+
}
|
|
261
539
|
const { result, noValue } = normalizeEvalResult(d.result);
|
|
262
540
|
const reload = d.reloadResult !== undefined ? normalizeEvalResult(d.reloadResult) : undefined;
|
|
263
|
-
const execTimeout = evalExecTimeoutMessage(d.result);
|
|
541
|
+
const execTimeout = evalExecTimeoutMessage(d.result, scriptBudgetMs);
|
|
264
542
|
if (execTimeout)
|
|
265
543
|
throw new Error(execTimeout);
|
|
544
|
+
// Each --step is reported on its own, against the expression that produced
|
|
545
|
+
// it — N results in one blob would be unreadable and unattributable.
|
|
546
|
+
const stepOut = steps.map((stepExpr, i) => {
|
|
547
|
+
const raw = d.stepResults?.[i];
|
|
548
|
+
const norm = raw !== undefined ? normalizeEvalResult(raw) : undefined;
|
|
549
|
+
return { expr: stepExpr, result: norm?.result ?? '', noValue: norm?.noValue ?? true };
|
|
550
|
+
});
|
|
551
|
+
const emptyState = !noValue && isEmptyStateResult(result);
|
|
552
|
+
// An empty read on a camera run has a different cause and a different fix
|
|
553
|
+
// than an empty read on a normal page: the generic hint's "poll harder,
|
|
554
|
+
// raise --timeout" is guaranteed-wasted advice against a looped still.
|
|
555
|
+
const emptyHint = camera ? EVAL_CAMERA_EMPTY_HINT : EVAL_EMPTY_STATE_HINT;
|
|
266
556
|
if (opts.json) {
|
|
267
557
|
console.log(JSON.stringify({
|
|
268
558
|
...d, result,
|
|
559
|
+
...(stepOut.length ? { stepResults: stepOut.map((s) => s.result) } : {}),
|
|
269
560
|
...(reload ? { reloadResult: reload.result } : {}),
|
|
270
561
|
...(noValue ? { hint: EVAL_NO_VALUE_HINT } : {}),
|
|
562
|
+
...(emptyState ? { hint: emptyHint } : {}),
|
|
271
563
|
}));
|
|
272
564
|
return;
|
|
273
565
|
}
|
|
@@ -280,10 +572,7 @@ export const pageEvalCommand = new Command('eval')
|
|
|
280
572
|
// `setTimeout(2000)` may advance only a fraction of a second of app time.
|
|
281
573
|
// Without this line the eval returns a plausible-looking false negative.
|
|
282
574
|
if (d.slowRender) {
|
|
283
|
-
console.log(
|
|
284
|
-
+ `Waiting on real time (setTimeout) advances animation/physics time far slower than it looks — `
|
|
285
|
-
+ `assertions after a wall-clock wait can report a false negative. `
|
|
286
|
-
+ `Step the app's own loop deterministically instead (3D templates: ${bold('core.advance(seconds)')}).`);
|
|
575
|
+
console.log(slowRenderMessage(d.slowRender.fps, { camera: !!camera, waitMs }));
|
|
287
576
|
}
|
|
288
577
|
// Auth state: without this line an agent can't distinguish "signed-in
|
|
289
578
|
// eval" from "--auth silently no-op'd against the anonymous page".
|
|
@@ -293,14 +582,25 @@ export const pageEvalCommand = new Command('eval')
|
|
|
293
582
|
? `${muted('Auth:')} ${success('session established')}${who ? muted(` as ${who}`) : ''} ${muted('(what the page renders with it is app-defined)')}`
|
|
294
583
|
: `${warning('Auth: session NOT established')}${d.auth.detail ? ` — ${d.auth.detail}` : ''} ${muted('(this is the anonymous view)')}`);
|
|
295
584
|
}
|
|
585
|
+
if (camera)
|
|
586
|
+
console.log(`${muted('Camera:')} ${camera.name} ${muted('(played as the webcam feed; getUserMedia resolves)')}`);
|
|
296
587
|
if (hosted.length)
|
|
297
588
|
console.log(`${muted('Fixtures:')} ${hosted.map((h) => h.name).join(', ')}`);
|
|
298
589
|
console.log(opts.file ? `${muted('Script:')} ${opts.file}` : `${muted('Expression:')} ${summarizeExpr(expr)}`);
|
|
299
590
|
console.log(`\n${result.trim() ? result : muted('(empty result)')}`);
|
|
300
591
|
if (noValue)
|
|
301
592
|
console.log(muted(`\n${EVAL_NO_VALUE_HINT}`));
|
|
593
|
+
if (emptyState)
|
|
594
|
+
console.log(muted(`\n${emptyHint}`));
|
|
302
595
|
if (d.truncated)
|
|
303
596
|
console.log(muted('\n(result truncated to fit context - narrow the expression for the full value)'));
|
|
597
|
+
for (let i = 0; i < stepOut.length; i++) {
|
|
598
|
+
const s = stepOut[i];
|
|
599
|
+
console.log(`\n${bold(`Step ${i + 1}`)} ${muted(summarizeExpr(s.expr))} ${muted('(same page load)')}`);
|
|
600
|
+
console.log(s.result.trim() ? s.result : muted('(empty result)'));
|
|
601
|
+
if (s.noValue)
|
|
602
|
+
console.log(muted(EVAL_NO_VALUE_HINT));
|
|
603
|
+
}
|
|
304
604
|
if (reload) {
|
|
305
605
|
console.log(`\n${bold('After reload')} ${muted('(page reloaded in place — storage preserved)')}`);
|
|
306
606
|
console.log(reload.result.trim() ? reload.result : muted('(empty result)'));
|
|
@@ -311,7 +611,7 @@ export const pageEvalCommand = new Command('eval')
|
|
|
311
611
|
}
|
|
312
612
|
}
|
|
313
613
|
finally {
|
|
314
|
-
for (const h of hosted) {
|
|
614
|
+
for (const h of [...hosted, ...(camera ? [camera] : [])]) {
|
|
315
615
|
try {
|
|
316
616
|
await deleteFixture(projectGuid, h.guid);
|
|
317
617
|
}
|
|
@@ -347,15 +647,52 @@ Examples:
|
|
|
347
647
|
"localStorage.setItem('todo','milk'); document.title" \\
|
|
348
648
|
--reload "({ restored: localStorage.getItem('todo'), heading: document.querySelector('h1')?.textContent })"
|
|
349
649
|
|
|
650
|
+
# Camera app (MediaPipe / YOLOX / any getUserMedia app): play a real image as
|
|
651
|
+
# the webcam so the app's OWN pipeline runs on a frame you control — no need to
|
|
652
|
+
# stub the model or export internals just to test around a missing camera.
|
|
653
|
+
# --camera waits ${CAMERA_DEFAULT_WAIT_MS / 1000}s before evaluating (model load + first frame); the script
|
|
654
|
+
# itself then gets ${EVAL_SCRIPT_BUDGET_CAMERA_MS / 1000}s in the page (--timeout, max ${EVAL_SCRIPT_BUDGET_MAX_MS / 1000}s).
|
|
655
|
+
gipity generate image "a hand making a closed fist, palm to camera, plain background" -o fist.png
|
|
656
|
+
gipity page eval "https://dev.gipity.ai/me/app/" --camera fist.png \\
|
|
657
|
+
"document.querySelector('#detected-gesture').textContent"
|
|
658
|
+
# Camera app that only starts on a click? Start it on page load instead (a headless
|
|
659
|
+
# run has no user), and expose a ready signal so the wait ends the moment it's up:
|
|
660
|
+
gipity page eval "https://dev.gipity.ai/me/app/" --camera fist.png \\
|
|
661
|
+
--wait-for "#detected-gesture:not(:empty)" --wait-timeout ${WAIT_FOR_MAX_MS} \\
|
|
662
|
+
"document.querySelector('#detected-gesture').textContent"
|
|
663
|
+
# Several assertions about the SAME page? Use --step, not several commands: a camera
|
|
664
|
+
# page load pays for getUserMedia + the vision model ONCE and every step reuses it.
|
|
665
|
+
gipity page eval "https://dev.gipity.ai/me/app/" --camera fist.png \\
|
|
666
|
+
--wait-for '[data-vision="ready"]' \\
|
|
667
|
+
"window.__vision.gesture()" \\
|
|
668
|
+
--step "document.getElementById('see').textContent" \\
|
|
669
|
+
--step "({ score: score.textContent, verdict: verdict.textContent })"
|
|
670
|
+
|
|
350
671
|
Module resolution: dynamic import() specifiers starting with ./ or ../ resolve
|
|
351
672
|
against the PAGE URL, so import('./packages/i18n/index.js') loads the app's own
|
|
352
673
|
module without hand-building the deployed /account/project/ path. Absolute paths
|
|
353
674
|
and full URLs pass through unchanged.
|
|
354
675
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
676
|
+
Time budget: the script runs under a ${EVAL_SCRIPT_BUDGET_MS / 1000}s in-page budget (${EVAL_SCRIPT_BUDGET_CAMERA_MS / 1000}s with --camera/--fake-media),
|
|
677
|
+
counting its own await/setTimeout pauses. Two separate knobs:
|
|
678
|
+
--timeout <ms> how long the SCRIPT may run in the page (max ${EVAL_SCRIPT_BUDGET_MAX_MS}) — raise this to
|
|
679
|
+
trace a sequence that unfolds over time (a game round, an animation)
|
|
680
|
+
--wait / --wait-for how long to settle BEFORE the script runs — a blind sleep, a
|
|
681
|
+
selector gate, or both (gate first, then sleep). Neither extends --timeout.
|
|
682
|
+
Which one:
|
|
683
|
+
- waiting for the app to REACH a state before you read it → --wait-for '<selector>'
|
|
684
|
+
(gate on the end state — '#verdict:not(:empty)' — not just a ready flag; a bare
|
|
685
|
+
--wait samples one arbitrary instant and can land mid-sequence). Passing both means
|
|
686
|
+
gate first, then settle --wait ms.
|
|
687
|
+
- watching state evolve (poll until a result lands, record a trace) → --timeout
|
|
688
|
+
- a slow ONE-TIME init (WASM/model download, big asset) → do NOT split the body across
|
|
689
|
+
calls; every call re-loads the page and re-pays it. Start that work on page load and
|
|
690
|
+
absorb it in the pre-eval window: --wait <ms> (max ${MAX_WAIT_MS}) or, better,
|
|
691
|
+
--wait-for '<ready-selector>' --wait-timeout ${WAIT_FOR_MAX_MS}.
|
|
692
|
+
|
|
693
|
+
Several things to check on one page? Pass --step (max ${MAX_EVAL_STEPS}) instead of running
|
|
694
|
+
'page eval' N times: the steps run in order against the ONE loaded page, so a slow
|
|
695
|
+
boot (vision model, game, socket) is paid once, not once per assertion.
|
|
359
696
|
|
|
360
697
|
Testing realtime/shared state across clients?
|
|
361
698
|
Separate 'page eval' calls run sequentially (one finishes before the next
|
|
@@ -41,7 +41,7 @@ export const pageInspectCommand = new Command('inspect')
|
|
|
41
41
|
.option('--no-reprobe', 'Skip the automatic second probe that filters one-time transient console errors - roughly halves inspect time on a page with any console error, at the cost of possibly reporting a cold-load transient as real')
|
|
42
42
|
.option('--no-truncate', 'Show full URLs instead of truncating long ones with middle-ellipsis')
|
|
43
43
|
.option('--all', 'Include render-blocking, large resources, oversized images, overflow culprits, and LCP detail')
|
|
44
|
-
.option('--fake-media',
|
|
44
|
+
.option('--no-fake-media', "Inspect with NO camera or microphone present, so a getUserMedia app takes its no-device error path (use this only when that fallback IS what you're inspecting)")
|
|
45
45
|
.option('--device <name>', `Inspect as a real touch device: ${INSPECT_DEVICES.join(', ')}. Emulates touch events, mobile user-agent and DPR, so a touch-gated mobile layout is what gets inspected.`)
|
|
46
46
|
.option('--auth', 'Load the page signed in as you (your Gipity account), so pages behind a Sign-in-with-Gipity login are reachable. Only works for apps using Sign in with Gipity, hosted on *.gipity.ai.')
|
|
47
47
|
// Hidden redirect: agents reach for `page inspect --screenshot`. We don't take
|
|
@@ -65,11 +65,17 @@ export async function inspectPage(url, opts = {}) {
|
|
|
65
65
|
const waitForTimeoutMs = Number.isFinite(parsedTimeout) && parsedTimeout >= 0 ? parsedTimeout : 5000;
|
|
66
66
|
const truncate = opts.truncate !== false;
|
|
67
67
|
const showAll = opts.all === true;
|
|
68
|
+
// The headless browser has a synthetic camera + mic BY DEFAULT. A real user
|
|
69
|
+
// inspecting a camera app has a camera; a browser without one makes every
|
|
70
|
+
// getUserMedia app report a NotFoundError that is an artifact of the probe,
|
|
71
|
+
// not a defect in the page — and worse, the app stops at its error path, so
|
|
72
|
+
// the pipeline the agent actually wanted inspected never runs. A synthetic
|
|
73
|
+
// device costs a page that never asks for media exactly nothing.
|
|
68
74
|
const inspectBody = {
|
|
69
75
|
url, waitMs,
|
|
70
76
|
waitForSelector: opts.waitFor || undefined,
|
|
71
77
|
waitForTimeoutMs: opts.waitFor ? waitForTimeoutMs : undefined,
|
|
72
|
-
fakeMedia: opts.fakeMedia
|
|
78
|
+
fakeMedia: opts.fakeMedia !== false ? true : undefined,
|
|
73
79
|
device: opts.device ? resolveTouchDevice(opts.device) : undefined,
|
|
74
80
|
auth: opts.auth || undefined,
|
|
75
81
|
};
|