browser-debug-mcp-bridge 1.11.0 → 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 +3455 -357
- 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-capabilities.js +22 -1
- package/apps/mcp-server/dist/override-capabilities.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/override-response-planner.js +6 -4
- package/apps/mcp-server/dist/override-response-planner.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,143 +975,356 @@ 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
|
-
requestMethod: { type: 'string' },
|
|
653
|
-
requestHeaders: { type: 'object' },
|
|
654
1095
|
timeoutMs: { type: 'number' },
|
|
655
|
-
|
|
656
|
-
includeBody: { type: 'boolean' },
|
|
1096
|
+
pollIntervalMs: { type: 'number' },
|
|
657
1097
|
},
|
|
658
1098
|
},
|
|
659
|
-
|
|
1099
|
+
wait_for_download: {
|
|
660
1100
|
type: 'object',
|
|
661
1101
|
required: ['sessionId'],
|
|
662
1102
|
properties: {
|
|
663
1103
|
sessionId: { type: 'string' },
|
|
664
|
-
|
|
665
|
-
|
|
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' },
|
|
666
1113
|
},
|
|
667
1114
|
},
|
|
668
|
-
|
|
1115
|
+
wait_for_popup: {
|
|
669
1116
|
type: 'object',
|
|
670
|
-
required: ['
|
|
1117
|
+
required: ['sessionId'],
|
|
671
1118
|
properties: {
|
|
672
1119
|
sessionId: { type: 'string' },
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
maxResults: { type: 'number' },
|
|
680
|
-
fetchProductionAssets: { type: 'boolean' },
|
|
681
|
-
productionFetchTimeoutMs: { type: 'number' },
|
|
682
|
-
maxProductionAssetBytes: { type: 'number' },
|
|
683
|
-
maxDriftCandidates: { type: 'number' },
|
|
684
|
-
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' },
|
|
685
1126
|
},
|
|
686
1127
|
},
|
|
687
|
-
|
|
1128
|
+
wait_for_request: {
|
|
688
1129
|
type: 'object',
|
|
1130
|
+
required: ['sessionId'],
|
|
689
1131
|
properties: {
|
|
690
1132
|
sessionId: { type: 'string' },
|
|
691
|
-
|
|
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: {
|
|
1324
|
+
type: 'object',
|
|
1325
|
+
properties: {
|
|
1326
|
+
sessionId: { type: 'string' },
|
|
1327
|
+
tabId: { type: 'number' },
|
|
692
1328
|
targetUrl: { type: 'string' },
|
|
693
1329
|
targetAssetUrl: { type: 'string' },
|
|
694
1330
|
captureMode: { type: 'string', enum: ['extension-fetch', 'cdp-response'] },
|
|
@@ -861,7 +1497,7 @@ const TOOL_SCHEMAS = {
|
|
|
861
1497
|
properties: {
|
|
862
1498
|
sessionId: { type: 'string' },
|
|
863
1499
|
status: { type: 'string', enum: ['requested', 'started', 'succeeded', 'failed', 'rejected', 'stopped'] },
|
|
864
|
-
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'] },
|
|
865
1501
|
traceId: { type: 'string' },
|
|
866
1502
|
limit: { type: 'number' },
|
|
867
1503
|
offset: { type: 'number' },
|
|
@@ -884,16 +1520,53 @@ const TOOL_SCHEMAS = {
|
|
|
884
1520
|
required: ['sessionId', 'action'],
|
|
885
1521
|
properties: {
|
|
886
1522
|
sessionId: { type: 'string' },
|
|
887
|
-
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'] },
|
|
888
1524
|
traceId: { type: 'string' },
|
|
889
1525
|
target: {
|
|
890
1526
|
type: 'object',
|
|
891
1527
|
properties: {
|
|
892
1528
|
selector: { type: 'string' },
|
|
893
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
|
+
},
|
|
894
1538
|
tabId: { type: 'number' },
|
|
895
1539
|
frameId: { type: 'number' },
|
|
896
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' },
|
|
897
1570
|
},
|
|
898
1571
|
},
|
|
899
1572
|
input: { type: 'object' },
|
|
@@ -916,14 +1589,22 @@ const TOOL_SCHEMAS = {
|
|
|
916
1589
|
type: 'object',
|
|
917
1590
|
required: ['scope'],
|
|
918
1591
|
properties: {
|
|
919
|
-
scope: { type: 'string', enum: ['buttons', 'inputs', 'modals', 'focused', 'page'] },
|
|
1592
|
+
scope: { type: 'string', enum: ['buttons', 'links', 'inputs', 'modals', 'focused', 'page'] },
|
|
920
1593
|
selector: { type: 'string' },
|
|
921
1594
|
testId: { type: 'string' },
|
|
922
1595
|
textContains: { type: 'string' },
|
|
923
1596
|
labelContains: { type: 'string' },
|
|
924
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' },
|
|
925
1605
|
urlContains: { type: 'string' },
|
|
926
1606
|
language: { type: 'string' },
|
|
1607
|
+
visible: { type: 'boolean' },
|
|
927
1608
|
disabled: { type: 'boolean' },
|
|
928
1609
|
selected: { type: 'boolean' },
|
|
929
1610
|
pressed: { type: 'boolean' },
|
|
@@ -960,7 +1641,7 @@ const TOOL_SCHEMAS = {
|
|
|
960
1641
|
properties: {
|
|
961
1642
|
id: { type: 'string' },
|
|
962
1643
|
note: { type: 'string' },
|
|
963
|
-
kind: { type: 'string', enum: ['action', 'waitFor', 'assert'] },
|
|
1644
|
+
kind: { type: 'string', enum: ['action', 'waitFor', 'assert', 'wait'] },
|
|
964
1645
|
action: { type: 'string' },
|
|
965
1646
|
traceId: { type: 'string' },
|
|
966
1647
|
target: {
|
|
@@ -968,16 +1649,37 @@ const TOOL_SCHEMAS = {
|
|
|
968
1649
|
properties: {
|
|
969
1650
|
selector: { type: 'string' },
|
|
970
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
|
+
},
|
|
971
1660
|
tabId: { type: 'number' },
|
|
972
1661
|
frameId: { type: 'number' },
|
|
973
1662
|
url: { type: 'string' },
|
|
1663
|
+
locator: ACTION_LOCATOR_TOOL_SCHEMA,
|
|
1664
|
+
frameUrlContains: { type: 'string' },
|
|
1665
|
+
frameTitleContains: { type: 'string' },
|
|
974
1666
|
testId: { type: 'string' },
|
|
975
|
-
scope: { type: 'string', enum: ['buttons', 'inputs', 'modals', 'focused'] },
|
|
1667
|
+
scope: { type: 'string', enum: ['buttons', 'links', 'inputs', 'modals', 'focused'] },
|
|
976
1668
|
textContains: { type: 'string' },
|
|
977
1669
|
labelContains: { type: 'string' },
|
|
978
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' },
|
|
979
1680
|
tagName: { type: 'string' },
|
|
980
1681
|
type: { type: 'string' },
|
|
1682
|
+
visible: { type: 'boolean' },
|
|
981
1683
|
disabled: { type: 'boolean' },
|
|
982
1684
|
selected: { type: 'boolean' },
|
|
983
1685
|
pressed: { type: 'boolean' },
|
|
@@ -1011,14 +1713,22 @@ const TOOL_SCHEMAS = {
|
|
|
1011
1713
|
matcher: {
|
|
1012
1714
|
type: 'object',
|
|
1013
1715
|
properties: {
|
|
1014
|
-
scope: { type: 'string', enum: ['buttons', 'inputs', 'modals', 'focused', 'page'] },
|
|
1716
|
+
scope: { type: 'string', enum: ['buttons', 'links', 'inputs', 'modals', 'focused', 'page'] },
|
|
1015
1717
|
selector: { type: 'string' },
|
|
1016
1718
|
testId: { type: 'string' },
|
|
1017
1719
|
textContains: { type: 'string' },
|
|
1018
1720
|
labelContains: { type: 'string' },
|
|
1019
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' },
|
|
1020
1729
|
urlContains: { type: 'string' },
|
|
1021
1730
|
language: { type: 'string' },
|
|
1731
|
+
visible: { type: 'boolean' },
|
|
1022
1732
|
disabled: { type: 'boolean' },
|
|
1023
1733
|
selected: { type: 'boolean' },
|
|
1024
1734
|
pressed: { type: 'boolean' },
|
|
@@ -1035,6 +1745,7 @@ const TOOL_SCHEMAS = {
|
|
|
1035
1745
|
pollIntervalMs: { type: 'number' },
|
|
1036
1746
|
},
|
|
1037
1747
|
},
|
|
1748
|
+
wait: AUTOMATION_WAIT_TOOL_SCHEMA,
|
|
1038
1749
|
},
|
|
1039
1750
|
},
|
|
1040
1751
|
},
|
|
@@ -1061,11 +1772,25 @@ const TOOL_DESCRIPTIONS = {
|
|
|
1061
1772
|
get_computed_styles: 'Read computed CSS styles for an element',
|
|
1062
1773
|
get_layout_metrics: 'Read viewport and element layout metrics',
|
|
1063
1774
|
get_page_state: 'Read a compact structured page model for forms, buttons, modals, and viewport state',
|
|
1064
|
-
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',
|
|
1065
1776
|
get_live_session_health: 'Read live transport health and session binding details for one session',
|
|
1066
1777
|
set_viewport: 'Resize the live browser window for a session and return the resulting viewport metrics',
|
|
1067
1778
|
assert_page_state: 'Assert compact page-state conditions without pulling raw DOM payloads',
|
|
1068
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',
|
|
1069
1794
|
capture_ui_snapshot: 'Capture redacted UI snapshot (DOM/styles/optional PNG) and persist it',
|
|
1070
1795
|
get_live_console_logs: 'Read in-memory live console logs for a connected session',
|
|
1071
1796
|
list_override_profiles: 'List configured browser override profiles',
|
|
@@ -1112,7 +1837,10 @@ const MAX_BODY_CHUNK_BYTES = 256 * 1024;
|
|
|
1112
1837
|
const DEFAULT_NETWORK_POLL_TIMEOUT_MS = 15_000;
|
|
1113
1838
|
const MAX_NETWORK_POLL_TIMEOUT_MS = 120_000;
|
|
1114
1839
|
const DEFAULT_NETWORK_POLL_INTERVAL_MS = 250;
|
|
1840
|
+
const DEFAULT_AUTOMATION_WAIT_LOOKBACK_MS = 5_000;
|
|
1115
1841
|
const LIVE_SESSION_DISCONNECTED_CODE = 'LIVE_SESSION_DISCONNECTED';
|
|
1842
|
+
const OVERRIDE_LIVE_COMMAND_TIMEOUT_CODE = 'OVERRIDE_LIVE_COMMAND_TIMEOUT';
|
|
1843
|
+
const OVERRIDE_LIVE_COMMAND_FAILED_CODE = 'OVERRIDE_LIVE_COMMAND_FAILED';
|
|
1116
1844
|
const STALE_LIVE_CONNECTION_GRACE_WINDOW_MS = 30 * 60 * 1000;
|
|
1117
1845
|
const NOISE_SESSION_HOST_PATTERNS = [
|
|
1118
1846
|
/(^|\.)adtrafficquality\.google$/i,
|
|
@@ -1392,7 +2120,7 @@ function buildOverrideProfileRecords() {
|
|
|
1392
2120
|
active: profile.profileId === summary.activeProfileId,
|
|
1393
2121
|
configEnabled: summary.configEnabled,
|
|
1394
2122
|
enabled: profile.enabled,
|
|
1395
|
-
effectiveEnabled:
|
|
2123
|
+
effectiveEnabled: profile.enabled && profile.enabledRuleCount > 0,
|
|
1396
2124
|
autoReload: profile.autoReload,
|
|
1397
2125
|
configPath: summary.configPath,
|
|
1398
2126
|
fileExists: profile.fileExists,
|
|
@@ -1411,6 +2139,72 @@ function resolveOverrideProfileRecord(value) {
|
|
|
1411
2139
|
}
|
|
1412
2140
|
return profile;
|
|
1413
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
|
+
}
|
|
1414
2208
|
const SHA256_HEX_PATTERN = /^[a-f0-9]{64}$/;
|
|
1415
2209
|
function sha256Text(value) {
|
|
1416
2210
|
return createHash('sha256').update(value, 'utf8').digest('hex');
|
|
@@ -1422,6 +2216,29 @@ function isRecordWithRscFlightMetadata(value) {
|
|
|
1422
2216
|
&& value.source !== undefined
|
|
1423
2217
|
&& value.patchKind !== undefined;
|
|
1424
2218
|
}
|
|
2219
|
+
function normalizeRuleStringHeaders(value) {
|
|
2220
|
+
if (!isRecord(value)) {
|
|
2221
|
+
return undefined;
|
|
2222
|
+
}
|
|
2223
|
+
const headers = {};
|
|
2224
|
+
for (const [name, rawValue] of Object.entries(value)) {
|
|
2225
|
+
if (typeof rawValue !== 'string') {
|
|
2226
|
+
continue;
|
|
2227
|
+
}
|
|
2228
|
+
const normalizedName = name.trim().toLowerCase();
|
|
2229
|
+
const normalizedValue = rawValue.trim();
|
|
2230
|
+
if (normalizedName.length > 0 && normalizedValue.length > 0) {
|
|
2231
|
+
headers[normalizedName] = normalizedValue;
|
|
2232
|
+
}
|
|
2233
|
+
}
|
|
2234
|
+
return Object.keys(headers).length > 0 ? headers : undefined;
|
|
2235
|
+
}
|
|
2236
|
+
function getRscFlightRuleRequestHeaders(rule) {
|
|
2237
|
+
return isRecord(rule.rscFlight) ? normalizeRuleStringHeaders(rule.rscFlight.requestHeaders) : undefined;
|
|
2238
|
+
}
|
|
2239
|
+
function getOverrideRuleRequestHeaders(rule) {
|
|
2240
|
+
return normalizeRuleStringHeaders(rule.requestHeaders) ?? getRscFlightRuleRequestHeaders(rule);
|
|
2241
|
+
}
|
|
1425
2242
|
function buildRscFlightRuleIssues(rule) {
|
|
1426
2243
|
const ruleId = String(rule.ruleId ?? 'unknown');
|
|
1427
2244
|
const issues = [];
|
|
@@ -1465,17 +2282,20 @@ function buildRscFlightRuleIssues(rule) {
|
|
|
1465
2282
|
}
|
|
1466
2283
|
}
|
|
1467
2284
|
}
|
|
1468
|
-
|
|
2285
|
+
const requestMethod = normalizeOverrideRequestMethod(rule.requestMethod);
|
|
2286
|
+
const requestHeaders = getRscFlightRuleRequestHeaders(rule);
|
|
2287
|
+
const isCapturedPostRscFlight = requestMethod === 'POST' && requestHeaders?.rsc === '1';
|
|
2288
|
+
if (requestMethod !== 'GET' && !isCapturedPostRscFlight) {
|
|
1469
2289
|
issues.push({
|
|
1470
2290
|
code: 'RSC_FLIGHT_METHOD_UNSUPPORTED',
|
|
1471
2291
|
severity: 'error',
|
|
1472
|
-
message: `Rule ${ruleId} RSC flight overrides only support GET requests.`,
|
|
2292
|
+
message: `Rule ${ruleId} RSC flight overrides only support GET requests or captured POST RSC response-stage patches.`,
|
|
1473
2293
|
});
|
|
1474
2294
|
}
|
|
1475
2295
|
const targetAssetUrl = typeof rule.targetAssetUrl === 'string' ? rule.targetAssetUrl : '';
|
|
1476
2296
|
try {
|
|
1477
2297
|
const parsed = new URL(targetAssetUrl);
|
|
1478
|
-
if (!parsed.searchParams.has('_rsc')) {
|
|
2298
|
+
if (requestMethod === 'GET' && !parsed.searchParams.has('_rsc')) {
|
|
1479
2299
|
issues.push({
|
|
1480
2300
|
code: 'RSC_FLIGHT_TARGET_INVALID',
|
|
1481
2301
|
severity: 'error',
|
|
@@ -1553,13 +2373,6 @@ function buildOverrideProfileIssues(profile) {
|
|
|
1553
2373
|
const rules = Array.isArray(profile.rules)
|
|
1554
2374
|
? profile.rules.filter((rule) => isRecord(rule))
|
|
1555
2375
|
: [];
|
|
1556
|
-
if (profile.configEnabled !== true) {
|
|
1557
|
-
issues.push({
|
|
1558
|
-
code: 'CONFIG_DISABLED',
|
|
1559
|
-
severity: 'warning',
|
|
1560
|
-
message: 'The override config is disabled and cannot replace requests until enabled.',
|
|
1561
|
-
});
|
|
1562
|
-
}
|
|
1563
2376
|
if (profile.enabled !== true) {
|
|
1564
2377
|
issues.push({
|
|
1565
2378
|
code: 'PROFILE_DISABLED',
|
|
@@ -1595,7 +2408,7 @@ function buildOverrideProfileIssues(profile) {
|
|
|
1595
2408
|
issues.push(...classifyOverrideResponseRequestCapability({
|
|
1596
2409
|
ruleId: rule.ruleId,
|
|
1597
2410
|
requestMethod: rule.requestMethod,
|
|
1598
|
-
requestHeaders: rule
|
|
2411
|
+
requestHeaders: getOverrideRuleRequestHeaders(rule),
|
|
1599
2412
|
ruleType: rule.ruleType,
|
|
1600
2413
|
}).issues.map((issue) => ({ ...issue })));
|
|
1601
2414
|
if (rule.ruleType === 'rsc-flight') {
|
|
@@ -1648,12 +2461,6 @@ function buildOverrideProfileNextActions(profile, issues) {
|
|
|
1648
2461
|
message: 'Regenerate the RSC rule with plan_override_response_patch from a captured text/x-component response body.',
|
|
1649
2462
|
}];
|
|
1650
2463
|
}
|
|
1651
|
-
if (profile.configEnabled !== true) {
|
|
1652
|
-
return [{
|
|
1653
|
-
code: 'ENABLE_CONFIG',
|
|
1654
|
-
message: 'Set the root override config enabled=true after reviewing the profile.',
|
|
1655
|
-
}];
|
|
1656
|
-
}
|
|
1657
2464
|
if (profile.enabled !== true) {
|
|
1658
2465
|
return [{
|
|
1659
2466
|
code: 'ENABLE_PROFILE',
|
|
@@ -1676,8 +2483,14 @@ function hasEnabledExperimentalRscFlightRule(profile) {
|
|
|
1676
2483
|
});
|
|
1677
2484
|
}
|
|
1678
2485
|
function canBypassPreflightForExperimentalRsc(profile, blockingCodes) {
|
|
2486
|
+
const allowedExperimentalBlockers = new Set([
|
|
2487
|
+
'UNSUPPORTED_RSC_FLIGHT_RULE',
|
|
2488
|
+
'NO_OBSERVED_ASSETS',
|
|
2489
|
+
'TARGET_ASSET_NOT_OBSERVED',
|
|
2490
|
+
]);
|
|
1679
2491
|
return blockingCodes.length > 0
|
|
1680
|
-
&& blockingCodes.
|
|
2492
|
+
&& blockingCodes.includes('UNSUPPORTED_RSC_FLIGHT_RULE')
|
|
2493
|
+
&& blockingCodes.every((code) => allowedExperimentalBlockers.has(code))
|
|
1681
2494
|
&& hasEnabledExperimentalRscFlightRule(profile);
|
|
1682
2495
|
}
|
|
1683
2496
|
const OVERRIDE_VARIANT_HEADER_ALLOWLIST = new Set([
|
|
@@ -1797,6 +2610,36 @@ function pushOverridePreflightIssue(issues, issue) {
|
|
|
1797
2610
|
}
|
|
1798
2611
|
issues.push(issue);
|
|
1799
2612
|
}
|
|
2613
|
+
function getPreflightIssues(preflight) {
|
|
2614
|
+
return Array.isArray(preflight?.issues)
|
|
2615
|
+
? preflight.issues.filter((issue) => isRecord(issue))
|
|
2616
|
+
: [];
|
|
2617
|
+
}
|
|
2618
|
+
function getBlockingPreflightCodes(preflight) {
|
|
2619
|
+
return getPreflightIssues(preflight)
|
|
2620
|
+
.filter((issue) => issue.severity === 'error')
|
|
2621
|
+
.map((issue) => String(issue.code ?? 'UNKNOWN'));
|
|
2622
|
+
}
|
|
2623
|
+
function hasPreflightIssue(preflight, codes) {
|
|
2624
|
+
const expected = new Set(codes);
|
|
2625
|
+
return getPreflightIssues(preflight).some((issue) => expected.has(String(issue.code ?? '')));
|
|
2626
|
+
}
|
|
2627
|
+
function shouldRefreshObservedAssetsForEnable(preflight) {
|
|
2628
|
+
const assetReadinessCodes = new Set([
|
|
2629
|
+
'NO_OBSERVED_ASSETS',
|
|
2630
|
+
'TARGET_ASSET_NOT_OBSERVED',
|
|
2631
|
+
'SESSION_SCOPE_DRIFT',
|
|
2632
|
+
]);
|
|
2633
|
+
const blockingCodes = getBlockingPreflightCodes(preflight);
|
|
2634
|
+
if (blockingCodes.length === 0 || !blockingCodes.every((code) => assetReadinessCodes.has(code))) {
|
|
2635
|
+
return false;
|
|
2636
|
+
}
|
|
2637
|
+
return hasPreflightIssue(preflight, [
|
|
2638
|
+
'NO_OBSERVED_ASSETS',
|
|
2639
|
+
'TARGET_ASSET_NOT_OBSERVED',
|
|
2640
|
+
'SESSION_SCOPE_DRIFT',
|
|
2641
|
+
]);
|
|
2642
|
+
}
|
|
1800
2643
|
function buildOverridePreflight(options) {
|
|
1801
2644
|
const session = options.db
|
|
1802
2645
|
.prepare(`
|
|
@@ -1835,7 +2678,20 @@ function buildOverridePreflight(options) {
|
|
|
1835
2678
|
.filter((context) => context !== null)
|
|
1836
2679
|
.map((context) => [String(context.variantKey ?? JSON.stringify(context)), context])).values()];
|
|
1837
2680
|
const sessionState = options.getSessionConnectionState?.(options.sessionId);
|
|
2681
|
+
const hasLiveConnectionLookup = typeof options.getSessionConnectionState === 'function';
|
|
1838
2682
|
const diagnosis = session ? diagnoseOverridePoc(options.db, options.sessionId, latestRun?.runId) : null;
|
|
2683
|
+
const observedAssetTabs = [...new Set(observedAssets
|
|
2684
|
+
.map((asset) => asset.tabId)
|
|
2685
|
+
.filter((tabId) => typeof tabId === 'number' && Number.isFinite(tabId)))].sort((a, b) => a - b);
|
|
2686
|
+
const observedAssetPageUrls = [...new Set(observedAssets
|
|
2687
|
+
.map((asset) => asset.pageUrl)
|
|
2688
|
+
.filter((pageUrl) => typeof pageUrl === 'string' && pageUrl.trim().length > 0))].slice(0, 5);
|
|
2689
|
+
const sessionTabId = typeof session?.tab_id === 'number' ? session.tab_id : undefined;
|
|
2690
|
+
const observedAssetsWithKnownTabs = observedAssets.filter((asset) => typeof asset.tabId === 'number');
|
|
2691
|
+
const topLevelScopeLikely = sessionTabId === undefined
|
|
2692
|
+
|| observedAssets.length === 0
|
|
2693
|
+
|| observedAssetsWithKnownTabs.length === 0
|
|
2694
|
+
|| observedAssetsWithKnownTabs.some((asset) => asset.tabId === sessionTabId);
|
|
1839
2695
|
for (const issue of buildOverrideProfileIssues(profile)) {
|
|
1840
2696
|
pushOverridePreflightIssue(issues, { ...issue, source: 'profile' });
|
|
1841
2697
|
}
|
|
@@ -1865,55 +2721,117 @@ function buildOverridePreflight(options) {
|
|
|
1865
2721
|
message: `Session ${options.sessionId} has ended and cannot enable overrides.`,
|
|
1866
2722
|
});
|
|
1867
2723
|
}
|
|
1868
|
-
if (
|
|
2724
|
+
if (hasLiveConnectionLookup && (!sessionState || sessionState.connected !== true)) {
|
|
1869
2725
|
pushOverridePreflightIssue(issues, {
|
|
1870
2726
|
code: LIVE_SESSION_DISCONNECTED_CODE,
|
|
1871
2727
|
severity: 'error',
|
|
1872
2728
|
source: 'connection',
|
|
1873
|
-
message:
|
|
2729
|
+
message: sessionState
|
|
2730
|
+
? `Session ${options.sessionId} is not currently connected to the live extension bridge. Last disconnect reason: ${sessionState.disconnectReason ?? 'unknown'}.`
|
|
2731
|
+
: `Session ${options.sessionId} has no current live extension connection state.`,
|
|
2732
|
+
disconnectedAt: sessionState?.disconnectedAt,
|
|
2733
|
+
disconnectReason: sessionState?.disconnectReason,
|
|
1874
2734
|
});
|
|
1875
2735
|
}
|
|
1876
2736
|
}
|
|
1877
2737
|
const enabledRules = Array.isArray(profile.rules)
|
|
1878
2738
|
? profile.rules.filter((rule) => isRecord(rule) && rule.enabled === true)
|
|
1879
2739
|
: [];
|
|
2740
|
+
const enabledRuleAssetReadiness = enabledRules
|
|
2741
|
+
.map((rule) => {
|
|
2742
|
+
const targetAssetUrl = normalizeOptionalString(rule.targetAssetUrl);
|
|
2743
|
+
if (!targetAssetUrl) {
|
|
2744
|
+
return null;
|
|
2745
|
+
}
|
|
2746
|
+
const requestMethod = normalizeOverrideRequestMethod(rule.requestMethod);
|
|
2747
|
+
const matchMode = String(rule.matchMode ?? 'exact');
|
|
2748
|
+
const matchingAssets = observedAssets.filter((asset) => {
|
|
2749
|
+
const methodMatches = normalizeOverrideRequestMethod(asset.requestMethod) === requestMethod;
|
|
2750
|
+
if (!methodMatches) {
|
|
2751
|
+
return false;
|
|
2752
|
+
}
|
|
2753
|
+
return matchMode === 'prefix'
|
|
2754
|
+
? asset.url.startsWith(targetAssetUrl)
|
|
2755
|
+
: asset.url === targetAssetUrl;
|
|
2756
|
+
});
|
|
2757
|
+
return {
|
|
2758
|
+
ruleId: String(rule.ruleId ?? 'unknown'),
|
|
2759
|
+
targetAssetUrl,
|
|
2760
|
+
requestMethod,
|
|
2761
|
+
matchMode,
|
|
2762
|
+
captureProven: rule.ruleType === 'rsc-flight' && isRecordWithRscFlightMetadata(rule.rscFlight),
|
|
2763
|
+
matchingAssets,
|
|
2764
|
+
};
|
|
2765
|
+
})
|
|
2766
|
+
.filter((readiness) => readiness !== null);
|
|
2767
|
+
const matchedTargetAssetCount = enabledRuleAssetReadiness.filter((readiness) => readiness.matchingAssets.length > 0).length;
|
|
2768
|
+
const capturedTargetAssetCount = enabledRuleAssetReadiness
|
|
2769
|
+
.filter((readiness) => readiness.matchingAssets.length === 0 && readiness.captureProven)
|
|
2770
|
+
.length;
|
|
2771
|
+
const unobservedTargetAssetCount = enabledRuleAssetReadiness.length - matchedTargetAssetCount;
|
|
2772
|
+
const unsatisfiedTargetAssetCount = enabledRuleAssetReadiness.length - matchedTargetAssetCount - capturedTargetAssetCount;
|
|
2773
|
+
const targetAssetObserved = observedAssets.length > 0 && matchedTargetAssetCount > 0;
|
|
2774
|
+
const targetAssetReadinessSatisfied = observedAssets.length > 0
|
|
2775
|
+
&& (enabledRuleAssetReadiness.length === 0 || matchedTargetAssetCount > 0 || capturedTargetAssetCount > 0);
|
|
1880
2776
|
const anyServiceWorkerControlled = observedAssets.some((asset) => asset.serviceWorkerControlled);
|
|
1881
2777
|
const cspMetaTags = [...new Set(observedAssets.flatMap((asset) => asset.cspMetaTags))];
|
|
1882
2778
|
if (observedAssets.length === 0) {
|
|
1883
2779
|
pushOverridePreflightIssue(issues, {
|
|
1884
2780
|
code: 'NO_OBSERVED_ASSETS',
|
|
1885
|
-
severity: '
|
|
2781
|
+
severity: 'error',
|
|
1886
2782
|
source: 'observed-assets',
|
|
1887
|
-
message: 'No observed production assets are stored for this session yet.',
|
|
2783
|
+
message: 'No observed production assets are stored for this session yet; the target route is not capture-ready for override enablement.',
|
|
1888
2784
|
});
|
|
1889
2785
|
}
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
&& normalizeOverrideRequestMethod(asset.requestMethod) === requestMethod;
|
|
2786
|
+
else if (!topLevelScopeLikely) {
|
|
2787
|
+
pushOverridePreflightIssue(issues, {
|
|
2788
|
+
code: 'SESSION_SCOPE_DRIFT',
|
|
2789
|
+
severity: 'error',
|
|
2790
|
+
source: 'observed-assets',
|
|
2791
|
+
message: `Observed override assets were recorded only for tab(s) ${observedAssetTabs.join(', ')}, but the session top-level tab is ${sessionTabId}.`,
|
|
2792
|
+
observedAssetTabs,
|
|
2793
|
+
sessionTabId,
|
|
2794
|
+
observedPageUrls: observedAssetPageUrls,
|
|
1900
2795
|
});
|
|
1901
|
-
|
|
2796
|
+
}
|
|
2797
|
+
if (observedAssets.length > 0 && enabledRuleAssetReadiness.length > 0 && matchedTargetAssetCount === 0 && capturedTargetAssetCount === 0) {
|
|
2798
|
+
const sampleTargets = enabledRuleAssetReadiness.slice(0, 5).map((readiness) => ({
|
|
2799
|
+
ruleId: readiness.ruleId,
|
|
2800
|
+
requestMethod: readiness.requestMethod,
|
|
2801
|
+
matchMode: readiness.matchMode,
|
|
2802
|
+
targetAssetUrl: readiness.targetAssetUrl,
|
|
2803
|
+
}));
|
|
2804
|
+
pushOverridePreflightIssue(issues, {
|
|
2805
|
+
code: 'TARGET_ASSET_NOT_OBSERVED',
|
|
2806
|
+
severity: 'error',
|
|
2807
|
+
source: 'observed-assets',
|
|
2808
|
+
message: enabledRuleAssetReadiness.length === 1
|
|
2809
|
+
? `Rule ${enabledRuleAssetReadiness[0].ruleId} target asset was not observed for ${enabledRuleAssetReadiness[0].requestMethod} ${enabledRuleAssetReadiness[0].targetAssetUrl}.`
|
|
2810
|
+
: `None of the ${enabledRuleAssetReadiness.length} enabled override targets were observed for this session.`,
|
|
2811
|
+
checkedTargetAssetCount: enabledRuleAssetReadiness.length,
|
|
2812
|
+
sampleTargets,
|
|
2813
|
+
});
|
|
2814
|
+
}
|
|
2815
|
+
for (const readiness of enabledRuleAssetReadiness) {
|
|
2816
|
+
if (readiness.matchingAssets.length === 0) {
|
|
2817
|
+
if (readiness.captureProven) {
|
|
2818
|
+
continue;
|
|
2819
|
+
}
|
|
1902
2820
|
pushOverridePreflightIssue(issues, {
|
|
1903
|
-
code: '
|
|
2821
|
+
code: 'TARGET_ASSET_NOT_OBSERVED_FOR_RULE',
|
|
1904
2822
|
severity: 'warning',
|
|
1905
2823
|
source: 'observed-assets',
|
|
1906
|
-
message: `Rule ${ruleId} target asset was not observed for ${requestMethod} ${targetAssetUrl}.`,
|
|
2824
|
+
message: `Rule ${readiness.ruleId} target asset was not observed for ${readiness.requestMethod} ${readiness.targetAssetUrl}.`,
|
|
1907
2825
|
});
|
|
1908
2826
|
continue;
|
|
1909
2827
|
}
|
|
1910
|
-
for (const asset of matchingAssets) {
|
|
2828
|
+
for (const asset of readiness.matchingAssets) {
|
|
1911
2829
|
if (typeof asset.integrity === 'string' && asset.integrity.length > 0) {
|
|
1912
2830
|
pushOverridePreflightIssue(issues, {
|
|
1913
2831
|
code: 'TARGET_ASSET_SRI_PRESENT',
|
|
1914
2832
|
severity: 'error',
|
|
1915
2833
|
source: 'observed-assets',
|
|
1916
|
-
message: `Rule ${ruleId} target asset ${asset.url} includes integrity="${asset.integrity}" and cannot be overridden safely.`,
|
|
2834
|
+
message: `Rule ${readiness.ruleId} target asset ${asset.url} includes integrity="${asset.integrity}" and cannot be overridden safely.`,
|
|
1917
2835
|
});
|
|
1918
2836
|
}
|
|
1919
2837
|
}
|
|
@@ -1936,23 +2854,29 @@ function buildOverridePreflight(options) {
|
|
|
1936
2854
|
}
|
|
1937
2855
|
const ready = !issues.some((issue) => issue.severity === 'error');
|
|
1938
2856
|
const nextActions = !ready
|
|
1939
|
-
? issues.some((issue) => issue.code === '
|
|
1940
|
-
? [{
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
: issues.some((issue) => issue.code === '
|
|
1954
|
-
? [{ code: '
|
|
1955
|
-
:
|
|
2857
|
+
? issues.some((issue) => issue.code === 'SESSION_NOT_FOUND' || issue.code === 'SESSION_PAUSED' || issue.code === 'SESSION_ENDED' || issue.code === LIVE_SESSION_DISCONNECTED_CODE)
|
|
2858
|
+
? [{ code: 'RECONNECT_SESSION', message: 'Reconnect or resume the target session before enabling overrides.' }]
|
|
2859
|
+
: issues.some((issue) => issue.code === 'SESSION_SCOPE_DRIFT')
|
|
2860
|
+
? [{ code: 'FOCUS_BOUND_TAB', message: 'Focus or reselect the bound top-level tab, then observe override assets again.' }]
|
|
2861
|
+
: issues.some((issue) => issue.code === 'SERVER_ACTION_UNSUPPORTED')
|
|
2862
|
+
? [{
|
|
2863
|
+
code: 'REPLAN_SERVER_ACTION_OVERRIDE',
|
|
2864
|
+
message: 'Server actions stay unsupported in production override mode; move the override to a GET document/data/API response.',
|
|
2865
|
+
}]
|
|
2866
|
+
: issues.some((issue) => issue.code === 'MUTATION_REPLAY_UNSUPPORTED')
|
|
2867
|
+
? [{
|
|
2868
|
+
code: 'REPLAN_MUTATION_OVERRIDE',
|
|
2869
|
+
message: 'Mutation responses are not replay-safe; use a GET document/data/API response path instead.',
|
|
2870
|
+
}]
|
|
2871
|
+
: issues.some((issue) => issue.code === 'UNSAFE_REQUEST_METHOD')
|
|
2872
|
+
? [{ code: 'REPLAN_GET_ONLY_OVERRIDE', message: 'Remove or regenerate non-GET rules before enabling overrides.' }]
|
|
2873
|
+
: issues.some((issue) => issue.code === 'TARGET_ASSET_SRI_PRESENT')
|
|
2874
|
+
? [{ code: 'CHOOSE_ANOTHER_OVERRIDE_PATH', message: 'Choose a document/data response path or remove SRI on the production asset before enabling overrides.' }]
|
|
2875
|
+
: issues.some((issue) => issue.code === 'NO_OBSERVED_ASSETS')
|
|
2876
|
+
? [{ code: 'OBSERVE_OVERRIDE_ASSETS', message: 'Observe the bound target route before enabling overrides.' }]
|
|
2877
|
+
: issues.some((issue) => issue.code === 'TARGET_ASSET_NOT_OBSERVED')
|
|
2878
|
+
? [{ code: 'OBSERVE_TARGET_ROUTE', message: 'Load the route that requests the configured target and observe assets again.' }]
|
|
2879
|
+
: buildOverrideProfileNextActions(profile, issues)
|
|
1956
2880
|
: observedAssets.length === 0
|
|
1957
2881
|
? [{ code: 'OBSERVE_OVERRIDE_ASSETS', message: 'Run observe_override_assets on the target route before enabling overrides in production workflows.' }]
|
|
1958
2882
|
: [{ code: 'ENABLE_OVERRIDES', message: 'Preflight checks passed; the selected profile can be enabled on the live session.' }];
|
|
@@ -1976,8 +2900,22 @@ function buildOverridePreflight(options) {
|
|
|
1976
2900
|
checks: {
|
|
1977
2901
|
sessionFound: session !== undefined,
|
|
1978
2902
|
connected: sessionState?.connected === true,
|
|
2903
|
+
captureReady: session !== undefined
|
|
2904
|
+
&& getSessionStatus(session) === 'active'
|
|
2905
|
+
&& (!hasLiveConnectionLookup || sessionState?.connected === true)
|
|
2906
|
+
&& observedAssets.length > 0
|
|
2907
|
+
&& topLevelScopeLikely
|
|
2908
|
+
&& !issues.some((issue) => issue.severity === 'error'),
|
|
2909
|
+
topLevelScopeLikely,
|
|
2910
|
+
observedAssetTabs,
|
|
2911
|
+
observedAssetPageUrls,
|
|
1979
2912
|
observedAssetCount: observedAssets.length,
|
|
1980
|
-
targetAssetObserved
|
|
2913
|
+
targetAssetObserved,
|
|
2914
|
+
targetAssetReadinessSatisfied,
|
|
2915
|
+
matchedTargetAssetCount,
|
|
2916
|
+
capturedTargetAssetCount,
|
|
2917
|
+
unobservedTargetAssetCount,
|
|
2918
|
+
unsatisfiedTargetAssetCount,
|
|
1981
2919
|
serviceWorkerControlled: anyServiceWorkerControlled,
|
|
1982
2920
|
cspMetaTagCount: cspMetaTags.length,
|
|
1983
2921
|
recentPlanCount: recentPlans.length,
|
|
@@ -1985,6 +2923,14 @@ function buildOverridePreflight(options) {
|
|
|
1985
2923
|
},
|
|
1986
2924
|
observedAssets: {
|
|
1987
2925
|
count: observedAssets.length,
|
|
2926
|
+
tabIds: observedAssetTabs,
|
|
2927
|
+
pageUrls: observedAssetPageUrls,
|
|
2928
|
+
targetAssetObserved,
|
|
2929
|
+
targetAssetReadinessSatisfied,
|
|
2930
|
+
matchedTargetAssetCount,
|
|
2931
|
+
capturedTargetAssetCount,
|
|
2932
|
+
unobservedTargetAssetCount,
|
|
2933
|
+
unsatisfiedTargetAssetCount,
|
|
1988
2934
|
serviceWorkerControlled: anyServiceWorkerControlled,
|
|
1989
2935
|
cspMetaTags,
|
|
1990
2936
|
},
|
|
@@ -2390,6 +3336,7 @@ function mapAutomationRunRecord(row) {
|
|
|
2390
3336
|
: undefined,
|
|
2391
3337
|
stopReason: row.stop_reason ?? undefined,
|
|
2392
3338
|
target: parseJsonOrUndefined(row.target_summary_json),
|
|
3339
|
+
diagnostics: parseJsonOrUndefined(row.diagnostics_json),
|
|
2393
3340
|
failure: parseJsonOrUndefined(row.failure_json),
|
|
2394
3341
|
redaction: parseJsonOrUndefined(row.redaction_json),
|
|
2395
3342
|
stepCount: row.step_count,
|
|
@@ -2414,6 +3361,7 @@ function mapAutomationStepRecord(row) {
|
|
|
2414
3361
|
durationMs: row.duration_ms ?? undefined,
|
|
2415
3362
|
tabId: row.tab_id ?? undefined,
|
|
2416
3363
|
target: parseJsonOrUndefined(row.target_summary_json),
|
|
3364
|
+
diagnostics: parseJsonOrUndefined(row.diagnostics_json),
|
|
2417
3365
|
redaction: parseJsonOrUndefined(row.redaction_json),
|
|
2418
3366
|
failure: parseJsonOrUndefined(row.failure_json),
|
|
2419
3367
|
inputMetadata: parseJsonOrUndefined(row.input_metadata_json),
|
|
@@ -2424,6 +3372,109 @@ function mapAutomationStepRecord(row) {
|
|
|
2424
3372
|
source: 'automation_steps',
|
|
2425
3373
|
};
|
|
2426
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
|
+
}
|
|
2427
3478
|
function formatUrlPath(url) {
|
|
2428
3479
|
try {
|
|
2429
3480
|
const parsed = new URL(url);
|
|
@@ -2556,15 +3607,25 @@ function resolveViewportDimension(value, axis) {
|
|
|
2556
3607
|
}
|
|
2557
3608
|
return floored;
|
|
2558
3609
|
}
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
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;
|
|
3621
|
+
}
|
|
3622
|
+
if (typeof options.candidateCount === 'number') {
|
|
3623
|
+
diagnostics.candidateCount = options.candidateCount;
|
|
2567
3624
|
}
|
|
3625
|
+
if (Array.isArray(options.sampledCandidates) && options.sampledCandidates.length > 0) {
|
|
3626
|
+
diagnostics.sampledCandidates = options.sampledCandidates;
|
|
3627
|
+
}
|
|
3628
|
+
return diagnostics;
|
|
2568
3629
|
}
|
|
2569
3630
|
function resolveOptionalMatcherString(value) {
|
|
2570
3631
|
if (typeof value !== 'string') {
|
|
@@ -2587,10 +3648,10 @@ function resolveOptionalMatcherCount(value, field) {
|
|
|
2587
3648
|
return floored;
|
|
2588
3649
|
}
|
|
2589
3650
|
function resolvePageStateScope(value) {
|
|
2590
|
-
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') {
|
|
2591
3652
|
return value;
|
|
2592
3653
|
}
|
|
2593
|
-
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');
|
|
2594
3655
|
}
|
|
2595
3656
|
function resolvePageStateMatcher(input) {
|
|
2596
3657
|
const matcher = {
|
|
@@ -2600,6 +3661,13 @@ function resolvePageStateMatcher(input) {
|
|
|
2600
3661
|
textContains: resolveOptionalMatcherString(input.textContains),
|
|
2601
3662
|
labelContains: resolveOptionalMatcherString(input.labelContains),
|
|
2602
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),
|
|
2603
3671
|
urlContains: resolveOptionalMatcherString(input.urlContains),
|
|
2604
3672
|
language: resolveOptionalMatcherString(input.language),
|
|
2605
3673
|
disabled: resolveOptionalMatcherBoolean(input.disabled),
|
|
@@ -2610,6 +3678,7 @@ function resolvePageStateMatcher(input) {
|
|
|
2610
3678
|
requiredField: resolveOptionalMatcherBoolean(input.requiredField),
|
|
2611
3679
|
tagName: resolveOptionalMatcherString(input.tagName)?.toLowerCase(),
|
|
2612
3680
|
type: resolveOptionalMatcherString(input.type)?.toLowerCase(),
|
|
3681
|
+
visible: resolveOptionalMatcherBoolean(input.visible),
|
|
2613
3682
|
countExactly: resolveOptionalMatcherCount(input.countExactly, 'countExactly'),
|
|
2614
3683
|
countAtLeast: resolveOptionalMatcherCount(input.countAtLeast, 'countAtLeast'),
|
|
2615
3684
|
};
|
|
@@ -2624,6 +3693,19 @@ function includesNormalized(value, needle) {
|
|
|
2624
3693
|
}
|
|
2625
3694
|
return typeof value === 'string' && value.toLowerCase().includes(needle.toLowerCase());
|
|
2626
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
|
+
}
|
|
2627
3709
|
function equalsNormalized(value, expected) {
|
|
2628
3710
|
if (!expected) {
|
|
2629
3711
|
return true;
|
|
@@ -2637,7 +3719,7 @@ function equalsOptionalBoolean(value, expected) {
|
|
|
2637
3719
|
return value === expected;
|
|
2638
3720
|
}
|
|
2639
3721
|
function pickPageStateScopeItems(payload, scope) {
|
|
2640
|
-
if (scope === 'buttons' || scope === 'inputs' || scope === 'modals') {
|
|
3722
|
+
if (scope === 'buttons' || scope === 'links' || scope === 'inputs' || scope === 'modals') {
|
|
2641
3723
|
const value = payload[scope];
|
|
2642
3724
|
return asRecordArray(value);
|
|
2643
3725
|
}
|
|
@@ -2650,13 +3732,20 @@ function pickPageStateScopeItems(payload, scope) {
|
|
|
2650
3732
|
function matchesPageStateItem(item, matcher) {
|
|
2651
3733
|
return (includesNormalized(item.selector, matcher.selector)
|
|
2652
3734
|
&& equalsNormalized(item.testId, matcher.testId)
|
|
2653
|
-
&&
|
|
2654
|
-
&&
|
|
2655
|
-
&&
|
|
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)
|
|
2656
3744
|
&& includesNormalized(item.url, matcher.urlContains)
|
|
2657
3745
|
&& equalsNormalized(item.language, matcher.language)
|
|
2658
3746
|
&& equalsNormalized(item.tagName, matcher.tagName)
|
|
2659
3747
|
&& equalsNormalized(item.type, matcher.type)
|
|
3748
|
+
&& equalsOptionalBoolean(item.visible, matcher.visible)
|
|
2660
3749
|
&& equalsOptionalBoolean(item.disabled, matcher.disabled)
|
|
2661
3750
|
&& equalsOptionalBoolean(item.selected, matcher.selected)
|
|
2662
3751
|
&& equalsOptionalBoolean(item.pressed, matcher.pressed)
|
|
@@ -2713,7 +3802,7 @@ function createPageChangeSummary(previousCapture, currentCapture) {
|
|
|
2713
3802
|
const previousSummary = previous?.summary;
|
|
2714
3803
|
const currentSummary = current.summary;
|
|
2715
3804
|
const summaryDelta = {};
|
|
2716
|
-
for (const key of ['buttons', 'inputs', 'modals']) {
|
|
3805
|
+
for (const key of ['buttons', 'links', 'inputs', 'modals']) {
|
|
2717
3806
|
const previousValue = typeof previousSummary?.[key] === 'number' ? previousSummary[key] : undefined;
|
|
2718
3807
|
const currentValue = typeof currentSummary?.[key] === 'number' ? currentSummary[key] : undefined;
|
|
2719
3808
|
if (previousValue !== currentValue && currentValue !== undefined) {
|
|
@@ -2742,13 +3831,13 @@ function createPageChangeSummary(previousCapture, currentCapture) {
|
|
|
2742
3831
|
}
|
|
2743
3832
|
function resolveInteractiveKinds(value) {
|
|
2744
3833
|
if (!Array.isArray(value) || value.length === 0) {
|
|
2745
|
-
return ['buttons', 'inputs', 'modals', 'focused'];
|
|
3834
|
+
return ['buttons', 'links', 'inputs', 'modals', 'focused'];
|
|
2746
3835
|
}
|
|
2747
|
-
const allowed = new Set(['buttons', 'inputs', 'modals', 'focused']);
|
|
3836
|
+
const allowed = new Set(['buttons', 'links', 'inputs', 'modals', 'focused']);
|
|
2748
3837
|
const kinds = value
|
|
2749
3838
|
.filter((entry) => typeof entry === 'string' && allowed.has(entry))
|
|
2750
3839
|
.map((entry) => entry);
|
|
2751
|
-
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'];
|
|
2752
3841
|
}
|
|
2753
3842
|
function collectInteractiveElementRefs(payload, kinds, maxItems) {
|
|
2754
3843
|
const refs = [];
|
|
@@ -2868,132 +3957,1345 @@ async function waitForPageStateCondition(sessionId, input, capturePageState) {
|
|
|
2868
3957
|
const { lastCapture: _lastCapture, ...waited } = detailed;
|
|
2869
3958
|
return waited;
|
|
2870
3959
|
}
|
|
2871
|
-
function
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
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);
|
|
4000
|
+
}
|
|
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
|
+
}
|
|
4097
|
+
return {
|
|
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
|
+
},
|
|
4125
|
+
};
|
|
4126
|
+
}
|
|
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
|
+
}
|
|
4182
|
+
return {
|
|
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
|
+
},
|
|
4212
|
+
};
|
|
4213
|
+
}
|
|
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
|
+
};
|
|
2876
4272
|
}
|
|
2877
|
-
function
|
|
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
|
+
}
|
|
2878
4311
|
return {
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
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,
|
|
4367
|
+
},
|
|
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;
|
|
4474
|
+
}
|
|
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,
|
|
4674
|
+
},
|
|
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.',
|
|
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
|
+
},
|
|
2886
4996
|
};
|
|
2887
4997
|
}
|
|
2888
|
-
function
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
}
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
};
|
|
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
|
+
}
|
|
2932
5041
|
}
|
|
2933
|
-
|
|
2934
|
-
|
|
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) {
|
|
2935
5080
|
return {
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
5081
|
+
sensitiveInputs: [],
|
|
5082
|
+
frameCount: 0,
|
|
5083
|
+
crossOriginFrameCount: 0,
|
|
2939
5084
|
};
|
|
2940
5085
|
}
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
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,
|
|
2954
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.',
|
|
5145
|
+
});
|
|
2955
5146
|
}
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
5147
|
+
if (!session) {
|
|
5148
|
+
blockers.push({
|
|
5149
|
+
code: 'SESSION_NOT_FOUND',
|
|
5150
|
+
severity: 'error',
|
|
5151
|
+
source: 'session',
|
|
5152
|
+
message: `Session not found: ${options.sessionId}`,
|
|
5153
|
+
});
|
|
5154
|
+
}
|
|
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,
|
|
2972
5242
|
});
|
|
2973
5243
|
}
|
|
2974
|
-
if (
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
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,
|
|
2979
5252
|
});
|
|
2980
5253
|
}
|
|
2981
|
-
const
|
|
5254
|
+
const ready = blockers.length === 0;
|
|
2982
5255
|
return {
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
},
|
|
2996
|
-
|
|
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
|
+
})),
|
|
2997
5299
|
};
|
|
2998
5300
|
}
|
|
2999
5301
|
function createWorkflowStepId(step, index) {
|
|
@@ -3004,6 +5306,7 @@ async function captureWorkflowPageState(sessionId, capturePageState, mode) {
|
|
|
3004
5306
|
const maxTextLength = mode === 'fast' ? 60 : 80;
|
|
3005
5307
|
return capturePageState(sessionId, {
|
|
3006
5308
|
includeButtons: true,
|
|
5309
|
+
includeLinks: true,
|
|
3007
5310
|
includeInputs: true,
|
|
3008
5311
|
includeModals: true,
|
|
3009
5312
|
maxItems,
|
|
@@ -3058,6 +5361,20 @@ function resolveWorkflowRecommendedAction(error) {
|
|
|
3058
5361
|
if (error.code === 'page_state_not_matched' || error.code === 'page_state_assertion_failed') {
|
|
3059
5362
|
return 'inspect_page_state';
|
|
3060
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
|
+
}
|
|
3061
5378
|
return undefined;
|
|
3062
5379
|
}
|
|
3063
5380
|
function resolveWorkflowFailureSelector(step, stepResultTarget) {
|
|
@@ -3156,6 +5473,66 @@ function normalizeCaptureError(sessionId, error) {
|
|
|
3156
5473
|
}
|
|
3157
5474
|
return fallback;
|
|
3158
5475
|
}
|
|
5476
|
+
function isCaptureTimeoutMessage(message) {
|
|
5477
|
+
const normalized = message.toLowerCase();
|
|
5478
|
+
return normalized.includes('timed out') || normalized.includes('timeout');
|
|
5479
|
+
}
|
|
5480
|
+
function isRecoverableOverrideLiveCommandError(error) {
|
|
5481
|
+
if (isLiveSessionDisconnectedError(error)) {
|
|
5482
|
+
return true;
|
|
5483
|
+
}
|
|
5484
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
5485
|
+
return isCaptureTimeoutMessage(message);
|
|
5486
|
+
}
|
|
5487
|
+
function extractTimeoutMsFromMessage(message, fallback) {
|
|
5488
|
+
const match = message.match(/(?:after|waiting)\s+(\d+)ms/i);
|
|
5489
|
+
if (!match) {
|
|
5490
|
+
return fallback;
|
|
5491
|
+
}
|
|
5492
|
+
const parsed = Number.parseInt(match[1] ?? '', 10);
|
|
5493
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
5494
|
+
}
|
|
5495
|
+
function buildOverrideLiveCommandFailure(options) {
|
|
5496
|
+
const originalMessage = options.error instanceof Error ? options.error.message : String(options.error);
|
|
5497
|
+
const timeout = extractTimeoutMsFromMessage(originalMessage, options.timeoutMs);
|
|
5498
|
+
const timedOut = isCaptureTimeoutMessage(originalMessage);
|
|
5499
|
+
const disconnected = isLiveSessionDisconnectedError(options.error);
|
|
5500
|
+
const sessionState = options.getSessionConnectionState?.(options.sessionId);
|
|
5501
|
+
const code = disconnected
|
|
5502
|
+
? LIVE_SESSION_DISCONNECTED_CODE
|
|
5503
|
+
: timedOut
|
|
5504
|
+
? OVERRIDE_LIVE_COMMAND_TIMEOUT_CODE
|
|
5505
|
+
: OVERRIDE_LIVE_COMMAND_FAILED_CODE;
|
|
5506
|
+
const message = timedOut
|
|
5507
|
+
? `${options.command} for session ${options.sessionId} timed out after ${timeout}ms before the live extension returned an override command result.`
|
|
5508
|
+
: disconnected
|
|
5509
|
+
? `${options.command} for session ${options.sessionId} could not reach a connected live extension target.`
|
|
5510
|
+
: `${options.command} for session ${options.sessionId} failed before returning an override command result.`;
|
|
5511
|
+
return {
|
|
5512
|
+
ok: false,
|
|
5513
|
+
available: false,
|
|
5514
|
+
code,
|
|
5515
|
+
command: options.command,
|
|
5516
|
+
timeoutMs: timeout,
|
|
5517
|
+
timedOut,
|
|
5518
|
+
disconnected,
|
|
5519
|
+
message,
|
|
5520
|
+
originalMessage,
|
|
5521
|
+
sessionConnected: sessionState?.connected,
|
|
5522
|
+
disconnectedAt: sessionState?.disconnectedAt,
|
|
5523
|
+
disconnectReason: sessionState?.disconnectReason,
|
|
5524
|
+
};
|
|
5525
|
+
}
|
|
5526
|
+
function createOverrideLiveCommandError(options) {
|
|
5527
|
+
const failure = buildOverrideLiveCommandFailure(options);
|
|
5528
|
+
const code = String(failure.code ?? OVERRIDE_LIVE_COMMAND_FAILED_CODE);
|
|
5529
|
+
const message = `${code}: ${options.command} for session ${options.sessionId} ${failure.timedOut === true
|
|
5530
|
+
? `timed out after ${String(failure.timeoutMs)}ms`
|
|
5531
|
+
: `failed`}. ${String(failure.message ?? '')} Original error: ${String(failure.originalMessage ?? 'unknown')}`;
|
|
5532
|
+
const error = new Error(message);
|
|
5533
|
+
Object.assign(error, { code, details: failure });
|
|
5534
|
+
return error;
|
|
5535
|
+
}
|
|
3159
5536
|
function isLiveSessionDisconnectedError(error) {
|
|
3160
5537
|
return error instanceof LiveSessionDisconnectedError;
|
|
3161
5538
|
}
|
|
@@ -3167,12 +5544,99 @@ async function executeLiveCapture(captureClient, sessionId, command, payload, ti
|
|
|
3167
5544
|
throw normalizeCaptureError(sessionId, error);
|
|
3168
5545
|
}
|
|
3169
5546
|
}
|
|
5547
|
+
async function executeOverrideLiveCaptureWithDiagnostics(options) {
|
|
5548
|
+
try {
|
|
5549
|
+
const capture = await executeLiveCapture(options.captureClient, options.sessionId, options.command, options.payload, options.timeoutMs);
|
|
5550
|
+
return { capture, payload: ensureCaptureSuccess(capture, options.sessionId) };
|
|
5551
|
+
}
|
|
5552
|
+
catch (error) {
|
|
5553
|
+
throw createOverrideLiveCommandError({
|
|
5554
|
+
sessionId: options.sessionId,
|
|
5555
|
+
command: options.command,
|
|
5556
|
+
timeoutMs: options.timeoutMs,
|
|
5557
|
+
error,
|
|
5558
|
+
getSessionConnectionState: options.getSessionConnectionState,
|
|
5559
|
+
});
|
|
5560
|
+
}
|
|
5561
|
+
}
|
|
3170
5562
|
function ensureCaptureSuccess(result, sessionId) {
|
|
3171
5563
|
if (!result.ok) {
|
|
3172
5564
|
throw normalizeCaptureError(sessionId, new Error(result.error ?? 'Capture command failed'));
|
|
3173
5565
|
}
|
|
3174
5566
|
return result.payload ?? {};
|
|
3175
5567
|
}
|
|
5568
|
+
async function refreshObservedAssetsForOverrideEnable(options) {
|
|
5569
|
+
const { payload } = await executeOverrideLiveCaptureWithDiagnostics({
|
|
5570
|
+
captureClient: options.captureClient,
|
|
5571
|
+
sessionId: options.sessionId,
|
|
5572
|
+
command: 'CAPTURE_OVERRIDE_OBSERVE_ASSETS',
|
|
5573
|
+
payload: { tabId: options.tabId, includePerformance: true },
|
|
5574
|
+
timeoutMs: 5_000,
|
|
5575
|
+
getSessionConnectionState: options.getSessionConnectionState,
|
|
5576
|
+
});
|
|
5577
|
+
persistObservedOverrideAssets(options.db, {
|
|
5578
|
+
...payload,
|
|
5579
|
+
sessionId: options.sessionId,
|
|
5580
|
+
tabId: payload.tabId ?? options.tabId,
|
|
5581
|
+
});
|
|
5582
|
+
return {
|
|
5583
|
+
tabId: typeof payload.tabId === 'number' ? payload.tabId : options.tabId,
|
|
5584
|
+
pageUrl: typeof payload.pageUrl === 'string' ? payload.pageUrl : undefined,
|
|
5585
|
+
assetCount: Array.isArray(payload.assets) ? payload.assets.length : 0,
|
|
5586
|
+
};
|
|
5587
|
+
}
|
|
5588
|
+
function buildPersistedOverrideStatus(options) {
|
|
5589
|
+
let profile = null;
|
|
5590
|
+
let profileError;
|
|
5591
|
+
try {
|
|
5592
|
+
profile = resolveOverrideProfileRecord(options.profileId);
|
|
5593
|
+
}
|
|
5594
|
+
catch (error) {
|
|
5595
|
+
profileError = error instanceof Error ? error.message : String(error);
|
|
5596
|
+
}
|
|
5597
|
+
const latestRun = listOverridePocRuns(options.db, options.sessionId, 1, 0).runs[0] ?? null;
|
|
5598
|
+
const recentRequests = listOverridePocRequests(options.db, options.sessionId, 5, 0, latestRun?.runId).requests;
|
|
5599
|
+
const recentPlans = listOverridePlanAudits(options.db, { sessionId: options.sessionId, limit: 5, offset: 0 }).plans;
|
|
5600
|
+
let preflight;
|
|
5601
|
+
if (profileError) {
|
|
5602
|
+
preflight = {
|
|
5603
|
+
ready: false,
|
|
5604
|
+
profileId: null,
|
|
5605
|
+
profile: null,
|
|
5606
|
+
issues: [{
|
|
5607
|
+
code: 'OVERRIDE_CONFIG_UNAVAILABLE',
|
|
5608
|
+
severity: 'error',
|
|
5609
|
+
source: 'profile',
|
|
5610
|
+
message: profileError,
|
|
5611
|
+
}],
|
|
5612
|
+
checks: {
|
|
5613
|
+
sessionFound: auditSessionExists(options.db, options.sessionId),
|
|
5614
|
+
connected: options.getSessionConnectionState?.(options.sessionId)?.connected === true,
|
|
5615
|
+
},
|
|
5616
|
+
nextActions: [{
|
|
5617
|
+
code: 'FIX_OVERRIDE_CONFIG_PATH',
|
|
5618
|
+
message: 'Create a readable override-poc config or point OVERRIDE_POC_CONFIG_PATH at the intended config, then retry override status.',
|
|
5619
|
+
}],
|
|
5620
|
+
};
|
|
5621
|
+
}
|
|
5622
|
+
else {
|
|
5623
|
+
preflight = buildOverridePreflight({
|
|
5624
|
+
db: options.db,
|
|
5625
|
+
sessionId: options.sessionId,
|
|
5626
|
+
profileId: options.profileId,
|
|
5627
|
+
getSessionConnectionState: options.getSessionConnectionState,
|
|
5628
|
+
});
|
|
5629
|
+
}
|
|
5630
|
+
return {
|
|
5631
|
+
profile,
|
|
5632
|
+
profileError,
|
|
5633
|
+
latestRun,
|
|
5634
|
+
recentRequests,
|
|
5635
|
+
recentPlans,
|
|
5636
|
+
preflight,
|
|
5637
|
+
diagnosis: diagnoseOverridePoc(options.db, options.sessionId, latestRun?.runId),
|
|
5638
|
+
};
|
|
5639
|
+
}
|
|
3176
5640
|
function auditSessionExists(db, sessionId) {
|
|
3177
5641
|
const row = db.prepare('SELECT 1 FROM sessions WHERE session_id = ? LIMIT 1').get(sessionId);
|
|
3178
5642
|
return row !== undefined;
|
|
@@ -3655,15 +6119,17 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
|
|
|
3655
6119
|
recommendedAction,
|
|
3656
6120
|
};
|
|
3657
6121
|
},
|
|
3658
|
-
list_override_profiles: async () => {
|
|
6122
|
+
list_override_profiles: async (input) => {
|
|
3659
6123
|
const profiles = buildOverrideProfileRecords();
|
|
6124
|
+
const responseProfile = resolveOverrideResponseProfile(input.responseProfile);
|
|
3660
6125
|
return {
|
|
3661
6126
|
...createBaseResponse(),
|
|
3662
6127
|
limitsApplied: {
|
|
3663
6128
|
maxResults: profiles.length,
|
|
3664
6129
|
truncated: false,
|
|
3665
6130
|
},
|
|
3666
|
-
|
|
6131
|
+
responseProfile,
|
|
6132
|
+
profiles: profiles.map((profile) => serializeOverrideProfile(profile, responseProfile)),
|
|
3667
6133
|
nextActions: profiles.length > 0
|
|
3668
6134
|
? [{ code: 'VALIDATE_PROFILE', message: 'Run validate_override_profile before enabling overrides.' }]
|
|
3669
6135
|
: [{ code: 'CREATE_PROFILE', message: 'Run create_override_profile to generate a candidate profile.' }],
|
|
@@ -3701,6 +6167,8 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
|
|
|
3701
6167
|
});
|
|
3702
6168
|
const writeConfig = normalizeOptionalBooleanInput(input.writeConfig, 'writeConfig') ?? false;
|
|
3703
6169
|
const overwrite = normalizeOptionalBooleanInput(input.overwrite, 'overwrite') ?? false;
|
|
6170
|
+
const responseProfile = resolveOverrideResponseProfile(input.responseProfile);
|
|
6171
|
+
const includeConfigJson = input.includeConfigJson === true || responseProfile === 'full';
|
|
3704
6172
|
const write = {
|
|
3705
6173
|
written: false,
|
|
3706
6174
|
path: generated.suggestedConfigPath,
|
|
@@ -3749,21 +6217,31 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
|
|
|
3749
6217
|
warnings: generated.warnings,
|
|
3750
6218
|
nextActions,
|
|
3751
6219
|
write,
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
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,
|
|
3755
6231
|
};
|
|
3756
6232
|
},
|
|
3757
6233
|
validate_override_profile: async (input) => {
|
|
3758
6234
|
const profile = resolveOverrideProfileRecord(input.profileId);
|
|
3759
6235
|
const issues = buildOverrideProfileIssues(profile);
|
|
6236
|
+
const responseProfile = resolveOverrideResponseProfile(input.responseProfile);
|
|
3760
6237
|
return {
|
|
3761
6238
|
...createBaseResponse(),
|
|
3762
6239
|
profileId: profile.profileId,
|
|
3763
6240
|
valid: !issues.some((issue) => issue.severity === 'error'),
|
|
3764
6241
|
issues,
|
|
3765
6242
|
nextActions: buildOverrideProfileNextActions(profile, issues),
|
|
3766
|
-
|
|
6243
|
+
responseProfile,
|
|
6244
|
+
profile: serializeOverrideProfile(profile, responseProfile),
|
|
3767
6245
|
};
|
|
3768
6246
|
},
|
|
3769
6247
|
preflight_overrides: async (input) => {
|
|
@@ -3790,16 +6268,18 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
|
|
|
3790
6268
|
}
|
|
3791
6269
|
const assets = listObservedOverrideAssets(getDb(), {
|
|
3792
6270
|
sessionId,
|
|
3793
|
-
limit: typeof input.limit === 'number' ? input.limit :
|
|
6271
|
+
limit: typeof input.limit === 'number' ? input.limit : 50,
|
|
3794
6272
|
sinceTimestamp: typeof input.sinceTimestamp === 'number' ? input.sinceTimestamp : undefined,
|
|
3795
6273
|
});
|
|
6274
|
+
const responseProfile = resolveOverrideResponseProfile(input.responseProfile);
|
|
3796
6275
|
return {
|
|
3797
6276
|
...createBaseResponse(sessionId),
|
|
3798
6277
|
limitsApplied: {
|
|
3799
6278
|
maxResults: assets.length,
|
|
3800
6279
|
truncated: false,
|
|
3801
6280
|
},
|
|
3802
|
-
|
|
6281
|
+
responseProfile,
|
|
6282
|
+
assets: responseProfile === 'full' ? assets : assets.map(compactObservedOverrideAsset),
|
|
3803
6283
|
};
|
|
3804
6284
|
},
|
|
3805
6285
|
plan_override_response_patch: async (input) => {
|
|
@@ -5247,9 +7727,10 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
|
|
|
5247
7727
|
r.status,
|
|
5248
7728
|
r.started_at,
|
|
5249
7729
|
r.completed_at,
|
|
5250
|
-
|
|
5251
|
-
|
|
5252
|
-
|
|
7730
|
+
r.stop_reason,
|
|
7731
|
+
r.target_summary_json,
|
|
7732
|
+
r.diagnostics_json,
|
|
7733
|
+
r.failure_json,
|
|
5253
7734
|
r.redaction_json,
|
|
5254
7735
|
r.created_at,
|
|
5255
7736
|
r.updated_at,
|
|
@@ -5311,9 +7792,10 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
|
|
|
5311
7792
|
r.status,
|
|
5312
7793
|
r.started_at,
|
|
5313
7794
|
r.completed_at,
|
|
5314
|
-
|
|
5315
|
-
|
|
5316
|
-
|
|
7795
|
+
r.stop_reason,
|
|
7796
|
+
r.target_summary_json,
|
|
7797
|
+
r.diagnostics_json,
|
|
7798
|
+
r.failure_json,
|
|
5317
7799
|
r.redaction_json,
|
|
5318
7800
|
r.created_at,
|
|
5319
7801
|
r.updated_at,
|
|
@@ -5345,9 +7827,10 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
|
|
|
5345
7827
|
started_at,
|
|
5346
7828
|
finished_at,
|
|
5347
7829
|
duration_ms,
|
|
5348
|
-
|
|
5349
|
-
|
|
5350
|
-
|
|
7830
|
+
tab_id,
|
|
7831
|
+
target_summary_json,
|
|
7832
|
+
diagnostics_json,
|
|
7833
|
+
redaction_json,
|
|
5351
7834
|
failure_json,
|
|
5352
7835
|
input_metadata_json,
|
|
5353
7836
|
event_type,
|
|
@@ -5381,12 +7864,14 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
|
|
|
5381
7864
|
const maxItems = resolveStructuredMaxItems(input.maxItems, 40);
|
|
5382
7865
|
const maxTextLength = resolveStructuredTextLength(input.maxTextLength, 80);
|
|
5383
7866
|
const includeButtons = input.includeButtons !== false;
|
|
7867
|
+
const includeLinks = input.includeLinks !== false;
|
|
5384
7868
|
const includeInputs = input.includeInputs !== false;
|
|
5385
7869
|
const includeModals = input.includeModals !== false;
|
|
5386
7870
|
const capture = await executeLiveCapture(captureClient, sessionId, 'CAPTURE_PAGE_STATE', {
|
|
5387
7871
|
maxItems,
|
|
5388
7872
|
maxTextLength,
|
|
5389
7873
|
includeButtons,
|
|
7874
|
+
includeLinks,
|
|
5390
7875
|
includeInputs,
|
|
5391
7876
|
includeModals,
|
|
5392
7877
|
}, 4_000);
|
|
@@ -5405,8 +7890,14 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
|
|
|
5405
7890
|
throw new Error('sessionId is required');
|
|
5406
7891
|
}
|
|
5407
7892
|
const tabId = resolveOptionalTabId(input.tabId);
|
|
5408
|
-
const capture = await
|
|
5409
|
-
|
|
7893
|
+
const { capture, payload } = await executeOverrideLiveCaptureWithDiagnostics({
|
|
7894
|
+
captureClient,
|
|
7895
|
+
sessionId,
|
|
7896
|
+
command: 'CAPTURE_OVERRIDE_OBSERVE_ASSETS',
|
|
7897
|
+
payload: { tabId, includePerformance: input.includePerformance !== false },
|
|
7898
|
+
timeoutMs: 5_000,
|
|
7899
|
+
getSessionConnectionState,
|
|
7900
|
+
});
|
|
5410
7901
|
const assetCount = Array.isArray(payload.assets) ? payload.assets.length : 0;
|
|
5411
7902
|
const persisted = getDb
|
|
5412
7903
|
? persistObservedOverrideAssets(getDb(), { ...payload, sessionId, tabId: payload.tabId ?? tabId })
|
|
@@ -5436,23 +7927,31 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
|
|
|
5436
7927
|
assertOverrideResponseRequestCaptureSafe({
|
|
5437
7928
|
requestMethod: input.requestMethod,
|
|
5438
7929
|
requestHeaders: input.requestHeaders,
|
|
7930
|
+
ruleType: input.ruleType,
|
|
5439
7931
|
subject: 'Response body capture request',
|
|
5440
7932
|
});
|
|
5441
7933
|
const tabId = resolveOptionalTabId(input.tabId);
|
|
5442
7934
|
const timeoutMs = resolveTimeoutMs(input.timeoutMs, 10_000, 60_000);
|
|
5443
|
-
const capture = await
|
|
5444
|
-
|
|
5445
|
-
|
|
5446
|
-
|
|
5447
|
-
|
|
5448
|
-
|
|
5449
|
-
|
|
5450
|
-
|
|
5451
|
-
|
|
5452
|
-
|
|
5453
|
-
|
|
5454
|
-
|
|
5455
|
-
|
|
7935
|
+
const { capture, payload } = await executeOverrideLiveCaptureWithDiagnostics({
|
|
7936
|
+
captureClient,
|
|
7937
|
+
sessionId,
|
|
7938
|
+
command: 'CAPTURE_OVERRIDE_RESPONSE_BODY',
|
|
7939
|
+
payload: {
|
|
7940
|
+
targetUrl,
|
|
7941
|
+
tabId,
|
|
7942
|
+
captureMode: normalizeOptionalString(input.captureMode),
|
|
7943
|
+
triggerReload: typeof input.triggerReload === 'boolean' ? input.triggerReload : undefined,
|
|
7944
|
+
matchMode: normalizeOptionalString(input.matchMode),
|
|
7945
|
+
ruleType: normalizeOptionalString(input.ruleType),
|
|
7946
|
+
requestMethod: input.requestMethod,
|
|
7947
|
+
requestHeaders: isRecord(input.requestHeaders) ? input.requestHeaders : undefined,
|
|
7948
|
+
timeoutMs,
|
|
7949
|
+
maxBodyBytes: input.maxBodyBytes,
|
|
7950
|
+
includeBody: input.includeBody === true,
|
|
7951
|
+
},
|
|
7952
|
+
timeoutMs: timeoutMs + 2_000,
|
|
7953
|
+
getSessionConnectionState,
|
|
7954
|
+
});
|
|
5456
7955
|
return {
|
|
5457
7956
|
...createBaseResponse(sessionId),
|
|
5458
7957
|
limitsApplied: {
|
|
@@ -5480,19 +7979,26 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
|
|
|
5480
7979
|
}
|
|
5481
7980
|
const tabId = resolveOptionalTabId(input.tabId);
|
|
5482
7981
|
const timeoutMs = resolveTimeoutMs(input.timeoutMs, 10_000, 60_000);
|
|
5483
|
-
const
|
|
5484
|
-
|
|
5485
|
-
|
|
5486
|
-
|
|
5487
|
-
|
|
5488
|
-
|
|
5489
|
-
|
|
5490
|
-
|
|
5491
|
-
|
|
5492
|
-
|
|
5493
|
-
|
|
5494
|
-
|
|
5495
|
-
|
|
7982
|
+
const { payload } = await executeOverrideLiveCaptureWithDiagnostics({
|
|
7983
|
+
captureClient,
|
|
7984
|
+
sessionId,
|
|
7985
|
+
command: 'CAPTURE_OVERRIDE_RESPONSE_BODY',
|
|
7986
|
+
payload: {
|
|
7987
|
+
targetUrl,
|
|
7988
|
+
tabId,
|
|
7989
|
+
captureMode: normalizeOptionalString(input.captureMode),
|
|
7990
|
+
triggerReload: typeof input.triggerReload === 'boolean' ? input.triggerReload : undefined,
|
|
7991
|
+
matchMode: normalizeOptionalString(input.matchMode),
|
|
7992
|
+
ruleType: normalizeOptionalString(input.ruleType),
|
|
7993
|
+
requestMethod: input.requestMethod,
|
|
7994
|
+
requestHeaders: isRecord(input.requestHeaders) ? input.requestHeaders : undefined,
|
|
7995
|
+
timeoutMs,
|
|
7996
|
+
maxBodyBytes: input.maxBodyBytes,
|
|
7997
|
+
includeBody: true,
|
|
7998
|
+
},
|
|
7999
|
+
timeoutMs: timeoutMs + 2_000,
|
|
8000
|
+
getSessionConnectionState,
|
|
8001
|
+
});
|
|
5496
8002
|
if (payload.truncated === true) {
|
|
5497
8003
|
throw new Error('Captured response body was truncated; increase maxBodyBytes before planning a patch.');
|
|
5498
8004
|
}
|
|
@@ -5585,8 +8091,10 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
|
|
|
5585
8091
|
let observedFromPersisted;
|
|
5586
8092
|
if (!Array.isArray(observedAssets) && sessionId) {
|
|
5587
8093
|
const tabId = resolveOptionalTabId(input.tabId);
|
|
8094
|
+
const command = 'CAPTURE_OVERRIDE_OBSERVE_ASSETS';
|
|
8095
|
+
const timeoutMs = 5_000;
|
|
5588
8096
|
try {
|
|
5589
|
-
const capture = await executeLiveCapture(captureClient, sessionId,
|
|
8097
|
+
const capture = await executeLiveCapture(captureClient, sessionId, command, { tabId, includePerformance: true }, timeoutMs);
|
|
5590
8098
|
observedFromLiveTab = ensureCaptureSuccess(capture, sessionId);
|
|
5591
8099
|
observedAssets = observedFromLiveTab.assets;
|
|
5592
8100
|
if (getDb) {
|
|
@@ -5594,11 +8102,22 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
|
|
|
5594
8102
|
}
|
|
5595
8103
|
}
|
|
5596
8104
|
catch (error) {
|
|
5597
|
-
if (
|
|
8105
|
+
if (getDb && isRecoverableOverrideLiveCommandError(error)) {
|
|
8106
|
+
observedAssets = listObservedOverrideAssets(getDb(), { sessionId });
|
|
8107
|
+
observedFromPersisted = { sessionId, assetCount: Array.isArray(observedAssets) ? observedAssets.length : 0 };
|
|
8108
|
+
}
|
|
8109
|
+
else if (isRecoverableOverrideLiveCommandError(error)) {
|
|
8110
|
+
throw createOverrideLiveCommandError({
|
|
8111
|
+
sessionId,
|
|
8112
|
+
command,
|
|
8113
|
+
timeoutMs,
|
|
8114
|
+
error,
|
|
8115
|
+
getSessionConnectionState,
|
|
8116
|
+
});
|
|
8117
|
+
}
|
|
8118
|
+
else {
|
|
5598
8119
|
throw error;
|
|
5599
8120
|
}
|
|
5600
|
-
observedAssets = listObservedOverrideAssets(getDb(), { sessionId });
|
|
5601
|
-
observedFromPersisted = { sessionId, assetCount: Array.isArray(observedAssets) ? observedAssets.length : 0 };
|
|
5602
8121
|
}
|
|
5603
8122
|
}
|
|
5604
8123
|
const mapping = await mapNextOverrideAssetsWithDrift({
|
|
@@ -5642,8 +8161,10 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
|
|
|
5642
8161
|
let observedFromPersisted;
|
|
5643
8162
|
if (!Array.isArray(observedAssets) && sessionId) {
|
|
5644
8163
|
const tabId = resolveOptionalTabId(input.tabId);
|
|
8164
|
+
const command = 'CAPTURE_OVERRIDE_OBSERVE_ASSETS';
|
|
8165
|
+
const timeoutMs = 5_000;
|
|
5645
8166
|
try {
|
|
5646
|
-
const capture = await executeLiveCapture(captureClient, sessionId,
|
|
8167
|
+
const capture = await executeLiveCapture(captureClient, sessionId, command, { tabId, includePerformance: true }, timeoutMs);
|
|
5647
8168
|
observedFromLiveTab = ensureCaptureSuccess(capture, sessionId);
|
|
5648
8169
|
observedAssets = observedFromLiveTab.assets;
|
|
5649
8170
|
if (getDb) {
|
|
@@ -5651,11 +8172,22 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
|
|
|
5651
8172
|
}
|
|
5652
8173
|
}
|
|
5653
8174
|
catch (error) {
|
|
5654
|
-
if (
|
|
8175
|
+
if (getDb && isRecoverableOverrideLiveCommandError(error)) {
|
|
8176
|
+
observedAssets = listObservedOverrideAssets(getDb(), { sessionId });
|
|
8177
|
+
observedFromPersisted = { sessionId, assetCount: Array.isArray(observedAssets) ? observedAssets.length : 0 };
|
|
8178
|
+
}
|
|
8179
|
+
else if (isRecoverableOverrideLiveCommandError(error)) {
|
|
8180
|
+
throw createOverrideLiveCommandError({
|
|
8181
|
+
sessionId,
|
|
8182
|
+
command,
|
|
8183
|
+
timeoutMs,
|
|
8184
|
+
error,
|
|
8185
|
+
getSessionConnectionState,
|
|
8186
|
+
});
|
|
8187
|
+
}
|
|
8188
|
+
else {
|
|
5655
8189
|
throw error;
|
|
5656
8190
|
}
|
|
5657
|
-
observedAssets = listObservedOverrideAssets(getDb(), { sessionId });
|
|
5658
|
-
observedFromPersisted = { sessionId, assetCount: Array.isArray(observedAssets) ? observedAssets.length : 0 };
|
|
5659
8191
|
}
|
|
5660
8192
|
}
|
|
5661
8193
|
const plan = await planNextSourceOverride({
|
|
@@ -5716,14 +8248,58 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
|
|
|
5716
8248
|
if (!sessionId) {
|
|
5717
8249
|
throw new Error('sessionId is required');
|
|
5718
8250
|
}
|
|
5719
|
-
const
|
|
5720
|
-
const
|
|
8251
|
+
const command = 'CAPTURE_OVERRIDE_POC_GET_STATUS';
|
|
8252
|
+
const timeoutMs = 3_000;
|
|
8253
|
+
let capture;
|
|
8254
|
+
let payload;
|
|
8255
|
+
try {
|
|
8256
|
+
capture = await executeLiveCapture(captureClient, sessionId, command, {}, timeoutMs);
|
|
8257
|
+
payload = ensureCaptureSuccess(capture, sessionId);
|
|
8258
|
+
}
|
|
8259
|
+
catch (error) {
|
|
8260
|
+
if (!getDb || !isRecoverableOverrideLiveCommandError(error)) {
|
|
8261
|
+
throw error;
|
|
8262
|
+
}
|
|
8263
|
+
const persisted = buildPersistedOverrideStatus({
|
|
8264
|
+
db: getDb(),
|
|
8265
|
+
sessionId,
|
|
8266
|
+
profileId: input.profileId,
|
|
8267
|
+
getSessionConnectionState,
|
|
8268
|
+
});
|
|
8269
|
+
const liveStatus = buildOverrideLiveCommandFailure({
|
|
8270
|
+
sessionId,
|
|
8271
|
+
command,
|
|
8272
|
+
timeoutMs,
|
|
8273
|
+
error,
|
|
8274
|
+
getSessionConnectionState,
|
|
8275
|
+
});
|
|
8276
|
+
return {
|
|
8277
|
+
...createBaseResponse(sessionId),
|
|
8278
|
+
limitsApplied: {
|
|
8279
|
+
maxResults: 1,
|
|
8280
|
+
truncated: false,
|
|
8281
|
+
},
|
|
8282
|
+
statusSource: 'persisted-audit',
|
|
8283
|
+
liveStatus,
|
|
8284
|
+
...persisted,
|
|
8285
|
+
nextActions: [
|
|
8286
|
+
{ code: 'RECONNECT_OR_RETRY_OVERRIDE_STATUS', message: 'Reconnect or rebind the top-level session, then retry get_override_status for live debugger state.' },
|
|
8287
|
+
{ code: 'DIAGNOSE_OVERRIDES', message: 'Run diagnose_overrides to inspect persisted run and readiness signals while live status is unavailable.' },
|
|
8288
|
+
],
|
|
8289
|
+
};
|
|
8290
|
+
}
|
|
5721
8291
|
return {
|
|
5722
8292
|
...createBaseResponse(sessionId),
|
|
5723
8293
|
limitsApplied: {
|
|
5724
8294
|
maxResults: 1,
|
|
5725
8295
|
truncated: capture.truncated ?? false,
|
|
5726
8296
|
},
|
|
8297
|
+
statusSource: 'live',
|
|
8298
|
+
liveStatus: {
|
|
8299
|
+
available: true,
|
|
8300
|
+
command,
|
|
8301
|
+
timeoutMs,
|
|
8302
|
+
},
|
|
5727
8303
|
preflight: getDb
|
|
5728
8304
|
? buildOverridePreflight({
|
|
5729
8305
|
db: getDb(),
|
|
@@ -5763,7 +8339,8 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
|
|
|
5763
8339
|
if (!sessionId) {
|
|
5764
8340
|
throw new Error('sessionId is required');
|
|
5765
8341
|
}
|
|
5766
|
-
const
|
|
8342
|
+
const tabId = resolveOptionalTabId(input.tabId);
|
|
8343
|
+
let preflight = getDb
|
|
5767
8344
|
? buildOverridePreflight({
|
|
5768
8345
|
db: getDb(),
|
|
5769
8346
|
sessionId,
|
|
@@ -5771,20 +8348,58 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
|
|
|
5771
8348
|
getSessionConnectionState,
|
|
5772
8349
|
})
|
|
5773
8350
|
: null;
|
|
8351
|
+
let observedBeforeEnable;
|
|
8352
|
+
let observedAssetRefreshError;
|
|
8353
|
+
const initialBlockingCodes = getBlockingPreflightCodes(preflight);
|
|
8354
|
+
const initialProfile = isRecord(preflight?.profile) ? preflight.profile : {};
|
|
8355
|
+
const initialExperimentalBypass = canBypassPreflightForExperimentalRsc(initialProfile, initialBlockingCodes);
|
|
8356
|
+
if (preflight && getDb && !initialExperimentalBypass && shouldRefreshObservedAssetsForEnable(preflight)) {
|
|
8357
|
+
try {
|
|
8358
|
+
observedBeforeEnable = await refreshObservedAssetsForOverrideEnable({
|
|
8359
|
+
captureClient,
|
|
8360
|
+
db: getDb(),
|
|
8361
|
+
sessionId,
|
|
8362
|
+
tabId,
|
|
8363
|
+
getSessionConnectionState,
|
|
8364
|
+
});
|
|
8365
|
+
preflight = buildOverridePreflight({
|
|
8366
|
+
db: getDb(),
|
|
8367
|
+
sessionId,
|
|
8368
|
+
profileId: input.profileId,
|
|
8369
|
+
getSessionConnectionState,
|
|
8370
|
+
});
|
|
8371
|
+
}
|
|
8372
|
+
catch (error) {
|
|
8373
|
+
observedAssetRefreshError = error instanceof Error ? error.message : String(error);
|
|
8374
|
+
}
|
|
8375
|
+
}
|
|
5774
8376
|
if (preflight && preflight.ready !== true) {
|
|
5775
|
-
const blockingCodes =
|
|
5776
|
-
? preflight.issues
|
|
5777
|
-
.filter((issue) => isRecord(issue) && issue.severity === 'error')
|
|
5778
|
-
.map((issue) => String(issue.code ?? 'UNKNOWN'))
|
|
5779
|
-
: [];
|
|
8377
|
+
const blockingCodes = getBlockingPreflightCodes(preflight);
|
|
5780
8378
|
const profile = isRecord(preflight.profile) ? preflight.profile : {};
|
|
5781
8379
|
if (!canBypassPreflightForExperimentalRsc(profile, blockingCodes)) {
|
|
5782
|
-
|
|
8380
|
+
const refreshSuffix = observedAssetRefreshError
|
|
8381
|
+
? `; observed asset refresh failed: ${observedAssetRefreshError}`
|
|
8382
|
+
: '';
|
|
8383
|
+
throw new Error(`Override preflight failed: ${blockingCodes.join(', ') || 'UNKNOWN'}${refreshSuffix}`);
|
|
5783
8384
|
}
|
|
5784
8385
|
}
|
|
5785
|
-
const
|
|
5786
|
-
const
|
|
5787
|
-
|
|
8386
|
+
const command = 'CAPTURE_OVERRIDE_POC_ENABLE';
|
|
8387
|
+
const timeoutMs = 8_000;
|
|
8388
|
+
let capture;
|
|
8389
|
+
let payload;
|
|
8390
|
+
try {
|
|
8391
|
+
capture = await executeLiveCapture(captureClient, sessionId, command, { tabId }, timeoutMs);
|
|
8392
|
+
payload = ensureCaptureSuccess(capture, sessionId);
|
|
8393
|
+
}
|
|
8394
|
+
catch (error) {
|
|
8395
|
+
throw createOverrideLiveCommandError({
|
|
8396
|
+
sessionId,
|
|
8397
|
+
command,
|
|
8398
|
+
timeoutMs,
|
|
8399
|
+
error,
|
|
8400
|
+
getSessionConnectionState,
|
|
8401
|
+
});
|
|
8402
|
+
}
|
|
5788
8403
|
return {
|
|
5789
8404
|
...createBaseResponse(sessionId),
|
|
5790
8405
|
limitsApplied: {
|
|
@@ -5792,6 +8407,7 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
|
|
|
5792
8407
|
truncated: capture.truncated ?? false,
|
|
5793
8408
|
},
|
|
5794
8409
|
preflight,
|
|
8410
|
+
observedBeforeEnable,
|
|
5795
8411
|
...payload,
|
|
5796
8412
|
nextActions: [{ code: 'RELOAD_OR_INTERACT', message: 'Reload or interact with the tab so configured asset requests occur under the active override.' }],
|
|
5797
8413
|
};
|
|
@@ -5801,14 +8417,64 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
|
|
|
5801
8417
|
if (!sessionId) {
|
|
5802
8418
|
throw new Error('sessionId is required');
|
|
5803
8419
|
}
|
|
5804
|
-
const
|
|
5805
|
-
const
|
|
8420
|
+
const command = 'CAPTURE_OVERRIDE_POC_DISABLE';
|
|
8421
|
+
const timeoutMs = 5_000;
|
|
8422
|
+
let capture;
|
|
8423
|
+
let payload;
|
|
8424
|
+
try {
|
|
8425
|
+
capture = await executeLiveCapture(captureClient, sessionId, command, {}, timeoutMs);
|
|
8426
|
+
payload = ensureCaptureSuccess(capture, sessionId);
|
|
8427
|
+
}
|
|
8428
|
+
catch (error) {
|
|
8429
|
+
if (!getDb || !isRecoverableOverrideLiveCommandError(error)) {
|
|
8430
|
+
throw createOverrideLiveCommandError({
|
|
8431
|
+
sessionId,
|
|
8432
|
+
command,
|
|
8433
|
+
timeoutMs,
|
|
8434
|
+
error,
|
|
8435
|
+
getSessionConnectionState,
|
|
8436
|
+
});
|
|
8437
|
+
}
|
|
8438
|
+
const persisted = buildPersistedOverrideStatus({
|
|
8439
|
+
db: getDb(),
|
|
8440
|
+
sessionId,
|
|
8441
|
+
profileId: input.profileId,
|
|
8442
|
+
getSessionConnectionState,
|
|
8443
|
+
});
|
|
8444
|
+
const disableAttempt = buildOverrideLiveCommandFailure({
|
|
8445
|
+
sessionId,
|
|
8446
|
+
command,
|
|
8447
|
+
timeoutMs,
|
|
8448
|
+
error,
|
|
8449
|
+
getSessionConnectionState,
|
|
8450
|
+
});
|
|
8451
|
+
return {
|
|
8452
|
+
...createBaseResponse(sessionId),
|
|
8453
|
+
limitsApplied: {
|
|
8454
|
+
maxResults: 1,
|
|
8455
|
+
truncated: false,
|
|
8456
|
+
},
|
|
8457
|
+
statusSource: 'persisted-audit',
|
|
8458
|
+
disableAttempt,
|
|
8459
|
+
...persisted,
|
|
8460
|
+
nextActions: [
|
|
8461
|
+
{ code: 'RECONNECT_OR_RETRY_DISABLE', message: 'Reconnect or rebind the top-level session, then retry disable_overrides to confirm debugger detachment.' },
|
|
8462
|
+
{ code: 'GET_OVERRIDE_STATUS', message: 'Run get_override_status after reconnecting to verify whether the override is still active.' },
|
|
8463
|
+
],
|
|
8464
|
+
};
|
|
8465
|
+
}
|
|
5806
8466
|
return {
|
|
5807
8467
|
...createBaseResponse(sessionId),
|
|
5808
8468
|
limitsApplied: {
|
|
5809
8469
|
maxResults: 1,
|
|
5810
8470
|
truncated: capture.truncated ?? false,
|
|
5811
8471
|
},
|
|
8472
|
+
statusSource: 'live',
|
|
8473
|
+
disableAttempt: {
|
|
8474
|
+
ok: true,
|
|
8475
|
+
command,
|
|
8476
|
+
timeoutMs,
|
|
8477
|
+
},
|
|
5812
8478
|
...payload,
|
|
5813
8479
|
nextActions: [{ code: 'VERIFY_DISABLED', message: 'Run get_override_status if you need to confirm the debugger override is inactive.' }],
|
|
5814
8480
|
};
|
|
@@ -5880,7 +8546,10 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
|
|
|
5880
8546
|
throw new Error('selector is required');
|
|
5881
8547
|
}
|
|
5882
8548
|
const properties = asStringArray(input.properties, 64);
|
|
5883
|
-
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);
|
|
5884
8553
|
return {
|
|
5885
8554
|
...createBaseResponse(sessionId),
|
|
5886
8555
|
limitsApplied: {
|
|
@@ -5896,7 +8565,10 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
|
|
|
5896
8565
|
throw new Error('sessionId is required');
|
|
5897
8566
|
}
|
|
5898
8567
|
const selector = typeof input.selector === 'string' ? input.selector : undefined;
|
|
5899
|
-
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);
|
|
5900
8572
|
return {
|
|
5901
8573
|
...createBaseResponse(sessionId),
|
|
5902
8574
|
limitsApplied: {
|
|
@@ -5927,6 +8599,7 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
|
|
|
5927
8599
|
const normalizedInput = {
|
|
5928
8600
|
...input,
|
|
5929
8601
|
includeButtons: kinds.includes('buttons'),
|
|
8602
|
+
includeLinks: kinds.includes('links'),
|
|
5930
8603
|
includeInputs: kinds.includes('inputs'),
|
|
5931
8604
|
includeModals: kinds.includes('modals'),
|
|
5932
8605
|
};
|
|
@@ -6007,6 +8680,247 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
|
|
|
6007
8680
|
...waited,
|
|
6008
8681
|
};
|
|
6009
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
|
+
},
|
|
6010
8924
|
run_ui_steps: async (input) => {
|
|
6011
8925
|
const request = RunUIStepsSchema.parse(input);
|
|
6012
8926
|
const workflowTraceId = createUIWorkflowTraceId();
|
|
@@ -6034,7 +8948,16 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
|
|
|
6034
8948
|
const previousCapture = lastPageCapture;
|
|
6035
8949
|
try {
|
|
6036
8950
|
if (step.kind === 'action') {
|
|
6037
|
-
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);
|
|
6038
8961
|
const liveRequest = LiveUIActionRequestSchema.parse({
|
|
6039
8962
|
action: step.action,
|
|
6040
8963
|
target: resolvedTarget.target,
|
|
@@ -6045,6 +8968,12 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
|
|
|
6045
8968
|
const payload = ensureCaptureSuccess(capture, request.sessionId);
|
|
6046
8969
|
const actionResult = payload;
|
|
6047
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;
|
|
6048
8977
|
let currentCapture = resolvedTarget.pageCapture ?? lastPageCapture;
|
|
6049
8978
|
if (!failed && request.mode === 'fast') {
|
|
6050
8979
|
await sleep(75);
|
|
@@ -6059,7 +8988,7 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
|
|
|
6059
8988
|
action: step.action,
|
|
6060
8989
|
traceId: actionResult.traceId,
|
|
6061
8990
|
target: {
|
|
6062
|
-
resolution: resolvedTarget.resolution,
|
|
8991
|
+
resolution: nativeLocatorResolution ?? resolvedTarget.resolution,
|
|
6063
8992
|
actionTarget: typeof actionResult.target === 'object' && actionResult.target !== null
|
|
6064
8993
|
? actionResult.target
|
|
6065
8994
|
: undefined,
|
|
@@ -6072,6 +9001,14 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
|
|
|
6072
9001
|
: undefined,
|
|
6073
9002
|
pageChangeSummary: createPageChangeSummary(previousCapture, currentCapture),
|
|
6074
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
|
+
}
|
|
6075
9012
|
}
|
|
6076
9013
|
else if (step.kind === 'waitFor') {
|
|
6077
9014
|
const waitInput = {
|
|
@@ -6099,6 +9036,48 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
|
|
|
6099
9036
|
pageChangeSummary: createPageChangeSummary(previousCapture, waited.lastCapture),
|
|
6100
9037
|
};
|
|
6101
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
|
+
}
|
|
6102
9081
|
else {
|
|
6103
9082
|
const capture = request.mode === 'fast' && lastPageCapture
|
|
6104
9083
|
? lastPageCapture
|
|
@@ -6133,7 +9112,8 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
|
|
|
6133
9112
|
target: step.kind === 'action' && workflowError
|
|
6134
9113
|
? workflowError.details
|
|
6135
9114
|
: undefined,
|
|
6136
|
-
matcher: step.kind === '
|
|
9115
|
+
matcher: step.kind === 'assert' || step.kind === 'waitFor' ? step.matcher : undefined,
|
|
9116
|
+
wait: step.kind === 'wait' ? step.wait : undefined,
|
|
6137
9117
|
error: normalizeWorkflowError(error),
|
|
6138
9118
|
};
|
|
6139
9119
|
}
|
|
@@ -6156,6 +9136,14 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
|
|
|
6156
9136
|
if (evidence) {
|
|
6157
9137
|
failureCaptureCount += 1;
|
|
6158
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
|
+
}
|
|
6159
9147
|
}
|
|
6160
9148
|
}
|
|
6161
9149
|
stepResults.push(finalStepResult);
|
|
@@ -6175,7 +9163,8 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
|
|
|
6175
9163
|
status: 'skipped',
|
|
6176
9164
|
durationMs: 0,
|
|
6177
9165
|
action: step.kind === 'action' ? step.action : undefined,
|
|
6178
|
-
matcher: step.kind === '
|
|
9166
|
+
matcher: step.kind === 'assert' || step.kind === 'waitFor' ? step.matcher : undefined,
|
|
9167
|
+
wait: step.kind === 'wait' ? step.wait : undefined,
|
|
6179
9168
|
pageChangeSummary: undefined,
|
|
6180
9169
|
error: {
|
|
6181
9170
|
code: 'workflow_stopped_early',
|
|
@@ -6375,7 +9364,62 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
|
|
|
6375
9364
|
const actionInput = { ...input };
|
|
6376
9365
|
delete actionInput.sessionId;
|
|
6377
9366
|
delete actionInput.captureOnFailure;
|
|
6378
|
-
|
|
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
|
+
}
|
|
6379
9423
|
const failureCaptureOptions = resolveFailureEvidenceCaptureOptions(input);
|
|
6380
9424
|
const capture = await executeLiveCapture(captureClient, sessionId, 'EXECUTE_UI_ACTION', request, 5_000);
|
|
6381
9425
|
const payload = ensureCaptureSuccess(capture, sessionId);
|
|
@@ -6400,6 +9444,24 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
|
|
|
6400
9444
|
const target = typeof actionResult.target === 'object' && actionResult.target !== null
|
|
6401
9445
|
? actionResult.target
|
|
6402
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
|
+
}
|
|
6403
9465
|
return {
|
|
6404
9466
|
...createBaseResponse(sessionId),
|
|
6405
9467
|
limitsApplied: {
|
|
@@ -6418,6 +9480,7 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
|
|
|
6418
9480
|
: undefined,
|
|
6419
9481
|
actionResult,
|
|
6420
9482
|
target,
|
|
9483
|
+
targetResolution,
|
|
6421
9484
|
tabContext: {
|
|
6422
9485
|
tabId: typeof target.tabId === 'number' ? target.tabId : undefined,
|
|
6423
9486
|
frameId: typeof target.frameId === 'number' ? target.frameId : 0,
|
|
@@ -6428,7 +9491,7 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
|
|
|
6428
9491
|
postActionState,
|
|
6429
9492
|
supportedScopes: {
|
|
6430
9493
|
executionScope: actionResult.executionScope,
|
|
6431
|
-
topDocumentOnly:
|
|
9494
|
+
topDocumentOnly: false,
|
|
6432
9495
|
opensNewBrowserSession: false,
|
|
6433
9496
|
},
|
|
6434
9497
|
};
|
|
@@ -6480,13 +9543,37 @@ export function createToolRegistry(overrides = {}) {
|
|
|
6480
9543
|
};
|
|
6481
9544
|
});
|
|
6482
9545
|
}
|
|
6483
|
-
export async function routeToolCall(tools, toolName, input) {
|
|
9546
|
+
export async function routeToolCall(tools, toolName, input, options = {}) {
|
|
6484
9547
|
const tool = tools.find((candidate) => candidate.name === toolName);
|
|
6485
9548
|
if (!tool) {
|
|
6486
9549
|
throw new Error(`Unknown tool: ${toolName}`);
|
|
6487
9550
|
}
|
|
6488
|
-
const
|
|
6489
|
-
|
|
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
|
+
}
|
|
6490
9577
|
}
|
|
6491
9578
|
export function createMCPServer(overrides = {}, options = {}) {
|
|
6492
9579
|
const logger = options.logger ?? createDefaultMcpLogger();
|
|
@@ -6498,6 +9585,17 @@ export function createMCPServer(overrides = {}, options = {}) {
|
|
|
6498
9585
|
...v2Handlers,
|
|
6499
9586
|
...overrides,
|
|
6500
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
|
+
});
|
|
6501
9599
|
const server = new Server({
|
|
6502
9600
|
name: 'browser-debug-mcp-bridge',
|
|
6503
9601
|
version: '1.0.0',
|
|
@@ -6521,7 +9619,7 @@ export function createMCPServer(overrides = {}, options = {}) {
|
|
|
6521
9619
|
const startedAt = Date.now();
|
|
6522
9620
|
logger.info({ component: 'mcp', event: 'tool_call_started', toolName }, '[MCPServer][MCP] Tool call started');
|
|
6523
9621
|
try {
|
|
6524
|
-
const response = await routeToolCall(tools, toolName, request.params.arguments);
|
|
9622
|
+
const response = await routeToolCall(tools, toolName, request.params.arguments, { loopGuard });
|
|
6525
9623
|
logger.info({
|
|
6526
9624
|
component: 'mcp',
|
|
6527
9625
|
event: 'tool_call_completed',
|