pi-agent-browser-native 0.2.47 → 0.2.49
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/CHANGELOG.md +63 -19
- package/README.md +52 -19
- package/dist/extensions/agent-browser/index.js +785 -0
- package/dist/extensions/agent-browser/lib/argv-descriptor.js +71 -0
- package/dist/extensions/agent-browser/lib/argv-grammar.js +121 -0
- package/dist/extensions/agent-browser/lib/bash-guard.js +190 -0
- package/dist/extensions/agent-browser/lib/command-policy.js +85 -0
- package/dist/extensions/agent-browser/lib/command-taxonomy.js +302 -0
- package/dist/extensions/agent-browser/lib/config-policy.js +686 -0
- package/dist/extensions/agent-browser/lib/config.js +122 -0
- package/dist/extensions/agent-browser/lib/electron/cdp.js +51 -0
- package/dist/extensions/agent-browser/lib/electron/cleanup.js +212 -0
- package/dist/extensions/agent-browser/lib/electron/discovery.js +633 -0
- package/dist/extensions/agent-browser/lib/electron/launch.js +351 -0
- package/{extensions/agent-browser/lib/electron/text.ts → dist/extensions/agent-browser/lib/electron/text.js} +5 -5
- package/dist/extensions/agent-browser/lib/executable-path.js +20 -0
- package/dist/extensions/agent-browser/lib/fs-utils.js +18 -0
- package/dist/extensions/agent-browser/lib/input-modes/electron.js +165 -0
- package/dist/extensions/agent-browser/lib/input-modes/job.js +519 -0
- package/dist/extensions/agent-browser/lib/input-modes/lookups.js +440 -0
- package/dist/extensions/agent-browser/lib/input-modes/params.js +164 -0
- package/dist/extensions/agent-browser/lib/input-modes/semantic-action.js +119 -0
- package/dist/extensions/agent-browser/lib/input-modes/shared.js +42 -0
- package/dist/extensions/agent-browser/lib/input-modes/types.js +21 -0
- package/dist/extensions/agent-browser/lib/input-modes.js +10 -0
- package/dist/extensions/agent-browser/lib/json-schema.js +58 -0
- package/dist/extensions/agent-browser/lib/launch-scoped-flags.js +59 -0
- package/dist/extensions/agent-browser/lib/navigation-policy.js +83 -0
- package/dist/extensions/agent-browser/lib/orchestration/batch-stdin.js +62 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/artifact-paths.js +39 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/click-dispatch.js +276 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.js +909 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/final-result.js +443 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/index.js +47 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/direct-anchor-download.js +141 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/network-page-filter.js +108 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/scroll-shims.js +112 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/snapshot-filter.js +158 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/wait-timeouts.js +54 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare.js +762 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/process-output.js +491 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prompt-guards.js +40 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-artifacts.js +5 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-state.js +731 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/types.js +1 -0
- package/dist/extensions/agent-browser/lib/orchestration/electron-host/index.js +718 -0
- package/dist/extensions/agent-browser/lib/orchestration/input-plan.js +247 -0
- package/dist/extensions/agent-browser/lib/orchestration/output-file.js +68 -0
- package/{extensions/agent-browser/lib/parsing.ts → dist/extensions/agent-browser/lib/parsing.js} +12 -11
- package/dist/extensions/agent-browser/lib/pi-tool-rendering.js +241 -0
- package/dist/extensions/agent-browser/lib/playbook.js +121 -0
- package/dist/extensions/agent-browser/lib/process.js +448 -0
- package/dist/extensions/agent-browser/lib/prompt-policy.js +91 -0
- package/dist/extensions/agent-browser/lib/results/action-recommendations.js +220 -0
- package/dist/extensions/agent-browser/lib/results/artifact-manifest.js +111 -0
- package/{extensions/agent-browser/lib/results/artifact-state.ts → dist/extensions/agent-browser/lib/results/artifact-state.js} +4 -8
- package/dist/extensions/agent-browser/lib/results/categories.js +76 -0
- package/dist/extensions/agent-browser/lib/results/confirmation.js +63 -0
- package/dist/extensions/agent-browser/lib/results/contracts.js +8 -0
- package/dist/extensions/agent-browser/lib/results/editable-ref-evidence.js +74 -0
- package/dist/extensions/agent-browser/lib/results/envelope.js +166 -0
- package/dist/extensions/agent-browser/lib/results/network-routes.js +92 -0
- package/dist/extensions/agent-browser/lib/results/network.js +73 -0
- package/dist/extensions/agent-browser/lib/results/next-actions.js +72 -0
- package/dist/extensions/agent-browser/lib/results/presentation/artifacts.js +515 -0
- package/dist/extensions/agent-browser/lib/results/presentation/batch.js +397 -0
- package/dist/extensions/agent-browser/lib/results/presentation/browser-profile-recovery.js +55 -0
- package/dist/extensions/agent-browser/lib/results/presentation/common.js +46 -0
- package/dist/extensions/agent-browser/lib/results/presentation/content.js +24 -0
- package/dist/extensions/agent-browser/lib/results/presentation/diagnostics.js +960 -0
- package/dist/extensions/agent-browser/lib/results/presentation/errors.js +205 -0
- package/dist/extensions/agent-browser/lib/results/presentation/large-output.js +134 -0
- package/dist/extensions/agent-browser/lib/results/presentation/navigation.js +159 -0
- package/dist/extensions/agent-browser/lib/results/presentation/registry.js +216 -0
- package/dist/extensions/agent-browser/lib/results/presentation/semantic-action.js +104 -0
- package/dist/extensions/agent-browser/lib/results/presentation/skills.js +152 -0
- package/dist/extensions/agent-browser/lib/results/presentation.js +177 -0
- package/dist/extensions/agent-browser/lib/results/recovery-actions.js +107 -0
- package/dist/extensions/agent-browser/lib/results/recovery-next-actions.js +50 -0
- package/dist/extensions/agent-browser/lib/results/selector-recovery.js +225 -0
- package/{extensions/agent-browser/lib/results/shared.ts → dist/extensions/agent-browser/lib/results/shared.js} +0 -1
- package/dist/extensions/agent-browser/lib/results/snapshot-high-value-controls.js +208 -0
- package/dist/extensions/agent-browser/lib/results/snapshot-refs.js +78 -0
- package/dist/extensions/agent-browser/lib/results/snapshot-segments.js +331 -0
- package/dist/extensions/agent-browser/lib/results/snapshot-spill.js +40 -0
- package/dist/extensions/agent-browser/lib/results/snapshot.js +264 -0
- package/dist/extensions/agent-browser/lib/results/text.js +40 -0
- package/{extensions/agent-browser/lib/results.ts → dist/extensions/agent-browser/lib/results.js} +2 -32
- package/dist/extensions/agent-browser/lib/runtime.js +816 -0
- package/dist/extensions/agent-browser/lib/session-page-state.js +411 -0
- package/dist/extensions/agent-browser/lib/string-enum-schema.js +13 -0
- package/dist/extensions/agent-browser/lib/temp.js +498 -0
- package/dist/extensions/agent-browser/lib/web-search.js +562 -0
- package/docs/ARCHITECTURE.md +10 -10
- package/docs/COMMAND_REFERENCE.md +35 -21
- package/docs/ELECTRON.md +3 -3
- package/docs/RELEASE.md +46 -26
- package/docs/REQUIREMENTS.md +1 -1
- package/docs/SUPPORT_MATRIX.md +35 -106
- package/docs/TOOL_CONTRACT.md +23 -21
- package/package.json +12 -8
- package/scripts/agent-browser-capability-baseline.mjs +6 -3
- package/scripts/config.mjs +8 -2
- package/scripts/doctor.mjs +19 -17
- package/scripts/platform-smoke.mjs +1 -1
- package/extensions/agent-browser/index.ts +0 -952
- package/extensions/agent-browser/lib/argv-descriptor.ts +0 -90
- package/extensions/agent-browser/lib/argv-grammar.ts +0 -128
- package/extensions/agent-browser/lib/bash-guard.ts +0 -205
- package/extensions/agent-browser/lib/command-policy.ts +0 -71
- package/extensions/agent-browser/lib/command-taxonomy.ts +0 -336
- package/extensions/agent-browser/lib/config-policy.js +0 -690
- package/extensions/agent-browser/lib/config.ts +0 -209
- package/extensions/agent-browser/lib/electron/cdp.ts +0 -69
- package/extensions/agent-browser/lib/electron/cleanup.ts +0 -235
- package/extensions/agent-browser/lib/electron/discovery.ts +0 -710
- package/extensions/agent-browser/lib/electron/launch.ts +0 -499
- package/extensions/agent-browser/lib/executable-path.ts +0 -19
- package/extensions/agent-browser/lib/fs-utils.ts +0 -18
- package/extensions/agent-browser/lib/input-modes/electron.ts +0 -170
- package/extensions/agent-browser/lib/input-modes/job.ts +0 -451
- package/extensions/agent-browser/lib/input-modes/lookups.ts +0 -447
- package/extensions/agent-browser/lib/input-modes/params.ts +0 -205
- package/extensions/agent-browser/lib/input-modes/semantic-action.ts +0 -127
- package/extensions/agent-browser/lib/input-modes/shared.ts +0 -46
- package/extensions/agent-browser/lib/input-modes/types.ts +0 -225
- package/extensions/agent-browser/lib/input-modes.ts +0 -45
- package/extensions/agent-browser/lib/json-schema.ts +0 -73
- package/extensions/agent-browser/lib/launch-scoped-flags.ts +0 -67
- package/extensions/agent-browser/lib/navigation-policy.ts +0 -95
- package/extensions/agent-browser/lib/orchestration/batch-stdin.ts +0 -65
- package/extensions/agent-browser/lib/orchestration/browser-run/click-dispatch.ts +0 -257
- package/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.ts +0 -912
- package/extensions/agent-browser/lib/orchestration/browser-run/final-result.ts +0 -512
- package/extensions/agent-browser/lib/orchestration/browser-run/index.ts +0 -53
- package/extensions/agent-browser/lib/orchestration/browser-run/prepare.ts +0 -1481
- package/extensions/agent-browser/lib/orchestration/browser-run/process-output.ts +0 -564
- package/extensions/agent-browser/lib/orchestration/browser-run/prompt-guards.ts +0 -47
- package/extensions/agent-browser/lib/orchestration/browser-run/session-state.ts +0 -868
- package/extensions/agent-browser/lib/orchestration/browser-run/types.ts +0 -564
- package/extensions/agent-browser/lib/orchestration/electron-host/index.ts +0 -855
- package/extensions/agent-browser/lib/orchestration/input-plan.ts +0 -375
- package/extensions/agent-browser/lib/orchestration/output-file.ts +0 -86
- package/extensions/agent-browser/lib/pi-tool-rendering.ts +0 -252
- package/extensions/agent-browser/lib/playbook.ts +0 -142
- package/extensions/agent-browser/lib/process.ts +0 -516
- package/extensions/agent-browser/lib/prompt-policy.ts +0 -105
- package/extensions/agent-browser/lib/results/action-recommendations.ts +0 -264
- package/extensions/agent-browser/lib/results/artifact-manifest.ts +0 -111
- package/extensions/agent-browser/lib/results/categories.ts +0 -106
- package/extensions/agent-browser/lib/results/confirmation.ts +0 -76
- package/extensions/agent-browser/lib/results/contracts.ts +0 -241
- package/extensions/agent-browser/lib/results/editable-ref-evidence.ts +0 -72
- package/extensions/agent-browser/lib/results/envelope.ts +0 -195
- package/extensions/agent-browser/lib/results/network-routes.ts +0 -83
- package/extensions/agent-browser/lib/results/network.ts +0 -78
- package/extensions/agent-browser/lib/results/next-actions.ts +0 -117
- package/extensions/agent-browser/lib/results/presentation/artifacts.ts +0 -588
- package/extensions/agent-browser/lib/results/presentation/batch.ts +0 -450
- package/extensions/agent-browser/lib/results/presentation/browser-profile-recovery.ts +0 -67
- package/extensions/agent-browser/lib/results/presentation/common.ts +0 -53
- package/extensions/agent-browser/lib/results/presentation/content.ts +0 -36
- package/extensions/agent-browser/lib/results/presentation/diagnostics.ts +0 -923
- package/extensions/agent-browser/lib/results/presentation/errors.ts +0 -227
- package/extensions/agent-browser/lib/results/presentation/large-output.ts +0 -182
- package/extensions/agent-browser/lib/results/presentation/navigation.ts +0 -184
- package/extensions/agent-browser/lib/results/presentation/registry.ts +0 -242
- package/extensions/agent-browser/lib/results/presentation/semantic-action.ts +0 -131
- package/extensions/agent-browser/lib/results/presentation/skills.ts +0 -143
- package/extensions/agent-browser/lib/results/presentation.ts +0 -257
- package/extensions/agent-browser/lib/results/recovery-actions.ts +0 -139
- package/extensions/agent-browser/lib/results/recovery-next-actions.ts +0 -71
- package/extensions/agent-browser/lib/results/selector-recovery.ts +0 -320
- package/extensions/agent-browser/lib/results/snapshot-high-value-controls.ts +0 -273
- package/extensions/agent-browser/lib/results/snapshot-refs.ts +0 -100
- package/extensions/agent-browser/lib/results/snapshot-segments.ts +0 -366
- package/extensions/agent-browser/lib/results/snapshot-spill.ts +0 -63
- package/extensions/agent-browser/lib/results/snapshot.ts +0 -329
- package/extensions/agent-browser/lib/results/text.ts +0 -40
- package/extensions/agent-browser/lib/runtime.ts +0 -988
- package/extensions/agent-browser/lib/session-page-state.ts +0 -512
- package/extensions/agent-browser/lib/string-enum-schema.ts +0 -20
- package/extensions/agent-browser/lib/temp.ts +0 -577
- package/extensions/agent-browser/lib/web-search.ts +0 -721
- /package/{extensions/agent-browser/lib/orchestration/browser-run.ts → dist/extensions/agent-browser/lib/orchestration/browser-run.js} +0 -0
|
@@ -1,170 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Purpose: Compile top-level Electron wrapper inputs into validated Electron actions.
|
|
3
|
-
* Responsibilities: Enforce action-specific fields, launch-target rules, and wrapper-owned flag safety.
|
|
4
|
-
* Scope: Electron input-mode validation only; launch/probe/cleanup execution stays in the extension entrypoint.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { isRecord } from "../parsing.js";
|
|
8
|
-
import {
|
|
9
|
-
AGENT_BROWSER_ELECTRON_ACTIONS,
|
|
10
|
-
AGENT_BROWSER_ELECTRON_HANDOFFS,
|
|
11
|
-
AGENT_BROWSER_ELECTRON_LIST_FIELDS,
|
|
12
|
-
AGENT_BROWSER_ELECTRON_PROBE_FIELDS,
|
|
13
|
-
AGENT_BROWSER_ELECTRON_RESERVED_APP_ARGS,
|
|
14
|
-
AGENT_BROWSER_ELECTRON_TARGET_TYPES,
|
|
15
|
-
type AgentBrowserElectronAction,
|
|
16
|
-
type CompiledAgentBrowserElectron,
|
|
17
|
-
} from "./types.js";
|
|
18
|
-
|
|
19
|
-
function validateOptionalNonEmptyString(input: Record<string, unknown>, fieldName: string): { value?: string; error?: string } {
|
|
20
|
-
const value = input[fieldName];
|
|
21
|
-
if (value === undefined) return {};
|
|
22
|
-
if (typeof value !== "string" || value.trim().length === 0) {
|
|
23
|
-
return { error: `electron.${fieldName} must be a non-empty string when provided.` };
|
|
24
|
-
}
|
|
25
|
-
return { value: value.trim() };
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function validateOptionalElectronStringArray(input: Record<string, unknown>, fieldName: "allow" | "appArgs" | "deny"): string | undefined {
|
|
29
|
-
const value = input[fieldName];
|
|
30
|
-
if (value === undefined) return undefined;
|
|
31
|
-
if (!Array.isArray(value) || value.some((item) => typeof item !== "string" || item.trim().length === 0)) {
|
|
32
|
-
return `electron.${fieldName} must be an array of non-empty strings when provided.`;
|
|
33
|
-
}
|
|
34
|
-
return undefined;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function validateOptionalElectronEnum<T extends string>(input: Record<string, unknown>, fieldName: string, values: readonly T[]): string | undefined {
|
|
38
|
-
const value = input[fieldName];
|
|
39
|
-
if (value === undefined) return undefined;
|
|
40
|
-
if (typeof value !== "string" || !values.includes(value as T)) {
|
|
41
|
-
return `electron.${fieldName} must be one of: ${values.join(", ")}.`;
|
|
42
|
-
}
|
|
43
|
-
return undefined;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function getReservedElectronAppArg(appArgs: string[] | undefined): string | undefined {
|
|
47
|
-
return appArgs?.find((arg) => {
|
|
48
|
-
const trimmed = arg.trim();
|
|
49
|
-
return trimmed === "--" || AGENT_BROWSER_ELECTRON_RESERVED_APP_ARGS.some((reserved) => trimmed === reserved || trimmed.startsWith(`${reserved}=`));
|
|
50
|
-
});
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function validateElectronLaunchAppArgs(appArgs: string[] | undefined): string | undefined {
|
|
54
|
-
const reservedArg = getReservedElectronAppArg(appArgs);
|
|
55
|
-
return reservedArg
|
|
56
|
-
? `electron.appArgs must not include wrapper-owned launch flag ${reservedArg}.`
|
|
57
|
-
: undefined;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function validateOptionalElectronPositiveInteger(input: Record<string, unknown>, fieldName: "maxResults" | "timeoutMs"): { value?: number; error?: string } {
|
|
61
|
-
const value = input[fieldName];
|
|
62
|
-
if (value === undefined) return {};
|
|
63
|
-
if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) {
|
|
64
|
-
return { error: `electron.${fieldName} must be a positive integer when provided.` };
|
|
65
|
-
}
|
|
66
|
-
return { value };
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function onlyAllowedElectronFields(input: Record<string, unknown>, action: string, allowedFields: ReadonlySet<string>): string | undefined {
|
|
70
|
-
return Object.keys(input).find((fieldName) => !allowedFields.has(fieldName))
|
|
71
|
-
? `electron.${action} does not support electron.${Object.keys(input).find((fieldName) => !allowedFields.has(fieldName))}.`
|
|
72
|
-
: undefined;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export function compileAgentBrowserElectron(input: unknown): { compiled?: CompiledAgentBrowserElectron; error?: string } {
|
|
76
|
-
if (!isRecord(input)) return { error: "electron must be an object." };
|
|
77
|
-
const action = input.action;
|
|
78
|
-
if (typeof action !== "string" || !AGENT_BROWSER_ELECTRON_ACTIONS.includes(action as AgentBrowserElectronAction)) {
|
|
79
|
-
return { error: `electron.action must be one of: ${AGENT_BROWSER_ELECTRON_ACTIONS.join(", ")}.` };
|
|
80
|
-
}
|
|
81
|
-
for (const fieldName of ["query", "appPath", "appName", "bundleId", "executablePath", "launchId"] as const) {
|
|
82
|
-
const validation = validateOptionalNonEmptyString(input, fieldName);
|
|
83
|
-
if (validation.error) return { error: validation.error };
|
|
84
|
-
}
|
|
85
|
-
for (const fieldName of ["appArgs", "allow", "deny"] as const) {
|
|
86
|
-
const error = validateOptionalElectronStringArray(input, fieldName);
|
|
87
|
-
if (error) return { error };
|
|
88
|
-
}
|
|
89
|
-
const handoffError = validateOptionalElectronEnum(input, "handoff", AGENT_BROWSER_ELECTRON_HANDOFFS);
|
|
90
|
-
if (handoffError) return { error: handoffError };
|
|
91
|
-
const targetTypeError = validateOptionalElectronEnum(input, "targetType", AGENT_BROWSER_ELECTRON_TARGET_TYPES);
|
|
92
|
-
if (targetTypeError) return { error: targetTypeError };
|
|
93
|
-
for (const fieldName of ["maxResults", "timeoutMs"] as const) {
|
|
94
|
-
const validation = validateOptionalElectronPositiveInteger(input, fieldName);
|
|
95
|
-
if (validation.error) return { error: validation.error };
|
|
96
|
-
}
|
|
97
|
-
if (input.all !== undefined && input.all !== true) {
|
|
98
|
-
return { error: "electron.all must be true when provided." };
|
|
99
|
-
}
|
|
100
|
-
if (action === "list") {
|
|
101
|
-
const unsupportedListField = Object.keys(input).find((fieldName) => !AGENT_BROWSER_ELECTRON_LIST_FIELDS.has(fieldName));
|
|
102
|
-
if (unsupportedListField) {
|
|
103
|
-
return { error: `electron.list only supports query and maxResults; remove electron.${unsupportedListField}.` };
|
|
104
|
-
}
|
|
105
|
-
return {
|
|
106
|
-
compiled: {
|
|
107
|
-
action: "list",
|
|
108
|
-
maxResults: validateOptionalElectronPositiveInteger(input, "maxResults").value,
|
|
109
|
-
query: validateOptionalNonEmptyString(input, "query").value,
|
|
110
|
-
},
|
|
111
|
-
};
|
|
112
|
-
}
|
|
113
|
-
if (action === "probe") {
|
|
114
|
-
const unsupportedProbeField = Object.keys(input).find((fieldName) => !AGENT_BROWSER_ELECTRON_PROBE_FIELDS.has(fieldName));
|
|
115
|
-
if (unsupportedProbeField) {
|
|
116
|
-
return { error: `electron.probe only supports action, launchId, and timeoutMs; remove electron.${unsupportedProbeField}.` };
|
|
117
|
-
}
|
|
118
|
-
const launchId = validateOptionalNonEmptyString(input, "launchId").value;
|
|
119
|
-
const timeoutMs = validateOptionalElectronPositiveInteger(input, "timeoutMs").value;
|
|
120
|
-
return {
|
|
121
|
-
compiled: {
|
|
122
|
-
action: "probe",
|
|
123
|
-
...(launchId ? { launchId } : {}),
|
|
124
|
-
...(timeoutMs ? { timeoutMs } : {}),
|
|
125
|
-
},
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
|
-
if (action === "launch") {
|
|
129
|
-
const allowedFields = new Set(["action", "allow", "appArgs", "appName", "appPath", "bundleId", "deny", "executablePath", "handoff", "targetType", "timeoutMs"]);
|
|
130
|
-
const unsupportedFieldError = onlyAllowedElectronFields(input, action, allowedFields);
|
|
131
|
-
if (unsupportedFieldError) return { error: unsupportedFieldError };
|
|
132
|
-
const appArgs = (input.appArgs as string[] | undefined)?.map((item) => item.trim());
|
|
133
|
-
const appArgsError = validateElectronLaunchAppArgs(appArgs);
|
|
134
|
-
if (appArgsError) return { error: appArgsError };
|
|
135
|
-
const targetFields = ["appPath", "appName", "bundleId", "executablePath"] as const;
|
|
136
|
-
const providedTargets = targetFields.filter((fieldName) => input[fieldName] !== undefined);
|
|
137
|
-
if (providedTargets.length !== 1) {
|
|
138
|
-
return { error: "electron.launch requires exactly one of appPath, appName, bundleId, or executablePath." };
|
|
139
|
-
}
|
|
140
|
-
return {
|
|
141
|
-
compiled: {
|
|
142
|
-
action: "launch",
|
|
143
|
-
allow: (input.allow as string[] | undefined)?.map((item) => item.trim()),
|
|
144
|
-
appArgs,
|
|
145
|
-
deny: (input.deny as string[] | undefined)?.map((item) => item.trim()),
|
|
146
|
-
appName: validateOptionalNonEmptyString(input, "appName").value,
|
|
147
|
-
appPath: validateOptionalNonEmptyString(input, "appPath").value,
|
|
148
|
-
bundleId: validateOptionalNonEmptyString(input, "bundleId").value,
|
|
149
|
-
executablePath: validateOptionalNonEmptyString(input, "executablePath").value,
|
|
150
|
-
handoff: (input.handoff as "connect" | "snapshot" | "tabs" | undefined) ?? "snapshot",
|
|
151
|
-
targetType: (input.targetType as "any" | "page" | "webview" | undefined) ?? "page",
|
|
152
|
-
timeoutMs: validateOptionalElectronPositiveInteger(input, "timeoutMs").value,
|
|
153
|
-
},
|
|
154
|
-
};
|
|
155
|
-
}
|
|
156
|
-
const allowedFields = new Set(["action", "all", "launchId", "timeoutMs"]);
|
|
157
|
-
const unsupportedFieldError = onlyAllowedElectronFields(input, action, allowedFields);
|
|
158
|
-
if (unsupportedFieldError) return { error: unsupportedFieldError };
|
|
159
|
-
if (input.all === true && input.launchId !== undefined) {
|
|
160
|
-
return { error: `electron.${action} accepts launchId or all, not both.` };
|
|
161
|
-
}
|
|
162
|
-
return {
|
|
163
|
-
compiled: {
|
|
164
|
-
action: action as "cleanup" | "status",
|
|
165
|
-
all: input.all === true || undefined,
|
|
166
|
-
launchId: validateOptionalNonEmptyString(input, "launchId").value,
|
|
167
|
-
timeoutMs: validateOptionalElectronPositiveInteger(input, "timeoutMs").value,
|
|
168
|
-
},
|
|
169
|
-
};
|
|
170
|
-
}
|
|
@@ -1,451 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Purpose: Compile constrained job and lightweight QA wrapper inputs to upstream batch commands.
|
|
3
|
-
* Responsibilities: Validate job/QA fields, produce argv/stdin, and summarize QA diagnostic results.
|
|
4
|
-
* Scope: Job and QA modes only.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import type { ArtifactVerificationSummary } from "../results/contracts.js";
|
|
8
|
-
import { isRecord } from "../parsing.js";
|
|
9
|
-
import { summarizeNetworkFailures } from "../results/network.js";
|
|
10
|
-
import { getBatchResultItems, getCommandNameFromBatchItem, getSelectValues } from "./shared.js";
|
|
11
|
-
import { compileAgentBrowserSemanticAction } from "./semantic-action.js";
|
|
12
|
-
import {
|
|
13
|
-
AGENT_BROWSER_JOB_STEP_ACTIONS,
|
|
14
|
-
AGENT_BROWSER_JOB_TYPE_DELAYED_TEXT_MAX_CHARACTERS,
|
|
15
|
-
AGENT_BROWSER_QA_LOAD_STATES,
|
|
16
|
-
type AgentBrowserJobStepAction,
|
|
17
|
-
type AgentBrowserQaLoadState,
|
|
18
|
-
type AgentBrowserQaPresetAnalysis,
|
|
19
|
-
type CompiledAgentBrowserJob,
|
|
20
|
-
type CompiledAgentBrowserJobStep,
|
|
21
|
-
type CompiledAgentBrowserQaPreset,
|
|
22
|
-
} from "./types.js";
|
|
23
|
-
|
|
24
|
-
function getRequiredJobString(step: Record<string, unknown>, field: "path" | "selector" | "text" | "url", action: AgentBrowserJobStepAction): { value?: string; error?: string } {
|
|
25
|
-
const value = step[field];
|
|
26
|
-
if (typeof value !== "string" || value.trim().length === 0) {
|
|
27
|
-
return { error: `job step ${action} requires a non-empty ${field} string.` };
|
|
28
|
-
}
|
|
29
|
-
return { value };
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function compileJobClickOrFillStep(step: Record<string, unknown>, action: "click" | "fill"): { args?: string[]; error?: string } {
|
|
33
|
-
const hasSelector = typeof step.selector === "string" && step.selector.trim().length > 0;
|
|
34
|
-
const hasLocator = step.locator !== undefined || step.role !== undefined || step.name !== undefined || step.value !== undefined;
|
|
35
|
-
if (hasSelector && hasLocator) {
|
|
36
|
-
return { error: `job step ${action} must use either selector or semantic locator fields, not both.` };
|
|
37
|
-
}
|
|
38
|
-
if (hasSelector) {
|
|
39
|
-
if (action === "click") return { args: ["click", step.selector as string] };
|
|
40
|
-
const text = getRequiredJobString(step, "text", action);
|
|
41
|
-
if (text.error) return { error: text.error };
|
|
42
|
-
return { args: ["fill", step.selector as string, text.value as string] };
|
|
43
|
-
}
|
|
44
|
-
if (!hasLocator) {
|
|
45
|
-
return { error: `job step ${action} requires either a non-empty selector string or semantic locator fields.` };
|
|
46
|
-
}
|
|
47
|
-
const compiled = compileAgentBrowserSemanticAction({
|
|
48
|
-
action,
|
|
49
|
-
locator: step.locator,
|
|
50
|
-
name: step.name,
|
|
51
|
-
role: step.role,
|
|
52
|
-
text: step.text,
|
|
53
|
-
value: step.value,
|
|
54
|
-
});
|
|
55
|
-
if (compiled.error) return { error: compiled.error.replaceAll("semanticAction", `job step ${action}`) };
|
|
56
|
-
return { args: compiled.compiled?.args };
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function getUnsupportedJobStepField(step: Record<string, unknown>, allowedFields: ReadonlySet<string>): string | undefined {
|
|
60
|
-
return Object.keys(step).find((field) => !allowedFields.has(field));
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const JOB_TYPE_ALLOWED_FIELDS = new Set(["action", "delayMs", "press", "selector", "text"]);
|
|
64
|
-
|
|
65
|
-
function compileJobTypeSteps(step: Record<string, unknown>): { error?: string; steps?: CompiledAgentBrowserJobStep[] } {
|
|
66
|
-
const unsupportedField = getUnsupportedJobStepField(step, JOB_TYPE_ALLOWED_FIELDS);
|
|
67
|
-
if (unsupportedField) return { error: `job step type does not support ${unsupportedField}; supported fields are selector, text, delayMs, and press.` };
|
|
68
|
-
const text = getRequiredJobString(step, "text", "type");
|
|
69
|
-
if (text.error) return { error: text.error };
|
|
70
|
-
const selector = step.selector;
|
|
71
|
-
if (selector !== undefined && (typeof selector !== "string" || selector.trim().length === 0)) {
|
|
72
|
-
return { error: "job step type selector must be a non-empty string when provided." };
|
|
73
|
-
}
|
|
74
|
-
if (step.locator !== undefined || step.role !== undefined || step.name !== undefined || step.value !== undefined || step.values !== undefined) {
|
|
75
|
-
return { error: "job step type supports selector, text, delayMs, and press only; focus the target first or use click/fill semantic locator fields in a separate step." };
|
|
76
|
-
}
|
|
77
|
-
const delayMs = step.delayMs;
|
|
78
|
-
if (delayMs !== undefined && (typeof delayMs !== "number" || !Number.isInteger(delayMs) || delayMs <= 0)) {
|
|
79
|
-
return { error: "job step type delayMs must be a positive integer when provided." };
|
|
80
|
-
}
|
|
81
|
-
const press = step.press;
|
|
82
|
-
if (press !== undefined && (typeof press !== "string" || press.trim().length === 0)) {
|
|
83
|
-
return { error: "job step type press must be a non-empty key string when provided." };
|
|
84
|
-
}
|
|
85
|
-
const typedText = text.value as string;
|
|
86
|
-
const typedChars = Array.from(typedText);
|
|
87
|
-
if (typedChars.length === 0) return { error: "job step type requires non-empty text." };
|
|
88
|
-
if (delayMs !== undefined && typedChars.length > AGENT_BROWSER_JOB_TYPE_DELAYED_TEXT_MAX_CHARACTERS) {
|
|
89
|
-
return { error: `job step type delayMs supports at most ${AGENT_BROWSER_JOB_TYPE_DELAYED_TEXT_MAX_CHARACTERS} characters; split longer text into shorter calls or omit delayMs.` };
|
|
90
|
-
}
|
|
91
|
-
const compiledSteps: CompiledAgentBrowserJobStep[] = [];
|
|
92
|
-
if (delayMs === undefined) {
|
|
93
|
-
compiledSteps.push({ action: "type", args: typeof selector === "string" ? ["type", selector, typedText] : ["keyboard", "type", typedText] });
|
|
94
|
-
} else {
|
|
95
|
-
if (typeof selector === "string") compiledSteps.push({ action: "type", args: ["focus", selector], generatedFrom: "type.selector" });
|
|
96
|
-
for (const [index, char] of typedChars.entries()) {
|
|
97
|
-
compiledSteps.push({ action: "type", args: ["keyboard", "type", char], generatedFrom: "type.delayMs" });
|
|
98
|
-
if (index < typedChars.length - 1) compiledSteps.push({ action: "wait", args: ["wait", String(delayMs)], generatedFrom: "type.delayMs" });
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
if (typeof press === "string") compiledSteps.push({ action: "type", args: ["press", press], generatedFrom: "type.press" });
|
|
102
|
-
return { steps: compiledSteps };
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
export function compileAgentBrowserJob(input: unknown): { compiled?: CompiledAgentBrowserJob; error?: string } {
|
|
106
|
-
if (!isRecord(input)) {
|
|
107
|
-
return { error: "job must be an object." };
|
|
108
|
-
}
|
|
109
|
-
const rawFailFast = input.failFast;
|
|
110
|
-
if (rawFailFast !== undefined && typeof rawFailFast !== "boolean") {
|
|
111
|
-
return { error: "job.failFast must be a boolean when provided." };
|
|
112
|
-
}
|
|
113
|
-
const failFast = rawFailFast !== false;
|
|
114
|
-
const rawSteps = input.steps;
|
|
115
|
-
if (!Array.isArray(rawSteps) || rawSteps.length === 0) {
|
|
116
|
-
return { error: "job.steps must be a non-empty array." };
|
|
117
|
-
}
|
|
118
|
-
const steps: CompiledAgentBrowserJobStep[] = [];
|
|
119
|
-
for (const [index, rawStep] of rawSteps.entries()) {
|
|
120
|
-
if (!isRecord(rawStep)) {
|
|
121
|
-
return { error: `job.steps[${index}] must be an object.` };
|
|
122
|
-
}
|
|
123
|
-
const action = rawStep.action;
|
|
124
|
-
if (typeof action !== "string" || !AGENT_BROWSER_JOB_STEP_ACTIONS.includes(action as AgentBrowserJobStepAction)) {
|
|
125
|
-
return { error: `job.steps[${index}].action must be one of: ${AGENT_BROWSER_JOB_STEP_ACTIONS.join(", ")}.` };
|
|
126
|
-
}
|
|
127
|
-
const jobAction = action as AgentBrowserJobStepAction;
|
|
128
|
-
let args: string[];
|
|
129
|
-
let generatedFrom: string | undefined;
|
|
130
|
-
let extraSteps: CompiledAgentBrowserJobStep[] = [];
|
|
131
|
-
if (jobAction === "open") {
|
|
132
|
-
const result = getRequiredJobString(rawStep, "url", jobAction);
|
|
133
|
-
if (result.error) return { error: `job.steps[${index}]: ${result.error}` };
|
|
134
|
-
args = ["open", result.value as string];
|
|
135
|
-
if (rawStep.loadState !== undefined) {
|
|
136
|
-
if (typeof rawStep.loadState !== "string" || !AGENT_BROWSER_QA_LOAD_STATES.includes(rawStep.loadState as AgentBrowserQaLoadState)) {
|
|
137
|
-
return { error: `job.steps[${index}].loadState must be one of: ${AGENT_BROWSER_QA_LOAD_STATES.join(", ")}.` };
|
|
138
|
-
}
|
|
139
|
-
extraSteps = [{ action: "wait", args: ["wait", "--load", rawStep.loadState], generatedFrom: "open.loadState" }];
|
|
140
|
-
}
|
|
141
|
-
} else if (jobAction === "click" || jobAction === "fill") {
|
|
142
|
-
const result = compileJobClickOrFillStep(rawStep, jobAction);
|
|
143
|
-
if (result.error) return { error: `job.steps[${index}]: ${result.error}` };
|
|
144
|
-
args = result.args as string[];
|
|
145
|
-
} else if (jobAction === "type") {
|
|
146
|
-
const result = compileJobTypeSteps(rawStep);
|
|
147
|
-
if (result.error) return { error: `job.steps[${index}]: ${result.error}` };
|
|
148
|
-
const [firstStep, ...restSteps] = result.steps as CompiledAgentBrowserJobStep[];
|
|
149
|
-
args = firstStep.args;
|
|
150
|
-
generatedFrom = firstStep.generatedFrom;
|
|
151
|
-
extraSteps = restSteps;
|
|
152
|
-
} else if (jobAction === "select") {
|
|
153
|
-
const selector = getRequiredJobString(rawStep, "selector", jobAction);
|
|
154
|
-
if (selector.error) return { error: `job.steps[${index}]: ${selector.error}` };
|
|
155
|
-
const values = getSelectValues(rawStep, `job.steps[${index}]`);
|
|
156
|
-
if (values.error) return { error: values.error };
|
|
157
|
-
args = ["select", selector.value as string, ...(values.values as string[])];
|
|
158
|
-
} else if (jobAction === "wait") {
|
|
159
|
-
const milliseconds = rawStep.milliseconds;
|
|
160
|
-
if (typeof milliseconds !== "number" || !Number.isInteger(milliseconds) || milliseconds <= 0) {
|
|
161
|
-
return { error: `job.steps[${index}]: job step wait requires a positive integer milliseconds value.` };
|
|
162
|
-
}
|
|
163
|
-
args = ["wait", String(milliseconds)];
|
|
164
|
-
} else if (jobAction === "assertText") {
|
|
165
|
-
const result = getRequiredJobString(rawStep, "text", jobAction);
|
|
166
|
-
if (result.error) return { error: `job.steps[${index}]: ${result.error}` };
|
|
167
|
-
args = ["wait", "--text", result.value as string];
|
|
168
|
-
} else if (jobAction === "assertUrl") {
|
|
169
|
-
const result = getRequiredJobString(rawStep, "url", jobAction);
|
|
170
|
-
if (result.error) return { error: `job.steps[${index}]: ${result.error}` };
|
|
171
|
-
args = ["wait", "--url", result.value as string];
|
|
172
|
-
} else if (jobAction === "waitForDownload") {
|
|
173
|
-
const result = getRequiredJobString(rawStep, "path", jobAction);
|
|
174
|
-
if (result.error) return { error: `job.steps[${index}]: ${result.error}` };
|
|
175
|
-
args = ["wait", "--download", result.value as string];
|
|
176
|
-
} else if (jobAction === "snapshot") {
|
|
177
|
-
args = ["snapshot", "-i"];
|
|
178
|
-
} else {
|
|
179
|
-
const result = getRequiredJobString(rawStep, "path", jobAction);
|
|
180
|
-
if (result.error) return { error: `job.steps[${index}]: ${result.error}` };
|
|
181
|
-
args = ["screenshot", result.value as string];
|
|
182
|
-
}
|
|
183
|
-
steps.push({ action: jobAction, args, generatedFrom }, ...extraSteps);
|
|
184
|
-
}
|
|
185
|
-
return { compiled: { args: failFast ? ["batch", "--bail"] : ["batch"], failFast, stdin: JSON.stringify(steps.map((step) => step.args)), steps } };
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
export function isHttpOrHttpsUrl(url: string): boolean {
|
|
189
|
-
try {
|
|
190
|
-
const protocol = new URL(url).protocol;
|
|
191
|
-
return protocol === "http:" || protocol === "https:";
|
|
192
|
-
} catch {
|
|
193
|
-
return false;
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
function describeQaChecksRun(checks: CompiledAgentBrowserQaPreset["checks"]): string {
|
|
198
|
-
const parts = [`load:${checks.loadState}`];
|
|
199
|
-
if (checks.expectedText.length > 0) parts.push(`text×${checks.expectedText.length}`);
|
|
200
|
-
if (checks.expectedSelector) parts.push("selector");
|
|
201
|
-
if (checks.checkNetwork) parts.push("network");
|
|
202
|
-
if (checks.checkConsole) parts.push("console");
|
|
203
|
-
if (checks.checkErrors) parts.push("errors");
|
|
204
|
-
if (checks.diagnosticsResetAtStart) parts.push("diagnostics-reset");
|
|
205
|
-
else if (checks.checkNetwork || checks.checkConsole || checks.checkErrors) parts.push("attached-diagnostics-preserved");
|
|
206
|
-
if (checks.screenshotPath) parts.push("screenshot");
|
|
207
|
-
return parts.join(", ");
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
export function extractQaPageContext(options: {
|
|
211
|
-
attachedTarget?: { title?: string; url?: string };
|
|
212
|
-
batchData?: unknown;
|
|
213
|
-
compiled?: CompiledAgentBrowserQaPreset;
|
|
214
|
-
}): { title?: string; url?: string } {
|
|
215
|
-
if (options.attachedTarget?.title || options.attachedTarget?.url) {
|
|
216
|
-
return { title: options.attachedTarget.title, url: options.attachedTarget.url };
|
|
217
|
-
}
|
|
218
|
-
for (const item of getBatchResultItems(options.batchData)) {
|
|
219
|
-
if (getCommandNameFromBatchItem(item) !== "open" || !isRecord(item.result)) continue;
|
|
220
|
-
const url = typeof item.result.url === "string" ? item.result.url : undefined;
|
|
221
|
-
const title = typeof item.result.title === "string" ? item.result.title : undefined;
|
|
222
|
-
if (url || title) return { title, url };
|
|
223
|
-
}
|
|
224
|
-
if (options.compiled?.checks.url) {
|
|
225
|
-
return { url: options.compiled.checks.url };
|
|
226
|
-
}
|
|
227
|
-
return {};
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
export function buildQaCompactPassText(options: {
|
|
231
|
-
artifactVerification?: ArtifactVerificationSummary;
|
|
232
|
-
batchStepCount: number;
|
|
233
|
-
checks: CompiledAgentBrowserQaPreset["checks"];
|
|
234
|
-
page?: { title?: string; url?: string };
|
|
235
|
-
qaPreset: AgentBrowserQaPresetAnalysis;
|
|
236
|
-
}): string {
|
|
237
|
-
const lines = [options.qaPreset.summary];
|
|
238
|
-
const pageParts = [options.page?.title, options.page?.url].filter((part): part is string => typeof part === "string" && part.length > 0);
|
|
239
|
-
if (pageParts.length > 0) lines.push(`Page: ${pageParts.join(" — ")}`);
|
|
240
|
-
lines.push(`Checks run: ${describeQaChecksRun(options.checks)} (${options.batchStepCount} batch step${options.batchStepCount === 1 ? "" : "s"})`);
|
|
241
|
-
if (options.checks.attached && !options.checks.diagnosticsResetAtStart && (options.checks.checkNetwork || options.checks.checkConsole || options.checks.checkErrors)) {
|
|
242
|
-
lines.push("Attached diagnostics: existing upstream session console/network/error buffers were preserved; rows may include events from before qa.attached started.");
|
|
243
|
-
}
|
|
244
|
-
if (options.checks.screenshotPath) {
|
|
245
|
-
const verification = options.artifactVerification;
|
|
246
|
-
lines.push(verification
|
|
247
|
-
? `Screenshot: ${options.checks.screenshotPath} (${verification.verifiedCount}/${verification.artifacts.length} verified on disk)`
|
|
248
|
-
: `Screenshot: ${options.checks.screenshotPath}`);
|
|
249
|
-
}
|
|
250
|
-
lines.push("Full diagnostic matrix: see details.qaPreset and details.batchSteps.");
|
|
251
|
-
return lines.join("\n");
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
const QA_VISIBLE_TEXT_TIMEOUT_MS = 5_000;
|
|
255
|
-
|
|
256
|
-
function formatQaExpectedTextPreview(text: string): string {
|
|
257
|
-
return JSON.stringify(text.length > 80 ? `${text.slice(0, 77)}...` : text);
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
function buildQaVisibleTextPredicate(text: string): string {
|
|
261
|
-
return `(() => {
|
|
262
|
-
const expected = ${JSON.stringify(text)}.replace(/\\s+/g, " ").trim();
|
|
263
|
-
if (!expected) return false;
|
|
264
|
-
const root = document.body || document.documentElement;
|
|
265
|
-
if (!root) return false;
|
|
266
|
-
const skipTags = new Set(["SCRIPT", "STYLE", "NOSCRIPT", "SVG"]);
|
|
267
|
-
const normalize = (value) => String(value ?? "").replace(/\\s+/g, " ").trim();
|
|
268
|
-
const isVisibleElement = (element) => {
|
|
269
|
-
if (!(element instanceof HTMLElement)) return false;
|
|
270
|
-
if (skipTags.has(element.tagName)) return false;
|
|
271
|
-
const style = window.getComputedStyle(element);
|
|
272
|
-
if (style.display === "none" || style.visibility === "hidden" || Number(style.opacity) === 0) return false;
|
|
273
|
-
return element.getClientRects().length > 0;
|
|
274
|
-
};
|
|
275
|
-
const hasVisibleAncestors = (node) => {
|
|
276
|
-
for (let element = node.parentElement; element; element = element.parentElement) {
|
|
277
|
-
if (!isVisibleElement(element)) return false;
|
|
278
|
-
if (element === root) break;
|
|
279
|
-
}
|
|
280
|
-
return true;
|
|
281
|
-
};
|
|
282
|
-
const textWalker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
|
|
283
|
-
let visitedText = 0;
|
|
284
|
-
for (let node = textWalker.nextNode(); node && visitedText < 6000; node = textWalker.nextNode(), visitedText += 1) {
|
|
285
|
-
if (!hasVisibleAncestors(node)) continue;
|
|
286
|
-
if (normalize(node.nodeValue).includes(expected)) return true;
|
|
287
|
-
}
|
|
288
|
-
const elementWalker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
|
|
289
|
-
let visitedElements = 0;
|
|
290
|
-
for (let node = elementWalker.nextNode(); node && visitedElements < 3000; node = elementWalker.nextNode(), visitedElements += 1) {
|
|
291
|
-
const element = node;
|
|
292
|
-
if (!isVisibleElement(element) || !("value" in element)) continue;
|
|
293
|
-
if (normalize(element.value).includes(expected)) return true;
|
|
294
|
-
}
|
|
295
|
-
return false;
|
|
296
|
-
})()`;
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
function qaVisibleTextWaitPassed(item: ReturnType<typeof getBatchResultItems>[number] | undefined, step: CompiledAgentBrowserJobStep): boolean | undefined {
|
|
300
|
-
if (step.args[0] !== "wait" || step.args[1] !== "--fn") return undefined;
|
|
301
|
-
if (!item || item.success === false) return false;
|
|
302
|
-
if (typeof item.result === "boolean") return item.result;
|
|
303
|
-
if (isRecord(item.result) && typeof item.result.result === "boolean") return item.result.result;
|
|
304
|
-
return true;
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
function extractQaTextAssertionResultText(item: ReturnType<typeof getBatchResultItems>[number] | undefined): string | undefined {
|
|
308
|
-
if (!item || item.success === false) return undefined;
|
|
309
|
-
const result = item.result;
|
|
310
|
-
if (typeof result === "string") return result;
|
|
311
|
-
if (!isRecord(result)) return undefined;
|
|
312
|
-
for (const key of ["result", "text", "value"] as const) {
|
|
313
|
-
const value = result[key];
|
|
314
|
-
if (typeof value === "string") return value;
|
|
315
|
-
}
|
|
316
|
-
return undefined;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
export function analyzeQaPresetTimeout(compiled: CompiledAgentBrowserQaPreset): AgentBrowserQaPresetAnalysis | undefined {
|
|
320
|
-
if (compiled.checks.expectedText.length === 0) return undefined;
|
|
321
|
-
const failedChecks = compiled.checks.expectedText.map((text) => `expected text was not verified before timeout: ${formatQaExpectedTextPreview(text)}`);
|
|
322
|
-
return {
|
|
323
|
-
failedChecks,
|
|
324
|
-
passed: false,
|
|
325
|
-
summary: `QA preset failed: ${failedChecks.join("; ")}.`,
|
|
326
|
-
warnings: ["The wrapper timed out before expected-text evidence could be verified; inspect timeoutPartialProgress and retry with a narrower readiness condition if the page was still loading."],
|
|
327
|
-
};
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
export function analyzeQaPresetResults(data: unknown, compiled?: CompiledAgentBrowserQaPreset): AgentBrowserQaPresetAnalysis | undefined {
|
|
331
|
-
const items = getBatchResultItems(data);
|
|
332
|
-
if (items.length === 0) return undefined;
|
|
333
|
-
const failedChecks: string[] = [];
|
|
334
|
-
const warnings: string[] = [];
|
|
335
|
-
for (const item of items) {
|
|
336
|
-
if (item.success === false) {
|
|
337
|
-
failedChecks.push(`${getCommandNameFromBatchItem(item) ?? "step"} failed`);
|
|
338
|
-
}
|
|
339
|
-
const result = isRecord(item.result) ? item.result : undefined;
|
|
340
|
-
const commandName = getCommandNameFromBatchItem(item);
|
|
341
|
-
if (commandName === "errors" && Array.isArray(result?.errors) && result.errors.length > 0) {
|
|
342
|
-
failedChecks.push(`${result.errors.length} page error(s)`);
|
|
343
|
-
}
|
|
344
|
-
if (commandName === "console" && Array.isArray(result?.messages)) {
|
|
345
|
-
const errorCount = result.messages.filter((message) => isRecord(message) && /error/i.test(String(message.type ?? message.level ?? ""))).length;
|
|
346
|
-
if (errorCount > 0) failedChecks.push(`${errorCount} console error message(s)`);
|
|
347
|
-
}
|
|
348
|
-
if (commandName === "network" && Array.isArray(result?.requests)) {
|
|
349
|
-
const networkFailures = summarizeNetworkFailures(result.requests);
|
|
350
|
-
if (networkFailures.actionableCount > 0) failedChecks.push(`${networkFailures.actionableCount} actionable failed network request(s)`);
|
|
351
|
-
if (networkFailures.benignCount > 0) warnings.push(`${networkFailures.benignCount} benign network request failure(s) ignored`);
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
if (compiled?.checks.expectedText.length) {
|
|
355
|
-
let expectedTextIndex = 0;
|
|
356
|
-
compiled.steps.forEach((step, index) => {
|
|
357
|
-
if (step.action !== "assertText") return;
|
|
358
|
-
const expected = compiled.checks.expectedText[expectedTextIndex++];
|
|
359
|
-
if (!expected) return;
|
|
360
|
-
const visibleTextPassed = qaVisibleTextWaitPassed(items[index], step);
|
|
361
|
-
if (visibleTextPassed === true) return;
|
|
362
|
-
const actual = extractQaTextAssertionResultText(items[index]);
|
|
363
|
-
if (!actual || !actual.includes(expected)) failedChecks.push(`expected text not found: ${formatQaExpectedTextPreview(expected)}`);
|
|
364
|
-
});
|
|
365
|
-
}
|
|
366
|
-
const uniqueFailures = [...new Set(failedChecks)];
|
|
367
|
-
const uniqueWarnings = [...new Set(warnings)];
|
|
368
|
-
return {
|
|
369
|
-
failedChecks: uniqueFailures,
|
|
370
|
-
passed: uniqueFailures.length === 0,
|
|
371
|
-
summary: uniqueFailures.length === 0
|
|
372
|
-
? uniqueWarnings.length === 0 ? "QA preset passed." : `QA preset passed with warnings: ${uniqueWarnings.join("; ")}.`
|
|
373
|
-
: `QA preset failed: ${uniqueFailures.join("; ")}.`,
|
|
374
|
-
warnings: uniqueWarnings,
|
|
375
|
-
};
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
export function compileAgentBrowserQaPreset(input: unknown): { compiled?: CompiledAgentBrowserQaPreset; error?: string } {
|
|
379
|
-
if (!isRecord(input)) {
|
|
380
|
-
return { error: "qa must be an object." };
|
|
381
|
-
}
|
|
382
|
-
const attached = input.attached === true;
|
|
383
|
-
if (input.attached !== undefined && typeof input.attached !== "boolean") {
|
|
384
|
-
return { error: "qa.attached must be a boolean when provided." };
|
|
385
|
-
}
|
|
386
|
-
const url = input.url;
|
|
387
|
-
if (attached && url !== undefined) {
|
|
388
|
-
return { error: "qa.url must be omitted when qa.attached is true." };
|
|
389
|
-
}
|
|
390
|
-
if (!attached && (typeof url !== "string" || url.trim().length === 0)) {
|
|
391
|
-
return { error: "qa.url must be a non-empty string." };
|
|
392
|
-
}
|
|
393
|
-
const normalizedUrl = typeof url === "string" ? url.trim() : undefined;
|
|
394
|
-
const expectedText = input.expectedText === undefined
|
|
395
|
-
? []
|
|
396
|
-
: typeof input.expectedText === "string"
|
|
397
|
-
? [input.expectedText]
|
|
398
|
-
: Array.isArray(input.expectedText)
|
|
399
|
-
? input.expectedText
|
|
400
|
-
: undefined;
|
|
401
|
-
if (!expectedText || expectedText.some((text) => typeof text !== "string" || text.trim().length === 0)) {
|
|
402
|
-
return { error: "qa.expectedText must be a non-empty string or array of non-empty strings when provided." };
|
|
403
|
-
}
|
|
404
|
-
const expectedSelector = input.expectedSelector;
|
|
405
|
-
if (expectedSelector !== undefined && (typeof expectedSelector !== "string" || expectedSelector.trim().length === 0)) {
|
|
406
|
-
return { error: "qa.expectedSelector must be a non-empty string when provided." };
|
|
407
|
-
}
|
|
408
|
-
const screenshotPath = input.screenshotPath;
|
|
409
|
-
if (screenshotPath !== undefined && (typeof screenshotPath !== "string" || screenshotPath.trim().length === 0)) {
|
|
410
|
-
return { error: "qa.screenshotPath must be a non-empty string when provided." };
|
|
411
|
-
}
|
|
412
|
-
for (const field of ["checkConsole", "checkErrors", "checkNetwork"] as const) {
|
|
413
|
-
if (input[field] !== undefined && typeof input[field] !== "boolean") {
|
|
414
|
-
return { error: `qa.${field} must be a boolean when provided.` };
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
const rawLoadState = input.loadState;
|
|
418
|
-
if (rawLoadState !== undefined && (typeof rawLoadState !== "string" || !AGENT_BROWSER_QA_LOAD_STATES.includes(rawLoadState as AgentBrowserQaLoadState))) {
|
|
419
|
-
return { error: `qa.loadState must be one of: ${AGENT_BROWSER_QA_LOAD_STATES.join(", ")}.` };
|
|
420
|
-
}
|
|
421
|
-
const checkConsole = typeof input.checkConsole === "boolean" ? input.checkConsole : !attached;
|
|
422
|
-
const checkErrors = typeof input.checkErrors === "boolean" ? input.checkErrors : !attached;
|
|
423
|
-
const checkNetwork = typeof input.checkNetwork === "boolean" ? input.checkNetwork : !attached;
|
|
424
|
-
const loadState = (rawLoadState as AgentBrowserQaLoadState | undefined) ?? "domcontentloaded";
|
|
425
|
-
const diagnosticsResetAtStart = !attached;
|
|
426
|
-
const steps: CompiledAgentBrowserJobStep[] = [];
|
|
427
|
-
if (diagnosticsResetAtStart && checkNetwork) steps.push({ action: "wait", args: ["network", "requests", "--clear"] });
|
|
428
|
-
if (diagnosticsResetAtStart && checkConsole) steps.push({ action: "wait", args: ["console", "--clear"] });
|
|
429
|
-
if (diagnosticsResetAtStart && checkErrors) steps.push({ action: "wait", args: ["errors", "--clear"] });
|
|
430
|
-
if (!attached && normalizedUrl) steps.push({ action: "open", args: ["open", normalizedUrl] });
|
|
431
|
-
steps.push({ action: "wait", args: ["wait", "--load", loadState] });
|
|
432
|
-
for (const text of expectedText) {
|
|
433
|
-
steps.push({ action: "assertText", args: ["wait", "--fn", buildQaVisibleTextPredicate(text), "--timeout", String(QA_VISIBLE_TEXT_TIMEOUT_MS)] });
|
|
434
|
-
}
|
|
435
|
-
if (typeof expectedSelector === "string") {
|
|
436
|
-
steps.push({ action: "wait", args: ["wait", expectedSelector] });
|
|
437
|
-
}
|
|
438
|
-
if (checkNetwork) steps.push({ action: "wait", args: ["network", "requests"] });
|
|
439
|
-
if (checkConsole) steps.push({ action: "wait", args: ["console"] });
|
|
440
|
-
if (checkErrors) steps.push({ action: "wait", args: ["errors"] });
|
|
441
|
-
if (typeof screenshotPath === "string") steps.push({ action: "screenshot", args: ["screenshot", screenshotPath] });
|
|
442
|
-
return {
|
|
443
|
-
compiled: {
|
|
444
|
-
args: ["batch", "--bail"],
|
|
445
|
-
checks: { attached, checkConsole, checkErrors, checkNetwork, diagnosticsResetAtStart, expectedSelector, expectedText, loadState, screenshotPath, url: normalizedUrl },
|
|
446
|
-
failFast: true,
|
|
447
|
-
stdin: JSON.stringify(steps.map((step) => step.args)),
|
|
448
|
-
steps,
|
|
449
|
-
},
|
|
450
|
-
};
|
|
451
|
-
}
|