browser-debug-mcp-bridge 1.11.1 → 1.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/apps/mcp-server/dist/db/automation-repository.js +9 -4
- package/apps/mcp-server/dist/db/automation-repository.js.map +1 -1
- package/apps/mcp-server/dist/db/migrations.js +79 -0
- package/apps/mcp-server/dist/db/migrations.js.map +1 -1
- package/apps/mcp-server/dist/db/schema.js +60 -1
- package/apps/mcp-server/dist/db/schema.js.map +1 -1
- package/apps/mcp-server/dist/mcp/server.js +2864 -262
- package/apps/mcp-server/dist/mcp/server.js.map +1 -1
- package/apps/mcp-server/dist/mcp/target-resolution.js +390 -0
- package/apps/mcp-server/dist/mcp/target-resolution.js.map +1 -0
- package/apps/mcp-server/dist/mcp/tool-loop-guard.js +655 -0
- package/apps/mcp-server/dist/mcp/tool-loop-guard.js.map +1 -0
- package/apps/mcp-server/dist/override-audit.js +3 -3
- package/apps/mcp-server/dist/override-audit.js.map +1 -1
- package/apps/mcp-server/dist/override-poc.js +4 -4
- package/apps/mcp-server/dist/override-poc.js.map +1 -1
- package/apps/mcp-server/dist/override-profile-generator.js +3 -9
- package/apps/mcp-server/dist/override-profile-generator.js.map +1 -1
- package/apps/mcp-server/dist/websocket/messages.js +5 -0
- package/apps/mcp-server/dist/websocket/messages.js.map +1 -1
- package/package.json +8 -3
|
@@ -5,6 +5,7 @@ import { createHash, randomUUID } from 'crypto';
|
|
|
5
5
|
import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'fs';
|
|
6
6
|
import { dirname, resolve } from 'path';
|
|
7
7
|
import { z } from 'zod';
|
|
8
|
+
import { WorkflowTargetResolutionError, hasSemanticActionTargetMatcher, resolveWorkflowActionTarget, summarizeWorkflowTargetMatcher, } from './target-resolution.js';
|
|
8
9
|
import { getConnection } from '../db/connection.js';
|
|
9
10
|
import { diagnoseOverridePoc, insertOverridePlanAudit, listOverridePlanAudits, listOverridePocRequests, listOverridePocRuns, } from '../override-audit.js';
|
|
10
11
|
import { createOverrideProfileConfig, OVERRIDE_PROFILE_ADAPTERS, } from '../override-profile-generator.js';
|
|
@@ -15,6 +16,7 @@ import { mapNextOverrideAssetsWithDrift } from '../next-asset-mapper.js';
|
|
|
15
16
|
import { planNextSourceOverride } from '../next-source-override-planner.js';
|
|
16
17
|
import { listObservedOverrideAssets, persistObservedOverrideAssets } from '../override-observed-assets.js';
|
|
17
18
|
import { planOverrideResponsePatch } from '../override-response-planner.js';
|
|
19
|
+
import { createToolLoopGuard } from './tool-loop-guard.js';
|
|
18
20
|
function createDefaultMcpLogger() {
|
|
19
21
|
const write = (level, message, payload) => {
|
|
20
22
|
process.stderr.write(`${message} ${JSON.stringify({ level, ...payload })}\n`);
|
|
@@ -31,12 +33,93 @@ function createDefaultMcpLogger() {
|
|
|
31
33
|
},
|
|
32
34
|
};
|
|
33
35
|
}
|
|
36
|
+
const UIActionTargetScopeSchema = z.enum(['buttons', 'links', 'inputs', 'modals', 'focused']);
|
|
37
|
+
const UIActionLocatorMatcherSchema = z.union([
|
|
38
|
+
z.string().min(1),
|
|
39
|
+
z.object({
|
|
40
|
+
pattern: z.string().min(1),
|
|
41
|
+
flags: z.string().regex(/^[imsu]*$/).optional(),
|
|
42
|
+
}),
|
|
43
|
+
]);
|
|
44
|
+
const UIActionLocatorStepSchema = z.object({
|
|
45
|
+
kind: z.enum(['css', 'role', 'text', 'label', 'testId', 'placeholder', 'altText']),
|
|
46
|
+
value: UIActionLocatorMatcherSchema.optional(),
|
|
47
|
+
role: z.string().min(1).optional(),
|
|
48
|
+
name: UIActionLocatorMatcherSchema.optional(),
|
|
49
|
+
exact: z.boolean().optional(),
|
|
50
|
+
relation: z.enum(['filter', 'descendant', 'ancestor']).optional(),
|
|
51
|
+
}).superRefine((value, ctx) => {
|
|
52
|
+
if (value.kind === 'role' && !value.role && !value.value) {
|
|
53
|
+
ctx.addIssue({
|
|
54
|
+
code: z.ZodIssueCode.custom,
|
|
55
|
+
message: 'role locator step requires role or value',
|
|
56
|
+
path: ['role'],
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
if (value.kind !== 'role' && !value.value) {
|
|
60
|
+
ctx.addIssue({
|
|
61
|
+
code: z.ZodIssueCode.custom,
|
|
62
|
+
message: `${value.kind} locator step requires value`,
|
|
63
|
+
path: ['value'],
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
const UIActionLocatorSchema = z.object({
|
|
68
|
+
scope: UIActionTargetScopeSchema.optional(),
|
|
69
|
+
frame: z.object({
|
|
70
|
+
selector: z.string().min(1).optional(),
|
|
71
|
+
urlContains: z.string().min(1).optional(),
|
|
72
|
+
titleContains: z.string().min(1).optional(),
|
|
73
|
+
}).optional(),
|
|
74
|
+
steps: z.array(UIActionLocatorStepSchema).min(1).max(8),
|
|
75
|
+
});
|
|
76
|
+
const UIActionCoordinateTargetSchema = z.object({
|
|
77
|
+
x: z.number().finite(),
|
|
78
|
+
y: z.number().finite(),
|
|
79
|
+
frameId: z.number().int().min(0).optional(),
|
|
80
|
+
});
|
|
34
81
|
const LiveUIActionTargetSchema = z.object({
|
|
35
82
|
selector: z.string().min(1).optional(),
|
|
36
83
|
elementRef: z.string().min(1).optional(),
|
|
84
|
+
coordinates: UIActionCoordinateTargetSchema.optional(),
|
|
37
85
|
tabId: z.number().int().min(0).optional(),
|
|
38
86
|
frameId: z.number().int().min(0).optional(),
|
|
39
87
|
url: z.string().url().optional(),
|
|
88
|
+
locator: UIActionLocatorSchema.optional(),
|
|
89
|
+
frameUrlContains: z.string().min(1).optional(),
|
|
90
|
+
frameTitleContains: z.string().min(1).optional(),
|
|
91
|
+
testId: z.string().min(1).optional(),
|
|
92
|
+
scope: UIActionTargetScopeSchema.optional(),
|
|
93
|
+
textContains: z.string().min(1).optional(),
|
|
94
|
+
labelContains: z.string().min(1).optional(),
|
|
95
|
+
titleContains: z.string().min(1).optional(),
|
|
96
|
+
role: z.string().min(1).optional(),
|
|
97
|
+
name: z.string().min(1).optional(),
|
|
98
|
+
placeholder: z.string().min(1).optional(),
|
|
99
|
+
altText: z.string().min(1).optional(),
|
|
100
|
+
tagName: z.string().min(1).optional(),
|
|
101
|
+
type: z.string().min(1).optional(),
|
|
102
|
+
exact: z.boolean().optional(),
|
|
103
|
+
nth: z.number().int().min(0).optional(),
|
|
104
|
+
first: z.boolean().optional(),
|
|
105
|
+
last: z.boolean().optional(),
|
|
106
|
+
strict: z.boolean().optional(),
|
|
107
|
+
visible: z.boolean().optional(),
|
|
108
|
+
disabled: z.boolean().optional(),
|
|
109
|
+
selected: z.boolean().optional(),
|
|
110
|
+
pressed: z.boolean().optional(),
|
|
111
|
+
expanded: z.boolean().optional(),
|
|
112
|
+
readOnly: z.boolean().optional(),
|
|
113
|
+
requiredField: z.boolean().optional(),
|
|
114
|
+
}).superRefine((value, ctx) => {
|
|
115
|
+
const positionFields = [value.nth !== undefined, value.first === true, value.last === true].filter(Boolean).length;
|
|
116
|
+
if (positionFields > 1) {
|
|
117
|
+
ctx.addIssue({
|
|
118
|
+
code: z.ZodIssueCode.custom,
|
|
119
|
+
message: 'target can use only one of nth, first, or last',
|
|
120
|
+
path: ['target'],
|
|
121
|
+
});
|
|
122
|
+
}
|
|
40
123
|
});
|
|
41
124
|
const LiveUIActionBaseSchema = z.object({
|
|
42
125
|
traceId: z.string().min(1).optional(),
|
|
@@ -50,6 +133,10 @@ const LiveUIActionRequestSchema = z.discriminatedUnion('action', [
|
|
|
50
133
|
clickCount: z.number().int().min(1).max(3).optional(),
|
|
51
134
|
}).optional(),
|
|
52
135
|
}),
|
|
136
|
+
LiveUIActionBaseSchema.extend({
|
|
137
|
+
action: z.literal('hover'),
|
|
138
|
+
input: z.object({}).optional(),
|
|
139
|
+
}),
|
|
53
140
|
LiveUIActionBaseSchema.extend({
|
|
54
141
|
action: z.literal('input'),
|
|
55
142
|
input: z.object({
|
|
@@ -95,20 +182,34 @@ const LiveUIActionRequestSchema = z.discriminatedUnion('action', [
|
|
|
95
182
|
]);
|
|
96
183
|
const UIWorkflowModeSchema = z.enum(['safe', 'fast']);
|
|
97
184
|
const UIWorkflowFailureStrategySchema = z.enum(['stop', 'continue', 'retry_once']);
|
|
98
|
-
const UIWorkflowActionTargetScopeSchema =
|
|
185
|
+
const UIWorkflowActionTargetScopeSchema = UIActionTargetScopeSchema;
|
|
99
186
|
const UIWorkflowActionTargetSchema = z.object({
|
|
100
187
|
selector: z.string().min(1).optional(),
|
|
101
188
|
elementRef: z.string().min(1).optional(),
|
|
189
|
+
coordinates: UIActionCoordinateTargetSchema.optional(),
|
|
102
190
|
tabId: z.number().int().min(0).optional(),
|
|
103
191
|
frameId: z.number().int().min(0).optional(),
|
|
104
192
|
url: z.string().url().optional(),
|
|
193
|
+
locator: UIActionLocatorSchema.optional(),
|
|
194
|
+
frameUrlContains: z.string().min(1).optional(),
|
|
195
|
+
frameTitleContains: z.string().min(1).optional(),
|
|
105
196
|
testId: z.string().min(1).optional(),
|
|
106
197
|
scope: UIWorkflowActionTargetScopeSchema.optional(),
|
|
107
198
|
textContains: z.string().min(1).optional(),
|
|
108
199
|
labelContains: z.string().min(1).optional(),
|
|
109
200
|
titleContains: z.string().min(1).optional(),
|
|
201
|
+
role: z.string().min(1).optional(),
|
|
202
|
+
name: z.string().min(1).optional(),
|
|
203
|
+
placeholder: z.string().min(1).optional(),
|
|
204
|
+
altText: z.string().min(1).optional(),
|
|
110
205
|
tagName: z.string().min(1).optional(),
|
|
111
206
|
type: z.string().min(1).optional(),
|
|
207
|
+
exact: z.boolean().optional(),
|
|
208
|
+
nth: z.number().int().min(0).optional(),
|
|
209
|
+
first: z.boolean().optional(),
|
|
210
|
+
last: z.boolean().optional(),
|
|
211
|
+
strict: z.boolean().optional(),
|
|
212
|
+
visible: z.boolean().optional(),
|
|
112
213
|
disabled: z.boolean().optional(),
|
|
113
214
|
selected: z.boolean().optional(),
|
|
114
215
|
pressed: z.boolean().optional(),
|
|
@@ -118,13 +219,28 @@ const UIWorkflowActionTargetSchema = z.object({
|
|
|
118
219
|
}).superRefine((value, ctx) => {
|
|
119
220
|
if (!value.selector
|
|
120
221
|
&& !value.elementRef
|
|
222
|
+
&& !value.coordinates
|
|
223
|
+
&& !value.locator
|
|
224
|
+
&& !value.scope
|
|
121
225
|
&& !value.testId
|
|
122
226
|
&& !value.textContains
|
|
123
227
|
&& !value.labelContains
|
|
124
|
-
&& !value.titleContains
|
|
228
|
+
&& !value.titleContains
|
|
229
|
+
&& !value.role
|
|
230
|
+
&& !value.name
|
|
231
|
+
&& !value.placeholder
|
|
232
|
+
&& !value.altText) {
|
|
233
|
+
ctx.addIssue({
|
|
234
|
+
code: z.ZodIssueCode.custom,
|
|
235
|
+
message: 'target requires selector, elementRef, coordinates, locator, scope, testId, textContains, labelContains, titleContains, role, name, placeholder, or altText',
|
|
236
|
+
path: ['target'],
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
const positionFields = [value.nth !== undefined, value.first === true, value.last === true].filter(Boolean).length;
|
|
240
|
+
if (positionFields > 1) {
|
|
125
241
|
ctx.addIssue({
|
|
126
242
|
code: z.ZodIssueCode.custom,
|
|
127
|
-
message: 'target
|
|
243
|
+
message: 'target can use only one of nth, first, or last',
|
|
128
244
|
path: ['target'],
|
|
129
245
|
});
|
|
130
246
|
}
|
|
@@ -163,6 +279,10 @@ const UIWorkflowActionStepSchema = z.discriminatedUnion('action', [
|
|
|
163
279
|
clickCount: z.number().int().min(1).max(3).optional(),
|
|
164
280
|
}).optional(),
|
|
165
281
|
}),
|
|
282
|
+
UIWorkflowActionBaseSchema.extend({
|
|
283
|
+
action: z.literal('hover'),
|
|
284
|
+
input: z.object({}).optional(),
|
|
285
|
+
}),
|
|
166
286
|
UIWorkflowActionBaseSchema.extend({
|
|
167
287
|
action: z.literal('input'),
|
|
168
288
|
input: z.object({
|
|
@@ -207,14 +327,22 @@ const UIWorkflowActionStepSchema = z.discriminatedUnion('action', [
|
|
|
207
327
|
}),
|
|
208
328
|
]);
|
|
209
329
|
const UIWorkflowPageStateMatcherSchema = z.object({
|
|
210
|
-
scope: z.enum(['buttons', 'inputs', 'modals', 'focused', 'page']),
|
|
330
|
+
scope: z.enum(['buttons', 'links', 'inputs', 'modals', 'focused', 'page']),
|
|
211
331
|
selector: z.string().optional(),
|
|
212
332
|
testId: z.string().optional(),
|
|
213
333
|
textContains: z.string().optional(),
|
|
214
334
|
labelContains: z.string().optional(),
|
|
215
335
|
titleContains: z.string().optional(),
|
|
336
|
+
role: z.string().optional(),
|
|
337
|
+
name: z.string().optional(),
|
|
338
|
+
placeholder: z.string().optional(),
|
|
339
|
+
altText: z.string().optional(),
|
|
340
|
+
exact: z.boolean().optional(),
|
|
341
|
+
frameUrlContains: z.string().optional(),
|
|
342
|
+
frameTitleContains: z.string().optional(),
|
|
216
343
|
urlContains: z.string().optional(),
|
|
217
344
|
language: z.string().optional(),
|
|
345
|
+
visible: z.boolean().optional(),
|
|
218
346
|
disabled: z.boolean().optional(),
|
|
219
347
|
selected: z.boolean().optional(),
|
|
220
348
|
pressed: z.boolean().optional(),
|
|
@@ -247,10 +375,201 @@ const UIWorkflowAssertStepSchema = UIWorkflowStepBaseSchema.extend({
|
|
|
247
375
|
kind: z.literal('assert'),
|
|
248
376
|
matcher: UIWorkflowPageStateMatcherSchema,
|
|
249
377
|
});
|
|
378
|
+
const AutomationWaitBaseSchema = z.object({
|
|
379
|
+
timeoutMs: z.number().int().min(100).max(120000).optional(),
|
|
380
|
+
pollIntervalMs: z.number().int().min(50).max(5000).optional(),
|
|
381
|
+
});
|
|
382
|
+
const AutomationWaitUrlSchema = AutomationWaitBaseSchema.extend({
|
|
383
|
+
waitKind: z.literal('url'),
|
|
384
|
+
urlContains: z.string().min(1).optional(),
|
|
385
|
+
urlRegex: z.string().min(1).optional(),
|
|
386
|
+
exactUrl: z.string().min(1).optional(),
|
|
387
|
+
}).superRefine((value, ctx) => {
|
|
388
|
+
if (!value.urlContains && !value.urlRegex && !value.exactUrl) {
|
|
389
|
+
ctx.addIssue({
|
|
390
|
+
code: z.ZodIssueCode.custom,
|
|
391
|
+
message: 'url wait requires urlContains, urlRegex, or exactUrl',
|
|
392
|
+
path: ['wait'],
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
});
|
|
396
|
+
const AutomationWaitNavigationSchema = AutomationWaitBaseSchema.extend({
|
|
397
|
+
waitKind: z.literal('navigation'),
|
|
398
|
+
urlContains: z.string().min(1).optional(),
|
|
399
|
+
urlRegex: z.string().min(1).optional(),
|
|
400
|
+
exactUrl: z.string().min(1).optional(),
|
|
401
|
+
fromUrlContains: z.string().min(1).optional(),
|
|
402
|
+
fromUrlRegex: z.string().min(1).optional(),
|
|
403
|
+
trigger: z.string().min(1).optional(),
|
|
404
|
+
sinceTs: z.number().int().min(0).optional(),
|
|
405
|
+
tabId: z.number().int().min(0).optional(),
|
|
406
|
+
}).superRefine((value, ctx) => {
|
|
407
|
+
if (!value.urlContains
|
|
408
|
+
&& !value.urlRegex
|
|
409
|
+
&& !value.exactUrl
|
|
410
|
+
&& !value.fromUrlContains
|
|
411
|
+
&& !value.fromUrlRegex
|
|
412
|
+
&& !value.trigger) {
|
|
413
|
+
ctx.addIssue({
|
|
414
|
+
code: z.ZodIssueCode.custom,
|
|
415
|
+
message: 'navigation wait requires a URL, from-URL, or trigger predicate',
|
|
416
|
+
path: ['wait'],
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
});
|
|
420
|
+
const AutomationWaitNavigationLifecycleSchema = AutomationWaitBaseSchema.extend({
|
|
421
|
+
waitKind: z.literal('navigation_lifecycle'),
|
|
422
|
+
state: z.enum(['commit', 'same_document', 'domcontentloaded', 'load', 'network_idle']).default('load'),
|
|
423
|
+
urlContains: z.string().min(1).optional(),
|
|
424
|
+
urlRegex: z.string().min(1).optional(),
|
|
425
|
+
exactUrl: z.string().min(1).optional(),
|
|
426
|
+
tabId: z.number().int().min(0).optional(),
|
|
427
|
+
});
|
|
428
|
+
const AutomationWaitLoadStateSchema = AutomationWaitBaseSchema.extend({
|
|
429
|
+
waitKind: z.literal('load_state'),
|
|
430
|
+
state: z.enum(['domcontentloaded', 'load']).default('load'),
|
|
431
|
+
urlContains: z.string().min(1).optional(),
|
|
432
|
+
urlRegex: z.string().min(1).optional(),
|
|
433
|
+
exactUrl: z.string().min(1).optional(),
|
|
434
|
+
});
|
|
435
|
+
const AutomationWaitSelectorStateSchema = AutomationWaitBaseSchema.extend({
|
|
436
|
+
waitKind: z.literal('selector_state'),
|
|
437
|
+
selector: z.string().min(1),
|
|
438
|
+
state: z.enum(['attached', 'detached', 'visible', 'hidden']).default('visible'),
|
|
439
|
+
frameId: z.number().int().min(0).default(0),
|
|
440
|
+
});
|
|
441
|
+
const AutomationWaitConsoleSchema = AutomationWaitBaseSchema.extend({
|
|
442
|
+
waitKind: z.literal('console'),
|
|
443
|
+
levels: z.array(z.string().min(1)).optional(),
|
|
444
|
+
contains: z.string().min(1).optional(),
|
|
445
|
+
sinceTs: z.number().int().min(0).optional(),
|
|
446
|
+
includeRuntimeErrors: z.boolean().optional(),
|
|
447
|
+
});
|
|
448
|
+
const AutomationWaitDialogSchema = AutomationWaitBaseSchema.extend({
|
|
449
|
+
waitKind: z.literal('dialog'),
|
|
450
|
+
type: z.enum(['alert', 'confirm', 'prompt', 'beforeunload']).optional(),
|
|
451
|
+
messageContains: z.string().min(1).optional(),
|
|
452
|
+
urlContains: z.string().min(1).optional(),
|
|
453
|
+
action: z.enum(['none', 'accept', 'dismiss']).default('none'),
|
|
454
|
+
promptText: z.string().optional(),
|
|
455
|
+
tabId: z.number().int().min(0).optional(),
|
|
456
|
+
});
|
|
457
|
+
const AutomationWaitStableLayoutSchema = AutomationWaitBaseSchema.extend({
|
|
458
|
+
waitKind: z.literal('stable_layout'),
|
|
459
|
+
selector: z.string().min(1).optional(),
|
|
460
|
+
stableMs: z.number().int().min(100).max(10000).default(500),
|
|
461
|
+
tabId: z.number().int().min(0).optional(),
|
|
462
|
+
});
|
|
463
|
+
const AutomationWaitDownloadSchema = AutomationWaitBaseSchema.extend({
|
|
464
|
+
waitKind: z.literal('download'),
|
|
465
|
+
urlContains: z.string().min(1).optional(),
|
|
466
|
+
urlRegex: z.string().min(1).optional(),
|
|
467
|
+
exactUrl: z.string().min(1).optional(),
|
|
468
|
+
filenameContains: z.string().min(1).optional(),
|
|
469
|
+
filenameRegex: z.string().min(1).optional(),
|
|
470
|
+
state: z.enum(['started', 'completed']).default('started'),
|
|
471
|
+
tabId: z.number().int().min(0).optional(),
|
|
472
|
+
}).superRefine((value, ctx) => {
|
|
473
|
+
if (!value.urlContains && !value.urlRegex && !value.exactUrl && !value.filenameContains && !value.filenameRegex) {
|
|
474
|
+
ctx.addIssue({
|
|
475
|
+
code: z.ZodIssueCode.custom,
|
|
476
|
+
message: 'download wait requires a URL or filename predicate',
|
|
477
|
+
path: ['wait'],
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
});
|
|
481
|
+
const AutomationWaitPopupSchema = AutomationWaitBaseSchema.extend({
|
|
482
|
+
waitKind: z.literal('popup'),
|
|
483
|
+
urlContains: z.string().min(1).optional(),
|
|
484
|
+
urlRegex: z.string().min(1).optional(),
|
|
485
|
+
exactUrl: z.string().min(1).optional(),
|
|
486
|
+
openerTabId: z.number().int().min(0).optional(),
|
|
487
|
+
}).superRefine((value, ctx) => {
|
|
488
|
+
if (!value.urlContains && !value.urlRegex && !value.exactUrl && value.openerTabId === undefined) {
|
|
489
|
+
ctx.addIssue({
|
|
490
|
+
code: z.ZodIssueCode.custom,
|
|
491
|
+
message: 'popup wait requires a URL predicate or openerTabId',
|
|
492
|
+
path: ['wait'],
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
});
|
|
496
|
+
const AutomationWaitNetworkQuietSchema = AutomationWaitBaseSchema.extend({
|
|
497
|
+
waitKind: z.literal('network_quiet'),
|
|
498
|
+
quietMs: z.number().int().min(100).max(10000).default(500),
|
|
499
|
+
urlContains: z.string().min(1).optional(),
|
|
500
|
+
method: z.string().min(1).optional(),
|
|
501
|
+
tabId: z.number().int().min(0).optional(),
|
|
502
|
+
});
|
|
503
|
+
const AutomationWaitNetworkBaseSchema = AutomationWaitBaseSchema.extend({
|
|
504
|
+
urlContains: z.string().min(1).optional(),
|
|
505
|
+
urlRegex: z.string().min(1).optional(),
|
|
506
|
+
exactUrl: z.string().min(1).optional(),
|
|
507
|
+
method: z.string().min(1).optional(),
|
|
508
|
+
traceId: z.string().min(1).optional(),
|
|
509
|
+
initiator: z.enum(['fetch', 'xhr', 'img', 'script', 'other']).optional(),
|
|
510
|
+
requestContentType: z.string().min(1).optional(),
|
|
511
|
+
sinceTs: z.number().int().min(0).optional(),
|
|
512
|
+
tabId: z.number().int().min(0).optional(),
|
|
513
|
+
includeBodies: z.boolean().optional(),
|
|
514
|
+
});
|
|
515
|
+
const AutomationWaitRequestSchema = AutomationWaitNetworkBaseSchema.extend({
|
|
516
|
+
waitKind: z.literal('request'),
|
|
517
|
+
}).superRefine((value, ctx) => {
|
|
518
|
+
if (!value.urlContains && !value.urlRegex && !value.exactUrl && !value.traceId) {
|
|
519
|
+
ctx.addIssue({
|
|
520
|
+
code: z.ZodIssueCode.custom,
|
|
521
|
+
message: 'request wait requires urlContains, urlRegex, exactUrl, or traceId',
|
|
522
|
+
path: ['wait'],
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
});
|
|
526
|
+
const AutomationWaitResponseSchema = AutomationWaitNetworkBaseSchema.extend({
|
|
527
|
+
waitKind: z.literal('response'),
|
|
528
|
+
statusIn: z.array(z.number().int().min(100).max(599)).optional(),
|
|
529
|
+
statusGte: z.number().int().min(100).max(599).optional(),
|
|
530
|
+
statusLt: z.number().int().min(100).max(600).optional(),
|
|
531
|
+
responseContentType: z.string().min(1).optional(),
|
|
532
|
+
errorType: z.string().min(1).optional(),
|
|
533
|
+
}).superRefine((value, ctx) => {
|
|
534
|
+
if (!value.urlContains && !value.urlRegex && !value.exactUrl && !value.traceId) {
|
|
535
|
+
ctx.addIssue({
|
|
536
|
+
code: z.ZodIssueCode.custom,
|
|
537
|
+
message: 'response wait requires urlContains, urlRegex, exactUrl, or traceId',
|
|
538
|
+
path: ['wait'],
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
if (value.statusGte !== undefined && value.statusLt !== undefined && value.statusGte >= value.statusLt) {
|
|
542
|
+
ctx.addIssue({
|
|
543
|
+
code: z.ZodIssueCode.custom,
|
|
544
|
+
message: 'statusGte must be less than statusLt',
|
|
545
|
+
path: ['statusGte'],
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
});
|
|
549
|
+
const AutomationWaitSpecSchema = z.discriminatedUnion('waitKind', [
|
|
550
|
+
AutomationWaitUrlSchema,
|
|
551
|
+
AutomationWaitNavigationSchema,
|
|
552
|
+
AutomationWaitNavigationLifecycleSchema,
|
|
553
|
+
AutomationWaitLoadStateSchema,
|
|
554
|
+
AutomationWaitSelectorStateSchema,
|
|
555
|
+
AutomationWaitConsoleSchema,
|
|
556
|
+
AutomationWaitDialogSchema,
|
|
557
|
+
AutomationWaitStableLayoutSchema,
|
|
558
|
+
AutomationWaitDownloadSchema,
|
|
559
|
+
AutomationWaitPopupSchema,
|
|
560
|
+
AutomationWaitNetworkQuietSchema,
|
|
561
|
+
AutomationWaitRequestSchema,
|
|
562
|
+
AutomationWaitResponseSchema,
|
|
563
|
+
]);
|
|
564
|
+
const UIWorkflowGenericWaitStepSchema = UIWorkflowStepBaseSchema.extend({
|
|
565
|
+
kind: z.literal('wait'),
|
|
566
|
+
wait: AutomationWaitSpecSchema,
|
|
567
|
+
});
|
|
250
568
|
const UIWorkflowStepSchema = z.discriminatedUnion('kind', [
|
|
251
569
|
UIWorkflowActionStepSchema,
|
|
252
570
|
UIWorkflowWaitForStepSchema,
|
|
253
571
|
UIWorkflowAssertStepSchema,
|
|
572
|
+
UIWorkflowGenericWaitStepSchema,
|
|
254
573
|
]);
|
|
255
574
|
const RunUIStepsSchema = z.object({
|
|
256
575
|
sessionId: z.string().min(1),
|
|
@@ -263,6 +582,91 @@ const RunUIStepsSchema = z.object({
|
|
|
263
582
|
function createUIWorkflowTraceId() {
|
|
264
583
|
return `uiworkflow-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
265
584
|
}
|
|
585
|
+
const LOCATOR_MATCHER_TOOL_SCHEMA = {
|
|
586
|
+
anyOf: [
|
|
587
|
+
{ type: 'string' },
|
|
588
|
+
{
|
|
589
|
+
type: 'object',
|
|
590
|
+
required: ['pattern'],
|
|
591
|
+
properties: {
|
|
592
|
+
pattern: { type: 'string' },
|
|
593
|
+
flags: { type: 'string' },
|
|
594
|
+
},
|
|
595
|
+
},
|
|
596
|
+
],
|
|
597
|
+
};
|
|
598
|
+
const ACTION_LOCATOR_TOOL_SCHEMA = {
|
|
599
|
+
type: 'object',
|
|
600
|
+
required: ['steps'],
|
|
601
|
+
properties: {
|
|
602
|
+
scope: { type: 'string', enum: ['buttons', 'links', 'inputs', 'modals', 'focused'] },
|
|
603
|
+
frame: {
|
|
604
|
+
type: 'object',
|
|
605
|
+
properties: {
|
|
606
|
+
selector: { type: 'string' },
|
|
607
|
+
urlContains: { type: 'string' },
|
|
608
|
+
titleContains: { type: 'string' },
|
|
609
|
+
},
|
|
610
|
+
},
|
|
611
|
+
steps: {
|
|
612
|
+
type: 'array',
|
|
613
|
+
minItems: 1,
|
|
614
|
+
maxItems: 8,
|
|
615
|
+
items: {
|
|
616
|
+
type: 'object',
|
|
617
|
+
required: ['kind'],
|
|
618
|
+
properties: {
|
|
619
|
+
kind: { type: 'string', enum: ['css', 'role', 'text', 'label', 'testId', 'placeholder', 'altText'] },
|
|
620
|
+
value: LOCATOR_MATCHER_TOOL_SCHEMA,
|
|
621
|
+
role: { type: 'string' },
|
|
622
|
+
name: LOCATOR_MATCHER_TOOL_SCHEMA,
|
|
623
|
+
exact: { type: 'boolean' },
|
|
624
|
+
relation: { type: 'string', enum: ['filter', 'descendant', 'ancestor'] },
|
|
625
|
+
},
|
|
626
|
+
},
|
|
627
|
+
},
|
|
628
|
+
},
|
|
629
|
+
};
|
|
630
|
+
const AUTOMATION_WAIT_TOOL_SCHEMA = {
|
|
631
|
+
type: 'object',
|
|
632
|
+
required: ['waitKind'],
|
|
633
|
+
properties: {
|
|
634
|
+
waitKind: { type: 'string', enum: ['url', 'navigation', 'navigation_lifecycle', 'load_state', 'selector_state', 'console', 'dialog', 'stable_layout', 'download', 'popup', 'network_quiet', 'request', 'response'] },
|
|
635
|
+
timeoutMs: { type: 'number' },
|
|
636
|
+
pollIntervalMs: { type: 'number' },
|
|
637
|
+
urlContains: { type: 'string' },
|
|
638
|
+
urlRegex: { type: 'string' },
|
|
639
|
+
exactUrl: { type: 'string' },
|
|
640
|
+
fromUrlContains: { type: 'string' },
|
|
641
|
+
fromUrlRegex: { type: 'string' },
|
|
642
|
+
trigger: { type: 'string' },
|
|
643
|
+
state: { type: 'string', enum: ['commit', 'same_document', 'domcontentloaded', 'load', 'network_idle', 'attached', 'detached', 'visible', 'hidden', 'started', 'completed'] },
|
|
644
|
+
selector: { type: 'string' },
|
|
645
|
+
frameId: { type: 'number' },
|
|
646
|
+
levels: { type: 'array', items: { type: 'string' } },
|
|
647
|
+
contains: { type: 'string' },
|
|
648
|
+
sinceTs: { type: 'number' },
|
|
649
|
+
includeRuntimeErrors: { type: 'boolean' },
|
|
650
|
+
action: { type: 'string', enum: ['none', 'accept', 'dismiss'] },
|
|
651
|
+
promptText: { type: 'string' },
|
|
652
|
+
stableMs: { type: 'number' },
|
|
653
|
+
filenameContains: { type: 'string' },
|
|
654
|
+
filenameRegex: { type: 'string' },
|
|
655
|
+
openerTabId: { type: 'number' },
|
|
656
|
+
quietMs: { type: 'number' },
|
|
657
|
+
method: { type: 'string' },
|
|
658
|
+
traceId: { type: 'string' },
|
|
659
|
+
initiator: { type: 'string', enum: ['fetch', 'xhr', 'img', 'script', 'other'] },
|
|
660
|
+
requestContentType: { type: 'string' },
|
|
661
|
+
responseContentType: { type: 'string' },
|
|
662
|
+
statusIn: { type: 'array', items: { type: 'number' } },
|
|
663
|
+
statusGte: { type: 'number' },
|
|
664
|
+
statusLt: { type: 'number' },
|
|
665
|
+
errorType: { type: 'string' },
|
|
666
|
+
includeBodies: { type: 'boolean' },
|
|
667
|
+
tabId: { type: 'number' },
|
|
668
|
+
},
|
|
669
|
+
};
|
|
266
670
|
const TOOL_SCHEMAS = {
|
|
267
671
|
list_sessions: {
|
|
268
672
|
type: 'object',
|
|
@@ -451,6 +855,7 @@ const TOOL_SCHEMAS = {
|
|
|
451
855
|
properties: {
|
|
452
856
|
sessionId: { type: 'string' },
|
|
453
857
|
selector: { type: 'string' },
|
|
858
|
+
frameId: { type: 'number' },
|
|
454
859
|
properties: { type: 'array', items: { type: 'string' } },
|
|
455
860
|
},
|
|
456
861
|
},
|
|
@@ -460,6 +865,7 @@ const TOOL_SCHEMAS = {
|
|
|
460
865
|
properties: {
|
|
461
866
|
sessionId: { type: 'string' },
|
|
462
867
|
selector: { type: 'string' },
|
|
868
|
+
frameId: { type: 'number' },
|
|
463
869
|
},
|
|
464
870
|
},
|
|
465
871
|
get_page_state: {
|
|
@@ -470,6 +876,7 @@ const TOOL_SCHEMAS = {
|
|
|
470
876
|
maxItems: { type: 'number' },
|
|
471
877
|
maxTextLength: { type: 'number' },
|
|
472
878
|
includeButtons: { type: 'boolean' },
|
|
879
|
+
includeLinks: { type: 'boolean' },
|
|
473
880
|
includeInputs: { type: 'boolean' },
|
|
474
881
|
includeModals: { type: 'boolean' },
|
|
475
882
|
},
|
|
@@ -481,7 +888,7 @@ const TOOL_SCHEMAS = {
|
|
|
481
888
|
sessionId: { type: 'string' },
|
|
482
889
|
kinds: {
|
|
483
890
|
type: 'array',
|
|
484
|
-
items: { type: 'string', enum: ['buttons', 'inputs', 'modals', 'focused'] },
|
|
891
|
+
items: { type: 'string', enum: ['buttons', 'links', 'inputs', 'modals', 'focused'] },
|
|
485
892
|
},
|
|
486
893
|
maxItems: { type: 'number' },
|
|
487
894
|
maxTextLength: { type: 'number' },
|
|
@@ -501,14 +908,22 @@ const TOOL_SCHEMAS = {
|
|
|
501
908
|
required: ['sessionId', 'scope'],
|
|
502
909
|
properties: {
|
|
503
910
|
sessionId: { type: 'string' },
|
|
504
|
-
scope: { type: 'string', enum: ['buttons', 'inputs', 'modals', 'focused', 'page'] },
|
|
911
|
+
scope: { type: 'string', enum: ['buttons', 'links', 'inputs', 'modals', 'focused', 'page'] },
|
|
505
912
|
selector: { type: 'string' },
|
|
506
913
|
testId: { type: 'string' },
|
|
507
914
|
textContains: { type: 'string' },
|
|
508
915
|
labelContains: { type: 'string' },
|
|
509
916
|
titleContains: { type: 'string' },
|
|
917
|
+
role: { type: 'string' },
|
|
918
|
+
name: { type: 'string' },
|
|
919
|
+
placeholder: { type: 'string' },
|
|
920
|
+
altText: { type: 'string' },
|
|
921
|
+
exact: { type: 'boolean' },
|
|
922
|
+
frameUrlContains: { type: 'string' },
|
|
923
|
+
frameTitleContains: { type: 'string' },
|
|
510
924
|
urlContains: { type: 'string' },
|
|
511
925
|
language: { type: 'string' },
|
|
926
|
+
visible: { type: 'boolean' },
|
|
512
927
|
disabled: { type: 'boolean' },
|
|
513
928
|
selected: { type: 'boolean' },
|
|
514
929
|
pressed: { type: 'boolean' },
|
|
@@ -528,14 +943,22 @@ const TOOL_SCHEMAS = {
|
|
|
528
943
|
required: ['sessionId', 'scope'],
|
|
529
944
|
properties: {
|
|
530
945
|
sessionId: { type: 'string' },
|
|
531
|
-
scope: { type: 'string', enum: ['buttons', 'inputs', 'modals', 'focused', 'page'] },
|
|
946
|
+
scope: { type: 'string', enum: ['buttons', 'links', 'inputs', 'modals', 'focused', 'page'] },
|
|
532
947
|
selector: { type: 'string' },
|
|
533
948
|
testId: { type: 'string' },
|
|
534
949
|
textContains: { type: 'string' },
|
|
535
950
|
labelContains: { type: 'string' },
|
|
536
951
|
titleContains: { type: 'string' },
|
|
952
|
+
role: { type: 'string' },
|
|
953
|
+
name: { type: 'string' },
|
|
954
|
+
placeholder: { type: 'string' },
|
|
955
|
+
altText: { type: 'string' },
|
|
956
|
+
exact: { type: 'boolean' },
|
|
957
|
+
frameUrlContains: { type: 'string' },
|
|
958
|
+
frameTitleContains: { type: 'string' },
|
|
537
959
|
urlContains: { type: 'string' },
|
|
538
960
|
language: { type: 'string' },
|
|
961
|
+
visible: { type: 'boolean' },
|
|
539
962
|
disabled: { type: 'boolean' },
|
|
540
963
|
selected: { type: 'boolean' },
|
|
541
964
|
pressed: { type: 'boolean' },
|
|
@@ -552,140 +975,352 @@ const TOOL_SCHEMAS = {
|
|
|
552
975
|
pollIntervalMs: { type: 'number' },
|
|
553
976
|
},
|
|
554
977
|
},
|
|
555
|
-
|
|
978
|
+
preflight_automation_flow: {
|
|
556
979
|
type: 'object',
|
|
557
980
|
required: ['sessionId'],
|
|
558
981
|
properties: {
|
|
559
982
|
sessionId: { type: 'string' },
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
maxAncestors: { type: 'number' },
|
|
567
|
-
includeDom: { type: 'boolean' },
|
|
568
|
-
includeStyles: { type: 'boolean' },
|
|
569
|
-
includePngDataUrl: { type: 'boolean' },
|
|
983
|
+
expectedUrlContains: { type: 'string' },
|
|
984
|
+
requireSensitiveAutomation: { type: 'boolean' },
|
|
985
|
+
plannedActions: { type: 'array', items: { type: 'string' } },
|
|
986
|
+
includePageState: { type: 'boolean' },
|
|
987
|
+
maxItems: { type: 'number' },
|
|
988
|
+
maxTextLength: { type: 'number' },
|
|
570
989
|
},
|
|
571
990
|
},
|
|
572
|
-
|
|
991
|
+
wait_for_url: {
|
|
573
992
|
type: 'object',
|
|
574
993
|
required: ['sessionId'],
|
|
575
994
|
properties: {
|
|
576
995
|
sessionId: { type: 'string' },
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
996
|
+
urlContains: { type: 'string' },
|
|
997
|
+
urlRegex: { type: 'string' },
|
|
998
|
+
exactUrl: { type: 'string' },
|
|
999
|
+
timeoutMs: { type: 'number' },
|
|
1000
|
+
pollIntervalMs: { type: 'number' },
|
|
1001
|
+
},
|
|
1002
|
+
},
|
|
1003
|
+
wait_for_navigation: {
|
|
1004
|
+
type: 'object',
|
|
1005
|
+
required: ['sessionId'],
|
|
1006
|
+
properties: {
|
|
1007
|
+
sessionId: { type: 'string' },
|
|
1008
|
+
urlContains: { type: 'string' },
|
|
1009
|
+
urlRegex: { type: 'string' },
|
|
1010
|
+
exactUrl: { type: 'string' },
|
|
1011
|
+
fromUrlContains: { type: 'string' },
|
|
1012
|
+
fromUrlRegex: { type: 'string' },
|
|
1013
|
+
trigger: { type: 'string' },
|
|
581
1014
|
sinceTs: { type: 'number' },
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
responseProfile: { type: 'string' },
|
|
586
|
-
includeArgs: { type: 'boolean' },
|
|
587
|
-
maxResponseBytes: { type: 'number' },
|
|
1015
|
+
tabId: { type: 'number' },
|
|
1016
|
+
timeoutMs: { type: 'number' },
|
|
1017
|
+
pollIntervalMs: { type: 'number' },
|
|
588
1018
|
},
|
|
589
1019
|
},
|
|
590
|
-
|
|
1020
|
+
wait_for_navigation_lifecycle: {
|
|
591
1021
|
type: 'object',
|
|
592
|
-
|
|
1022
|
+
required: ['sessionId'],
|
|
1023
|
+
properties: {
|
|
1024
|
+
sessionId: { type: 'string' },
|
|
1025
|
+
state: { type: 'string', enum: ['commit', 'same_document', 'domcontentloaded', 'load', 'network_idle'] },
|
|
1026
|
+
urlContains: { type: 'string' },
|
|
1027
|
+
urlRegex: { type: 'string' },
|
|
1028
|
+
exactUrl: { type: 'string' },
|
|
1029
|
+
tabId: { type: 'number' },
|
|
1030
|
+
timeoutMs: { type: 'number' },
|
|
1031
|
+
pollIntervalMs: { type: 'number' },
|
|
1032
|
+
},
|
|
593
1033
|
},
|
|
594
|
-
|
|
1034
|
+
wait_for_load_state: {
|
|
595
1035
|
type: 'object',
|
|
596
|
-
required: ['
|
|
1036
|
+
required: ['sessionId'],
|
|
597
1037
|
properties: {
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
profileId: { type: 'string' },
|
|
606
|
-
profileName: { type: 'string' },
|
|
607
|
-
enabled: { type: 'boolean' },
|
|
608
|
-
profileEnabled: { type: 'boolean' },
|
|
609
|
-
autoReload: { type: 'boolean' },
|
|
610
|
-
includeManifestFiles: { type: 'boolean' },
|
|
611
|
-
includeStaticFiles: { type: 'boolean' },
|
|
612
|
-
extensions: { type: 'array', items: { type: 'string' } },
|
|
613
|
-
maxRules: { type: 'number' },
|
|
614
|
-
writeConfig: { type: 'boolean' },
|
|
615
|
-
overwrite: { type: 'boolean' },
|
|
1038
|
+
sessionId: { type: 'string' },
|
|
1039
|
+
state: { type: 'string', enum: ['domcontentloaded', 'load'] },
|
|
1040
|
+
urlContains: { type: 'string' },
|
|
1041
|
+
urlRegex: { type: 'string' },
|
|
1042
|
+
exactUrl: { type: 'string' },
|
|
1043
|
+
timeoutMs: { type: 'number' },
|
|
1044
|
+
pollIntervalMs: { type: 'number' },
|
|
616
1045
|
},
|
|
617
1046
|
},
|
|
618
|
-
|
|
1047
|
+
wait_for_selector_state: {
|
|
619
1048
|
type: 'object',
|
|
1049
|
+
required: ['sessionId', 'selector'],
|
|
620
1050
|
properties: {
|
|
621
|
-
|
|
1051
|
+
sessionId: { type: 'string' },
|
|
1052
|
+
selector: { type: 'string' },
|
|
1053
|
+
state: { type: 'string', enum: ['attached', 'detached', 'visible', 'hidden'] },
|
|
1054
|
+
frameId: { type: 'number' },
|
|
1055
|
+
timeoutMs: { type: 'number' },
|
|
1056
|
+
pollIntervalMs: { type: 'number' },
|
|
622
1057
|
},
|
|
623
1058
|
},
|
|
624
|
-
|
|
1059
|
+
wait_for_console: {
|
|
625
1060
|
type: 'object',
|
|
626
1061
|
required: ['sessionId'],
|
|
627
1062
|
properties: {
|
|
628
1063
|
sessionId: { type: 'string' },
|
|
629
|
-
|
|
1064
|
+
levels: { type: 'array', items: { type: 'string' } },
|
|
1065
|
+
contains: { type: 'string' },
|
|
1066
|
+
sinceTs: { type: 'number' },
|
|
1067
|
+
includeRuntimeErrors: { type: 'boolean' },
|
|
1068
|
+
timeoutMs: { type: 'number' },
|
|
1069
|
+
pollIntervalMs: { type: 'number' },
|
|
630
1070
|
},
|
|
631
1071
|
},
|
|
632
|
-
|
|
1072
|
+
wait_for_dialog: {
|
|
633
1073
|
type: 'object',
|
|
634
1074
|
required: ['sessionId'],
|
|
635
1075
|
properties: {
|
|
636
1076
|
sessionId: { type: 'string' },
|
|
1077
|
+
type: { type: 'string', enum: ['alert', 'confirm', 'prompt', 'beforeunload'] },
|
|
1078
|
+
messageContains: { type: 'string' },
|
|
1079
|
+
urlContains: { type: 'string' },
|
|
1080
|
+
action: { type: 'string', enum: ['none', 'accept', 'dismiss'] },
|
|
1081
|
+
promptText: { type: 'string' },
|
|
637
1082
|
tabId: { type: 'number' },
|
|
638
|
-
|
|
1083
|
+
timeoutMs: { type: 'number' },
|
|
1084
|
+
pollIntervalMs: { type: 'number' },
|
|
639
1085
|
},
|
|
640
1086
|
},
|
|
641
|
-
|
|
1087
|
+
wait_for_stable_layout: {
|
|
642
1088
|
type: 'object',
|
|
643
1089
|
required: ['sessionId'],
|
|
644
1090
|
properties: {
|
|
645
1091
|
sessionId: { type: 'string' },
|
|
1092
|
+
selector: { type: 'string' },
|
|
1093
|
+
stableMs: { type: 'number' },
|
|
646
1094
|
tabId: { type: 'number' },
|
|
647
|
-
targetUrl: { type: 'string' },
|
|
648
|
-
targetAssetUrl: { type: 'string' },
|
|
649
|
-
captureMode: { type: 'string', enum: ['extension-fetch', 'cdp-response'] },
|
|
650
|
-
triggerReload: { type: 'boolean' },
|
|
651
|
-
matchMode: { type: 'string', enum: ['exact', 'prefix'] },
|
|
652
|
-
ruleType: { type: 'string' },
|
|
653
|
-
requestMethod: { type: 'string' },
|
|
654
|
-
requestHeaders: { type: 'object' },
|
|
655
1095
|
timeoutMs: { type: 'number' },
|
|
656
|
-
|
|
657
|
-
includeBody: { type: 'boolean' },
|
|
1096
|
+
pollIntervalMs: { type: 'number' },
|
|
658
1097
|
},
|
|
659
1098
|
},
|
|
660
|
-
|
|
1099
|
+
wait_for_download: {
|
|
661
1100
|
type: 'object',
|
|
662
1101
|
required: ['sessionId'],
|
|
663
1102
|
properties: {
|
|
664
1103
|
sessionId: { type: 'string' },
|
|
665
|
-
|
|
666
|
-
|
|
1104
|
+
urlContains: { type: 'string' },
|
|
1105
|
+
urlRegex: { type: 'string' },
|
|
1106
|
+
exactUrl: { type: 'string' },
|
|
1107
|
+
filenameContains: { type: 'string' },
|
|
1108
|
+
filenameRegex: { type: 'string' },
|
|
1109
|
+
state: { type: 'string', enum: ['started', 'completed'] },
|
|
1110
|
+
tabId: { type: 'number' },
|
|
1111
|
+
timeoutMs: { type: 'number' },
|
|
1112
|
+
pollIntervalMs: { type: 'number' },
|
|
667
1113
|
},
|
|
668
1114
|
},
|
|
669
|
-
|
|
1115
|
+
wait_for_popup: {
|
|
670
1116
|
type: 'object',
|
|
671
|
-
required: ['
|
|
1117
|
+
required: ['sessionId'],
|
|
672
1118
|
properties: {
|
|
673
1119
|
sessionId: { type: 'string' },
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
maxResults: { type: 'number' },
|
|
681
|
-
fetchProductionAssets: { type: 'boolean' },
|
|
682
|
-
productionFetchTimeoutMs: { type: 'number' },
|
|
683
|
-
maxProductionAssetBytes: { type: 'number' },
|
|
684
|
-
maxDriftCandidates: { type: 'number' },
|
|
685
|
-
productionFetchConcurrency: { type: 'number' },
|
|
1120
|
+
urlContains: { type: 'string' },
|
|
1121
|
+
urlRegex: { type: 'string' },
|
|
1122
|
+
exactUrl: { type: 'string' },
|
|
1123
|
+
openerTabId: { type: 'number' },
|
|
1124
|
+
timeoutMs: { type: 'number' },
|
|
1125
|
+
pollIntervalMs: { type: 'number' },
|
|
686
1126
|
},
|
|
687
1127
|
},
|
|
688
|
-
|
|
1128
|
+
wait_for_request: {
|
|
1129
|
+
type: 'object',
|
|
1130
|
+
required: ['sessionId'],
|
|
1131
|
+
properties: {
|
|
1132
|
+
sessionId: { type: 'string' },
|
|
1133
|
+
urlContains: { type: 'string' },
|
|
1134
|
+
urlRegex: { type: 'string' },
|
|
1135
|
+
exactUrl: { type: 'string' },
|
|
1136
|
+
method: { type: 'string' },
|
|
1137
|
+
traceId: { type: 'string' },
|
|
1138
|
+
initiator: { type: 'string', enum: ['fetch', 'xhr', 'img', 'script', 'other'] },
|
|
1139
|
+
requestContentType: { type: 'string' },
|
|
1140
|
+
sinceTs: { type: 'number' },
|
|
1141
|
+
tabId: { type: 'number' },
|
|
1142
|
+
includeBodies: { type: 'boolean' },
|
|
1143
|
+
timeoutMs: { type: 'number' },
|
|
1144
|
+
pollIntervalMs: { type: 'number' },
|
|
1145
|
+
},
|
|
1146
|
+
},
|
|
1147
|
+
wait_for_response: {
|
|
1148
|
+
type: 'object',
|
|
1149
|
+
required: ['sessionId'],
|
|
1150
|
+
properties: {
|
|
1151
|
+
sessionId: { type: 'string' },
|
|
1152
|
+
urlContains: { type: 'string' },
|
|
1153
|
+
urlRegex: { type: 'string' },
|
|
1154
|
+
exactUrl: { type: 'string' },
|
|
1155
|
+
method: { type: 'string' },
|
|
1156
|
+
traceId: { type: 'string' },
|
|
1157
|
+
initiator: { type: 'string', enum: ['fetch', 'xhr', 'img', 'script', 'other'] },
|
|
1158
|
+
requestContentType: { type: 'string' },
|
|
1159
|
+
responseContentType: { type: 'string' },
|
|
1160
|
+
statusIn: { type: 'array', items: { type: 'number' } },
|
|
1161
|
+
statusGte: { type: 'number' },
|
|
1162
|
+
statusLt: { type: 'number' },
|
|
1163
|
+
errorType: { type: 'string' },
|
|
1164
|
+
sinceTs: { type: 'number' },
|
|
1165
|
+
tabId: { type: 'number' },
|
|
1166
|
+
includeBodies: { type: 'boolean' },
|
|
1167
|
+
timeoutMs: { type: 'number' },
|
|
1168
|
+
pollIntervalMs: { type: 'number' },
|
|
1169
|
+
},
|
|
1170
|
+
},
|
|
1171
|
+
wait_for_network_quiet: {
|
|
1172
|
+
type: 'object',
|
|
1173
|
+
required: ['sessionId'],
|
|
1174
|
+
properties: {
|
|
1175
|
+
sessionId: { type: 'string' },
|
|
1176
|
+
quietMs: { type: 'number' },
|
|
1177
|
+
urlContains: { type: 'string' },
|
|
1178
|
+
method: { type: 'string' },
|
|
1179
|
+
tabId: { type: 'number' },
|
|
1180
|
+
timeoutMs: { type: 'number' },
|
|
1181
|
+
pollIntervalMs: { type: 'number' },
|
|
1182
|
+
},
|
|
1183
|
+
},
|
|
1184
|
+
capture_ui_snapshot: {
|
|
1185
|
+
type: 'object',
|
|
1186
|
+
required: ['sessionId'],
|
|
1187
|
+
properties: {
|
|
1188
|
+
sessionId: { type: 'string' },
|
|
1189
|
+
selector: { type: 'string' },
|
|
1190
|
+
trigger: { type: 'string' },
|
|
1191
|
+
mode: { type: 'string' },
|
|
1192
|
+
styleMode: { type: 'string' },
|
|
1193
|
+
maxDepth: { type: 'number' },
|
|
1194
|
+
maxBytes: { type: 'number' },
|
|
1195
|
+
maxAncestors: { type: 'number' },
|
|
1196
|
+
includeDom: { type: 'boolean' },
|
|
1197
|
+
includeStyles: { type: 'boolean' },
|
|
1198
|
+
includePngDataUrl: { type: 'boolean' },
|
|
1199
|
+
},
|
|
1200
|
+
},
|
|
1201
|
+
get_live_console_logs: {
|
|
1202
|
+
type: 'object',
|
|
1203
|
+
required: ['sessionId'],
|
|
1204
|
+
properties: {
|
|
1205
|
+
sessionId: { type: 'string' },
|
|
1206
|
+
url: { type: 'string' },
|
|
1207
|
+
tabId: { type: 'number' },
|
|
1208
|
+
levels: { type: 'array', items: { type: 'string' } },
|
|
1209
|
+
contains: { type: 'string' },
|
|
1210
|
+
sinceTs: { type: 'number' },
|
|
1211
|
+
includeRuntimeErrors: { type: 'boolean' },
|
|
1212
|
+
dedupeWindowMs: { type: 'number' },
|
|
1213
|
+
limit: { type: 'number' },
|
|
1214
|
+
responseProfile: { type: 'string' },
|
|
1215
|
+
includeArgs: { type: 'boolean' },
|
|
1216
|
+
maxResponseBytes: { type: 'number' },
|
|
1217
|
+
},
|
|
1218
|
+
},
|
|
1219
|
+
list_override_profiles: {
|
|
1220
|
+
type: 'object',
|
|
1221
|
+
properties: {
|
|
1222
|
+
responseProfile: { type: 'string', enum: ['compact', 'full'] },
|
|
1223
|
+
},
|
|
1224
|
+
},
|
|
1225
|
+
create_override_profile: {
|
|
1226
|
+
type: 'object',
|
|
1227
|
+
required: ['targetBaseUrl'],
|
|
1228
|
+
properties: {
|
|
1229
|
+
adapter: { type: 'string' },
|
|
1230
|
+
mode: { type: 'string' },
|
|
1231
|
+
targetBaseUrl: { type: 'string' },
|
|
1232
|
+
projectRoot: { type: 'string' },
|
|
1233
|
+
assetRoot: { type: 'string' },
|
|
1234
|
+
nextDir: { type: 'string' },
|
|
1235
|
+
configPath: { type: 'string' },
|
|
1236
|
+
profileId: { type: 'string' },
|
|
1237
|
+
profileName: { type: 'string' },
|
|
1238
|
+
enabled: { type: 'boolean' },
|
|
1239
|
+
profileEnabled: { type: 'boolean' },
|
|
1240
|
+
autoReload: { type: 'boolean' },
|
|
1241
|
+
includeManifestFiles: { type: 'boolean' },
|
|
1242
|
+
includeStaticFiles: { type: 'boolean' },
|
|
1243
|
+
extensions: { type: 'array', items: { type: 'string' } },
|
|
1244
|
+
maxRules: { type: 'number' },
|
|
1245
|
+
writeConfig: { type: 'boolean' },
|
|
1246
|
+
overwrite: { type: 'boolean' },
|
|
1247
|
+
responseProfile: { type: 'string', enum: ['compact', 'full'] },
|
|
1248
|
+
includeConfigJson: { type: 'boolean' },
|
|
1249
|
+
},
|
|
1250
|
+
},
|
|
1251
|
+
validate_override_profile: {
|
|
1252
|
+
type: 'object',
|
|
1253
|
+
properties: {
|
|
1254
|
+
profileId: { type: 'string' },
|
|
1255
|
+
responseProfile: { type: 'string', enum: ['compact', 'full'] },
|
|
1256
|
+
},
|
|
1257
|
+
},
|
|
1258
|
+
preflight_overrides: {
|
|
1259
|
+
type: 'object',
|
|
1260
|
+
required: ['sessionId'],
|
|
1261
|
+
properties: {
|
|
1262
|
+
sessionId: { type: 'string' },
|
|
1263
|
+
profileId: { type: 'string' },
|
|
1264
|
+
},
|
|
1265
|
+
},
|
|
1266
|
+
observe_override_assets: {
|
|
1267
|
+
type: 'object',
|
|
1268
|
+
required: ['sessionId'],
|
|
1269
|
+
properties: {
|
|
1270
|
+
sessionId: { type: 'string' },
|
|
1271
|
+
tabId: { type: 'number' },
|
|
1272
|
+
includePerformance: { type: 'boolean' },
|
|
1273
|
+
},
|
|
1274
|
+
},
|
|
1275
|
+
capture_override_response_body: {
|
|
1276
|
+
type: 'object',
|
|
1277
|
+
required: ['sessionId'],
|
|
1278
|
+
properties: {
|
|
1279
|
+
sessionId: { type: 'string' },
|
|
1280
|
+
tabId: { type: 'number' },
|
|
1281
|
+
targetUrl: { type: 'string' },
|
|
1282
|
+
targetAssetUrl: { type: 'string' },
|
|
1283
|
+
captureMode: { type: 'string', enum: ['extension-fetch', 'cdp-response'] },
|
|
1284
|
+
triggerReload: { type: 'boolean' },
|
|
1285
|
+
matchMode: { type: 'string', enum: ['exact', 'prefix'] },
|
|
1286
|
+
ruleType: { type: 'string' },
|
|
1287
|
+
requestMethod: { type: 'string' },
|
|
1288
|
+
requestHeaders: { type: 'object' },
|
|
1289
|
+
timeoutMs: { type: 'number' },
|
|
1290
|
+
maxBodyBytes: { type: 'number' },
|
|
1291
|
+
includeBody: { type: 'boolean' },
|
|
1292
|
+
},
|
|
1293
|
+
},
|
|
1294
|
+
list_observed_override_assets: {
|
|
1295
|
+
type: 'object',
|
|
1296
|
+
required: ['sessionId'],
|
|
1297
|
+
properties: {
|
|
1298
|
+
sessionId: { type: 'string' },
|
|
1299
|
+
limit: { type: 'number' },
|
|
1300
|
+
sinceTimestamp: { type: 'number' },
|
|
1301
|
+
responseProfile: { type: 'string', enum: ['compact', 'full'] },
|
|
1302
|
+
},
|
|
1303
|
+
},
|
|
1304
|
+
map_next_override_assets: {
|
|
1305
|
+
type: 'object',
|
|
1306
|
+
required: ['projectRoot'],
|
|
1307
|
+
properties: {
|
|
1308
|
+
sessionId: { type: 'string' },
|
|
1309
|
+
tabId: { type: 'number' },
|
|
1310
|
+
projectRoot: { type: 'string' },
|
|
1311
|
+
nextDir: { type: 'string' },
|
|
1312
|
+
route: { type: 'string' },
|
|
1313
|
+
sourcePaths: { type: 'array', items: { type: 'string' } },
|
|
1314
|
+
observedAssets: { type: 'array', items: { type: 'object' } },
|
|
1315
|
+
maxResults: { type: 'number' },
|
|
1316
|
+
fetchProductionAssets: { type: 'boolean' },
|
|
1317
|
+
productionFetchTimeoutMs: { type: 'number' },
|
|
1318
|
+
maxProductionAssetBytes: { type: 'number' },
|
|
1319
|
+
maxDriftCandidates: { type: 'number' },
|
|
1320
|
+
productionFetchConcurrency: { type: 'number' },
|
|
1321
|
+
},
|
|
1322
|
+
},
|
|
1323
|
+
plan_override_response_patch: {
|
|
689
1324
|
type: 'object',
|
|
690
1325
|
properties: {
|
|
691
1326
|
sessionId: { type: 'string' },
|
|
@@ -862,7 +1497,7 @@ const TOOL_SCHEMAS = {
|
|
|
862
1497
|
properties: {
|
|
863
1498
|
sessionId: { type: 'string' },
|
|
864
1499
|
status: { type: 'string', enum: ['requested', 'started', 'succeeded', 'failed', 'rejected', 'stopped'] },
|
|
865
|
-
action: { type: 'string', enum: ['click', 'input', 'focus', 'blur', 'scroll', 'press_key', 'submit', 'reload'] },
|
|
1500
|
+
action: { type: 'string', enum: ['click', 'hover', 'input', 'focus', 'blur', 'scroll', 'press_key', 'submit', 'reload'] },
|
|
866
1501
|
traceId: { type: 'string' },
|
|
867
1502
|
limit: { type: 'number' },
|
|
868
1503
|
offset: { type: 'number' },
|
|
@@ -885,16 +1520,53 @@ const TOOL_SCHEMAS = {
|
|
|
885
1520
|
required: ['sessionId', 'action'],
|
|
886
1521
|
properties: {
|
|
887
1522
|
sessionId: { type: 'string' },
|
|
888
|
-
action: { type: 'string', enum: ['click', 'input', 'focus', 'blur', 'scroll', 'press_key', 'submit', 'reload'] },
|
|
1523
|
+
action: { type: 'string', enum: ['click', 'hover', 'input', 'focus', 'blur', 'scroll', 'press_key', 'submit', 'reload'] },
|
|
889
1524
|
traceId: { type: 'string' },
|
|
890
1525
|
target: {
|
|
891
1526
|
type: 'object',
|
|
892
1527
|
properties: {
|
|
893
1528
|
selector: { type: 'string' },
|
|
894
1529
|
elementRef: { type: 'string' },
|
|
1530
|
+
coordinates: {
|
|
1531
|
+
type: 'object',
|
|
1532
|
+
properties: {
|
|
1533
|
+
x: { type: 'number' },
|
|
1534
|
+
y: { type: 'number' },
|
|
1535
|
+
frameId: { type: 'number' },
|
|
1536
|
+
},
|
|
1537
|
+
},
|
|
895
1538
|
tabId: { type: 'number' },
|
|
896
1539
|
frameId: { type: 'number' },
|
|
897
1540
|
url: { type: 'string' },
|
|
1541
|
+
locator: ACTION_LOCATOR_TOOL_SCHEMA,
|
|
1542
|
+
frameUrlContains: { type: 'string' },
|
|
1543
|
+
frameTitleContains: { type: 'string' },
|
|
1544
|
+
testId: { type: 'string' },
|
|
1545
|
+
scope: { type: 'string', enum: ['buttons', 'links', 'inputs', 'modals', 'focused'] },
|
|
1546
|
+
textContains: { type: 'string' },
|
|
1547
|
+
labelContains: { type: 'string' },
|
|
1548
|
+
titleContains: { type: 'string' },
|
|
1549
|
+
role: { type: 'string' },
|
|
1550
|
+
name: { type: 'string' },
|
|
1551
|
+
placeholder: { type: 'string' },
|
|
1552
|
+
altText: { type: 'string' },
|
|
1553
|
+
exact: { type: 'boolean' },
|
|
1554
|
+
nth: { type: 'number' },
|
|
1555
|
+
first: { type: 'boolean' },
|
|
1556
|
+
last: { type: 'boolean' },
|
|
1557
|
+
strict: { type: 'boolean' },
|
|
1558
|
+
tagName: { type: 'string' },
|
|
1559
|
+
type: { type: 'string' },
|
|
1560
|
+
visible: { type: 'boolean' },
|
|
1561
|
+
enabled: { type: 'boolean' },
|
|
1562
|
+
disabled: { type: 'boolean' },
|
|
1563
|
+
editable: { type: 'boolean' },
|
|
1564
|
+
checked: { type: 'boolean' },
|
|
1565
|
+
selected: { type: 'boolean' },
|
|
1566
|
+
pressed: { type: 'boolean' },
|
|
1567
|
+
expanded: { type: 'boolean' },
|
|
1568
|
+
readOnly: { type: 'boolean' },
|
|
1569
|
+
requiredField: { type: 'boolean' },
|
|
898
1570
|
},
|
|
899
1571
|
},
|
|
900
1572
|
input: { type: 'object' },
|
|
@@ -917,14 +1589,22 @@ const TOOL_SCHEMAS = {
|
|
|
917
1589
|
type: 'object',
|
|
918
1590
|
required: ['scope'],
|
|
919
1591
|
properties: {
|
|
920
|
-
scope: { type: 'string', enum: ['buttons', 'inputs', 'modals', 'focused', 'page'] },
|
|
1592
|
+
scope: { type: 'string', enum: ['buttons', 'links', 'inputs', 'modals', 'focused', 'page'] },
|
|
921
1593
|
selector: { type: 'string' },
|
|
922
1594
|
testId: { type: 'string' },
|
|
923
1595
|
textContains: { type: 'string' },
|
|
924
1596
|
labelContains: { type: 'string' },
|
|
925
1597
|
titleContains: { type: 'string' },
|
|
1598
|
+
role: { type: 'string' },
|
|
1599
|
+
name: { type: 'string' },
|
|
1600
|
+
placeholder: { type: 'string' },
|
|
1601
|
+
altText: { type: 'string' },
|
|
1602
|
+
exact: { type: 'boolean' },
|
|
1603
|
+
frameUrlContains: { type: 'string' },
|
|
1604
|
+
frameTitleContains: { type: 'string' },
|
|
926
1605
|
urlContains: { type: 'string' },
|
|
927
1606
|
language: { type: 'string' },
|
|
1607
|
+
visible: { type: 'boolean' },
|
|
928
1608
|
disabled: { type: 'boolean' },
|
|
929
1609
|
selected: { type: 'boolean' },
|
|
930
1610
|
pressed: { type: 'boolean' },
|
|
@@ -961,7 +1641,7 @@ const TOOL_SCHEMAS = {
|
|
|
961
1641
|
properties: {
|
|
962
1642
|
id: { type: 'string' },
|
|
963
1643
|
note: { type: 'string' },
|
|
964
|
-
kind: { type: 'string', enum: ['action', 'waitFor', 'assert'] },
|
|
1644
|
+
kind: { type: 'string', enum: ['action', 'waitFor', 'assert', 'wait'] },
|
|
965
1645
|
action: { type: 'string' },
|
|
966
1646
|
traceId: { type: 'string' },
|
|
967
1647
|
target: {
|
|
@@ -969,16 +1649,37 @@ const TOOL_SCHEMAS = {
|
|
|
969
1649
|
properties: {
|
|
970
1650
|
selector: { type: 'string' },
|
|
971
1651
|
elementRef: { type: 'string' },
|
|
1652
|
+
coordinates: {
|
|
1653
|
+
type: 'object',
|
|
1654
|
+
properties: {
|
|
1655
|
+
x: { type: 'number' },
|
|
1656
|
+
y: { type: 'number' },
|
|
1657
|
+
frameId: { type: 'number' },
|
|
1658
|
+
},
|
|
1659
|
+
},
|
|
972
1660
|
tabId: { type: 'number' },
|
|
973
1661
|
frameId: { type: 'number' },
|
|
974
1662
|
url: { type: 'string' },
|
|
1663
|
+
locator: ACTION_LOCATOR_TOOL_SCHEMA,
|
|
1664
|
+
frameUrlContains: { type: 'string' },
|
|
1665
|
+
frameTitleContains: { type: 'string' },
|
|
975
1666
|
testId: { type: 'string' },
|
|
976
|
-
scope: { type: 'string', enum: ['buttons', 'inputs', 'modals', 'focused'] },
|
|
1667
|
+
scope: { type: 'string', enum: ['buttons', 'links', 'inputs', 'modals', 'focused'] },
|
|
977
1668
|
textContains: { type: 'string' },
|
|
978
1669
|
labelContains: { type: 'string' },
|
|
979
1670
|
titleContains: { type: 'string' },
|
|
1671
|
+
role: { type: 'string' },
|
|
1672
|
+
name: { type: 'string' },
|
|
1673
|
+
placeholder: { type: 'string' },
|
|
1674
|
+
altText: { type: 'string' },
|
|
1675
|
+
exact: { type: 'boolean' },
|
|
1676
|
+
nth: { type: 'number' },
|
|
1677
|
+
first: { type: 'boolean' },
|
|
1678
|
+
last: { type: 'boolean' },
|
|
1679
|
+
strict: { type: 'boolean' },
|
|
980
1680
|
tagName: { type: 'string' },
|
|
981
1681
|
type: { type: 'string' },
|
|
1682
|
+
visible: { type: 'boolean' },
|
|
982
1683
|
disabled: { type: 'boolean' },
|
|
983
1684
|
selected: { type: 'boolean' },
|
|
984
1685
|
pressed: { type: 'boolean' },
|
|
@@ -1012,14 +1713,22 @@ const TOOL_SCHEMAS = {
|
|
|
1012
1713
|
matcher: {
|
|
1013
1714
|
type: 'object',
|
|
1014
1715
|
properties: {
|
|
1015
|
-
scope: { type: 'string', enum: ['buttons', 'inputs', 'modals', 'focused', 'page'] },
|
|
1716
|
+
scope: { type: 'string', enum: ['buttons', 'links', 'inputs', 'modals', 'focused', 'page'] },
|
|
1016
1717
|
selector: { type: 'string' },
|
|
1017
1718
|
testId: { type: 'string' },
|
|
1018
1719
|
textContains: { type: 'string' },
|
|
1019
1720
|
labelContains: { type: 'string' },
|
|
1020
1721
|
titleContains: { type: 'string' },
|
|
1722
|
+
role: { type: 'string' },
|
|
1723
|
+
name: { type: 'string' },
|
|
1724
|
+
placeholder: { type: 'string' },
|
|
1725
|
+
altText: { type: 'string' },
|
|
1726
|
+
exact: { type: 'boolean' },
|
|
1727
|
+
frameUrlContains: { type: 'string' },
|
|
1728
|
+
frameTitleContains: { type: 'string' },
|
|
1021
1729
|
urlContains: { type: 'string' },
|
|
1022
1730
|
language: { type: 'string' },
|
|
1731
|
+
visible: { type: 'boolean' },
|
|
1023
1732
|
disabled: { type: 'boolean' },
|
|
1024
1733
|
selected: { type: 'boolean' },
|
|
1025
1734
|
pressed: { type: 'boolean' },
|
|
@@ -1036,6 +1745,7 @@ const TOOL_SCHEMAS = {
|
|
|
1036
1745
|
pollIntervalMs: { type: 'number' },
|
|
1037
1746
|
},
|
|
1038
1747
|
},
|
|
1748
|
+
wait: AUTOMATION_WAIT_TOOL_SCHEMA,
|
|
1039
1749
|
},
|
|
1040
1750
|
},
|
|
1041
1751
|
},
|
|
@@ -1062,11 +1772,25 @@ const TOOL_DESCRIPTIONS = {
|
|
|
1062
1772
|
get_computed_styles: 'Read computed CSS styles for an element',
|
|
1063
1773
|
get_layout_metrics: 'Read viewport and element layout metrics',
|
|
1064
1774
|
get_page_state: 'Read a compact structured page model for forms, buttons, modals, and viewport state',
|
|
1065
|
-
get_interactive_elements: 'Read compact live element references for buttons, inputs, modals, and focused elements',
|
|
1775
|
+
get_interactive_elements: 'Read compact live element references for buttons, links, inputs, modals, and focused elements',
|
|
1066
1776
|
get_live_session_health: 'Read live transport health and session binding details for one session',
|
|
1067
1777
|
set_viewport: 'Resize the live browser window for a session and return the resulting viewport metrics',
|
|
1068
1778
|
assert_page_state: 'Assert compact page-state conditions without pulling raw DOM payloads',
|
|
1069
1779
|
wait_for_page_state: 'Poll compact page state until a structured assertion becomes true',
|
|
1780
|
+
preflight_automation_flow: 'Check live-session readiness and production risks before running an automation flow',
|
|
1781
|
+
wait_for_url: 'Poll the live page URL until it matches an exact, contains, or regex condition',
|
|
1782
|
+
wait_for_navigation: 'Poll persisted navigation events until a matching URL or trigger is observed',
|
|
1783
|
+
wait_for_navigation_lifecycle: 'Wait for a live navigation lifecycle milestone such as commit, load, or network idle',
|
|
1784
|
+
wait_for_load_state: 'Poll the live page document readiness until domcontentloaded or load is reached',
|
|
1785
|
+
wait_for_selector_state: 'Poll a selector until it is attached, detached, visible, or hidden',
|
|
1786
|
+
wait_for_console: 'Poll live console logs until a matching message appears',
|
|
1787
|
+
wait_for_dialog: 'Wait for a native JavaScript dialog and optionally accept or dismiss it',
|
|
1788
|
+
wait_for_stable_layout: 'Wait until the page or selector layout stays unchanged for a stable window',
|
|
1789
|
+
wait_for_download: 'Wait for a download started by the bound tab and optionally until completion',
|
|
1790
|
+
wait_for_popup: 'Wait for a popup tab or window opened from the bound session tab',
|
|
1791
|
+
wait_for_network_quiet: 'Wait until persisted network activity is quiet for a bounded window',
|
|
1792
|
+
wait_for_request: 'Poll persisted network activity until a matching request is observed',
|
|
1793
|
+
wait_for_response: 'Poll persisted network activity until a matching response is observed',
|
|
1070
1794
|
capture_ui_snapshot: 'Capture redacted UI snapshot (DOM/styles/optional PNG) and persist it',
|
|
1071
1795
|
get_live_console_logs: 'Read in-memory live console logs for a connected session',
|
|
1072
1796
|
list_override_profiles: 'List configured browser override profiles',
|
|
@@ -1113,6 +1837,7 @@ const MAX_BODY_CHUNK_BYTES = 256 * 1024;
|
|
|
1113
1837
|
const DEFAULT_NETWORK_POLL_TIMEOUT_MS = 15_000;
|
|
1114
1838
|
const MAX_NETWORK_POLL_TIMEOUT_MS = 120_000;
|
|
1115
1839
|
const DEFAULT_NETWORK_POLL_INTERVAL_MS = 250;
|
|
1840
|
+
const DEFAULT_AUTOMATION_WAIT_LOOKBACK_MS = 5_000;
|
|
1116
1841
|
const LIVE_SESSION_DISCONNECTED_CODE = 'LIVE_SESSION_DISCONNECTED';
|
|
1117
1842
|
const OVERRIDE_LIVE_COMMAND_TIMEOUT_CODE = 'OVERRIDE_LIVE_COMMAND_TIMEOUT';
|
|
1118
1843
|
const OVERRIDE_LIVE_COMMAND_FAILED_CODE = 'OVERRIDE_LIVE_COMMAND_FAILED';
|
|
@@ -1395,7 +2120,7 @@ function buildOverrideProfileRecords() {
|
|
|
1395
2120
|
active: profile.profileId === summary.activeProfileId,
|
|
1396
2121
|
configEnabled: summary.configEnabled,
|
|
1397
2122
|
enabled: profile.enabled,
|
|
1398
|
-
effectiveEnabled:
|
|
2123
|
+
effectiveEnabled: profile.enabled && profile.enabledRuleCount > 0,
|
|
1399
2124
|
autoReload: profile.autoReload,
|
|
1400
2125
|
configPath: summary.configPath,
|
|
1401
2126
|
fileExists: profile.fileExists,
|
|
@@ -1414,6 +2139,72 @@ function resolveOverrideProfileRecord(value) {
|
|
|
1414
2139
|
}
|
|
1415
2140
|
return profile;
|
|
1416
2141
|
}
|
|
2142
|
+
function resolveOverrideResponseProfile(value) {
|
|
2143
|
+
return value === 'full' ? 'full' : 'compact';
|
|
2144
|
+
}
|
|
2145
|
+
function compactOverrideRule(rule) {
|
|
2146
|
+
if (!isRecord(rule)) {
|
|
2147
|
+
return {};
|
|
2148
|
+
}
|
|
2149
|
+
return {
|
|
2150
|
+
ruleId: rule.ruleId,
|
|
2151
|
+
enabled: rule.enabled,
|
|
2152
|
+
ruleType: rule.ruleType,
|
|
2153
|
+
requestMethod: rule.requestMethod,
|
|
2154
|
+
matchMode: rule.matchMode,
|
|
2155
|
+
targetAssetUrl: rule.targetAssetUrl,
|
|
2156
|
+
localFilePath: rule.localFilePath,
|
|
2157
|
+
contentType: rule.contentType,
|
|
2158
|
+
fileExists: rule.fileExists,
|
|
2159
|
+
integrity: rule.integrity,
|
|
2160
|
+
};
|
|
2161
|
+
}
|
|
2162
|
+
function compactOverrideProfile(profile, ruleLimit = 10) {
|
|
2163
|
+
const rules = Array.isArray(profile.rules) ? profile.rules : [];
|
|
2164
|
+
return {
|
|
2165
|
+
profileId: profile.profileId,
|
|
2166
|
+
name: profile.name,
|
|
2167
|
+
active: profile.active,
|
|
2168
|
+
configEnabled: profile.configEnabled,
|
|
2169
|
+
enabled: profile.enabled,
|
|
2170
|
+
effectiveEnabled: profile.effectiveEnabled,
|
|
2171
|
+
autoReload: profile.autoReload,
|
|
2172
|
+
configPath: profile.configPath,
|
|
2173
|
+
fileExists: profile.fileExists,
|
|
2174
|
+
ruleCount: profile.ruleCount,
|
|
2175
|
+
enabledRuleCount: profile.enabledRuleCount,
|
|
2176
|
+
rules: rules.slice(0, ruleLimit).map(compactOverrideRule),
|
|
2177
|
+
rulesOmitted: Math.max(0, rules.length - ruleLimit),
|
|
2178
|
+
};
|
|
2179
|
+
}
|
|
2180
|
+
function serializeOverrideProfile(profile, responseProfile) {
|
|
2181
|
+
return responseProfile === 'full' ? profile : compactOverrideProfile(profile);
|
|
2182
|
+
}
|
|
2183
|
+
function compactObservedOverrideAsset(asset) {
|
|
2184
|
+
if (!isRecord(asset)) {
|
|
2185
|
+
return {};
|
|
2186
|
+
}
|
|
2187
|
+
return {
|
|
2188
|
+
observedAssetId: asset.observedAssetId,
|
|
2189
|
+
lastSeenAt: asset.lastSeenAt,
|
|
2190
|
+
tabId: asset.tabId,
|
|
2191
|
+
url: asset.url,
|
|
2192
|
+
ruleType: asset.ruleType,
|
|
2193
|
+
requestMethod: asset.requestMethod,
|
|
2194
|
+
resourceType: asset.resourceType,
|
|
2195
|
+
contentType: asset.contentType,
|
|
2196
|
+
statusCode: asset.statusCode,
|
|
2197
|
+
pathname: asset.pathname,
|
|
2198
|
+
assetPath: asset.assetPath,
|
|
2199
|
+
kind: asset.kind,
|
|
2200
|
+
integrity: asset.integrity,
|
|
2201
|
+
fromDom: asset.fromDom,
|
|
2202
|
+
fromPerformance: asset.fromPerformance,
|
|
2203
|
+
fromNavigation: asset.fromNavigation,
|
|
2204
|
+
fromFetch: asset.fromFetch,
|
|
2205
|
+
serviceWorkerControlled: asset.serviceWorkerControlled,
|
|
2206
|
+
};
|
|
2207
|
+
}
|
|
1417
2208
|
const SHA256_HEX_PATTERN = /^[a-f0-9]{64}$/;
|
|
1418
2209
|
function sha256Text(value) {
|
|
1419
2210
|
return createHash('sha256').update(value, 'utf8').digest('hex');
|
|
@@ -1582,13 +2373,6 @@ function buildOverrideProfileIssues(profile) {
|
|
|
1582
2373
|
const rules = Array.isArray(profile.rules)
|
|
1583
2374
|
? profile.rules.filter((rule) => isRecord(rule))
|
|
1584
2375
|
: [];
|
|
1585
|
-
if (profile.configEnabled !== true) {
|
|
1586
|
-
issues.push({
|
|
1587
|
-
code: 'CONFIG_DISABLED',
|
|
1588
|
-
severity: 'warning',
|
|
1589
|
-
message: 'The override config is disabled and cannot replace requests until enabled.',
|
|
1590
|
-
});
|
|
1591
|
-
}
|
|
1592
2376
|
if (profile.enabled !== true) {
|
|
1593
2377
|
issues.push({
|
|
1594
2378
|
code: 'PROFILE_DISABLED',
|
|
@@ -1677,12 +2461,6 @@ function buildOverrideProfileNextActions(profile, issues) {
|
|
|
1677
2461
|
message: 'Regenerate the RSC rule with plan_override_response_patch from a captured text/x-component response body.',
|
|
1678
2462
|
}];
|
|
1679
2463
|
}
|
|
1680
|
-
if (profile.configEnabled !== true) {
|
|
1681
|
-
return [{
|
|
1682
|
-
code: 'ENABLE_CONFIG',
|
|
1683
|
-
message: 'Set the root override config enabled=true after reviewing the profile.',
|
|
1684
|
-
}];
|
|
1685
|
-
}
|
|
1686
2464
|
if (profile.enabled !== true) {
|
|
1687
2465
|
return [{
|
|
1688
2466
|
code: 'ENABLE_PROFILE',
|
|
@@ -2558,6 +3336,7 @@ function mapAutomationRunRecord(row) {
|
|
|
2558
3336
|
: undefined,
|
|
2559
3337
|
stopReason: row.stop_reason ?? undefined,
|
|
2560
3338
|
target: parseJsonOrUndefined(row.target_summary_json),
|
|
3339
|
+
diagnostics: parseJsonOrUndefined(row.diagnostics_json),
|
|
2561
3340
|
failure: parseJsonOrUndefined(row.failure_json),
|
|
2562
3341
|
redaction: parseJsonOrUndefined(row.redaction_json),
|
|
2563
3342
|
stepCount: row.step_count,
|
|
@@ -2582,6 +3361,7 @@ function mapAutomationStepRecord(row) {
|
|
|
2582
3361
|
durationMs: row.duration_ms ?? undefined,
|
|
2583
3362
|
tabId: row.tab_id ?? undefined,
|
|
2584
3363
|
target: parseJsonOrUndefined(row.target_summary_json),
|
|
3364
|
+
diagnostics: parseJsonOrUndefined(row.diagnostics_json),
|
|
2585
3365
|
redaction: parseJsonOrUndefined(row.redaction_json),
|
|
2586
3366
|
failure: parseJsonOrUndefined(row.failure_json),
|
|
2587
3367
|
inputMetadata: parseJsonOrUndefined(row.input_metadata_json),
|
|
@@ -2592,6 +3372,109 @@ function mapAutomationStepRecord(row) {
|
|
|
2592
3372
|
source: 'automation_steps',
|
|
2593
3373
|
};
|
|
2594
3374
|
}
|
|
3375
|
+
function asRecordOrUndefined(value) {
|
|
3376
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
3377
|
+
? value
|
|
3378
|
+
: undefined;
|
|
3379
|
+
}
|
|
3380
|
+
function buildFailureEvidenceSummary(failureEvidence) {
|
|
3381
|
+
if (!failureEvidence) {
|
|
3382
|
+
return undefined;
|
|
3383
|
+
}
|
|
3384
|
+
const snapshot = asRecordOrUndefined(failureEvidence.snapshot);
|
|
3385
|
+
const snapshotRoot = asRecordOrUndefined(snapshot?.snapshot);
|
|
3386
|
+
return {
|
|
3387
|
+
captured: failureEvidence.captured === true,
|
|
3388
|
+
error: typeof failureEvidence.error === 'string' ? failureEvidence.error : undefined,
|
|
3389
|
+
limitsApplied: asRecordOrUndefined(failureEvidence.limitsApplied),
|
|
3390
|
+
snapshot: snapshot
|
|
3391
|
+
? {
|
|
3392
|
+
timestamp: typeof snapshot.timestamp === 'number' ? snapshot.timestamp : undefined,
|
|
3393
|
+
trigger: typeof snapshot.trigger === 'string' ? snapshot.trigger : undefined,
|
|
3394
|
+
selector: typeof snapshot.selector === 'string' ? snapshot.selector : undefined,
|
|
3395
|
+
url: typeof snapshot.url === 'string' ? snapshot.url : undefined,
|
|
3396
|
+
mode: snapshot.mode,
|
|
3397
|
+
hasDom: Boolean(snapshotRoot && 'dom' in snapshotRoot),
|
|
3398
|
+
hasStyles: Boolean(snapshotRoot && 'styles' in snapshotRoot),
|
|
3399
|
+
hasPng: Boolean(snapshot.png),
|
|
3400
|
+
}
|
|
3401
|
+
: undefined,
|
|
3402
|
+
};
|
|
3403
|
+
}
|
|
3404
|
+
function findRelatedFailureSnapshot(db, sessionId, failureEvidence) {
|
|
3405
|
+
const snapshotSummary = asRecordOrUndefined(buildFailureEvidenceSummary(failureEvidence)?.snapshot);
|
|
3406
|
+
if (!snapshotSummary) {
|
|
3407
|
+
return undefined;
|
|
3408
|
+
}
|
|
3409
|
+
const timestamp = typeof snapshotSummary.timestamp === 'number' ? snapshotSummary.timestamp : undefined;
|
|
3410
|
+
const selector = typeof snapshotSummary.selector === 'string' ? snapshotSummary.selector : undefined;
|
|
3411
|
+
const url = typeof snapshotSummary.url === 'string' ? snapshotSummary.url : undefined;
|
|
3412
|
+
const where = ['session_id = ?'];
|
|
3413
|
+
const params = [sessionId];
|
|
3414
|
+
if (selector) {
|
|
3415
|
+
where.push('selector = ?');
|
|
3416
|
+
params.push(selector);
|
|
3417
|
+
}
|
|
3418
|
+
if (url) {
|
|
3419
|
+
where.push('url = ?');
|
|
3420
|
+
params.push(url);
|
|
3421
|
+
}
|
|
3422
|
+
if (timestamp !== undefined) {
|
|
3423
|
+
where.push('ts BETWEEN ? AND ?');
|
|
3424
|
+
params.push(timestamp - 10_000, timestamp + 10_000);
|
|
3425
|
+
}
|
|
3426
|
+
const row = db.prepare(`SELECT snapshot_id, trigger_event_id, ts, selector, url
|
|
3427
|
+
FROM snapshots
|
|
3428
|
+
WHERE ${where.join(' AND ')}
|
|
3429
|
+
ORDER BY ${timestamp !== undefined ? 'ABS(ts - ?) ASC,' : ''} ts DESC
|
|
3430
|
+
LIMIT 1`).get(...params, ...(timestamp !== undefined ? [timestamp] : []));
|
|
3431
|
+
if (!row) {
|
|
3432
|
+
return undefined;
|
|
3433
|
+
}
|
|
3434
|
+
return {
|
|
3435
|
+
snapshotId: row.snapshot_id,
|
|
3436
|
+
triggerEventId: row.trigger_event_id ?? undefined,
|
|
3437
|
+
timestamp: row.ts,
|
|
3438
|
+
selector: row.selector ?? undefined,
|
|
3439
|
+
url: row.url ?? undefined,
|
|
3440
|
+
};
|
|
3441
|
+
}
|
|
3442
|
+
function mergeAutomationDiagnosticsEvidence(db, options) {
|
|
3443
|
+
if (!options.traceId) {
|
|
3444
|
+
return;
|
|
3445
|
+
}
|
|
3446
|
+
const failureEvidence = buildFailureEvidenceSummary(options.failureEvidence);
|
|
3447
|
+
const linkedSnapshot = findRelatedFailureSnapshot(db, options.sessionId, options.failureEvidence);
|
|
3448
|
+
if (!failureEvidence && !linkedSnapshot && !options.cdpFailure) {
|
|
3449
|
+
return;
|
|
3450
|
+
}
|
|
3451
|
+
const updateDiagnosticsJson = (tableName, keyColumn, keyValue, existingJson) => {
|
|
3452
|
+
const existing = asRecordOrUndefined(parseJsonOrUndefined(existingJson)) ?? {};
|
|
3453
|
+
const merged = {
|
|
3454
|
+
...existing,
|
|
3455
|
+
...(options.cdpFailure ? { cdpFailure: options.cdpFailure } : {}),
|
|
3456
|
+
...(failureEvidence ? { failureEvidence } : {}),
|
|
3457
|
+
...(linkedSnapshot ? { linkedSnapshot } : {}),
|
|
3458
|
+
};
|
|
3459
|
+
db.prepare(`UPDATE ${tableName} SET diagnostics_json = ?, updated_at = ? WHERE ${keyColumn} = ?`).run(JSON.stringify(merged), Date.now(), keyValue);
|
|
3460
|
+
};
|
|
3461
|
+
const runRow = db.prepare(`SELECT run_id, diagnostics_json
|
|
3462
|
+
FROM automation_runs
|
|
3463
|
+
WHERE session_id = ? AND trace_id = ?
|
|
3464
|
+
ORDER BY started_at DESC, updated_at DESC
|
|
3465
|
+
LIMIT 1`).get(options.sessionId, options.traceId);
|
|
3466
|
+
if (runRow) {
|
|
3467
|
+
updateDiagnosticsJson('automation_runs', 'run_id', runRow.run_id, runRow.diagnostics_json);
|
|
3468
|
+
}
|
|
3469
|
+
const stepRow = db.prepare(`SELECT step_id, diagnostics_json
|
|
3470
|
+
FROM automation_steps
|
|
3471
|
+
WHERE session_id = ? AND trace_id = ?
|
|
3472
|
+
ORDER BY step_order DESC, updated_at DESC
|
|
3473
|
+
LIMIT 1`).get(options.sessionId, options.traceId);
|
|
3474
|
+
if (stepRow) {
|
|
3475
|
+
updateDiagnosticsJson('automation_steps', 'step_id', stepRow.step_id, stepRow.diagnostics_json);
|
|
3476
|
+
}
|
|
3477
|
+
}
|
|
2595
3478
|
function formatUrlPath(url) {
|
|
2596
3479
|
try {
|
|
2597
3480
|
const parsed = new URL(url);
|
|
@@ -2724,15 +3607,25 @@ function resolveViewportDimension(value, axis) {
|
|
|
2724
3607
|
}
|
|
2725
3608
|
return floored;
|
|
2726
3609
|
}
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
3610
|
+
function buildWaitTimeoutDiagnostics(options) {
|
|
3611
|
+
const diagnostics = {
|
|
3612
|
+
waitKind: options.waitKind,
|
|
3613
|
+
timeoutMs: options.timeoutMs,
|
|
3614
|
+
waitedMs: options.waitedMs,
|
|
3615
|
+
attempts: options.attempts,
|
|
3616
|
+
pollIntervalMs: options.pollIntervalMs,
|
|
3617
|
+
matcherSummary: options.matcherSummary,
|
|
3618
|
+
};
|
|
3619
|
+
if (options.lastObserved !== undefined) {
|
|
3620
|
+
diagnostics.lastObserved = options.lastObserved;
|
|
2735
3621
|
}
|
|
3622
|
+
if (typeof options.candidateCount === 'number') {
|
|
3623
|
+
diagnostics.candidateCount = options.candidateCount;
|
|
3624
|
+
}
|
|
3625
|
+
if (Array.isArray(options.sampledCandidates) && options.sampledCandidates.length > 0) {
|
|
3626
|
+
diagnostics.sampledCandidates = options.sampledCandidates;
|
|
3627
|
+
}
|
|
3628
|
+
return diagnostics;
|
|
2736
3629
|
}
|
|
2737
3630
|
function resolveOptionalMatcherString(value) {
|
|
2738
3631
|
if (typeof value !== 'string') {
|
|
@@ -2755,10 +3648,10 @@ function resolveOptionalMatcherCount(value, field) {
|
|
|
2755
3648
|
return floored;
|
|
2756
3649
|
}
|
|
2757
3650
|
function resolvePageStateScope(value) {
|
|
2758
|
-
if (value === 'buttons' || value === 'inputs' || value === 'modals' || value === 'focused' || value === 'page') {
|
|
3651
|
+
if (value === 'buttons' || value === 'links' || value === 'inputs' || value === 'modals' || value === 'focused' || value === 'page') {
|
|
2759
3652
|
return value;
|
|
2760
3653
|
}
|
|
2761
|
-
throw new Error('scope must be one of buttons, inputs, modals, focused, or page');
|
|
3654
|
+
throw new Error('scope must be one of buttons, links, inputs, modals, focused, or page');
|
|
2762
3655
|
}
|
|
2763
3656
|
function resolvePageStateMatcher(input) {
|
|
2764
3657
|
const matcher = {
|
|
@@ -2768,6 +3661,13 @@ function resolvePageStateMatcher(input) {
|
|
|
2768
3661
|
textContains: resolveOptionalMatcherString(input.textContains),
|
|
2769
3662
|
labelContains: resolveOptionalMatcherString(input.labelContains),
|
|
2770
3663
|
titleContains: resolveOptionalMatcherString(input.titleContains),
|
|
3664
|
+
role: resolveOptionalMatcherString(input.role)?.toLowerCase(),
|
|
3665
|
+
name: resolveOptionalMatcherString(input.name),
|
|
3666
|
+
placeholder: resolveOptionalMatcherString(input.placeholder),
|
|
3667
|
+
altText: resolveOptionalMatcherString(input.altText),
|
|
3668
|
+
exact: resolveOptionalMatcherBoolean(input.exact),
|
|
3669
|
+
frameUrlContains: resolveOptionalMatcherString(input.frameUrlContains),
|
|
3670
|
+
frameTitleContains: resolveOptionalMatcherString(input.frameTitleContains),
|
|
2771
3671
|
urlContains: resolveOptionalMatcherString(input.urlContains),
|
|
2772
3672
|
language: resolveOptionalMatcherString(input.language),
|
|
2773
3673
|
disabled: resolveOptionalMatcherBoolean(input.disabled),
|
|
@@ -2778,6 +3678,7 @@ function resolvePageStateMatcher(input) {
|
|
|
2778
3678
|
requiredField: resolveOptionalMatcherBoolean(input.requiredField),
|
|
2779
3679
|
tagName: resolveOptionalMatcherString(input.tagName)?.toLowerCase(),
|
|
2780
3680
|
type: resolveOptionalMatcherString(input.type)?.toLowerCase(),
|
|
3681
|
+
visible: resolveOptionalMatcherBoolean(input.visible),
|
|
2781
3682
|
countExactly: resolveOptionalMatcherCount(input.countExactly, 'countExactly'),
|
|
2782
3683
|
countAtLeast: resolveOptionalMatcherCount(input.countAtLeast, 'countAtLeast'),
|
|
2783
3684
|
};
|
|
@@ -2792,6 +3693,19 @@ function includesNormalized(value, needle) {
|
|
|
2792
3693
|
}
|
|
2793
3694
|
return typeof value === 'string' && value.toLowerCase().includes(needle.toLowerCase());
|
|
2794
3695
|
}
|
|
3696
|
+
function matchesTextValue(value, expected, exact) {
|
|
3697
|
+
if (!expected) {
|
|
3698
|
+
return true;
|
|
3699
|
+
}
|
|
3700
|
+
if (typeof value !== 'string') {
|
|
3701
|
+
return false;
|
|
3702
|
+
}
|
|
3703
|
+
const normalizedValue = value.trim().toLowerCase();
|
|
3704
|
+
const normalizedExpected = expected.trim().toLowerCase();
|
|
3705
|
+
return exact === true
|
|
3706
|
+
? normalizedValue === normalizedExpected
|
|
3707
|
+
: normalizedValue.includes(normalizedExpected);
|
|
3708
|
+
}
|
|
2795
3709
|
function equalsNormalized(value, expected) {
|
|
2796
3710
|
if (!expected) {
|
|
2797
3711
|
return true;
|
|
@@ -2805,7 +3719,7 @@ function equalsOptionalBoolean(value, expected) {
|
|
|
2805
3719
|
return value === expected;
|
|
2806
3720
|
}
|
|
2807
3721
|
function pickPageStateScopeItems(payload, scope) {
|
|
2808
|
-
if (scope === 'buttons' || scope === 'inputs' || scope === 'modals') {
|
|
3722
|
+
if (scope === 'buttons' || scope === 'links' || scope === 'inputs' || scope === 'modals') {
|
|
2809
3723
|
const value = payload[scope];
|
|
2810
3724
|
return asRecordArray(value);
|
|
2811
3725
|
}
|
|
@@ -2818,13 +3732,20 @@ function pickPageStateScopeItems(payload, scope) {
|
|
|
2818
3732
|
function matchesPageStateItem(item, matcher) {
|
|
2819
3733
|
return (includesNormalized(item.selector, matcher.selector)
|
|
2820
3734
|
&& equalsNormalized(item.testId, matcher.testId)
|
|
2821
|
-
&&
|
|
2822
|
-
&&
|
|
2823
|
-
&&
|
|
3735
|
+
&& matchesTextValue(item.text, matcher.textContains, matcher.exact)
|
|
3736
|
+
&& matchesTextValue(item.label, matcher.labelContains, matcher.exact)
|
|
3737
|
+
&& matchesTextValue(item.title, matcher.titleContains, matcher.exact)
|
|
3738
|
+
&& equalsNormalized(item.role, matcher.role)
|
|
3739
|
+
&& matchesTextValue(item.name, matcher.name, matcher.exact)
|
|
3740
|
+
&& matchesTextValue(item.placeholder, matcher.placeholder, matcher.exact)
|
|
3741
|
+
&& matchesTextValue(item.altText, matcher.altText, matcher.exact)
|
|
3742
|
+
&& includesNormalized(item.frameUrl, matcher.frameUrlContains)
|
|
3743
|
+
&& includesNormalized(item.frameTitle, matcher.frameTitleContains)
|
|
2824
3744
|
&& includesNormalized(item.url, matcher.urlContains)
|
|
2825
3745
|
&& equalsNormalized(item.language, matcher.language)
|
|
2826
3746
|
&& equalsNormalized(item.tagName, matcher.tagName)
|
|
2827
3747
|
&& equalsNormalized(item.type, matcher.type)
|
|
3748
|
+
&& equalsOptionalBoolean(item.visible, matcher.visible)
|
|
2828
3749
|
&& equalsOptionalBoolean(item.disabled, matcher.disabled)
|
|
2829
3750
|
&& equalsOptionalBoolean(item.selected, matcher.selected)
|
|
2830
3751
|
&& equalsOptionalBoolean(item.pressed, matcher.pressed)
|
|
@@ -2881,7 +3802,7 @@ function createPageChangeSummary(previousCapture, currentCapture) {
|
|
|
2881
3802
|
const previousSummary = previous?.summary;
|
|
2882
3803
|
const currentSummary = current.summary;
|
|
2883
3804
|
const summaryDelta = {};
|
|
2884
|
-
for (const key of ['buttons', 'inputs', 'modals']) {
|
|
3805
|
+
for (const key of ['buttons', 'links', 'inputs', 'modals']) {
|
|
2885
3806
|
const previousValue = typeof previousSummary?.[key] === 'number' ? previousSummary[key] : undefined;
|
|
2886
3807
|
const currentValue = typeof currentSummary?.[key] === 'number' ? currentSummary[key] : undefined;
|
|
2887
3808
|
if (previousValue !== currentValue && currentValue !== undefined) {
|
|
@@ -2910,13 +3831,13 @@ function createPageChangeSummary(previousCapture, currentCapture) {
|
|
|
2910
3831
|
}
|
|
2911
3832
|
function resolveInteractiveKinds(value) {
|
|
2912
3833
|
if (!Array.isArray(value) || value.length === 0) {
|
|
2913
|
-
return ['buttons', 'inputs', 'modals', 'focused'];
|
|
3834
|
+
return ['buttons', 'links', 'inputs', 'modals', 'focused'];
|
|
2914
3835
|
}
|
|
2915
|
-
const allowed = new Set(['buttons', 'inputs', 'modals', 'focused']);
|
|
3836
|
+
const allowed = new Set(['buttons', 'links', 'inputs', 'modals', 'focused']);
|
|
2916
3837
|
const kinds = value
|
|
2917
3838
|
.filter((entry) => typeof entry === 'string' && allowed.has(entry))
|
|
2918
3839
|
.map((entry) => entry);
|
|
2919
|
-
return kinds.length > 0 ? Array.from(new Set(kinds)) : ['buttons', 'inputs', 'modals', 'focused'];
|
|
3840
|
+
return kinds.length > 0 ? Array.from(new Set(kinds)) : ['buttons', 'links', 'inputs', 'modals', 'focused'];
|
|
2920
3841
|
}
|
|
2921
3842
|
function collectInteractiveElementRefs(payload, kinds, maxItems) {
|
|
2922
3843
|
const refs = [];
|
|
@@ -3036,132 +3957,1345 @@ async function waitForPageStateCondition(sessionId, input, capturePageState) {
|
|
|
3036
3957
|
const { lastCapture: _lastCapture, ...waited } = detailed;
|
|
3037
3958
|
return waited;
|
|
3038
3959
|
}
|
|
3039
|
-
function
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3960
|
+
function compileWaitRegex(value, fieldName) {
|
|
3961
|
+
if (!value) {
|
|
3962
|
+
return undefined;
|
|
3963
|
+
}
|
|
3964
|
+
try {
|
|
3965
|
+
return new RegExp(value);
|
|
3966
|
+
}
|
|
3967
|
+
catch {
|
|
3968
|
+
throw new Error(`${fieldName} must be a valid regular expression`);
|
|
3969
|
+
}
|
|
3970
|
+
}
|
|
3971
|
+
function matchesUrlWait(url, wait) {
|
|
3972
|
+
return matchesUrlPredicates(url, {
|
|
3973
|
+
exactUrl: wait.exactUrl,
|
|
3974
|
+
urlContains: wait.urlContains,
|
|
3975
|
+
urlRegex: wait.urlRegex,
|
|
3976
|
+
});
|
|
3977
|
+
}
|
|
3978
|
+
function matchesUrlPredicates(url, predicates) {
|
|
3979
|
+
if (typeof url !== 'string') {
|
|
3980
|
+
return false;
|
|
3981
|
+
}
|
|
3982
|
+
if (predicates.exactUrl && url !== predicates.exactUrl) {
|
|
3983
|
+
return false;
|
|
3984
|
+
}
|
|
3985
|
+
if (predicates.urlContains && !url.includes(predicates.urlContains)) {
|
|
3986
|
+
return false;
|
|
3987
|
+
}
|
|
3988
|
+
const regex = compileWaitRegex(predicates.urlRegex, predicates.regexFieldName ?? 'urlRegex');
|
|
3989
|
+
if (regex && !regex.test(url)) {
|
|
3990
|
+
return false;
|
|
3991
|
+
}
|
|
3992
|
+
return true;
|
|
3993
|
+
}
|
|
3994
|
+
function resolveAutomationWaitSinceTs(value) {
|
|
3995
|
+
return resolveOptionalTimestamp(value) ?? Math.max(0, Date.now() - DEFAULT_AUTOMATION_WAIT_LOOKBACK_MS);
|
|
3996
|
+
}
|
|
3997
|
+
function isElementMissingError(error) {
|
|
3998
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3999
|
+
return /no element found for selector/i.test(message);
|
|
3044
4000
|
}
|
|
3045
|
-
function
|
|
4001
|
+
async function captureSelectorState(captureClient, sessionId, selector, frameId = 0) {
|
|
4002
|
+
try {
|
|
4003
|
+
const [styleCapture, layoutCapture] = await Promise.all([
|
|
4004
|
+
executeLiveCapture(captureClient, sessionId, 'CAPTURE_COMPUTED_STYLES', { selector, frameId, properties: ['display', 'visibility', 'opacity'] }, 3_000),
|
|
4005
|
+
executeLiveCapture(captureClient, sessionId, 'CAPTURE_LAYOUT_METRICS', { selector, frameId }, 3_000),
|
|
4006
|
+
]);
|
|
4007
|
+
const stylePayload = ensureCaptureSuccess(styleCapture, sessionId);
|
|
4008
|
+
const layoutPayload = ensureCaptureSuccess(layoutCapture, sessionId);
|
|
4009
|
+
const properties = isRecord(stylePayload.properties) ? stylePayload.properties : {};
|
|
4010
|
+
const element = isRecord(layoutPayload.element) ? layoutPayload.element : {};
|
|
4011
|
+
const width = typeof element.width === 'number' ? element.width : 0;
|
|
4012
|
+
const height = typeof element.height === 'number' ? element.height : 0;
|
|
4013
|
+
const display = typeof properties.display === 'string' ? properties.display : undefined;
|
|
4014
|
+
const visibility = typeof properties.visibility === 'string' ? properties.visibility : undefined;
|
|
4015
|
+
const opacityText = typeof properties.opacity === 'string' ? properties.opacity : undefined;
|
|
4016
|
+
const opacity = opacityText !== undefined ? Number.parseFloat(opacityText) : undefined;
|
|
4017
|
+
const visible = display !== 'none'
|
|
4018
|
+
&& visibility !== 'hidden'
|
|
4019
|
+
&& visibility !== 'collapse'
|
|
4020
|
+
&& opacity !== 0
|
|
4021
|
+
&& width > 0
|
|
4022
|
+
&& height > 0;
|
|
4023
|
+
return {
|
|
4024
|
+
selector,
|
|
4025
|
+
frameId,
|
|
4026
|
+
attached: true,
|
|
4027
|
+
visible,
|
|
4028
|
+
styles: properties,
|
|
4029
|
+
element,
|
|
4030
|
+
viewport: layoutPayload.viewport,
|
|
4031
|
+
};
|
|
4032
|
+
}
|
|
4033
|
+
catch (error) {
|
|
4034
|
+
if (isElementMissingError(error)) {
|
|
4035
|
+
return {
|
|
4036
|
+
selector,
|
|
4037
|
+
frameId,
|
|
4038
|
+
attached: false,
|
|
4039
|
+
visible: false,
|
|
4040
|
+
missing: true,
|
|
4041
|
+
message: error instanceof Error ? error.message : String(error),
|
|
4042
|
+
};
|
|
4043
|
+
}
|
|
4044
|
+
throw error;
|
|
4045
|
+
}
|
|
4046
|
+
}
|
|
4047
|
+
function selectorStateMatches(state, expectedState) {
|
|
4048
|
+
const attached = state.attached === true;
|
|
4049
|
+
const visible = state.visible === true;
|
|
4050
|
+
switch (expectedState) {
|
|
4051
|
+
case 'attached':
|
|
4052
|
+
return attached;
|
|
4053
|
+
case 'detached':
|
|
4054
|
+
return !attached;
|
|
4055
|
+
case 'visible':
|
|
4056
|
+
return attached && visible;
|
|
4057
|
+
case 'hidden':
|
|
4058
|
+
return !attached || !visible;
|
|
4059
|
+
}
|
|
4060
|
+
}
|
|
4061
|
+
async function waitForUrlCondition(sessionId, wait, capturePageState) {
|
|
4062
|
+
const timeoutMs = resolveTimeoutMs(wait.timeoutMs, 5_000, MAX_NETWORK_POLL_TIMEOUT_MS);
|
|
4063
|
+
const pollIntervalMs = resolveDurationMs(wait.pollIntervalMs, 100, 5_000);
|
|
4064
|
+
const startedAt = Date.now();
|
|
4065
|
+
const deadline = startedAt + timeoutMs;
|
|
4066
|
+
let attempts = 0;
|
|
4067
|
+
let lastPage;
|
|
4068
|
+
while (Date.now() <= deadline) {
|
|
4069
|
+
attempts += 1;
|
|
4070
|
+
const capture = await capturePageState(sessionId, {
|
|
4071
|
+
includeButtons: false,
|
|
4072
|
+
includeLinks: false,
|
|
4073
|
+
includeInputs: false,
|
|
4074
|
+
includeModals: false,
|
|
4075
|
+
maxItems: 1,
|
|
4076
|
+
maxTextLength: 40,
|
|
4077
|
+
});
|
|
4078
|
+
lastPage = {
|
|
4079
|
+
url: capture.payload.url,
|
|
4080
|
+
title: capture.payload.title,
|
|
4081
|
+
language: capture.payload.language,
|
|
4082
|
+
viewport: capture.payload.viewport,
|
|
4083
|
+
};
|
|
4084
|
+
if (matchesUrlWait(capture.payload.url, wait)) {
|
|
4085
|
+
return {
|
|
4086
|
+
waitKind: 'url',
|
|
4087
|
+
matched: true,
|
|
4088
|
+
waitedMs: Date.now() - startedAt,
|
|
4089
|
+
attempts,
|
|
4090
|
+
timeoutMs,
|
|
4091
|
+
pollIntervalMs,
|
|
4092
|
+
evidence: { page: lastPage },
|
|
4093
|
+
};
|
|
4094
|
+
}
|
|
4095
|
+
await sleep(pollIntervalMs);
|
|
4096
|
+
}
|
|
3046
4097
|
return {
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
4098
|
+
waitKind: 'url',
|
|
4099
|
+
matched: false,
|
|
4100
|
+
waitedMs: Date.now() - startedAt,
|
|
4101
|
+
attempts,
|
|
4102
|
+
timeoutMs,
|
|
4103
|
+
pollIntervalMs,
|
|
4104
|
+
evidence: {
|
|
4105
|
+
page: lastPage,
|
|
4106
|
+
expected: wait,
|
|
4107
|
+
timeoutDiagnostics: buildWaitTimeoutDiagnostics({
|
|
4108
|
+
waitKind: 'url',
|
|
4109
|
+
timeoutMs,
|
|
4110
|
+
waitedMs: Date.now() - startedAt,
|
|
4111
|
+
attempts,
|
|
4112
|
+
pollIntervalMs,
|
|
4113
|
+
matcherSummary: {
|
|
4114
|
+
exactUrl: wait.exactUrl,
|
|
4115
|
+
urlContains: wait.urlContains,
|
|
4116
|
+
urlRegex: wait.urlRegex,
|
|
4117
|
+
},
|
|
4118
|
+
lastObserved: lastPage,
|
|
4119
|
+
}),
|
|
4120
|
+
},
|
|
4121
|
+
error: {
|
|
4122
|
+
code: 'url_wait_timeout',
|
|
4123
|
+
message: 'Timed out waiting for the page URL to match the requested condition.',
|
|
4124
|
+
},
|
|
3054
4125
|
};
|
|
3055
4126
|
}
|
|
3056
|
-
function
|
|
3057
|
-
if (
|
|
3058
|
-
return
|
|
3059
|
-
}
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
4127
|
+
function pageReadyStateMatches(readyState, expectedState) {
|
|
4128
|
+
if (readyState !== 'loading' && readyState !== 'interactive' && readyState !== 'complete') {
|
|
4129
|
+
return false;
|
|
4130
|
+
}
|
|
4131
|
+
if (expectedState === 'domcontentloaded') {
|
|
4132
|
+
return readyState === 'interactive' || readyState === 'complete';
|
|
4133
|
+
}
|
|
4134
|
+
return readyState === 'complete';
|
|
4135
|
+
}
|
|
4136
|
+
async function waitForLoadStateCondition(sessionId, wait, capturePageState) {
|
|
4137
|
+
const timeoutMs = resolveTimeoutMs(wait.timeoutMs, 5_000, MAX_NETWORK_POLL_TIMEOUT_MS);
|
|
4138
|
+
const pollIntervalMs = resolveDurationMs(wait.pollIntervalMs, 100, 5_000);
|
|
4139
|
+
const expectedState = wait.state ?? 'load';
|
|
4140
|
+
const startedAt = Date.now();
|
|
4141
|
+
const deadline = startedAt + timeoutMs;
|
|
4142
|
+
let attempts = 0;
|
|
4143
|
+
let lastPage;
|
|
4144
|
+
while (Date.now() <= deadline) {
|
|
4145
|
+
attempts += 1;
|
|
4146
|
+
const capture = await capturePageState(sessionId, {
|
|
4147
|
+
includeButtons: false,
|
|
4148
|
+
includeLinks: false,
|
|
4149
|
+
includeInputs: false,
|
|
4150
|
+
includeModals: false,
|
|
4151
|
+
maxItems: 1,
|
|
4152
|
+
maxTextLength: 40,
|
|
4153
|
+
});
|
|
4154
|
+
lastPage = {
|
|
4155
|
+
url: capture.payload.url,
|
|
4156
|
+
title: capture.payload.title,
|
|
4157
|
+
readyState: capture.payload.readyState,
|
|
4158
|
+
language: capture.payload.language,
|
|
4159
|
+
viewport: capture.payload.viewport,
|
|
4160
|
+
};
|
|
4161
|
+
const urlMatches = matchesUrlPredicates(typeof capture.payload.url === 'string' ? capture.payload.url : undefined, {
|
|
4162
|
+
exactUrl: wait.exactUrl,
|
|
4163
|
+
urlContains: wait.urlContains,
|
|
4164
|
+
urlRegex: wait.urlRegex,
|
|
4165
|
+
});
|
|
4166
|
+
if (urlMatches && pageReadyStateMatches(capture.payload.readyState, expectedState)) {
|
|
4167
|
+
return {
|
|
4168
|
+
waitKind: 'load_state',
|
|
4169
|
+
matched: true,
|
|
4170
|
+
waitedMs: Date.now() - startedAt,
|
|
4171
|
+
attempts,
|
|
4172
|
+
timeoutMs,
|
|
4173
|
+
pollIntervalMs,
|
|
4174
|
+
evidence: {
|
|
4175
|
+
state: expectedState,
|
|
4176
|
+
page: lastPage,
|
|
4177
|
+
},
|
|
4178
|
+
};
|
|
4179
|
+
}
|
|
4180
|
+
await sleep(pollIntervalMs);
|
|
4181
|
+
}
|
|
3083
4182
|
return {
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
4183
|
+
waitKind: 'load_state',
|
|
4184
|
+
matched: false,
|
|
4185
|
+
waitedMs: Date.now() - startedAt,
|
|
4186
|
+
attempts,
|
|
4187
|
+
timeoutMs,
|
|
4188
|
+
pollIntervalMs,
|
|
4189
|
+
evidence: {
|
|
4190
|
+
state: expectedState,
|
|
4191
|
+
page: lastPage,
|
|
4192
|
+
expected: wait,
|
|
4193
|
+
timeoutDiagnostics: buildWaitTimeoutDiagnostics({
|
|
4194
|
+
waitKind: 'load_state',
|
|
4195
|
+
timeoutMs,
|
|
4196
|
+
waitedMs: Date.now() - startedAt,
|
|
4197
|
+
attempts,
|
|
4198
|
+
pollIntervalMs,
|
|
4199
|
+
matcherSummary: {
|
|
4200
|
+
state: expectedState,
|
|
4201
|
+
exactUrl: wait.exactUrl,
|
|
4202
|
+
urlContains: wait.urlContains,
|
|
4203
|
+
urlRegex: wait.urlRegex,
|
|
4204
|
+
},
|
|
4205
|
+
lastObserved: lastPage,
|
|
4206
|
+
}),
|
|
4207
|
+
},
|
|
4208
|
+
error: {
|
|
4209
|
+
code: 'load_state_wait_timeout',
|
|
4210
|
+
message: `Timed out waiting for page load state "${expectedState}".`,
|
|
4211
|
+
},
|
|
3099
4212
|
};
|
|
3100
4213
|
}
|
|
3101
|
-
async function
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
4214
|
+
async function waitForSelectorStateCondition(sessionId, wait, captureClient) {
|
|
4215
|
+
const timeoutMs = resolveTimeoutMs(wait.timeoutMs, 5_000, MAX_NETWORK_POLL_TIMEOUT_MS);
|
|
4216
|
+
const pollIntervalMs = resolveDurationMs(wait.pollIntervalMs, 100, 5_000);
|
|
4217
|
+
const expectedState = wait.state ?? 'visible';
|
|
4218
|
+
const startedAt = Date.now();
|
|
4219
|
+
const deadline = startedAt + timeoutMs;
|
|
4220
|
+
let attempts = 0;
|
|
4221
|
+
let lastState;
|
|
4222
|
+
while (Date.now() <= deadline) {
|
|
4223
|
+
attempts += 1;
|
|
4224
|
+
lastState = await captureSelectorState(captureClient, sessionId, wait.selector, wait.frameId);
|
|
4225
|
+
if (selectorStateMatches(lastState, expectedState)) {
|
|
4226
|
+
return {
|
|
4227
|
+
waitKind: 'selector_state',
|
|
4228
|
+
matched: true,
|
|
4229
|
+
waitedMs: Date.now() - startedAt,
|
|
4230
|
+
attempts,
|
|
4231
|
+
timeoutMs,
|
|
4232
|
+
pollIntervalMs,
|
|
4233
|
+
evidence: {
|
|
4234
|
+
selector: wait.selector,
|
|
4235
|
+
state: expectedState,
|
|
4236
|
+
selectorState: lastState,
|
|
4237
|
+
},
|
|
4238
|
+
};
|
|
4239
|
+
}
|
|
4240
|
+
await sleep(pollIntervalMs);
|
|
4241
|
+
}
|
|
4242
|
+
return {
|
|
4243
|
+
waitKind: 'selector_state',
|
|
4244
|
+
matched: false,
|
|
4245
|
+
waitedMs: Date.now() - startedAt,
|
|
4246
|
+
attempts,
|
|
4247
|
+
timeoutMs,
|
|
4248
|
+
pollIntervalMs,
|
|
4249
|
+
evidence: {
|
|
4250
|
+
selector: wait.selector,
|
|
4251
|
+
state: expectedState,
|
|
4252
|
+
selectorState: lastState,
|
|
4253
|
+
timeoutDiagnostics: buildWaitTimeoutDiagnostics({
|
|
4254
|
+
waitKind: 'selector_state',
|
|
4255
|
+
timeoutMs,
|
|
4256
|
+
waitedMs: Date.now() - startedAt,
|
|
4257
|
+
attempts,
|
|
4258
|
+
pollIntervalMs,
|
|
4259
|
+
matcherSummary: {
|
|
4260
|
+
selector: wait.selector,
|
|
4261
|
+
state: expectedState,
|
|
4262
|
+
frameId: wait.frameId,
|
|
4263
|
+
},
|
|
4264
|
+
lastObserved: lastState,
|
|
4265
|
+
}),
|
|
4266
|
+
},
|
|
4267
|
+
error: {
|
|
4268
|
+
code: 'selector_state_wait_timeout',
|
|
4269
|
+
message: `Timed out waiting for selector "${wait.selector}" to become ${expectedState}.`,
|
|
4270
|
+
},
|
|
4271
|
+
};
|
|
4272
|
+
}
|
|
4273
|
+
async function waitForConsoleCondition(sessionId, wait, captureClient) {
|
|
4274
|
+
const timeoutMs = resolveTimeoutMs(wait.timeoutMs, 5_000, MAX_NETWORK_POLL_TIMEOUT_MS);
|
|
4275
|
+
const pollIntervalMs = resolveDurationMs(wait.pollIntervalMs, 100, 5_000);
|
|
4276
|
+
const levels = resolveLiveConsoleLevels(wait.levels);
|
|
4277
|
+
const contains = normalizeOptionalString(wait.contains);
|
|
4278
|
+
const sinceTs = resolveAutomationWaitSinceTs(wait.sinceTs);
|
|
4279
|
+
const includeRuntimeErrors = wait.includeRuntimeErrors !== false;
|
|
4280
|
+
const startedAt = Date.now();
|
|
4281
|
+
const deadline = startedAt + timeoutMs;
|
|
4282
|
+
let attempts = 0;
|
|
4283
|
+
let lastLogs = [];
|
|
4284
|
+
while (Date.now() <= deadline) {
|
|
4285
|
+
attempts += 1;
|
|
4286
|
+
const capture = await executeLiveCapture(captureClient, sessionId, 'CAPTURE_GET_LIVE_CONSOLE_LOGS', {
|
|
4287
|
+
levels,
|
|
4288
|
+
contains,
|
|
4289
|
+
sinceTs,
|
|
4290
|
+
includeRuntimeErrors,
|
|
4291
|
+
limit: 10,
|
|
4292
|
+
}, 3_000);
|
|
4293
|
+
const payload = ensureCaptureSuccess(capture, sessionId);
|
|
4294
|
+
lastLogs = asRecordArray(payload.logs);
|
|
4295
|
+
if (lastLogs.length > 0) {
|
|
4296
|
+
return {
|
|
4297
|
+
waitKind: 'console',
|
|
4298
|
+
matched: true,
|
|
4299
|
+
waitedMs: Date.now() - startedAt,
|
|
4300
|
+
attempts,
|
|
4301
|
+
timeoutMs,
|
|
4302
|
+
pollIntervalMs,
|
|
4303
|
+
evidence: {
|
|
4304
|
+
filters: { levels, contains, sinceTs, includeRuntimeErrors },
|
|
4305
|
+
logs: lastLogs.slice(0, 5).map((log) => mapLiveConsoleLogRecord(log, 'compact')),
|
|
4306
|
+
},
|
|
4307
|
+
};
|
|
4308
|
+
}
|
|
4309
|
+
await sleep(pollIntervalMs);
|
|
4310
|
+
}
|
|
4311
|
+
return {
|
|
4312
|
+
waitKind: 'console',
|
|
4313
|
+
matched: false,
|
|
4314
|
+
waitedMs: Date.now() - startedAt,
|
|
4315
|
+
attempts,
|
|
4316
|
+
timeoutMs,
|
|
4317
|
+
pollIntervalMs,
|
|
4318
|
+
evidence: {
|
|
4319
|
+
filters: { levels, contains, sinceTs, includeRuntimeErrors },
|
|
4320
|
+
sampledLogs: lastLogs.slice(0, 5).map((log) => mapLiveConsoleLogRecord(log, 'compact')),
|
|
4321
|
+
timeoutDiagnostics: buildWaitTimeoutDiagnostics({
|
|
4322
|
+
waitKind: 'console',
|
|
4323
|
+
timeoutMs,
|
|
4324
|
+
waitedMs: Date.now() - startedAt,
|
|
4325
|
+
attempts,
|
|
4326
|
+
pollIntervalMs,
|
|
4327
|
+
matcherSummary: { levels, contains, sinceTs, includeRuntimeErrors },
|
|
4328
|
+
lastObserved: lastLogs[0],
|
|
4329
|
+
candidateCount: lastLogs.length,
|
|
4330
|
+
sampledCandidates: lastLogs.slice(0, 5).map((log) => mapLiveConsoleLogRecord(log, 'compact')),
|
|
4331
|
+
}),
|
|
4332
|
+
},
|
|
4333
|
+
error: {
|
|
4334
|
+
code: 'console_wait_timeout',
|
|
4335
|
+
message: 'Timed out waiting for a matching live console log.',
|
|
4336
|
+
},
|
|
4337
|
+
};
|
|
4338
|
+
}
|
|
4339
|
+
async function waitForDialogCondition(sessionId, wait, captureClient) {
|
|
4340
|
+
const timeoutMs = resolveTimeoutMs(wait.timeoutMs, 5_000, MAX_NETWORK_POLL_TIMEOUT_MS);
|
|
4341
|
+
const startedAt = Date.now();
|
|
4342
|
+
const capture = await executeLiveCapture(captureClient, sessionId, 'CAPTURE_WAIT_FOR_DIALOG', {
|
|
4343
|
+
type: wait.type,
|
|
4344
|
+
messageContains: wait.messageContains,
|
|
4345
|
+
urlContains: wait.urlContains,
|
|
4346
|
+
action: wait.action,
|
|
4347
|
+
promptText: wait.promptText,
|
|
4348
|
+
tabId: wait.tabId,
|
|
4349
|
+
timeoutMs,
|
|
4350
|
+
}, timeoutMs + 2_000);
|
|
4351
|
+
const payload = ensureCaptureSuccess(capture, sessionId);
|
|
4352
|
+
const matched = payload.matched === true;
|
|
4353
|
+
return {
|
|
4354
|
+
waitKind: 'dialog',
|
|
4355
|
+
matched,
|
|
4356
|
+
waitedMs: Date.now() - startedAt,
|
|
4357
|
+
attempts: 1,
|
|
4358
|
+
timeoutMs,
|
|
4359
|
+
pollIntervalMs: timeoutMs,
|
|
4360
|
+
evidence: {
|
|
4361
|
+
filters: {
|
|
4362
|
+
type: wait.type,
|
|
4363
|
+
messageContains: wait.messageContains,
|
|
4364
|
+
urlContains: wait.urlContains,
|
|
4365
|
+
action: wait.action,
|
|
4366
|
+
tabId: wait.tabId,
|
|
3106
4367
|
},
|
|
3107
|
-
|
|
4368
|
+
dialog: matched ? payload : undefined,
|
|
4369
|
+
expected: matched ? undefined : payload.expected ?? wait,
|
|
4370
|
+
timeoutDiagnostics: matched
|
|
4371
|
+
? undefined
|
|
4372
|
+
: buildWaitTimeoutDiagnostics({
|
|
4373
|
+
waitKind: 'dialog',
|
|
4374
|
+
timeoutMs,
|
|
4375
|
+
waitedMs: Date.now() - startedAt,
|
|
4376
|
+
attempts: 1,
|
|
4377
|
+
pollIntervalMs: timeoutMs,
|
|
4378
|
+
matcherSummary: {
|
|
4379
|
+
type: wait.type,
|
|
4380
|
+
messageContains: wait.messageContains,
|
|
4381
|
+
urlContains: wait.urlContains,
|
|
4382
|
+
action: wait.action,
|
|
4383
|
+
tabId: wait.tabId,
|
|
4384
|
+
},
|
|
4385
|
+
lastObserved: payload.lastObserved,
|
|
4386
|
+
candidateCount: typeof payload.observedCount === 'number' ? payload.observedCount : undefined,
|
|
4387
|
+
}),
|
|
4388
|
+
},
|
|
4389
|
+
error: matched
|
|
4390
|
+
? undefined
|
|
4391
|
+
: {
|
|
4392
|
+
code: 'dialog_wait_timeout',
|
|
4393
|
+
message: 'Timed out waiting for a matching JavaScript dialog.',
|
|
4394
|
+
},
|
|
4395
|
+
};
|
|
4396
|
+
}
|
|
4397
|
+
async function waitForStableLayoutCondition(sessionId, wait, captureClient) {
|
|
4398
|
+
const timeoutMs = resolveTimeoutMs(wait.timeoutMs, 5_000, MAX_NETWORK_POLL_TIMEOUT_MS);
|
|
4399
|
+
const stableMs = wait.stableMs;
|
|
4400
|
+
const pollIntervalMs = resolveDurationMs(wait.pollIntervalMs, 100, 5_000);
|
|
4401
|
+
const startedAt = Date.now();
|
|
4402
|
+
const capture = await executeLiveCapture(captureClient, sessionId, 'CAPTURE_WAIT_FOR_STABLE_LAYOUT', {
|
|
4403
|
+
selector: wait.selector,
|
|
4404
|
+
stableMs,
|
|
4405
|
+
tabId: wait.tabId,
|
|
4406
|
+
timeoutMs,
|
|
4407
|
+
pollIntervalMs,
|
|
4408
|
+
}, timeoutMs + 2_000);
|
|
4409
|
+
const payload = ensureCaptureSuccess(capture, sessionId);
|
|
4410
|
+
const matched = payload.matched === true;
|
|
4411
|
+
return {
|
|
4412
|
+
waitKind: 'stable_layout',
|
|
4413
|
+
matched,
|
|
4414
|
+
waitedMs: Date.now() - startedAt,
|
|
4415
|
+
attempts: 1,
|
|
4416
|
+
timeoutMs,
|
|
4417
|
+
pollIntervalMs,
|
|
4418
|
+
evidence: {
|
|
4419
|
+
filters: {
|
|
4420
|
+
selector: wait.selector,
|
|
4421
|
+
stableMs,
|
|
4422
|
+
tabId: wait.tabId,
|
|
4423
|
+
},
|
|
4424
|
+
layout: payload,
|
|
4425
|
+
timeoutDiagnostics: matched
|
|
4426
|
+
? undefined
|
|
4427
|
+
: buildWaitTimeoutDiagnostics({
|
|
4428
|
+
waitKind: 'stable_layout',
|
|
4429
|
+
timeoutMs,
|
|
4430
|
+
waitedMs: Date.now() - startedAt,
|
|
4431
|
+
attempts: typeof payload.attempts === 'number' ? payload.attempts : 1,
|
|
4432
|
+
pollIntervalMs,
|
|
4433
|
+
matcherSummary: {
|
|
4434
|
+
selector: wait.selector,
|
|
4435
|
+
stableMs,
|
|
4436
|
+
tabId: wait.tabId,
|
|
4437
|
+
},
|
|
4438
|
+
lastObserved: payload.snapshot,
|
|
4439
|
+
}),
|
|
4440
|
+
},
|
|
4441
|
+
error: matched
|
|
4442
|
+
? undefined
|
|
4443
|
+
: {
|
|
4444
|
+
code: 'stable_layout_wait_timeout',
|
|
4445
|
+
message: 'Timed out waiting for layout to stay stable.',
|
|
4446
|
+
},
|
|
4447
|
+
};
|
|
4448
|
+
}
|
|
4449
|
+
function mapNavigationWaitEvent(row) {
|
|
4450
|
+
const payload = readJsonPayload(row.payload_json);
|
|
4451
|
+
return {
|
|
4452
|
+
eventId: row.event_id,
|
|
4453
|
+
sessionId: row.session_id,
|
|
4454
|
+
timestamp: row.ts,
|
|
4455
|
+
tabId: row.tab_id ?? undefined,
|
|
4456
|
+
origin: row.origin ?? undefined,
|
|
4457
|
+
url: resolveLastUrl(payload),
|
|
4458
|
+
from: typeof payload.from === 'string' ? payload.from : undefined,
|
|
4459
|
+
trigger: typeof payload.trigger === 'string' ? payload.trigger : undefined,
|
|
4460
|
+
payload,
|
|
4461
|
+
};
|
|
4462
|
+
}
|
|
4463
|
+
function navigationEventMatches(row, wait) {
|
|
4464
|
+
const payload = readJsonPayload(row.payload_json);
|
|
4465
|
+
const toUrl = resolveLastUrl(payload);
|
|
4466
|
+
const fromUrl = typeof payload.from === 'string' ? payload.from : undefined;
|
|
4467
|
+
const trigger = typeof payload.trigger === 'string' ? payload.trigger : undefined;
|
|
4468
|
+
if (!matchesUrlPredicates(toUrl, {
|
|
4469
|
+
exactUrl: wait.exactUrl,
|
|
4470
|
+
urlContains: wait.urlContains,
|
|
4471
|
+
urlRegex: wait.urlRegex,
|
|
4472
|
+
})) {
|
|
4473
|
+
return false;
|
|
3108
4474
|
}
|
|
3109
|
-
if (
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
4475
|
+
if (wait.fromUrlContains || wait.fromUrlRegex) {
|
|
4476
|
+
if (!matchesUrlPredicates(fromUrl, {
|
|
4477
|
+
urlContains: wait.fromUrlContains,
|
|
4478
|
+
urlRegex: wait.fromUrlRegex,
|
|
4479
|
+
regexFieldName: 'fromUrlRegex',
|
|
4480
|
+
})) {
|
|
4481
|
+
return false;
|
|
4482
|
+
}
|
|
4483
|
+
}
|
|
4484
|
+
if (wait.trigger && trigger !== wait.trigger) {
|
|
4485
|
+
return false;
|
|
4486
|
+
}
|
|
4487
|
+
return true;
|
|
4488
|
+
}
|
|
4489
|
+
function queryNavigationWaitCandidates(db, options) {
|
|
4490
|
+
const where = ['session_id = ?', "type = 'nav'", 'ts >= ?'];
|
|
4491
|
+
const params = [options.sessionId, options.sinceTs];
|
|
4492
|
+
if (options.tabId !== undefined) {
|
|
4493
|
+
where.push('tab_id = ?');
|
|
4494
|
+
params.push(options.tabId);
|
|
4495
|
+
}
|
|
4496
|
+
return db.prepare(`SELECT event_id, session_id, ts, type, payload_json, tab_id, origin
|
|
4497
|
+
FROM events
|
|
4498
|
+
WHERE ${where.join(' AND ')}
|
|
4499
|
+
ORDER BY ts ASC
|
|
4500
|
+
LIMIT 200`).all(...params);
|
|
4501
|
+
}
|
|
4502
|
+
async function waitForNavigationCondition(sessionId, wait, db) {
|
|
4503
|
+
const timeoutMs = resolveTimeoutMs(wait.timeoutMs, 5_000, MAX_NETWORK_POLL_TIMEOUT_MS);
|
|
4504
|
+
const pollIntervalMs = resolveDurationMs(wait.pollIntervalMs, 100, 5_000);
|
|
4505
|
+
const sinceTs = resolveAutomationWaitSinceTs(wait.sinceTs);
|
|
4506
|
+
const tabId = resolveOptionalTabId(wait.tabId);
|
|
4507
|
+
const startedAt = Date.now();
|
|
4508
|
+
const deadline = startedAt + timeoutMs;
|
|
4509
|
+
let attempts = 0;
|
|
4510
|
+
let lastEvents = [];
|
|
4511
|
+
while (Date.now() <= deadline) {
|
|
4512
|
+
attempts += 1;
|
|
4513
|
+
lastEvents = queryNavigationWaitCandidates(db, { sessionId, sinceTs, tabId });
|
|
4514
|
+
const matched = lastEvents.find((row) => navigationEventMatches(row, wait));
|
|
4515
|
+
if (matched) {
|
|
4516
|
+
return {
|
|
4517
|
+
waitKind: 'navigation',
|
|
4518
|
+
matched: true,
|
|
4519
|
+
waitedMs: Date.now() - startedAt,
|
|
4520
|
+
attempts,
|
|
4521
|
+
timeoutMs,
|
|
4522
|
+
pollIntervalMs,
|
|
4523
|
+
evidence: {
|
|
4524
|
+
filters: {
|
|
4525
|
+
urlContains: wait.urlContains,
|
|
4526
|
+
urlRegex: wait.urlRegex,
|
|
4527
|
+
exactUrl: wait.exactUrl,
|
|
4528
|
+
fromUrlContains: wait.fromUrlContains,
|
|
4529
|
+
fromUrlRegex: wait.fromUrlRegex,
|
|
4530
|
+
trigger: wait.trigger,
|
|
4531
|
+
sinceTs,
|
|
4532
|
+
tabId,
|
|
4533
|
+
},
|
|
4534
|
+
navigation: mapNavigationWaitEvent(matched),
|
|
4535
|
+
},
|
|
4536
|
+
};
|
|
4537
|
+
}
|
|
4538
|
+
await sleep(pollIntervalMs);
|
|
4539
|
+
}
|
|
4540
|
+
return {
|
|
4541
|
+
waitKind: 'navigation',
|
|
4542
|
+
matched: false,
|
|
4543
|
+
waitedMs: Date.now() - startedAt,
|
|
4544
|
+
attempts,
|
|
4545
|
+
timeoutMs,
|
|
4546
|
+
pollIntervalMs,
|
|
4547
|
+
evidence: {
|
|
4548
|
+
filters: {
|
|
4549
|
+
urlContains: wait.urlContains,
|
|
4550
|
+
urlRegex: wait.urlRegex,
|
|
4551
|
+
exactUrl: wait.exactUrl,
|
|
4552
|
+
fromUrlContains: wait.fromUrlContains,
|
|
4553
|
+
fromUrlRegex: wait.fromUrlRegex,
|
|
4554
|
+
trigger: wait.trigger,
|
|
4555
|
+
sinceTs,
|
|
4556
|
+
tabId,
|
|
4557
|
+
},
|
|
4558
|
+
sampledEvents: lastEvents.slice(0, 5).map((row) => mapNavigationWaitEvent(row)),
|
|
4559
|
+
timeoutDiagnostics: buildWaitTimeoutDiagnostics({
|
|
4560
|
+
waitKind: 'navigation',
|
|
4561
|
+
timeoutMs,
|
|
4562
|
+
waitedMs: Date.now() - startedAt,
|
|
4563
|
+
attempts,
|
|
4564
|
+
pollIntervalMs,
|
|
4565
|
+
matcherSummary: {
|
|
4566
|
+
urlContains: wait.urlContains,
|
|
4567
|
+
urlRegex: wait.urlRegex,
|
|
4568
|
+
exactUrl: wait.exactUrl,
|
|
4569
|
+
fromUrlContains: wait.fromUrlContains,
|
|
4570
|
+
fromUrlRegex: wait.fromUrlRegex,
|
|
4571
|
+
trigger: wait.trigger,
|
|
4572
|
+
sinceTs,
|
|
4573
|
+
tabId,
|
|
4574
|
+
},
|
|
4575
|
+
lastObserved: lastEvents.length > 0 ? mapNavigationWaitEvent(lastEvents[lastEvents.length - 1]) : undefined,
|
|
4576
|
+
candidateCount: lastEvents.length,
|
|
4577
|
+
sampledCandidates: lastEvents.slice(0, 5).map((row) => mapNavigationWaitEvent(row)),
|
|
4578
|
+
}),
|
|
4579
|
+
},
|
|
4580
|
+
error: {
|
|
4581
|
+
code: 'navigation_wait_timeout',
|
|
4582
|
+
message: 'Timed out waiting for a matching navigation event.',
|
|
4583
|
+
},
|
|
4584
|
+
};
|
|
4585
|
+
}
|
|
4586
|
+
async function waitForNavigationLifecycleCondition(sessionId, wait, captureClient) {
|
|
4587
|
+
const timeoutMs = resolveTimeoutMs(wait.timeoutMs, 5_000, MAX_NETWORK_POLL_TIMEOUT_MS);
|
|
4588
|
+
const startedAt = Date.now();
|
|
4589
|
+
const capture = await executeLiveCapture(captureClient, sessionId, 'CAPTURE_WAIT_FOR_NAVIGATION_LIFECYCLE', {
|
|
4590
|
+
state: wait.state,
|
|
4591
|
+
urlContains: wait.urlContains,
|
|
4592
|
+
urlRegex: wait.urlRegex,
|
|
4593
|
+
exactUrl: wait.exactUrl,
|
|
4594
|
+
tabId: wait.tabId,
|
|
4595
|
+
timeoutMs,
|
|
4596
|
+
}, timeoutMs + 2_000);
|
|
4597
|
+
const payload = ensureCaptureSuccess(capture, sessionId);
|
|
4598
|
+
const matched = payload.matched === true;
|
|
4599
|
+
return {
|
|
4600
|
+
waitKind: 'navigation_lifecycle',
|
|
4601
|
+
matched,
|
|
4602
|
+
waitedMs: Date.now() - startedAt,
|
|
4603
|
+
attempts: 1,
|
|
4604
|
+
timeoutMs,
|
|
4605
|
+
pollIntervalMs: timeoutMs,
|
|
4606
|
+
evidence: {
|
|
4607
|
+
filters: {
|
|
4608
|
+
state: wait.state,
|
|
4609
|
+
urlContains: wait.urlContains,
|
|
4610
|
+
urlRegex: wait.urlRegex,
|
|
4611
|
+
exactUrl: wait.exactUrl,
|
|
4612
|
+
tabId: wait.tabId,
|
|
4613
|
+
},
|
|
4614
|
+
lifecycle: matched ? payload : undefined,
|
|
4615
|
+
expected: matched ? undefined : payload.expected ?? wait,
|
|
4616
|
+
timeoutDiagnostics: matched
|
|
4617
|
+
? undefined
|
|
4618
|
+
: buildWaitTimeoutDiagnostics({
|
|
4619
|
+
waitKind: 'navigation_lifecycle',
|
|
4620
|
+
timeoutMs,
|
|
4621
|
+
waitedMs: Date.now() - startedAt,
|
|
4622
|
+
attempts: 1,
|
|
4623
|
+
pollIntervalMs: timeoutMs,
|
|
4624
|
+
matcherSummary: {
|
|
4625
|
+
state: wait.state,
|
|
4626
|
+
urlContains: wait.urlContains,
|
|
4627
|
+
urlRegex: wait.urlRegex,
|
|
4628
|
+
exactUrl: wait.exactUrl,
|
|
4629
|
+
tabId: wait.tabId,
|
|
4630
|
+
},
|
|
4631
|
+
lastObserved: payload.lastObserved,
|
|
4632
|
+
candidateCount: typeof payload.observedEventCount === 'number' ? payload.observedEventCount : undefined,
|
|
4633
|
+
}),
|
|
4634
|
+
},
|
|
4635
|
+
error: matched
|
|
4636
|
+
? undefined
|
|
4637
|
+
: {
|
|
4638
|
+
code: 'navigation_lifecycle_wait_timeout',
|
|
4639
|
+
message: 'Timed out waiting for a matching navigation lifecycle event.',
|
|
4640
|
+
},
|
|
4641
|
+
};
|
|
4642
|
+
}
|
|
4643
|
+
async function waitForDownloadCondition(sessionId, wait, captureClient) {
|
|
4644
|
+
const timeoutMs = resolveTimeoutMs(wait.timeoutMs, 5_000, MAX_NETWORK_POLL_TIMEOUT_MS);
|
|
4645
|
+
const startedAt = Date.now();
|
|
4646
|
+
const capture = await executeLiveCapture(captureClient, sessionId, 'CAPTURE_WAIT_FOR_DOWNLOAD', {
|
|
4647
|
+
urlContains: wait.urlContains,
|
|
4648
|
+
urlRegex: wait.urlRegex,
|
|
4649
|
+
exactUrl: wait.exactUrl,
|
|
4650
|
+
filenameContains: wait.filenameContains,
|
|
4651
|
+
filenameRegex: wait.filenameRegex,
|
|
4652
|
+
state: wait.state,
|
|
4653
|
+
tabId: wait.tabId,
|
|
4654
|
+
timeoutMs,
|
|
4655
|
+
}, timeoutMs + 2_000);
|
|
4656
|
+
const payload = ensureCaptureSuccess(capture, sessionId);
|
|
4657
|
+
const matched = payload.matched === true;
|
|
4658
|
+
return {
|
|
4659
|
+
waitKind: 'download',
|
|
4660
|
+
matched,
|
|
4661
|
+
waitedMs: Date.now() - startedAt,
|
|
4662
|
+
attempts: 1,
|
|
4663
|
+
timeoutMs,
|
|
4664
|
+
pollIntervalMs: timeoutMs,
|
|
4665
|
+
evidence: {
|
|
4666
|
+
filters: {
|
|
4667
|
+
urlContains: wait.urlContains,
|
|
4668
|
+
urlRegex: wait.urlRegex,
|
|
4669
|
+
exactUrl: wait.exactUrl,
|
|
4670
|
+
filenameContains: wait.filenameContains,
|
|
4671
|
+
filenameRegex: wait.filenameRegex,
|
|
4672
|
+
state: wait.state,
|
|
4673
|
+
tabId: wait.tabId,
|
|
3117
4674
|
},
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
4675
|
+
download: matched ? payload : undefined,
|
|
4676
|
+
expected: matched ? undefined : payload.expected ?? wait,
|
|
4677
|
+
timeoutDiagnostics: matched
|
|
4678
|
+
? undefined
|
|
4679
|
+
: buildWaitTimeoutDiagnostics({
|
|
4680
|
+
waitKind: 'download',
|
|
4681
|
+
timeoutMs,
|
|
4682
|
+
waitedMs: Date.now() - startedAt,
|
|
4683
|
+
attempts: 1,
|
|
4684
|
+
pollIntervalMs: timeoutMs,
|
|
4685
|
+
matcherSummary: {
|
|
4686
|
+
urlContains: wait.urlContains,
|
|
4687
|
+
urlRegex: wait.urlRegex,
|
|
4688
|
+
exactUrl: wait.exactUrl,
|
|
4689
|
+
filenameContains: wait.filenameContains,
|
|
4690
|
+
filenameRegex: wait.filenameRegex,
|
|
4691
|
+
state: wait.state,
|
|
4692
|
+
tabId: wait.tabId,
|
|
4693
|
+
},
|
|
4694
|
+
lastObserved: payload.lastObserved ?? payload.lastMatchedDownload,
|
|
4695
|
+
candidateCount: typeof payload.observedEventCount === 'number' ? payload.observedEventCount : undefined,
|
|
4696
|
+
}),
|
|
4697
|
+
},
|
|
4698
|
+
error: matched
|
|
4699
|
+
? undefined
|
|
4700
|
+
: {
|
|
4701
|
+
code: 'download_wait_timeout',
|
|
4702
|
+
message: 'Timed out waiting for a matching download.',
|
|
3121
4703
|
},
|
|
4704
|
+
};
|
|
4705
|
+
}
|
|
4706
|
+
async function waitForPopupCondition(sessionId, wait, captureClient) {
|
|
4707
|
+
const timeoutMs = resolveTimeoutMs(wait.timeoutMs, 5_000, MAX_NETWORK_POLL_TIMEOUT_MS);
|
|
4708
|
+
const startedAt = Date.now();
|
|
4709
|
+
const capture = await executeLiveCapture(captureClient, sessionId, 'CAPTURE_WAIT_FOR_POPUP', {
|
|
4710
|
+
urlContains: wait.urlContains,
|
|
4711
|
+
urlRegex: wait.urlRegex,
|
|
4712
|
+
exactUrl: wait.exactUrl,
|
|
4713
|
+
openerTabId: wait.openerTabId,
|
|
4714
|
+
timeoutMs,
|
|
4715
|
+
}, timeoutMs + 2_000);
|
|
4716
|
+
const payload = ensureCaptureSuccess(capture, sessionId);
|
|
4717
|
+
const matched = payload.matched === true;
|
|
4718
|
+
return {
|
|
4719
|
+
waitKind: 'popup',
|
|
4720
|
+
matched,
|
|
4721
|
+
waitedMs: Date.now() - startedAt,
|
|
4722
|
+
attempts: 1,
|
|
4723
|
+
timeoutMs,
|
|
4724
|
+
pollIntervalMs: timeoutMs,
|
|
4725
|
+
evidence: {
|
|
4726
|
+
filters: {
|
|
4727
|
+
urlContains: wait.urlContains,
|
|
4728
|
+
urlRegex: wait.urlRegex,
|
|
4729
|
+
exactUrl: wait.exactUrl,
|
|
4730
|
+
openerTabId: wait.openerTabId,
|
|
4731
|
+
},
|
|
4732
|
+
popup: matched ? payload : undefined,
|
|
4733
|
+
expected: matched ? undefined : payload.expected ?? wait,
|
|
4734
|
+
timeoutDiagnostics: matched
|
|
4735
|
+
? undefined
|
|
4736
|
+
: buildWaitTimeoutDiagnostics({
|
|
4737
|
+
waitKind: 'popup',
|
|
4738
|
+
timeoutMs,
|
|
4739
|
+
waitedMs: Date.now() - startedAt,
|
|
4740
|
+
attempts: 1,
|
|
4741
|
+
pollIntervalMs: timeoutMs,
|
|
4742
|
+
matcherSummary: {
|
|
4743
|
+
urlContains: wait.urlContains,
|
|
4744
|
+
urlRegex: wait.urlRegex,
|
|
4745
|
+
exactUrl: wait.exactUrl,
|
|
4746
|
+
openerTabId: wait.openerTabId,
|
|
4747
|
+
},
|
|
4748
|
+
lastObserved: payload.lastObserved,
|
|
4749
|
+
candidateCount: typeof payload.observedPopupCount === 'number' ? payload.observedPopupCount : undefined,
|
|
4750
|
+
sampledCandidates: Array.isArray(payload.pendingTabIds) ? payload.pendingTabIds : undefined,
|
|
4751
|
+
}),
|
|
4752
|
+
},
|
|
4753
|
+
error: matched
|
|
4754
|
+
? undefined
|
|
4755
|
+
: {
|
|
4756
|
+
code: 'popup_wait_timeout',
|
|
4757
|
+
message: 'Timed out waiting for a matching popup tab.',
|
|
4758
|
+
},
|
|
4759
|
+
};
|
|
4760
|
+
}
|
|
4761
|
+
function normalizeNetworkWaitFilters(wait) {
|
|
4762
|
+
const responseWait = wait.waitKind === 'response' ? wait : undefined;
|
|
4763
|
+
return {
|
|
4764
|
+
urlContains: normalizeOptionalString(wait.urlContains),
|
|
4765
|
+
urlRegex: normalizeOptionalString(wait.urlRegex),
|
|
4766
|
+
exactUrl: normalizeOptionalString(wait.exactUrl),
|
|
4767
|
+
method: normalizeHttpMethod(wait.method),
|
|
4768
|
+
traceId: normalizeOptionalString(wait.traceId),
|
|
4769
|
+
initiator: normalizeOptionalString(wait.initiator),
|
|
4770
|
+
requestContentType: normalizeOptionalString(wait.requestContentType),
|
|
4771
|
+
responseContentType: normalizeOptionalString(responseWait?.responseContentType),
|
|
4772
|
+
statusIn: responseWait ? normalizeStatusIn(responseWait.statusIn) : [],
|
|
4773
|
+
statusGte: responseWait?.statusGte,
|
|
4774
|
+
statusLt: responseWait?.statusLt,
|
|
4775
|
+
errorType: normalizeOptionalString(responseWait?.errorType),
|
|
4776
|
+
sinceTs: resolveAutomationWaitSinceTs(wait.sinceTs),
|
|
4777
|
+
tabId: resolveOptionalTabId(wait.tabId),
|
|
4778
|
+
includeBodies: wait.includeBodies === true,
|
|
4779
|
+
};
|
|
4780
|
+
}
|
|
4781
|
+
function queryNetworkWaitCandidates(db, sessionId, filters) {
|
|
4782
|
+
const where = ['session_id = ?', 'ts_start >= ?'];
|
|
4783
|
+
const params = [sessionId, filters.sinceTs];
|
|
4784
|
+
if (filters.exactUrl) {
|
|
4785
|
+
where.push('url = ?');
|
|
4786
|
+
params.push(filters.exactUrl);
|
|
4787
|
+
}
|
|
4788
|
+
else if (filters.urlContains) {
|
|
4789
|
+
where.push('url LIKE ?');
|
|
4790
|
+
params.push(`%${filters.urlContains}%`);
|
|
4791
|
+
}
|
|
4792
|
+
if (filters.method) {
|
|
4793
|
+
where.push('method = ?');
|
|
4794
|
+
params.push(filters.method);
|
|
4795
|
+
}
|
|
4796
|
+
if (filters.traceId) {
|
|
4797
|
+
where.push('trace_id = ?');
|
|
4798
|
+
params.push(filters.traceId);
|
|
4799
|
+
}
|
|
4800
|
+
if (filters.initiator) {
|
|
4801
|
+
where.push('initiator = ?');
|
|
4802
|
+
params.push(filters.initiator);
|
|
4803
|
+
}
|
|
4804
|
+
if (filters.requestContentType) {
|
|
4805
|
+
where.push('request_content_type LIKE ?');
|
|
4806
|
+
params.push(`%${filters.requestContentType}%`);
|
|
4807
|
+
}
|
|
4808
|
+
if (filters.responseContentType) {
|
|
4809
|
+
where.push('response_content_type LIKE ?');
|
|
4810
|
+
params.push(`%${filters.responseContentType}%`);
|
|
4811
|
+
}
|
|
4812
|
+
const statusIn = Array.isArray(filters.statusIn) ? filters.statusIn : [];
|
|
4813
|
+
if (statusIn.length > 0) {
|
|
4814
|
+
where.push(`status IN (${statusIn.map(() => '?').join(', ')})`);
|
|
4815
|
+
params.push(...statusIn);
|
|
4816
|
+
}
|
|
4817
|
+
if (typeof filters.statusGte === 'number') {
|
|
4818
|
+
where.push('status >= ?');
|
|
4819
|
+
params.push(filters.statusGte);
|
|
4820
|
+
}
|
|
4821
|
+
if (typeof filters.statusLt === 'number') {
|
|
4822
|
+
where.push('status < ?');
|
|
4823
|
+
params.push(filters.statusLt);
|
|
4824
|
+
}
|
|
4825
|
+
if (filters.tabId !== undefined) {
|
|
4826
|
+
where.push('tab_id = ?');
|
|
4827
|
+
params.push(filters.tabId);
|
|
4828
|
+
}
|
|
4829
|
+
return db.prepare(`SELECT ${NETWORK_CALL_SELECT_COLUMNS}
|
|
4830
|
+
FROM network
|
|
4831
|
+
WHERE ${where.join(' AND ')}
|
|
4832
|
+
ORDER BY ts_start ASC
|
|
4833
|
+
LIMIT 200`).all(...params);
|
|
4834
|
+
}
|
|
4835
|
+
function networkCallMatchesFilters(row, filters) {
|
|
4836
|
+
if (!matchesUrlPredicates(row.url, {
|
|
4837
|
+
exactUrl: typeof filters.exactUrl === 'string' ? filters.exactUrl : undefined,
|
|
4838
|
+
urlContains: typeof filters.urlContains === 'string' ? filters.urlContains : undefined,
|
|
4839
|
+
urlRegex: typeof filters.urlRegex === 'string' ? filters.urlRegex : undefined,
|
|
4840
|
+
})) {
|
|
4841
|
+
return false;
|
|
4842
|
+
}
|
|
4843
|
+
if (typeof filters.errorType === 'string' && classifyNetworkFailure(row.status, row.error_class) !== filters.errorType) {
|
|
4844
|
+
return false;
|
|
4845
|
+
}
|
|
4846
|
+
return true;
|
|
4847
|
+
}
|
|
4848
|
+
async function waitForNetworkMatchCondition(sessionId, wait, db) {
|
|
4849
|
+
const timeoutMs = resolveTimeoutMs(wait.timeoutMs, DEFAULT_NETWORK_POLL_TIMEOUT_MS, MAX_NETWORK_POLL_TIMEOUT_MS);
|
|
4850
|
+
const pollIntervalMs = resolveDurationMs(wait.pollIntervalMs, DEFAULT_NETWORK_POLL_INTERVAL_MS, 5_000);
|
|
4851
|
+
const filters = normalizeNetworkWaitFilters(wait);
|
|
4852
|
+
const startedAt = Date.now();
|
|
4853
|
+
const deadline = startedAt + timeoutMs;
|
|
4854
|
+
let attempts = 0;
|
|
4855
|
+
let lastCalls = [];
|
|
4856
|
+
while (Date.now() <= deadline) {
|
|
4857
|
+
attempts += 1;
|
|
4858
|
+
lastCalls = queryNetworkWaitCandidates(db, sessionId, filters);
|
|
4859
|
+
const matched = lastCalls.find((row) => networkCallMatchesFilters(row, filters));
|
|
4860
|
+
if (matched) {
|
|
4861
|
+
return {
|
|
4862
|
+
waitKind: wait.waitKind,
|
|
4863
|
+
matched: true,
|
|
4864
|
+
waitedMs: Date.now() - startedAt,
|
|
4865
|
+
attempts,
|
|
4866
|
+
timeoutMs,
|
|
4867
|
+
pollIntervalMs,
|
|
4868
|
+
evidence: {
|
|
4869
|
+
filters,
|
|
4870
|
+
call: mapNetworkCallRecord(matched, filters.includeBodies === true),
|
|
4871
|
+
},
|
|
4872
|
+
};
|
|
4873
|
+
}
|
|
4874
|
+
await sleep(pollIntervalMs);
|
|
4875
|
+
}
|
|
4876
|
+
return {
|
|
4877
|
+
waitKind: wait.waitKind,
|
|
4878
|
+
matched: false,
|
|
4879
|
+
waitedMs: Date.now() - startedAt,
|
|
4880
|
+
attempts,
|
|
4881
|
+
timeoutMs,
|
|
4882
|
+
pollIntervalMs,
|
|
4883
|
+
evidence: {
|
|
4884
|
+
filters,
|
|
4885
|
+
sampledCalls: lastCalls.slice(0, 5).map((row) => mapNetworkCallRecord(row, false)),
|
|
4886
|
+
timeoutDiagnostics: buildWaitTimeoutDiagnostics({
|
|
4887
|
+
waitKind: wait.waitKind,
|
|
4888
|
+
timeoutMs,
|
|
4889
|
+
waitedMs: Date.now() - startedAt,
|
|
4890
|
+
attempts,
|
|
4891
|
+
pollIntervalMs,
|
|
4892
|
+
matcherSummary: filters,
|
|
4893
|
+
lastObserved: lastCalls.length > 0 ? mapNetworkCallRecord(lastCalls[lastCalls.length - 1], false) : undefined,
|
|
4894
|
+
candidateCount: lastCalls.length,
|
|
4895
|
+
sampledCandidates: lastCalls.slice(0, 5).map((row) => mapNetworkCallRecord(row, false)),
|
|
4896
|
+
}),
|
|
4897
|
+
},
|
|
4898
|
+
error: {
|
|
4899
|
+
code: wait.waitKind === 'request' ? 'request_wait_timeout' : 'response_wait_timeout',
|
|
4900
|
+
message: `Timed out waiting for a matching ${wait.waitKind}.`,
|
|
4901
|
+
},
|
|
4902
|
+
};
|
|
4903
|
+
}
|
|
4904
|
+
function queryRecentNetworkActivity(db, options) {
|
|
4905
|
+
const where = ['session_id = ?', 'ts_start >= ?'];
|
|
4906
|
+
const params = [options.sessionId, options.sinceTs];
|
|
4907
|
+
if (options.urlContains) {
|
|
4908
|
+
where.push('url LIKE ?');
|
|
4909
|
+
params.push(`%${options.urlContains}%`);
|
|
4910
|
+
}
|
|
4911
|
+
if (options.method) {
|
|
4912
|
+
where.push('method = ?');
|
|
4913
|
+
params.push(options.method);
|
|
4914
|
+
}
|
|
4915
|
+
if (options.tabId !== undefined) {
|
|
4916
|
+
where.push('tab_id = ?');
|
|
4917
|
+
params.push(options.tabId);
|
|
4918
|
+
}
|
|
4919
|
+
return db.prepare(`SELECT ${NETWORK_CALL_SELECT_COLUMNS}
|
|
4920
|
+
FROM network
|
|
4921
|
+
WHERE ${where.join(' AND ')}
|
|
4922
|
+
ORDER BY ts_start DESC
|
|
4923
|
+
LIMIT 10`).all(...params);
|
|
4924
|
+
}
|
|
4925
|
+
async function waitForNetworkQuietCondition(sessionId, wait, db) {
|
|
4926
|
+
const timeoutMs = resolveTimeoutMs(wait.timeoutMs, DEFAULT_NETWORK_POLL_TIMEOUT_MS, MAX_NETWORK_POLL_TIMEOUT_MS);
|
|
4927
|
+
const pollIntervalMs = resolveDurationMs(wait.pollIntervalMs, DEFAULT_NETWORK_POLL_INTERVAL_MS, 5_000);
|
|
4928
|
+
const quietMs = resolveDurationMs(wait.quietMs, 500, 10_000);
|
|
4929
|
+
const urlContains = normalizeOptionalString(wait.urlContains);
|
|
4930
|
+
const method = normalizeHttpMethod(wait.method);
|
|
4931
|
+
const tabId = resolveOptionalTabId(wait.tabId);
|
|
4932
|
+
const startedAt = Date.now();
|
|
4933
|
+
const deadline = startedAt + timeoutMs;
|
|
4934
|
+
let attempts = 0;
|
|
4935
|
+
let lastActivityAt = startedAt;
|
|
4936
|
+
let lastCalls = [];
|
|
4937
|
+
while (Date.now() <= deadline) {
|
|
4938
|
+
attempts += 1;
|
|
4939
|
+
const rows = queryRecentNetworkActivity(db, {
|
|
4940
|
+
sessionId,
|
|
4941
|
+
sinceTs: lastActivityAt + 1,
|
|
4942
|
+
urlContains,
|
|
4943
|
+
method,
|
|
4944
|
+
tabId,
|
|
4945
|
+
});
|
|
4946
|
+
if (rows.length > 0) {
|
|
4947
|
+
lastCalls = rows;
|
|
4948
|
+
lastActivityAt = Math.max(...rows.map((row) => row.ts_start), Date.now());
|
|
4949
|
+
}
|
|
4950
|
+
if (Date.now() - lastActivityAt >= quietMs) {
|
|
4951
|
+
return {
|
|
4952
|
+
waitKind: 'network_quiet',
|
|
4953
|
+
matched: true,
|
|
4954
|
+
waitedMs: Date.now() - startedAt,
|
|
4955
|
+
attempts,
|
|
4956
|
+
timeoutMs,
|
|
4957
|
+
pollIntervalMs,
|
|
4958
|
+
evidence: {
|
|
4959
|
+
quietMs,
|
|
4960
|
+
filters: { urlContains, method, tabId },
|
|
4961
|
+
lastActivityAt,
|
|
4962
|
+
sampledCalls: lastCalls.slice(0, 5).map((row) => mapNetworkCallRecord(row, false)),
|
|
4963
|
+
},
|
|
4964
|
+
};
|
|
4965
|
+
}
|
|
4966
|
+
await sleep(pollIntervalMs);
|
|
4967
|
+
}
|
|
4968
|
+
return {
|
|
4969
|
+
waitKind: 'network_quiet',
|
|
4970
|
+
matched: false,
|
|
4971
|
+
waitedMs: Date.now() - startedAt,
|
|
4972
|
+
attempts,
|
|
4973
|
+
timeoutMs,
|
|
4974
|
+
pollIntervalMs,
|
|
4975
|
+
evidence: {
|
|
4976
|
+
quietMs,
|
|
4977
|
+
filters: { urlContains, method, tabId },
|
|
4978
|
+
lastActivityAt,
|
|
4979
|
+
sampledCalls: lastCalls.slice(0, 5).map((row) => mapNetworkCallRecord(row, false)),
|
|
4980
|
+
timeoutDiagnostics: buildWaitTimeoutDiagnostics({
|
|
4981
|
+
waitKind: 'network_quiet',
|
|
4982
|
+
timeoutMs,
|
|
4983
|
+
waitedMs: Date.now() - startedAt,
|
|
4984
|
+
attempts,
|
|
4985
|
+
pollIntervalMs,
|
|
4986
|
+
matcherSummary: { quietMs, urlContains, method, tabId },
|
|
4987
|
+
lastObserved: lastCalls.length > 0 ? mapNetworkCallRecord(lastCalls[0], false) : undefined,
|
|
4988
|
+
candidateCount: lastCalls.length,
|
|
4989
|
+
sampledCandidates: lastCalls.slice(0, 5).map((row) => mapNetworkCallRecord(row, false)),
|
|
4990
|
+
}),
|
|
4991
|
+
},
|
|
4992
|
+
error: {
|
|
4993
|
+
code: 'network_quiet_timeout',
|
|
4994
|
+
message: `Timed out waiting for ${quietMs}ms of quiet network activity.`,
|
|
4995
|
+
},
|
|
4996
|
+
};
|
|
4997
|
+
}
|
|
4998
|
+
async function runAutomationWait(options) {
|
|
4999
|
+
switch (options.wait.waitKind) {
|
|
5000
|
+
case 'url':
|
|
5001
|
+
return waitForUrlCondition(options.sessionId, options.wait, options.capturePageState);
|
|
5002
|
+
case 'navigation': {
|
|
5003
|
+
const db = options.getDb?.();
|
|
5004
|
+
if (!db) {
|
|
5005
|
+
throw new Error('navigation waits require database access');
|
|
5006
|
+
}
|
|
5007
|
+
return waitForNavigationCondition(options.sessionId, options.wait, db);
|
|
5008
|
+
}
|
|
5009
|
+
case 'navigation_lifecycle':
|
|
5010
|
+
return waitForNavigationLifecycleCondition(options.sessionId, options.wait, options.captureClient);
|
|
5011
|
+
case 'load_state':
|
|
5012
|
+
return waitForLoadStateCondition(options.sessionId, options.wait, options.capturePageState);
|
|
5013
|
+
case 'selector_state':
|
|
5014
|
+
return waitForSelectorStateCondition(options.sessionId, options.wait, options.captureClient);
|
|
5015
|
+
case 'console':
|
|
5016
|
+
return waitForConsoleCondition(options.sessionId, options.wait, options.captureClient);
|
|
5017
|
+
case 'dialog':
|
|
5018
|
+
return waitForDialogCondition(options.sessionId, options.wait, options.captureClient);
|
|
5019
|
+
case 'stable_layout':
|
|
5020
|
+
return waitForStableLayoutCondition(options.sessionId, options.wait, options.captureClient);
|
|
5021
|
+
case 'download':
|
|
5022
|
+
return waitForDownloadCondition(options.sessionId, options.wait, options.captureClient);
|
|
5023
|
+
case 'popup':
|
|
5024
|
+
return waitForPopupCondition(options.sessionId, options.wait, options.captureClient);
|
|
5025
|
+
case 'network_quiet': {
|
|
5026
|
+
const db = options.getDb?.();
|
|
5027
|
+
if (!db) {
|
|
5028
|
+
throw new Error('network_quiet waits require database access');
|
|
5029
|
+
}
|
|
5030
|
+
return waitForNetworkQuietCondition(options.sessionId, options.wait, db);
|
|
5031
|
+
}
|
|
5032
|
+
case 'request':
|
|
5033
|
+
case 'response': {
|
|
5034
|
+
const db = options.getDb?.();
|
|
5035
|
+
if (!db) {
|
|
5036
|
+
throw new Error(`${options.wait.waitKind} waits require database access`);
|
|
5037
|
+
}
|
|
5038
|
+
return waitForNetworkMatchCondition(options.sessionId, options.wait, db);
|
|
5039
|
+
}
|
|
5040
|
+
}
|
|
5041
|
+
}
|
|
5042
|
+
function getSessionRow(db, sessionId) {
|
|
5043
|
+
return db.prepare(`
|
|
5044
|
+
SELECT
|
|
5045
|
+
session_id,
|
|
5046
|
+
created_at,
|
|
5047
|
+
last_seen_at,
|
|
5048
|
+
paused_at,
|
|
5049
|
+
ended_at,
|
|
5050
|
+
tab_id,
|
|
5051
|
+
window_id,
|
|
5052
|
+
url_start,
|
|
5053
|
+
url_last,
|
|
5054
|
+
user_agent,
|
|
5055
|
+
viewport_w,
|
|
5056
|
+
viewport_h,
|
|
5057
|
+
dpr,
|
|
5058
|
+
safe_mode,
|
|
5059
|
+
pinned
|
|
5060
|
+
FROM sessions
|
|
5061
|
+
WHERE session_id = ?
|
|
5062
|
+
LIMIT 1
|
|
5063
|
+
`).get(sessionId);
|
|
5064
|
+
}
|
|
5065
|
+
function looksSensitiveText(value) {
|
|
5066
|
+
return typeof value === 'string'
|
|
5067
|
+
&& /(password|passwd|pwd|token|secret|auth|session|email|card|cvv|cvc|ssn|iban|payment|billing)/i.test(value);
|
|
5068
|
+
}
|
|
5069
|
+
function isSensitivePageInput(input) {
|
|
5070
|
+
const type = typeof input.type === 'string' ? input.type.toLowerCase() : '';
|
|
5071
|
+
return type === 'password'
|
|
5072
|
+
|| looksSensitiveText(input.selector)
|
|
5073
|
+
|| looksSensitiveText(input.label)
|
|
5074
|
+
|| looksSensitiveText(input.name)
|
|
5075
|
+
|| looksSensitiveText(input.placeholder)
|
|
5076
|
+
|| looksSensitiveText(input.testId);
|
|
5077
|
+
}
|
|
5078
|
+
function collectAutomationPageRisks(payload) {
|
|
5079
|
+
if (!payload) {
|
|
5080
|
+
return {
|
|
5081
|
+
sensitiveInputs: [],
|
|
5082
|
+
frameCount: 0,
|
|
5083
|
+
crossOriginFrameCount: 0,
|
|
3122
5084
|
};
|
|
3123
5085
|
}
|
|
3124
|
-
const
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
.
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
5086
|
+
const inputs = asRecordArray(payload.inputs);
|
|
5087
|
+
const frames = asRecordArray(payload.frames);
|
|
5088
|
+
const sensitiveInputs = inputs
|
|
5089
|
+
.filter(isSensitivePageInput)
|
|
5090
|
+
.slice(0, 8)
|
|
5091
|
+
.map((input) => ({
|
|
5092
|
+
selector: input.selector,
|
|
5093
|
+
type: input.type,
|
|
5094
|
+
label: input.label,
|
|
5095
|
+
name: input.name,
|
|
5096
|
+
placeholder: input.placeholder,
|
|
5097
|
+
frameId: input.frameId,
|
|
5098
|
+
frameUrl: input.frameUrl,
|
|
5099
|
+
}));
|
|
5100
|
+
const crossOriginFrames = frames
|
|
5101
|
+
.filter((frame) => frame.sameOrigin === false || frame.accessible === false || frame.crossOrigin === true)
|
|
5102
|
+
.slice(0, 8)
|
|
5103
|
+
.map((frame) => ({
|
|
5104
|
+
frameId: frame.frameId,
|
|
5105
|
+
url: frame.url ?? frame.frameUrl,
|
|
5106
|
+
title: frame.title ?? frame.frameTitle,
|
|
5107
|
+
sameOrigin: frame.sameOrigin,
|
|
5108
|
+
accessible: frame.accessible,
|
|
5109
|
+
}));
|
|
5110
|
+
return {
|
|
5111
|
+
sensitiveInputs,
|
|
5112
|
+
sensitiveInputCount: sensitiveInputs.length,
|
|
5113
|
+
frameCount: frames.length,
|
|
5114
|
+
crossOriginFrameCount: crossOriginFrames.length,
|
|
5115
|
+
crossOriginFrames,
|
|
5116
|
+
};
|
|
5117
|
+
}
|
|
5118
|
+
async function buildAutomationFlowPreflight(options) {
|
|
5119
|
+
const blockers = [];
|
|
5120
|
+
const warnings = [];
|
|
5121
|
+
const includePageState = options.input.includePageState !== false;
|
|
5122
|
+
const expectedUrlContains = normalizeOptionalString(options.input.expectedUrlContains);
|
|
5123
|
+
const requireSensitiveAutomation = options.input.requireSensitiveAutomation === true;
|
|
5124
|
+
const plannedActions = Array.isArray(options.input.plannedActions)
|
|
5125
|
+
? options.input.plannedActions.filter((entry) => typeof entry === 'string' && entry.trim().length > 0)
|
|
5126
|
+
: [];
|
|
5127
|
+
const db = options.getDb?.();
|
|
5128
|
+
const session = db ? getSessionRow(db, options.sessionId) : undefined;
|
|
5129
|
+
const sessionState = options.getSessionConnectionState?.(options.sessionId);
|
|
5130
|
+
const hasLiveConnectionLookup = typeof options.getSessionConnectionState === 'function';
|
|
5131
|
+
const scope = classifySessionUrl(session?.url_last ?? undefined);
|
|
5132
|
+
const liveConnection = session
|
|
5133
|
+
? buildLiveConnectionRecord(session, scope, sessionState)
|
|
5134
|
+
: {
|
|
5135
|
+
connected: sessionState?.connected === true,
|
|
5136
|
+
status: sessionState?.connected === true ? 'connected' : 'unknown',
|
|
5137
|
+
recommendedForLiveCapture: false,
|
|
5138
|
+
};
|
|
5139
|
+
if (!db) {
|
|
5140
|
+
warnings.push({
|
|
5141
|
+
code: 'DB_UNAVAILABLE',
|
|
5142
|
+
severity: 'warning',
|
|
5143
|
+
source: 'server',
|
|
5144
|
+
message: 'Database access was not available; session history checks were skipped.',
|
|
3140
5145
|
});
|
|
3141
5146
|
}
|
|
3142
|
-
if (
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
5147
|
+
if (!session) {
|
|
5148
|
+
blockers.push({
|
|
5149
|
+
code: 'SESSION_NOT_FOUND',
|
|
5150
|
+
severity: 'error',
|
|
5151
|
+
source: 'session',
|
|
5152
|
+
message: `Session not found: ${options.sessionId}`,
|
|
3147
5153
|
});
|
|
3148
5154
|
}
|
|
3149
|
-
|
|
5155
|
+
else {
|
|
5156
|
+
const status = getSessionStatus(session);
|
|
5157
|
+
if (status === 'paused') {
|
|
5158
|
+
blockers.push({
|
|
5159
|
+
code: 'SESSION_PAUSED',
|
|
5160
|
+
severity: 'error',
|
|
5161
|
+
source: 'session',
|
|
5162
|
+
message: 'Resume the session before running an automation flow.',
|
|
5163
|
+
});
|
|
5164
|
+
}
|
|
5165
|
+
if (status === 'ended') {
|
|
5166
|
+
blockers.push({
|
|
5167
|
+
code: 'SESSION_ENDED',
|
|
5168
|
+
severity: 'error',
|
|
5169
|
+
source: 'session',
|
|
5170
|
+
message: 'Start a new session before running an automation flow.',
|
|
5171
|
+
});
|
|
5172
|
+
}
|
|
5173
|
+
if (scope.kind === 'likely_iframe_noise') {
|
|
5174
|
+
blockers.push({
|
|
5175
|
+
code: 'SESSION_SCOPE_NOISE',
|
|
5176
|
+
severity: 'error',
|
|
5177
|
+
source: 'session',
|
|
5178
|
+
message: 'The selected session appears to be bound to iframe/ad traffic rather than the app surface.',
|
|
5179
|
+
});
|
|
5180
|
+
}
|
|
5181
|
+
if (scope.kind === 'top_level_page' && scope.isLocalhost !== true) {
|
|
5182
|
+
warnings.push({
|
|
5183
|
+
code: 'PRODUCTION_OR_REMOTE_ORIGIN',
|
|
5184
|
+
severity: 'warning',
|
|
5185
|
+
source: 'session',
|
|
5186
|
+
message: 'The current session URL is remote/production-like. Keep the flow scoped and avoid destructive actions.',
|
|
5187
|
+
origin: scope.origin,
|
|
5188
|
+
});
|
|
5189
|
+
}
|
|
5190
|
+
if (expectedUrlContains && !String(session.url_last ?? '').includes(expectedUrlContains)) {
|
|
5191
|
+
blockers.push({
|
|
5192
|
+
code: 'EXPECTED_URL_MISMATCH',
|
|
5193
|
+
severity: 'error',
|
|
5194
|
+
source: 'session',
|
|
5195
|
+
message: `Current session URL does not include "${expectedUrlContains}".`,
|
|
5196
|
+
currentUrl: session.url_last,
|
|
5197
|
+
});
|
|
5198
|
+
}
|
|
5199
|
+
}
|
|
5200
|
+
if (hasLiveConnectionLookup && (!sessionState || sessionState.connected !== true)) {
|
|
5201
|
+
blockers.push({
|
|
5202
|
+
code: LIVE_SESSION_DISCONNECTED_CODE,
|
|
5203
|
+
severity: 'error',
|
|
5204
|
+
source: 'connection',
|
|
5205
|
+
message: 'The session is not currently connected to a live extension target.',
|
|
5206
|
+
disconnectedAt: sessionState?.disconnectedAt,
|
|
5207
|
+
disconnectReason: sessionState?.disconnectReason,
|
|
5208
|
+
});
|
|
5209
|
+
}
|
|
5210
|
+
let pageCapture;
|
|
5211
|
+
if (includePageState && blockers.length === 0) {
|
|
5212
|
+
try {
|
|
5213
|
+
pageCapture = await options.capturePageState(options.sessionId, {
|
|
5214
|
+
includeButtons: true,
|
|
5215
|
+
includeLinks: true,
|
|
5216
|
+
includeInputs: true,
|
|
5217
|
+
includeModals: true,
|
|
5218
|
+
maxItems: resolveLimit(options.input.maxItems, 40),
|
|
5219
|
+
maxTextLength: resolveDurationMs(options.input.maxTextLength, 80, 200),
|
|
5220
|
+
});
|
|
5221
|
+
}
|
|
5222
|
+
catch (error) {
|
|
5223
|
+
blockers.push({
|
|
5224
|
+
code: isLiveSessionDisconnectedError(error) ? LIVE_SESSION_DISCONNECTED_CODE : 'PAGE_STATE_CAPTURE_FAILED',
|
|
5225
|
+
severity: 'error',
|
|
5226
|
+
source: 'page-state',
|
|
5227
|
+
message: error instanceof Error ? error.message : String(error),
|
|
5228
|
+
});
|
|
5229
|
+
}
|
|
5230
|
+
}
|
|
5231
|
+
const pageRisks = collectAutomationPageRisks(pageCapture?.payload);
|
|
5232
|
+
const sensitiveInputs = Array.isArray(pageRisks.sensitiveInputs) ? pageRisks.sensitiveInputs : [];
|
|
5233
|
+
const hasInputLikeAction = plannedActions.some((action) => ['input', 'type', 'clear', 'select_option', 'press_key'].includes(action));
|
|
5234
|
+
if (sensitiveInputs.length > 0 && (requireSensitiveAutomation || hasInputLikeAction)) {
|
|
5235
|
+
warnings.push({
|
|
5236
|
+
code: 'SENSITIVE_FIELD_AUTOMATION_RISK',
|
|
5237
|
+
severity: 'warning',
|
|
5238
|
+
source: 'page-state',
|
|
5239
|
+
message: 'Sensitive-looking fields are present. The extension sensitive-field opt-in may be required before input-like actions.',
|
|
5240
|
+
count: sensitiveInputs.length,
|
|
5241
|
+
sampledInputs: sensitiveInputs,
|
|
5242
|
+
});
|
|
5243
|
+
}
|
|
5244
|
+
if (typeof pageRisks.crossOriginFrameCount === 'number' && pageRisks.crossOriginFrameCount > 0) {
|
|
5245
|
+
warnings.push({
|
|
5246
|
+
code: 'CROSS_ORIGIN_FRAME_PRESENT',
|
|
5247
|
+
severity: 'warning',
|
|
5248
|
+
source: 'page-state',
|
|
5249
|
+
message: 'Cross-origin or inaccessible frames are present. Automation inside those frames may be diagnostic-only.',
|
|
5250
|
+
count: pageRisks.crossOriginFrameCount,
|
|
5251
|
+
frames: pageRisks.crossOriginFrames,
|
|
5252
|
+
});
|
|
5253
|
+
}
|
|
5254
|
+
const ready = blockers.length === 0;
|
|
3150
5255
|
return {
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
},
|
|
3164
|
-
|
|
5256
|
+
ready,
|
|
5257
|
+
blockers,
|
|
5258
|
+
warnings,
|
|
5259
|
+
checks: {
|
|
5260
|
+
sessionFound: Boolean(session),
|
|
5261
|
+
liveConnected: sessionState?.connected === true || (hasLiveConnectionLookup ? false : undefined),
|
|
5262
|
+
recommendedForLiveCapture: liveConnection.recommendedForLiveCapture,
|
|
5263
|
+
expectedUrlMatched: expectedUrlContains ? blockers.every((blocker) => blocker.code !== 'EXPECTED_URL_MISMATCH') : undefined,
|
|
5264
|
+
pageStateCaptured: pageCapture !== undefined,
|
|
5265
|
+
remoteOrProductionLike: scope.kind === 'top_level_page' && scope.isLocalhost !== true,
|
|
5266
|
+
sensitiveInputCount: sensitiveInputs.length,
|
|
5267
|
+
crossOriginFrameCount: pageRisks.crossOriginFrameCount,
|
|
5268
|
+
},
|
|
5269
|
+
session: session
|
|
5270
|
+
? {
|
|
5271
|
+
sessionId: session.session_id,
|
|
5272
|
+
status: getSessionStatus(session),
|
|
5273
|
+
tabId: session.tab_id ?? undefined,
|
|
5274
|
+
windowId: session.window_id ?? undefined,
|
|
5275
|
+
urlStart: session.url_start ?? undefined,
|
|
5276
|
+
urlLast: session.url_last ?? undefined,
|
|
5277
|
+
lastSeenAt: resolveSessionLastSeenAt(session, sessionState),
|
|
5278
|
+
safeMode: session.safe_mode === 1,
|
|
5279
|
+
}
|
|
5280
|
+
: undefined,
|
|
5281
|
+
scope,
|
|
5282
|
+
liveConnection,
|
|
5283
|
+
page: pageCapture
|
|
5284
|
+
? {
|
|
5285
|
+
url: pageCapture.payload.url,
|
|
5286
|
+
title: pageCapture.payload.title,
|
|
5287
|
+
language: pageCapture.payload.language,
|
|
5288
|
+
viewport: pageCapture.payload.viewport,
|
|
5289
|
+
summary: pageCapture.payload.summary,
|
|
5290
|
+
}
|
|
5291
|
+
: undefined,
|
|
5292
|
+
detectedRisks: pageRisks,
|
|
5293
|
+
nextActions: ready
|
|
5294
|
+
? [{ code: 'RUN_FLOW', message: 'Run the automation flow with bounded waits and failure capture enabled.' }]
|
|
5295
|
+
: blockers.map((blocker) => ({
|
|
5296
|
+
code: String(blocker.code ?? 'FIX_BLOCKER'),
|
|
5297
|
+
message: String(blocker.message ?? 'Resolve this preflight blocker before running the flow.'),
|
|
5298
|
+
})),
|
|
3165
5299
|
};
|
|
3166
5300
|
}
|
|
3167
5301
|
function createWorkflowStepId(step, index) {
|
|
@@ -3172,6 +5306,7 @@ async function captureWorkflowPageState(sessionId, capturePageState, mode) {
|
|
|
3172
5306
|
const maxTextLength = mode === 'fast' ? 60 : 80;
|
|
3173
5307
|
return capturePageState(sessionId, {
|
|
3174
5308
|
includeButtons: true,
|
|
5309
|
+
includeLinks: true,
|
|
3175
5310
|
includeInputs: true,
|
|
3176
5311
|
includeModals: true,
|
|
3177
5312
|
maxItems,
|
|
@@ -3226,6 +5361,20 @@ function resolveWorkflowRecommendedAction(error) {
|
|
|
3226
5361
|
if (error.code === 'page_state_not_matched' || error.code === 'page_state_assertion_failed') {
|
|
3227
5362
|
return 'inspect_page_state';
|
|
3228
5363
|
}
|
|
5364
|
+
if (error.code === 'url_wait_timeout' || error.code === 'navigation_wait_timeout') {
|
|
5365
|
+
return 'inspect_navigation_state';
|
|
5366
|
+
}
|
|
5367
|
+
if (error.code === 'selector_state_wait_timeout') {
|
|
5368
|
+
return 'inspect_selector_state';
|
|
5369
|
+
}
|
|
5370
|
+
if (error.code === 'console_wait_timeout') {
|
|
5371
|
+
return 'inspect_live_console_logs';
|
|
5372
|
+
}
|
|
5373
|
+
if (error.code === 'network_quiet_timeout'
|
|
5374
|
+
|| error.code === 'request_wait_timeout'
|
|
5375
|
+
|| error.code === 'response_wait_timeout') {
|
|
5376
|
+
return 'inspect_network_calls';
|
|
5377
|
+
}
|
|
3229
5378
|
return undefined;
|
|
3230
5379
|
}
|
|
3231
5380
|
function resolveWorkflowFailureSelector(step, stepResultTarget) {
|
|
@@ -3970,15 +6119,17 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
|
|
|
3970
6119
|
recommendedAction,
|
|
3971
6120
|
};
|
|
3972
6121
|
},
|
|
3973
|
-
list_override_profiles: async () => {
|
|
6122
|
+
list_override_profiles: async (input) => {
|
|
3974
6123
|
const profiles = buildOverrideProfileRecords();
|
|
6124
|
+
const responseProfile = resolveOverrideResponseProfile(input.responseProfile);
|
|
3975
6125
|
return {
|
|
3976
6126
|
...createBaseResponse(),
|
|
3977
6127
|
limitsApplied: {
|
|
3978
6128
|
maxResults: profiles.length,
|
|
3979
6129
|
truncated: false,
|
|
3980
6130
|
},
|
|
3981
|
-
|
|
6131
|
+
responseProfile,
|
|
6132
|
+
profiles: profiles.map((profile) => serializeOverrideProfile(profile, responseProfile)),
|
|
3982
6133
|
nextActions: profiles.length > 0
|
|
3983
6134
|
? [{ code: 'VALIDATE_PROFILE', message: 'Run validate_override_profile before enabling overrides.' }]
|
|
3984
6135
|
: [{ code: 'CREATE_PROFILE', message: 'Run create_override_profile to generate a candidate profile.' }],
|
|
@@ -4016,6 +6167,8 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
|
|
|
4016
6167
|
});
|
|
4017
6168
|
const writeConfig = normalizeOptionalBooleanInput(input.writeConfig, 'writeConfig') ?? false;
|
|
4018
6169
|
const overwrite = normalizeOptionalBooleanInput(input.overwrite, 'overwrite') ?? false;
|
|
6170
|
+
const responseProfile = resolveOverrideResponseProfile(input.responseProfile);
|
|
6171
|
+
const includeConfigJson = input.includeConfigJson === true || responseProfile === 'full';
|
|
4019
6172
|
const write = {
|
|
4020
6173
|
written: false,
|
|
4021
6174
|
path: generated.suggestedConfigPath,
|
|
@@ -4064,21 +6217,31 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
|
|
|
4064
6217
|
warnings: generated.warnings,
|
|
4065
6218
|
nextActions,
|
|
4066
6219
|
write,
|
|
4067
|
-
|
|
4068
|
-
|
|
4069
|
-
|
|
6220
|
+
responseProfile,
|
|
6221
|
+
profile: responseProfile === 'full' ? generated.profile : compactOverrideProfile(generated.profile),
|
|
6222
|
+
config: responseProfile === 'full'
|
|
6223
|
+
? generated.config
|
|
6224
|
+
: {
|
|
6225
|
+
enabled: generated.config.enabled,
|
|
6226
|
+
activeProfileId: generated.config.activeProfileId,
|
|
6227
|
+
profileCount: generated.config.profiles.length,
|
|
6228
|
+
},
|
|
6229
|
+
configJson: includeConfigJson ? generated.configJson : undefined,
|
|
6230
|
+
configJsonOmitted: !includeConfigJson,
|
|
4070
6231
|
};
|
|
4071
6232
|
},
|
|
4072
6233
|
validate_override_profile: async (input) => {
|
|
4073
6234
|
const profile = resolveOverrideProfileRecord(input.profileId);
|
|
4074
6235
|
const issues = buildOverrideProfileIssues(profile);
|
|
6236
|
+
const responseProfile = resolveOverrideResponseProfile(input.responseProfile);
|
|
4075
6237
|
return {
|
|
4076
6238
|
...createBaseResponse(),
|
|
4077
6239
|
profileId: profile.profileId,
|
|
4078
6240
|
valid: !issues.some((issue) => issue.severity === 'error'),
|
|
4079
6241
|
issues,
|
|
4080
6242
|
nextActions: buildOverrideProfileNextActions(profile, issues),
|
|
4081
|
-
|
|
6243
|
+
responseProfile,
|
|
6244
|
+
profile: serializeOverrideProfile(profile, responseProfile),
|
|
4082
6245
|
};
|
|
4083
6246
|
},
|
|
4084
6247
|
preflight_overrides: async (input) => {
|
|
@@ -4105,16 +6268,18 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
|
|
|
4105
6268
|
}
|
|
4106
6269
|
const assets = listObservedOverrideAssets(getDb(), {
|
|
4107
6270
|
sessionId,
|
|
4108
|
-
limit: typeof input.limit === 'number' ? input.limit :
|
|
6271
|
+
limit: typeof input.limit === 'number' ? input.limit : 50,
|
|
4109
6272
|
sinceTimestamp: typeof input.sinceTimestamp === 'number' ? input.sinceTimestamp : undefined,
|
|
4110
6273
|
});
|
|
6274
|
+
const responseProfile = resolveOverrideResponseProfile(input.responseProfile);
|
|
4111
6275
|
return {
|
|
4112
6276
|
...createBaseResponse(sessionId),
|
|
4113
6277
|
limitsApplied: {
|
|
4114
6278
|
maxResults: assets.length,
|
|
4115
6279
|
truncated: false,
|
|
4116
6280
|
},
|
|
4117
|
-
|
|
6281
|
+
responseProfile,
|
|
6282
|
+
assets: responseProfile === 'full' ? assets : assets.map(compactObservedOverrideAsset),
|
|
4118
6283
|
};
|
|
4119
6284
|
},
|
|
4120
6285
|
plan_override_response_patch: async (input) => {
|
|
@@ -5562,9 +7727,10 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
|
|
|
5562
7727
|
r.status,
|
|
5563
7728
|
r.started_at,
|
|
5564
7729
|
r.completed_at,
|
|
5565
|
-
|
|
5566
|
-
|
|
5567
|
-
|
|
7730
|
+
r.stop_reason,
|
|
7731
|
+
r.target_summary_json,
|
|
7732
|
+
r.diagnostics_json,
|
|
7733
|
+
r.failure_json,
|
|
5568
7734
|
r.redaction_json,
|
|
5569
7735
|
r.created_at,
|
|
5570
7736
|
r.updated_at,
|
|
@@ -5626,9 +7792,10 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
|
|
|
5626
7792
|
r.status,
|
|
5627
7793
|
r.started_at,
|
|
5628
7794
|
r.completed_at,
|
|
5629
|
-
|
|
5630
|
-
|
|
5631
|
-
|
|
7795
|
+
r.stop_reason,
|
|
7796
|
+
r.target_summary_json,
|
|
7797
|
+
r.diagnostics_json,
|
|
7798
|
+
r.failure_json,
|
|
5632
7799
|
r.redaction_json,
|
|
5633
7800
|
r.created_at,
|
|
5634
7801
|
r.updated_at,
|
|
@@ -5660,9 +7827,10 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
|
|
|
5660
7827
|
started_at,
|
|
5661
7828
|
finished_at,
|
|
5662
7829
|
duration_ms,
|
|
5663
|
-
|
|
5664
|
-
|
|
5665
|
-
|
|
7830
|
+
tab_id,
|
|
7831
|
+
target_summary_json,
|
|
7832
|
+
diagnostics_json,
|
|
7833
|
+
redaction_json,
|
|
5666
7834
|
failure_json,
|
|
5667
7835
|
input_metadata_json,
|
|
5668
7836
|
event_type,
|
|
@@ -5696,12 +7864,14 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
|
|
|
5696
7864
|
const maxItems = resolveStructuredMaxItems(input.maxItems, 40);
|
|
5697
7865
|
const maxTextLength = resolveStructuredTextLength(input.maxTextLength, 80);
|
|
5698
7866
|
const includeButtons = input.includeButtons !== false;
|
|
7867
|
+
const includeLinks = input.includeLinks !== false;
|
|
5699
7868
|
const includeInputs = input.includeInputs !== false;
|
|
5700
7869
|
const includeModals = input.includeModals !== false;
|
|
5701
7870
|
const capture = await executeLiveCapture(captureClient, sessionId, 'CAPTURE_PAGE_STATE', {
|
|
5702
7871
|
maxItems,
|
|
5703
7872
|
maxTextLength,
|
|
5704
7873
|
includeButtons,
|
|
7874
|
+
includeLinks,
|
|
5705
7875
|
includeInputs,
|
|
5706
7876
|
includeModals,
|
|
5707
7877
|
}, 4_000);
|
|
@@ -6376,7 +8546,10 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
|
|
|
6376
8546
|
throw new Error('selector is required');
|
|
6377
8547
|
}
|
|
6378
8548
|
const properties = asStringArray(input.properties, 64);
|
|
6379
|
-
const
|
|
8549
|
+
const frameId = typeof input.frameId === 'number' && Number.isFinite(input.frameId)
|
|
8550
|
+
? Math.max(0, Math.floor(input.frameId))
|
|
8551
|
+
: 0;
|
|
8552
|
+
const capture = await executeLiveCapture(captureClient, sessionId, 'CAPTURE_COMPUTED_STYLES', { selector, frameId, properties }, 3_000);
|
|
6380
8553
|
return {
|
|
6381
8554
|
...createBaseResponse(sessionId),
|
|
6382
8555
|
limitsApplied: {
|
|
@@ -6392,7 +8565,10 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
|
|
|
6392
8565
|
throw new Error('sessionId is required');
|
|
6393
8566
|
}
|
|
6394
8567
|
const selector = typeof input.selector === 'string' ? input.selector : undefined;
|
|
6395
|
-
const
|
|
8568
|
+
const frameId = typeof input.frameId === 'number' && Number.isFinite(input.frameId)
|
|
8569
|
+
? Math.max(0, Math.floor(input.frameId))
|
|
8570
|
+
: 0;
|
|
8571
|
+
const capture = await executeLiveCapture(captureClient, sessionId, 'CAPTURE_LAYOUT_METRICS', { selector, frameId }, 3_000);
|
|
6396
8572
|
return {
|
|
6397
8573
|
...createBaseResponse(sessionId),
|
|
6398
8574
|
limitsApplied: {
|
|
@@ -6423,6 +8599,7 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
|
|
|
6423
8599
|
const normalizedInput = {
|
|
6424
8600
|
...input,
|
|
6425
8601
|
includeButtons: kinds.includes('buttons'),
|
|
8602
|
+
includeLinks: kinds.includes('links'),
|
|
6426
8603
|
includeInputs: kinds.includes('inputs'),
|
|
6427
8604
|
includeModals: kinds.includes('modals'),
|
|
6428
8605
|
};
|
|
@@ -6503,6 +8680,247 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
|
|
|
6503
8680
|
...waited,
|
|
6504
8681
|
};
|
|
6505
8682
|
},
|
|
8683
|
+
preflight_automation_flow: async (input) => {
|
|
8684
|
+
const sessionId = getSessionId(input);
|
|
8685
|
+
if (!sessionId) {
|
|
8686
|
+
throw new Error('sessionId is required');
|
|
8687
|
+
}
|
|
8688
|
+
const preflight = await buildAutomationFlowPreflight({
|
|
8689
|
+
sessionId,
|
|
8690
|
+
input,
|
|
8691
|
+
capturePageState,
|
|
8692
|
+
getDb,
|
|
8693
|
+
getSessionConnectionState,
|
|
8694
|
+
});
|
|
8695
|
+
return {
|
|
8696
|
+
...createBaseResponse(sessionId),
|
|
8697
|
+
limitsApplied: {
|
|
8698
|
+
maxResults: 1,
|
|
8699
|
+
truncated: false,
|
|
8700
|
+
},
|
|
8701
|
+
...preflight,
|
|
8702
|
+
};
|
|
8703
|
+
},
|
|
8704
|
+
wait_for_url: async (input) => {
|
|
8705
|
+
const sessionId = getSessionId(input);
|
|
8706
|
+
if (!sessionId) {
|
|
8707
|
+
throw new Error('sessionId is required');
|
|
8708
|
+
}
|
|
8709
|
+
const wait = AutomationWaitUrlSchema.parse({ ...input, waitKind: 'url' });
|
|
8710
|
+
const waited = await waitForUrlCondition(sessionId, wait, capturePageState);
|
|
8711
|
+
return {
|
|
8712
|
+
...createBaseResponse(sessionId),
|
|
8713
|
+
limitsApplied: {
|
|
8714
|
+
maxResults: 1,
|
|
8715
|
+
truncated: false,
|
|
8716
|
+
},
|
|
8717
|
+
...waited,
|
|
8718
|
+
};
|
|
8719
|
+
},
|
|
8720
|
+
wait_for_navigation: async (input) => {
|
|
8721
|
+
const sessionId = getSessionId(input);
|
|
8722
|
+
if (!sessionId) {
|
|
8723
|
+
throw new Error('sessionId is required');
|
|
8724
|
+
}
|
|
8725
|
+
if (!getDb) {
|
|
8726
|
+
throw new Error('wait_for_navigation requires database access');
|
|
8727
|
+
}
|
|
8728
|
+
const wait = AutomationWaitNavigationSchema.parse({ ...input, waitKind: 'navigation' });
|
|
8729
|
+
const waited = await waitForNavigationCondition(sessionId, wait, getDb());
|
|
8730
|
+
return {
|
|
8731
|
+
...createBaseResponse(sessionId),
|
|
8732
|
+
limitsApplied: {
|
|
8733
|
+
maxResults: 10,
|
|
8734
|
+
truncated: false,
|
|
8735
|
+
},
|
|
8736
|
+
...waited,
|
|
8737
|
+
};
|
|
8738
|
+
},
|
|
8739
|
+
wait_for_navigation_lifecycle: async (input) => {
|
|
8740
|
+
const sessionId = getSessionId(input);
|
|
8741
|
+
if (!sessionId) {
|
|
8742
|
+
throw new Error('sessionId is required');
|
|
8743
|
+
}
|
|
8744
|
+
const wait = AutomationWaitNavigationLifecycleSchema.parse({ ...input, waitKind: 'navigation_lifecycle' });
|
|
8745
|
+
const waited = await waitForNavigationLifecycleCondition(sessionId, wait, captureClient);
|
|
8746
|
+
return {
|
|
8747
|
+
...createBaseResponse(sessionId),
|
|
8748
|
+
limitsApplied: {
|
|
8749
|
+
maxResults: 1,
|
|
8750
|
+
truncated: false,
|
|
8751
|
+
},
|
|
8752
|
+
...waited,
|
|
8753
|
+
};
|
|
8754
|
+
},
|
|
8755
|
+
wait_for_load_state: async (input) => {
|
|
8756
|
+
const sessionId = getSessionId(input);
|
|
8757
|
+
if (!sessionId) {
|
|
8758
|
+
throw new Error('sessionId is required');
|
|
8759
|
+
}
|
|
8760
|
+
const wait = AutomationWaitLoadStateSchema.parse({ ...input, waitKind: 'load_state' });
|
|
8761
|
+
const waited = await waitForLoadStateCondition(sessionId, wait, capturePageState);
|
|
8762
|
+
return {
|
|
8763
|
+
...createBaseResponse(sessionId),
|
|
8764
|
+
limitsApplied: {
|
|
8765
|
+
maxResults: 1,
|
|
8766
|
+
truncated: false,
|
|
8767
|
+
},
|
|
8768
|
+
...waited,
|
|
8769
|
+
};
|
|
8770
|
+
},
|
|
8771
|
+
wait_for_selector_state: async (input) => {
|
|
8772
|
+
const sessionId = getSessionId(input);
|
|
8773
|
+
if (!sessionId) {
|
|
8774
|
+
throw new Error('sessionId is required');
|
|
8775
|
+
}
|
|
8776
|
+
const wait = AutomationWaitSelectorStateSchema.parse({ ...input, waitKind: 'selector_state' });
|
|
8777
|
+
const waited = await waitForSelectorStateCondition(sessionId, wait, captureClient);
|
|
8778
|
+
return {
|
|
8779
|
+
...createBaseResponse(sessionId),
|
|
8780
|
+
limitsApplied: {
|
|
8781
|
+
maxResults: 1,
|
|
8782
|
+
truncated: false,
|
|
8783
|
+
},
|
|
8784
|
+
...waited,
|
|
8785
|
+
};
|
|
8786
|
+
},
|
|
8787
|
+
wait_for_request: async (input) => {
|
|
8788
|
+
const sessionId = getSessionId(input);
|
|
8789
|
+
if (!sessionId) {
|
|
8790
|
+
throw new Error('sessionId is required');
|
|
8791
|
+
}
|
|
8792
|
+
if (!getDb) {
|
|
8793
|
+
throw new Error('wait_for_request requires database access');
|
|
8794
|
+
}
|
|
8795
|
+
const wait = AutomationWaitRequestSchema.parse({ ...input, waitKind: 'request' });
|
|
8796
|
+
const waited = await waitForNetworkMatchCondition(sessionId, wait, getDb());
|
|
8797
|
+
return {
|
|
8798
|
+
...createBaseResponse(sessionId),
|
|
8799
|
+
limitsApplied: {
|
|
8800
|
+
maxResults: 10,
|
|
8801
|
+
truncated: false,
|
|
8802
|
+
},
|
|
8803
|
+
...waited,
|
|
8804
|
+
};
|
|
8805
|
+
},
|
|
8806
|
+
wait_for_response: async (input) => {
|
|
8807
|
+
const sessionId = getSessionId(input);
|
|
8808
|
+
if (!sessionId) {
|
|
8809
|
+
throw new Error('sessionId is required');
|
|
8810
|
+
}
|
|
8811
|
+
if (!getDb) {
|
|
8812
|
+
throw new Error('wait_for_response requires database access');
|
|
8813
|
+
}
|
|
8814
|
+
const wait = AutomationWaitResponseSchema.parse({ ...input, waitKind: 'response' });
|
|
8815
|
+
const waited = await waitForNetworkMatchCondition(sessionId, wait, getDb());
|
|
8816
|
+
return {
|
|
8817
|
+
...createBaseResponse(sessionId),
|
|
8818
|
+
limitsApplied: {
|
|
8819
|
+
maxResults: 10,
|
|
8820
|
+
truncated: false,
|
|
8821
|
+
},
|
|
8822
|
+
...waited,
|
|
8823
|
+
};
|
|
8824
|
+
},
|
|
8825
|
+
wait_for_console: async (input) => {
|
|
8826
|
+
const sessionId = getSessionId(input);
|
|
8827
|
+
if (!sessionId) {
|
|
8828
|
+
throw new Error('sessionId is required');
|
|
8829
|
+
}
|
|
8830
|
+
const wait = AutomationWaitConsoleSchema.parse({ ...input, waitKind: 'console' });
|
|
8831
|
+
const waited = await waitForConsoleCondition(sessionId, wait, captureClient);
|
|
8832
|
+
return {
|
|
8833
|
+
...createBaseResponse(sessionId),
|
|
8834
|
+
limitsApplied: {
|
|
8835
|
+
maxResults: 10,
|
|
8836
|
+
truncated: false,
|
|
8837
|
+
},
|
|
8838
|
+
...waited,
|
|
8839
|
+
};
|
|
8840
|
+
},
|
|
8841
|
+
wait_for_dialog: async (input) => {
|
|
8842
|
+
const sessionId = getSessionId(input);
|
|
8843
|
+
if (!sessionId) {
|
|
8844
|
+
throw new Error('sessionId is required');
|
|
8845
|
+
}
|
|
8846
|
+
const wait = AutomationWaitDialogSchema.parse({ ...input, waitKind: 'dialog' });
|
|
8847
|
+
const waited = await waitForDialogCondition(sessionId, wait, captureClient);
|
|
8848
|
+
return {
|
|
8849
|
+
...createBaseResponse(sessionId),
|
|
8850
|
+
limitsApplied: {
|
|
8851
|
+
maxResults: 1,
|
|
8852
|
+
truncated: false,
|
|
8853
|
+
},
|
|
8854
|
+
...waited,
|
|
8855
|
+
};
|
|
8856
|
+
},
|
|
8857
|
+
wait_for_stable_layout: async (input) => {
|
|
8858
|
+
const sessionId = getSessionId(input);
|
|
8859
|
+
if (!sessionId) {
|
|
8860
|
+
throw new Error('sessionId is required');
|
|
8861
|
+
}
|
|
8862
|
+
const wait = AutomationWaitStableLayoutSchema.parse({ ...input, waitKind: 'stable_layout' });
|
|
8863
|
+
const waited = await waitForStableLayoutCondition(sessionId, wait, captureClient);
|
|
8864
|
+
return {
|
|
8865
|
+
...createBaseResponse(sessionId),
|
|
8866
|
+
limitsApplied: {
|
|
8867
|
+
maxResults: 1,
|
|
8868
|
+
truncated: false,
|
|
8869
|
+
},
|
|
8870
|
+
...waited,
|
|
8871
|
+
};
|
|
8872
|
+
},
|
|
8873
|
+
wait_for_download: async (input) => {
|
|
8874
|
+
const sessionId = getSessionId(input);
|
|
8875
|
+
if (!sessionId) {
|
|
8876
|
+
throw new Error('sessionId is required');
|
|
8877
|
+
}
|
|
8878
|
+
const wait = AutomationWaitDownloadSchema.parse({ ...input, waitKind: 'download' });
|
|
8879
|
+
const waited = await waitForDownloadCondition(sessionId, wait, captureClient);
|
|
8880
|
+
return {
|
|
8881
|
+
...createBaseResponse(sessionId),
|
|
8882
|
+
limitsApplied: {
|
|
8883
|
+
maxResults: 1,
|
|
8884
|
+
truncated: false,
|
|
8885
|
+
},
|
|
8886
|
+
...waited,
|
|
8887
|
+
};
|
|
8888
|
+
},
|
|
8889
|
+
wait_for_popup: async (input) => {
|
|
8890
|
+
const sessionId = getSessionId(input);
|
|
8891
|
+
if (!sessionId) {
|
|
8892
|
+
throw new Error('sessionId is required');
|
|
8893
|
+
}
|
|
8894
|
+
const wait = AutomationWaitPopupSchema.parse({ ...input, waitKind: 'popup' });
|
|
8895
|
+
const waited = await waitForPopupCondition(sessionId, wait, captureClient);
|
|
8896
|
+
return {
|
|
8897
|
+
...createBaseResponse(sessionId),
|
|
8898
|
+
limitsApplied: {
|
|
8899
|
+
maxResults: 1,
|
|
8900
|
+
truncated: false,
|
|
8901
|
+
},
|
|
8902
|
+
...waited,
|
|
8903
|
+
};
|
|
8904
|
+
},
|
|
8905
|
+
wait_for_network_quiet: async (input) => {
|
|
8906
|
+
const sessionId = getSessionId(input);
|
|
8907
|
+
if (!sessionId) {
|
|
8908
|
+
throw new Error('sessionId is required');
|
|
8909
|
+
}
|
|
8910
|
+
if (!getDb) {
|
|
8911
|
+
throw new Error('wait_for_network_quiet requires database access');
|
|
8912
|
+
}
|
|
8913
|
+
const wait = AutomationWaitNetworkQuietSchema.parse({ ...input, waitKind: 'network_quiet' });
|
|
8914
|
+
const waited = await waitForNetworkQuietCondition(sessionId, wait, getDb());
|
|
8915
|
+
return {
|
|
8916
|
+
...createBaseResponse(sessionId),
|
|
8917
|
+
limitsApplied: {
|
|
8918
|
+
maxResults: 10,
|
|
8919
|
+
truncated: false,
|
|
8920
|
+
},
|
|
8921
|
+
...waited,
|
|
8922
|
+
};
|
|
8923
|
+
},
|
|
6506
8924
|
run_ui_steps: async (input) => {
|
|
6507
8925
|
const request = RunUIStepsSchema.parse(input);
|
|
6508
8926
|
const workflowTraceId = createUIWorkflowTraceId();
|
|
@@ -6530,7 +8948,16 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
|
|
|
6530
8948
|
const previousCapture = lastPageCapture;
|
|
6531
8949
|
try {
|
|
6532
8950
|
if (step.kind === 'action') {
|
|
6533
|
-
const resolvedTarget =
|
|
8951
|
+
const resolvedTarget = step.target?.locator
|
|
8952
|
+
? {
|
|
8953
|
+
target: step.target,
|
|
8954
|
+
resolution: {
|
|
8955
|
+
strategy: 'native_locator_pending',
|
|
8956
|
+
matcher: summarizeWorkflowTargetMatcher(step.target),
|
|
8957
|
+
},
|
|
8958
|
+
pageCapture: undefined,
|
|
8959
|
+
}
|
|
8960
|
+
: await resolveWorkflowActionTarget(request.sessionId, step.target, workflowCapturePageState, request.mode === 'fast' ? lastPageCapture : undefined);
|
|
6534
8961
|
const liveRequest = LiveUIActionRequestSchema.parse({
|
|
6535
8962
|
action: step.action,
|
|
6536
8963
|
target: resolvedTarget.target,
|
|
@@ -6541,6 +8968,12 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
|
|
|
6541
8968
|
const payload = ensureCaptureSuccess(capture, request.sessionId);
|
|
6542
8969
|
const actionResult = payload;
|
|
6543
8970
|
const failed = actionResult.status === 'failed' || actionResult.status === 'rejected';
|
|
8971
|
+
const actionResultPayload = typeof actionResult.result === 'object' && actionResult.result !== null
|
|
8972
|
+
? actionResult.result
|
|
8973
|
+
: undefined;
|
|
8974
|
+
const nativeLocatorResolution = typeof actionResultPayload?.locatorResolution === 'object' && actionResultPayload.locatorResolution !== null
|
|
8975
|
+
? actionResultPayload.locatorResolution
|
|
8976
|
+
: undefined;
|
|
6544
8977
|
let currentCapture = resolvedTarget.pageCapture ?? lastPageCapture;
|
|
6545
8978
|
if (!failed && request.mode === 'fast') {
|
|
6546
8979
|
await sleep(75);
|
|
@@ -6555,7 +8988,7 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
|
|
|
6555
8988
|
action: step.action,
|
|
6556
8989
|
traceId: actionResult.traceId,
|
|
6557
8990
|
target: {
|
|
6558
|
-
resolution: resolvedTarget.resolution,
|
|
8991
|
+
resolution: nativeLocatorResolution ?? resolvedTarget.resolution,
|
|
6559
8992
|
actionTarget: typeof actionResult.target === 'object' && actionResult.target !== null
|
|
6560
8993
|
? actionResult.target
|
|
6561
8994
|
: undefined,
|
|
@@ -6568,6 +9001,14 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
|
|
|
6568
9001
|
: undefined,
|
|
6569
9002
|
pageChangeSummary: createPageChangeSummary(previousCapture, currentCapture),
|
|
6570
9003
|
};
|
|
9004
|
+
if (failed && getDb && finalStepResult.traceId) {
|
|
9005
|
+
mergeAutomationDiagnosticsEvidence(getDb(), {
|
|
9006
|
+
sessionId: request.sessionId,
|
|
9007
|
+
traceId: finalStepResult.traceId,
|
|
9008
|
+
failureEvidence: finalStepResult.failureEvidence,
|
|
9009
|
+
cdpFailure: actionResult.failureReason,
|
|
9010
|
+
});
|
|
9011
|
+
}
|
|
6571
9012
|
}
|
|
6572
9013
|
else if (step.kind === 'waitFor') {
|
|
6573
9014
|
const waitInput = {
|
|
@@ -6595,6 +9036,48 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
|
|
|
6595
9036
|
pageChangeSummary: createPageChangeSummary(previousCapture, waited.lastCapture),
|
|
6596
9037
|
};
|
|
6597
9038
|
}
|
|
9039
|
+
else if (step.kind === 'wait') {
|
|
9040
|
+
const waitSpec = AutomationWaitSpecSchema.parse({
|
|
9041
|
+
...step.wait,
|
|
9042
|
+
timeoutMs: step.wait.timeoutMs ?? request.defaultTimeoutMs,
|
|
9043
|
+
pollIntervalMs: step.wait.pollIntervalMs ?? request.defaultPollIntervalMs,
|
|
9044
|
+
});
|
|
9045
|
+
const waited = await runAutomationWait({
|
|
9046
|
+
sessionId: request.sessionId,
|
|
9047
|
+
wait: waitSpec,
|
|
9048
|
+
capturePageState: workflowCapturePageState,
|
|
9049
|
+
captureClient,
|
|
9050
|
+
getDb,
|
|
9051
|
+
});
|
|
9052
|
+
if (waited.waitKind === 'url' || waited.waitKind === 'navigation' || waited.waitKind === 'load_state') {
|
|
9053
|
+
lastPageCapture = await workflowCapturePageState(request.sessionId, {
|
|
9054
|
+
includeButtons: true,
|
|
9055
|
+
includeLinks: true,
|
|
9056
|
+
includeInputs: true,
|
|
9057
|
+
includeModals: true,
|
|
9058
|
+
maxItems: request.mode === 'fast' ? 12 : 20,
|
|
9059
|
+
maxTextLength: request.mode === 'fast' ? 60 : 80,
|
|
9060
|
+
}).catch(() => lastPageCapture);
|
|
9061
|
+
}
|
|
9062
|
+
finalStepResult = {
|
|
9063
|
+
id: stepId,
|
|
9064
|
+
kind: step.kind,
|
|
9065
|
+
status: waited.matched ? 'succeeded' : 'failed',
|
|
9066
|
+
durationMs: Math.max(0, Date.now() - startedAt),
|
|
9067
|
+
wait: {
|
|
9068
|
+
...waitSpec,
|
|
9069
|
+
waitKind: waited.waitKind,
|
|
9070
|
+
matched: waited.matched,
|
|
9071
|
+
timeoutMs: waited.timeoutMs,
|
|
9072
|
+
pollIntervalMs: waited.pollIntervalMs,
|
|
9073
|
+
},
|
|
9074
|
+
waitedMs: waited.waitedMs,
|
|
9075
|
+
attempts: waited.attempts,
|
|
9076
|
+
error: waited.error,
|
|
9077
|
+
pageChangeSummary: createPageChangeSummary(previousCapture, lastPageCapture),
|
|
9078
|
+
target: waited.evidence,
|
|
9079
|
+
};
|
|
9080
|
+
}
|
|
6598
9081
|
else {
|
|
6599
9082
|
const capture = request.mode === 'fast' && lastPageCapture
|
|
6600
9083
|
? lastPageCapture
|
|
@@ -6629,7 +9112,8 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
|
|
|
6629
9112
|
target: step.kind === 'action' && workflowError
|
|
6630
9113
|
? workflowError.details
|
|
6631
9114
|
: undefined,
|
|
6632
|
-
matcher: step.kind === '
|
|
9115
|
+
matcher: step.kind === 'assert' || step.kind === 'waitFor' ? step.matcher : undefined,
|
|
9116
|
+
wait: step.kind === 'wait' ? step.wait : undefined,
|
|
6633
9117
|
error: normalizeWorkflowError(error),
|
|
6634
9118
|
};
|
|
6635
9119
|
}
|
|
@@ -6652,6 +9136,14 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
|
|
|
6652
9136
|
if (evidence) {
|
|
6653
9137
|
failureCaptureCount += 1;
|
|
6654
9138
|
finalStepResult.failureEvidence = evidence;
|
|
9139
|
+
if (getDb && finalStepResult.traceId) {
|
|
9140
|
+
mergeAutomationDiagnosticsEvidence(getDb(), {
|
|
9141
|
+
sessionId: request.sessionId,
|
|
9142
|
+
traceId: finalStepResult.traceId,
|
|
9143
|
+
failureEvidence: evidence,
|
|
9144
|
+
cdpFailure: finalStepResult.error,
|
|
9145
|
+
});
|
|
9146
|
+
}
|
|
6655
9147
|
}
|
|
6656
9148
|
}
|
|
6657
9149
|
stepResults.push(finalStepResult);
|
|
@@ -6671,7 +9163,8 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
|
|
|
6671
9163
|
status: 'skipped',
|
|
6672
9164
|
durationMs: 0,
|
|
6673
9165
|
action: step.kind === 'action' ? step.action : undefined,
|
|
6674
|
-
matcher: step.kind === '
|
|
9166
|
+
matcher: step.kind === 'assert' || step.kind === 'waitFor' ? step.matcher : undefined,
|
|
9167
|
+
wait: step.kind === 'wait' ? step.wait : undefined,
|
|
6675
9168
|
pageChangeSummary: undefined,
|
|
6676
9169
|
error: {
|
|
6677
9170
|
code: 'workflow_stopped_early',
|
|
@@ -6871,7 +9364,62 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
|
|
|
6871
9364
|
const actionInput = { ...input };
|
|
6872
9365
|
delete actionInput.sessionId;
|
|
6873
9366
|
delete actionInput.captureOnFailure;
|
|
6874
|
-
|
|
9367
|
+
let request = LiveUIActionRequestSchema.parse(actionInput);
|
|
9368
|
+
let targetResolution;
|
|
9369
|
+
try {
|
|
9370
|
+
if (request.target?.locator) {
|
|
9371
|
+
targetResolution = {
|
|
9372
|
+
strategy: 'native_locator_pending',
|
|
9373
|
+
matcher: summarizeWorkflowTargetMatcher(request.target),
|
|
9374
|
+
};
|
|
9375
|
+
}
|
|
9376
|
+
else if (hasSemanticActionTargetMatcher(request.target)) {
|
|
9377
|
+
const resolvedTarget = await resolveWorkflowActionTarget(sessionId, request.target, capturePageState);
|
|
9378
|
+
targetResolution = resolvedTarget.resolution;
|
|
9379
|
+
request = LiveUIActionRequestSchema.parse({
|
|
9380
|
+
...request,
|
|
9381
|
+
target: resolvedTarget.target,
|
|
9382
|
+
});
|
|
9383
|
+
}
|
|
9384
|
+
}
|
|
9385
|
+
catch (error) {
|
|
9386
|
+
if (error instanceof WorkflowTargetResolutionError) {
|
|
9387
|
+
const traceId = request.traceId ?? `uiaction-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
9388
|
+
return {
|
|
9389
|
+
...createBaseResponse(sessionId),
|
|
9390
|
+
limitsApplied: {
|
|
9391
|
+
maxResults: 1,
|
|
9392
|
+
truncated: false,
|
|
9393
|
+
},
|
|
9394
|
+
action: request.action,
|
|
9395
|
+
status: 'rejected',
|
|
9396
|
+
traceId,
|
|
9397
|
+
startedAt: Date.now(),
|
|
9398
|
+
finishedAt: Date.now(),
|
|
9399
|
+
durationMs: 0,
|
|
9400
|
+
target: {
|
|
9401
|
+
matched: false,
|
|
9402
|
+
},
|
|
9403
|
+
tabContext: {
|
|
9404
|
+
frameId: 0,
|
|
9405
|
+
},
|
|
9406
|
+
failureDetails: {
|
|
9407
|
+
code: error.code,
|
|
9408
|
+
message: error.message,
|
|
9409
|
+
},
|
|
9410
|
+
targetResolution: {
|
|
9411
|
+
...error.details,
|
|
9412
|
+
strategy: 'semantic_failed',
|
|
9413
|
+
},
|
|
9414
|
+
supportedScopes: {
|
|
9415
|
+
executionScope: 'top-document-v1',
|
|
9416
|
+
topDocumentOnly: false,
|
|
9417
|
+
opensNewBrowserSession: false,
|
|
9418
|
+
},
|
|
9419
|
+
};
|
|
9420
|
+
}
|
|
9421
|
+
throw error;
|
|
9422
|
+
}
|
|
6875
9423
|
const failureCaptureOptions = resolveFailureEvidenceCaptureOptions(input);
|
|
6876
9424
|
const capture = await executeLiveCapture(captureClient, sessionId, 'EXECUTE_UI_ACTION', request, 5_000);
|
|
6877
9425
|
const payload = ensureCaptureSuccess(capture, sessionId);
|
|
@@ -6896,6 +9444,24 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
|
|
|
6896
9444
|
const target = typeof actionResult.target === 'object' && actionResult.target !== null
|
|
6897
9445
|
? actionResult.target
|
|
6898
9446
|
: {};
|
|
9447
|
+
const actionResultRecord = actionResult;
|
|
9448
|
+
const nativeResult = typeof actionResultRecord.result === 'object' && actionResultRecord.result !== null
|
|
9449
|
+
? actionResultRecord.result
|
|
9450
|
+
: undefined;
|
|
9451
|
+
const nativeLocatorResolution = typeof nativeResult?.locatorResolution === 'object' && nativeResult.locatorResolution !== null
|
|
9452
|
+
? nativeResult.locatorResolution
|
|
9453
|
+
: undefined;
|
|
9454
|
+
if (nativeLocatorResolution) {
|
|
9455
|
+
targetResolution = nativeLocatorResolution;
|
|
9456
|
+
}
|
|
9457
|
+
if (failed && getDb && actionResult.traceId) {
|
|
9458
|
+
mergeAutomationDiagnosticsEvidence(getDb(), {
|
|
9459
|
+
sessionId,
|
|
9460
|
+
traceId: actionResult.traceId,
|
|
9461
|
+
failureEvidence,
|
|
9462
|
+
cdpFailure: actionResult.failureReason,
|
|
9463
|
+
});
|
|
9464
|
+
}
|
|
6899
9465
|
return {
|
|
6900
9466
|
...createBaseResponse(sessionId),
|
|
6901
9467
|
limitsApplied: {
|
|
@@ -6914,6 +9480,7 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
|
|
|
6914
9480
|
: undefined,
|
|
6915
9481
|
actionResult,
|
|
6916
9482
|
target,
|
|
9483
|
+
targetResolution,
|
|
6917
9484
|
tabContext: {
|
|
6918
9485
|
tabId: typeof target.tabId === 'number' ? target.tabId : undefined,
|
|
6919
9486
|
frameId: typeof target.frameId === 'number' ? target.frameId : 0,
|
|
@@ -6924,7 +9491,7 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
|
|
|
6924
9491
|
postActionState,
|
|
6925
9492
|
supportedScopes: {
|
|
6926
9493
|
executionScope: actionResult.executionScope,
|
|
6927
|
-
topDocumentOnly:
|
|
9494
|
+
topDocumentOnly: false,
|
|
6928
9495
|
opensNewBrowserSession: false,
|
|
6929
9496
|
},
|
|
6930
9497
|
};
|
|
@@ -6976,13 +9543,37 @@ export function createToolRegistry(overrides = {}) {
|
|
|
6976
9543
|
};
|
|
6977
9544
|
});
|
|
6978
9545
|
}
|
|
6979
|
-
export async function routeToolCall(tools, toolName, input) {
|
|
9546
|
+
export async function routeToolCall(tools, toolName, input, options = {}) {
|
|
6980
9547
|
const tool = tools.find((candidate) => candidate.name === toolName);
|
|
6981
9548
|
if (!tool) {
|
|
6982
9549
|
throw new Error(`Unknown tool: ${toolName}`);
|
|
6983
9550
|
}
|
|
6984
|
-
const
|
|
6985
|
-
|
|
9551
|
+
const normalizedInput = isRecord(input) ? input : {};
|
|
9552
|
+
const guardCall = options.loopGuard?.prepareCall(toolName, normalizedInput);
|
|
9553
|
+
const beforeCall = guardCall ? await options.loopGuard?.beforeCall(guardCall) : undefined;
|
|
9554
|
+
if (beforeCall?.blocked) {
|
|
9555
|
+
return attachResponseBytes(beforeCall.response);
|
|
9556
|
+
}
|
|
9557
|
+
const startedAt = Date.now();
|
|
9558
|
+
try {
|
|
9559
|
+
const response = await tool.handler(normalizedInput);
|
|
9560
|
+
const guarded = guardCall
|
|
9561
|
+
? await options.loopGuard?.afterCall(guardCall, {
|
|
9562
|
+
response,
|
|
9563
|
+
durationMs: Date.now() - startedAt,
|
|
9564
|
+
})
|
|
9565
|
+
: undefined;
|
|
9566
|
+
return attachResponseBytes((guarded?.response ?? response));
|
|
9567
|
+
}
|
|
9568
|
+
catch (error) {
|
|
9569
|
+
if (guardCall) {
|
|
9570
|
+
await options.loopGuard?.afterCall(guardCall, {
|
|
9571
|
+
error,
|
|
9572
|
+
durationMs: Date.now() - startedAt,
|
|
9573
|
+
});
|
|
9574
|
+
}
|
|
9575
|
+
throw error;
|
|
9576
|
+
}
|
|
6986
9577
|
}
|
|
6987
9578
|
export function createMCPServer(overrides = {}, options = {}) {
|
|
6988
9579
|
const logger = options.logger ?? createDefaultMcpLogger();
|
|
@@ -6994,6 +9585,17 @@ export function createMCPServer(overrides = {}, options = {}) {
|
|
|
6994
9585
|
...v2Handlers,
|
|
6995
9586
|
...overrides,
|
|
6996
9587
|
});
|
|
9588
|
+
const loopGuard = options.loopGuard === false
|
|
9589
|
+
? undefined
|
|
9590
|
+
: options.loopGuard ?? createToolLoopGuard({
|
|
9591
|
+
getDb: () => getConnection().db,
|
|
9592
|
+
onEvent: (event) => {
|
|
9593
|
+
logger.info({
|
|
9594
|
+
component: 'mcp',
|
|
9595
|
+
...event,
|
|
9596
|
+
}, `[MCPServer][MCP] ${event.event}`);
|
|
9597
|
+
},
|
|
9598
|
+
});
|
|
6997
9599
|
const server = new Server({
|
|
6998
9600
|
name: 'browser-debug-mcp-bridge',
|
|
6999
9601
|
version: '1.0.0',
|
|
@@ -7017,7 +9619,7 @@ export function createMCPServer(overrides = {}, options = {}) {
|
|
|
7017
9619
|
const startedAt = Date.now();
|
|
7018
9620
|
logger.info({ component: 'mcp', event: 'tool_call_started', toolName }, '[MCPServer][MCP] Tool call started');
|
|
7019
9621
|
try {
|
|
7020
|
-
const response = await routeToolCall(tools, toolName, request.params.arguments);
|
|
9622
|
+
const response = await routeToolCall(tools, toolName, request.params.arguments, { loopGuard });
|
|
7021
9623
|
logger.info({
|
|
7022
9624
|
component: 'mcp',
|
|
7023
9625
|
event: 'tool_call_completed',
|