browser-debug-mcp-bridge 1.11.1 → 1.13.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 +4 -2
- 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 +300 -1
- package/apps/mcp-server/dist/db/migrations.js.map +1 -1
- package/apps/mcp-server/dist/db/schema.js +226 -2
- package/apps/mcp-server/dist/db/schema.js.map +1 -1
- package/apps/mcp-server/dist/lighthouse-report.js +1001 -0
- package/apps/mcp-server/dist/lighthouse-report.js.map +1 -0
- package/apps/mcp-server/dist/main.js +249 -1
- package/apps/mcp-server/dist/main.js.map +1 -1
- package/apps/mcp-server/dist/mcp/server.js +3705 -311
- 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/mock-store.js +408 -0
- package/apps/mcp-server/dist/mock-store.js.map +1 -0
- package/apps/mcp-server/dist/override-audit-contract.js +58 -0
- package/apps/mcp-server/dist/override-audit-contract.js.map +1 -1
- package/apps/mcp-server/dist/override-audit.js +100 -4
- package/apps/mcp-server/dist/override-audit.js.map +1 -1
- package/apps/mcp-server/dist/override-poc.js +4 -4
- package/apps/mcp-server/dist/override-poc.js.map +1 -1
- package/apps/mcp-server/dist/override-profile-generator.js +3 -9
- package/apps/mcp-server/dist/override-profile-generator.js.map +1 -1
- package/apps/mcp-server/dist/ssr-mock.js +480 -0
- package/apps/mcp-server/dist/ssr-mock.js.map +1 -0
- package/apps/mcp-server/dist/websocket/messages.js +5 -0
- package/apps/mcp-server/dist/websocket/messages.js.map +1 -1
- package/apps/mcp-server/package.json +5 -0
- package/package.json +9 -4
|
@@ -5,8 +5,10 @@ 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
|
-
import { diagnoseOverridePoc, insertOverridePlanAudit, listOverridePlanAudits, listOverridePocRequests, listOverridePocRuns, } from '../override-audit.js';
|
|
10
|
+
import { diagnoseOverridePoc, insertOverridePlanAudit, insertSsrMockAudit, listOverridePlanAudits, listOverridePocRequests, listOverridePocRuns, listSsrMockAudits, } from '../override-audit.js';
|
|
11
|
+
import { deleteMockRoute, getMockRoute, listMockHits, listMockRoutes, listMockRuns, upsertMockRoute } from '../mock-store.js';
|
|
10
12
|
import { createOverrideProfileConfig, OVERRIDE_PROFILE_ADAPTERS, } from '../override-profile-generator.js';
|
|
11
13
|
import { assertOverrideResponseRequestCaptureSafe, classifyOverrideResponseRequestCapability, } from '../override-capabilities.js';
|
|
12
14
|
import { getOverridePocConfigSummary } from '../override-poc.js';
|
|
@@ -15,6 +17,9 @@ import { mapNextOverrideAssetsWithDrift } from '../next-asset-mapper.js';
|
|
|
15
17
|
import { planNextSourceOverride } from '../next-source-override-planner.js';
|
|
16
18
|
import { listObservedOverrideAssets, persistObservedOverrideAssets } from '../override-observed-assets.js';
|
|
17
19
|
import { planOverrideResponsePatch } from '../override-response-planner.js';
|
|
20
|
+
import { applySsrMockConfig, discoverSsrMockability, removeSsrMockConfig } from '../ssr-mock.js';
|
|
21
|
+
import { getLighthouseReport, getLighthouseReportAsset, listLighthouseReports, normalizeLighthouseAsset, planLighthouseFixes, runLighthouseReport, } from '../lighthouse-report.js';
|
|
22
|
+
import { createToolLoopGuard } from './tool-loop-guard.js';
|
|
18
23
|
function createDefaultMcpLogger() {
|
|
19
24
|
const write = (level, message, payload) => {
|
|
20
25
|
process.stderr.write(`${message} ${JSON.stringify({ level, ...payload })}\n`);
|
|
@@ -31,12 +36,93 @@ function createDefaultMcpLogger() {
|
|
|
31
36
|
},
|
|
32
37
|
};
|
|
33
38
|
}
|
|
39
|
+
const UIActionTargetScopeSchema = z.enum(['buttons', 'links', 'inputs', 'modals', 'focused']);
|
|
40
|
+
const UIActionLocatorMatcherSchema = z.union([
|
|
41
|
+
z.string().min(1),
|
|
42
|
+
z.object({
|
|
43
|
+
pattern: z.string().min(1),
|
|
44
|
+
flags: z.string().regex(/^[imsu]*$/).optional(),
|
|
45
|
+
}),
|
|
46
|
+
]);
|
|
47
|
+
const UIActionLocatorStepSchema = z.object({
|
|
48
|
+
kind: z.enum(['css', 'role', 'text', 'label', 'testId', 'placeholder', 'altText']),
|
|
49
|
+
value: UIActionLocatorMatcherSchema.optional(),
|
|
50
|
+
role: z.string().min(1).optional(),
|
|
51
|
+
name: UIActionLocatorMatcherSchema.optional(),
|
|
52
|
+
exact: z.boolean().optional(),
|
|
53
|
+
relation: z.enum(['filter', 'descendant', 'ancestor']).optional(),
|
|
54
|
+
}).superRefine((value, ctx) => {
|
|
55
|
+
if (value.kind === 'role' && !value.role && !value.value) {
|
|
56
|
+
ctx.addIssue({
|
|
57
|
+
code: z.ZodIssueCode.custom,
|
|
58
|
+
message: 'role locator step requires role or value',
|
|
59
|
+
path: ['role'],
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
if (value.kind !== 'role' && !value.value) {
|
|
63
|
+
ctx.addIssue({
|
|
64
|
+
code: z.ZodIssueCode.custom,
|
|
65
|
+
message: `${value.kind} locator step requires value`,
|
|
66
|
+
path: ['value'],
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
const UIActionLocatorSchema = z.object({
|
|
71
|
+
scope: UIActionTargetScopeSchema.optional(),
|
|
72
|
+
frame: z.object({
|
|
73
|
+
selector: z.string().min(1).optional(),
|
|
74
|
+
urlContains: z.string().min(1).optional(),
|
|
75
|
+
titleContains: z.string().min(1).optional(),
|
|
76
|
+
}).optional(),
|
|
77
|
+
steps: z.array(UIActionLocatorStepSchema).min(1).max(8),
|
|
78
|
+
});
|
|
79
|
+
const UIActionCoordinateTargetSchema = z.object({
|
|
80
|
+
x: z.number().finite(),
|
|
81
|
+
y: z.number().finite(),
|
|
82
|
+
frameId: z.number().int().min(0).optional(),
|
|
83
|
+
});
|
|
34
84
|
const LiveUIActionTargetSchema = z.object({
|
|
35
85
|
selector: z.string().min(1).optional(),
|
|
36
86
|
elementRef: z.string().min(1).optional(),
|
|
87
|
+
coordinates: UIActionCoordinateTargetSchema.optional(),
|
|
37
88
|
tabId: z.number().int().min(0).optional(),
|
|
38
89
|
frameId: z.number().int().min(0).optional(),
|
|
39
90
|
url: z.string().url().optional(),
|
|
91
|
+
locator: UIActionLocatorSchema.optional(),
|
|
92
|
+
frameUrlContains: z.string().min(1).optional(),
|
|
93
|
+
frameTitleContains: z.string().min(1).optional(),
|
|
94
|
+
testId: z.string().min(1).optional(),
|
|
95
|
+
scope: UIActionTargetScopeSchema.optional(),
|
|
96
|
+
textContains: z.string().min(1).optional(),
|
|
97
|
+
labelContains: z.string().min(1).optional(),
|
|
98
|
+
titleContains: z.string().min(1).optional(),
|
|
99
|
+
role: z.string().min(1).optional(),
|
|
100
|
+
name: z.string().min(1).optional(),
|
|
101
|
+
placeholder: z.string().min(1).optional(),
|
|
102
|
+
altText: z.string().min(1).optional(),
|
|
103
|
+
tagName: z.string().min(1).optional(),
|
|
104
|
+
type: z.string().min(1).optional(),
|
|
105
|
+
exact: z.boolean().optional(),
|
|
106
|
+
nth: z.number().int().min(0).optional(),
|
|
107
|
+
first: z.boolean().optional(),
|
|
108
|
+
last: z.boolean().optional(),
|
|
109
|
+
strict: z.boolean().optional(),
|
|
110
|
+
visible: z.boolean().optional(),
|
|
111
|
+
disabled: z.boolean().optional(),
|
|
112
|
+
selected: z.boolean().optional(),
|
|
113
|
+
pressed: z.boolean().optional(),
|
|
114
|
+
expanded: z.boolean().optional(),
|
|
115
|
+
readOnly: z.boolean().optional(),
|
|
116
|
+
requiredField: z.boolean().optional(),
|
|
117
|
+
}).superRefine((value, ctx) => {
|
|
118
|
+
const positionFields = [value.nth !== undefined, value.first === true, value.last === true].filter(Boolean).length;
|
|
119
|
+
if (positionFields > 1) {
|
|
120
|
+
ctx.addIssue({
|
|
121
|
+
code: z.ZodIssueCode.custom,
|
|
122
|
+
message: 'target can use only one of nth, first, or last',
|
|
123
|
+
path: ['target'],
|
|
124
|
+
});
|
|
125
|
+
}
|
|
40
126
|
});
|
|
41
127
|
const LiveUIActionBaseSchema = z.object({
|
|
42
128
|
traceId: z.string().min(1).optional(),
|
|
@@ -50,6 +136,10 @@ const LiveUIActionRequestSchema = z.discriminatedUnion('action', [
|
|
|
50
136
|
clickCount: z.number().int().min(1).max(3).optional(),
|
|
51
137
|
}).optional(),
|
|
52
138
|
}),
|
|
139
|
+
LiveUIActionBaseSchema.extend({
|
|
140
|
+
action: z.literal('hover'),
|
|
141
|
+
input: z.object({}).optional(),
|
|
142
|
+
}),
|
|
53
143
|
LiveUIActionBaseSchema.extend({
|
|
54
144
|
action: z.literal('input'),
|
|
55
145
|
input: z.object({
|
|
@@ -95,20 +185,34 @@ const LiveUIActionRequestSchema = z.discriminatedUnion('action', [
|
|
|
95
185
|
]);
|
|
96
186
|
const UIWorkflowModeSchema = z.enum(['safe', 'fast']);
|
|
97
187
|
const UIWorkflowFailureStrategySchema = z.enum(['stop', 'continue', 'retry_once']);
|
|
98
|
-
const UIWorkflowActionTargetScopeSchema =
|
|
188
|
+
const UIWorkflowActionTargetScopeSchema = UIActionTargetScopeSchema;
|
|
99
189
|
const UIWorkflowActionTargetSchema = z.object({
|
|
100
190
|
selector: z.string().min(1).optional(),
|
|
101
191
|
elementRef: z.string().min(1).optional(),
|
|
192
|
+
coordinates: UIActionCoordinateTargetSchema.optional(),
|
|
102
193
|
tabId: z.number().int().min(0).optional(),
|
|
103
194
|
frameId: z.number().int().min(0).optional(),
|
|
104
195
|
url: z.string().url().optional(),
|
|
196
|
+
locator: UIActionLocatorSchema.optional(),
|
|
197
|
+
frameUrlContains: z.string().min(1).optional(),
|
|
198
|
+
frameTitleContains: z.string().min(1).optional(),
|
|
105
199
|
testId: z.string().min(1).optional(),
|
|
106
200
|
scope: UIWorkflowActionTargetScopeSchema.optional(),
|
|
107
201
|
textContains: z.string().min(1).optional(),
|
|
108
202
|
labelContains: z.string().min(1).optional(),
|
|
109
203
|
titleContains: z.string().min(1).optional(),
|
|
204
|
+
role: z.string().min(1).optional(),
|
|
205
|
+
name: z.string().min(1).optional(),
|
|
206
|
+
placeholder: z.string().min(1).optional(),
|
|
207
|
+
altText: z.string().min(1).optional(),
|
|
110
208
|
tagName: z.string().min(1).optional(),
|
|
111
209
|
type: z.string().min(1).optional(),
|
|
210
|
+
exact: z.boolean().optional(),
|
|
211
|
+
nth: z.number().int().min(0).optional(),
|
|
212
|
+
first: z.boolean().optional(),
|
|
213
|
+
last: z.boolean().optional(),
|
|
214
|
+
strict: z.boolean().optional(),
|
|
215
|
+
visible: z.boolean().optional(),
|
|
112
216
|
disabled: z.boolean().optional(),
|
|
113
217
|
selected: z.boolean().optional(),
|
|
114
218
|
pressed: z.boolean().optional(),
|
|
@@ -118,13 +222,28 @@ const UIWorkflowActionTargetSchema = z.object({
|
|
|
118
222
|
}).superRefine((value, ctx) => {
|
|
119
223
|
if (!value.selector
|
|
120
224
|
&& !value.elementRef
|
|
225
|
+
&& !value.coordinates
|
|
226
|
+
&& !value.locator
|
|
227
|
+
&& !value.scope
|
|
121
228
|
&& !value.testId
|
|
122
229
|
&& !value.textContains
|
|
123
230
|
&& !value.labelContains
|
|
124
|
-
&& !value.titleContains
|
|
231
|
+
&& !value.titleContains
|
|
232
|
+
&& !value.role
|
|
233
|
+
&& !value.name
|
|
234
|
+
&& !value.placeholder
|
|
235
|
+
&& !value.altText) {
|
|
236
|
+
ctx.addIssue({
|
|
237
|
+
code: z.ZodIssueCode.custom,
|
|
238
|
+
message: 'target requires selector, elementRef, coordinates, locator, scope, testId, textContains, labelContains, titleContains, role, name, placeholder, or altText',
|
|
239
|
+
path: ['target'],
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
const positionFields = [value.nth !== undefined, value.first === true, value.last === true].filter(Boolean).length;
|
|
243
|
+
if (positionFields > 1) {
|
|
125
244
|
ctx.addIssue({
|
|
126
245
|
code: z.ZodIssueCode.custom,
|
|
127
|
-
message: 'target
|
|
246
|
+
message: 'target can use only one of nth, first, or last',
|
|
128
247
|
path: ['target'],
|
|
129
248
|
});
|
|
130
249
|
}
|
|
@@ -163,6 +282,10 @@ const UIWorkflowActionStepSchema = z.discriminatedUnion('action', [
|
|
|
163
282
|
clickCount: z.number().int().min(1).max(3).optional(),
|
|
164
283
|
}).optional(),
|
|
165
284
|
}),
|
|
285
|
+
UIWorkflowActionBaseSchema.extend({
|
|
286
|
+
action: z.literal('hover'),
|
|
287
|
+
input: z.object({}).optional(),
|
|
288
|
+
}),
|
|
166
289
|
UIWorkflowActionBaseSchema.extend({
|
|
167
290
|
action: z.literal('input'),
|
|
168
291
|
input: z.object({
|
|
@@ -207,14 +330,22 @@ const UIWorkflowActionStepSchema = z.discriminatedUnion('action', [
|
|
|
207
330
|
}),
|
|
208
331
|
]);
|
|
209
332
|
const UIWorkflowPageStateMatcherSchema = z.object({
|
|
210
|
-
scope: z.enum(['buttons', 'inputs', 'modals', 'focused', 'page']),
|
|
333
|
+
scope: z.enum(['buttons', 'links', 'inputs', 'modals', 'focused', 'page']),
|
|
211
334
|
selector: z.string().optional(),
|
|
212
335
|
testId: z.string().optional(),
|
|
213
336
|
textContains: z.string().optional(),
|
|
214
337
|
labelContains: z.string().optional(),
|
|
215
338
|
titleContains: z.string().optional(),
|
|
339
|
+
role: z.string().optional(),
|
|
340
|
+
name: z.string().optional(),
|
|
341
|
+
placeholder: z.string().optional(),
|
|
342
|
+
altText: z.string().optional(),
|
|
343
|
+
exact: z.boolean().optional(),
|
|
344
|
+
frameUrlContains: z.string().optional(),
|
|
345
|
+
frameTitleContains: z.string().optional(),
|
|
216
346
|
urlContains: z.string().optional(),
|
|
217
347
|
language: z.string().optional(),
|
|
348
|
+
visible: z.boolean().optional(),
|
|
218
349
|
disabled: z.boolean().optional(),
|
|
219
350
|
selected: z.boolean().optional(),
|
|
220
351
|
pressed: z.boolean().optional(),
|
|
@@ -247,10 +378,201 @@ const UIWorkflowAssertStepSchema = UIWorkflowStepBaseSchema.extend({
|
|
|
247
378
|
kind: z.literal('assert'),
|
|
248
379
|
matcher: UIWorkflowPageStateMatcherSchema,
|
|
249
380
|
});
|
|
381
|
+
const AutomationWaitBaseSchema = z.object({
|
|
382
|
+
timeoutMs: z.number().int().min(100).max(120000).optional(),
|
|
383
|
+
pollIntervalMs: z.number().int().min(50).max(5000).optional(),
|
|
384
|
+
});
|
|
385
|
+
const AutomationWaitUrlSchema = AutomationWaitBaseSchema.extend({
|
|
386
|
+
waitKind: z.literal('url'),
|
|
387
|
+
urlContains: z.string().min(1).optional(),
|
|
388
|
+
urlRegex: z.string().min(1).optional(),
|
|
389
|
+
exactUrl: z.string().min(1).optional(),
|
|
390
|
+
}).superRefine((value, ctx) => {
|
|
391
|
+
if (!value.urlContains && !value.urlRegex && !value.exactUrl) {
|
|
392
|
+
ctx.addIssue({
|
|
393
|
+
code: z.ZodIssueCode.custom,
|
|
394
|
+
message: 'url wait requires urlContains, urlRegex, or exactUrl',
|
|
395
|
+
path: ['wait'],
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
});
|
|
399
|
+
const AutomationWaitNavigationSchema = AutomationWaitBaseSchema.extend({
|
|
400
|
+
waitKind: z.literal('navigation'),
|
|
401
|
+
urlContains: z.string().min(1).optional(),
|
|
402
|
+
urlRegex: z.string().min(1).optional(),
|
|
403
|
+
exactUrl: z.string().min(1).optional(),
|
|
404
|
+
fromUrlContains: z.string().min(1).optional(),
|
|
405
|
+
fromUrlRegex: z.string().min(1).optional(),
|
|
406
|
+
trigger: z.string().min(1).optional(),
|
|
407
|
+
sinceTs: z.number().int().min(0).optional(),
|
|
408
|
+
tabId: z.number().int().min(0).optional(),
|
|
409
|
+
}).superRefine((value, ctx) => {
|
|
410
|
+
if (!value.urlContains
|
|
411
|
+
&& !value.urlRegex
|
|
412
|
+
&& !value.exactUrl
|
|
413
|
+
&& !value.fromUrlContains
|
|
414
|
+
&& !value.fromUrlRegex
|
|
415
|
+
&& !value.trigger) {
|
|
416
|
+
ctx.addIssue({
|
|
417
|
+
code: z.ZodIssueCode.custom,
|
|
418
|
+
message: 'navigation wait requires a URL, from-URL, or trigger predicate',
|
|
419
|
+
path: ['wait'],
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
const AutomationWaitNavigationLifecycleSchema = AutomationWaitBaseSchema.extend({
|
|
424
|
+
waitKind: z.literal('navigation_lifecycle'),
|
|
425
|
+
state: z.enum(['commit', 'same_document', 'domcontentloaded', 'load', 'network_idle']).default('load'),
|
|
426
|
+
urlContains: z.string().min(1).optional(),
|
|
427
|
+
urlRegex: z.string().min(1).optional(),
|
|
428
|
+
exactUrl: z.string().min(1).optional(),
|
|
429
|
+
tabId: z.number().int().min(0).optional(),
|
|
430
|
+
});
|
|
431
|
+
const AutomationWaitLoadStateSchema = AutomationWaitBaseSchema.extend({
|
|
432
|
+
waitKind: z.literal('load_state'),
|
|
433
|
+
state: z.enum(['domcontentloaded', 'load']).default('load'),
|
|
434
|
+
urlContains: z.string().min(1).optional(),
|
|
435
|
+
urlRegex: z.string().min(1).optional(),
|
|
436
|
+
exactUrl: z.string().min(1).optional(),
|
|
437
|
+
});
|
|
438
|
+
const AutomationWaitSelectorStateSchema = AutomationWaitBaseSchema.extend({
|
|
439
|
+
waitKind: z.literal('selector_state'),
|
|
440
|
+
selector: z.string().min(1),
|
|
441
|
+
state: z.enum(['attached', 'detached', 'visible', 'hidden']).default('visible'),
|
|
442
|
+
frameId: z.number().int().min(0).default(0),
|
|
443
|
+
});
|
|
444
|
+
const AutomationWaitConsoleSchema = AutomationWaitBaseSchema.extend({
|
|
445
|
+
waitKind: z.literal('console'),
|
|
446
|
+
levels: z.array(z.string().min(1)).optional(),
|
|
447
|
+
contains: z.string().min(1).optional(),
|
|
448
|
+
sinceTs: z.number().int().min(0).optional(),
|
|
449
|
+
includeRuntimeErrors: z.boolean().optional(),
|
|
450
|
+
});
|
|
451
|
+
const AutomationWaitDialogSchema = AutomationWaitBaseSchema.extend({
|
|
452
|
+
waitKind: z.literal('dialog'),
|
|
453
|
+
type: z.enum(['alert', 'confirm', 'prompt', 'beforeunload']).optional(),
|
|
454
|
+
messageContains: z.string().min(1).optional(),
|
|
455
|
+
urlContains: z.string().min(1).optional(),
|
|
456
|
+
action: z.enum(['none', 'accept', 'dismiss']).default('none'),
|
|
457
|
+
promptText: z.string().optional(),
|
|
458
|
+
tabId: z.number().int().min(0).optional(),
|
|
459
|
+
});
|
|
460
|
+
const AutomationWaitStableLayoutSchema = AutomationWaitBaseSchema.extend({
|
|
461
|
+
waitKind: z.literal('stable_layout'),
|
|
462
|
+
selector: z.string().min(1).optional(),
|
|
463
|
+
stableMs: z.number().int().min(100).max(10000).default(500),
|
|
464
|
+
tabId: z.number().int().min(0).optional(),
|
|
465
|
+
});
|
|
466
|
+
const AutomationWaitDownloadSchema = AutomationWaitBaseSchema.extend({
|
|
467
|
+
waitKind: z.literal('download'),
|
|
468
|
+
urlContains: z.string().min(1).optional(),
|
|
469
|
+
urlRegex: z.string().min(1).optional(),
|
|
470
|
+
exactUrl: z.string().min(1).optional(),
|
|
471
|
+
filenameContains: z.string().min(1).optional(),
|
|
472
|
+
filenameRegex: z.string().min(1).optional(),
|
|
473
|
+
state: z.enum(['started', 'completed']).default('started'),
|
|
474
|
+
tabId: z.number().int().min(0).optional(),
|
|
475
|
+
}).superRefine((value, ctx) => {
|
|
476
|
+
if (!value.urlContains && !value.urlRegex && !value.exactUrl && !value.filenameContains && !value.filenameRegex) {
|
|
477
|
+
ctx.addIssue({
|
|
478
|
+
code: z.ZodIssueCode.custom,
|
|
479
|
+
message: 'download wait requires a URL or filename predicate',
|
|
480
|
+
path: ['wait'],
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
});
|
|
484
|
+
const AutomationWaitPopupSchema = AutomationWaitBaseSchema.extend({
|
|
485
|
+
waitKind: z.literal('popup'),
|
|
486
|
+
urlContains: z.string().min(1).optional(),
|
|
487
|
+
urlRegex: z.string().min(1).optional(),
|
|
488
|
+
exactUrl: z.string().min(1).optional(),
|
|
489
|
+
openerTabId: z.number().int().min(0).optional(),
|
|
490
|
+
}).superRefine((value, ctx) => {
|
|
491
|
+
if (!value.urlContains && !value.urlRegex && !value.exactUrl && value.openerTabId === undefined) {
|
|
492
|
+
ctx.addIssue({
|
|
493
|
+
code: z.ZodIssueCode.custom,
|
|
494
|
+
message: 'popup wait requires a URL predicate or openerTabId',
|
|
495
|
+
path: ['wait'],
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
});
|
|
499
|
+
const AutomationWaitNetworkQuietSchema = AutomationWaitBaseSchema.extend({
|
|
500
|
+
waitKind: z.literal('network_quiet'),
|
|
501
|
+
quietMs: z.number().int().min(100).max(10000).default(500),
|
|
502
|
+
urlContains: z.string().min(1).optional(),
|
|
503
|
+
method: z.string().min(1).optional(),
|
|
504
|
+
tabId: z.number().int().min(0).optional(),
|
|
505
|
+
});
|
|
506
|
+
const AutomationWaitNetworkBaseSchema = AutomationWaitBaseSchema.extend({
|
|
507
|
+
urlContains: z.string().min(1).optional(),
|
|
508
|
+
urlRegex: z.string().min(1).optional(),
|
|
509
|
+
exactUrl: z.string().min(1).optional(),
|
|
510
|
+
method: z.string().min(1).optional(),
|
|
511
|
+
traceId: z.string().min(1).optional(),
|
|
512
|
+
initiator: z.enum(['fetch', 'xhr', 'img', 'script', 'other']).optional(),
|
|
513
|
+
requestContentType: z.string().min(1).optional(),
|
|
514
|
+
sinceTs: z.number().int().min(0).optional(),
|
|
515
|
+
tabId: z.number().int().min(0).optional(),
|
|
516
|
+
includeBodies: z.boolean().optional(),
|
|
517
|
+
});
|
|
518
|
+
const AutomationWaitRequestSchema = AutomationWaitNetworkBaseSchema.extend({
|
|
519
|
+
waitKind: z.literal('request'),
|
|
520
|
+
}).superRefine((value, ctx) => {
|
|
521
|
+
if (!value.urlContains && !value.urlRegex && !value.exactUrl && !value.traceId) {
|
|
522
|
+
ctx.addIssue({
|
|
523
|
+
code: z.ZodIssueCode.custom,
|
|
524
|
+
message: 'request wait requires urlContains, urlRegex, exactUrl, or traceId',
|
|
525
|
+
path: ['wait'],
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
});
|
|
529
|
+
const AutomationWaitResponseSchema = AutomationWaitNetworkBaseSchema.extend({
|
|
530
|
+
waitKind: z.literal('response'),
|
|
531
|
+
statusIn: z.array(z.number().int().min(100).max(599)).optional(),
|
|
532
|
+
statusGte: z.number().int().min(100).max(599).optional(),
|
|
533
|
+
statusLt: z.number().int().min(100).max(600).optional(),
|
|
534
|
+
responseContentType: z.string().min(1).optional(),
|
|
535
|
+
errorType: z.string().min(1).optional(),
|
|
536
|
+
}).superRefine((value, ctx) => {
|
|
537
|
+
if (!value.urlContains && !value.urlRegex && !value.exactUrl && !value.traceId) {
|
|
538
|
+
ctx.addIssue({
|
|
539
|
+
code: z.ZodIssueCode.custom,
|
|
540
|
+
message: 'response wait requires urlContains, urlRegex, exactUrl, or traceId',
|
|
541
|
+
path: ['wait'],
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
if (value.statusGte !== undefined && value.statusLt !== undefined && value.statusGte >= value.statusLt) {
|
|
545
|
+
ctx.addIssue({
|
|
546
|
+
code: z.ZodIssueCode.custom,
|
|
547
|
+
message: 'statusGte must be less than statusLt',
|
|
548
|
+
path: ['statusGte'],
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
});
|
|
552
|
+
const AutomationWaitSpecSchema = z.discriminatedUnion('waitKind', [
|
|
553
|
+
AutomationWaitUrlSchema,
|
|
554
|
+
AutomationWaitNavigationSchema,
|
|
555
|
+
AutomationWaitNavigationLifecycleSchema,
|
|
556
|
+
AutomationWaitLoadStateSchema,
|
|
557
|
+
AutomationWaitSelectorStateSchema,
|
|
558
|
+
AutomationWaitConsoleSchema,
|
|
559
|
+
AutomationWaitDialogSchema,
|
|
560
|
+
AutomationWaitStableLayoutSchema,
|
|
561
|
+
AutomationWaitDownloadSchema,
|
|
562
|
+
AutomationWaitPopupSchema,
|
|
563
|
+
AutomationWaitNetworkQuietSchema,
|
|
564
|
+
AutomationWaitRequestSchema,
|
|
565
|
+
AutomationWaitResponseSchema,
|
|
566
|
+
]);
|
|
567
|
+
const UIWorkflowGenericWaitStepSchema = UIWorkflowStepBaseSchema.extend({
|
|
568
|
+
kind: z.literal('wait'),
|
|
569
|
+
wait: AutomationWaitSpecSchema,
|
|
570
|
+
});
|
|
250
571
|
const UIWorkflowStepSchema = z.discriminatedUnion('kind', [
|
|
251
572
|
UIWorkflowActionStepSchema,
|
|
252
573
|
UIWorkflowWaitForStepSchema,
|
|
253
574
|
UIWorkflowAssertStepSchema,
|
|
575
|
+
UIWorkflowGenericWaitStepSchema,
|
|
254
576
|
]);
|
|
255
577
|
const RunUIStepsSchema = z.object({
|
|
256
578
|
sessionId: z.string().min(1),
|
|
@@ -263,6 +585,91 @@ const RunUIStepsSchema = z.object({
|
|
|
263
585
|
function createUIWorkflowTraceId() {
|
|
264
586
|
return `uiworkflow-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
265
587
|
}
|
|
588
|
+
const LOCATOR_MATCHER_TOOL_SCHEMA = {
|
|
589
|
+
anyOf: [
|
|
590
|
+
{ type: 'string' },
|
|
591
|
+
{
|
|
592
|
+
type: 'object',
|
|
593
|
+
required: ['pattern'],
|
|
594
|
+
properties: {
|
|
595
|
+
pattern: { type: 'string' },
|
|
596
|
+
flags: { type: 'string' },
|
|
597
|
+
},
|
|
598
|
+
},
|
|
599
|
+
],
|
|
600
|
+
};
|
|
601
|
+
const ACTION_LOCATOR_TOOL_SCHEMA = {
|
|
602
|
+
type: 'object',
|
|
603
|
+
required: ['steps'],
|
|
604
|
+
properties: {
|
|
605
|
+
scope: { type: 'string', enum: ['buttons', 'links', 'inputs', 'modals', 'focused'] },
|
|
606
|
+
frame: {
|
|
607
|
+
type: 'object',
|
|
608
|
+
properties: {
|
|
609
|
+
selector: { type: 'string' },
|
|
610
|
+
urlContains: { type: 'string' },
|
|
611
|
+
titleContains: { type: 'string' },
|
|
612
|
+
},
|
|
613
|
+
},
|
|
614
|
+
steps: {
|
|
615
|
+
type: 'array',
|
|
616
|
+
minItems: 1,
|
|
617
|
+
maxItems: 8,
|
|
618
|
+
items: {
|
|
619
|
+
type: 'object',
|
|
620
|
+
required: ['kind'],
|
|
621
|
+
properties: {
|
|
622
|
+
kind: { type: 'string', enum: ['css', 'role', 'text', 'label', 'testId', 'placeholder', 'altText'] },
|
|
623
|
+
value: LOCATOR_MATCHER_TOOL_SCHEMA,
|
|
624
|
+
role: { type: 'string' },
|
|
625
|
+
name: LOCATOR_MATCHER_TOOL_SCHEMA,
|
|
626
|
+
exact: { type: 'boolean' },
|
|
627
|
+
relation: { type: 'string', enum: ['filter', 'descendant', 'ancestor'] },
|
|
628
|
+
},
|
|
629
|
+
},
|
|
630
|
+
},
|
|
631
|
+
},
|
|
632
|
+
};
|
|
633
|
+
const AUTOMATION_WAIT_TOOL_SCHEMA = {
|
|
634
|
+
type: 'object',
|
|
635
|
+
required: ['waitKind'],
|
|
636
|
+
properties: {
|
|
637
|
+
waitKind: { type: 'string', enum: ['url', 'navigation', 'navigation_lifecycle', 'load_state', 'selector_state', 'console', 'dialog', 'stable_layout', 'download', 'popup', 'network_quiet', 'request', 'response'] },
|
|
638
|
+
timeoutMs: { type: 'number' },
|
|
639
|
+
pollIntervalMs: { type: 'number' },
|
|
640
|
+
urlContains: { type: 'string' },
|
|
641
|
+
urlRegex: { type: 'string' },
|
|
642
|
+
exactUrl: { type: 'string' },
|
|
643
|
+
fromUrlContains: { type: 'string' },
|
|
644
|
+
fromUrlRegex: { type: 'string' },
|
|
645
|
+
trigger: { type: 'string' },
|
|
646
|
+
state: { type: 'string', enum: ['commit', 'same_document', 'domcontentloaded', 'load', 'network_idle', 'attached', 'detached', 'visible', 'hidden', 'started', 'completed'] },
|
|
647
|
+
selector: { type: 'string' },
|
|
648
|
+
frameId: { type: 'number' },
|
|
649
|
+
levels: { type: 'array', items: { type: 'string' } },
|
|
650
|
+
contains: { type: 'string' },
|
|
651
|
+
sinceTs: { type: 'number' },
|
|
652
|
+
includeRuntimeErrors: { type: 'boolean' },
|
|
653
|
+
action: { type: 'string', enum: ['none', 'accept', 'dismiss'] },
|
|
654
|
+
promptText: { type: 'string' },
|
|
655
|
+
stableMs: { type: 'number' },
|
|
656
|
+
filenameContains: { type: 'string' },
|
|
657
|
+
filenameRegex: { type: 'string' },
|
|
658
|
+
openerTabId: { type: 'number' },
|
|
659
|
+
quietMs: { type: 'number' },
|
|
660
|
+
method: { type: 'string' },
|
|
661
|
+
traceId: { type: 'string' },
|
|
662
|
+
initiator: { type: 'string', enum: ['fetch', 'xhr', 'img', 'script', 'other'] },
|
|
663
|
+
requestContentType: { type: 'string' },
|
|
664
|
+
responseContentType: { type: 'string' },
|
|
665
|
+
statusIn: { type: 'array', items: { type: 'number' } },
|
|
666
|
+
statusGte: { type: 'number' },
|
|
667
|
+
statusLt: { type: 'number' },
|
|
668
|
+
errorType: { type: 'string' },
|
|
669
|
+
includeBodies: { type: 'boolean' },
|
|
670
|
+
tabId: { type: 'number' },
|
|
671
|
+
},
|
|
672
|
+
};
|
|
266
673
|
const TOOL_SCHEMAS = {
|
|
267
674
|
list_sessions: {
|
|
268
675
|
type: 'object',
|
|
@@ -451,6 +858,7 @@ const TOOL_SCHEMAS = {
|
|
|
451
858
|
properties: {
|
|
452
859
|
sessionId: { type: 'string' },
|
|
453
860
|
selector: { type: 'string' },
|
|
861
|
+
frameId: { type: 'number' },
|
|
454
862
|
properties: { type: 'array', items: { type: 'string' } },
|
|
455
863
|
},
|
|
456
864
|
},
|
|
@@ -460,6 +868,7 @@ const TOOL_SCHEMAS = {
|
|
|
460
868
|
properties: {
|
|
461
869
|
sessionId: { type: 'string' },
|
|
462
870
|
selector: { type: 'string' },
|
|
871
|
+
frameId: { type: 'number' },
|
|
463
872
|
},
|
|
464
873
|
},
|
|
465
874
|
get_page_state: {
|
|
@@ -470,6 +879,7 @@ const TOOL_SCHEMAS = {
|
|
|
470
879
|
maxItems: { type: 'number' },
|
|
471
880
|
maxTextLength: { type: 'number' },
|
|
472
881
|
includeButtons: { type: 'boolean' },
|
|
882
|
+
includeLinks: { type: 'boolean' },
|
|
473
883
|
includeInputs: { type: 'boolean' },
|
|
474
884
|
includeModals: { type: 'boolean' },
|
|
475
885
|
},
|
|
@@ -481,7 +891,7 @@ const TOOL_SCHEMAS = {
|
|
|
481
891
|
sessionId: { type: 'string' },
|
|
482
892
|
kinds: {
|
|
483
893
|
type: 'array',
|
|
484
|
-
items: { type: 'string', enum: ['buttons', 'inputs', 'modals', 'focused'] },
|
|
894
|
+
items: { type: 'string', enum: ['buttons', 'links', 'inputs', 'modals', 'focused'] },
|
|
485
895
|
},
|
|
486
896
|
maxItems: { type: 'number' },
|
|
487
897
|
maxTextLength: { type: 'number' },
|
|
@@ -501,14 +911,22 @@ const TOOL_SCHEMAS = {
|
|
|
501
911
|
required: ['sessionId', 'scope'],
|
|
502
912
|
properties: {
|
|
503
913
|
sessionId: { type: 'string' },
|
|
504
|
-
scope: { type: 'string', enum: ['buttons', 'inputs', 'modals', 'focused', 'page'] },
|
|
914
|
+
scope: { type: 'string', enum: ['buttons', 'links', 'inputs', 'modals', 'focused', 'page'] },
|
|
505
915
|
selector: { type: 'string' },
|
|
506
916
|
testId: { type: 'string' },
|
|
507
917
|
textContains: { type: 'string' },
|
|
508
918
|
labelContains: { type: 'string' },
|
|
509
919
|
titleContains: { type: 'string' },
|
|
920
|
+
role: { type: 'string' },
|
|
921
|
+
name: { type: 'string' },
|
|
922
|
+
placeholder: { type: 'string' },
|
|
923
|
+
altText: { type: 'string' },
|
|
924
|
+
exact: { type: 'boolean' },
|
|
925
|
+
frameUrlContains: { type: 'string' },
|
|
926
|
+
frameTitleContains: { type: 'string' },
|
|
510
927
|
urlContains: { type: 'string' },
|
|
511
928
|
language: { type: 'string' },
|
|
929
|
+
visible: { type: 'boolean' },
|
|
512
930
|
disabled: { type: 'boolean' },
|
|
513
931
|
selected: { type: 'boolean' },
|
|
514
932
|
pressed: { type: 'boolean' },
|
|
@@ -528,14 +946,22 @@ const TOOL_SCHEMAS = {
|
|
|
528
946
|
required: ['sessionId', 'scope'],
|
|
529
947
|
properties: {
|
|
530
948
|
sessionId: { type: 'string' },
|
|
531
|
-
scope: { type: 'string', enum: ['buttons', 'inputs', 'modals', 'focused', 'page'] },
|
|
949
|
+
scope: { type: 'string', enum: ['buttons', 'links', 'inputs', 'modals', 'focused', 'page'] },
|
|
532
950
|
selector: { type: 'string' },
|
|
533
951
|
testId: { type: 'string' },
|
|
534
952
|
textContains: { type: 'string' },
|
|
535
953
|
labelContains: { type: 'string' },
|
|
536
954
|
titleContains: { type: 'string' },
|
|
955
|
+
role: { type: 'string' },
|
|
956
|
+
name: { type: 'string' },
|
|
957
|
+
placeholder: { type: 'string' },
|
|
958
|
+
altText: { type: 'string' },
|
|
959
|
+
exact: { type: 'boolean' },
|
|
960
|
+
frameUrlContains: { type: 'string' },
|
|
961
|
+
frameTitleContains: { type: 'string' },
|
|
537
962
|
urlContains: { type: 'string' },
|
|
538
963
|
language: { type: 'string' },
|
|
964
|
+
visible: { type: 'boolean' },
|
|
539
965
|
disabled: { type: 'boolean' },
|
|
540
966
|
selected: { type: 'boolean' },
|
|
541
967
|
pressed: { type: 'boolean' },
|
|
@@ -552,140 +978,352 @@ const TOOL_SCHEMAS = {
|
|
|
552
978
|
pollIntervalMs: { type: 'number' },
|
|
553
979
|
},
|
|
554
980
|
},
|
|
555
|
-
|
|
981
|
+
preflight_automation_flow: {
|
|
556
982
|
type: 'object',
|
|
557
983
|
required: ['sessionId'],
|
|
558
984
|
properties: {
|
|
559
985
|
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' },
|
|
986
|
+
expectedUrlContains: { type: 'string' },
|
|
987
|
+
requireSensitiveAutomation: { type: 'boolean' },
|
|
988
|
+
plannedActions: { type: 'array', items: { type: 'string' } },
|
|
989
|
+
includePageState: { type: 'boolean' },
|
|
990
|
+
maxItems: { type: 'number' },
|
|
991
|
+
maxTextLength: { type: 'number' },
|
|
570
992
|
},
|
|
571
993
|
},
|
|
572
|
-
|
|
994
|
+
wait_for_url: {
|
|
573
995
|
type: 'object',
|
|
574
996
|
required: ['sessionId'],
|
|
575
997
|
properties: {
|
|
576
998
|
sessionId: { type: 'string' },
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
999
|
+
urlContains: { type: 'string' },
|
|
1000
|
+
urlRegex: { type: 'string' },
|
|
1001
|
+
exactUrl: { type: 'string' },
|
|
1002
|
+
timeoutMs: { type: 'number' },
|
|
1003
|
+
pollIntervalMs: { type: 'number' },
|
|
1004
|
+
},
|
|
1005
|
+
},
|
|
1006
|
+
wait_for_navigation: {
|
|
1007
|
+
type: 'object',
|
|
1008
|
+
required: ['sessionId'],
|
|
1009
|
+
properties: {
|
|
1010
|
+
sessionId: { type: 'string' },
|
|
1011
|
+
urlContains: { type: 'string' },
|
|
1012
|
+
urlRegex: { type: 'string' },
|
|
1013
|
+
exactUrl: { type: 'string' },
|
|
1014
|
+
fromUrlContains: { type: 'string' },
|
|
1015
|
+
fromUrlRegex: { type: 'string' },
|
|
1016
|
+
trigger: { type: 'string' },
|
|
581
1017
|
sinceTs: { type: 'number' },
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
responseProfile: { type: 'string' },
|
|
586
|
-
includeArgs: { type: 'boolean' },
|
|
587
|
-
maxResponseBytes: { type: 'number' },
|
|
1018
|
+
tabId: { type: 'number' },
|
|
1019
|
+
timeoutMs: { type: 'number' },
|
|
1020
|
+
pollIntervalMs: { type: 'number' },
|
|
588
1021
|
},
|
|
589
1022
|
},
|
|
590
|
-
|
|
1023
|
+
wait_for_navigation_lifecycle: {
|
|
591
1024
|
type: 'object',
|
|
592
|
-
|
|
1025
|
+
required: ['sessionId'],
|
|
1026
|
+
properties: {
|
|
1027
|
+
sessionId: { type: 'string' },
|
|
1028
|
+
state: { type: 'string', enum: ['commit', 'same_document', 'domcontentloaded', 'load', 'network_idle'] },
|
|
1029
|
+
urlContains: { type: 'string' },
|
|
1030
|
+
urlRegex: { type: 'string' },
|
|
1031
|
+
exactUrl: { type: 'string' },
|
|
1032
|
+
tabId: { type: 'number' },
|
|
1033
|
+
timeoutMs: { type: 'number' },
|
|
1034
|
+
pollIntervalMs: { type: 'number' },
|
|
1035
|
+
},
|
|
593
1036
|
},
|
|
594
|
-
|
|
1037
|
+
wait_for_load_state: {
|
|
595
1038
|
type: 'object',
|
|
596
|
-
required: ['
|
|
1039
|
+
required: ['sessionId'],
|
|
597
1040
|
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' },
|
|
1041
|
+
sessionId: { type: 'string' },
|
|
1042
|
+
state: { type: 'string', enum: ['domcontentloaded', 'load'] },
|
|
1043
|
+
urlContains: { type: 'string' },
|
|
1044
|
+
urlRegex: { type: 'string' },
|
|
1045
|
+
exactUrl: { type: 'string' },
|
|
1046
|
+
timeoutMs: { type: 'number' },
|
|
1047
|
+
pollIntervalMs: { type: 'number' },
|
|
616
1048
|
},
|
|
617
1049
|
},
|
|
618
|
-
|
|
1050
|
+
wait_for_selector_state: {
|
|
619
1051
|
type: 'object',
|
|
1052
|
+
required: ['sessionId', 'selector'],
|
|
620
1053
|
properties: {
|
|
621
|
-
|
|
1054
|
+
sessionId: { type: 'string' },
|
|
1055
|
+
selector: { type: 'string' },
|
|
1056
|
+
state: { type: 'string', enum: ['attached', 'detached', 'visible', 'hidden'] },
|
|
1057
|
+
frameId: { type: 'number' },
|
|
1058
|
+
timeoutMs: { type: 'number' },
|
|
1059
|
+
pollIntervalMs: { type: 'number' },
|
|
622
1060
|
},
|
|
623
1061
|
},
|
|
624
|
-
|
|
1062
|
+
wait_for_console: {
|
|
625
1063
|
type: 'object',
|
|
626
1064
|
required: ['sessionId'],
|
|
627
1065
|
properties: {
|
|
628
1066
|
sessionId: { type: 'string' },
|
|
629
|
-
|
|
1067
|
+
levels: { type: 'array', items: { type: 'string' } },
|
|
1068
|
+
contains: { type: 'string' },
|
|
1069
|
+
sinceTs: { type: 'number' },
|
|
1070
|
+
includeRuntimeErrors: { type: 'boolean' },
|
|
1071
|
+
timeoutMs: { type: 'number' },
|
|
1072
|
+
pollIntervalMs: { type: 'number' },
|
|
630
1073
|
},
|
|
631
1074
|
},
|
|
632
|
-
|
|
1075
|
+
wait_for_dialog: {
|
|
633
1076
|
type: 'object',
|
|
634
1077
|
required: ['sessionId'],
|
|
635
1078
|
properties: {
|
|
636
1079
|
sessionId: { type: 'string' },
|
|
1080
|
+
type: { type: 'string', enum: ['alert', 'confirm', 'prompt', 'beforeunload'] },
|
|
1081
|
+
messageContains: { type: 'string' },
|
|
1082
|
+
urlContains: { type: 'string' },
|
|
1083
|
+
action: { type: 'string', enum: ['none', 'accept', 'dismiss'] },
|
|
1084
|
+
promptText: { type: 'string' },
|
|
637
1085
|
tabId: { type: 'number' },
|
|
638
|
-
|
|
1086
|
+
timeoutMs: { type: 'number' },
|
|
1087
|
+
pollIntervalMs: { type: 'number' },
|
|
639
1088
|
},
|
|
640
1089
|
},
|
|
641
|
-
|
|
1090
|
+
wait_for_stable_layout: {
|
|
642
1091
|
type: 'object',
|
|
643
1092
|
required: ['sessionId'],
|
|
644
1093
|
properties: {
|
|
645
1094
|
sessionId: { type: 'string' },
|
|
1095
|
+
selector: { type: 'string' },
|
|
1096
|
+
stableMs: { type: 'number' },
|
|
646
1097
|
tabId: { type: 'number' },
|
|
647
|
-
targetUrl: { type: 'string' },
|
|
648
|
-
targetAssetUrl: { type: 'string' },
|
|
649
|
-
captureMode: { type: 'string', enum: ['extension-fetch', 'cdp-response'] },
|
|
650
|
-
triggerReload: { type: 'boolean' },
|
|
651
|
-
matchMode: { type: 'string', enum: ['exact', 'prefix'] },
|
|
652
|
-
ruleType: { type: 'string' },
|
|
653
|
-
requestMethod: { type: 'string' },
|
|
654
|
-
requestHeaders: { type: 'object' },
|
|
655
1098
|
timeoutMs: { type: 'number' },
|
|
656
|
-
|
|
657
|
-
includeBody: { type: 'boolean' },
|
|
1099
|
+
pollIntervalMs: { type: 'number' },
|
|
658
1100
|
},
|
|
659
1101
|
},
|
|
660
|
-
|
|
1102
|
+
wait_for_download: {
|
|
661
1103
|
type: 'object',
|
|
662
1104
|
required: ['sessionId'],
|
|
663
1105
|
properties: {
|
|
664
1106
|
sessionId: { type: 'string' },
|
|
665
|
-
|
|
666
|
-
|
|
1107
|
+
urlContains: { type: 'string' },
|
|
1108
|
+
urlRegex: { type: 'string' },
|
|
1109
|
+
exactUrl: { type: 'string' },
|
|
1110
|
+
filenameContains: { type: 'string' },
|
|
1111
|
+
filenameRegex: { type: 'string' },
|
|
1112
|
+
state: { type: 'string', enum: ['started', 'completed'] },
|
|
1113
|
+
tabId: { type: 'number' },
|
|
1114
|
+
timeoutMs: { type: 'number' },
|
|
1115
|
+
pollIntervalMs: { type: 'number' },
|
|
667
1116
|
},
|
|
668
1117
|
},
|
|
669
|
-
|
|
1118
|
+
wait_for_popup: {
|
|
670
1119
|
type: 'object',
|
|
671
|
-
required: ['
|
|
1120
|
+
required: ['sessionId'],
|
|
672
1121
|
properties: {
|
|
673
1122
|
sessionId: { type: 'string' },
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
maxResults: { type: 'number' },
|
|
681
|
-
fetchProductionAssets: { type: 'boolean' },
|
|
682
|
-
productionFetchTimeoutMs: { type: 'number' },
|
|
683
|
-
maxProductionAssetBytes: { type: 'number' },
|
|
684
|
-
maxDriftCandidates: { type: 'number' },
|
|
685
|
-
productionFetchConcurrency: { type: 'number' },
|
|
1123
|
+
urlContains: { type: 'string' },
|
|
1124
|
+
urlRegex: { type: 'string' },
|
|
1125
|
+
exactUrl: { type: 'string' },
|
|
1126
|
+
openerTabId: { type: 'number' },
|
|
1127
|
+
timeoutMs: { type: 'number' },
|
|
1128
|
+
pollIntervalMs: { type: 'number' },
|
|
686
1129
|
},
|
|
687
1130
|
},
|
|
688
|
-
|
|
1131
|
+
wait_for_request: {
|
|
1132
|
+
type: 'object',
|
|
1133
|
+
required: ['sessionId'],
|
|
1134
|
+
properties: {
|
|
1135
|
+
sessionId: { type: 'string' },
|
|
1136
|
+
urlContains: { type: 'string' },
|
|
1137
|
+
urlRegex: { type: 'string' },
|
|
1138
|
+
exactUrl: { type: 'string' },
|
|
1139
|
+
method: { type: 'string' },
|
|
1140
|
+
traceId: { type: 'string' },
|
|
1141
|
+
initiator: { type: 'string', enum: ['fetch', 'xhr', 'img', 'script', 'other'] },
|
|
1142
|
+
requestContentType: { type: 'string' },
|
|
1143
|
+
sinceTs: { type: 'number' },
|
|
1144
|
+
tabId: { type: 'number' },
|
|
1145
|
+
includeBodies: { type: 'boolean' },
|
|
1146
|
+
timeoutMs: { type: 'number' },
|
|
1147
|
+
pollIntervalMs: { type: 'number' },
|
|
1148
|
+
},
|
|
1149
|
+
},
|
|
1150
|
+
wait_for_response: {
|
|
1151
|
+
type: 'object',
|
|
1152
|
+
required: ['sessionId'],
|
|
1153
|
+
properties: {
|
|
1154
|
+
sessionId: { type: 'string' },
|
|
1155
|
+
urlContains: { type: 'string' },
|
|
1156
|
+
urlRegex: { type: 'string' },
|
|
1157
|
+
exactUrl: { type: 'string' },
|
|
1158
|
+
method: { type: 'string' },
|
|
1159
|
+
traceId: { type: 'string' },
|
|
1160
|
+
initiator: { type: 'string', enum: ['fetch', 'xhr', 'img', 'script', 'other'] },
|
|
1161
|
+
requestContentType: { type: 'string' },
|
|
1162
|
+
responseContentType: { type: 'string' },
|
|
1163
|
+
statusIn: { type: 'array', items: { type: 'number' } },
|
|
1164
|
+
statusGte: { type: 'number' },
|
|
1165
|
+
statusLt: { type: 'number' },
|
|
1166
|
+
errorType: { type: 'string' },
|
|
1167
|
+
sinceTs: { type: 'number' },
|
|
1168
|
+
tabId: { type: 'number' },
|
|
1169
|
+
includeBodies: { type: 'boolean' },
|
|
1170
|
+
timeoutMs: { type: 'number' },
|
|
1171
|
+
pollIntervalMs: { type: 'number' },
|
|
1172
|
+
},
|
|
1173
|
+
},
|
|
1174
|
+
wait_for_network_quiet: {
|
|
1175
|
+
type: 'object',
|
|
1176
|
+
required: ['sessionId'],
|
|
1177
|
+
properties: {
|
|
1178
|
+
sessionId: { type: 'string' },
|
|
1179
|
+
quietMs: { type: 'number' },
|
|
1180
|
+
urlContains: { type: 'string' },
|
|
1181
|
+
method: { type: 'string' },
|
|
1182
|
+
tabId: { type: 'number' },
|
|
1183
|
+
timeoutMs: { type: 'number' },
|
|
1184
|
+
pollIntervalMs: { type: 'number' },
|
|
1185
|
+
},
|
|
1186
|
+
},
|
|
1187
|
+
capture_ui_snapshot: {
|
|
1188
|
+
type: 'object',
|
|
1189
|
+
required: ['sessionId'],
|
|
1190
|
+
properties: {
|
|
1191
|
+
sessionId: { type: 'string' },
|
|
1192
|
+
selector: { type: 'string' },
|
|
1193
|
+
trigger: { type: 'string' },
|
|
1194
|
+
mode: { type: 'string' },
|
|
1195
|
+
styleMode: { type: 'string' },
|
|
1196
|
+
maxDepth: { type: 'number' },
|
|
1197
|
+
maxBytes: { type: 'number' },
|
|
1198
|
+
maxAncestors: { type: 'number' },
|
|
1199
|
+
includeDom: { type: 'boolean' },
|
|
1200
|
+
includeStyles: { type: 'boolean' },
|
|
1201
|
+
includePngDataUrl: { type: 'boolean' },
|
|
1202
|
+
},
|
|
1203
|
+
},
|
|
1204
|
+
get_live_console_logs: {
|
|
1205
|
+
type: 'object',
|
|
1206
|
+
required: ['sessionId'],
|
|
1207
|
+
properties: {
|
|
1208
|
+
sessionId: { type: 'string' },
|
|
1209
|
+
url: { type: 'string' },
|
|
1210
|
+
tabId: { type: 'number' },
|
|
1211
|
+
levels: { type: 'array', items: { type: 'string' } },
|
|
1212
|
+
contains: { type: 'string' },
|
|
1213
|
+
sinceTs: { type: 'number' },
|
|
1214
|
+
includeRuntimeErrors: { type: 'boolean' },
|
|
1215
|
+
dedupeWindowMs: { type: 'number' },
|
|
1216
|
+
limit: { type: 'number' },
|
|
1217
|
+
responseProfile: { type: 'string' },
|
|
1218
|
+
includeArgs: { type: 'boolean' },
|
|
1219
|
+
maxResponseBytes: { type: 'number' },
|
|
1220
|
+
},
|
|
1221
|
+
},
|
|
1222
|
+
list_override_profiles: {
|
|
1223
|
+
type: 'object',
|
|
1224
|
+
properties: {
|
|
1225
|
+
responseProfile: { type: 'string', enum: ['compact', 'full'] },
|
|
1226
|
+
},
|
|
1227
|
+
},
|
|
1228
|
+
create_override_profile: {
|
|
1229
|
+
type: 'object',
|
|
1230
|
+
required: ['targetBaseUrl'],
|
|
1231
|
+
properties: {
|
|
1232
|
+
adapter: { type: 'string' },
|
|
1233
|
+
mode: { type: 'string' },
|
|
1234
|
+
targetBaseUrl: { type: 'string' },
|
|
1235
|
+
projectRoot: { type: 'string' },
|
|
1236
|
+
assetRoot: { type: 'string' },
|
|
1237
|
+
nextDir: { type: 'string' },
|
|
1238
|
+
configPath: { type: 'string' },
|
|
1239
|
+
profileId: { type: 'string' },
|
|
1240
|
+
profileName: { type: 'string' },
|
|
1241
|
+
enabled: { type: 'boolean' },
|
|
1242
|
+
profileEnabled: { type: 'boolean' },
|
|
1243
|
+
autoReload: { type: 'boolean' },
|
|
1244
|
+
includeManifestFiles: { type: 'boolean' },
|
|
1245
|
+
includeStaticFiles: { type: 'boolean' },
|
|
1246
|
+
extensions: { type: 'array', items: { type: 'string' } },
|
|
1247
|
+
maxRules: { type: 'number' },
|
|
1248
|
+
writeConfig: { type: 'boolean' },
|
|
1249
|
+
overwrite: { type: 'boolean' },
|
|
1250
|
+
responseProfile: { type: 'string', enum: ['compact', 'full'] },
|
|
1251
|
+
includeConfigJson: { type: 'boolean' },
|
|
1252
|
+
},
|
|
1253
|
+
},
|
|
1254
|
+
validate_override_profile: {
|
|
1255
|
+
type: 'object',
|
|
1256
|
+
properties: {
|
|
1257
|
+
profileId: { type: 'string' },
|
|
1258
|
+
responseProfile: { type: 'string', enum: ['compact', 'full'] },
|
|
1259
|
+
},
|
|
1260
|
+
},
|
|
1261
|
+
preflight_overrides: {
|
|
1262
|
+
type: 'object',
|
|
1263
|
+
required: ['sessionId'],
|
|
1264
|
+
properties: {
|
|
1265
|
+
sessionId: { type: 'string' },
|
|
1266
|
+
profileId: { type: 'string' },
|
|
1267
|
+
},
|
|
1268
|
+
},
|
|
1269
|
+
observe_override_assets: {
|
|
1270
|
+
type: 'object',
|
|
1271
|
+
required: ['sessionId'],
|
|
1272
|
+
properties: {
|
|
1273
|
+
sessionId: { type: 'string' },
|
|
1274
|
+
tabId: { type: 'number' },
|
|
1275
|
+
includePerformance: { type: 'boolean' },
|
|
1276
|
+
},
|
|
1277
|
+
},
|
|
1278
|
+
capture_override_response_body: {
|
|
1279
|
+
type: 'object',
|
|
1280
|
+
required: ['sessionId'],
|
|
1281
|
+
properties: {
|
|
1282
|
+
sessionId: { type: 'string' },
|
|
1283
|
+
tabId: { type: 'number' },
|
|
1284
|
+
targetUrl: { type: 'string' },
|
|
1285
|
+
targetAssetUrl: { type: 'string' },
|
|
1286
|
+
captureMode: { type: 'string', enum: ['extension-fetch', 'cdp-response'] },
|
|
1287
|
+
triggerReload: { type: 'boolean' },
|
|
1288
|
+
matchMode: { type: 'string', enum: ['exact', 'prefix'] },
|
|
1289
|
+
ruleType: { type: 'string' },
|
|
1290
|
+
requestMethod: { type: 'string' },
|
|
1291
|
+
requestHeaders: { type: 'object' },
|
|
1292
|
+
timeoutMs: { type: 'number' },
|
|
1293
|
+
maxBodyBytes: { type: 'number' },
|
|
1294
|
+
includeBody: { type: 'boolean' },
|
|
1295
|
+
},
|
|
1296
|
+
},
|
|
1297
|
+
list_observed_override_assets: {
|
|
1298
|
+
type: 'object',
|
|
1299
|
+
required: ['sessionId'],
|
|
1300
|
+
properties: {
|
|
1301
|
+
sessionId: { type: 'string' },
|
|
1302
|
+
limit: { type: 'number' },
|
|
1303
|
+
sinceTimestamp: { type: 'number' },
|
|
1304
|
+
responseProfile: { type: 'string', enum: ['compact', 'full'] },
|
|
1305
|
+
},
|
|
1306
|
+
},
|
|
1307
|
+
map_next_override_assets: {
|
|
1308
|
+
type: 'object',
|
|
1309
|
+
required: ['projectRoot'],
|
|
1310
|
+
properties: {
|
|
1311
|
+
sessionId: { type: 'string' },
|
|
1312
|
+
tabId: { type: 'number' },
|
|
1313
|
+
projectRoot: { type: 'string' },
|
|
1314
|
+
nextDir: { type: 'string' },
|
|
1315
|
+
route: { type: 'string' },
|
|
1316
|
+
sourcePaths: { type: 'array', items: { type: 'string' } },
|
|
1317
|
+
observedAssets: { type: 'array', items: { type: 'object' } },
|
|
1318
|
+
maxResults: { type: 'number' },
|
|
1319
|
+
fetchProductionAssets: { type: 'boolean' },
|
|
1320
|
+
productionFetchTimeoutMs: { type: 'number' },
|
|
1321
|
+
maxProductionAssetBytes: { type: 'number' },
|
|
1322
|
+
maxDriftCandidates: { type: 'number' },
|
|
1323
|
+
productionFetchConcurrency: { type: 'number' },
|
|
1324
|
+
},
|
|
1325
|
+
},
|
|
1326
|
+
plan_override_response_patch: {
|
|
689
1327
|
type: 'object',
|
|
690
1328
|
properties: {
|
|
691
1329
|
sessionId: { type: 'string' },
|
|
@@ -805,96 +1443,326 @@ const TOOL_SCHEMAS = {
|
|
|
805
1443
|
runId: { type: 'string' },
|
|
806
1444
|
},
|
|
807
1445
|
},
|
|
808
|
-
|
|
1446
|
+
discover_ssr_mockability: {
|
|
809
1447
|
type: 'object',
|
|
810
|
-
required: ['
|
|
1448
|
+
required: ['projectRoot'],
|
|
811
1449
|
properties: {
|
|
812
|
-
|
|
813
|
-
|
|
1450
|
+
projectRoot: { type: 'string' },
|
|
1451
|
+
targetUrl: { type: 'string' },
|
|
1452
|
+
apiHost: { type: 'string' },
|
|
1453
|
+
maxFiles: { type: 'number' },
|
|
814
1454
|
},
|
|
815
1455
|
},
|
|
816
|
-
|
|
1456
|
+
apply_ssr_mock_config: {
|
|
817
1457
|
type: 'object',
|
|
818
|
-
required: ['
|
|
1458
|
+
required: ['projectRoot', 'envVarName', 'mockBaseUrl'],
|
|
819
1459
|
properties: {
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
1460
|
+
projectRoot: { type: 'string' },
|
|
1461
|
+
envVarName: { type: 'string' },
|
|
1462
|
+
mockBaseUrl: { type: 'string' },
|
|
1463
|
+
envFilePath: { type: 'string' },
|
|
1464
|
+
rollbackId: { type: 'string' },
|
|
823
1465
|
},
|
|
824
1466
|
},
|
|
825
|
-
|
|
1467
|
+
remove_ssr_mock_config: {
|
|
826
1468
|
type: 'object',
|
|
827
|
-
required: ['
|
|
1469
|
+
required: ['envFilePath', 'envVarName'],
|
|
828
1470
|
properties: {
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
1471
|
+
envFilePath: { type: 'string' },
|
|
1472
|
+
envVarName: { type: 'string' },
|
|
1473
|
+
rollbackId: { type: 'string' },
|
|
1474
|
+
},
|
|
1475
|
+
},
|
|
1476
|
+
get_ssr_mock_audit_log: {
|
|
1477
|
+
type: 'object',
|
|
1478
|
+
properties: {
|
|
1479
|
+
projectRoot: { type: 'string' },
|
|
1480
|
+
rollbackId: { type: 'string' },
|
|
1481
|
+
envVarName: { type: 'string' },
|
|
833
1482
|
limit: { type: 'number' },
|
|
834
1483
|
offset: { type: 'number' },
|
|
835
1484
|
maxResponseBytes: { type: 'number' },
|
|
836
1485
|
},
|
|
837
1486
|
},
|
|
838
|
-
|
|
1487
|
+
create_mock_route: {
|
|
839
1488
|
type: 'object',
|
|
840
|
-
required: ['
|
|
1489
|
+
required: ['targetUrl'],
|
|
841
1490
|
properties: {
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
1491
|
+
routeId: { type: 'string' },
|
|
1492
|
+
enabled: { type: 'boolean' },
|
|
1493
|
+
mode: { type: 'string' },
|
|
1494
|
+
method: { type: 'string' },
|
|
1495
|
+
matchMode: { type: 'string' },
|
|
1496
|
+
targetUrl: { type: 'string' },
|
|
1497
|
+
statusCode: { type: 'number' },
|
|
1498
|
+
responseHeaders: { type: 'object' },
|
|
1499
|
+
bodyJson: {},
|
|
1500
|
+
bodyText: { type: 'string' },
|
|
1501
|
+
bodyBase64: { type: 'string' },
|
|
1502
|
+
bodyFilePath: { type: 'string' },
|
|
1503
|
+
delayMs: { type: 'number' },
|
|
1504
|
+
sourceKind: { type: 'string' },
|
|
1505
|
+
sessionScope: { type: 'string' },
|
|
1506
|
+
projectRoot: { type: 'string' },
|
|
1507
|
+
ttlMs: { type: 'number' },
|
|
845
1508
|
},
|
|
846
1509
|
},
|
|
847
|
-
|
|
1510
|
+
update_mock_route: {
|
|
848
1511
|
type: 'object',
|
|
849
|
-
required: ['
|
|
1512
|
+
required: ['routeId'],
|
|
850
1513
|
properties: {
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
1514
|
+
routeId: { type: 'string' },
|
|
1515
|
+
enabled: { type: 'boolean' },
|
|
1516
|
+
mode: { type: 'string' },
|
|
1517
|
+
method: { type: 'string' },
|
|
1518
|
+
matchMode: { type: 'string' },
|
|
1519
|
+
targetUrl: { type: 'string' },
|
|
1520
|
+
statusCode: { type: 'number' },
|
|
1521
|
+
responseHeaders: { type: 'object' },
|
|
1522
|
+
bodyJson: {},
|
|
1523
|
+
bodyText: { type: 'string' },
|
|
1524
|
+
bodyBase64: { type: 'string' },
|
|
1525
|
+
bodyFilePath: { type: 'string' },
|
|
1526
|
+
delayMs: { type: 'number' },
|
|
1527
|
+
sourceKind: { type: 'string' },
|
|
1528
|
+
sessionScope: { type: 'string' },
|
|
1529
|
+
projectRoot: { type: 'string' },
|
|
1530
|
+
ttlMs: { type: 'number' },
|
|
1531
|
+
},
|
|
1532
|
+
},
|
|
1533
|
+
delete_mock_route: {
|
|
1534
|
+
type: 'object',
|
|
1535
|
+
required: ['routeId'],
|
|
1536
|
+
properties: {
|
|
1537
|
+
routeId: { type: 'string' },
|
|
1538
|
+
},
|
|
1539
|
+
},
|
|
1540
|
+
list_mock_routes: {
|
|
1541
|
+
type: 'object',
|
|
1542
|
+
properties: {
|
|
1543
|
+
projectRoot: { type: 'string' },
|
|
1544
|
+
mode: { type: 'string' },
|
|
1545
|
+
enabled: { type: 'boolean' },
|
|
1546
|
+
limit: { type: 'number' },
|
|
854
1547
|
offset: { type: 'number' },
|
|
855
|
-
|
|
856
|
-
encoding: { type: 'string' },
|
|
1548
|
+
maxResponseBytes: { type: 'number' },
|
|
857
1549
|
},
|
|
858
1550
|
},
|
|
859
|
-
|
|
1551
|
+
get_mock_route: {
|
|
1552
|
+
type: 'object',
|
|
1553
|
+
required: ['routeId'],
|
|
1554
|
+
properties: {
|
|
1555
|
+
routeId: { type: 'string' },
|
|
1556
|
+
},
|
|
1557
|
+
},
|
|
1558
|
+
get_mock_run_log: {
|
|
860
1559
|
type: 'object',
|
|
861
|
-
required: ['sessionId'],
|
|
862
1560
|
properties: {
|
|
1561
|
+
routeId: { type: 'string' },
|
|
863
1562
|
sessionId: { type: 'string' },
|
|
864
|
-
status: { type: 'string', enum: ['requested', 'started', 'succeeded', 'failed', 'rejected', 'stopped'] },
|
|
865
|
-
action: { type: 'string', enum: ['click', 'input', 'focus', 'blur', 'scroll', 'press_key', 'submit', 'reload'] },
|
|
866
|
-
traceId: { type: 'string' },
|
|
867
1563
|
limit: { type: 'number' },
|
|
868
1564
|
offset: { type: 'number' },
|
|
869
1565
|
maxResponseBytes: { type: 'number' },
|
|
870
1566
|
},
|
|
871
1567
|
},
|
|
872
|
-
|
|
1568
|
+
get_mock_hit_log: {
|
|
873
1569
|
type: 'object',
|
|
874
|
-
required: ['sessionId', 'runId'],
|
|
875
1570
|
properties: {
|
|
876
|
-
|
|
1571
|
+
routeId: { type: 'string' },
|
|
877
1572
|
runId: { type: 'string' },
|
|
878
|
-
|
|
879
|
-
|
|
1573
|
+
limit: { type: 'number' },
|
|
1574
|
+
offset: { type: 'number' },
|
|
880
1575
|
maxResponseBytes: { type: 'number' },
|
|
881
1576
|
},
|
|
882
1577
|
},
|
|
883
|
-
|
|
1578
|
+
get_mock_status: {
|
|
1579
|
+
type: 'object',
|
|
1580
|
+
properties: {
|
|
1581
|
+
routeId: { type: 'string' },
|
|
1582
|
+
projectRoot: { type: 'string' },
|
|
1583
|
+
},
|
|
1584
|
+
},
|
|
1585
|
+
explain_last_failure: {
|
|
1586
|
+
type: 'object',
|
|
1587
|
+
required: ['sessionId'],
|
|
1588
|
+
properties: {
|
|
1589
|
+
sessionId: { type: 'string' },
|
|
1590
|
+
lookbackSeconds: { type: 'number' },
|
|
1591
|
+
},
|
|
1592
|
+
},
|
|
1593
|
+
get_event_correlation: {
|
|
1594
|
+
type: 'object',
|
|
1595
|
+
required: ['sessionId', 'eventId'],
|
|
1596
|
+
properties: {
|
|
1597
|
+
sessionId: { type: 'string' },
|
|
1598
|
+
eventId: { type: 'string' },
|
|
1599
|
+
windowSeconds: { type: 'number' },
|
|
1600
|
+
},
|
|
1601
|
+
},
|
|
1602
|
+
list_snapshots: {
|
|
1603
|
+
type: 'object',
|
|
1604
|
+
required: ['sessionId'],
|
|
1605
|
+
properties: {
|
|
1606
|
+
sessionId: { type: 'string' },
|
|
1607
|
+
trigger: { type: 'string' },
|
|
1608
|
+
sinceTimestamp: { type: 'number' },
|
|
1609
|
+
untilTimestamp: { type: 'number' },
|
|
1610
|
+
limit: { type: 'number' },
|
|
1611
|
+
offset: { type: 'number' },
|
|
1612
|
+
maxResponseBytes: { type: 'number' },
|
|
1613
|
+
},
|
|
1614
|
+
},
|
|
1615
|
+
get_snapshot_for_event: {
|
|
1616
|
+
type: 'object',
|
|
1617
|
+
required: ['sessionId', 'eventId'],
|
|
1618
|
+
properties: {
|
|
1619
|
+
sessionId: { type: 'string' },
|
|
1620
|
+
eventId: { type: 'string' },
|
|
1621
|
+
maxDeltaMs: { type: 'number' },
|
|
1622
|
+
},
|
|
1623
|
+
},
|
|
1624
|
+
get_snapshot_asset: {
|
|
1625
|
+
type: 'object',
|
|
1626
|
+
required: ['sessionId', 'snapshotId'],
|
|
1627
|
+
properties: {
|
|
1628
|
+
sessionId: { type: 'string' },
|
|
1629
|
+
snapshotId: { type: 'string' },
|
|
1630
|
+
asset: { type: 'string' },
|
|
1631
|
+
offset: { type: 'number' },
|
|
1632
|
+
maxBytes: { type: 'number' },
|
|
1633
|
+
encoding: { type: 'string' },
|
|
1634
|
+
},
|
|
1635
|
+
},
|
|
1636
|
+
run_lighthouse_report: {
|
|
1637
|
+
type: 'object',
|
|
1638
|
+
properties: {
|
|
1639
|
+
sessionId: { type: 'string' },
|
|
1640
|
+
url: { type: 'string' },
|
|
1641
|
+
formFactor: { type: 'string', enum: ['mobile', 'desktop'] },
|
|
1642
|
+
categories: {
|
|
1643
|
+
type: 'array',
|
|
1644
|
+
items: { type: 'string', enum: ['performance', 'accessibility', 'best-practices', 'seo', 'pwa'] },
|
|
1645
|
+
},
|
|
1646
|
+
maxWaitForLoadMs: { type: 'number' },
|
|
1647
|
+
chromeFlags: { type: 'array', items: { type: 'string' } },
|
|
1648
|
+
},
|
|
1649
|
+
},
|
|
1650
|
+
list_lighthouse_reports: {
|
|
1651
|
+
type: 'object',
|
|
1652
|
+
properties: {
|
|
1653
|
+
sessionId: { type: 'string' },
|
|
1654
|
+
urlContains: { type: 'string' },
|
|
1655
|
+
status: { type: 'string', enum: ['succeeded', 'failed'] },
|
|
1656
|
+
limit: { type: 'number' },
|
|
1657
|
+
offset: { type: 'number' },
|
|
1658
|
+
},
|
|
1659
|
+
},
|
|
1660
|
+
get_lighthouse_report: {
|
|
1661
|
+
type: 'object',
|
|
1662
|
+
required: ['reportId'],
|
|
1663
|
+
properties: {
|
|
1664
|
+
reportId: { type: 'string' },
|
|
1665
|
+
},
|
|
1666
|
+
},
|
|
1667
|
+
get_lighthouse_report_asset: {
|
|
1668
|
+
type: 'object',
|
|
1669
|
+
required: ['reportId'],
|
|
1670
|
+
properties: {
|
|
1671
|
+
reportId: { type: 'string' },
|
|
1672
|
+
asset: { type: 'string', enum: ['json', 'html'] },
|
|
1673
|
+
offset: { type: 'number' },
|
|
1674
|
+
maxBytes: { type: 'number' },
|
|
1675
|
+
encoding: { type: 'string', enum: ['base64', 'raw'] },
|
|
1676
|
+
},
|
|
1677
|
+
},
|
|
1678
|
+
plan_lighthouse_fixes: {
|
|
1679
|
+
type: 'object',
|
|
1680
|
+
required: ['reportId'],
|
|
1681
|
+
properties: {
|
|
1682
|
+
reportId: { type: 'string' },
|
|
1683
|
+
minPriority: { type: 'string', enum: ['critical', 'high', 'medium', 'low'] },
|
|
1684
|
+
limit: { type: 'number' },
|
|
1685
|
+
projectRoot: { type: 'string' },
|
|
1686
|
+
routePath: { type: 'string' },
|
|
1687
|
+
sourceCandidateLimit: { type: 'number' },
|
|
1688
|
+
},
|
|
1689
|
+
},
|
|
1690
|
+
list_automation_runs: {
|
|
1691
|
+
type: 'object',
|
|
1692
|
+
required: ['sessionId'],
|
|
1693
|
+
properties: {
|
|
1694
|
+
sessionId: { type: 'string' },
|
|
1695
|
+
status: { type: 'string', enum: ['requested', 'started', 'succeeded', 'failed', 'rejected', 'stopped'] },
|
|
1696
|
+
action: { type: 'string', enum: ['click', 'hover', 'input', 'focus', 'blur', 'scroll', 'press_key', 'submit', 'reload'] },
|
|
1697
|
+
traceId: { type: 'string' },
|
|
1698
|
+
limit: { type: 'number' },
|
|
1699
|
+
offset: { type: 'number' },
|
|
1700
|
+
maxResponseBytes: { type: 'number' },
|
|
1701
|
+
},
|
|
1702
|
+
},
|
|
1703
|
+
get_automation_run: {
|
|
1704
|
+
type: 'object',
|
|
1705
|
+
required: ['sessionId', 'runId'],
|
|
1706
|
+
properties: {
|
|
1707
|
+
sessionId: { type: 'string' },
|
|
1708
|
+
runId: { type: 'string' },
|
|
1709
|
+
stepLimit: { type: 'number' },
|
|
1710
|
+
stepOffset: { type: 'number' },
|
|
1711
|
+
maxResponseBytes: { type: 'number' },
|
|
1712
|
+
},
|
|
1713
|
+
},
|
|
1714
|
+
execute_ui_action: {
|
|
884
1715
|
type: 'object',
|
|
885
1716
|
required: ['sessionId', 'action'],
|
|
886
1717
|
properties: {
|
|
887
1718
|
sessionId: { type: 'string' },
|
|
888
|
-
action: { type: 'string', enum: ['click', 'input', 'focus', 'blur', 'scroll', 'press_key', 'submit', 'reload'] },
|
|
1719
|
+
action: { type: 'string', enum: ['click', 'hover', 'input', 'focus', 'blur', 'scroll', 'press_key', 'submit', 'reload'] },
|
|
889
1720
|
traceId: { type: 'string' },
|
|
890
1721
|
target: {
|
|
891
1722
|
type: 'object',
|
|
892
1723
|
properties: {
|
|
893
1724
|
selector: { type: 'string' },
|
|
894
1725
|
elementRef: { type: 'string' },
|
|
1726
|
+
coordinates: {
|
|
1727
|
+
type: 'object',
|
|
1728
|
+
properties: {
|
|
1729
|
+
x: { type: 'number' },
|
|
1730
|
+
y: { type: 'number' },
|
|
1731
|
+
frameId: { type: 'number' },
|
|
1732
|
+
},
|
|
1733
|
+
},
|
|
895
1734
|
tabId: { type: 'number' },
|
|
896
1735
|
frameId: { type: 'number' },
|
|
897
1736
|
url: { type: 'string' },
|
|
1737
|
+
locator: ACTION_LOCATOR_TOOL_SCHEMA,
|
|
1738
|
+
frameUrlContains: { type: 'string' },
|
|
1739
|
+
frameTitleContains: { type: 'string' },
|
|
1740
|
+
testId: { type: 'string' },
|
|
1741
|
+
scope: { type: 'string', enum: ['buttons', 'links', 'inputs', 'modals', 'focused'] },
|
|
1742
|
+
textContains: { type: 'string' },
|
|
1743
|
+
labelContains: { type: 'string' },
|
|
1744
|
+
titleContains: { type: 'string' },
|
|
1745
|
+
role: { type: 'string' },
|
|
1746
|
+
name: { type: 'string' },
|
|
1747
|
+
placeholder: { type: 'string' },
|
|
1748
|
+
altText: { type: 'string' },
|
|
1749
|
+
exact: { type: 'boolean' },
|
|
1750
|
+
nth: { type: 'number' },
|
|
1751
|
+
first: { type: 'boolean' },
|
|
1752
|
+
last: { type: 'boolean' },
|
|
1753
|
+
strict: { type: 'boolean' },
|
|
1754
|
+
tagName: { type: 'string' },
|
|
1755
|
+
type: { type: 'string' },
|
|
1756
|
+
visible: { type: 'boolean' },
|
|
1757
|
+
enabled: { type: 'boolean' },
|
|
1758
|
+
disabled: { type: 'boolean' },
|
|
1759
|
+
editable: { type: 'boolean' },
|
|
1760
|
+
checked: { type: 'boolean' },
|
|
1761
|
+
selected: { type: 'boolean' },
|
|
1762
|
+
pressed: { type: 'boolean' },
|
|
1763
|
+
expanded: { type: 'boolean' },
|
|
1764
|
+
readOnly: { type: 'boolean' },
|
|
1765
|
+
requiredField: { type: 'boolean' },
|
|
898
1766
|
},
|
|
899
1767
|
},
|
|
900
1768
|
input: { type: 'object' },
|
|
@@ -917,14 +1785,22 @@ const TOOL_SCHEMAS = {
|
|
|
917
1785
|
type: 'object',
|
|
918
1786
|
required: ['scope'],
|
|
919
1787
|
properties: {
|
|
920
|
-
scope: { type: 'string', enum: ['buttons', 'inputs', 'modals', 'focused', 'page'] },
|
|
1788
|
+
scope: { type: 'string', enum: ['buttons', 'links', 'inputs', 'modals', 'focused', 'page'] },
|
|
921
1789
|
selector: { type: 'string' },
|
|
922
1790
|
testId: { type: 'string' },
|
|
923
1791
|
textContains: { type: 'string' },
|
|
924
1792
|
labelContains: { type: 'string' },
|
|
925
1793
|
titleContains: { type: 'string' },
|
|
1794
|
+
role: { type: 'string' },
|
|
1795
|
+
name: { type: 'string' },
|
|
1796
|
+
placeholder: { type: 'string' },
|
|
1797
|
+
altText: { type: 'string' },
|
|
1798
|
+
exact: { type: 'boolean' },
|
|
1799
|
+
frameUrlContains: { type: 'string' },
|
|
1800
|
+
frameTitleContains: { type: 'string' },
|
|
926
1801
|
urlContains: { type: 'string' },
|
|
927
1802
|
language: { type: 'string' },
|
|
1803
|
+
visible: { type: 'boolean' },
|
|
928
1804
|
disabled: { type: 'boolean' },
|
|
929
1805
|
selected: { type: 'boolean' },
|
|
930
1806
|
pressed: { type: 'boolean' },
|
|
@@ -961,7 +1837,7 @@ const TOOL_SCHEMAS = {
|
|
|
961
1837
|
properties: {
|
|
962
1838
|
id: { type: 'string' },
|
|
963
1839
|
note: { type: 'string' },
|
|
964
|
-
kind: { type: 'string', enum: ['action', 'waitFor', 'assert'] },
|
|
1840
|
+
kind: { type: 'string', enum: ['action', 'waitFor', 'assert', 'wait'] },
|
|
965
1841
|
action: { type: 'string' },
|
|
966
1842
|
traceId: { type: 'string' },
|
|
967
1843
|
target: {
|
|
@@ -969,16 +1845,37 @@ const TOOL_SCHEMAS = {
|
|
|
969
1845
|
properties: {
|
|
970
1846
|
selector: { type: 'string' },
|
|
971
1847
|
elementRef: { type: 'string' },
|
|
1848
|
+
coordinates: {
|
|
1849
|
+
type: 'object',
|
|
1850
|
+
properties: {
|
|
1851
|
+
x: { type: 'number' },
|
|
1852
|
+
y: { type: 'number' },
|
|
1853
|
+
frameId: { type: 'number' },
|
|
1854
|
+
},
|
|
1855
|
+
},
|
|
972
1856
|
tabId: { type: 'number' },
|
|
973
1857
|
frameId: { type: 'number' },
|
|
974
1858
|
url: { type: 'string' },
|
|
1859
|
+
locator: ACTION_LOCATOR_TOOL_SCHEMA,
|
|
1860
|
+
frameUrlContains: { type: 'string' },
|
|
1861
|
+
frameTitleContains: { type: 'string' },
|
|
975
1862
|
testId: { type: 'string' },
|
|
976
|
-
scope: { type: 'string', enum: ['buttons', 'inputs', 'modals', 'focused'] },
|
|
1863
|
+
scope: { type: 'string', enum: ['buttons', 'links', 'inputs', 'modals', 'focused'] },
|
|
977
1864
|
textContains: { type: 'string' },
|
|
978
1865
|
labelContains: { type: 'string' },
|
|
979
1866
|
titleContains: { type: 'string' },
|
|
1867
|
+
role: { type: 'string' },
|
|
1868
|
+
name: { type: 'string' },
|
|
1869
|
+
placeholder: { type: 'string' },
|
|
1870
|
+
altText: { type: 'string' },
|
|
1871
|
+
exact: { type: 'boolean' },
|
|
1872
|
+
nth: { type: 'number' },
|
|
1873
|
+
first: { type: 'boolean' },
|
|
1874
|
+
last: { type: 'boolean' },
|
|
1875
|
+
strict: { type: 'boolean' },
|
|
980
1876
|
tagName: { type: 'string' },
|
|
981
1877
|
type: { type: 'string' },
|
|
1878
|
+
visible: { type: 'boolean' },
|
|
982
1879
|
disabled: { type: 'boolean' },
|
|
983
1880
|
selected: { type: 'boolean' },
|
|
984
1881
|
pressed: { type: 'boolean' },
|
|
@@ -1012,14 +1909,22 @@ const TOOL_SCHEMAS = {
|
|
|
1012
1909
|
matcher: {
|
|
1013
1910
|
type: 'object',
|
|
1014
1911
|
properties: {
|
|
1015
|
-
scope: { type: 'string', enum: ['buttons', 'inputs', 'modals', 'focused', 'page'] },
|
|
1912
|
+
scope: { type: 'string', enum: ['buttons', 'links', 'inputs', 'modals', 'focused', 'page'] },
|
|
1016
1913
|
selector: { type: 'string' },
|
|
1017
1914
|
testId: { type: 'string' },
|
|
1018
1915
|
textContains: { type: 'string' },
|
|
1019
1916
|
labelContains: { type: 'string' },
|
|
1020
1917
|
titleContains: { type: 'string' },
|
|
1918
|
+
role: { type: 'string' },
|
|
1919
|
+
name: { type: 'string' },
|
|
1920
|
+
placeholder: { type: 'string' },
|
|
1921
|
+
altText: { type: 'string' },
|
|
1922
|
+
exact: { type: 'boolean' },
|
|
1923
|
+
frameUrlContains: { type: 'string' },
|
|
1924
|
+
frameTitleContains: { type: 'string' },
|
|
1021
1925
|
urlContains: { type: 'string' },
|
|
1022
1926
|
language: { type: 'string' },
|
|
1927
|
+
visible: { type: 'boolean' },
|
|
1023
1928
|
disabled: { type: 'boolean' },
|
|
1024
1929
|
selected: { type: 'boolean' },
|
|
1025
1930
|
pressed: { type: 'boolean' },
|
|
@@ -1036,6 +1941,7 @@ const TOOL_SCHEMAS = {
|
|
|
1036
1941
|
pollIntervalMs: { type: 'number' },
|
|
1037
1942
|
},
|
|
1038
1943
|
},
|
|
1944
|
+
wait: AUTOMATION_WAIT_TOOL_SCHEMA,
|
|
1039
1945
|
},
|
|
1040
1946
|
},
|
|
1041
1947
|
},
|
|
@@ -1062,11 +1968,25 @@ const TOOL_DESCRIPTIONS = {
|
|
|
1062
1968
|
get_computed_styles: 'Read computed CSS styles for an element',
|
|
1063
1969
|
get_layout_metrics: 'Read viewport and element layout metrics',
|
|
1064
1970
|
get_page_state: 'Read a compact structured page model for forms, buttons, modals, and viewport state',
|
|
1065
|
-
get_interactive_elements: 'Read compact live element references for buttons, inputs, modals, and focused elements',
|
|
1971
|
+
get_interactive_elements: 'Read compact live element references for buttons, links, inputs, modals, and focused elements',
|
|
1066
1972
|
get_live_session_health: 'Read live transport health and session binding details for one session',
|
|
1067
1973
|
set_viewport: 'Resize the live browser window for a session and return the resulting viewport metrics',
|
|
1068
1974
|
assert_page_state: 'Assert compact page-state conditions without pulling raw DOM payloads',
|
|
1069
1975
|
wait_for_page_state: 'Poll compact page state until a structured assertion becomes true',
|
|
1976
|
+
preflight_automation_flow: 'Check live-session readiness and production risks before running an automation flow',
|
|
1977
|
+
wait_for_url: 'Poll the live page URL until it matches an exact, contains, or regex condition',
|
|
1978
|
+
wait_for_navigation: 'Poll persisted navigation events until a matching URL or trigger is observed',
|
|
1979
|
+
wait_for_navigation_lifecycle: 'Wait for a live navigation lifecycle milestone such as commit, load, or network idle',
|
|
1980
|
+
wait_for_load_state: 'Poll the live page document readiness until domcontentloaded or load is reached',
|
|
1981
|
+
wait_for_selector_state: 'Poll a selector until it is attached, detached, visible, or hidden',
|
|
1982
|
+
wait_for_console: 'Poll live console logs until a matching message appears',
|
|
1983
|
+
wait_for_dialog: 'Wait for a native JavaScript dialog and optionally accept or dismiss it',
|
|
1984
|
+
wait_for_stable_layout: 'Wait until the page or selector layout stays unchanged for a stable window',
|
|
1985
|
+
wait_for_download: 'Wait for a download started by the bound tab and optionally until completion',
|
|
1986
|
+
wait_for_popup: 'Wait for a popup tab or window opened from the bound session tab',
|
|
1987
|
+
wait_for_network_quiet: 'Wait until persisted network activity is quiet for a bounded window',
|
|
1988
|
+
wait_for_request: 'Poll persisted network activity until a matching request is observed',
|
|
1989
|
+
wait_for_response: 'Poll persisted network activity until a matching response is observed',
|
|
1070
1990
|
capture_ui_snapshot: 'Capture redacted UI snapshot (DOM/styles/optional PNG) and persist it',
|
|
1071
1991
|
get_live_console_logs: 'Read in-memory live console logs for a connected session',
|
|
1072
1992
|
list_override_profiles: 'List configured browser override profiles',
|
|
@@ -1085,11 +2005,28 @@ const TOOL_DESCRIPTIONS = {
|
|
|
1085
2005
|
get_override_request_log: 'Read persisted browser override request audit rows',
|
|
1086
2006
|
get_override_plan_log: 'Read persisted generated override plan audit rows with previews, hashes, and rollback metadata',
|
|
1087
2007
|
diagnose_overrides: 'Diagnose persisted browser override runs and failure indicators',
|
|
2008
|
+
discover_ssr_mockability: 'Inspect a local project for env-driven or central-client SSR mock injection points',
|
|
2009
|
+
apply_ssr_mock_config: 'Apply a temporary SSR mock base URL by commenting the old env value and writing a managed replacement',
|
|
2010
|
+
remove_ssr_mock_config: 'Restore or remove a managed SSR mock env patch from a local env file',
|
|
2011
|
+
get_ssr_mock_audit_log: 'Read persisted SSR mock discovery and env patch audit rows',
|
|
2012
|
+
create_mock_route: 'Create or persist a reusable browser or SSR mock route',
|
|
2013
|
+
update_mock_route: 'Update an existing mock route definition',
|
|
2014
|
+
delete_mock_route: 'Delete a persisted mock route',
|
|
2015
|
+
list_mock_routes: 'List persisted mock route definitions',
|
|
2016
|
+
get_mock_route: 'Read one persisted mock route definition',
|
|
2017
|
+
get_mock_run_log: 'Read persisted mock route run records',
|
|
2018
|
+
get_mock_hit_log: 'Read persisted mock route hit records',
|
|
2019
|
+
get_mock_status: 'Summarize persisted mock route, run, and hit state',
|
|
1088
2020
|
explain_last_failure: 'Explain the latest failure timeline',
|
|
1089
2021
|
get_event_correlation: 'Correlate related events by window',
|
|
1090
2022
|
list_snapshots: 'List snapshot metadata by session/time/trigger',
|
|
1091
2023
|
get_snapshot_for_event: 'Find snapshot most related to an event',
|
|
1092
2024
|
get_snapshot_asset: 'Read bounded binary chunks for snapshot assets',
|
|
2025
|
+
run_lighthouse_report: 'Run an official Lighthouse report for a URL or session URL and persist JSON/HTML artifacts',
|
|
2026
|
+
list_lighthouse_reports: 'List persisted Lighthouse report metadata',
|
|
2027
|
+
get_lighthouse_report: 'Read one persisted Lighthouse report summary',
|
|
2028
|
+
get_lighthouse_report_asset: 'Read bounded chunks from a persisted Lighthouse JSON or HTML report artifact',
|
|
2029
|
+
plan_lighthouse_fixes: 'Create a prioritized fix plan from a persisted Lighthouse report',
|
|
1093
2030
|
list_automation_runs: 'List first-class automation runs from dedicated automation tables',
|
|
1094
2031
|
get_automation_run: 'Inspect one automation run with bounded step details',
|
|
1095
2032
|
execute_ui_action: 'Execute one live UI action in the current bound extension session',
|
|
@@ -1113,6 +2050,7 @@ const MAX_BODY_CHUNK_BYTES = 256 * 1024;
|
|
|
1113
2050
|
const DEFAULT_NETWORK_POLL_TIMEOUT_MS = 15_000;
|
|
1114
2051
|
const MAX_NETWORK_POLL_TIMEOUT_MS = 120_000;
|
|
1115
2052
|
const DEFAULT_NETWORK_POLL_INTERVAL_MS = 250;
|
|
2053
|
+
const DEFAULT_AUTOMATION_WAIT_LOOKBACK_MS = 5_000;
|
|
1116
2054
|
const LIVE_SESSION_DISCONNECTED_CODE = 'LIVE_SESSION_DISCONNECTED';
|
|
1117
2055
|
const OVERRIDE_LIVE_COMMAND_TIMEOUT_CODE = 'OVERRIDE_LIVE_COMMAND_TIMEOUT';
|
|
1118
2056
|
const OVERRIDE_LIVE_COMMAND_FAILED_CODE = 'OVERRIDE_LIVE_COMMAND_FAILED';
|
|
@@ -1182,6 +2120,23 @@ function resolveMaxResponseBytes(value) {
|
|
|
1182
2120
|
}
|
|
1183
2121
|
return Math.min(floored, MAX_RESPONSE_BYTES);
|
|
1184
2122
|
}
|
|
2123
|
+
function createSsrMockAuditRecord(input) {
|
|
2124
|
+
return {
|
|
2125
|
+
auditId: randomUUID(),
|
|
2126
|
+
createdAt: Date.now(),
|
|
2127
|
+
action: input.action,
|
|
2128
|
+
status: input.status,
|
|
2129
|
+
projectRoot: input.projectRoot,
|
|
2130
|
+
targetUrl: input.targetUrl ?? null,
|
|
2131
|
+
apiHost: input.apiHost ?? null,
|
|
2132
|
+
envVarName: input.envVarName ?? null,
|
|
2133
|
+
envFilePath: input.envFilePath ?? null,
|
|
2134
|
+
mockBaseUrl: input.mockBaseUrl ?? null,
|
|
2135
|
+
rollbackId: input.rollbackId ?? null,
|
|
2136
|
+
summary: input.summary ?? null,
|
|
2137
|
+
result: input.result ?? null,
|
|
2138
|
+
};
|
|
2139
|
+
}
|
|
1185
2140
|
function estimateJsonBytes(value) {
|
|
1186
2141
|
return Buffer.byteLength(JSON.stringify(value), 'utf-8');
|
|
1187
2142
|
}
|
|
@@ -1395,7 +2350,7 @@ function buildOverrideProfileRecords() {
|
|
|
1395
2350
|
active: profile.profileId === summary.activeProfileId,
|
|
1396
2351
|
configEnabled: summary.configEnabled,
|
|
1397
2352
|
enabled: profile.enabled,
|
|
1398
|
-
effectiveEnabled:
|
|
2353
|
+
effectiveEnabled: profile.enabled && profile.enabledRuleCount > 0,
|
|
1399
2354
|
autoReload: profile.autoReload,
|
|
1400
2355
|
configPath: summary.configPath,
|
|
1401
2356
|
fileExists: profile.fileExists,
|
|
@@ -1414,6 +2369,72 @@ function resolveOverrideProfileRecord(value) {
|
|
|
1414
2369
|
}
|
|
1415
2370
|
return profile;
|
|
1416
2371
|
}
|
|
2372
|
+
function resolveOverrideResponseProfile(value) {
|
|
2373
|
+
return value === 'full' ? 'full' : 'compact';
|
|
2374
|
+
}
|
|
2375
|
+
function compactOverrideRule(rule) {
|
|
2376
|
+
if (!isRecord(rule)) {
|
|
2377
|
+
return {};
|
|
2378
|
+
}
|
|
2379
|
+
return {
|
|
2380
|
+
ruleId: rule.ruleId,
|
|
2381
|
+
enabled: rule.enabled,
|
|
2382
|
+
ruleType: rule.ruleType,
|
|
2383
|
+
requestMethod: rule.requestMethod,
|
|
2384
|
+
matchMode: rule.matchMode,
|
|
2385
|
+
targetAssetUrl: rule.targetAssetUrl,
|
|
2386
|
+
localFilePath: rule.localFilePath,
|
|
2387
|
+
contentType: rule.contentType,
|
|
2388
|
+
fileExists: rule.fileExists,
|
|
2389
|
+
integrity: rule.integrity,
|
|
2390
|
+
};
|
|
2391
|
+
}
|
|
2392
|
+
function compactOverrideProfile(profile, ruleLimit = 10) {
|
|
2393
|
+
const rules = Array.isArray(profile.rules) ? profile.rules : [];
|
|
2394
|
+
return {
|
|
2395
|
+
profileId: profile.profileId,
|
|
2396
|
+
name: profile.name,
|
|
2397
|
+
active: profile.active,
|
|
2398
|
+
configEnabled: profile.configEnabled,
|
|
2399
|
+
enabled: profile.enabled,
|
|
2400
|
+
effectiveEnabled: profile.effectiveEnabled,
|
|
2401
|
+
autoReload: profile.autoReload,
|
|
2402
|
+
configPath: profile.configPath,
|
|
2403
|
+
fileExists: profile.fileExists,
|
|
2404
|
+
ruleCount: profile.ruleCount,
|
|
2405
|
+
enabledRuleCount: profile.enabledRuleCount,
|
|
2406
|
+
rules: rules.slice(0, ruleLimit).map(compactOverrideRule),
|
|
2407
|
+
rulesOmitted: Math.max(0, rules.length - ruleLimit),
|
|
2408
|
+
};
|
|
2409
|
+
}
|
|
2410
|
+
function serializeOverrideProfile(profile, responseProfile) {
|
|
2411
|
+
return responseProfile === 'full' ? profile : compactOverrideProfile(profile);
|
|
2412
|
+
}
|
|
2413
|
+
function compactObservedOverrideAsset(asset) {
|
|
2414
|
+
if (!isRecord(asset)) {
|
|
2415
|
+
return {};
|
|
2416
|
+
}
|
|
2417
|
+
return {
|
|
2418
|
+
observedAssetId: asset.observedAssetId,
|
|
2419
|
+
lastSeenAt: asset.lastSeenAt,
|
|
2420
|
+
tabId: asset.tabId,
|
|
2421
|
+
url: asset.url,
|
|
2422
|
+
ruleType: asset.ruleType,
|
|
2423
|
+
requestMethod: asset.requestMethod,
|
|
2424
|
+
resourceType: asset.resourceType,
|
|
2425
|
+
contentType: asset.contentType,
|
|
2426
|
+
statusCode: asset.statusCode,
|
|
2427
|
+
pathname: asset.pathname,
|
|
2428
|
+
assetPath: asset.assetPath,
|
|
2429
|
+
kind: asset.kind,
|
|
2430
|
+
integrity: asset.integrity,
|
|
2431
|
+
fromDom: asset.fromDom,
|
|
2432
|
+
fromPerformance: asset.fromPerformance,
|
|
2433
|
+
fromNavigation: asset.fromNavigation,
|
|
2434
|
+
fromFetch: asset.fromFetch,
|
|
2435
|
+
serviceWorkerControlled: asset.serviceWorkerControlled,
|
|
2436
|
+
};
|
|
2437
|
+
}
|
|
1417
2438
|
const SHA256_HEX_PATTERN = /^[a-f0-9]{64}$/;
|
|
1418
2439
|
function sha256Text(value) {
|
|
1419
2440
|
return createHash('sha256').update(value, 'utf8').digest('hex');
|
|
@@ -1582,13 +2603,6 @@ function buildOverrideProfileIssues(profile) {
|
|
|
1582
2603
|
const rules = Array.isArray(profile.rules)
|
|
1583
2604
|
? profile.rules.filter((rule) => isRecord(rule))
|
|
1584
2605
|
: [];
|
|
1585
|
-
if (profile.configEnabled !== true) {
|
|
1586
|
-
issues.push({
|
|
1587
|
-
code: 'CONFIG_DISABLED',
|
|
1588
|
-
severity: 'warning',
|
|
1589
|
-
message: 'The override config is disabled and cannot replace requests until enabled.',
|
|
1590
|
-
});
|
|
1591
|
-
}
|
|
1592
2606
|
if (profile.enabled !== true) {
|
|
1593
2607
|
issues.push({
|
|
1594
2608
|
code: 'PROFILE_DISABLED',
|
|
@@ -1677,12 +2691,6 @@ function buildOverrideProfileNextActions(profile, issues) {
|
|
|
1677
2691
|
message: 'Regenerate the RSC rule with plan_override_response_patch from a captured text/x-component response body.',
|
|
1678
2692
|
}];
|
|
1679
2693
|
}
|
|
1680
|
-
if (profile.configEnabled !== true) {
|
|
1681
|
-
return [{
|
|
1682
|
-
code: 'ENABLE_CONFIG',
|
|
1683
|
-
message: 'Set the root override config enabled=true after reviewing the profile.',
|
|
1684
|
-
}];
|
|
1685
|
-
}
|
|
1686
2694
|
if (profile.enabled !== true) {
|
|
1687
2695
|
return [{
|
|
1688
2696
|
code: 'ENABLE_PROFILE',
|
|
@@ -2385,6 +3393,94 @@ function normalizeOptionalString(value) {
|
|
|
2385
3393
|
const trimmed = value.trim();
|
|
2386
3394
|
return trimmed.length > 0 ? trimmed : undefined;
|
|
2387
3395
|
}
|
|
3396
|
+
function normalizeMockRouteMode(value) {
|
|
3397
|
+
return value === 'ssr' || value === 'both' ? value : 'browser';
|
|
3398
|
+
}
|
|
3399
|
+
function normalizeMockRouteSourceKind(value) {
|
|
3400
|
+
return value === 'captured' || value === 'patched' ? value : 'manual';
|
|
3401
|
+
}
|
|
3402
|
+
function normalizeMockRouteBody(input) {
|
|
3403
|
+
if (Object.prototype.hasOwnProperty.call(input, 'bodyJson')) {
|
|
3404
|
+
return {
|
|
3405
|
+
bodyKind: 'json',
|
|
3406
|
+
bodyJson: input.bodyJson,
|
|
3407
|
+
};
|
|
3408
|
+
}
|
|
3409
|
+
const bodyText = normalizeOptionalString(input.bodyText);
|
|
3410
|
+
if (bodyText !== undefined) {
|
|
3411
|
+
return {
|
|
3412
|
+
bodyKind: 'text',
|
|
3413
|
+
bodyText,
|
|
3414
|
+
};
|
|
3415
|
+
}
|
|
3416
|
+
const bodyBase64 = normalizeOptionalString(input.bodyBase64);
|
|
3417
|
+
if (bodyBase64 !== undefined) {
|
|
3418
|
+
return {
|
|
3419
|
+
bodyKind: 'base64',
|
|
3420
|
+
bodyBase64,
|
|
3421
|
+
};
|
|
3422
|
+
}
|
|
3423
|
+
const bodyFilePath = normalizeOptionalString(input.bodyFilePath);
|
|
3424
|
+
if (bodyFilePath !== undefined) {
|
|
3425
|
+
return {
|
|
3426
|
+
bodyKind: 'file',
|
|
3427
|
+
bodyFilePath,
|
|
3428
|
+
};
|
|
3429
|
+
}
|
|
3430
|
+
return null;
|
|
3431
|
+
}
|
|
3432
|
+
function createMockRouteRecord(input, existing) {
|
|
3433
|
+
const now = Date.now();
|
|
3434
|
+
const targetUrl = normalizeOptionalString(input.targetUrl) ?? existing?.targetUrl;
|
|
3435
|
+
if (!targetUrl) {
|
|
3436
|
+
throw new Error('targetUrl is required');
|
|
3437
|
+
}
|
|
3438
|
+
const body = normalizeMockRouteBody(input);
|
|
3439
|
+
const ttlMs = typeof input.ttlMs === 'number' && Number.isFinite(input.ttlMs) && input.ttlMs > 0
|
|
3440
|
+
? Math.floor(input.ttlMs)
|
|
3441
|
+
: existing?.ttlMs ?? null;
|
|
3442
|
+
return {
|
|
3443
|
+
routeId: normalizeOptionalString(input.routeId) ?? existing?.routeId ?? randomUUID(),
|
|
3444
|
+
createdAt: existing?.createdAt ?? now,
|
|
3445
|
+
updatedAt: now,
|
|
3446
|
+
enabled: typeof input.enabled === 'boolean' ? input.enabled : existing?.enabled ?? false,
|
|
3447
|
+
mode: normalizeMockRouteMode(input.mode ?? existing?.mode),
|
|
3448
|
+
method: normalizeHttpMethod(input.method) ?? existing?.method ?? 'GET',
|
|
3449
|
+
matchMode: input.matchMode === 'prefix' ? 'prefix' : existing?.matchMode ?? 'exact',
|
|
3450
|
+
targetUrl,
|
|
3451
|
+
statusCode: typeof input.statusCode === 'number' && Number.isFinite(input.statusCode)
|
|
3452
|
+
? Math.max(100, Math.min(599, Math.floor(input.statusCode)))
|
|
3453
|
+
: existing?.statusCode ?? 200,
|
|
3454
|
+
responseHeaders: Object.keys(normalizeMockHeaders(input.responseHeaders)).length > 0
|
|
3455
|
+
? normalizeMockHeaders(input.responseHeaders)
|
|
3456
|
+
: existing?.responseHeaders ?? {},
|
|
3457
|
+
bodyKind: body?.bodyKind ?? existing?.bodyKind ?? 'json',
|
|
3458
|
+
bodyJson: body?.bodyJson ?? existing?.bodyJson,
|
|
3459
|
+
bodyText: body?.bodyText ?? existing?.bodyText ?? null,
|
|
3460
|
+
bodyBase64: body?.bodyBase64 ?? existing?.bodyBase64 ?? null,
|
|
3461
|
+
bodyFilePath: body?.bodyFilePath ?? existing?.bodyFilePath ?? null,
|
|
3462
|
+
delayMs: typeof input.delayMs === 'number' && Number.isFinite(input.delayMs) && input.delayMs >= 0
|
|
3463
|
+
? Math.floor(input.delayMs)
|
|
3464
|
+
: existing?.delayMs ?? 0,
|
|
3465
|
+
sourceKind: normalizeMockRouteSourceKind(input.sourceKind ?? existing?.sourceKind),
|
|
3466
|
+
sessionScope: normalizeOptionalString(input.sessionScope) ?? existing?.sessionScope ?? null,
|
|
3467
|
+
projectRoot: normalizeOptionalString(input.projectRoot) ?? existing?.projectRoot ?? null,
|
|
3468
|
+
ttlMs,
|
|
3469
|
+
expiresAt: ttlMs !== null ? now + ttlMs : null,
|
|
3470
|
+
};
|
|
3471
|
+
}
|
|
3472
|
+
function normalizeMockHeaders(value) {
|
|
3473
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
3474
|
+
return {};
|
|
3475
|
+
}
|
|
3476
|
+
const headers = {};
|
|
3477
|
+
for (const [key, raw] of Object.entries(value)) {
|
|
3478
|
+
if (typeof raw === 'string') {
|
|
3479
|
+
headers[key.toLowerCase()] = raw;
|
|
3480
|
+
}
|
|
3481
|
+
}
|
|
3482
|
+
return headers;
|
|
3483
|
+
}
|
|
2388
3484
|
function normalizeStatusIn(value) {
|
|
2389
3485
|
if (!Array.isArray(value)) {
|
|
2390
3486
|
return [];
|
|
@@ -2558,6 +3654,7 @@ function mapAutomationRunRecord(row) {
|
|
|
2558
3654
|
: undefined,
|
|
2559
3655
|
stopReason: row.stop_reason ?? undefined,
|
|
2560
3656
|
target: parseJsonOrUndefined(row.target_summary_json),
|
|
3657
|
+
diagnostics: parseJsonOrUndefined(row.diagnostics_json),
|
|
2561
3658
|
failure: parseJsonOrUndefined(row.failure_json),
|
|
2562
3659
|
redaction: parseJsonOrUndefined(row.redaction_json),
|
|
2563
3660
|
stepCount: row.step_count,
|
|
@@ -2582,6 +3679,7 @@ function mapAutomationStepRecord(row) {
|
|
|
2582
3679
|
durationMs: row.duration_ms ?? undefined,
|
|
2583
3680
|
tabId: row.tab_id ?? undefined,
|
|
2584
3681
|
target: parseJsonOrUndefined(row.target_summary_json),
|
|
3682
|
+
diagnostics: parseJsonOrUndefined(row.diagnostics_json),
|
|
2585
3683
|
redaction: parseJsonOrUndefined(row.redaction_json),
|
|
2586
3684
|
failure: parseJsonOrUndefined(row.failure_json),
|
|
2587
3685
|
inputMetadata: parseJsonOrUndefined(row.input_metadata_json),
|
|
@@ -2592,6 +3690,109 @@ function mapAutomationStepRecord(row) {
|
|
|
2592
3690
|
source: 'automation_steps',
|
|
2593
3691
|
};
|
|
2594
3692
|
}
|
|
3693
|
+
function asRecordOrUndefined(value) {
|
|
3694
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
3695
|
+
? value
|
|
3696
|
+
: undefined;
|
|
3697
|
+
}
|
|
3698
|
+
function buildFailureEvidenceSummary(failureEvidence) {
|
|
3699
|
+
if (!failureEvidence) {
|
|
3700
|
+
return undefined;
|
|
3701
|
+
}
|
|
3702
|
+
const snapshot = asRecordOrUndefined(failureEvidence.snapshot);
|
|
3703
|
+
const snapshotRoot = asRecordOrUndefined(snapshot?.snapshot);
|
|
3704
|
+
return {
|
|
3705
|
+
captured: failureEvidence.captured === true,
|
|
3706
|
+
error: typeof failureEvidence.error === 'string' ? failureEvidence.error : undefined,
|
|
3707
|
+
limitsApplied: asRecordOrUndefined(failureEvidence.limitsApplied),
|
|
3708
|
+
snapshot: snapshot
|
|
3709
|
+
? {
|
|
3710
|
+
timestamp: typeof snapshot.timestamp === 'number' ? snapshot.timestamp : undefined,
|
|
3711
|
+
trigger: typeof snapshot.trigger === 'string' ? snapshot.trigger : undefined,
|
|
3712
|
+
selector: typeof snapshot.selector === 'string' ? snapshot.selector : undefined,
|
|
3713
|
+
url: typeof snapshot.url === 'string' ? snapshot.url : undefined,
|
|
3714
|
+
mode: snapshot.mode,
|
|
3715
|
+
hasDom: Boolean(snapshotRoot && 'dom' in snapshotRoot),
|
|
3716
|
+
hasStyles: Boolean(snapshotRoot && 'styles' in snapshotRoot),
|
|
3717
|
+
hasPng: Boolean(snapshot.png),
|
|
3718
|
+
}
|
|
3719
|
+
: undefined,
|
|
3720
|
+
};
|
|
3721
|
+
}
|
|
3722
|
+
function findRelatedFailureSnapshot(db, sessionId, failureEvidence) {
|
|
3723
|
+
const snapshotSummary = asRecordOrUndefined(buildFailureEvidenceSummary(failureEvidence)?.snapshot);
|
|
3724
|
+
if (!snapshotSummary) {
|
|
3725
|
+
return undefined;
|
|
3726
|
+
}
|
|
3727
|
+
const timestamp = typeof snapshotSummary.timestamp === 'number' ? snapshotSummary.timestamp : undefined;
|
|
3728
|
+
const selector = typeof snapshotSummary.selector === 'string' ? snapshotSummary.selector : undefined;
|
|
3729
|
+
const url = typeof snapshotSummary.url === 'string' ? snapshotSummary.url : undefined;
|
|
3730
|
+
const where = ['session_id = ?'];
|
|
3731
|
+
const params = [sessionId];
|
|
3732
|
+
if (selector) {
|
|
3733
|
+
where.push('selector = ?');
|
|
3734
|
+
params.push(selector);
|
|
3735
|
+
}
|
|
3736
|
+
if (url) {
|
|
3737
|
+
where.push('url = ?');
|
|
3738
|
+
params.push(url);
|
|
3739
|
+
}
|
|
3740
|
+
if (timestamp !== undefined) {
|
|
3741
|
+
where.push('ts BETWEEN ? AND ?');
|
|
3742
|
+
params.push(timestamp - 10_000, timestamp + 10_000);
|
|
3743
|
+
}
|
|
3744
|
+
const row = db.prepare(`SELECT snapshot_id, trigger_event_id, ts, selector, url
|
|
3745
|
+
FROM snapshots
|
|
3746
|
+
WHERE ${where.join(' AND ')}
|
|
3747
|
+
ORDER BY ${timestamp !== undefined ? 'ABS(ts - ?) ASC,' : ''} ts DESC
|
|
3748
|
+
LIMIT 1`).get(...params, ...(timestamp !== undefined ? [timestamp] : []));
|
|
3749
|
+
if (!row) {
|
|
3750
|
+
return undefined;
|
|
3751
|
+
}
|
|
3752
|
+
return {
|
|
3753
|
+
snapshotId: row.snapshot_id,
|
|
3754
|
+
triggerEventId: row.trigger_event_id ?? undefined,
|
|
3755
|
+
timestamp: row.ts,
|
|
3756
|
+
selector: row.selector ?? undefined,
|
|
3757
|
+
url: row.url ?? undefined,
|
|
3758
|
+
};
|
|
3759
|
+
}
|
|
3760
|
+
function mergeAutomationDiagnosticsEvidence(db, options) {
|
|
3761
|
+
if (!options.traceId) {
|
|
3762
|
+
return;
|
|
3763
|
+
}
|
|
3764
|
+
const failureEvidence = buildFailureEvidenceSummary(options.failureEvidence);
|
|
3765
|
+
const linkedSnapshot = findRelatedFailureSnapshot(db, options.sessionId, options.failureEvidence);
|
|
3766
|
+
if (!failureEvidence && !linkedSnapshot && !options.cdpFailure) {
|
|
3767
|
+
return;
|
|
3768
|
+
}
|
|
3769
|
+
const updateDiagnosticsJson = (tableName, keyColumn, keyValue, existingJson) => {
|
|
3770
|
+
const existing = asRecordOrUndefined(parseJsonOrUndefined(existingJson)) ?? {};
|
|
3771
|
+
const merged = {
|
|
3772
|
+
...existing,
|
|
3773
|
+
...(options.cdpFailure ? { cdpFailure: options.cdpFailure } : {}),
|
|
3774
|
+
...(failureEvidence ? { failureEvidence } : {}),
|
|
3775
|
+
...(linkedSnapshot ? { linkedSnapshot } : {}),
|
|
3776
|
+
};
|
|
3777
|
+
db.prepare(`UPDATE ${tableName} SET diagnostics_json = ?, updated_at = ? WHERE ${keyColumn} = ?`).run(JSON.stringify(merged), Date.now(), keyValue);
|
|
3778
|
+
};
|
|
3779
|
+
const runRow = db.prepare(`SELECT run_id, diagnostics_json
|
|
3780
|
+
FROM automation_runs
|
|
3781
|
+
WHERE session_id = ? AND trace_id = ?
|
|
3782
|
+
ORDER BY started_at DESC, updated_at DESC
|
|
3783
|
+
LIMIT 1`).get(options.sessionId, options.traceId);
|
|
3784
|
+
if (runRow) {
|
|
3785
|
+
updateDiagnosticsJson('automation_runs', 'run_id', runRow.run_id, runRow.diagnostics_json);
|
|
3786
|
+
}
|
|
3787
|
+
const stepRow = db.prepare(`SELECT step_id, diagnostics_json
|
|
3788
|
+
FROM automation_steps
|
|
3789
|
+
WHERE session_id = ? AND trace_id = ?
|
|
3790
|
+
ORDER BY step_order DESC, updated_at DESC
|
|
3791
|
+
LIMIT 1`).get(options.sessionId, options.traceId);
|
|
3792
|
+
if (stepRow) {
|
|
3793
|
+
updateDiagnosticsJson('automation_steps', 'step_id', stepRow.step_id, stepRow.diagnostics_json);
|
|
3794
|
+
}
|
|
3795
|
+
}
|
|
2595
3796
|
function formatUrlPath(url) {
|
|
2596
3797
|
try {
|
|
2597
3798
|
const parsed = new URL(url);
|
|
@@ -2724,15 +3925,25 @@ function resolveViewportDimension(value, axis) {
|
|
|
2724
3925
|
}
|
|
2725
3926
|
return floored;
|
|
2726
3927
|
}
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
3928
|
+
function buildWaitTimeoutDiagnostics(options) {
|
|
3929
|
+
const diagnostics = {
|
|
3930
|
+
waitKind: options.waitKind,
|
|
3931
|
+
timeoutMs: options.timeoutMs,
|
|
3932
|
+
waitedMs: options.waitedMs,
|
|
3933
|
+
attempts: options.attempts,
|
|
3934
|
+
pollIntervalMs: options.pollIntervalMs,
|
|
3935
|
+
matcherSummary: options.matcherSummary,
|
|
3936
|
+
};
|
|
3937
|
+
if (options.lastObserved !== undefined) {
|
|
3938
|
+
diagnostics.lastObserved = options.lastObserved;
|
|
3939
|
+
}
|
|
3940
|
+
if (typeof options.candidateCount === 'number') {
|
|
3941
|
+
diagnostics.candidateCount = options.candidateCount;
|
|
3942
|
+
}
|
|
3943
|
+
if (Array.isArray(options.sampledCandidates) && options.sampledCandidates.length > 0) {
|
|
3944
|
+
diagnostics.sampledCandidates = options.sampledCandidates;
|
|
2735
3945
|
}
|
|
3946
|
+
return diagnostics;
|
|
2736
3947
|
}
|
|
2737
3948
|
function resolveOptionalMatcherString(value) {
|
|
2738
3949
|
if (typeof value !== 'string') {
|
|
@@ -2755,10 +3966,10 @@ function resolveOptionalMatcherCount(value, field) {
|
|
|
2755
3966
|
return floored;
|
|
2756
3967
|
}
|
|
2757
3968
|
function resolvePageStateScope(value) {
|
|
2758
|
-
if (value === 'buttons' || value === 'inputs' || value === 'modals' || value === 'focused' || value === 'page') {
|
|
3969
|
+
if (value === 'buttons' || value === 'links' || value === 'inputs' || value === 'modals' || value === 'focused' || value === 'page') {
|
|
2759
3970
|
return value;
|
|
2760
3971
|
}
|
|
2761
|
-
throw new Error('scope must be one of buttons, inputs, modals, focused, or page');
|
|
3972
|
+
throw new Error('scope must be one of buttons, links, inputs, modals, focused, or page');
|
|
2762
3973
|
}
|
|
2763
3974
|
function resolvePageStateMatcher(input) {
|
|
2764
3975
|
const matcher = {
|
|
@@ -2768,6 +3979,13 @@ function resolvePageStateMatcher(input) {
|
|
|
2768
3979
|
textContains: resolveOptionalMatcherString(input.textContains),
|
|
2769
3980
|
labelContains: resolveOptionalMatcherString(input.labelContains),
|
|
2770
3981
|
titleContains: resolveOptionalMatcherString(input.titleContains),
|
|
3982
|
+
role: resolveOptionalMatcherString(input.role)?.toLowerCase(),
|
|
3983
|
+
name: resolveOptionalMatcherString(input.name),
|
|
3984
|
+
placeholder: resolveOptionalMatcherString(input.placeholder),
|
|
3985
|
+
altText: resolveOptionalMatcherString(input.altText),
|
|
3986
|
+
exact: resolveOptionalMatcherBoolean(input.exact),
|
|
3987
|
+
frameUrlContains: resolveOptionalMatcherString(input.frameUrlContains),
|
|
3988
|
+
frameTitleContains: resolveOptionalMatcherString(input.frameTitleContains),
|
|
2771
3989
|
urlContains: resolveOptionalMatcherString(input.urlContains),
|
|
2772
3990
|
language: resolveOptionalMatcherString(input.language),
|
|
2773
3991
|
disabled: resolveOptionalMatcherBoolean(input.disabled),
|
|
@@ -2778,6 +3996,7 @@ function resolvePageStateMatcher(input) {
|
|
|
2778
3996
|
requiredField: resolveOptionalMatcherBoolean(input.requiredField),
|
|
2779
3997
|
tagName: resolveOptionalMatcherString(input.tagName)?.toLowerCase(),
|
|
2780
3998
|
type: resolveOptionalMatcherString(input.type)?.toLowerCase(),
|
|
3999
|
+
visible: resolveOptionalMatcherBoolean(input.visible),
|
|
2781
4000
|
countExactly: resolveOptionalMatcherCount(input.countExactly, 'countExactly'),
|
|
2782
4001
|
countAtLeast: resolveOptionalMatcherCount(input.countAtLeast, 'countAtLeast'),
|
|
2783
4002
|
};
|
|
@@ -2792,6 +4011,19 @@ function includesNormalized(value, needle) {
|
|
|
2792
4011
|
}
|
|
2793
4012
|
return typeof value === 'string' && value.toLowerCase().includes(needle.toLowerCase());
|
|
2794
4013
|
}
|
|
4014
|
+
function matchesTextValue(value, expected, exact) {
|
|
4015
|
+
if (!expected) {
|
|
4016
|
+
return true;
|
|
4017
|
+
}
|
|
4018
|
+
if (typeof value !== 'string') {
|
|
4019
|
+
return false;
|
|
4020
|
+
}
|
|
4021
|
+
const normalizedValue = value.trim().toLowerCase();
|
|
4022
|
+
const normalizedExpected = expected.trim().toLowerCase();
|
|
4023
|
+
return exact === true
|
|
4024
|
+
? normalizedValue === normalizedExpected
|
|
4025
|
+
: normalizedValue.includes(normalizedExpected);
|
|
4026
|
+
}
|
|
2795
4027
|
function equalsNormalized(value, expected) {
|
|
2796
4028
|
if (!expected) {
|
|
2797
4029
|
return true;
|
|
@@ -2805,7 +4037,7 @@ function equalsOptionalBoolean(value, expected) {
|
|
|
2805
4037
|
return value === expected;
|
|
2806
4038
|
}
|
|
2807
4039
|
function pickPageStateScopeItems(payload, scope) {
|
|
2808
|
-
if (scope === 'buttons' || scope === 'inputs' || scope === 'modals') {
|
|
4040
|
+
if (scope === 'buttons' || scope === 'links' || scope === 'inputs' || scope === 'modals') {
|
|
2809
4041
|
const value = payload[scope];
|
|
2810
4042
|
return asRecordArray(value);
|
|
2811
4043
|
}
|
|
@@ -2818,13 +4050,20 @@ function pickPageStateScopeItems(payload, scope) {
|
|
|
2818
4050
|
function matchesPageStateItem(item, matcher) {
|
|
2819
4051
|
return (includesNormalized(item.selector, matcher.selector)
|
|
2820
4052
|
&& equalsNormalized(item.testId, matcher.testId)
|
|
2821
|
-
&&
|
|
2822
|
-
&&
|
|
2823
|
-
&&
|
|
4053
|
+
&& matchesTextValue(item.text, matcher.textContains, matcher.exact)
|
|
4054
|
+
&& matchesTextValue(item.label, matcher.labelContains, matcher.exact)
|
|
4055
|
+
&& matchesTextValue(item.title, matcher.titleContains, matcher.exact)
|
|
4056
|
+
&& equalsNormalized(item.role, matcher.role)
|
|
4057
|
+
&& matchesTextValue(item.name, matcher.name, matcher.exact)
|
|
4058
|
+
&& matchesTextValue(item.placeholder, matcher.placeholder, matcher.exact)
|
|
4059
|
+
&& matchesTextValue(item.altText, matcher.altText, matcher.exact)
|
|
4060
|
+
&& includesNormalized(item.frameUrl, matcher.frameUrlContains)
|
|
4061
|
+
&& includesNormalized(item.frameTitle, matcher.frameTitleContains)
|
|
2824
4062
|
&& includesNormalized(item.url, matcher.urlContains)
|
|
2825
4063
|
&& equalsNormalized(item.language, matcher.language)
|
|
2826
4064
|
&& equalsNormalized(item.tagName, matcher.tagName)
|
|
2827
4065
|
&& equalsNormalized(item.type, matcher.type)
|
|
4066
|
+
&& equalsOptionalBoolean(item.visible, matcher.visible)
|
|
2828
4067
|
&& equalsOptionalBoolean(item.disabled, matcher.disabled)
|
|
2829
4068
|
&& equalsOptionalBoolean(item.selected, matcher.selected)
|
|
2830
4069
|
&& equalsOptionalBoolean(item.pressed, matcher.pressed)
|
|
@@ -2881,7 +4120,7 @@ function createPageChangeSummary(previousCapture, currentCapture) {
|
|
|
2881
4120
|
const previousSummary = previous?.summary;
|
|
2882
4121
|
const currentSummary = current.summary;
|
|
2883
4122
|
const summaryDelta = {};
|
|
2884
|
-
for (const key of ['buttons', 'inputs', 'modals']) {
|
|
4123
|
+
for (const key of ['buttons', 'links', 'inputs', 'modals']) {
|
|
2885
4124
|
const previousValue = typeof previousSummary?.[key] === 'number' ? previousSummary[key] : undefined;
|
|
2886
4125
|
const currentValue = typeof currentSummary?.[key] === 'number' ? currentSummary[key] : undefined;
|
|
2887
4126
|
if (previousValue !== currentValue && currentValue !== undefined) {
|
|
@@ -2910,13 +4149,13 @@ function createPageChangeSummary(previousCapture, currentCapture) {
|
|
|
2910
4149
|
}
|
|
2911
4150
|
function resolveInteractiveKinds(value) {
|
|
2912
4151
|
if (!Array.isArray(value) || value.length === 0) {
|
|
2913
|
-
return ['buttons', 'inputs', 'modals', 'focused'];
|
|
4152
|
+
return ['buttons', 'links', 'inputs', 'modals', 'focused'];
|
|
2914
4153
|
}
|
|
2915
|
-
const allowed = new Set(['buttons', 'inputs', 'modals', 'focused']);
|
|
4154
|
+
const allowed = new Set(['buttons', 'links', 'inputs', 'modals', 'focused']);
|
|
2916
4155
|
const kinds = value
|
|
2917
4156
|
.filter((entry) => typeof entry === 'string' && allowed.has(entry))
|
|
2918
4157
|
.map((entry) => entry);
|
|
2919
|
-
return kinds.length > 0 ? Array.from(new Set(kinds)) : ['buttons', 'inputs', 'modals', 'focused'];
|
|
4158
|
+
return kinds.length > 0 ? Array.from(new Set(kinds)) : ['buttons', 'links', 'inputs', 'modals', 'focused'];
|
|
2920
4159
|
}
|
|
2921
4160
|
function collectInteractiveElementRefs(payload, kinds, maxItems) {
|
|
2922
4161
|
const refs = [];
|
|
@@ -3036,132 +4275,1345 @@ async function waitForPageStateCondition(sessionId, input, capturePageState) {
|
|
|
3036
4275
|
const { lastCapture: _lastCapture, ...waited } = detailed;
|
|
3037
4276
|
return waited;
|
|
3038
4277
|
}
|
|
3039
|
-
function
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
.join(' ')
|
|
3043
|
-
.trim();
|
|
3044
|
-
}
|
|
3045
|
-
function describeWorkflowTargetCandidate(item) {
|
|
3046
|
-
return {
|
|
3047
|
-
text: candidateTextForWorkflowTarget(item) || undefined,
|
|
3048
|
-
testId: typeof item.testId === 'string' ? item.testId : undefined,
|
|
3049
|
-
selector: typeof item.selector === 'string' ? item.selector : undefined,
|
|
3050
|
-
tagName: typeof item.tagName === 'string' ? item.tagName : undefined,
|
|
3051
|
-
type: typeof item.type === 'string' ? item.type : undefined,
|
|
3052
|
-
disabled: typeof item.disabled === 'boolean' ? item.disabled : undefined,
|
|
3053
|
-
selected: typeof item.selected === 'boolean' ? item.selected : undefined,
|
|
3054
|
-
};
|
|
3055
|
-
}
|
|
3056
|
-
function pickWorkflowTargetItems(payload, scope) {
|
|
3057
|
-
if (scope) {
|
|
3058
|
-
return pickPageStateScopeItems(payload, scope);
|
|
3059
|
-
}
|
|
3060
|
-
return [
|
|
3061
|
-
...pickPageStateScopeItems(payload, 'buttons'),
|
|
3062
|
-
...pickPageStateScopeItems(payload, 'inputs'),
|
|
3063
|
-
...pickPageStateScopeItems(payload, 'modals'),
|
|
3064
|
-
...pickPageStateScopeItems(payload, 'focused'),
|
|
3065
|
-
];
|
|
3066
|
-
}
|
|
3067
|
-
function matchesWorkflowActionTarget(item, target) {
|
|
3068
|
-
return (equalsNormalized(item.testId, target.testId)
|
|
3069
|
-
&& includesNormalized(item.text, target.textContains)
|
|
3070
|
-
&& includesNormalized(item.label, target.labelContains)
|
|
3071
|
-
&& includesNormalized(item.title, target.titleContains)
|
|
3072
|
-
&& equalsNormalized(item.tagName, target.tagName)
|
|
3073
|
-
&& equalsNormalized(item.type, target.type)
|
|
3074
|
-
&& equalsOptionalBoolean(item.disabled, target.disabled)
|
|
3075
|
-
&& equalsOptionalBoolean(item.selected, target.selected)
|
|
3076
|
-
&& equalsOptionalBoolean(item.pressed, target.pressed)
|
|
3077
|
-
&& equalsOptionalBoolean(item.expanded, target.expanded)
|
|
3078
|
-
&& equalsOptionalBoolean(item.readOnly, target.readOnly)
|
|
3079
|
-
&& equalsOptionalBoolean(item.required, target.requiredField)
|
|
3080
|
-
&& (typeof item.elementRef === 'string' || typeof item.selector === 'string'));
|
|
3081
|
-
}
|
|
3082
|
-
function summarizeWorkflowTargetMatcher(target) {
|
|
3083
|
-
return {
|
|
3084
|
-
scope: target.scope,
|
|
3085
|
-
selector: target.selector,
|
|
3086
|
-
elementRef: target.elementRef,
|
|
3087
|
-
testId: target.testId,
|
|
3088
|
-
textContains: target.textContains,
|
|
3089
|
-
labelContains: target.labelContains,
|
|
3090
|
-
titleContains: target.titleContains,
|
|
3091
|
-
tagName: target.tagName,
|
|
3092
|
-
type: target.type,
|
|
3093
|
-
disabled: target.disabled,
|
|
3094
|
-
selected: target.selected,
|
|
3095
|
-
pressed: target.pressed,
|
|
3096
|
-
expanded: target.expanded,
|
|
3097
|
-
readOnly: target.readOnly,
|
|
3098
|
-
requiredField: target.requiredField,
|
|
3099
|
-
};
|
|
3100
|
-
}
|
|
3101
|
-
async function resolveWorkflowActionTarget(sessionId, target, capturePageState, existingCapture) {
|
|
3102
|
-
if (!target) {
|
|
3103
|
-
return {
|
|
3104
|
-
resolution: {
|
|
3105
|
-
strategy: 'none',
|
|
3106
|
-
},
|
|
3107
|
-
};
|
|
4278
|
+
function compileWaitRegex(value, fieldName) {
|
|
4279
|
+
if (!value) {
|
|
4280
|
+
return undefined;
|
|
3108
4281
|
}
|
|
3109
|
-
|
|
3110
|
-
return
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
tabId: target.tabId,
|
|
3115
|
-
frameId: target.frameId,
|
|
3116
|
-
url: target.url,
|
|
3117
|
-
},
|
|
3118
|
-
resolution: {
|
|
3119
|
-
strategy: target.elementRef ? 'elementRef' : 'selector',
|
|
3120
|
-
matcher: summarizeWorkflowTargetMatcher(target),
|
|
3121
|
-
},
|
|
3122
|
-
};
|
|
4282
|
+
try {
|
|
4283
|
+
return new RegExp(value);
|
|
4284
|
+
}
|
|
4285
|
+
catch {
|
|
4286
|
+
throw new Error(`${fieldName} must be a valid regular expression`);
|
|
3123
4287
|
}
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
4288
|
+
}
|
|
4289
|
+
function matchesUrlWait(url, wait) {
|
|
4290
|
+
return matchesUrlPredicates(url, {
|
|
4291
|
+
exactUrl: wait.exactUrl,
|
|
4292
|
+
urlContains: wait.urlContains,
|
|
4293
|
+
urlRegex: wait.urlRegex,
|
|
3130
4294
|
});
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
if (
|
|
3134
|
-
|
|
3135
|
-
matcher: summarizeWorkflowTargetMatcher(target),
|
|
3136
|
-
searchedScope: target.scope ?? 'all-interactive',
|
|
3137
|
-
sampledCandidates: pickWorkflowTargetItems(capture.payload, target.scope)
|
|
3138
|
-
.slice(0, 5)
|
|
3139
|
-
.map((item) => describeWorkflowTargetCandidate(item)),
|
|
3140
|
-
});
|
|
4295
|
+
}
|
|
4296
|
+
function matchesUrlPredicates(url, predicates) {
|
|
4297
|
+
if (typeof url !== 'string') {
|
|
4298
|
+
return false;
|
|
3141
4299
|
}
|
|
3142
|
-
if (
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
4300
|
+
if (predicates.exactUrl && url !== predicates.exactUrl) {
|
|
4301
|
+
return false;
|
|
4302
|
+
}
|
|
4303
|
+
if (predicates.urlContains && !url.includes(predicates.urlContains)) {
|
|
4304
|
+
return false;
|
|
4305
|
+
}
|
|
4306
|
+
const regex = compileWaitRegex(predicates.urlRegex, predicates.regexFieldName ?? 'urlRegex');
|
|
4307
|
+
if (regex && !regex.test(url)) {
|
|
4308
|
+
return false;
|
|
4309
|
+
}
|
|
4310
|
+
return true;
|
|
4311
|
+
}
|
|
4312
|
+
function resolveAutomationWaitSinceTs(value) {
|
|
4313
|
+
return resolveOptionalTimestamp(value) ?? Math.max(0, Date.now() - DEFAULT_AUTOMATION_WAIT_LOOKBACK_MS);
|
|
4314
|
+
}
|
|
4315
|
+
function isElementMissingError(error) {
|
|
4316
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
4317
|
+
return /no element found for selector/i.test(message);
|
|
4318
|
+
}
|
|
4319
|
+
async function captureSelectorState(captureClient, sessionId, selector, frameId = 0) {
|
|
4320
|
+
try {
|
|
4321
|
+
const [styleCapture, layoutCapture] = await Promise.all([
|
|
4322
|
+
executeLiveCapture(captureClient, sessionId, 'CAPTURE_COMPUTED_STYLES', { selector, frameId, properties: ['display', 'visibility', 'opacity'] }, 3_000),
|
|
4323
|
+
executeLiveCapture(captureClient, sessionId, 'CAPTURE_LAYOUT_METRICS', { selector, frameId }, 3_000),
|
|
4324
|
+
]);
|
|
4325
|
+
const stylePayload = ensureCaptureSuccess(styleCapture, sessionId);
|
|
4326
|
+
const layoutPayload = ensureCaptureSuccess(layoutCapture, sessionId);
|
|
4327
|
+
const properties = isRecord(stylePayload.properties) ? stylePayload.properties : {};
|
|
4328
|
+
const element = isRecord(layoutPayload.element) ? layoutPayload.element : {};
|
|
4329
|
+
const width = typeof element.width === 'number' ? element.width : 0;
|
|
4330
|
+
const height = typeof element.height === 'number' ? element.height : 0;
|
|
4331
|
+
const display = typeof properties.display === 'string' ? properties.display : undefined;
|
|
4332
|
+
const visibility = typeof properties.visibility === 'string' ? properties.visibility : undefined;
|
|
4333
|
+
const opacityText = typeof properties.opacity === 'string' ? properties.opacity : undefined;
|
|
4334
|
+
const opacity = opacityText !== undefined ? Number.parseFloat(opacityText) : undefined;
|
|
4335
|
+
const visible = display !== 'none'
|
|
4336
|
+
&& visibility !== 'hidden'
|
|
4337
|
+
&& visibility !== 'collapse'
|
|
4338
|
+
&& opacity !== 0
|
|
4339
|
+
&& width > 0
|
|
4340
|
+
&& height > 0;
|
|
4341
|
+
return {
|
|
4342
|
+
selector,
|
|
4343
|
+
frameId,
|
|
4344
|
+
attached: true,
|
|
4345
|
+
visible,
|
|
4346
|
+
styles: properties,
|
|
4347
|
+
element,
|
|
4348
|
+
viewport: layoutPayload.viewport,
|
|
4349
|
+
};
|
|
4350
|
+
}
|
|
4351
|
+
catch (error) {
|
|
4352
|
+
if (isElementMissingError(error)) {
|
|
4353
|
+
return {
|
|
4354
|
+
selector,
|
|
4355
|
+
frameId,
|
|
4356
|
+
attached: false,
|
|
4357
|
+
visible: false,
|
|
4358
|
+
missing: true,
|
|
4359
|
+
message: error instanceof Error ? error.message : String(error),
|
|
4360
|
+
};
|
|
4361
|
+
}
|
|
4362
|
+
throw error;
|
|
4363
|
+
}
|
|
4364
|
+
}
|
|
4365
|
+
function selectorStateMatches(state, expectedState) {
|
|
4366
|
+
const attached = state.attached === true;
|
|
4367
|
+
const visible = state.visible === true;
|
|
4368
|
+
switch (expectedState) {
|
|
4369
|
+
case 'attached':
|
|
4370
|
+
return attached;
|
|
4371
|
+
case 'detached':
|
|
4372
|
+
return !attached;
|
|
4373
|
+
case 'visible':
|
|
4374
|
+
return attached && visible;
|
|
4375
|
+
case 'hidden':
|
|
4376
|
+
return !attached || !visible;
|
|
4377
|
+
}
|
|
4378
|
+
}
|
|
4379
|
+
async function waitForUrlCondition(sessionId, wait, capturePageState) {
|
|
4380
|
+
const timeoutMs = resolveTimeoutMs(wait.timeoutMs, 5_000, MAX_NETWORK_POLL_TIMEOUT_MS);
|
|
4381
|
+
const pollIntervalMs = resolveDurationMs(wait.pollIntervalMs, 100, 5_000);
|
|
4382
|
+
const startedAt = Date.now();
|
|
4383
|
+
const deadline = startedAt + timeoutMs;
|
|
4384
|
+
let attempts = 0;
|
|
4385
|
+
let lastPage;
|
|
4386
|
+
while (Date.now() <= deadline) {
|
|
4387
|
+
attempts += 1;
|
|
4388
|
+
const capture = await capturePageState(sessionId, {
|
|
4389
|
+
includeButtons: false,
|
|
4390
|
+
includeLinks: false,
|
|
4391
|
+
includeInputs: false,
|
|
4392
|
+
includeModals: false,
|
|
4393
|
+
maxItems: 1,
|
|
4394
|
+
maxTextLength: 40,
|
|
3147
4395
|
});
|
|
4396
|
+
lastPage = {
|
|
4397
|
+
url: capture.payload.url,
|
|
4398
|
+
title: capture.payload.title,
|
|
4399
|
+
language: capture.payload.language,
|
|
4400
|
+
viewport: capture.payload.viewport,
|
|
4401
|
+
};
|
|
4402
|
+
if (matchesUrlWait(capture.payload.url, wait)) {
|
|
4403
|
+
return {
|
|
4404
|
+
waitKind: 'url',
|
|
4405
|
+
matched: true,
|
|
4406
|
+
waitedMs: Date.now() - startedAt,
|
|
4407
|
+
attempts,
|
|
4408
|
+
timeoutMs,
|
|
4409
|
+
pollIntervalMs,
|
|
4410
|
+
evidence: { page: lastPage },
|
|
4411
|
+
};
|
|
4412
|
+
}
|
|
4413
|
+
await sleep(pollIntervalMs);
|
|
4414
|
+
}
|
|
4415
|
+
return {
|
|
4416
|
+
waitKind: 'url',
|
|
4417
|
+
matched: false,
|
|
4418
|
+
waitedMs: Date.now() - startedAt,
|
|
4419
|
+
attempts,
|
|
4420
|
+
timeoutMs,
|
|
4421
|
+
pollIntervalMs,
|
|
4422
|
+
evidence: {
|
|
4423
|
+
page: lastPage,
|
|
4424
|
+
expected: wait,
|
|
4425
|
+
timeoutDiagnostics: buildWaitTimeoutDiagnostics({
|
|
4426
|
+
waitKind: 'url',
|
|
4427
|
+
timeoutMs,
|
|
4428
|
+
waitedMs: Date.now() - startedAt,
|
|
4429
|
+
attempts,
|
|
4430
|
+
pollIntervalMs,
|
|
4431
|
+
matcherSummary: {
|
|
4432
|
+
exactUrl: wait.exactUrl,
|
|
4433
|
+
urlContains: wait.urlContains,
|
|
4434
|
+
urlRegex: wait.urlRegex,
|
|
4435
|
+
},
|
|
4436
|
+
lastObserved: lastPage,
|
|
4437
|
+
}),
|
|
4438
|
+
},
|
|
4439
|
+
error: {
|
|
4440
|
+
code: 'url_wait_timeout',
|
|
4441
|
+
message: 'Timed out waiting for the page URL to match the requested condition.',
|
|
4442
|
+
},
|
|
4443
|
+
};
|
|
4444
|
+
}
|
|
4445
|
+
function pageReadyStateMatches(readyState, expectedState) {
|
|
4446
|
+
if (readyState !== 'loading' && readyState !== 'interactive' && readyState !== 'complete') {
|
|
4447
|
+
return false;
|
|
4448
|
+
}
|
|
4449
|
+
if (expectedState === 'domcontentloaded') {
|
|
4450
|
+
return readyState === 'interactive' || readyState === 'complete';
|
|
4451
|
+
}
|
|
4452
|
+
return readyState === 'complete';
|
|
4453
|
+
}
|
|
4454
|
+
async function waitForLoadStateCondition(sessionId, wait, capturePageState) {
|
|
4455
|
+
const timeoutMs = resolveTimeoutMs(wait.timeoutMs, 5_000, MAX_NETWORK_POLL_TIMEOUT_MS);
|
|
4456
|
+
const pollIntervalMs = resolveDurationMs(wait.pollIntervalMs, 100, 5_000);
|
|
4457
|
+
const expectedState = wait.state ?? 'load';
|
|
4458
|
+
const startedAt = Date.now();
|
|
4459
|
+
const deadline = startedAt + timeoutMs;
|
|
4460
|
+
let attempts = 0;
|
|
4461
|
+
let lastPage;
|
|
4462
|
+
while (Date.now() <= deadline) {
|
|
4463
|
+
attempts += 1;
|
|
4464
|
+
const capture = await capturePageState(sessionId, {
|
|
4465
|
+
includeButtons: false,
|
|
4466
|
+
includeLinks: false,
|
|
4467
|
+
includeInputs: false,
|
|
4468
|
+
includeModals: false,
|
|
4469
|
+
maxItems: 1,
|
|
4470
|
+
maxTextLength: 40,
|
|
4471
|
+
});
|
|
4472
|
+
lastPage = {
|
|
4473
|
+
url: capture.payload.url,
|
|
4474
|
+
title: capture.payload.title,
|
|
4475
|
+
readyState: capture.payload.readyState,
|
|
4476
|
+
language: capture.payload.language,
|
|
4477
|
+
viewport: capture.payload.viewport,
|
|
4478
|
+
};
|
|
4479
|
+
const urlMatches = matchesUrlPredicates(typeof capture.payload.url === 'string' ? capture.payload.url : undefined, {
|
|
4480
|
+
exactUrl: wait.exactUrl,
|
|
4481
|
+
urlContains: wait.urlContains,
|
|
4482
|
+
urlRegex: wait.urlRegex,
|
|
4483
|
+
});
|
|
4484
|
+
if (urlMatches && pageReadyStateMatches(capture.payload.readyState, expectedState)) {
|
|
4485
|
+
return {
|
|
4486
|
+
waitKind: 'load_state',
|
|
4487
|
+
matched: true,
|
|
4488
|
+
waitedMs: Date.now() - startedAt,
|
|
4489
|
+
attempts,
|
|
4490
|
+
timeoutMs,
|
|
4491
|
+
pollIntervalMs,
|
|
4492
|
+
evidence: {
|
|
4493
|
+
state: expectedState,
|
|
4494
|
+
page: lastPage,
|
|
4495
|
+
},
|
|
4496
|
+
};
|
|
4497
|
+
}
|
|
4498
|
+
await sleep(pollIntervalMs);
|
|
4499
|
+
}
|
|
4500
|
+
return {
|
|
4501
|
+
waitKind: 'load_state',
|
|
4502
|
+
matched: false,
|
|
4503
|
+
waitedMs: Date.now() - startedAt,
|
|
4504
|
+
attempts,
|
|
4505
|
+
timeoutMs,
|
|
4506
|
+
pollIntervalMs,
|
|
4507
|
+
evidence: {
|
|
4508
|
+
state: expectedState,
|
|
4509
|
+
page: lastPage,
|
|
4510
|
+
expected: wait,
|
|
4511
|
+
timeoutDiagnostics: buildWaitTimeoutDiagnostics({
|
|
4512
|
+
waitKind: 'load_state',
|
|
4513
|
+
timeoutMs,
|
|
4514
|
+
waitedMs: Date.now() - startedAt,
|
|
4515
|
+
attempts,
|
|
4516
|
+
pollIntervalMs,
|
|
4517
|
+
matcherSummary: {
|
|
4518
|
+
state: expectedState,
|
|
4519
|
+
exactUrl: wait.exactUrl,
|
|
4520
|
+
urlContains: wait.urlContains,
|
|
4521
|
+
urlRegex: wait.urlRegex,
|
|
4522
|
+
},
|
|
4523
|
+
lastObserved: lastPage,
|
|
4524
|
+
}),
|
|
4525
|
+
},
|
|
4526
|
+
error: {
|
|
4527
|
+
code: 'load_state_wait_timeout',
|
|
4528
|
+
message: `Timed out waiting for page load state "${expectedState}".`,
|
|
4529
|
+
},
|
|
4530
|
+
};
|
|
4531
|
+
}
|
|
4532
|
+
async function waitForSelectorStateCondition(sessionId, wait, captureClient) {
|
|
4533
|
+
const timeoutMs = resolveTimeoutMs(wait.timeoutMs, 5_000, MAX_NETWORK_POLL_TIMEOUT_MS);
|
|
4534
|
+
const pollIntervalMs = resolveDurationMs(wait.pollIntervalMs, 100, 5_000);
|
|
4535
|
+
const expectedState = wait.state ?? 'visible';
|
|
4536
|
+
const startedAt = Date.now();
|
|
4537
|
+
const deadline = startedAt + timeoutMs;
|
|
4538
|
+
let attempts = 0;
|
|
4539
|
+
let lastState;
|
|
4540
|
+
while (Date.now() <= deadline) {
|
|
4541
|
+
attempts += 1;
|
|
4542
|
+
lastState = await captureSelectorState(captureClient, sessionId, wait.selector, wait.frameId);
|
|
4543
|
+
if (selectorStateMatches(lastState, expectedState)) {
|
|
4544
|
+
return {
|
|
4545
|
+
waitKind: 'selector_state',
|
|
4546
|
+
matched: true,
|
|
4547
|
+
waitedMs: Date.now() - startedAt,
|
|
4548
|
+
attempts,
|
|
4549
|
+
timeoutMs,
|
|
4550
|
+
pollIntervalMs,
|
|
4551
|
+
evidence: {
|
|
4552
|
+
selector: wait.selector,
|
|
4553
|
+
state: expectedState,
|
|
4554
|
+
selectorState: lastState,
|
|
4555
|
+
},
|
|
4556
|
+
};
|
|
4557
|
+
}
|
|
4558
|
+
await sleep(pollIntervalMs);
|
|
4559
|
+
}
|
|
4560
|
+
return {
|
|
4561
|
+
waitKind: 'selector_state',
|
|
4562
|
+
matched: false,
|
|
4563
|
+
waitedMs: Date.now() - startedAt,
|
|
4564
|
+
attempts,
|
|
4565
|
+
timeoutMs,
|
|
4566
|
+
pollIntervalMs,
|
|
4567
|
+
evidence: {
|
|
4568
|
+
selector: wait.selector,
|
|
4569
|
+
state: expectedState,
|
|
4570
|
+
selectorState: lastState,
|
|
4571
|
+
timeoutDiagnostics: buildWaitTimeoutDiagnostics({
|
|
4572
|
+
waitKind: 'selector_state',
|
|
4573
|
+
timeoutMs,
|
|
4574
|
+
waitedMs: Date.now() - startedAt,
|
|
4575
|
+
attempts,
|
|
4576
|
+
pollIntervalMs,
|
|
4577
|
+
matcherSummary: {
|
|
4578
|
+
selector: wait.selector,
|
|
4579
|
+
state: expectedState,
|
|
4580
|
+
frameId: wait.frameId,
|
|
4581
|
+
},
|
|
4582
|
+
lastObserved: lastState,
|
|
4583
|
+
}),
|
|
4584
|
+
},
|
|
4585
|
+
error: {
|
|
4586
|
+
code: 'selector_state_wait_timeout',
|
|
4587
|
+
message: `Timed out waiting for selector "${wait.selector}" to become ${expectedState}.`,
|
|
4588
|
+
},
|
|
4589
|
+
};
|
|
4590
|
+
}
|
|
4591
|
+
async function waitForConsoleCondition(sessionId, wait, captureClient) {
|
|
4592
|
+
const timeoutMs = resolveTimeoutMs(wait.timeoutMs, 5_000, MAX_NETWORK_POLL_TIMEOUT_MS);
|
|
4593
|
+
const pollIntervalMs = resolveDurationMs(wait.pollIntervalMs, 100, 5_000);
|
|
4594
|
+
const levels = resolveLiveConsoleLevels(wait.levels);
|
|
4595
|
+
const contains = normalizeOptionalString(wait.contains);
|
|
4596
|
+
const sinceTs = resolveAutomationWaitSinceTs(wait.sinceTs);
|
|
4597
|
+
const includeRuntimeErrors = wait.includeRuntimeErrors !== false;
|
|
4598
|
+
const startedAt = Date.now();
|
|
4599
|
+
const deadline = startedAt + timeoutMs;
|
|
4600
|
+
let attempts = 0;
|
|
4601
|
+
let lastLogs = [];
|
|
4602
|
+
while (Date.now() <= deadline) {
|
|
4603
|
+
attempts += 1;
|
|
4604
|
+
const capture = await executeLiveCapture(captureClient, sessionId, 'CAPTURE_GET_LIVE_CONSOLE_LOGS', {
|
|
4605
|
+
levels,
|
|
4606
|
+
contains,
|
|
4607
|
+
sinceTs,
|
|
4608
|
+
includeRuntimeErrors,
|
|
4609
|
+
limit: 10,
|
|
4610
|
+
}, 3_000);
|
|
4611
|
+
const payload = ensureCaptureSuccess(capture, sessionId);
|
|
4612
|
+
lastLogs = asRecordArray(payload.logs);
|
|
4613
|
+
if (lastLogs.length > 0) {
|
|
4614
|
+
return {
|
|
4615
|
+
waitKind: 'console',
|
|
4616
|
+
matched: true,
|
|
4617
|
+
waitedMs: Date.now() - startedAt,
|
|
4618
|
+
attempts,
|
|
4619
|
+
timeoutMs,
|
|
4620
|
+
pollIntervalMs,
|
|
4621
|
+
evidence: {
|
|
4622
|
+
filters: { levels, contains, sinceTs, includeRuntimeErrors },
|
|
4623
|
+
logs: lastLogs.slice(0, 5).map((log) => mapLiveConsoleLogRecord(log, 'compact')),
|
|
4624
|
+
},
|
|
4625
|
+
};
|
|
4626
|
+
}
|
|
4627
|
+
await sleep(pollIntervalMs);
|
|
3148
4628
|
}
|
|
3149
|
-
const candidate = candidates[0];
|
|
3150
4629
|
return {
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
4630
|
+
waitKind: 'console',
|
|
4631
|
+
matched: false,
|
|
4632
|
+
waitedMs: Date.now() - startedAt,
|
|
4633
|
+
attempts,
|
|
4634
|
+
timeoutMs,
|
|
4635
|
+
pollIntervalMs,
|
|
4636
|
+
evidence: {
|
|
4637
|
+
filters: { levels, contains, sinceTs, includeRuntimeErrors },
|
|
4638
|
+
sampledLogs: lastLogs.slice(0, 5).map((log) => mapLiveConsoleLogRecord(log, 'compact')),
|
|
4639
|
+
timeoutDiagnostics: buildWaitTimeoutDiagnostics({
|
|
4640
|
+
waitKind: 'console',
|
|
4641
|
+
timeoutMs,
|
|
4642
|
+
waitedMs: Date.now() - startedAt,
|
|
4643
|
+
attempts,
|
|
4644
|
+
pollIntervalMs,
|
|
4645
|
+
matcherSummary: { levels, contains, sinceTs, includeRuntimeErrors },
|
|
4646
|
+
lastObserved: lastLogs[0],
|
|
4647
|
+
candidateCount: lastLogs.length,
|
|
4648
|
+
sampledCandidates: lastLogs.slice(0, 5).map((log) => mapLiveConsoleLogRecord(log, 'compact')),
|
|
4649
|
+
}),
|
|
4650
|
+
},
|
|
4651
|
+
error: {
|
|
4652
|
+
code: 'console_wait_timeout',
|
|
4653
|
+
message: 'Timed out waiting for a matching live console log.',
|
|
4654
|
+
},
|
|
4655
|
+
};
|
|
4656
|
+
}
|
|
4657
|
+
async function waitForDialogCondition(sessionId, wait, captureClient) {
|
|
4658
|
+
const timeoutMs = resolveTimeoutMs(wait.timeoutMs, 5_000, MAX_NETWORK_POLL_TIMEOUT_MS);
|
|
4659
|
+
const startedAt = Date.now();
|
|
4660
|
+
const capture = await executeLiveCapture(captureClient, sessionId, 'CAPTURE_WAIT_FOR_DIALOG', {
|
|
4661
|
+
type: wait.type,
|
|
4662
|
+
messageContains: wait.messageContains,
|
|
4663
|
+
urlContains: wait.urlContains,
|
|
4664
|
+
action: wait.action,
|
|
4665
|
+
promptText: wait.promptText,
|
|
4666
|
+
tabId: wait.tabId,
|
|
4667
|
+
timeoutMs,
|
|
4668
|
+
}, timeoutMs + 2_000);
|
|
4669
|
+
const payload = ensureCaptureSuccess(capture, sessionId);
|
|
4670
|
+
const matched = payload.matched === true;
|
|
4671
|
+
return {
|
|
4672
|
+
waitKind: 'dialog',
|
|
4673
|
+
matched,
|
|
4674
|
+
waitedMs: Date.now() - startedAt,
|
|
4675
|
+
attempts: 1,
|
|
4676
|
+
timeoutMs,
|
|
4677
|
+
pollIntervalMs: timeoutMs,
|
|
4678
|
+
evidence: {
|
|
4679
|
+
filters: {
|
|
4680
|
+
type: wait.type,
|
|
4681
|
+
messageContains: wait.messageContains,
|
|
4682
|
+
urlContains: wait.urlContains,
|
|
4683
|
+
action: wait.action,
|
|
4684
|
+
tabId: wait.tabId,
|
|
4685
|
+
},
|
|
4686
|
+
dialog: matched ? payload : undefined,
|
|
4687
|
+
expected: matched ? undefined : payload.expected ?? wait,
|
|
4688
|
+
timeoutDiagnostics: matched
|
|
4689
|
+
? undefined
|
|
4690
|
+
: buildWaitTimeoutDiagnostics({
|
|
4691
|
+
waitKind: 'dialog',
|
|
4692
|
+
timeoutMs,
|
|
4693
|
+
waitedMs: Date.now() - startedAt,
|
|
4694
|
+
attempts: 1,
|
|
4695
|
+
pollIntervalMs: timeoutMs,
|
|
4696
|
+
matcherSummary: {
|
|
4697
|
+
type: wait.type,
|
|
4698
|
+
messageContains: wait.messageContains,
|
|
4699
|
+
urlContains: wait.urlContains,
|
|
4700
|
+
action: wait.action,
|
|
4701
|
+
tabId: wait.tabId,
|
|
4702
|
+
},
|
|
4703
|
+
lastObserved: payload.lastObserved,
|
|
4704
|
+
candidateCount: typeof payload.observedCount === 'number' ? payload.observedCount : undefined,
|
|
4705
|
+
}),
|
|
4706
|
+
},
|
|
4707
|
+
error: matched
|
|
4708
|
+
? undefined
|
|
4709
|
+
: {
|
|
4710
|
+
code: 'dialog_wait_timeout',
|
|
4711
|
+
message: 'Timed out waiting for a matching JavaScript dialog.',
|
|
4712
|
+
},
|
|
4713
|
+
};
|
|
4714
|
+
}
|
|
4715
|
+
async function waitForStableLayoutCondition(sessionId, wait, captureClient) {
|
|
4716
|
+
const timeoutMs = resolveTimeoutMs(wait.timeoutMs, 5_000, MAX_NETWORK_POLL_TIMEOUT_MS);
|
|
4717
|
+
const stableMs = wait.stableMs;
|
|
4718
|
+
const pollIntervalMs = resolveDurationMs(wait.pollIntervalMs, 100, 5_000);
|
|
4719
|
+
const startedAt = Date.now();
|
|
4720
|
+
const capture = await executeLiveCapture(captureClient, sessionId, 'CAPTURE_WAIT_FOR_STABLE_LAYOUT', {
|
|
4721
|
+
selector: wait.selector,
|
|
4722
|
+
stableMs,
|
|
4723
|
+
tabId: wait.tabId,
|
|
4724
|
+
timeoutMs,
|
|
4725
|
+
pollIntervalMs,
|
|
4726
|
+
}, timeoutMs + 2_000);
|
|
4727
|
+
const payload = ensureCaptureSuccess(capture, sessionId);
|
|
4728
|
+
const matched = payload.matched === true;
|
|
4729
|
+
return {
|
|
4730
|
+
waitKind: 'stable_layout',
|
|
4731
|
+
matched,
|
|
4732
|
+
waitedMs: Date.now() - startedAt,
|
|
4733
|
+
attempts: 1,
|
|
4734
|
+
timeoutMs,
|
|
4735
|
+
pollIntervalMs,
|
|
4736
|
+
evidence: {
|
|
4737
|
+
filters: {
|
|
4738
|
+
selector: wait.selector,
|
|
4739
|
+
stableMs,
|
|
4740
|
+
tabId: wait.tabId,
|
|
4741
|
+
},
|
|
4742
|
+
layout: payload,
|
|
4743
|
+
timeoutDiagnostics: matched
|
|
4744
|
+
? undefined
|
|
4745
|
+
: buildWaitTimeoutDiagnostics({
|
|
4746
|
+
waitKind: 'stable_layout',
|
|
4747
|
+
timeoutMs,
|
|
4748
|
+
waitedMs: Date.now() - startedAt,
|
|
4749
|
+
attempts: typeof payload.attempts === 'number' ? payload.attempts : 1,
|
|
4750
|
+
pollIntervalMs,
|
|
4751
|
+
matcherSummary: {
|
|
4752
|
+
selector: wait.selector,
|
|
4753
|
+
stableMs,
|
|
4754
|
+
tabId: wait.tabId,
|
|
4755
|
+
},
|
|
4756
|
+
lastObserved: payload.snapshot,
|
|
4757
|
+
}),
|
|
4758
|
+
},
|
|
4759
|
+
error: matched
|
|
4760
|
+
? undefined
|
|
4761
|
+
: {
|
|
4762
|
+
code: 'stable_layout_wait_timeout',
|
|
4763
|
+
message: 'Timed out waiting for layout to stay stable.',
|
|
4764
|
+
},
|
|
4765
|
+
};
|
|
4766
|
+
}
|
|
4767
|
+
function mapNavigationWaitEvent(row) {
|
|
4768
|
+
const payload = readJsonPayload(row.payload_json);
|
|
4769
|
+
return {
|
|
4770
|
+
eventId: row.event_id,
|
|
4771
|
+
sessionId: row.session_id,
|
|
4772
|
+
timestamp: row.ts,
|
|
4773
|
+
tabId: row.tab_id ?? undefined,
|
|
4774
|
+
origin: row.origin ?? undefined,
|
|
4775
|
+
url: resolveLastUrl(payload),
|
|
4776
|
+
from: typeof payload.from === 'string' ? payload.from : undefined,
|
|
4777
|
+
trigger: typeof payload.trigger === 'string' ? payload.trigger : undefined,
|
|
4778
|
+
payload,
|
|
4779
|
+
};
|
|
4780
|
+
}
|
|
4781
|
+
function navigationEventMatches(row, wait) {
|
|
4782
|
+
const payload = readJsonPayload(row.payload_json);
|
|
4783
|
+
const toUrl = resolveLastUrl(payload);
|
|
4784
|
+
const fromUrl = typeof payload.from === 'string' ? payload.from : undefined;
|
|
4785
|
+
const trigger = typeof payload.trigger === 'string' ? payload.trigger : undefined;
|
|
4786
|
+
if (!matchesUrlPredicates(toUrl, {
|
|
4787
|
+
exactUrl: wait.exactUrl,
|
|
4788
|
+
urlContains: wait.urlContains,
|
|
4789
|
+
urlRegex: wait.urlRegex,
|
|
4790
|
+
})) {
|
|
4791
|
+
return false;
|
|
4792
|
+
}
|
|
4793
|
+
if (wait.fromUrlContains || wait.fromUrlRegex) {
|
|
4794
|
+
if (!matchesUrlPredicates(fromUrl, {
|
|
4795
|
+
urlContains: wait.fromUrlContains,
|
|
4796
|
+
urlRegex: wait.fromUrlRegex,
|
|
4797
|
+
regexFieldName: 'fromUrlRegex',
|
|
4798
|
+
})) {
|
|
4799
|
+
return false;
|
|
4800
|
+
}
|
|
4801
|
+
}
|
|
4802
|
+
if (wait.trigger && trigger !== wait.trigger) {
|
|
4803
|
+
return false;
|
|
4804
|
+
}
|
|
4805
|
+
return true;
|
|
4806
|
+
}
|
|
4807
|
+
function queryNavigationWaitCandidates(db, options) {
|
|
4808
|
+
const where = ['session_id = ?', "type = 'nav'", 'ts >= ?'];
|
|
4809
|
+
const params = [options.sessionId, options.sinceTs];
|
|
4810
|
+
if (options.tabId !== undefined) {
|
|
4811
|
+
where.push('tab_id = ?');
|
|
4812
|
+
params.push(options.tabId);
|
|
4813
|
+
}
|
|
4814
|
+
return db.prepare(`SELECT event_id, session_id, ts, type, payload_json, tab_id, origin
|
|
4815
|
+
FROM events
|
|
4816
|
+
WHERE ${where.join(' AND ')}
|
|
4817
|
+
ORDER BY ts ASC
|
|
4818
|
+
LIMIT 200`).all(...params);
|
|
4819
|
+
}
|
|
4820
|
+
async function waitForNavigationCondition(sessionId, wait, db) {
|
|
4821
|
+
const timeoutMs = resolveTimeoutMs(wait.timeoutMs, 5_000, MAX_NETWORK_POLL_TIMEOUT_MS);
|
|
4822
|
+
const pollIntervalMs = resolveDurationMs(wait.pollIntervalMs, 100, 5_000);
|
|
4823
|
+
const sinceTs = resolveAutomationWaitSinceTs(wait.sinceTs);
|
|
4824
|
+
const tabId = resolveOptionalTabId(wait.tabId);
|
|
4825
|
+
const startedAt = Date.now();
|
|
4826
|
+
const deadline = startedAt + timeoutMs;
|
|
4827
|
+
let attempts = 0;
|
|
4828
|
+
let lastEvents = [];
|
|
4829
|
+
while (Date.now() <= deadline) {
|
|
4830
|
+
attempts += 1;
|
|
4831
|
+
lastEvents = queryNavigationWaitCandidates(db, { sessionId, sinceTs, tabId });
|
|
4832
|
+
const matched = lastEvents.find((row) => navigationEventMatches(row, wait));
|
|
4833
|
+
if (matched) {
|
|
4834
|
+
return {
|
|
4835
|
+
waitKind: 'navigation',
|
|
4836
|
+
matched: true,
|
|
4837
|
+
waitedMs: Date.now() - startedAt,
|
|
4838
|
+
attempts,
|
|
4839
|
+
timeoutMs,
|
|
4840
|
+
pollIntervalMs,
|
|
4841
|
+
evidence: {
|
|
4842
|
+
filters: {
|
|
4843
|
+
urlContains: wait.urlContains,
|
|
4844
|
+
urlRegex: wait.urlRegex,
|
|
4845
|
+
exactUrl: wait.exactUrl,
|
|
4846
|
+
fromUrlContains: wait.fromUrlContains,
|
|
4847
|
+
fromUrlRegex: wait.fromUrlRegex,
|
|
4848
|
+
trigger: wait.trigger,
|
|
4849
|
+
sinceTs,
|
|
4850
|
+
tabId,
|
|
4851
|
+
},
|
|
4852
|
+
navigation: mapNavigationWaitEvent(matched),
|
|
4853
|
+
},
|
|
4854
|
+
};
|
|
4855
|
+
}
|
|
4856
|
+
await sleep(pollIntervalMs);
|
|
4857
|
+
}
|
|
4858
|
+
return {
|
|
4859
|
+
waitKind: 'navigation',
|
|
4860
|
+
matched: false,
|
|
4861
|
+
waitedMs: Date.now() - startedAt,
|
|
4862
|
+
attempts,
|
|
4863
|
+
timeoutMs,
|
|
4864
|
+
pollIntervalMs,
|
|
4865
|
+
evidence: {
|
|
4866
|
+
filters: {
|
|
4867
|
+
urlContains: wait.urlContains,
|
|
4868
|
+
urlRegex: wait.urlRegex,
|
|
4869
|
+
exactUrl: wait.exactUrl,
|
|
4870
|
+
fromUrlContains: wait.fromUrlContains,
|
|
4871
|
+
fromUrlRegex: wait.fromUrlRegex,
|
|
4872
|
+
trigger: wait.trigger,
|
|
4873
|
+
sinceTs,
|
|
4874
|
+
tabId,
|
|
4875
|
+
},
|
|
4876
|
+
sampledEvents: lastEvents.slice(0, 5).map((row) => mapNavigationWaitEvent(row)),
|
|
4877
|
+
timeoutDiagnostics: buildWaitTimeoutDiagnostics({
|
|
4878
|
+
waitKind: 'navigation',
|
|
4879
|
+
timeoutMs,
|
|
4880
|
+
waitedMs: Date.now() - startedAt,
|
|
4881
|
+
attempts,
|
|
4882
|
+
pollIntervalMs,
|
|
4883
|
+
matcherSummary: {
|
|
4884
|
+
urlContains: wait.urlContains,
|
|
4885
|
+
urlRegex: wait.urlRegex,
|
|
4886
|
+
exactUrl: wait.exactUrl,
|
|
4887
|
+
fromUrlContains: wait.fromUrlContains,
|
|
4888
|
+
fromUrlRegex: wait.fromUrlRegex,
|
|
4889
|
+
trigger: wait.trigger,
|
|
4890
|
+
sinceTs,
|
|
4891
|
+
tabId,
|
|
4892
|
+
},
|
|
4893
|
+
lastObserved: lastEvents.length > 0 ? mapNavigationWaitEvent(lastEvents[lastEvents.length - 1]) : undefined,
|
|
4894
|
+
candidateCount: lastEvents.length,
|
|
4895
|
+
sampledCandidates: lastEvents.slice(0, 5).map((row) => mapNavigationWaitEvent(row)),
|
|
4896
|
+
}),
|
|
4897
|
+
},
|
|
4898
|
+
error: {
|
|
4899
|
+
code: 'navigation_wait_timeout',
|
|
4900
|
+
message: 'Timed out waiting for a matching navigation event.',
|
|
4901
|
+
},
|
|
4902
|
+
};
|
|
4903
|
+
}
|
|
4904
|
+
async function waitForNavigationLifecycleCondition(sessionId, wait, captureClient) {
|
|
4905
|
+
const timeoutMs = resolveTimeoutMs(wait.timeoutMs, 5_000, MAX_NETWORK_POLL_TIMEOUT_MS);
|
|
4906
|
+
const startedAt = Date.now();
|
|
4907
|
+
const capture = await executeLiveCapture(captureClient, sessionId, 'CAPTURE_WAIT_FOR_NAVIGATION_LIFECYCLE', {
|
|
4908
|
+
state: wait.state,
|
|
4909
|
+
urlContains: wait.urlContains,
|
|
4910
|
+
urlRegex: wait.urlRegex,
|
|
4911
|
+
exactUrl: wait.exactUrl,
|
|
4912
|
+
tabId: wait.tabId,
|
|
4913
|
+
timeoutMs,
|
|
4914
|
+
}, timeoutMs + 2_000);
|
|
4915
|
+
const payload = ensureCaptureSuccess(capture, sessionId);
|
|
4916
|
+
const matched = payload.matched === true;
|
|
4917
|
+
return {
|
|
4918
|
+
waitKind: 'navigation_lifecycle',
|
|
4919
|
+
matched,
|
|
4920
|
+
waitedMs: Date.now() - startedAt,
|
|
4921
|
+
attempts: 1,
|
|
4922
|
+
timeoutMs,
|
|
4923
|
+
pollIntervalMs: timeoutMs,
|
|
4924
|
+
evidence: {
|
|
4925
|
+
filters: {
|
|
4926
|
+
state: wait.state,
|
|
4927
|
+
urlContains: wait.urlContains,
|
|
4928
|
+
urlRegex: wait.urlRegex,
|
|
4929
|
+
exactUrl: wait.exactUrl,
|
|
4930
|
+
tabId: wait.tabId,
|
|
4931
|
+
},
|
|
4932
|
+
lifecycle: matched ? payload : undefined,
|
|
4933
|
+
expected: matched ? undefined : payload.expected ?? wait,
|
|
4934
|
+
timeoutDiagnostics: matched
|
|
4935
|
+
? undefined
|
|
4936
|
+
: buildWaitTimeoutDiagnostics({
|
|
4937
|
+
waitKind: 'navigation_lifecycle',
|
|
4938
|
+
timeoutMs,
|
|
4939
|
+
waitedMs: Date.now() - startedAt,
|
|
4940
|
+
attempts: 1,
|
|
4941
|
+
pollIntervalMs: timeoutMs,
|
|
4942
|
+
matcherSummary: {
|
|
4943
|
+
state: wait.state,
|
|
4944
|
+
urlContains: wait.urlContains,
|
|
4945
|
+
urlRegex: wait.urlRegex,
|
|
4946
|
+
exactUrl: wait.exactUrl,
|
|
4947
|
+
tabId: wait.tabId,
|
|
4948
|
+
},
|
|
4949
|
+
lastObserved: payload.lastObserved,
|
|
4950
|
+
candidateCount: typeof payload.observedEventCount === 'number' ? payload.observedEventCount : undefined,
|
|
4951
|
+
}),
|
|
4952
|
+
},
|
|
4953
|
+
error: matched
|
|
4954
|
+
? undefined
|
|
4955
|
+
: {
|
|
4956
|
+
code: 'navigation_lifecycle_wait_timeout',
|
|
4957
|
+
message: 'Timed out waiting for a matching navigation lifecycle event.',
|
|
4958
|
+
},
|
|
4959
|
+
};
|
|
4960
|
+
}
|
|
4961
|
+
async function waitForDownloadCondition(sessionId, wait, captureClient) {
|
|
4962
|
+
const timeoutMs = resolveTimeoutMs(wait.timeoutMs, 5_000, MAX_NETWORK_POLL_TIMEOUT_MS);
|
|
4963
|
+
const startedAt = Date.now();
|
|
4964
|
+
const capture = await executeLiveCapture(captureClient, sessionId, 'CAPTURE_WAIT_FOR_DOWNLOAD', {
|
|
4965
|
+
urlContains: wait.urlContains,
|
|
4966
|
+
urlRegex: wait.urlRegex,
|
|
4967
|
+
exactUrl: wait.exactUrl,
|
|
4968
|
+
filenameContains: wait.filenameContains,
|
|
4969
|
+
filenameRegex: wait.filenameRegex,
|
|
4970
|
+
state: wait.state,
|
|
4971
|
+
tabId: wait.tabId,
|
|
4972
|
+
timeoutMs,
|
|
4973
|
+
}, timeoutMs + 2_000);
|
|
4974
|
+
const payload = ensureCaptureSuccess(capture, sessionId);
|
|
4975
|
+
const matched = payload.matched === true;
|
|
4976
|
+
return {
|
|
4977
|
+
waitKind: 'download',
|
|
4978
|
+
matched,
|
|
4979
|
+
waitedMs: Date.now() - startedAt,
|
|
4980
|
+
attempts: 1,
|
|
4981
|
+
timeoutMs,
|
|
4982
|
+
pollIntervalMs: timeoutMs,
|
|
4983
|
+
evidence: {
|
|
4984
|
+
filters: {
|
|
4985
|
+
urlContains: wait.urlContains,
|
|
4986
|
+
urlRegex: wait.urlRegex,
|
|
4987
|
+
exactUrl: wait.exactUrl,
|
|
4988
|
+
filenameContains: wait.filenameContains,
|
|
4989
|
+
filenameRegex: wait.filenameRegex,
|
|
4990
|
+
state: wait.state,
|
|
4991
|
+
tabId: wait.tabId,
|
|
4992
|
+
},
|
|
4993
|
+
download: matched ? payload : undefined,
|
|
4994
|
+
expected: matched ? undefined : payload.expected ?? wait,
|
|
4995
|
+
timeoutDiagnostics: matched
|
|
4996
|
+
? undefined
|
|
4997
|
+
: buildWaitTimeoutDiagnostics({
|
|
4998
|
+
waitKind: 'download',
|
|
4999
|
+
timeoutMs,
|
|
5000
|
+
waitedMs: Date.now() - startedAt,
|
|
5001
|
+
attempts: 1,
|
|
5002
|
+
pollIntervalMs: timeoutMs,
|
|
5003
|
+
matcherSummary: {
|
|
5004
|
+
urlContains: wait.urlContains,
|
|
5005
|
+
urlRegex: wait.urlRegex,
|
|
5006
|
+
exactUrl: wait.exactUrl,
|
|
5007
|
+
filenameContains: wait.filenameContains,
|
|
5008
|
+
filenameRegex: wait.filenameRegex,
|
|
5009
|
+
state: wait.state,
|
|
5010
|
+
tabId: wait.tabId,
|
|
5011
|
+
},
|
|
5012
|
+
lastObserved: payload.lastObserved ?? payload.lastMatchedDownload,
|
|
5013
|
+
candidateCount: typeof payload.observedEventCount === 'number' ? payload.observedEventCount : undefined,
|
|
5014
|
+
}),
|
|
5015
|
+
},
|
|
5016
|
+
error: matched
|
|
5017
|
+
? undefined
|
|
5018
|
+
: {
|
|
5019
|
+
code: 'download_wait_timeout',
|
|
5020
|
+
message: 'Timed out waiting for a matching download.',
|
|
5021
|
+
},
|
|
5022
|
+
};
|
|
5023
|
+
}
|
|
5024
|
+
async function waitForPopupCondition(sessionId, wait, captureClient) {
|
|
5025
|
+
const timeoutMs = resolveTimeoutMs(wait.timeoutMs, 5_000, MAX_NETWORK_POLL_TIMEOUT_MS);
|
|
5026
|
+
const startedAt = Date.now();
|
|
5027
|
+
const capture = await executeLiveCapture(captureClient, sessionId, 'CAPTURE_WAIT_FOR_POPUP', {
|
|
5028
|
+
urlContains: wait.urlContains,
|
|
5029
|
+
urlRegex: wait.urlRegex,
|
|
5030
|
+
exactUrl: wait.exactUrl,
|
|
5031
|
+
openerTabId: wait.openerTabId,
|
|
5032
|
+
timeoutMs,
|
|
5033
|
+
}, timeoutMs + 2_000);
|
|
5034
|
+
const payload = ensureCaptureSuccess(capture, sessionId);
|
|
5035
|
+
const matched = payload.matched === true;
|
|
5036
|
+
return {
|
|
5037
|
+
waitKind: 'popup',
|
|
5038
|
+
matched,
|
|
5039
|
+
waitedMs: Date.now() - startedAt,
|
|
5040
|
+
attempts: 1,
|
|
5041
|
+
timeoutMs,
|
|
5042
|
+
pollIntervalMs: timeoutMs,
|
|
5043
|
+
evidence: {
|
|
5044
|
+
filters: {
|
|
5045
|
+
urlContains: wait.urlContains,
|
|
5046
|
+
urlRegex: wait.urlRegex,
|
|
5047
|
+
exactUrl: wait.exactUrl,
|
|
5048
|
+
openerTabId: wait.openerTabId,
|
|
5049
|
+
},
|
|
5050
|
+
popup: matched ? payload : undefined,
|
|
5051
|
+
expected: matched ? undefined : payload.expected ?? wait,
|
|
5052
|
+
timeoutDiagnostics: matched
|
|
5053
|
+
? undefined
|
|
5054
|
+
: buildWaitTimeoutDiagnostics({
|
|
5055
|
+
waitKind: 'popup',
|
|
5056
|
+
timeoutMs,
|
|
5057
|
+
waitedMs: Date.now() - startedAt,
|
|
5058
|
+
attempts: 1,
|
|
5059
|
+
pollIntervalMs: timeoutMs,
|
|
5060
|
+
matcherSummary: {
|
|
5061
|
+
urlContains: wait.urlContains,
|
|
5062
|
+
urlRegex: wait.urlRegex,
|
|
5063
|
+
exactUrl: wait.exactUrl,
|
|
5064
|
+
openerTabId: wait.openerTabId,
|
|
5065
|
+
},
|
|
5066
|
+
lastObserved: payload.lastObserved,
|
|
5067
|
+
candidateCount: typeof payload.observedPopupCount === 'number' ? payload.observedPopupCount : undefined,
|
|
5068
|
+
sampledCandidates: Array.isArray(payload.pendingTabIds) ? payload.pendingTabIds : undefined,
|
|
5069
|
+
}),
|
|
5070
|
+
},
|
|
5071
|
+
error: matched
|
|
5072
|
+
? undefined
|
|
5073
|
+
: {
|
|
5074
|
+
code: 'popup_wait_timeout',
|
|
5075
|
+
message: 'Timed out waiting for a matching popup tab.',
|
|
5076
|
+
},
|
|
5077
|
+
};
|
|
5078
|
+
}
|
|
5079
|
+
function normalizeNetworkWaitFilters(wait) {
|
|
5080
|
+
const responseWait = wait.waitKind === 'response' ? wait : undefined;
|
|
5081
|
+
return {
|
|
5082
|
+
urlContains: normalizeOptionalString(wait.urlContains),
|
|
5083
|
+
urlRegex: normalizeOptionalString(wait.urlRegex),
|
|
5084
|
+
exactUrl: normalizeOptionalString(wait.exactUrl),
|
|
5085
|
+
method: normalizeHttpMethod(wait.method),
|
|
5086
|
+
traceId: normalizeOptionalString(wait.traceId),
|
|
5087
|
+
initiator: normalizeOptionalString(wait.initiator),
|
|
5088
|
+
requestContentType: normalizeOptionalString(wait.requestContentType),
|
|
5089
|
+
responseContentType: normalizeOptionalString(responseWait?.responseContentType),
|
|
5090
|
+
statusIn: responseWait ? normalizeStatusIn(responseWait.statusIn) : [],
|
|
5091
|
+
statusGte: responseWait?.statusGte,
|
|
5092
|
+
statusLt: responseWait?.statusLt,
|
|
5093
|
+
errorType: normalizeOptionalString(responseWait?.errorType),
|
|
5094
|
+
sinceTs: resolveAutomationWaitSinceTs(wait.sinceTs),
|
|
5095
|
+
tabId: resolveOptionalTabId(wait.tabId),
|
|
5096
|
+
includeBodies: wait.includeBodies === true,
|
|
5097
|
+
};
|
|
5098
|
+
}
|
|
5099
|
+
function queryNetworkWaitCandidates(db, sessionId, filters) {
|
|
5100
|
+
const where = ['session_id = ?', 'ts_start >= ?'];
|
|
5101
|
+
const params = [sessionId, filters.sinceTs];
|
|
5102
|
+
if (filters.exactUrl) {
|
|
5103
|
+
where.push('url = ?');
|
|
5104
|
+
params.push(filters.exactUrl);
|
|
5105
|
+
}
|
|
5106
|
+
else if (filters.urlContains) {
|
|
5107
|
+
where.push('url LIKE ?');
|
|
5108
|
+
params.push(`%${filters.urlContains}%`);
|
|
5109
|
+
}
|
|
5110
|
+
if (filters.method) {
|
|
5111
|
+
where.push('method = ?');
|
|
5112
|
+
params.push(filters.method);
|
|
5113
|
+
}
|
|
5114
|
+
if (filters.traceId) {
|
|
5115
|
+
where.push('trace_id = ?');
|
|
5116
|
+
params.push(filters.traceId);
|
|
5117
|
+
}
|
|
5118
|
+
if (filters.initiator) {
|
|
5119
|
+
where.push('initiator = ?');
|
|
5120
|
+
params.push(filters.initiator);
|
|
5121
|
+
}
|
|
5122
|
+
if (filters.requestContentType) {
|
|
5123
|
+
where.push('request_content_type LIKE ?');
|
|
5124
|
+
params.push(`%${filters.requestContentType}%`);
|
|
5125
|
+
}
|
|
5126
|
+
if (filters.responseContentType) {
|
|
5127
|
+
where.push('response_content_type LIKE ?');
|
|
5128
|
+
params.push(`%${filters.responseContentType}%`);
|
|
5129
|
+
}
|
|
5130
|
+
const statusIn = Array.isArray(filters.statusIn) ? filters.statusIn : [];
|
|
5131
|
+
if (statusIn.length > 0) {
|
|
5132
|
+
where.push(`status IN (${statusIn.map(() => '?').join(', ')})`);
|
|
5133
|
+
params.push(...statusIn);
|
|
5134
|
+
}
|
|
5135
|
+
if (typeof filters.statusGte === 'number') {
|
|
5136
|
+
where.push('status >= ?');
|
|
5137
|
+
params.push(filters.statusGte);
|
|
5138
|
+
}
|
|
5139
|
+
if (typeof filters.statusLt === 'number') {
|
|
5140
|
+
where.push('status < ?');
|
|
5141
|
+
params.push(filters.statusLt);
|
|
5142
|
+
}
|
|
5143
|
+
if (filters.tabId !== undefined) {
|
|
5144
|
+
where.push('tab_id = ?');
|
|
5145
|
+
params.push(filters.tabId);
|
|
5146
|
+
}
|
|
5147
|
+
return db.prepare(`SELECT ${NETWORK_CALL_SELECT_COLUMNS}
|
|
5148
|
+
FROM network
|
|
5149
|
+
WHERE ${where.join(' AND ')}
|
|
5150
|
+
ORDER BY ts_start ASC
|
|
5151
|
+
LIMIT 200`).all(...params);
|
|
5152
|
+
}
|
|
5153
|
+
function networkCallMatchesFilters(row, filters) {
|
|
5154
|
+
if (!matchesUrlPredicates(row.url, {
|
|
5155
|
+
exactUrl: typeof filters.exactUrl === 'string' ? filters.exactUrl : undefined,
|
|
5156
|
+
urlContains: typeof filters.urlContains === 'string' ? filters.urlContains : undefined,
|
|
5157
|
+
urlRegex: typeof filters.urlRegex === 'string' ? filters.urlRegex : undefined,
|
|
5158
|
+
})) {
|
|
5159
|
+
return false;
|
|
5160
|
+
}
|
|
5161
|
+
if (typeof filters.errorType === 'string' && classifyNetworkFailure(row.status, row.error_class) !== filters.errorType) {
|
|
5162
|
+
return false;
|
|
5163
|
+
}
|
|
5164
|
+
return true;
|
|
5165
|
+
}
|
|
5166
|
+
async function waitForNetworkMatchCondition(sessionId, wait, db) {
|
|
5167
|
+
const timeoutMs = resolveTimeoutMs(wait.timeoutMs, DEFAULT_NETWORK_POLL_TIMEOUT_MS, MAX_NETWORK_POLL_TIMEOUT_MS);
|
|
5168
|
+
const pollIntervalMs = resolveDurationMs(wait.pollIntervalMs, DEFAULT_NETWORK_POLL_INTERVAL_MS, 5_000);
|
|
5169
|
+
const filters = normalizeNetworkWaitFilters(wait);
|
|
5170
|
+
const startedAt = Date.now();
|
|
5171
|
+
const deadline = startedAt + timeoutMs;
|
|
5172
|
+
let attempts = 0;
|
|
5173
|
+
let lastCalls = [];
|
|
5174
|
+
while (Date.now() <= deadline) {
|
|
5175
|
+
attempts += 1;
|
|
5176
|
+
lastCalls = queryNetworkWaitCandidates(db, sessionId, filters);
|
|
5177
|
+
const matched = lastCalls.find((row) => networkCallMatchesFilters(row, filters));
|
|
5178
|
+
if (matched) {
|
|
5179
|
+
return {
|
|
5180
|
+
waitKind: wait.waitKind,
|
|
5181
|
+
matched: true,
|
|
5182
|
+
waitedMs: Date.now() - startedAt,
|
|
5183
|
+
attempts,
|
|
5184
|
+
timeoutMs,
|
|
5185
|
+
pollIntervalMs,
|
|
5186
|
+
evidence: {
|
|
5187
|
+
filters,
|
|
5188
|
+
call: mapNetworkCallRecord(matched, filters.includeBodies === true),
|
|
5189
|
+
},
|
|
5190
|
+
};
|
|
5191
|
+
}
|
|
5192
|
+
await sleep(pollIntervalMs);
|
|
5193
|
+
}
|
|
5194
|
+
return {
|
|
5195
|
+
waitKind: wait.waitKind,
|
|
5196
|
+
matched: false,
|
|
5197
|
+
waitedMs: Date.now() - startedAt,
|
|
5198
|
+
attempts,
|
|
5199
|
+
timeoutMs,
|
|
5200
|
+
pollIntervalMs,
|
|
5201
|
+
evidence: {
|
|
5202
|
+
filters,
|
|
5203
|
+
sampledCalls: lastCalls.slice(0, 5).map((row) => mapNetworkCallRecord(row, false)),
|
|
5204
|
+
timeoutDiagnostics: buildWaitTimeoutDiagnostics({
|
|
5205
|
+
waitKind: wait.waitKind,
|
|
5206
|
+
timeoutMs,
|
|
5207
|
+
waitedMs: Date.now() - startedAt,
|
|
5208
|
+
attempts,
|
|
5209
|
+
pollIntervalMs,
|
|
5210
|
+
matcherSummary: filters,
|
|
5211
|
+
lastObserved: lastCalls.length > 0 ? mapNetworkCallRecord(lastCalls[lastCalls.length - 1], false) : undefined,
|
|
5212
|
+
candidateCount: lastCalls.length,
|
|
5213
|
+
sampledCandidates: lastCalls.slice(0, 5).map((row) => mapNetworkCallRecord(row, false)),
|
|
5214
|
+
}),
|
|
5215
|
+
},
|
|
5216
|
+
error: {
|
|
5217
|
+
code: wait.waitKind === 'request' ? 'request_wait_timeout' : 'response_wait_timeout',
|
|
5218
|
+
message: `Timed out waiting for a matching ${wait.waitKind}.`,
|
|
5219
|
+
},
|
|
5220
|
+
};
|
|
5221
|
+
}
|
|
5222
|
+
function queryRecentNetworkActivity(db, options) {
|
|
5223
|
+
const where = ['session_id = ?', 'ts_start >= ?'];
|
|
5224
|
+
const params = [options.sessionId, options.sinceTs];
|
|
5225
|
+
if (options.urlContains) {
|
|
5226
|
+
where.push('url LIKE ?');
|
|
5227
|
+
params.push(`%${options.urlContains}%`);
|
|
5228
|
+
}
|
|
5229
|
+
if (options.method) {
|
|
5230
|
+
where.push('method = ?');
|
|
5231
|
+
params.push(options.method);
|
|
5232
|
+
}
|
|
5233
|
+
if (options.tabId !== undefined) {
|
|
5234
|
+
where.push('tab_id = ?');
|
|
5235
|
+
params.push(options.tabId);
|
|
5236
|
+
}
|
|
5237
|
+
return db.prepare(`SELECT ${NETWORK_CALL_SELECT_COLUMNS}
|
|
5238
|
+
FROM network
|
|
5239
|
+
WHERE ${where.join(' AND ')}
|
|
5240
|
+
ORDER BY ts_start DESC
|
|
5241
|
+
LIMIT 10`).all(...params);
|
|
5242
|
+
}
|
|
5243
|
+
async function waitForNetworkQuietCondition(sessionId, wait, db) {
|
|
5244
|
+
const timeoutMs = resolveTimeoutMs(wait.timeoutMs, DEFAULT_NETWORK_POLL_TIMEOUT_MS, MAX_NETWORK_POLL_TIMEOUT_MS);
|
|
5245
|
+
const pollIntervalMs = resolveDurationMs(wait.pollIntervalMs, DEFAULT_NETWORK_POLL_INTERVAL_MS, 5_000);
|
|
5246
|
+
const quietMs = resolveDurationMs(wait.quietMs, 500, 10_000);
|
|
5247
|
+
const urlContains = normalizeOptionalString(wait.urlContains);
|
|
5248
|
+
const method = normalizeHttpMethod(wait.method);
|
|
5249
|
+
const tabId = resolveOptionalTabId(wait.tabId);
|
|
5250
|
+
const startedAt = Date.now();
|
|
5251
|
+
const deadline = startedAt + timeoutMs;
|
|
5252
|
+
let attempts = 0;
|
|
5253
|
+
let lastActivityAt = startedAt;
|
|
5254
|
+
let lastCalls = [];
|
|
5255
|
+
while (Date.now() <= deadline) {
|
|
5256
|
+
attempts += 1;
|
|
5257
|
+
const rows = queryRecentNetworkActivity(db, {
|
|
5258
|
+
sessionId,
|
|
5259
|
+
sinceTs: lastActivityAt + 1,
|
|
5260
|
+
urlContains,
|
|
5261
|
+
method,
|
|
5262
|
+
tabId,
|
|
5263
|
+
});
|
|
5264
|
+
if (rows.length > 0) {
|
|
5265
|
+
lastCalls = rows;
|
|
5266
|
+
lastActivityAt = Math.max(...rows.map((row) => row.ts_start), Date.now());
|
|
5267
|
+
}
|
|
5268
|
+
if (Date.now() - lastActivityAt >= quietMs) {
|
|
5269
|
+
return {
|
|
5270
|
+
waitKind: 'network_quiet',
|
|
5271
|
+
matched: true,
|
|
5272
|
+
waitedMs: Date.now() - startedAt,
|
|
5273
|
+
attempts,
|
|
5274
|
+
timeoutMs,
|
|
5275
|
+
pollIntervalMs,
|
|
5276
|
+
evidence: {
|
|
5277
|
+
quietMs,
|
|
5278
|
+
filters: { urlContains, method, tabId },
|
|
5279
|
+
lastActivityAt,
|
|
5280
|
+
sampledCalls: lastCalls.slice(0, 5).map((row) => mapNetworkCallRecord(row, false)),
|
|
5281
|
+
},
|
|
5282
|
+
};
|
|
5283
|
+
}
|
|
5284
|
+
await sleep(pollIntervalMs);
|
|
5285
|
+
}
|
|
5286
|
+
return {
|
|
5287
|
+
waitKind: 'network_quiet',
|
|
5288
|
+
matched: false,
|
|
5289
|
+
waitedMs: Date.now() - startedAt,
|
|
5290
|
+
attempts,
|
|
5291
|
+
timeoutMs,
|
|
5292
|
+
pollIntervalMs,
|
|
5293
|
+
evidence: {
|
|
5294
|
+
quietMs,
|
|
5295
|
+
filters: { urlContains, method, tabId },
|
|
5296
|
+
lastActivityAt,
|
|
5297
|
+
sampledCalls: lastCalls.slice(0, 5).map((row) => mapNetworkCallRecord(row, false)),
|
|
5298
|
+
timeoutDiagnostics: buildWaitTimeoutDiagnostics({
|
|
5299
|
+
waitKind: 'network_quiet',
|
|
5300
|
+
timeoutMs,
|
|
5301
|
+
waitedMs: Date.now() - startedAt,
|
|
5302
|
+
attempts,
|
|
5303
|
+
pollIntervalMs,
|
|
5304
|
+
matcherSummary: { quietMs, urlContains, method, tabId },
|
|
5305
|
+
lastObserved: lastCalls.length > 0 ? mapNetworkCallRecord(lastCalls[0], false) : undefined,
|
|
5306
|
+
candidateCount: lastCalls.length,
|
|
5307
|
+
sampledCandidates: lastCalls.slice(0, 5).map((row) => mapNetworkCallRecord(row, false)),
|
|
5308
|
+
}),
|
|
5309
|
+
},
|
|
5310
|
+
error: {
|
|
5311
|
+
code: 'network_quiet_timeout',
|
|
5312
|
+
message: `Timed out waiting for ${quietMs}ms of quiet network activity.`,
|
|
5313
|
+
},
|
|
5314
|
+
};
|
|
5315
|
+
}
|
|
5316
|
+
async function runAutomationWait(options) {
|
|
5317
|
+
switch (options.wait.waitKind) {
|
|
5318
|
+
case 'url':
|
|
5319
|
+
return waitForUrlCondition(options.sessionId, options.wait, options.capturePageState);
|
|
5320
|
+
case 'navigation': {
|
|
5321
|
+
const db = options.getDb?.();
|
|
5322
|
+
if (!db) {
|
|
5323
|
+
throw new Error('navigation waits require database access');
|
|
5324
|
+
}
|
|
5325
|
+
return waitForNavigationCondition(options.sessionId, options.wait, db);
|
|
5326
|
+
}
|
|
5327
|
+
case 'navigation_lifecycle':
|
|
5328
|
+
return waitForNavigationLifecycleCondition(options.sessionId, options.wait, options.captureClient);
|
|
5329
|
+
case 'load_state':
|
|
5330
|
+
return waitForLoadStateCondition(options.sessionId, options.wait, options.capturePageState);
|
|
5331
|
+
case 'selector_state':
|
|
5332
|
+
return waitForSelectorStateCondition(options.sessionId, options.wait, options.captureClient);
|
|
5333
|
+
case 'console':
|
|
5334
|
+
return waitForConsoleCondition(options.sessionId, options.wait, options.captureClient);
|
|
5335
|
+
case 'dialog':
|
|
5336
|
+
return waitForDialogCondition(options.sessionId, options.wait, options.captureClient);
|
|
5337
|
+
case 'stable_layout':
|
|
5338
|
+
return waitForStableLayoutCondition(options.sessionId, options.wait, options.captureClient);
|
|
5339
|
+
case 'download':
|
|
5340
|
+
return waitForDownloadCondition(options.sessionId, options.wait, options.captureClient);
|
|
5341
|
+
case 'popup':
|
|
5342
|
+
return waitForPopupCondition(options.sessionId, options.wait, options.captureClient);
|
|
5343
|
+
case 'network_quiet': {
|
|
5344
|
+
const db = options.getDb?.();
|
|
5345
|
+
if (!db) {
|
|
5346
|
+
throw new Error('network_quiet waits require database access');
|
|
5347
|
+
}
|
|
5348
|
+
return waitForNetworkQuietCondition(options.sessionId, options.wait, db);
|
|
5349
|
+
}
|
|
5350
|
+
case 'request':
|
|
5351
|
+
case 'response': {
|
|
5352
|
+
const db = options.getDb?.();
|
|
5353
|
+
if (!db) {
|
|
5354
|
+
throw new Error(`${options.wait.waitKind} waits require database access`);
|
|
5355
|
+
}
|
|
5356
|
+
return waitForNetworkMatchCondition(options.sessionId, options.wait, db);
|
|
5357
|
+
}
|
|
5358
|
+
}
|
|
5359
|
+
}
|
|
5360
|
+
function getSessionRow(db, sessionId) {
|
|
5361
|
+
return db.prepare(`
|
|
5362
|
+
SELECT
|
|
5363
|
+
session_id,
|
|
5364
|
+
created_at,
|
|
5365
|
+
last_seen_at,
|
|
5366
|
+
paused_at,
|
|
5367
|
+
ended_at,
|
|
5368
|
+
tab_id,
|
|
5369
|
+
window_id,
|
|
5370
|
+
url_start,
|
|
5371
|
+
url_last,
|
|
5372
|
+
user_agent,
|
|
5373
|
+
viewport_w,
|
|
5374
|
+
viewport_h,
|
|
5375
|
+
dpr,
|
|
5376
|
+
safe_mode,
|
|
5377
|
+
pinned
|
|
5378
|
+
FROM sessions
|
|
5379
|
+
WHERE session_id = ?
|
|
5380
|
+
LIMIT 1
|
|
5381
|
+
`).get(sessionId);
|
|
5382
|
+
}
|
|
5383
|
+
function looksSensitiveText(value) {
|
|
5384
|
+
return typeof value === 'string'
|
|
5385
|
+
&& /(password|passwd|pwd|token|secret|auth|session|email|card|cvv|cvc|ssn|iban|payment|billing)/i.test(value);
|
|
5386
|
+
}
|
|
5387
|
+
function isSensitivePageInput(input) {
|
|
5388
|
+
const type = typeof input.type === 'string' ? input.type.toLowerCase() : '';
|
|
5389
|
+
return type === 'password'
|
|
5390
|
+
|| looksSensitiveText(input.selector)
|
|
5391
|
+
|| looksSensitiveText(input.label)
|
|
5392
|
+
|| looksSensitiveText(input.name)
|
|
5393
|
+
|| looksSensitiveText(input.placeholder)
|
|
5394
|
+
|| looksSensitiveText(input.testId);
|
|
5395
|
+
}
|
|
5396
|
+
function collectAutomationPageRisks(payload) {
|
|
5397
|
+
if (!payload) {
|
|
5398
|
+
return {
|
|
5399
|
+
sensitiveInputs: [],
|
|
5400
|
+
frameCount: 0,
|
|
5401
|
+
crossOriginFrameCount: 0,
|
|
5402
|
+
};
|
|
5403
|
+
}
|
|
5404
|
+
const inputs = asRecordArray(payload.inputs);
|
|
5405
|
+
const frames = asRecordArray(payload.frames);
|
|
5406
|
+
const sensitiveInputs = inputs
|
|
5407
|
+
.filter(isSensitivePageInput)
|
|
5408
|
+
.slice(0, 8)
|
|
5409
|
+
.map((input) => ({
|
|
5410
|
+
selector: input.selector,
|
|
5411
|
+
type: input.type,
|
|
5412
|
+
label: input.label,
|
|
5413
|
+
name: input.name,
|
|
5414
|
+
placeholder: input.placeholder,
|
|
5415
|
+
frameId: input.frameId,
|
|
5416
|
+
frameUrl: input.frameUrl,
|
|
5417
|
+
}));
|
|
5418
|
+
const crossOriginFrames = frames
|
|
5419
|
+
.filter((frame) => frame.sameOrigin === false || frame.accessible === false || frame.crossOrigin === true)
|
|
5420
|
+
.slice(0, 8)
|
|
5421
|
+
.map((frame) => ({
|
|
5422
|
+
frameId: frame.frameId,
|
|
5423
|
+
url: frame.url ?? frame.frameUrl,
|
|
5424
|
+
title: frame.title ?? frame.frameTitle,
|
|
5425
|
+
sameOrigin: frame.sameOrigin,
|
|
5426
|
+
accessible: frame.accessible,
|
|
5427
|
+
}));
|
|
5428
|
+
return {
|
|
5429
|
+
sensitiveInputs,
|
|
5430
|
+
sensitiveInputCount: sensitiveInputs.length,
|
|
5431
|
+
frameCount: frames.length,
|
|
5432
|
+
crossOriginFrameCount: crossOriginFrames.length,
|
|
5433
|
+
crossOriginFrames,
|
|
5434
|
+
};
|
|
5435
|
+
}
|
|
5436
|
+
async function buildAutomationFlowPreflight(options) {
|
|
5437
|
+
const blockers = [];
|
|
5438
|
+
const warnings = [];
|
|
5439
|
+
const includePageState = options.input.includePageState !== false;
|
|
5440
|
+
const expectedUrlContains = normalizeOptionalString(options.input.expectedUrlContains);
|
|
5441
|
+
const requireSensitiveAutomation = options.input.requireSensitiveAutomation === true;
|
|
5442
|
+
const plannedActions = Array.isArray(options.input.plannedActions)
|
|
5443
|
+
? options.input.plannedActions.filter((entry) => typeof entry === 'string' && entry.trim().length > 0)
|
|
5444
|
+
: [];
|
|
5445
|
+
const db = options.getDb?.();
|
|
5446
|
+
const session = db ? getSessionRow(db, options.sessionId) : undefined;
|
|
5447
|
+
const sessionState = options.getSessionConnectionState?.(options.sessionId);
|
|
5448
|
+
const hasLiveConnectionLookup = typeof options.getSessionConnectionState === 'function';
|
|
5449
|
+
const scope = classifySessionUrl(session?.url_last ?? undefined);
|
|
5450
|
+
const liveConnection = session
|
|
5451
|
+
? buildLiveConnectionRecord(session, scope, sessionState)
|
|
5452
|
+
: {
|
|
5453
|
+
connected: sessionState?.connected === true,
|
|
5454
|
+
status: sessionState?.connected === true ? 'connected' : 'unknown',
|
|
5455
|
+
recommendedForLiveCapture: false,
|
|
5456
|
+
};
|
|
5457
|
+
if (!db) {
|
|
5458
|
+
warnings.push({
|
|
5459
|
+
code: 'DB_UNAVAILABLE',
|
|
5460
|
+
severity: 'warning',
|
|
5461
|
+
source: 'server',
|
|
5462
|
+
message: 'Database access was not available; session history checks were skipped.',
|
|
5463
|
+
});
|
|
5464
|
+
}
|
|
5465
|
+
if (!session) {
|
|
5466
|
+
blockers.push({
|
|
5467
|
+
code: 'SESSION_NOT_FOUND',
|
|
5468
|
+
severity: 'error',
|
|
5469
|
+
source: 'session',
|
|
5470
|
+
message: `Session not found: ${options.sessionId}`,
|
|
5471
|
+
});
|
|
5472
|
+
}
|
|
5473
|
+
else {
|
|
5474
|
+
const status = getSessionStatus(session);
|
|
5475
|
+
if (status === 'paused') {
|
|
5476
|
+
blockers.push({
|
|
5477
|
+
code: 'SESSION_PAUSED',
|
|
5478
|
+
severity: 'error',
|
|
5479
|
+
source: 'session',
|
|
5480
|
+
message: 'Resume the session before running an automation flow.',
|
|
5481
|
+
});
|
|
5482
|
+
}
|
|
5483
|
+
if (status === 'ended') {
|
|
5484
|
+
blockers.push({
|
|
5485
|
+
code: 'SESSION_ENDED',
|
|
5486
|
+
severity: 'error',
|
|
5487
|
+
source: 'session',
|
|
5488
|
+
message: 'Start a new session before running an automation flow.',
|
|
5489
|
+
});
|
|
5490
|
+
}
|
|
5491
|
+
if (scope.kind === 'likely_iframe_noise') {
|
|
5492
|
+
blockers.push({
|
|
5493
|
+
code: 'SESSION_SCOPE_NOISE',
|
|
5494
|
+
severity: 'error',
|
|
5495
|
+
source: 'session',
|
|
5496
|
+
message: 'The selected session appears to be bound to iframe/ad traffic rather than the app surface.',
|
|
5497
|
+
});
|
|
5498
|
+
}
|
|
5499
|
+
if (scope.kind === 'top_level_page' && scope.isLocalhost !== true) {
|
|
5500
|
+
warnings.push({
|
|
5501
|
+
code: 'PRODUCTION_OR_REMOTE_ORIGIN',
|
|
5502
|
+
severity: 'warning',
|
|
5503
|
+
source: 'session',
|
|
5504
|
+
message: 'The current session URL is remote/production-like. Keep the flow scoped and avoid destructive actions.',
|
|
5505
|
+
origin: scope.origin,
|
|
5506
|
+
});
|
|
5507
|
+
}
|
|
5508
|
+
if (expectedUrlContains && !String(session.url_last ?? '').includes(expectedUrlContains)) {
|
|
5509
|
+
blockers.push({
|
|
5510
|
+
code: 'EXPECTED_URL_MISMATCH',
|
|
5511
|
+
severity: 'error',
|
|
5512
|
+
source: 'session',
|
|
5513
|
+
message: `Current session URL does not include "${expectedUrlContains}".`,
|
|
5514
|
+
currentUrl: session.url_last,
|
|
5515
|
+
});
|
|
5516
|
+
}
|
|
5517
|
+
}
|
|
5518
|
+
if (hasLiveConnectionLookup && (!sessionState || sessionState.connected !== true)) {
|
|
5519
|
+
blockers.push({
|
|
5520
|
+
code: LIVE_SESSION_DISCONNECTED_CODE,
|
|
5521
|
+
severity: 'error',
|
|
5522
|
+
source: 'connection',
|
|
5523
|
+
message: 'The session is not currently connected to a live extension target.',
|
|
5524
|
+
disconnectedAt: sessionState?.disconnectedAt,
|
|
5525
|
+
disconnectReason: sessionState?.disconnectReason,
|
|
5526
|
+
});
|
|
5527
|
+
}
|
|
5528
|
+
let pageCapture;
|
|
5529
|
+
if (includePageState && blockers.length === 0) {
|
|
5530
|
+
try {
|
|
5531
|
+
pageCapture = await options.capturePageState(options.sessionId, {
|
|
5532
|
+
includeButtons: true,
|
|
5533
|
+
includeLinks: true,
|
|
5534
|
+
includeInputs: true,
|
|
5535
|
+
includeModals: true,
|
|
5536
|
+
maxItems: resolveLimit(options.input.maxItems, 40),
|
|
5537
|
+
maxTextLength: resolveDurationMs(options.input.maxTextLength, 80, 200),
|
|
5538
|
+
});
|
|
5539
|
+
}
|
|
5540
|
+
catch (error) {
|
|
5541
|
+
blockers.push({
|
|
5542
|
+
code: isLiveSessionDisconnectedError(error) ? LIVE_SESSION_DISCONNECTED_CODE : 'PAGE_STATE_CAPTURE_FAILED',
|
|
5543
|
+
severity: 'error',
|
|
5544
|
+
source: 'page-state',
|
|
5545
|
+
message: error instanceof Error ? error.message : String(error),
|
|
5546
|
+
});
|
|
5547
|
+
}
|
|
5548
|
+
}
|
|
5549
|
+
const pageRisks = collectAutomationPageRisks(pageCapture?.payload);
|
|
5550
|
+
const sensitiveInputs = Array.isArray(pageRisks.sensitiveInputs) ? pageRisks.sensitiveInputs : [];
|
|
5551
|
+
const hasInputLikeAction = plannedActions.some((action) => ['input', 'type', 'clear', 'select_option', 'press_key'].includes(action));
|
|
5552
|
+
if (sensitiveInputs.length > 0 && (requireSensitiveAutomation || hasInputLikeAction)) {
|
|
5553
|
+
warnings.push({
|
|
5554
|
+
code: 'SENSITIVE_FIELD_AUTOMATION_RISK',
|
|
5555
|
+
severity: 'warning',
|
|
5556
|
+
source: 'page-state',
|
|
5557
|
+
message: 'Sensitive-looking fields are present. The extension sensitive-field opt-in may be required before input-like actions.',
|
|
5558
|
+
count: sensitiveInputs.length,
|
|
5559
|
+
sampledInputs: sensitiveInputs,
|
|
5560
|
+
});
|
|
5561
|
+
}
|
|
5562
|
+
if (typeof pageRisks.crossOriginFrameCount === 'number' && pageRisks.crossOriginFrameCount > 0) {
|
|
5563
|
+
warnings.push({
|
|
5564
|
+
code: 'CROSS_ORIGIN_FRAME_PRESENT',
|
|
5565
|
+
severity: 'warning',
|
|
5566
|
+
source: 'page-state',
|
|
5567
|
+
message: 'Cross-origin or inaccessible frames are present. Automation inside those frames may be diagnostic-only.',
|
|
5568
|
+
count: pageRisks.crossOriginFrameCount,
|
|
5569
|
+
frames: pageRisks.crossOriginFrames,
|
|
5570
|
+
});
|
|
5571
|
+
}
|
|
5572
|
+
const ready = blockers.length === 0;
|
|
5573
|
+
return {
|
|
5574
|
+
ready,
|
|
5575
|
+
blockers,
|
|
5576
|
+
warnings,
|
|
5577
|
+
checks: {
|
|
5578
|
+
sessionFound: Boolean(session),
|
|
5579
|
+
liveConnected: sessionState?.connected === true || (hasLiveConnectionLookup ? false : undefined),
|
|
5580
|
+
recommendedForLiveCapture: liveConnection.recommendedForLiveCapture,
|
|
5581
|
+
expectedUrlMatched: expectedUrlContains ? blockers.every((blocker) => blocker.code !== 'EXPECTED_URL_MISMATCH') : undefined,
|
|
5582
|
+
pageStateCaptured: pageCapture !== undefined,
|
|
5583
|
+
remoteOrProductionLike: scope.kind === 'top_level_page' && scope.isLocalhost !== true,
|
|
5584
|
+
sensitiveInputCount: sensitiveInputs.length,
|
|
5585
|
+
crossOriginFrameCount: pageRisks.crossOriginFrameCount,
|
|
5586
|
+
},
|
|
5587
|
+
session: session
|
|
5588
|
+
? {
|
|
5589
|
+
sessionId: session.session_id,
|
|
5590
|
+
status: getSessionStatus(session),
|
|
5591
|
+
tabId: session.tab_id ?? undefined,
|
|
5592
|
+
windowId: session.window_id ?? undefined,
|
|
5593
|
+
urlStart: session.url_start ?? undefined,
|
|
5594
|
+
urlLast: session.url_last ?? undefined,
|
|
5595
|
+
lastSeenAt: resolveSessionLastSeenAt(session, sessionState),
|
|
5596
|
+
safeMode: session.safe_mode === 1,
|
|
5597
|
+
}
|
|
5598
|
+
: undefined,
|
|
5599
|
+
scope,
|
|
5600
|
+
liveConnection,
|
|
5601
|
+
page: pageCapture
|
|
5602
|
+
? {
|
|
5603
|
+
url: pageCapture.payload.url,
|
|
5604
|
+
title: pageCapture.payload.title,
|
|
5605
|
+
language: pageCapture.payload.language,
|
|
5606
|
+
viewport: pageCapture.payload.viewport,
|
|
5607
|
+
summary: pageCapture.payload.summary,
|
|
5608
|
+
}
|
|
5609
|
+
: undefined,
|
|
5610
|
+
detectedRisks: pageRisks,
|
|
5611
|
+
nextActions: ready
|
|
5612
|
+
? [{ code: 'RUN_FLOW', message: 'Run the automation flow with bounded waits and failure capture enabled.' }]
|
|
5613
|
+
: blockers.map((blocker) => ({
|
|
5614
|
+
code: String(blocker.code ?? 'FIX_BLOCKER'),
|
|
5615
|
+
message: String(blocker.message ?? 'Resolve this preflight blocker before running the flow.'),
|
|
5616
|
+
})),
|
|
3165
5617
|
};
|
|
3166
5618
|
}
|
|
3167
5619
|
function createWorkflowStepId(step, index) {
|
|
@@ -3172,6 +5624,7 @@ async function captureWorkflowPageState(sessionId, capturePageState, mode) {
|
|
|
3172
5624
|
const maxTextLength = mode === 'fast' ? 60 : 80;
|
|
3173
5625
|
return capturePageState(sessionId, {
|
|
3174
5626
|
includeButtons: true,
|
|
5627
|
+
includeLinks: true,
|
|
3175
5628
|
includeInputs: true,
|
|
3176
5629
|
includeModals: true,
|
|
3177
5630
|
maxItems,
|
|
@@ -3226,6 +5679,20 @@ function resolveWorkflowRecommendedAction(error) {
|
|
|
3226
5679
|
if (error.code === 'page_state_not_matched' || error.code === 'page_state_assertion_failed') {
|
|
3227
5680
|
return 'inspect_page_state';
|
|
3228
5681
|
}
|
|
5682
|
+
if (error.code === 'url_wait_timeout' || error.code === 'navigation_wait_timeout') {
|
|
5683
|
+
return 'inspect_navigation_state';
|
|
5684
|
+
}
|
|
5685
|
+
if (error.code === 'selector_state_wait_timeout') {
|
|
5686
|
+
return 'inspect_selector_state';
|
|
5687
|
+
}
|
|
5688
|
+
if (error.code === 'console_wait_timeout') {
|
|
5689
|
+
return 'inspect_live_console_logs';
|
|
5690
|
+
}
|
|
5691
|
+
if (error.code === 'network_quiet_timeout'
|
|
5692
|
+
|| error.code === 'request_wait_timeout'
|
|
5693
|
+
|| error.code === 'response_wait_timeout') {
|
|
5694
|
+
return 'inspect_network_calls';
|
|
5695
|
+
}
|
|
3229
5696
|
return undefined;
|
|
3230
5697
|
}
|
|
3231
5698
|
function resolveWorkflowFailureSelector(step, stepResultTarget) {
|
|
@@ -3970,15 +6437,17 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
|
|
|
3970
6437
|
recommendedAction,
|
|
3971
6438
|
};
|
|
3972
6439
|
},
|
|
3973
|
-
list_override_profiles: async () => {
|
|
6440
|
+
list_override_profiles: async (input) => {
|
|
3974
6441
|
const profiles = buildOverrideProfileRecords();
|
|
6442
|
+
const responseProfile = resolveOverrideResponseProfile(input.responseProfile);
|
|
3975
6443
|
return {
|
|
3976
6444
|
...createBaseResponse(),
|
|
3977
6445
|
limitsApplied: {
|
|
3978
6446
|
maxResults: profiles.length,
|
|
3979
6447
|
truncated: false,
|
|
3980
6448
|
},
|
|
3981
|
-
|
|
6449
|
+
responseProfile,
|
|
6450
|
+
profiles: profiles.map((profile) => serializeOverrideProfile(profile, responseProfile)),
|
|
3982
6451
|
nextActions: profiles.length > 0
|
|
3983
6452
|
? [{ code: 'VALIDATE_PROFILE', message: 'Run validate_override_profile before enabling overrides.' }]
|
|
3984
6453
|
: [{ code: 'CREATE_PROFILE', message: 'Run create_override_profile to generate a candidate profile.' }],
|
|
@@ -4016,6 +6485,8 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
|
|
|
4016
6485
|
});
|
|
4017
6486
|
const writeConfig = normalizeOptionalBooleanInput(input.writeConfig, 'writeConfig') ?? false;
|
|
4018
6487
|
const overwrite = normalizeOptionalBooleanInput(input.overwrite, 'overwrite') ?? false;
|
|
6488
|
+
const responseProfile = resolveOverrideResponseProfile(input.responseProfile);
|
|
6489
|
+
const includeConfigJson = input.includeConfigJson === true || responseProfile === 'full';
|
|
4019
6490
|
const write = {
|
|
4020
6491
|
written: false,
|
|
4021
6492
|
path: generated.suggestedConfigPath,
|
|
@@ -4064,21 +6535,31 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
|
|
|
4064
6535
|
warnings: generated.warnings,
|
|
4065
6536
|
nextActions,
|
|
4066
6537
|
write,
|
|
4067
|
-
|
|
4068
|
-
|
|
4069
|
-
|
|
6538
|
+
responseProfile,
|
|
6539
|
+
profile: responseProfile === 'full' ? generated.profile : compactOverrideProfile(generated.profile),
|
|
6540
|
+
config: responseProfile === 'full'
|
|
6541
|
+
? generated.config
|
|
6542
|
+
: {
|
|
6543
|
+
enabled: generated.config.enabled,
|
|
6544
|
+
activeProfileId: generated.config.activeProfileId,
|
|
6545
|
+
profileCount: generated.config.profiles.length,
|
|
6546
|
+
},
|
|
6547
|
+
configJson: includeConfigJson ? generated.configJson : undefined,
|
|
6548
|
+
configJsonOmitted: !includeConfigJson,
|
|
4070
6549
|
};
|
|
4071
6550
|
},
|
|
4072
6551
|
validate_override_profile: async (input) => {
|
|
4073
6552
|
const profile = resolveOverrideProfileRecord(input.profileId);
|
|
4074
6553
|
const issues = buildOverrideProfileIssues(profile);
|
|
6554
|
+
const responseProfile = resolveOverrideResponseProfile(input.responseProfile);
|
|
4075
6555
|
return {
|
|
4076
6556
|
...createBaseResponse(),
|
|
4077
6557
|
profileId: profile.profileId,
|
|
4078
6558
|
valid: !issues.some((issue) => issue.severity === 'error'),
|
|
4079
6559
|
issues,
|
|
4080
6560
|
nextActions: buildOverrideProfileNextActions(profile, issues),
|
|
4081
|
-
|
|
6561
|
+
responseProfile,
|
|
6562
|
+
profile: serializeOverrideProfile(profile, responseProfile),
|
|
4082
6563
|
};
|
|
4083
6564
|
},
|
|
4084
6565
|
preflight_overrides: async (input) => {
|
|
@@ -4105,16 +6586,18 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
|
|
|
4105
6586
|
}
|
|
4106
6587
|
const assets = listObservedOverrideAssets(getDb(), {
|
|
4107
6588
|
sessionId,
|
|
4108
|
-
limit: typeof input.limit === 'number' ? input.limit :
|
|
6589
|
+
limit: typeof input.limit === 'number' ? input.limit : 50,
|
|
4109
6590
|
sinceTimestamp: typeof input.sinceTimestamp === 'number' ? input.sinceTimestamp : undefined,
|
|
4110
6591
|
});
|
|
6592
|
+
const responseProfile = resolveOverrideResponseProfile(input.responseProfile);
|
|
4111
6593
|
return {
|
|
4112
6594
|
...createBaseResponse(sessionId),
|
|
4113
6595
|
limitsApplied: {
|
|
4114
6596
|
maxResults: assets.length,
|
|
4115
6597
|
truncated: false,
|
|
4116
6598
|
},
|
|
4117
|
-
|
|
6599
|
+
responseProfile,
|
|
6600
|
+
assets: responseProfile === 'full' ? assets : assets.map(compactObservedOverrideAsset),
|
|
4118
6601
|
};
|
|
4119
6602
|
},
|
|
4120
6603
|
plan_override_response_patch: async (input) => {
|
|
@@ -4356,6 +6839,373 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
|
|
|
4356
6839
|
: [{ code: 'NO_DIAGNOSIS_ISSUES', message: 'No diagnosis issues were found for the selected override run.' }],
|
|
4357
6840
|
};
|
|
4358
6841
|
},
|
|
6842
|
+
discover_ssr_mockability: async (input) => {
|
|
6843
|
+
const db = getDb();
|
|
6844
|
+
const projectRoot = normalizeOptionalString(input.projectRoot);
|
|
6845
|
+
if (!projectRoot) {
|
|
6846
|
+
throw new Error('projectRoot is required');
|
|
6847
|
+
}
|
|
6848
|
+
const discovery = discoverSsrMockability({
|
|
6849
|
+
projectRoot,
|
|
6850
|
+
targetUrl: normalizeOptionalString(input.targetUrl),
|
|
6851
|
+
apiHost: normalizeOptionalString(input.apiHost),
|
|
6852
|
+
maxFiles: typeof input.maxFiles === 'number' ? input.maxFiles : undefined,
|
|
6853
|
+
});
|
|
6854
|
+
const auditRecord = createSsrMockAuditRecord({
|
|
6855
|
+
action: 'discover',
|
|
6856
|
+
status: discovery.mockable ? 'succeeded' : 'not_mockable',
|
|
6857
|
+
projectRoot: discovery.projectRoot,
|
|
6858
|
+
targetUrl: discovery.targetUrl,
|
|
6859
|
+
apiHost: discovery.apiHost,
|
|
6860
|
+
envVarName: discovery.preferredEnvVarName,
|
|
6861
|
+
envFilePath: discovery.preferredEnvFilePath,
|
|
6862
|
+
summary: {
|
|
6863
|
+
classification: discovery.classification,
|
|
6864
|
+
scannedFileCount: discovery.scannedFileCount,
|
|
6865
|
+
candidateCount: discovery.candidates.length,
|
|
6866
|
+
},
|
|
6867
|
+
result: discovery,
|
|
6868
|
+
});
|
|
6869
|
+
insertSsrMockAudit(db, auditRecord);
|
|
6870
|
+
return {
|
|
6871
|
+
...createBaseResponse(),
|
|
6872
|
+
limitsApplied: {
|
|
6873
|
+
maxResults: discovery.candidates.length,
|
|
6874
|
+
truncated: false,
|
|
6875
|
+
},
|
|
6876
|
+
audit: auditRecord,
|
|
6877
|
+
...discovery,
|
|
6878
|
+
};
|
|
6879
|
+
},
|
|
6880
|
+
apply_ssr_mock_config: async (input) => {
|
|
6881
|
+
const db = getDb();
|
|
6882
|
+
const projectRoot = normalizeOptionalString(input.projectRoot);
|
|
6883
|
+
if (!projectRoot) {
|
|
6884
|
+
throw new Error('projectRoot is required');
|
|
6885
|
+
}
|
|
6886
|
+
const envVarName = normalizeOptionalString(input.envVarName);
|
|
6887
|
+
if (!envVarName) {
|
|
6888
|
+
throw new Error('envVarName is required');
|
|
6889
|
+
}
|
|
6890
|
+
const mockBaseUrl = normalizeOptionalString(input.mockBaseUrl);
|
|
6891
|
+
if (!mockBaseUrl) {
|
|
6892
|
+
throw new Error('mockBaseUrl is required');
|
|
6893
|
+
}
|
|
6894
|
+
const applied = applySsrMockConfig({
|
|
6895
|
+
projectRoot,
|
|
6896
|
+
envVarName,
|
|
6897
|
+
mockBaseUrl,
|
|
6898
|
+
envFilePath: normalizeOptionalString(input.envFilePath),
|
|
6899
|
+
rollbackId: normalizeOptionalString(input.rollbackId),
|
|
6900
|
+
});
|
|
6901
|
+
const auditRecord = createSsrMockAuditRecord({
|
|
6902
|
+
action: 'apply-config',
|
|
6903
|
+
status: applied.changed ? 'succeeded' : 'no_change',
|
|
6904
|
+
projectRoot,
|
|
6905
|
+
envVarName,
|
|
6906
|
+
envFilePath: applied.envFilePath,
|
|
6907
|
+
mockBaseUrl: applied.mockBaseUrl,
|
|
6908
|
+
rollbackId: applied.rollbackId,
|
|
6909
|
+
summary: {
|
|
6910
|
+
mode: applied.mode,
|
|
6911
|
+
createdFile: applied.createdFile,
|
|
6912
|
+
changed: applied.changed,
|
|
6913
|
+
},
|
|
6914
|
+
result: applied,
|
|
6915
|
+
});
|
|
6916
|
+
insertSsrMockAudit(db, auditRecord);
|
|
6917
|
+
return {
|
|
6918
|
+
...createBaseResponse(),
|
|
6919
|
+
limitsApplied: {
|
|
6920
|
+
maxResults: 1,
|
|
6921
|
+
truncated: false,
|
|
6922
|
+
},
|
|
6923
|
+
audit: auditRecord,
|
|
6924
|
+
...applied,
|
|
6925
|
+
nextActions: [
|
|
6926
|
+
{ code: 'RESTART_APP_SERVER', message: 'Restart the SSR app server so the env change takes effect.' },
|
|
6927
|
+
{ code: 'REMOVE_SSR_MOCK_CONFIG', message: 'Run remove_ssr_mock_config when the mock session is finished.' },
|
|
6928
|
+
],
|
|
6929
|
+
};
|
|
6930
|
+
},
|
|
6931
|
+
remove_ssr_mock_config: async (input) => {
|
|
6932
|
+
const db = getDb();
|
|
6933
|
+
const envFilePath = normalizeOptionalString(input.envFilePath);
|
|
6934
|
+
if (!envFilePath) {
|
|
6935
|
+
throw new Error('envFilePath is required');
|
|
6936
|
+
}
|
|
6937
|
+
const envVarName = normalizeOptionalString(input.envVarName);
|
|
6938
|
+
if (!envVarName) {
|
|
6939
|
+
throw new Error('envVarName is required');
|
|
6940
|
+
}
|
|
6941
|
+
const removed = removeSsrMockConfig({
|
|
6942
|
+
envFilePath,
|
|
6943
|
+
envVarName,
|
|
6944
|
+
rollbackId: normalizeOptionalString(input.rollbackId),
|
|
6945
|
+
});
|
|
6946
|
+
const auditRecord = createSsrMockAuditRecord({
|
|
6947
|
+
action: 'remove-config',
|
|
6948
|
+
status: removed.restored ? 'succeeded' : 'not_found',
|
|
6949
|
+
projectRoot: envFilePath,
|
|
6950
|
+
envVarName,
|
|
6951
|
+
envFilePath: removed.envFilePath,
|
|
6952
|
+
rollbackId: removed.rollbackId,
|
|
6953
|
+
summary: {
|
|
6954
|
+
mode: removed.mode,
|
|
6955
|
+
restored: removed.restored,
|
|
6956
|
+
},
|
|
6957
|
+
result: removed,
|
|
6958
|
+
});
|
|
6959
|
+
insertSsrMockAudit(db, auditRecord);
|
|
6960
|
+
return {
|
|
6961
|
+
...createBaseResponse(),
|
|
6962
|
+
limitsApplied: {
|
|
6963
|
+
maxResults: 1,
|
|
6964
|
+
truncated: false,
|
|
6965
|
+
},
|
|
6966
|
+
audit: auditRecord,
|
|
6967
|
+
...removed,
|
|
6968
|
+
nextActions: removed.restored
|
|
6969
|
+
? [{ code: 'RESTART_APP_SERVER', message: 'Restart the SSR app server so the restored env value takes effect.' }]
|
|
6970
|
+
: [{ code: 'INSPECT_ENV_FILE', message: 'No managed SSR mock patch was found for this env var.' }],
|
|
6971
|
+
};
|
|
6972
|
+
},
|
|
6973
|
+
get_ssr_mock_audit_log: async (input) => {
|
|
6974
|
+
const db = getDb();
|
|
6975
|
+
const limit = resolveLimit(input.limit, DEFAULT_EVENT_LIMIT);
|
|
6976
|
+
const offset = resolveOffset(input.offset);
|
|
6977
|
+
const maxResponseBytes = resolveMaxResponseBytes(input.maxResponseBytes);
|
|
6978
|
+
const projectRoot = normalizeOptionalString(input.projectRoot);
|
|
6979
|
+
const rollbackId = normalizeOptionalString(input.rollbackId);
|
|
6980
|
+
const envVarName = normalizeOptionalString(input.envVarName);
|
|
6981
|
+
const result = listSsrMockAudits(db, {
|
|
6982
|
+
projectRoot,
|
|
6983
|
+
rollbackId,
|
|
6984
|
+
envVarName,
|
|
6985
|
+
limit,
|
|
6986
|
+
offset,
|
|
6987
|
+
});
|
|
6988
|
+
const bytePage = applyByteBudget(result.audits, maxResponseBytes);
|
|
6989
|
+
const truncated = result.hasMore || bytePage.truncatedByBytes;
|
|
6990
|
+
return {
|
|
6991
|
+
...createBaseResponse(),
|
|
6992
|
+
limitsApplied: {
|
|
6993
|
+
maxResults: limit,
|
|
6994
|
+
truncated,
|
|
6995
|
+
},
|
|
6996
|
+
projectRoot: projectRoot ?? null,
|
|
6997
|
+
rollbackId: rollbackId ?? null,
|
|
6998
|
+
envVarName: envVarName ?? null,
|
|
6999
|
+
pagination: buildOffsetPagination(offset, bytePage.items.length, truncated, maxResponseBytes),
|
|
7000
|
+
responseBytes: bytePage.responseBytes,
|
|
7001
|
+
audits: bytePage.items,
|
|
7002
|
+
nextActions: bytePage.items.length === 0
|
|
7003
|
+
? [{ code: 'DISCOVER_SSR_MOCKABILITY', message: 'Run discover_ssr_mockability or apply_ssr_mock_config to create SSR mock audit rows.' }]
|
|
7004
|
+
: [{ code: 'REVIEW_SSR_MOCK_CLEANUP', message: 'Review the latest audit rows to confirm rollback ids and env file cleanup state.' }],
|
|
7005
|
+
};
|
|
7006
|
+
},
|
|
7007
|
+
create_mock_route: async (input) => {
|
|
7008
|
+
const db = getDb();
|
|
7009
|
+
const record = createMockRouteRecord(input);
|
|
7010
|
+
upsertMockRoute(db, record);
|
|
7011
|
+
const ssrScope = record.sessionScope ?? record.routeId;
|
|
7012
|
+
const ssrMockBasePath = record.mode === 'ssr' || record.mode === 'both'
|
|
7013
|
+
? `/mock/ssr/${encodeURIComponent(ssrScope)}`
|
|
7014
|
+
: undefined;
|
|
7015
|
+
return {
|
|
7016
|
+
...createBaseResponse(),
|
|
7017
|
+
limitsApplied: {
|
|
7018
|
+
maxResults: 1,
|
|
7019
|
+
truncated: false,
|
|
7020
|
+
},
|
|
7021
|
+
route: record,
|
|
7022
|
+
ssrMockBasePath,
|
|
7023
|
+
nextActions: [record.enabled
|
|
7024
|
+
? {
|
|
7025
|
+
code: record.mode === 'browser' ? 'ENABLE_BROWSER_MOCKS' : 'APPLY_SSR_MOCK_CONFIG',
|
|
7026
|
+
message: record.mode === 'browser'
|
|
7027
|
+
? 'Enable browser overrides so matching requests are fulfilled.'
|
|
7028
|
+
: record.mode === 'ssr'
|
|
7029
|
+
? `Point the discovered SSR env var at ${ssrMockBasePath ?? '/mock/ssr/<scope>'}.`
|
|
7030
|
+
: `Use browser overrides or point the discovered SSR env var at ${ssrMockBasePath ?? '/mock/ssr/<scope>'}.`,
|
|
7031
|
+
}
|
|
7032
|
+
: {
|
|
7033
|
+
code: 'ENABLE_MOCK_ROUTE',
|
|
7034
|
+
message: 'Set enabled=true when the route should start affecting browser or SSR traffic.',
|
|
7035
|
+
}],
|
|
7036
|
+
};
|
|
7037
|
+
},
|
|
7038
|
+
update_mock_route: async (input) => {
|
|
7039
|
+
const db = getDb();
|
|
7040
|
+
const routeId = normalizeOptionalString(input.routeId);
|
|
7041
|
+
if (!routeId) {
|
|
7042
|
+
throw new Error('routeId is required');
|
|
7043
|
+
}
|
|
7044
|
+
const existing = getMockRoute(db, routeId);
|
|
7045
|
+
if (!existing) {
|
|
7046
|
+
throw new Error(`Unknown mock route: ${routeId}`);
|
|
7047
|
+
}
|
|
7048
|
+
const record = createMockRouteRecord(input, existing);
|
|
7049
|
+
upsertMockRoute(db, record);
|
|
7050
|
+
return {
|
|
7051
|
+
...createBaseResponse(),
|
|
7052
|
+
limitsApplied: {
|
|
7053
|
+
maxResults: 1,
|
|
7054
|
+
truncated: false,
|
|
7055
|
+
},
|
|
7056
|
+
route: record,
|
|
7057
|
+
nextActions: [{ code: 'REVIEW_MOCK_STATUS', message: 'Review route mode, target URL, and body payload before running the mock.' }],
|
|
7058
|
+
};
|
|
7059
|
+
},
|
|
7060
|
+
delete_mock_route: async (input) => {
|
|
7061
|
+
const db = getDb();
|
|
7062
|
+
const routeId = normalizeOptionalString(input.routeId);
|
|
7063
|
+
if (!routeId) {
|
|
7064
|
+
throw new Error('routeId is required');
|
|
7065
|
+
}
|
|
7066
|
+
const deleted = deleteMockRoute(db, routeId);
|
|
7067
|
+
return {
|
|
7068
|
+
...createBaseResponse(),
|
|
7069
|
+
limitsApplied: {
|
|
7070
|
+
maxResults: 1,
|
|
7071
|
+
truncated: false,
|
|
7072
|
+
},
|
|
7073
|
+
routeId,
|
|
7074
|
+
deleted,
|
|
7075
|
+
nextActions: deleted
|
|
7076
|
+
? [{ code: 'CONFIRM_CLEANUP', message: 'If a runtime was using this route, confirm no active mock run still references it.' }]
|
|
7077
|
+
: [{ code: 'LIST_MOCK_ROUTES', message: 'The route was not found; list persisted mock routes to confirm the intended routeId.' }],
|
|
7078
|
+
};
|
|
7079
|
+
},
|
|
7080
|
+
list_mock_routes: async (input) => {
|
|
7081
|
+
const db = getDb();
|
|
7082
|
+
const limit = resolveLimit(input.limit, DEFAULT_EVENT_LIMIT);
|
|
7083
|
+
const offset = resolveOffset(input.offset);
|
|
7084
|
+
const maxResponseBytes = resolveMaxResponseBytes(input.maxResponseBytes);
|
|
7085
|
+
const result = listMockRoutes(db, {
|
|
7086
|
+
projectRoot: normalizeOptionalString(input.projectRoot),
|
|
7087
|
+
mode: normalizeOptionalString(input.mode) === 'ssr' || normalizeOptionalString(input.mode) === 'both'
|
|
7088
|
+
? normalizeOptionalString(input.mode)
|
|
7089
|
+
: normalizeOptionalString(input.mode) === 'browser'
|
|
7090
|
+
? 'browser'
|
|
7091
|
+
: undefined,
|
|
7092
|
+
enabled: typeof input.enabled === 'boolean' ? input.enabled : undefined,
|
|
7093
|
+
limit,
|
|
7094
|
+
offset,
|
|
7095
|
+
});
|
|
7096
|
+
const bytePage = applyByteBudget(result.routes, maxResponseBytes);
|
|
7097
|
+
const truncated = result.hasMore || bytePage.truncatedByBytes;
|
|
7098
|
+
return {
|
|
7099
|
+
...createBaseResponse(),
|
|
7100
|
+
limitsApplied: {
|
|
7101
|
+
maxResults: limit,
|
|
7102
|
+
truncated,
|
|
7103
|
+
},
|
|
7104
|
+
pagination: buildOffsetPagination(offset, bytePage.items.length, truncated, maxResponseBytes),
|
|
7105
|
+
responseBytes: bytePage.responseBytes,
|
|
7106
|
+
routes: bytePage.items,
|
|
7107
|
+
nextActions: bytePage.items.length === 0
|
|
7108
|
+
? [{ code: 'CREATE_MOCK_ROUTE', message: 'Create a persisted mock route before enabling browser or SSR mocking.' }]
|
|
7109
|
+
: [{ code: 'GET_MOCK_STATUS', message: 'Inspect mock status and recent run or hit records for one route.' }],
|
|
7110
|
+
};
|
|
7111
|
+
},
|
|
7112
|
+
get_mock_route: async (input) => {
|
|
7113
|
+
const db = getDb();
|
|
7114
|
+
const routeId = normalizeOptionalString(input.routeId);
|
|
7115
|
+
if (!routeId) {
|
|
7116
|
+
throw new Error('routeId is required');
|
|
7117
|
+
}
|
|
7118
|
+
const route = getMockRoute(db, routeId);
|
|
7119
|
+
if (!route) {
|
|
7120
|
+
throw new Error(`Unknown mock route: ${routeId}`);
|
|
7121
|
+
}
|
|
7122
|
+
return {
|
|
7123
|
+
...createBaseResponse(),
|
|
7124
|
+
limitsApplied: {
|
|
7125
|
+
maxResults: 1,
|
|
7126
|
+
truncated: false,
|
|
7127
|
+
},
|
|
7128
|
+
route,
|
|
7129
|
+
nextActions: [{ code: 'GET_MOCK_STATUS', message: 'Inspect latest runs and hits before enabling or updating this route.' }],
|
|
7130
|
+
};
|
|
7131
|
+
},
|
|
7132
|
+
get_mock_run_log: async (input) => {
|
|
7133
|
+
const db = getDb();
|
|
7134
|
+
const limit = resolveLimit(input.limit, DEFAULT_EVENT_LIMIT);
|
|
7135
|
+
const offset = resolveOffset(input.offset);
|
|
7136
|
+
const maxResponseBytes = resolveMaxResponseBytes(input.maxResponseBytes);
|
|
7137
|
+
const result = listMockRuns(db, {
|
|
7138
|
+
routeId: normalizeOptionalString(input.routeId),
|
|
7139
|
+
sessionId: normalizeOptionalString(input.sessionId),
|
|
7140
|
+
limit,
|
|
7141
|
+
offset,
|
|
7142
|
+
});
|
|
7143
|
+
const bytePage = applyByteBudget(result.runs, maxResponseBytes);
|
|
7144
|
+
const truncated = result.hasMore || bytePage.truncatedByBytes;
|
|
7145
|
+
return {
|
|
7146
|
+
...createBaseResponse(),
|
|
7147
|
+
limitsApplied: {
|
|
7148
|
+
maxResults: limit,
|
|
7149
|
+
truncated,
|
|
7150
|
+
},
|
|
7151
|
+
pagination: buildOffsetPagination(offset, bytePage.items.length, truncated, maxResponseBytes),
|
|
7152
|
+
responseBytes: bytePage.responseBytes,
|
|
7153
|
+
runs: bytePage.items,
|
|
7154
|
+
nextActions: bytePage.items.length === 0
|
|
7155
|
+
? [{ code: 'ENABLE_BROWSER_MOCKS', message: 'No mock runs exist yet; wire a runtime flow that can create execution records.' }]
|
|
7156
|
+
: [{ code: 'GET_MOCK_HIT_LOG', message: 'Inspect matching hit records for the selected run or route.' }],
|
|
7157
|
+
};
|
|
7158
|
+
},
|
|
7159
|
+
get_mock_hit_log: async (input) => {
|
|
7160
|
+
const db = getDb();
|
|
7161
|
+
const limit = resolveLimit(input.limit, DEFAULT_EVENT_LIMIT);
|
|
7162
|
+
const offset = resolveOffset(input.offset);
|
|
7163
|
+
const maxResponseBytes = resolveMaxResponseBytes(input.maxResponseBytes);
|
|
7164
|
+
const result = listMockHits(db, {
|
|
7165
|
+
routeId: normalizeOptionalString(input.routeId),
|
|
7166
|
+
runId: normalizeOptionalString(input.runId),
|
|
7167
|
+
limit,
|
|
7168
|
+
offset,
|
|
7169
|
+
});
|
|
7170
|
+
const bytePage = applyByteBudget(result.hits, maxResponseBytes);
|
|
7171
|
+
const truncated = result.hasMore || bytePage.truncatedByBytes;
|
|
7172
|
+
return {
|
|
7173
|
+
...createBaseResponse(),
|
|
7174
|
+
limitsApplied: {
|
|
7175
|
+
maxResults: limit,
|
|
7176
|
+
truncated,
|
|
7177
|
+
},
|
|
7178
|
+
pagination: buildOffsetPagination(offset, bytePage.items.length, truncated, maxResponseBytes),
|
|
7179
|
+
responseBytes: bytePage.responseBytes,
|
|
7180
|
+
hits: bytePage.items,
|
|
7181
|
+
nextActions: bytePage.items.length === 0
|
|
7182
|
+
? [{ code: 'RUN_MOCK_ROUTE', message: 'No hit records exist yet; execute the route through a browser or SSR runtime.' }]
|
|
7183
|
+
: [{ code: 'DIAGNOSE_MOCK_ROUTE', message: 'Use hit results to verify whether the route matched and fulfilled as expected.' }],
|
|
7184
|
+
};
|
|
7185
|
+
},
|
|
7186
|
+
get_mock_status: async (input) => {
|
|
7187
|
+
const db = getDb();
|
|
7188
|
+
const routeId = normalizeOptionalString(input.routeId);
|
|
7189
|
+
const projectRoot = normalizeOptionalString(input.projectRoot);
|
|
7190
|
+
const route = routeId ? getMockRoute(db, routeId) : null;
|
|
7191
|
+
const recentRoutes = listMockRoutes(db, { projectRoot, limit: 5, offset: 0 }).routes;
|
|
7192
|
+
const recentRuns = listMockRuns(db, { routeId, limit: 5, offset: 0 }).runs;
|
|
7193
|
+
const recentHits = listMockHits(db, { routeId, limit: 5, offset: 0 }).hits;
|
|
7194
|
+
return {
|
|
7195
|
+
...createBaseResponse(),
|
|
7196
|
+
limitsApplied: {
|
|
7197
|
+
maxResults: 5,
|
|
7198
|
+
truncated: false,
|
|
7199
|
+
},
|
|
7200
|
+
route,
|
|
7201
|
+
recentRoutes,
|
|
7202
|
+
recentRuns,
|
|
7203
|
+
recentHits,
|
|
7204
|
+
nextActions: route
|
|
7205
|
+
? [{ code: 'EXECUTE_MOCK_ROUTE', message: 'Bind the persisted route to browser or SSR execution so runs and hits begin to accumulate.' }]
|
|
7206
|
+
: [{ code: 'CREATE_MOCK_ROUTE', message: 'Persist a mock route first, then inspect status again.' }],
|
|
7207
|
+
};
|
|
7208
|
+
},
|
|
4359
7209
|
get_recent_events: async (input) => {
|
|
4360
7210
|
const db = getDb();
|
|
4361
7211
|
const sessionId = getSessionId(input);
|
|
@@ -5526,6 +8376,113 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
|
|
|
5526
8376
|
chunkBase64: encoding === 'base64' ? chunkBuffer.toString('base64') : undefined,
|
|
5527
8377
|
};
|
|
5528
8378
|
},
|
|
8379
|
+
run_lighthouse_report: async (input) => {
|
|
8380
|
+
const db = getDb();
|
|
8381
|
+
const sessionId = getSessionId(input);
|
|
8382
|
+
const report = await runLighthouseReport(db, {
|
|
8383
|
+
sessionId,
|
|
8384
|
+
url: typeof input.url === 'string' ? input.url : undefined,
|
|
8385
|
+
formFactor: input.formFactor === 'desktop' ? 'desktop' : 'mobile',
|
|
8386
|
+
categories: Array.isArray(input.categories)
|
|
8387
|
+
? input.categories.filter((entry) => typeof entry === 'string')
|
|
8388
|
+
: undefined,
|
|
8389
|
+
maxWaitForLoadMs: typeof input.maxWaitForLoadMs === 'number' ? input.maxWaitForLoadMs : undefined,
|
|
8390
|
+
chromeFlags: Array.isArray(input.chromeFlags)
|
|
8391
|
+
? input.chromeFlags.filter((entry) => typeof entry === 'string')
|
|
8392
|
+
: undefined,
|
|
8393
|
+
});
|
|
8394
|
+
return {
|
|
8395
|
+
...createBaseResponse(sessionId),
|
|
8396
|
+
limitsApplied: {
|
|
8397
|
+
maxResults: 1,
|
|
8398
|
+
truncated: false,
|
|
8399
|
+
},
|
|
8400
|
+
report,
|
|
8401
|
+
};
|
|
8402
|
+
},
|
|
8403
|
+
list_lighthouse_reports: async (input) => {
|
|
8404
|
+
const db = getDb();
|
|
8405
|
+
const result = listLighthouseReports(db, {
|
|
8406
|
+
sessionId: getSessionId(input),
|
|
8407
|
+
urlContains: typeof input.urlContains === 'string' ? input.urlContains : undefined,
|
|
8408
|
+
status: typeof input.status === 'string' ? input.status : undefined,
|
|
8409
|
+
limit: typeof input.limit === 'number' ? input.limit : undefined,
|
|
8410
|
+
offset: typeof input.offset === 'number' ? input.offset : undefined,
|
|
8411
|
+
});
|
|
8412
|
+
return {
|
|
8413
|
+
...createBaseResponse(getSessionId(input)),
|
|
8414
|
+
limitsApplied: {
|
|
8415
|
+
maxResults: result.pagination.limit,
|
|
8416
|
+
truncated: result.pagination.hasMore,
|
|
8417
|
+
},
|
|
8418
|
+
pagination: result.pagination,
|
|
8419
|
+
reports: result.reports,
|
|
8420
|
+
};
|
|
8421
|
+
},
|
|
8422
|
+
get_lighthouse_report: async (input) => {
|
|
8423
|
+
const db = getDb();
|
|
8424
|
+
const reportId = typeof input.reportId === 'string' ? input.reportId : '';
|
|
8425
|
+
if (!reportId) {
|
|
8426
|
+
throw new Error('reportId is required');
|
|
8427
|
+
}
|
|
8428
|
+
const report = getLighthouseReport(db, reportId);
|
|
8429
|
+
return {
|
|
8430
|
+
...createBaseResponse(report.sessionId),
|
|
8431
|
+
limitsApplied: {
|
|
8432
|
+
maxResults: 1,
|
|
8433
|
+
truncated: false,
|
|
8434
|
+
},
|
|
8435
|
+
report,
|
|
8436
|
+
};
|
|
8437
|
+
},
|
|
8438
|
+
get_lighthouse_report_asset: async (input) => {
|
|
8439
|
+
const db = getDb();
|
|
8440
|
+
const reportId = typeof input.reportId === 'string' ? input.reportId : '';
|
|
8441
|
+
if (!reportId) {
|
|
8442
|
+
throw new Error('reportId is required');
|
|
8443
|
+
}
|
|
8444
|
+
const asset = normalizeLighthouseAsset(input.asset);
|
|
8445
|
+
const chunk = getLighthouseReportAsset(db, {
|
|
8446
|
+
reportId,
|
|
8447
|
+
asset,
|
|
8448
|
+
offset: typeof input.offset === 'number' ? input.offset : undefined,
|
|
8449
|
+
maxBytes: typeof input.maxBytes === 'number' ? input.maxBytes : undefined,
|
|
8450
|
+
encoding: input.encoding === 'raw' ? 'raw' : 'base64',
|
|
8451
|
+
});
|
|
8452
|
+
return {
|
|
8453
|
+
...createBaseResponse(),
|
|
8454
|
+
limitsApplied: {
|
|
8455
|
+
maxResults: typeof chunk.bytesReturned === 'number' ? chunk.bytesReturned : 0,
|
|
8456
|
+
truncated: chunk.hasMore === true,
|
|
8457
|
+
},
|
|
8458
|
+
...chunk,
|
|
8459
|
+
};
|
|
8460
|
+
},
|
|
8461
|
+
plan_lighthouse_fixes: async (input) => {
|
|
8462
|
+
const db = getDb();
|
|
8463
|
+
const reportId = typeof input.reportId === 'string' ? input.reportId : '';
|
|
8464
|
+
if (!reportId) {
|
|
8465
|
+
throw new Error('reportId is required');
|
|
8466
|
+
}
|
|
8467
|
+
const plan = planLighthouseFixes(db, {
|
|
8468
|
+
reportId,
|
|
8469
|
+
minPriority: input.minPriority === 'critical' || input.minPriority === 'high' || input.minPriority === 'medium' || input.minPriority === 'low'
|
|
8470
|
+
? input.minPriority
|
|
8471
|
+
: undefined,
|
|
8472
|
+
limit: typeof input.limit === 'number' ? input.limit : undefined,
|
|
8473
|
+
projectRoot: typeof input.projectRoot === 'string' ? input.projectRoot : undefined,
|
|
8474
|
+
routePath: typeof input.routePath === 'string' ? input.routePath : undefined,
|
|
8475
|
+
sourceCandidateLimit: typeof input.sourceCandidateLimit === 'number' ? input.sourceCandidateLimit : undefined,
|
|
8476
|
+
});
|
|
8477
|
+
return {
|
|
8478
|
+
...createBaseResponse(plan.sessionId),
|
|
8479
|
+
limitsApplied: {
|
|
8480
|
+
maxResults: plan.itemCount,
|
|
8481
|
+
truncated: false,
|
|
8482
|
+
},
|
|
8483
|
+
plan,
|
|
8484
|
+
};
|
|
8485
|
+
},
|
|
5529
8486
|
list_automation_runs: async (input) => {
|
|
5530
8487
|
const db = getDb();
|
|
5531
8488
|
const sessionId = getSessionId(input);
|
|
@@ -5562,9 +8519,10 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
|
|
|
5562
8519
|
r.status,
|
|
5563
8520
|
r.started_at,
|
|
5564
8521
|
r.completed_at,
|
|
5565
|
-
|
|
5566
|
-
|
|
5567
|
-
|
|
8522
|
+
r.stop_reason,
|
|
8523
|
+
r.target_summary_json,
|
|
8524
|
+
r.diagnostics_json,
|
|
8525
|
+
r.failure_json,
|
|
5568
8526
|
r.redaction_json,
|
|
5569
8527
|
r.created_at,
|
|
5570
8528
|
r.updated_at,
|
|
@@ -5626,9 +8584,10 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
|
|
|
5626
8584
|
r.status,
|
|
5627
8585
|
r.started_at,
|
|
5628
8586
|
r.completed_at,
|
|
5629
|
-
|
|
5630
|
-
|
|
5631
|
-
|
|
8587
|
+
r.stop_reason,
|
|
8588
|
+
r.target_summary_json,
|
|
8589
|
+
r.diagnostics_json,
|
|
8590
|
+
r.failure_json,
|
|
5632
8591
|
r.redaction_json,
|
|
5633
8592
|
r.created_at,
|
|
5634
8593
|
r.updated_at,
|
|
@@ -5660,9 +8619,10 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
|
|
|
5660
8619
|
started_at,
|
|
5661
8620
|
finished_at,
|
|
5662
8621
|
duration_ms,
|
|
5663
|
-
|
|
5664
|
-
|
|
5665
|
-
|
|
8622
|
+
tab_id,
|
|
8623
|
+
target_summary_json,
|
|
8624
|
+
diagnostics_json,
|
|
8625
|
+
redaction_json,
|
|
5666
8626
|
failure_json,
|
|
5667
8627
|
input_metadata_json,
|
|
5668
8628
|
event_type,
|
|
@@ -5696,12 +8656,14 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
|
|
|
5696
8656
|
const maxItems = resolveStructuredMaxItems(input.maxItems, 40);
|
|
5697
8657
|
const maxTextLength = resolveStructuredTextLength(input.maxTextLength, 80);
|
|
5698
8658
|
const includeButtons = input.includeButtons !== false;
|
|
8659
|
+
const includeLinks = input.includeLinks !== false;
|
|
5699
8660
|
const includeInputs = input.includeInputs !== false;
|
|
5700
8661
|
const includeModals = input.includeModals !== false;
|
|
5701
8662
|
const capture = await executeLiveCapture(captureClient, sessionId, 'CAPTURE_PAGE_STATE', {
|
|
5702
8663
|
maxItems,
|
|
5703
8664
|
maxTextLength,
|
|
5704
8665
|
includeButtons,
|
|
8666
|
+
includeLinks,
|
|
5705
8667
|
includeInputs,
|
|
5706
8668
|
includeModals,
|
|
5707
8669
|
}, 4_000);
|
|
@@ -6376,7 +9338,10 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
|
|
|
6376
9338
|
throw new Error('selector is required');
|
|
6377
9339
|
}
|
|
6378
9340
|
const properties = asStringArray(input.properties, 64);
|
|
6379
|
-
const
|
|
9341
|
+
const frameId = typeof input.frameId === 'number' && Number.isFinite(input.frameId)
|
|
9342
|
+
? Math.max(0, Math.floor(input.frameId))
|
|
9343
|
+
: 0;
|
|
9344
|
+
const capture = await executeLiveCapture(captureClient, sessionId, 'CAPTURE_COMPUTED_STYLES', { selector, frameId, properties }, 3_000);
|
|
6380
9345
|
return {
|
|
6381
9346
|
...createBaseResponse(sessionId),
|
|
6382
9347
|
limitsApplied: {
|
|
@@ -6392,7 +9357,10 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
|
|
|
6392
9357
|
throw new Error('sessionId is required');
|
|
6393
9358
|
}
|
|
6394
9359
|
const selector = typeof input.selector === 'string' ? input.selector : undefined;
|
|
6395
|
-
const
|
|
9360
|
+
const frameId = typeof input.frameId === 'number' && Number.isFinite(input.frameId)
|
|
9361
|
+
? Math.max(0, Math.floor(input.frameId))
|
|
9362
|
+
: 0;
|
|
9363
|
+
const capture = await executeLiveCapture(captureClient, sessionId, 'CAPTURE_LAYOUT_METRICS', { selector, frameId }, 3_000);
|
|
6396
9364
|
return {
|
|
6397
9365
|
...createBaseResponse(sessionId),
|
|
6398
9366
|
limitsApplied: {
|
|
@@ -6423,6 +9391,7 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
|
|
|
6423
9391
|
const normalizedInput = {
|
|
6424
9392
|
...input,
|
|
6425
9393
|
includeButtons: kinds.includes('buttons'),
|
|
9394
|
+
includeLinks: kinds.includes('links'),
|
|
6426
9395
|
includeInputs: kinds.includes('inputs'),
|
|
6427
9396
|
includeModals: kinds.includes('modals'),
|
|
6428
9397
|
};
|
|
@@ -6503,6 +9472,247 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
|
|
|
6503
9472
|
...waited,
|
|
6504
9473
|
};
|
|
6505
9474
|
},
|
|
9475
|
+
preflight_automation_flow: async (input) => {
|
|
9476
|
+
const sessionId = getSessionId(input);
|
|
9477
|
+
if (!sessionId) {
|
|
9478
|
+
throw new Error('sessionId is required');
|
|
9479
|
+
}
|
|
9480
|
+
const preflight = await buildAutomationFlowPreflight({
|
|
9481
|
+
sessionId,
|
|
9482
|
+
input,
|
|
9483
|
+
capturePageState,
|
|
9484
|
+
getDb,
|
|
9485
|
+
getSessionConnectionState,
|
|
9486
|
+
});
|
|
9487
|
+
return {
|
|
9488
|
+
...createBaseResponse(sessionId),
|
|
9489
|
+
limitsApplied: {
|
|
9490
|
+
maxResults: 1,
|
|
9491
|
+
truncated: false,
|
|
9492
|
+
},
|
|
9493
|
+
...preflight,
|
|
9494
|
+
};
|
|
9495
|
+
},
|
|
9496
|
+
wait_for_url: async (input) => {
|
|
9497
|
+
const sessionId = getSessionId(input);
|
|
9498
|
+
if (!sessionId) {
|
|
9499
|
+
throw new Error('sessionId is required');
|
|
9500
|
+
}
|
|
9501
|
+
const wait = AutomationWaitUrlSchema.parse({ ...input, waitKind: 'url' });
|
|
9502
|
+
const waited = await waitForUrlCondition(sessionId, wait, capturePageState);
|
|
9503
|
+
return {
|
|
9504
|
+
...createBaseResponse(sessionId),
|
|
9505
|
+
limitsApplied: {
|
|
9506
|
+
maxResults: 1,
|
|
9507
|
+
truncated: false,
|
|
9508
|
+
},
|
|
9509
|
+
...waited,
|
|
9510
|
+
};
|
|
9511
|
+
},
|
|
9512
|
+
wait_for_navigation: async (input) => {
|
|
9513
|
+
const sessionId = getSessionId(input);
|
|
9514
|
+
if (!sessionId) {
|
|
9515
|
+
throw new Error('sessionId is required');
|
|
9516
|
+
}
|
|
9517
|
+
if (!getDb) {
|
|
9518
|
+
throw new Error('wait_for_navigation requires database access');
|
|
9519
|
+
}
|
|
9520
|
+
const wait = AutomationWaitNavigationSchema.parse({ ...input, waitKind: 'navigation' });
|
|
9521
|
+
const waited = await waitForNavigationCondition(sessionId, wait, getDb());
|
|
9522
|
+
return {
|
|
9523
|
+
...createBaseResponse(sessionId),
|
|
9524
|
+
limitsApplied: {
|
|
9525
|
+
maxResults: 10,
|
|
9526
|
+
truncated: false,
|
|
9527
|
+
},
|
|
9528
|
+
...waited,
|
|
9529
|
+
};
|
|
9530
|
+
},
|
|
9531
|
+
wait_for_navigation_lifecycle: async (input) => {
|
|
9532
|
+
const sessionId = getSessionId(input);
|
|
9533
|
+
if (!sessionId) {
|
|
9534
|
+
throw new Error('sessionId is required');
|
|
9535
|
+
}
|
|
9536
|
+
const wait = AutomationWaitNavigationLifecycleSchema.parse({ ...input, waitKind: 'navigation_lifecycle' });
|
|
9537
|
+
const waited = await waitForNavigationLifecycleCondition(sessionId, wait, captureClient);
|
|
9538
|
+
return {
|
|
9539
|
+
...createBaseResponse(sessionId),
|
|
9540
|
+
limitsApplied: {
|
|
9541
|
+
maxResults: 1,
|
|
9542
|
+
truncated: false,
|
|
9543
|
+
},
|
|
9544
|
+
...waited,
|
|
9545
|
+
};
|
|
9546
|
+
},
|
|
9547
|
+
wait_for_load_state: async (input) => {
|
|
9548
|
+
const sessionId = getSessionId(input);
|
|
9549
|
+
if (!sessionId) {
|
|
9550
|
+
throw new Error('sessionId is required');
|
|
9551
|
+
}
|
|
9552
|
+
const wait = AutomationWaitLoadStateSchema.parse({ ...input, waitKind: 'load_state' });
|
|
9553
|
+
const waited = await waitForLoadStateCondition(sessionId, wait, capturePageState);
|
|
9554
|
+
return {
|
|
9555
|
+
...createBaseResponse(sessionId),
|
|
9556
|
+
limitsApplied: {
|
|
9557
|
+
maxResults: 1,
|
|
9558
|
+
truncated: false,
|
|
9559
|
+
},
|
|
9560
|
+
...waited,
|
|
9561
|
+
};
|
|
9562
|
+
},
|
|
9563
|
+
wait_for_selector_state: async (input) => {
|
|
9564
|
+
const sessionId = getSessionId(input);
|
|
9565
|
+
if (!sessionId) {
|
|
9566
|
+
throw new Error('sessionId is required');
|
|
9567
|
+
}
|
|
9568
|
+
const wait = AutomationWaitSelectorStateSchema.parse({ ...input, waitKind: 'selector_state' });
|
|
9569
|
+
const waited = await waitForSelectorStateCondition(sessionId, wait, captureClient);
|
|
9570
|
+
return {
|
|
9571
|
+
...createBaseResponse(sessionId),
|
|
9572
|
+
limitsApplied: {
|
|
9573
|
+
maxResults: 1,
|
|
9574
|
+
truncated: false,
|
|
9575
|
+
},
|
|
9576
|
+
...waited,
|
|
9577
|
+
};
|
|
9578
|
+
},
|
|
9579
|
+
wait_for_request: async (input) => {
|
|
9580
|
+
const sessionId = getSessionId(input);
|
|
9581
|
+
if (!sessionId) {
|
|
9582
|
+
throw new Error('sessionId is required');
|
|
9583
|
+
}
|
|
9584
|
+
if (!getDb) {
|
|
9585
|
+
throw new Error('wait_for_request requires database access');
|
|
9586
|
+
}
|
|
9587
|
+
const wait = AutomationWaitRequestSchema.parse({ ...input, waitKind: 'request' });
|
|
9588
|
+
const waited = await waitForNetworkMatchCondition(sessionId, wait, getDb());
|
|
9589
|
+
return {
|
|
9590
|
+
...createBaseResponse(sessionId),
|
|
9591
|
+
limitsApplied: {
|
|
9592
|
+
maxResults: 10,
|
|
9593
|
+
truncated: false,
|
|
9594
|
+
},
|
|
9595
|
+
...waited,
|
|
9596
|
+
};
|
|
9597
|
+
},
|
|
9598
|
+
wait_for_response: async (input) => {
|
|
9599
|
+
const sessionId = getSessionId(input);
|
|
9600
|
+
if (!sessionId) {
|
|
9601
|
+
throw new Error('sessionId is required');
|
|
9602
|
+
}
|
|
9603
|
+
if (!getDb) {
|
|
9604
|
+
throw new Error('wait_for_response requires database access');
|
|
9605
|
+
}
|
|
9606
|
+
const wait = AutomationWaitResponseSchema.parse({ ...input, waitKind: 'response' });
|
|
9607
|
+
const waited = await waitForNetworkMatchCondition(sessionId, wait, getDb());
|
|
9608
|
+
return {
|
|
9609
|
+
...createBaseResponse(sessionId),
|
|
9610
|
+
limitsApplied: {
|
|
9611
|
+
maxResults: 10,
|
|
9612
|
+
truncated: false,
|
|
9613
|
+
},
|
|
9614
|
+
...waited,
|
|
9615
|
+
};
|
|
9616
|
+
},
|
|
9617
|
+
wait_for_console: async (input) => {
|
|
9618
|
+
const sessionId = getSessionId(input);
|
|
9619
|
+
if (!sessionId) {
|
|
9620
|
+
throw new Error('sessionId is required');
|
|
9621
|
+
}
|
|
9622
|
+
const wait = AutomationWaitConsoleSchema.parse({ ...input, waitKind: 'console' });
|
|
9623
|
+
const waited = await waitForConsoleCondition(sessionId, wait, captureClient);
|
|
9624
|
+
return {
|
|
9625
|
+
...createBaseResponse(sessionId),
|
|
9626
|
+
limitsApplied: {
|
|
9627
|
+
maxResults: 10,
|
|
9628
|
+
truncated: false,
|
|
9629
|
+
},
|
|
9630
|
+
...waited,
|
|
9631
|
+
};
|
|
9632
|
+
},
|
|
9633
|
+
wait_for_dialog: async (input) => {
|
|
9634
|
+
const sessionId = getSessionId(input);
|
|
9635
|
+
if (!sessionId) {
|
|
9636
|
+
throw new Error('sessionId is required');
|
|
9637
|
+
}
|
|
9638
|
+
const wait = AutomationWaitDialogSchema.parse({ ...input, waitKind: 'dialog' });
|
|
9639
|
+
const waited = await waitForDialogCondition(sessionId, wait, captureClient);
|
|
9640
|
+
return {
|
|
9641
|
+
...createBaseResponse(sessionId),
|
|
9642
|
+
limitsApplied: {
|
|
9643
|
+
maxResults: 1,
|
|
9644
|
+
truncated: false,
|
|
9645
|
+
},
|
|
9646
|
+
...waited,
|
|
9647
|
+
};
|
|
9648
|
+
},
|
|
9649
|
+
wait_for_stable_layout: async (input) => {
|
|
9650
|
+
const sessionId = getSessionId(input);
|
|
9651
|
+
if (!sessionId) {
|
|
9652
|
+
throw new Error('sessionId is required');
|
|
9653
|
+
}
|
|
9654
|
+
const wait = AutomationWaitStableLayoutSchema.parse({ ...input, waitKind: 'stable_layout' });
|
|
9655
|
+
const waited = await waitForStableLayoutCondition(sessionId, wait, captureClient);
|
|
9656
|
+
return {
|
|
9657
|
+
...createBaseResponse(sessionId),
|
|
9658
|
+
limitsApplied: {
|
|
9659
|
+
maxResults: 1,
|
|
9660
|
+
truncated: false,
|
|
9661
|
+
},
|
|
9662
|
+
...waited,
|
|
9663
|
+
};
|
|
9664
|
+
},
|
|
9665
|
+
wait_for_download: async (input) => {
|
|
9666
|
+
const sessionId = getSessionId(input);
|
|
9667
|
+
if (!sessionId) {
|
|
9668
|
+
throw new Error('sessionId is required');
|
|
9669
|
+
}
|
|
9670
|
+
const wait = AutomationWaitDownloadSchema.parse({ ...input, waitKind: 'download' });
|
|
9671
|
+
const waited = await waitForDownloadCondition(sessionId, wait, captureClient);
|
|
9672
|
+
return {
|
|
9673
|
+
...createBaseResponse(sessionId),
|
|
9674
|
+
limitsApplied: {
|
|
9675
|
+
maxResults: 1,
|
|
9676
|
+
truncated: false,
|
|
9677
|
+
},
|
|
9678
|
+
...waited,
|
|
9679
|
+
};
|
|
9680
|
+
},
|
|
9681
|
+
wait_for_popup: async (input) => {
|
|
9682
|
+
const sessionId = getSessionId(input);
|
|
9683
|
+
if (!sessionId) {
|
|
9684
|
+
throw new Error('sessionId is required');
|
|
9685
|
+
}
|
|
9686
|
+
const wait = AutomationWaitPopupSchema.parse({ ...input, waitKind: 'popup' });
|
|
9687
|
+
const waited = await waitForPopupCondition(sessionId, wait, captureClient);
|
|
9688
|
+
return {
|
|
9689
|
+
...createBaseResponse(sessionId),
|
|
9690
|
+
limitsApplied: {
|
|
9691
|
+
maxResults: 1,
|
|
9692
|
+
truncated: false,
|
|
9693
|
+
},
|
|
9694
|
+
...waited,
|
|
9695
|
+
};
|
|
9696
|
+
},
|
|
9697
|
+
wait_for_network_quiet: async (input) => {
|
|
9698
|
+
const sessionId = getSessionId(input);
|
|
9699
|
+
if (!sessionId) {
|
|
9700
|
+
throw new Error('sessionId is required');
|
|
9701
|
+
}
|
|
9702
|
+
if (!getDb) {
|
|
9703
|
+
throw new Error('wait_for_network_quiet requires database access');
|
|
9704
|
+
}
|
|
9705
|
+
const wait = AutomationWaitNetworkQuietSchema.parse({ ...input, waitKind: 'network_quiet' });
|
|
9706
|
+
const waited = await waitForNetworkQuietCondition(sessionId, wait, getDb());
|
|
9707
|
+
return {
|
|
9708
|
+
...createBaseResponse(sessionId),
|
|
9709
|
+
limitsApplied: {
|
|
9710
|
+
maxResults: 10,
|
|
9711
|
+
truncated: false,
|
|
9712
|
+
},
|
|
9713
|
+
...waited,
|
|
9714
|
+
};
|
|
9715
|
+
},
|
|
6506
9716
|
run_ui_steps: async (input) => {
|
|
6507
9717
|
const request = RunUIStepsSchema.parse(input);
|
|
6508
9718
|
const workflowTraceId = createUIWorkflowTraceId();
|
|
@@ -6530,7 +9740,16 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
|
|
|
6530
9740
|
const previousCapture = lastPageCapture;
|
|
6531
9741
|
try {
|
|
6532
9742
|
if (step.kind === 'action') {
|
|
6533
|
-
const resolvedTarget =
|
|
9743
|
+
const resolvedTarget = step.target?.locator
|
|
9744
|
+
? {
|
|
9745
|
+
target: step.target,
|
|
9746
|
+
resolution: {
|
|
9747
|
+
strategy: 'native_locator_pending',
|
|
9748
|
+
matcher: summarizeWorkflowTargetMatcher(step.target),
|
|
9749
|
+
},
|
|
9750
|
+
pageCapture: undefined,
|
|
9751
|
+
}
|
|
9752
|
+
: await resolveWorkflowActionTarget(request.sessionId, step.target, workflowCapturePageState, request.mode === 'fast' ? lastPageCapture : undefined);
|
|
6534
9753
|
const liveRequest = LiveUIActionRequestSchema.parse({
|
|
6535
9754
|
action: step.action,
|
|
6536
9755
|
target: resolvedTarget.target,
|
|
@@ -6541,6 +9760,12 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
|
|
|
6541
9760
|
const payload = ensureCaptureSuccess(capture, request.sessionId);
|
|
6542
9761
|
const actionResult = payload;
|
|
6543
9762
|
const failed = actionResult.status === 'failed' || actionResult.status === 'rejected';
|
|
9763
|
+
const actionResultPayload = typeof actionResult.result === 'object' && actionResult.result !== null
|
|
9764
|
+
? actionResult.result
|
|
9765
|
+
: undefined;
|
|
9766
|
+
const nativeLocatorResolution = typeof actionResultPayload?.locatorResolution === 'object' && actionResultPayload.locatorResolution !== null
|
|
9767
|
+
? actionResultPayload.locatorResolution
|
|
9768
|
+
: undefined;
|
|
6544
9769
|
let currentCapture = resolvedTarget.pageCapture ?? lastPageCapture;
|
|
6545
9770
|
if (!failed && request.mode === 'fast') {
|
|
6546
9771
|
await sleep(75);
|
|
@@ -6555,7 +9780,7 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
|
|
|
6555
9780
|
action: step.action,
|
|
6556
9781
|
traceId: actionResult.traceId,
|
|
6557
9782
|
target: {
|
|
6558
|
-
resolution: resolvedTarget.resolution,
|
|
9783
|
+
resolution: nativeLocatorResolution ?? resolvedTarget.resolution,
|
|
6559
9784
|
actionTarget: typeof actionResult.target === 'object' && actionResult.target !== null
|
|
6560
9785
|
? actionResult.target
|
|
6561
9786
|
: undefined,
|
|
@@ -6568,6 +9793,14 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
|
|
|
6568
9793
|
: undefined,
|
|
6569
9794
|
pageChangeSummary: createPageChangeSummary(previousCapture, currentCapture),
|
|
6570
9795
|
};
|
|
9796
|
+
if (failed && getDb && finalStepResult.traceId) {
|
|
9797
|
+
mergeAutomationDiagnosticsEvidence(getDb(), {
|
|
9798
|
+
sessionId: request.sessionId,
|
|
9799
|
+
traceId: finalStepResult.traceId,
|
|
9800
|
+
failureEvidence: finalStepResult.failureEvidence,
|
|
9801
|
+
cdpFailure: actionResult.failureReason,
|
|
9802
|
+
});
|
|
9803
|
+
}
|
|
6571
9804
|
}
|
|
6572
9805
|
else if (step.kind === 'waitFor') {
|
|
6573
9806
|
const waitInput = {
|
|
@@ -6595,6 +9828,48 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
|
|
|
6595
9828
|
pageChangeSummary: createPageChangeSummary(previousCapture, waited.lastCapture),
|
|
6596
9829
|
};
|
|
6597
9830
|
}
|
|
9831
|
+
else if (step.kind === 'wait') {
|
|
9832
|
+
const waitSpec = AutomationWaitSpecSchema.parse({
|
|
9833
|
+
...step.wait,
|
|
9834
|
+
timeoutMs: step.wait.timeoutMs ?? request.defaultTimeoutMs,
|
|
9835
|
+
pollIntervalMs: step.wait.pollIntervalMs ?? request.defaultPollIntervalMs,
|
|
9836
|
+
});
|
|
9837
|
+
const waited = await runAutomationWait({
|
|
9838
|
+
sessionId: request.sessionId,
|
|
9839
|
+
wait: waitSpec,
|
|
9840
|
+
capturePageState: workflowCapturePageState,
|
|
9841
|
+
captureClient,
|
|
9842
|
+
getDb,
|
|
9843
|
+
});
|
|
9844
|
+
if (waited.waitKind === 'url' || waited.waitKind === 'navigation' || waited.waitKind === 'load_state') {
|
|
9845
|
+
lastPageCapture = await workflowCapturePageState(request.sessionId, {
|
|
9846
|
+
includeButtons: true,
|
|
9847
|
+
includeLinks: true,
|
|
9848
|
+
includeInputs: true,
|
|
9849
|
+
includeModals: true,
|
|
9850
|
+
maxItems: request.mode === 'fast' ? 12 : 20,
|
|
9851
|
+
maxTextLength: request.mode === 'fast' ? 60 : 80,
|
|
9852
|
+
}).catch(() => lastPageCapture);
|
|
9853
|
+
}
|
|
9854
|
+
finalStepResult = {
|
|
9855
|
+
id: stepId,
|
|
9856
|
+
kind: step.kind,
|
|
9857
|
+
status: waited.matched ? 'succeeded' : 'failed',
|
|
9858
|
+
durationMs: Math.max(0, Date.now() - startedAt),
|
|
9859
|
+
wait: {
|
|
9860
|
+
...waitSpec,
|
|
9861
|
+
waitKind: waited.waitKind,
|
|
9862
|
+
matched: waited.matched,
|
|
9863
|
+
timeoutMs: waited.timeoutMs,
|
|
9864
|
+
pollIntervalMs: waited.pollIntervalMs,
|
|
9865
|
+
},
|
|
9866
|
+
waitedMs: waited.waitedMs,
|
|
9867
|
+
attempts: waited.attempts,
|
|
9868
|
+
error: waited.error,
|
|
9869
|
+
pageChangeSummary: createPageChangeSummary(previousCapture, lastPageCapture),
|
|
9870
|
+
target: waited.evidence,
|
|
9871
|
+
};
|
|
9872
|
+
}
|
|
6598
9873
|
else {
|
|
6599
9874
|
const capture = request.mode === 'fast' && lastPageCapture
|
|
6600
9875
|
? lastPageCapture
|
|
@@ -6629,7 +9904,8 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
|
|
|
6629
9904
|
target: step.kind === 'action' && workflowError
|
|
6630
9905
|
? workflowError.details
|
|
6631
9906
|
: undefined,
|
|
6632
|
-
matcher: step.kind === '
|
|
9907
|
+
matcher: step.kind === 'assert' || step.kind === 'waitFor' ? step.matcher : undefined,
|
|
9908
|
+
wait: step.kind === 'wait' ? step.wait : undefined,
|
|
6633
9909
|
error: normalizeWorkflowError(error),
|
|
6634
9910
|
};
|
|
6635
9911
|
}
|
|
@@ -6652,6 +9928,14 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
|
|
|
6652
9928
|
if (evidence) {
|
|
6653
9929
|
failureCaptureCount += 1;
|
|
6654
9930
|
finalStepResult.failureEvidence = evidence;
|
|
9931
|
+
if (getDb && finalStepResult.traceId) {
|
|
9932
|
+
mergeAutomationDiagnosticsEvidence(getDb(), {
|
|
9933
|
+
sessionId: request.sessionId,
|
|
9934
|
+
traceId: finalStepResult.traceId,
|
|
9935
|
+
failureEvidence: evidence,
|
|
9936
|
+
cdpFailure: finalStepResult.error,
|
|
9937
|
+
});
|
|
9938
|
+
}
|
|
6655
9939
|
}
|
|
6656
9940
|
}
|
|
6657
9941
|
stepResults.push(finalStepResult);
|
|
@@ -6671,7 +9955,8 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
|
|
|
6671
9955
|
status: 'skipped',
|
|
6672
9956
|
durationMs: 0,
|
|
6673
9957
|
action: step.kind === 'action' ? step.action : undefined,
|
|
6674
|
-
matcher: step.kind === '
|
|
9958
|
+
matcher: step.kind === 'assert' || step.kind === 'waitFor' ? step.matcher : undefined,
|
|
9959
|
+
wait: step.kind === 'wait' ? step.wait : undefined,
|
|
6675
9960
|
pageChangeSummary: undefined,
|
|
6676
9961
|
error: {
|
|
6677
9962
|
code: 'workflow_stopped_early',
|
|
@@ -6871,7 +10156,62 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
|
|
|
6871
10156
|
const actionInput = { ...input };
|
|
6872
10157
|
delete actionInput.sessionId;
|
|
6873
10158
|
delete actionInput.captureOnFailure;
|
|
6874
|
-
|
|
10159
|
+
let request = LiveUIActionRequestSchema.parse(actionInput);
|
|
10160
|
+
let targetResolution;
|
|
10161
|
+
try {
|
|
10162
|
+
if (request.target?.locator) {
|
|
10163
|
+
targetResolution = {
|
|
10164
|
+
strategy: 'native_locator_pending',
|
|
10165
|
+
matcher: summarizeWorkflowTargetMatcher(request.target),
|
|
10166
|
+
};
|
|
10167
|
+
}
|
|
10168
|
+
else if (hasSemanticActionTargetMatcher(request.target)) {
|
|
10169
|
+
const resolvedTarget = await resolveWorkflowActionTarget(sessionId, request.target, capturePageState);
|
|
10170
|
+
targetResolution = resolvedTarget.resolution;
|
|
10171
|
+
request = LiveUIActionRequestSchema.parse({
|
|
10172
|
+
...request,
|
|
10173
|
+
target: resolvedTarget.target,
|
|
10174
|
+
});
|
|
10175
|
+
}
|
|
10176
|
+
}
|
|
10177
|
+
catch (error) {
|
|
10178
|
+
if (error instanceof WorkflowTargetResolutionError) {
|
|
10179
|
+
const traceId = request.traceId ?? `uiaction-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
10180
|
+
return {
|
|
10181
|
+
...createBaseResponse(sessionId),
|
|
10182
|
+
limitsApplied: {
|
|
10183
|
+
maxResults: 1,
|
|
10184
|
+
truncated: false,
|
|
10185
|
+
},
|
|
10186
|
+
action: request.action,
|
|
10187
|
+
status: 'rejected',
|
|
10188
|
+
traceId,
|
|
10189
|
+
startedAt: Date.now(),
|
|
10190
|
+
finishedAt: Date.now(),
|
|
10191
|
+
durationMs: 0,
|
|
10192
|
+
target: {
|
|
10193
|
+
matched: false,
|
|
10194
|
+
},
|
|
10195
|
+
tabContext: {
|
|
10196
|
+
frameId: 0,
|
|
10197
|
+
},
|
|
10198
|
+
failureDetails: {
|
|
10199
|
+
code: error.code,
|
|
10200
|
+
message: error.message,
|
|
10201
|
+
},
|
|
10202
|
+
targetResolution: {
|
|
10203
|
+
...error.details,
|
|
10204
|
+
strategy: 'semantic_failed',
|
|
10205
|
+
},
|
|
10206
|
+
supportedScopes: {
|
|
10207
|
+
executionScope: 'top-document-v1',
|
|
10208
|
+
topDocumentOnly: false,
|
|
10209
|
+
opensNewBrowserSession: false,
|
|
10210
|
+
},
|
|
10211
|
+
};
|
|
10212
|
+
}
|
|
10213
|
+
throw error;
|
|
10214
|
+
}
|
|
6875
10215
|
const failureCaptureOptions = resolveFailureEvidenceCaptureOptions(input);
|
|
6876
10216
|
const capture = await executeLiveCapture(captureClient, sessionId, 'EXECUTE_UI_ACTION', request, 5_000);
|
|
6877
10217
|
const payload = ensureCaptureSuccess(capture, sessionId);
|
|
@@ -6896,6 +10236,24 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
|
|
|
6896
10236
|
const target = typeof actionResult.target === 'object' && actionResult.target !== null
|
|
6897
10237
|
? actionResult.target
|
|
6898
10238
|
: {};
|
|
10239
|
+
const actionResultRecord = actionResult;
|
|
10240
|
+
const nativeResult = typeof actionResultRecord.result === 'object' && actionResultRecord.result !== null
|
|
10241
|
+
? actionResultRecord.result
|
|
10242
|
+
: undefined;
|
|
10243
|
+
const nativeLocatorResolution = typeof nativeResult?.locatorResolution === 'object' && nativeResult.locatorResolution !== null
|
|
10244
|
+
? nativeResult.locatorResolution
|
|
10245
|
+
: undefined;
|
|
10246
|
+
if (nativeLocatorResolution) {
|
|
10247
|
+
targetResolution = nativeLocatorResolution;
|
|
10248
|
+
}
|
|
10249
|
+
if (failed && getDb && actionResult.traceId) {
|
|
10250
|
+
mergeAutomationDiagnosticsEvidence(getDb(), {
|
|
10251
|
+
sessionId,
|
|
10252
|
+
traceId: actionResult.traceId,
|
|
10253
|
+
failureEvidence,
|
|
10254
|
+
cdpFailure: actionResult.failureReason,
|
|
10255
|
+
});
|
|
10256
|
+
}
|
|
6899
10257
|
return {
|
|
6900
10258
|
...createBaseResponse(sessionId),
|
|
6901
10259
|
limitsApplied: {
|
|
@@ -6914,6 +10272,7 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
|
|
|
6914
10272
|
: undefined,
|
|
6915
10273
|
actionResult,
|
|
6916
10274
|
target,
|
|
10275
|
+
targetResolution,
|
|
6917
10276
|
tabContext: {
|
|
6918
10277
|
tabId: typeof target.tabId === 'number' ? target.tabId : undefined,
|
|
6919
10278
|
frameId: typeof target.frameId === 'number' ? target.frameId : 0,
|
|
@@ -6924,7 +10283,7 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
|
|
|
6924
10283
|
postActionState,
|
|
6925
10284
|
supportedScopes: {
|
|
6926
10285
|
executionScope: actionResult.executionScope,
|
|
6927
|
-
topDocumentOnly:
|
|
10286
|
+
topDocumentOnly: false,
|
|
6928
10287
|
opensNewBrowserSession: false,
|
|
6929
10288
|
},
|
|
6930
10289
|
};
|
|
@@ -6976,13 +10335,37 @@ export function createToolRegistry(overrides = {}) {
|
|
|
6976
10335
|
};
|
|
6977
10336
|
});
|
|
6978
10337
|
}
|
|
6979
|
-
export async function routeToolCall(tools, toolName, input) {
|
|
10338
|
+
export async function routeToolCall(tools, toolName, input, options = {}) {
|
|
6980
10339
|
const tool = tools.find((candidate) => candidate.name === toolName);
|
|
6981
10340
|
if (!tool) {
|
|
6982
10341
|
throw new Error(`Unknown tool: ${toolName}`);
|
|
6983
10342
|
}
|
|
6984
|
-
const
|
|
6985
|
-
|
|
10343
|
+
const normalizedInput = isRecord(input) ? input : {};
|
|
10344
|
+
const guardCall = options.loopGuard?.prepareCall(toolName, normalizedInput);
|
|
10345
|
+
const beforeCall = guardCall ? await options.loopGuard?.beforeCall(guardCall) : undefined;
|
|
10346
|
+
if (beforeCall?.blocked) {
|
|
10347
|
+
return attachResponseBytes(beforeCall.response);
|
|
10348
|
+
}
|
|
10349
|
+
const startedAt = Date.now();
|
|
10350
|
+
try {
|
|
10351
|
+
const response = await tool.handler(normalizedInput);
|
|
10352
|
+
const guarded = guardCall
|
|
10353
|
+
? await options.loopGuard?.afterCall(guardCall, {
|
|
10354
|
+
response,
|
|
10355
|
+
durationMs: Date.now() - startedAt,
|
|
10356
|
+
})
|
|
10357
|
+
: undefined;
|
|
10358
|
+
return attachResponseBytes((guarded?.response ?? response));
|
|
10359
|
+
}
|
|
10360
|
+
catch (error) {
|
|
10361
|
+
if (guardCall) {
|
|
10362
|
+
await options.loopGuard?.afterCall(guardCall, {
|
|
10363
|
+
error,
|
|
10364
|
+
durationMs: Date.now() - startedAt,
|
|
10365
|
+
});
|
|
10366
|
+
}
|
|
10367
|
+
throw error;
|
|
10368
|
+
}
|
|
6986
10369
|
}
|
|
6987
10370
|
export function createMCPServer(overrides = {}, options = {}) {
|
|
6988
10371
|
const logger = options.logger ?? createDefaultMcpLogger();
|
|
@@ -6994,6 +10377,17 @@ export function createMCPServer(overrides = {}, options = {}) {
|
|
|
6994
10377
|
...v2Handlers,
|
|
6995
10378
|
...overrides,
|
|
6996
10379
|
});
|
|
10380
|
+
const loopGuard = options.loopGuard === false
|
|
10381
|
+
? undefined
|
|
10382
|
+
: options.loopGuard ?? createToolLoopGuard({
|
|
10383
|
+
getDb: () => getConnection().db,
|
|
10384
|
+
onEvent: (event) => {
|
|
10385
|
+
logger.info({
|
|
10386
|
+
component: 'mcp',
|
|
10387
|
+
...event,
|
|
10388
|
+
}, `[MCPServer][MCP] ${event.event}`);
|
|
10389
|
+
},
|
|
10390
|
+
});
|
|
6997
10391
|
const server = new Server({
|
|
6998
10392
|
name: 'browser-debug-mcp-bridge',
|
|
6999
10393
|
version: '1.0.0',
|
|
@@ -7017,7 +10411,7 @@ export function createMCPServer(overrides = {}, options = {}) {
|
|
|
7017
10411
|
const startedAt = Date.now();
|
|
7018
10412
|
logger.info({ component: 'mcp', event: 'tool_call_started', toolName }, '[MCPServer][MCP] Tool call started');
|
|
7019
10413
|
try {
|
|
7020
|
-
const response = await routeToolCall(tools, toolName, request.params.arguments);
|
|
10414
|
+
const response = await routeToolCall(tools, toolName, request.params.arguments, { loopGuard });
|
|
7021
10415
|
logger.info({
|
|
7022
10416
|
component: 'mcp',
|
|
7023
10417
|
event: 'tool_call_completed',
|