explorbot 0.4.4 → 0.4.6
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/boat/api-tester/src/apibot.ts +18 -2
- package/boat/api-tester/src/cli.ts +85 -274
- package/boat/api-tester/src/commands/api-command.ts +10 -0
- package/boat/api-tester/src/commands/explore-command.ts +52 -0
- package/boat/api-tester/src/commands/init-command.ts +119 -0
- package/boat/api-tester/src/commands/know-command.ts +44 -0
- package/boat/api-tester/src/commands/plan-command.ts +42 -0
- package/boat/api-tester/src/commands/test-command.ts +54 -0
- package/boat/prima/src/prima.ts +8 -3
- package/dist/boat/api-tester/src/apibot.js +14 -1
- package/dist/boat/api-tester/src/cli.js +87 -243
- package/dist/boat/api-tester/src/commands/api-command.js +7 -0
- package/dist/boat/api-tester/src/commands/explore-command.js +41 -0
- package/dist/boat/api-tester/src/commands/init-command.js +88 -0
- package/dist/boat/api-tester/src/commands/know-command.js +39 -0
- package/dist/boat/api-tester/src/commands/plan-command.js +37 -0
- package/dist/boat/api-tester/src/commands/test-command.js +45 -0
- package/dist/boat/prima/src/prima.js +10 -3
- package/dist/package.json +4 -4
- package/dist/src/ai/fisherman/tools.js +7 -1
- package/dist/src/ai/fisherman.js +2 -1
- package/dist/src/ai/pilot.d.ts +0 -1
- package/dist/src/ai/pilot.js +8 -24
- package/dist/src/ai/planner.d.ts +4 -0
- package/dist/src/ai/planner.js +28 -0
- package/dist/src/ai/provider.js +3 -1
- package/dist/src/ai/researcher/deep-analysis.d.ts +1 -1
- package/dist/src/ai/researcher/deep-analysis.js +14 -6
- package/dist/src/ai/rules.js +8 -7
- package/dist/src/ai/scout/tools.d.ts +17 -0
- package/dist/src/ai/scout/tools.js +130 -0
- package/dist/src/ai/scout.d.ts +21 -0
- package/dist/src/ai/scout.js +150 -0
- package/dist/src/ai/tools.d.ts +1 -1
- package/dist/src/ai/tools.js +62 -31
- package/dist/src/api/spec-reader.d.ts +1 -0
- package/dist/src/api/spec-reader.js +93 -1
- package/dist/src/application-spec.d.ts +3 -0
- package/dist/src/application-spec.js +21 -5
- package/dist/src/commands/base-command.d.ts +3 -3
- package/dist/src/commands/init-command.d.ts +3 -0
- package/dist/src/commands/init-command.js +6 -3
- package/dist/src/config.d.ts +6 -1
- package/dist/src/explorbot.d.ts +3 -0
- package/dist/src/explorbot.js +33 -0
- package/dist/src/explorer.d.ts +1 -1
- package/dist/src/explorer.js +1 -1
- package/dist/src/knowledge-tracker.d.ts +1 -0
- package/dist/src/knowledge-tracker.js +3 -0
- package/dist/src/utils/aria-ref.d.ts +16 -0
- package/dist/src/utils/aria-ref.js +47 -0
- package/dist/src/utils/aria.js +3 -3
- package/dist/src/utils/html-diff.js +4 -1
- package/dist/src/utils/web-annotate.js +3 -15
- package/dist/src/utils/web-element.d.ts +0 -2
- package/dist/src/utils/web-element.js +0 -8
- package/docs/api-testing/basics.md +26 -2
- package/docs/reference/configuration.md +28 -1
- package/docs/superpowers/specs/2026-09-09-pagination-rule-design.md +317 -0
- package/docs/web-testing/agents.md +9 -1
- package/docs/web-testing/planner.md +5 -0
- package/docs/workflow/application-spec.md +4 -0
- package/package.json +4 -4
- package/src/ai/fisherman/tools.ts +8 -1
- package/src/ai/fisherman.ts +2 -1
- package/src/ai/pilot.ts +8 -25
- package/src/ai/planner.ts +33 -0
- package/src/ai/provider.ts +2 -1
- package/src/ai/researcher/deep-analysis.ts +13 -6
- package/src/ai/rules.ts +8 -7
- package/src/ai/scout/tools.ts +150 -0
- package/src/ai/scout.ts +173 -0
- package/src/ai/tools.ts +75 -38
- package/src/api/spec-reader.ts +106 -1
- package/src/application-spec.ts +22 -4
- package/src/commands/base-command.ts +3 -3
- package/src/commands/init-command.ts +6 -3
- package/src/config.ts +7 -0
- package/src/explorbot.ts +36 -0
- package/src/explorer.ts +1 -1
- package/src/knowledge-tracker.ts +4 -0
- package/src/utils/aria-ref.ts +61 -0
- package/src/utils/aria.ts +3 -3
- package/src/utils/html-diff.ts +3 -1
- package/src/utils/web-annotate.ts +3 -15
- package/src/utils/web-element.ts +0 -9
|
@@ -13,7 +13,7 @@ export class ApplicationSpec {
|
|
|
13
13
|
this.load();
|
|
14
14
|
}
|
|
15
15
|
renderFor(state) {
|
|
16
|
-
const relevant = this.
|
|
16
|
+
const relevant = this.relevantPages(state);
|
|
17
17
|
if (relevant.length === 0)
|
|
18
18
|
return '';
|
|
19
19
|
tag('operation').log(`Found application specification for ${state.url}`);
|
|
@@ -25,6 +25,9 @@ export class ApplicationSpec {
|
|
|
25
25
|
</application_spec>
|
|
26
26
|
`;
|
|
27
27
|
}
|
|
28
|
+
matchedUrls(state) {
|
|
29
|
+
return this.relevantPages(state).map((page) => page.url);
|
|
30
|
+
}
|
|
28
31
|
get pageCount() {
|
|
29
32
|
return this.pages.length;
|
|
30
33
|
}
|
|
@@ -63,9 +66,22 @@ export class ApplicationSpec {
|
|
|
63
66
|
}
|
|
64
67
|
}
|
|
65
68
|
resolveSourcePath(sourcePath) {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
return
|
|
69
|
+
return resolveSpecSource(sourcePath);
|
|
70
|
+
}
|
|
71
|
+
relevantPages(state) {
|
|
72
|
+
return this.pages.filter((page) => state.isMatchedBy({ url: page.url }));
|
|
70
73
|
}
|
|
71
74
|
}
|
|
75
|
+
export function resolveSpecBundlePath(sourcePath) {
|
|
76
|
+
const resolved = resolveSpecSource(sourcePath);
|
|
77
|
+
if (!existsSync(resolved))
|
|
78
|
+
return null;
|
|
79
|
+
if (statSync(resolved).isDirectory())
|
|
80
|
+
return resolved;
|
|
81
|
+
return path.dirname(resolved);
|
|
82
|
+
}
|
|
83
|
+
function resolveSpecSource(sourcePath) {
|
|
84
|
+
if (path.isAbsolute(sourcePath))
|
|
85
|
+
return path.resolve(sourcePath);
|
|
86
|
+
return path.resolve(ConfigParser.getInstance().resolveProjectDir(sourcePath));
|
|
87
|
+
}
|
|
@@ -7,15 +7,15 @@ export interface Suggestion {
|
|
|
7
7
|
command?: string;
|
|
8
8
|
hint: string;
|
|
9
9
|
}
|
|
10
|
-
export declare abstract class BaseCommand {
|
|
10
|
+
export declare abstract class BaseCommand<T = ExplorBot> {
|
|
11
11
|
abstract name: string;
|
|
12
12
|
abstract description: string;
|
|
13
13
|
aliases: string[];
|
|
14
14
|
options: CommandOption[];
|
|
15
15
|
tuiEnabled: boolean;
|
|
16
16
|
suggestions: Suggestion[];
|
|
17
|
-
explorBot:
|
|
18
|
-
constructor(explorBot:
|
|
17
|
+
explorBot: T;
|
|
18
|
+
constructor(explorBot: T);
|
|
19
19
|
abstract execute(args: string): Promise<void>;
|
|
20
20
|
matches(commandName: string): boolean;
|
|
21
21
|
printSuggestions(): void;
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import { type ModelRole } from '../config.js';
|
|
2
|
+
export declare function envTemplate(provider: string): string;
|
|
1
3
|
export declare function runInit(options: InitCommandOptions): Promise<void>;
|
|
2
4
|
export declare function writeGlobalConfig(provider: string, apiKey?: string): void;
|
|
3
5
|
export declare function runInitCommand(options: InitCommandOptions): void;
|
|
6
|
+
export declare function modelLines(provider: string, only?: ModelRole[]): string;
|
|
4
7
|
type InitCommandOptions = {
|
|
5
8
|
configPath?: string;
|
|
6
9
|
force?: boolean;
|
|
@@ -37,7 +37,7 @@ ${modelLines(provider)}
|
|
|
37
37
|
${moduleExport}
|
|
38
38
|
`;
|
|
39
39
|
}
|
|
40
|
-
function envTemplate(provider) {
|
|
40
|
+
export function envTemplate(provider) {
|
|
41
41
|
const keyLines = Object.entries(PROVIDERS).map(([name, { envKey }]) => {
|
|
42
42
|
if (name === provider)
|
|
43
43
|
return `${envKey}=`;
|
|
@@ -219,14 +219,17 @@ async function renderLocalProviderWizard() {
|
|
|
219
219
|
}), { exitOnCtrlC: false, patchConsole: false });
|
|
220
220
|
});
|
|
221
221
|
}
|
|
222
|
-
function modelLines(provider) {
|
|
222
|
+
export function modelLines(provider, only) {
|
|
223
223
|
const recommended = ConfigParser.recommendedModels()[provider] || {};
|
|
224
224
|
const roles = [
|
|
225
225
|
['model', 'fast model with tool calling capabilities'],
|
|
226
226
|
['visionModel', 'vision model for screenshot analysis'],
|
|
227
227
|
['agenticModel', 'agentic model for decision making'],
|
|
228
228
|
];
|
|
229
|
-
|
|
229
|
+
let selected = roles;
|
|
230
|
+
if (only)
|
|
231
|
+
selected = roles.filter(([role]) => only.includes(role));
|
|
232
|
+
return selected.map(([role, comment]) => ` // ${comment}\n ${role}: '${provider}/${recommended[role] || '<model-id>'}',`).join('\n');
|
|
230
233
|
}
|
|
231
234
|
function globalConfigTemplate(provider) {
|
|
232
235
|
const { envKey } = PROVIDERS[provider];
|
package/dist/src/config.d.ts
CHANGED
|
@@ -105,6 +105,10 @@ interface RerunnerAgentConfig extends AgentConfig {
|
|
|
105
105
|
interface PlannerAgentConfig extends AgentConfig {
|
|
106
106
|
styles?: string[];
|
|
107
107
|
stylesDir?: string;
|
|
108
|
+
docsWeight?: number;
|
|
109
|
+
}
|
|
110
|
+
interface ScoutAgentConfig extends AgentConfig {
|
|
111
|
+
dirs?: string[];
|
|
108
112
|
}
|
|
109
113
|
interface ScreencastConfig {
|
|
110
114
|
size?: {
|
|
@@ -129,6 +133,7 @@ interface AgentsConfig {
|
|
|
129
133
|
quartermaster?: AgentConfig;
|
|
130
134
|
historian?: HistorianAgentConfig;
|
|
131
135
|
fisherman?: AgentConfig;
|
|
136
|
+
scout?: ScoutAgentConfig;
|
|
132
137
|
chief?: AgentConfig;
|
|
133
138
|
curler?: AgentConfig;
|
|
134
139
|
rerunner?: RerunnerAgentConfig;
|
|
@@ -221,7 +226,7 @@ interface ExplorbotConfig {
|
|
|
221
226
|
type RuleEntry = string | Record<string, string>;
|
|
222
227
|
export declare const EXPLORBOT_CONFIG_PATHS: string[];
|
|
223
228
|
export declare const EXPLORBOT_ENV_VARS: EnvVar[];
|
|
224
|
-
export type { ExplorbotConfig, PlaywrightConfig, AIConfig, HtmlConfig, ActionConfig, AgentConfig, AgentsConfig, HistorianAgentConfig, ResearcherAgentConfig, NavigatorAgentConfig, PlannerAgentConfig, RerunnerAgentConfig, HealRecipe, Hook, HookConfig, HooksConfig, PlaywrightHook, CodeceptJSHook, HookPatternMap, RuleEntry, ReporterConfig, ApiConfig, WebConfig, ApiHookFn, };
|
|
229
|
+
export type { ExplorbotConfig, PlaywrightConfig, AIConfig, HtmlConfig, ActionConfig, AgentConfig, AgentsConfig, HistorianAgentConfig, ResearcherAgentConfig, NavigatorAgentConfig, PlannerAgentConfig, ScoutAgentConfig, RerunnerAgentConfig, HealRecipe, Hook, HookConfig, HooksConfig, PlaywrightHook, CodeceptJSHook, HookPatternMap, RuleEntry, ReporterConfig, ApiConfig, WebConfig, ApiHookFn, };
|
|
225
230
|
export declare class ConfigParser {
|
|
226
231
|
static instance: ConfigParser;
|
|
227
232
|
static recommended: Record<string, Record<string, string>> | null;
|
package/dist/src/explorbot.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { AIProvider } from './ai/provider.js';
|
|
|
12
12
|
import { Quartermaster } from './ai/quartermaster.js';
|
|
13
13
|
import { Rerunner } from './ai/rerunner.js';
|
|
14
14
|
import { Researcher } from './ai/researcher.js';
|
|
15
|
+
import { Scout } from './ai/scout.js';
|
|
15
16
|
import { SessionAnalyst } from './ai/session-analyst.js';
|
|
16
17
|
import { Tester } from './ai/tester.js';
|
|
17
18
|
import { RequestStore } from './api/request-store.js';
|
|
@@ -99,6 +100,8 @@ export declare class ExplorBot {
|
|
|
99
100
|
agentDriller(): Driller;
|
|
100
101
|
agentSessionAnalyst(): SessionAnalyst;
|
|
101
102
|
agentFisherman(): Fisherman | null;
|
|
103
|
+
agentScout(): Scout | null;
|
|
104
|
+
scoutCorpusDirs(): string[];
|
|
102
105
|
getCurrentPlan(): Plan | undefined;
|
|
103
106
|
getSuite(): Suite | null;
|
|
104
107
|
getPlanFeature(): string | undefined;
|
package/dist/src/explorbot.js
CHANGED
|
@@ -12,12 +12,15 @@ import { AIProvider } from "./ai/provider.js";
|
|
|
12
12
|
import { Quartermaster } from "./ai/quartermaster.js";
|
|
13
13
|
import { Rerunner } from "./ai/rerunner.js";
|
|
14
14
|
import { Researcher } from "./ai/researcher.js";
|
|
15
|
+
import { Scout } from "./ai/scout.js";
|
|
16
|
+
import { loadScoutCorpus } from "./ai/scout/tools.js";
|
|
15
17
|
import { SessionAnalyst } from "./ai/session-analyst.js";
|
|
16
18
|
import { Tester } from "./ai/tester.js";
|
|
17
19
|
import { createAgentTools } from "./ai/tools.js";
|
|
18
20
|
import { ApiClient } from "./api/api-client.js";
|
|
19
21
|
import { RequestStore } from "./api/request-store.js";
|
|
20
22
|
import { loadSpec } from "./api/spec-reader.js";
|
|
23
|
+
import { resolveSpecBundlePath } from "./application-spec.js";
|
|
21
24
|
import { ConfigParser } from "./config.js";
|
|
22
25
|
import { ExperienceTracker } from "./experience-tracker.js";
|
|
23
26
|
import Explorer from "./explorer.js";
|
|
@@ -184,6 +187,9 @@ export class ExplorBot {
|
|
|
184
187
|
const fisherman = this.agentFisherman();
|
|
185
188
|
if (fisherman)
|
|
186
189
|
this.agents.planner.setFisherman(fisherman);
|
|
190
|
+
const scout = this.agentScout();
|
|
191
|
+
if (scout)
|
|
192
|
+
this.agents.planner.setScout(scout);
|
|
187
193
|
}
|
|
188
194
|
return this.agents.planner;
|
|
189
195
|
}
|
|
@@ -310,6 +316,33 @@ export class ExplorBot {
|
|
|
310
316
|
}
|
|
311
317
|
return this.agents.fisherman;
|
|
312
318
|
}
|
|
319
|
+
agentScout() {
|
|
320
|
+
const scoutConfig = this.config.ai?.agents?.scout;
|
|
321
|
+
if (scoutConfig?.enabled !== true)
|
|
322
|
+
return null;
|
|
323
|
+
const dirs = this.scoutCorpusDirs();
|
|
324
|
+
if (dirs.length === 0) {
|
|
325
|
+
tag('warning').log('Scout enabled but no documentation found — set --spec or ai.agents.scout.dirs');
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
return (this.agents.scout ||= this.createAgent(({ ai }) => new Scout(ai, loadScoutCorpus(dirs))));
|
|
329
|
+
}
|
|
330
|
+
scoutCorpusDirs() {
|
|
331
|
+
const dirs = [];
|
|
332
|
+
const specPath = this.options.applicationSpec || this.config.dirs?.spec;
|
|
333
|
+
if (specPath) {
|
|
334
|
+
const bundle = resolveSpecBundlePath(specPath);
|
|
335
|
+
const pagesDir = bundle && path.join(bundle, 'pages');
|
|
336
|
+
if (pagesDir && existsSync(pagesDir))
|
|
337
|
+
dirs.push(pagesDir);
|
|
338
|
+
}
|
|
339
|
+
for (const dir of this.config.ai?.agents?.scout?.dirs || []) {
|
|
340
|
+
const resolved = this.configParser.resolveProjectDir(dir);
|
|
341
|
+
if (existsSync(resolved))
|
|
342
|
+
dirs.push(resolved);
|
|
343
|
+
}
|
|
344
|
+
return [...new Set(dirs)];
|
|
345
|
+
}
|
|
313
346
|
getCurrentPlan() {
|
|
314
347
|
return this.currentPlan;
|
|
315
348
|
}
|
package/dist/src/explorer.d.ts
CHANGED
|
@@ -39,7 +39,7 @@ declare class Explorer {
|
|
|
39
39
|
testPageErrorHandler: ((error: Error) => void) | null;
|
|
40
40
|
testConsoleHandler: ((message: any) => void) | null;
|
|
41
41
|
testDialogHandler: ((dialog: any) => void) | null;
|
|
42
|
-
eventDispatcher:
|
|
42
|
+
eventDispatcher: NodeJS.EventEmitter<[never]>;
|
|
43
43
|
constructor(config: ExplorbotConfig, options: ExplorerOptions | undefined, deps: ExplorerDeps);
|
|
44
44
|
get actor(): CodeceptJS.I;
|
|
45
45
|
get page(): Page | null;
|
package/dist/src/explorer.js
CHANGED
|
@@ -208,7 +208,7 @@ class Explorer {
|
|
|
208
208
|
codeceptjs.container.create(this.convertToCodeceptConfig(this.config), {});
|
|
209
209
|
}
|
|
210
210
|
convertToCodeceptConfig(config) {
|
|
211
|
-
const playwrightConfig = { ...config.playwright };
|
|
211
|
+
const playwrightConfig = { visibleLocator: true, ...config.playwright };
|
|
212
212
|
if (this.options?.show !== undefined) {
|
|
213
213
|
playwrightConfig.show = this.options.show;
|
|
214
214
|
}
|
|
@@ -16,6 +16,7 @@ export declare class KnowledgeTracker {
|
|
|
16
16
|
renderEndpointKnowledge(endpoint: string): string;
|
|
17
17
|
renderRelevantContext(state: ActionResult): string;
|
|
18
18
|
renderApplicationSpec(state: ActionResult): string;
|
|
19
|
+
applicationSpecUrls(state: ActionResult): string[];
|
|
19
20
|
addKnowledge(urlPattern: string, description: string, opts?: {
|
|
20
21
|
replace?: boolean;
|
|
21
22
|
}): {
|
|
@@ -79,6 +79,9 @@ export class KnowledgeTracker {
|
|
|
79
79
|
renderApplicationSpec(state) {
|
|
80
80
|
return this.applicationSpec?.renderFor(state) || '';
|
|
81
81
|
}
|
|
82
|
+
applicationSpecUrls(state) {
|
|
83
|
+
return this.applicationSpec?.matchedUrls(state) || [];
|
|
84
|
+
}
|
|
82
85
|
addKnowledge(urlPattern, description, opts) {
|
|
83
86
|
const configParser = ConfigParser.getInstance();
|
|
84
87
|
const configPath = configParser.getConfigPath();
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { WebElement } from './web-element.js';
|
|
2
|
+
export declare function ariaRefSelector(ref: string): string;
|
|
3
|
+
export declare function isAriaRef(ref: string): boolean;
|
|
4
|
+
export declare function ariaRefSnapshot(page: any): Promise<string>;
|
|
5
|
+
export declare function parseAriaRefs(ariaSnapshot: string): AriaRefEntry[];
|
|
6
|
+
export declare function elementFromAriaRef(page: any, ref: string): Promise<WebElement | null>;
|
|
7
|
+
export declare function refIsGone(explorer: any, ref: string): Promise<boolean>;
|
|
8
|
+
export declare function describeRef(explorer: any, ref: string): Promise<{
|
|
9
|
+
role: string;
|
|
10
|
+
text: string;
|
|
11
|
+
} | null>;
|
|
12
|
+
export interface AriaRefEntry {
|
|
13
|
+
role: string;
|
|
14
|
+
name: string;
|
|
15
|
+
ref: string;
|
|
16
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { WebElement } from "./web-element.js";
|
|
2
|
+
const REF_LINE_PATTERN = /^(\s*)-\s+(\w+)\s*(?:"([^"]*)")?.*?\[ref=(e\d+)\]/;
|
|
3
|
+
const ARIA_REF_PATTERN = /^(f\d+)?e\d+$/i;
|
|
4
|
+
const REF_ROLES = { a: 'link', button: 'button', select: 'combobox', textarea: 'textbox' };
|
|
5
|
+
export function ariaRefSelector(ref) {
|
|
6
|
+
return `aria-ref=${ref}`;
|
|
7
|
+
}
|
|
8
|
+
export function isAriaRef(ref) {
|
|
9
|
+
return ARIA_REF_PATTERN.test(ref);
|
|
10
|
+
}
|
|
11
|
+
export function ariaRefSnapshot(page) {
|
|
12
|
+
return page.locator('body').ariaSnapshot({ mode: 'ai' });
|
|
13
|
+
}
|
|
14
|
+
export function parseAriaRefs(ariaSnapshot) {
|
|
15
|
+
const entries = [];
|
|
16
|
+
for (const line of ariaSnapshot.split('\n')) {
|
|
17
|
+
const match = line.match(REF_LINE_PATTERN);
|
|
18
|
+
if (!match)
|
|
19
|
+
continue;
|
|
20
|
+
entries.push({ role: match[2], name: match[3] || '', ref: match[4] });
|
|
21
|
+
}
|
|
22
|
+
return entries;
|
|
23
|
+
}
|
|
24
|
+
export async function elementFromAriaRef(page, ref) {
|
|
25
|
+
if (!isAriaRef(ref))
|
|
26
|
+
return null;
|
|
27
|
+
return WebElement.fromPlaywrightLocator(page.locator(ariaRefSelector(ref)));
|
|
28
|
+
}
|
|
29
|
+
export async function refIsGone(explorer, ref) {
|
|
30
|
+
const count = () => Promise.resolve(explorer?.withPage?.((page) => page.locator(ariaRefSelector(ref)).count())).catch(() => undefined);
|
|
31
|
+
if ((await count()) !== 0)
|
|
32
|
+
return false;
|
|
33
|
+
await Promise.resolve(explorer?.withPage?.(ariaRefSnapshot)).catch(() => null);
|
|
34
|
+
return (await count()) === 0;
|
|
35
|
+
}
|
|
36
|
+
export async function describeRef(explorer, ref) {
|
|
37
|
+
return Promise.resolve(explorer?.withPage?.((page) => page.locator(ariaRefSelector(ref)).evaluate((el, roles) => {
|
|
38
|
+
const tag = el.tagName.toLowerCase();
|
|
39
|
+
const role = el.getAttribute('role') || roles[tag];
|
|
40
|
+
if (!role)
|
|
41
|
+
return null;
|
|
42
|
+
const text = (el.getAttribute('aria-label') || el.innerText || el.value || '').trim().split('\n')[0];
|
|
43
|
+
if (!text)
|
|
44
|
+
return null;
|
|
45
|
+
return { role, text };
|
|
46
|
+
}, REF_ROLES))).catch(() => null);
|
|
47
|
+
}
|
package/dist/src/utils/aria.js
CHANGED
|
@@ -434,9 +434,9 @@ const formatDiffSection = (label, items) => {
|
|
|
434
434
|
const summary = countBy(items);
|
|
435
435
|
if (summary.size === 0)
|
|
436
436
|
return [` ${label}: []`];
|
|
437
|
-
const
|
|
438
|
-
const top =
|
|
439
|
-
const rest =
|
|
437
|
+
const ordered = Array.from(summary.entries());
|
|
438
|
+
const top = ordered.slice(0, TOP_DIFF_ITEMS);
|
|
439
|
+
const rest = ordered.slice(TOP_DIFF_ITEMS);
|
|
440
440
|
const lines = [` ${label}:`];
|
|
441
441
|
for (const [item, count] of top) {
|
|
442
442
|
let suffix = '';
|
|
@@ -4,7 +4,7 @@ import { isDynamicId, isGenericClass } from "./xpath.js";
|
|
|
4
4
|
const IGNORED_PATHS = new Set(['html[1]', 'html[1]/head[1]', 'html[1]/body[1]']);
|
|
5
5
|
const SHELL_RATIO = 0.8;
|
|
6
6
|
const ROOT_CONTENT_RATIO = 0.8;
|
|
7
|
-
const LIVE_REGION_ROLES = new Set(['alert', 'alertdialog', 'status', 'log']);
|
|
7
|
+
const LIVE_REGION_ROLES = new Set(['alert', 'alertdialog', 'status', 'log', 'tooltip']);
|
|
8
8
|
const TEXT_LINE_PREFIX = 'TEXT:';
|
|
9
9
|
const MESSAGE_MAX_LENGTH = 200;
|
|
10
10
|
const MESSAGE_LIMIT = 8;
|
|
@@ -447,6 +447,9 @@ function semanticSelectorFor(element, allElements) {
|
|
|
447
447
|
const selector = buildContainerSelector(current, allElements);
|
|
448
448
|
if (selector)
|
|
449
449
|
return selector;
|
|
450
|
+
const classAttr = (current.attrs ?? []).find((a) => a.name === 'class')?.value;
|
|
451
|
+
if (classAttr && filterContainerClasses(classAttr.split(/\s+/).filter(Boolean)).length > 0)
|
|
452
|
+
return undefined;
|
|
450
453
|
current = dominantChild(current);
|
|
451
454
|
}
|
|
452
455
|
return undefined;
|
|
@@ -1,24 +1,12 @@
|
|
|
1
|
+
import { ariaRefSnapshot, parseAriaRefs } from "./aria-ref.js";
|
|
1
2
|
import { ELEMENT_EXTRACTION_CONFIG, getElementDataExtractorSource } from "./html.js";
|
|
2
3
|
import { createDebug } from './logger.js';
|
|
3
4
|
import { WebElement } from "./web-element.js";
|
|
4
5
|
const debugLog = createDebug('explorbot:web-annotate');
|
|
5
|
-
const REF_LINE_PATTERN = /^(\s*)-\s+(\w+)\s*(?:"([^"]*)")?.*?\[ref=(e\d+)\]/;
|
|
6
6
|
const ANNOTATABLE_ROLES = new Set(['button', 'link', 'textbox', 'searchbox', 'checkbox', 'radio', 'switch', 'combobox', 'tab', 'menuitem', 'menuitemcheckbox', 'menuitemradio', 'option', 'slider', 'spinbutton', 'treeitem']);
|
|
7
|
-
function parseAriaRefs(ariaSnapshot) {
|
|
8
|
-
const entries = [];
|
|
9
|
-
for (const line of ariaSnapshot.split('\n')) {
|
|
10
|
-
const match = line.match(REF_LINE_PATTERN);
|
|
11
|
-
if (!match)
|
|
12
|
-
continue;
|
|
13
|
-
if (!ANNOTATABLE_ROLES.has(match[2]))
|
|
14
|
-
continue;
|
|
15
|
-
entries.push({ role: match[2], name: match[3] || '', ref: match[4] });
|
|
16
|
-
}
|
|
17
|
-
return entries;
|
|
18
|
-
}
|
|
19
7
|
export async function annotatePageElements(page) {
|
|
20
|
-
const ariaSnapshot = await page
|
|
21
|
-
const refEntries = parseAriaRefs(ariaSnapshot);
|
|
8
|
+
const ariaSnapshot = await ariaRefSnapshot(page);
|
|
9
|
+
const refEntries = parseAriaRefs(ariaSnapshot).filter((entry) => ANNOTATABLE_ROLES.has(entry.role));
|
|
22
10
|
const byRole = new Map();
|
|
23
11
|
for (const { role, name, ref } of refEntries) {
|
|
24
12
|
let list = byRole.get(role);
|
|
@@ -36,8 +36,6 @@ export declare class WebElement {
|
|
|
36
36
|
static fromXPathMatch(m: XPathMatch): WebElement;
|
|
37
37
|
static fromPlaywrightLocator(locator: any): Promise<WebElement | null>;
|
|
38
38
|
static fromEidx(page: any, eidx: string): Promise<WebElement | null>;
|
|
39
|
-
static isAriaRef(ref: string): boolean;
|
|
40
|
-
static fromAriaRef(page: any, ref: string): Promise<WebElement | null>;
|
|
41
39
|
static fromEidxList(page: any, eidxList: string[]): Promise<WebElement[]>;
|
|
42
40
|
static commonAncestor(page: any, eidxList: string[]): Promise<WebElement | null>;
|
|
43
41
|
static findByXPath(html: string, xpath: string): Promise<{
|
|
@@ -111,14 +111,6 @@ export class WebElement {
|
|
|
111
111
|
static async fromEidx(page, eidx) {
|
|
112
112
|
return WebElement.fromPlaywrightLocator(page.locator(`[${EXPLORBOT_ATTRS.eidx}="${eidx}"]`));
|
|
113
113
|
}
|
|
114
|
-
static isAriaRef(ref) {
|
|
115
|
-
return /^(f\d+)?e\d+$/i.test(ref);
|
|
116
|
-
}
|
|
117
|
-
static async fromAriaRef(page, ref) {
|
|
118
|
-
if (!WebElement.isAriaRef(ref))
|
|
119
|
-
return null;
|
|
120
|
-
return WebElement.fromPlaywrightLocator(page.locator(`aria-ref=${ref}`));
|
|
121
|
-
}
|
|
122
114
|
static async fromEidxList(page, eidxList) {
|
|
123
115
|
const validEidxList = eidxList.filter((eidx) => /^e\d+$/i.test(eidx));
|
|
124
116
|
if (validEidxList.length === 0)
|
|
@@ -66,7 +66,7 @@ npx explorbot api explore https://api.example.com/v1 \
|
|
|
66
66
|
-H "Authorization: Bearer $TOKEN"
|
|
67
67
|
```
|
|
68
68
|
|
|
69
|
-
`api explore` takes
|
|
69
|
+
`api explore` takes an endpoint or a pattern as its argument, so one line covers the whole run: it plans, executes each plan, and reports the totals. Passing the base endpoint covers every collection the spec describes. The other commands take a path within the API and read the base from `--endpoint`:
|
|
70
70
|
|
|
71
71
|
```bash
|
|
72
72
|
npx explorbot api plan /users \
|
|
@@ -81,7 +81,7 @@ The base endpoint keeps its path prefix: given `https://api.example.com/v1`, ste
|
|
|
81
81
|
|
|
82
82
|
### A dedicated API project
|
|
83
83
|
|
|
84
|
-
If you don't have a web `explorbot.config.js`, run `npx explorbot api init`. It asks for your base endpoint, spec, and a one-line description of the API, then writes a standalone `apibot.config.
|
|
84
|
+
If you don't have a web `explorbot.config.js`, run `npx explorbot api init`. It asks for your base endpoint, spec, and a one-line description of the API, then writes a standalone `apibot.config.js` (with an `ai` and `api` section) plus `output/` and `knowledge/` directories. When both files exist, `apibot.config.*` takes precedence over `explorbot.config.*`.
|
|
85
85
|
|
|
86
86
|
## Your first run
|
|
87
87
|
|
|
@@ -99,6 +99,30 @@ npx explorbot api test output/plans/users.md
|
|
|
99
99
|
|
|
100
100
|
Curler runs the scenarios and prints how many passed and failed.
|
|
101
101
|
|
|
102
|
+
### Covering many endpoints
|
|
103
|
+
|
|
104
|
+
`api explore` runs the whole loop for you: plan, test, re-plan. Given one endpoint it plans in every style.
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
npx explorbot api explore /users
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
The endpoint may be a pattern. `*` stands for one path segment, and a pattern also covers the paths below it, so `/users` and `/users/*` both cover `/users/{id}`. Quote it, or your shell will try to expand it first.
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
npx explorbot api explore '/projects/acme/*'
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Explorbot explores collections, not raw paths: `/users/{id}` and `/users/{id}/posts` fold into `/users`, whose spec lookup brings them along anyway. When a pattern matches several collections the planning styles spread across them, one style per collection, so covering a whole API stays one plan per collection rather than one per style.
|
|
117
|
+
|
|
118
|
+
Pass `/` to take every collection in the spec. The path parameters have to come from somewhere, so put them in the base endpoint.
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
npx explorbot api explore / --endpoint https://api.example.com/v2/acme
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
If a parameter is left with no value the run stops and names it, rather than sending requests to a literal `{project_id}`. Collections whose own parameters no pattern can fill, like `/analytics/stats/{kind}`, are listed and skipped.
|
|
125
|
+
|
|
102
126
|
## Output files
|
|
103
127
|
|
|
104
128
|
| Output | Location | What it is |
|
|
@@ -179,6 +179,7 @@ Each agent takes its own model and system prompt.
|
|
|
179
179
|
| `rerunner` | Heals failing steps when re-running generated tests |
|
|
180
180
|
| `analyst` | Writes the end-of-session markdown report |
|
|
181
181
|
| `fisherman` | Prepares test data through API requests |
|
|
182
|
+
| `scout` | Retrieves relevant documentation for the Planner |
|
|
182
183
|
| `chief` | API test planning |
|
|
183
184
|
| `curler` | API test execution |
|
|
184
185
|
|
|
@@ -206,7 +207,7 @@ agents: {
|
|
|
206
207
|
| `beforeHook` | `Hook \| HookPatternMap` | Code to run before agent execution |
|
|
207
208
|
| `afterHook` | `Hook \| HookPatternMap` | Code to run after agent execution |
|
|
208
209
|
|
|
209
|
-
Some agents take extra options: `pilot` accepts `stepsToReview` (recent steps reviewed per check, default 5); `planner` accepts `styles` (see [Planning styles](#planning-styles)); `rerunner` accepts `healLimit` (max heal attempts, default 3) and `recipes` (custom heal recipes, see [Rerunning Tests](../web-testing/rerun.md)). Researcher and Historian options are documented below.
|
|
210
|
+
Some agents take extra options: `pilot` accepts `stepsToReview` (recent steps reviewed per check, default 5); `planner` accepts `styles` (see [Planning styles](#planning-styles)) and `docsWeight` (share of scenarios grounded in documentation when Scout is enabled, default 70); `scout` accepts `dirs` (see [Scout agent](#scout-agent)); `rerunner` accepts `healLimit` (max heal attempts, default 3) and `recipes` (custom heal recipes, see [Rerunning Tests](../web-testing/rerun.md)). Researcher and Historian options are documented below.
|
|
210
211
|
|
|
211
212
|
See [Agent hooks](../web-testing/hooks.md) for hook configuration.
|
|
212
213
|
|
|
@@ -257,6 +258,31 @@ See [AI providers](../basics/providers.md) for recommended models and provider s
|
|
|
257
258
|
|
|
258
259
|
Fisherman prepares test data over the API before a scenario runs, and can also answer questions about data that already exists without creating or changing anything. Pilot reaches this read-only capability through its `askApi(question)` tool, calling it to check whether suitable data is already there — or to get the exact name or id of an existing record — before deciding whether to create anything through `precondition()`. In replicate mode, where Fisherman learns the API by watching browser traffic instead of reading a spec, the read endpoints it can query come from successful GET requests observed in the browser, alongside the write endpoints already captured from XHR traffic. The endpoint list shown to the model names only the path and its query-parameter names, never their values; the underlying capture on disk holds the full request URL and headers — what write captures already hold — but no response body.
|
|
259
260
|
|
|
261
|
+
### Scout agent
|
|
262
|
+
|
|
263
|
+
Scout retrieves documentation relevant to the page being planned and hands it to the Planner as a `<docs_context>` block, so scenarios can be grounded in what the application documents say. It is opt-in and needs documentation collected beforehand:
|
|
264
|
+
|
|
265
|
+
```javascript
|
|
266
|
+
ai: {
|
|
267
|
+
agents: {
|
|
268
|
+
scout: {
|
|
269
|
+
enabled: true, // Opt in — Scout never runs without this
|
|
270
|
+
dirs: ['docs'], // Extra markdown directories to search, beyond the spec bundle
|
|
271
|
+
},
|
|
272
|
+
planner: {
|
|
273
|
+
docsWeight: 70, // Roughly 70% of scenarios exercise documented behavior, the rest explore beyond it
|
|
274
|
+
},
|
|
275
|
+
},
|
|
276
|
+
},
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
| Option | Type | Description |
|
|
280
|
+
|--------|------|-------------|
|
|
281
|
+
| `enabled` | `boolean` | Turn Scout on. Default: off. |
|
|
282
|
+
| `dirs` | `string[]` | Markdown directories added to the corpus, resolved relative to the project |
|
|
283
|
+
|
|
284
|
+
The corpus combines the [application spec](../workflow/application-spec.md) bundle (`--spec` / `EXPLORBOT_SPEC` / `dirs.spec`, set by `explorbot docs collect`) with the `dirs` above. Scout scans it with the same `bash` + `readFile` tools Captain uses: the corpus is loaded into an in-memory sandbox and the model itself runs `rg` (or `grep` — whichever is installed) to explore it. One of the two must be on PATH — Scout fails loudly when neither is found. Pages already injected for the current URL as `<application_spec>` are excluded from the Scout corpus, so the two blocks never duplicate each other. Files under `dirs` that carry no page URL are listed as hand-written notes for Scout to inspect when they are relevant.
|
|
285
|
+
|
|
260
286
|
## Playwright settings
|
|
261
287
|
|
|
262
288
|
### Browser selection
|
|
@@ -512,6 +538,7 @@ export default {
|
|
|
512
538
|
quartermaster: { /* ... */ },
|
|
513
539
|
historian: { /* ... */ },
|
|
514
540
|
fisherman: { /* ... */ },
|
|
541
|
+
scout: { enabled: true, dirs: ['docs'] }, // Documentation retrieval for the Planner
|
|
515
542
|
rerunner: { /* ... */ },
|
|
516
543
|
analyst: { /* ... */ },
|
|
517
544
|
},
|