explorbot 0.1.25 → 0.1.27
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/dist/package.json +1 -1
- package/dist/src/ai/historian/codeceptjs.js +1 -1
- package/dist/src/ai/historian/experience.js +1 -1
- package/dist/src/ai/historian/playwright.js +1 -1
- package/dist/src/ai/historian/screencast.js +3 -3
- package/dist/src/ai/historian.js +1 -1
- package/dist/src/ai/navigator.js +4 -4
- package/dist/src/ai/pilot.js +8 -5
- package/dist/src/ai/planner.js +2 -4
- package/dist/src/ai/provider.js +5 -3
- package/dist/src/ai/researcher/cache.js +8 -0
- package/dist/src/ai/researcher/deep-analysis.js +146 -61
- package/dist/src/ai/researcher/locators.js +1 -1
- package/dist/src/ai/researcher.js +4 -4
- package/dist/src/ai/session-analyst.js +4 -1
- package/dist/src/ai/task-agent.js +2 -2
- package/dist/src/commands/context-aria-command.js +1 -1
- package/dist/src/commands/explore-command.js +67 -25
- package/dist/src/commands/test-command.js +2 -1
- package/dist/src/components/App.js +1 -33
- package/dist/src/components/LogPane.js +9 -3
- package/dist/src/experience-tracker.js +2 -2
- package/dist/src/explorbot.js +1 -1
- package/dist/src/reporter.js +24 -5
- package/dist/src/utils/log-filters.js +27 -0
- package/dist/src/utils/logger.js +28 -1
- package/dist/src/utils/next-steps.js +1 -7
- package/package.json +1 -1
- package/src/ai/historian/codeceptjs.ts +1 -1
- package/src/ai/historian/experience.ts +1 -1
- package/src/ai/historian/playwright.ts +1 -1
- package/src/ai/historian/screencast.ts +3 -3
- package/src/ai/historian.ts +1 -1
- package/src/ai/navigator.ts +4 -4
- package/src/ai/pilot.ts +8 -5
- package/src/ai/planner.ts +1 -3
- package/src/ai/provider.ts +5 -3
- package/src/ai/researcher/cache.ts +7 -0
- package/src/ai/researcher/deep-analysis.ts +166 -68
- package/src/ai/researcher/locators.ts +1 -1
- package/src/ai/researcher.ts +4 -4
- package/src/ai/session-analyst.ts +4 -1
- package/src/ai/task-agent.ts +2 -2
- package/src/commands/context-aria-command.ts +1 -1
- package/src/commands/explore-command.ts +68 -23
- package/src/commands/test-command.ts +2 -1
- package/src/components/App.tsx +0 -33
- package/src/components/LogPane.tsx +18 -3
- package/src/experience-tracker.ts +2 -2
- package/src/explorbot.ts +1 -1
- package/src/reporter.ts +24 -6
- package/src/utils/log-filters.ts +26 -0
- package/src/utils/logger.ts +24 -2
- package/src/utils/next-steps.ts +1 -6
package/src/ai/pilot.ts
CHANGED
|
@@ -104,7 +104,7 @@ export class Pilot implements Agent {
|
|
|
104
104
|
|
|
105
105
|
const schema = z.object({
|
|
106
106
|
decision: z.enum(['pass', 'fail', 'continue', 'skipped']).describe('pass = test succeeded, fail = test failed, continue = tester should keep going, skipped = scenario is irrelevant OR systematic execution failures prevented testing'),
|
|
107
|
-
reason: z.string().describe('
|
|
107
|
+
reason: z.string().describe('Concise user-facing reason, maximum 1 short sentence and 120 characters. Do NOT repeat the decision status; explain only the evidence. For continue: explain why rejected and suggest alternatives.'),
|
|
108
108
|
guidance: z.string().nullable().describe('Required for "continue": specific actionable instruction for the tester — what exactly to verify, retry differently, or complete next. Be concrete.'),
|
|
109
109
|
requestVerification: z
|
|
110
110
|
.string()
|
|
@@ -177,7 +177,7 @@ export class Pilot implements Agent {
|
|
|
177
177
|
}
|
|
178
178
|
}
|
|
179
179
|
|
|
180
|
-
tag('info').log(`Pilot: ${result.decision}
|
|
180
|
+
tag('info').log(`Pilot: ${result.decision} - ${result.reason}`);
|
|
181
181
|
task.summary = result.reason;
|
|
182
182
|
|
|
183
183
|
const verdictState = screenshotState || currentState;
|
|
@@ -221,7 +221,7 @@ export class Pilot implements Agent {
|
|
|
221
221
|
|
|
222
222
|
const schema = z.object({
|
|
223
223
|
decision: z.enum(['allow', 'fail', 'continue', 'skipped']).describe('allow = reset proceeds, fail = test failed (stop looping), continue = veto reset, tester should act on current page instead, skipped = scenario is irrelevant or cannot be executed'),
|
|
224
|
-
reason: z.string().describe('
|
|
224
|
+
reason: z.string().describe('Concise evidence-only reason, maximum 1 short sentence and 120 characters. Do not restate the decision.'),
|
|
225
225
|
guidance: z.string().nullable().describe('Required for "continue": concrete instruction for what the tester should do instead of resetting (e.g. which tool to call, what to verify).'),
|
|
226
226
|
});
|
|
227
227
|
|
|
@@ -388,8 +388,9 @@ export class Pilot implements Agent {
|
|
|
388
388
|
- "continue": tester hasn't completed the goal; provide concrete guidance (which tool, what to check).
|
|
389
389
|
If a verify() asserted a state that was ALREADY TRUE before the test, it proves nothing — reject.
|
|
390
390
|
|
|
391
|
-
reason field:
|
|
392
|
-
what was verified, what failed,
|
|
391
|
+
reason field: one short sentence, maximum 120 characters. Do NOT restate the decision
|
|
392
|
+
("scenario goal achieved/not achieved"). State what happened: what was verified, what failed,
|
|
393
|
+
or what evidence was found.
|
|
393
394
|
`;
|
|
394
395
|
}
|
|
395
396
|
|
|
@@ -1017,6 +1018,8 @@ export class Pilot implements Agent {
|
|
|
1017
1018
|
Response format:
|
|
1018
1019
|
PROGRESS: <1 sentence assessment>
|
|
1019
1020
|
NEXT: <specific actionable instruction for Tester>
|
|
1021
|
+
|
|
1022
|
+
Keep user-facing reasons concise: one short sentence, maximum 120 characters, evidence only, no repeated verdict wording.
|
|
1020
1023
|
`;
|
|
1021
1024
|
}
|
|
1022
1025
|
}
|
package/src/ai/planner.ts
CHANGED
|
@@ -36,7 +36,7 @@ const TasksSchema = z.object({
|
|
|
36
36
|
scenario: z.string().describe('A single sentence describing what to test'),
|
|
37
37
|
priority: z.enum(['critical', 'important', 'high', 'normal', 'low']).describe('Priority of the task based on business importance'),
|
|
38
38
|
startUrl: z.string().nullable().describe('Start URL for the test if different from plan URL (only for tests on visited subpages)'),
|
|
39
|
-
steps: z.array(z.string()).describe('List of steps to perform for this scenario. Each step should be a specific action (e.g., "
|
|
39
|
+
steps: z.array(z.string()).describe('List of steps to perform for this scenario. Each step should be a specific action (e.g., "Open the form", "Enter required data", "Submit the form"). Keep steps atomic and actionable.'),
|
|
40
40
|
expectedOutcomes: z
|
|
41
41
|
.array(z.string())
|
|
42
42
|
.describe('List of expected outcomes that can be verified. Each outcome should be simple, specific, and easy to check (e.g., "Success message appears", "URL changes to /dashboard", "Form field shows error"). Keep outcomes atomic - do not combine multiple checks into one.'),
|
|
@@ -226,9 +226,7 @@ export class Planner extends PlannerBase implements Agent {
|
|
|
226
226
|
}
|
|
227
227
|
}
|
|
228
228
|
|
|
229
|
-
const availableStyles = Object.keys(getStyles()).join(', ');
|
|
230
229
|
tag('success').log(`Planning complete! ${this.currentPlan.tests.length} tests in plan: ${this.currentPlan.title}`);
|
|
231
|
-
tag('info').log(`Planning style: ${this.lastStyleName} (available: ${availableStyles})`);
|
|
232
230
|
|
|
233
231
|
if (state.url) registerPlan(state.url, this.currentPlan, feature, state.hash);
|
|
234
232
|
|
package/src/ai/provider.ts
CHANGED
|
@@ -294,8 +294,11 @@ export class Provider {
|
|
|
294
294
|
}
|
|
295
295
|
throw new ContextLengthError(error.message || error.toString());
|
|
296
296
|
}
|
|
297
|
-
|
|
298
|
-
|
|
297
|
+
const message = error.message || error.toString();
|
|
298
|
+
if (message !== 'No response text from AI') {
|
|
299
|
+
tag('error').log(message);
|
|
300
|
+
}
|
|
301
|
+
throw new AiError(message);
|
|
299
302
|
}
|
|
300
303
|
}
|
|
301
304
|
|
|
@@ -376,7 +379,6 @@ export class Provider {
|
|
|
376
379
|
} catch (error: any) {
|
|
377
380
|
clearActivity();
|
|
378
381
|
if (error?.message?.includes('Tool choice is required')) {
|
|
379
|
-
tag('warning').log('Model completed without calling a tool, returning empty result');
|
|
380
382
|
return { text: '', toolCalls: [], toolResults: [], response: { messages: [] }, usage: null };
|
|
381
383
|
}
|
|
382
384
|
if (error?.name === 'AbortError') throw error;
|
|
@@ -49,6 +49,13 @@ export function getCachedResearch(hash: string): string {
|
|
|
49
49
|
return cached;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
export function getPreviousResearch(hash: string): string {
|
|
53
|
+
if (!hash) return '';
|
|
54
|
+
const researchFile = outputPath('research', `${hash}.md`);
|
|
55
|
+
if (!existsSync(researchFile)) return '';
|
|
56
|
+
return readFileSync(researchFile, 'utf8');
|
|
57
|
+
}
|
|
58
|
+
|
|
52
59
|
export function saveResearch(hash: string, text: string, combinedHtml?: string): string {
|
|
53
60
|
const researchDir = outputPath('research');
|
|
54
61
|
const researchFile = join(researchDir, `${hash}.md`);
|
|
@@ -5,10 +5,11 @@ import type Explorer from '../../explorer.ts';
|
|
|
5
5
|
import type { StateManager } from '../../state-manager.js';
|
|
6
6
|
import { WebPageState } from '../../state-manager.js';
|
|
7
7
|
import { detectFocusArea, diffAriaSnapshots } from '../../utils/aria.ts';
|
|
8
|
+
import { extractCodeBlocks } from '../../utils/code-extractor.ts';
|
|
8
9
|
import { tag } from '../../utils/logger.js';
|
|
9
10
|
import { mdq } from '../../utils/markdown-query.ts';
|
|
10
11
|
import type { Provider } from '../provider.js';
|
|
11
|
-
import { getCachedResearch, saveResearch } from './cache.ts';
|
|
12
|
+
import { getCachedResearch, getPreviousResearch, saveResearch } from './cache.ts';
|
|
12
13
|
import { type Constructor, debugLog } from './mixin.ts';
|
|
13
14
|
import { type ResearchElement, parseResearchSections } from './parser.ts';
|
|
14
15
|
import type { ResearchResult } from './research-result.ts';
|
|
@@ -26,14 +27,30 @@ export function WithDeepAnalysis<T extends Constructor>(Base: T) {
|
|
|
26
27
|
tag('info').log('Starting deep analysis of expandable elements');
|
|
27
28
|
await (this as any).navigateTo(state.fullUrl || state.url);
|
|
28
29
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
const maxClicks = (this.explorer.getConfig().ai?.agents?.researcher as any)?.maxExpandableClicks ?? DEFAULT_MAX_EXPANDABLE_CLICKS;
|
|
31
|
+
|
|
32
|
+
const expandedSections: string[] = [];
|
|
33
|
+
const navigationLinks: Array<{ code: string; url: string }> = [];
|
|
34
|
+
let verifiedCodes: string[] = [];
|
|
35
|
+
let missing: PreviousSection[] = [];
|
|
36
|
+
|
|
37
|
+
const previousSections = this._loadPreviousExtendedSections(state.hash || '');
|
|
38
|
+
if (previousSections.length > 0) {
|
|
39
|
+
tag('substep').log(`Replaying ${previousSections.length} previously discovered sections`);
|
|
40
|
+
const replay = await this._replayPreviousSections(state, previousSections, maxClicks);
|
|
41
|
+
expandedSections.push(...replay.verified);
|
|
42
|
+
verifiedCodes = replay.verifiedCodes;
|
|
43
|
+
missing = replay.missing;
|
|
44
|
+
tag('info').log(`Reused ${replay.verified.length}/${previousSections.length} previous sections, ${missing.length} to re-discover`);
|
|
45
|
+
|
|
46
|
+
if (missing.length === 0 && replay.verified.length >= maxClicks) {
|
|
47
|
+
tag('info').log('Page appears unchanged, reusing previous sections and skipping discovery');
|
|
48
|
+
this._appendExtendedResearch(result, expandedSections, navigationLinks);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
33
51
|
}
|
|
34
|
-
tag('substep').log(`Identified ${expandables.length} expandable elements`);
|
|
35
52
|
|
|
36
|
-
|
|
53
|
+
let expandables = await this._discoverExpandables(result.text, missing, verifiedCodes);
|
|
37
54
|
if (expandables.length > maxClicks) {
|
|
38
55
|
expandables = await this._selectExpandables(expandables, state.fullUrl || state.url, maxClicks);
|
|
39
56
|
tag('substep').log(`Selected ${expandables.length} expandables to click (max: ${maxClicks})`);
|
|
@@ -44,10 +61,12 @@ export function WithDeepAnalysis<T extends Constructor>(Base: T) {
|
|
|
44
61
|
commands: this._buildClickCommands(el),
|
|
45
62
|
description: el.name,
|
|
46
63
|
}))
|
|
47
|
-
.filter((el) => el.commands.length > 0)
|
|
64
|
+
.filter((el) => el.commands.length > 0)
|
|
65
|
+
.filter((el) => !el.commands.some((cmd) => verifiedCodes.includes(cmd)));
|
|
48
66
|
|
|
49
67
|
if (elements.length === 0) {
|
|
50
|
-
tag('info').log('No
|
|
68
|
+
tag('info').log('No new expandable elements to click');
|
|
69
|
+
this._appendExtendedResearch(result, expandedSections, navigationLinks);
|
|
51
70
|
return;
|
|
52
71
|
}
|
|
53
72
|
|
|
@@ -57,25 +76,11 @@ export function WithDeepAnalysis<T extends Constructor>(Base: T) {
|
|
|
57
76
|
for (const el of elements) debugLog(`Expandable: ${el.description} → ${el.commands[0]}`);
|
|
58
77
|
tag('substep').log(`Clicking ${elements.length} expandable elements`);
|
|
59
78
|
|
|
60
|
-
const expandedSections: string[] = [];
|
|
61
|
-
const navigationLinks: Array<{ code: string; url: string }> = [];
|
|
62
|
-
|
|
63
79
|
await this._clickExpandableElements(elements, state, expandedSections, navigationLinks);
|
|
64
80
|
|
|
65
81
|
tag('info').log(`Deep analysis complete. Sections: ${expandedSections.length}, navigation links: ${navigationLinks.length}`);
|
|
66
82
|
|
|
67
|
-
|
|
68
|
-
if (dedupedSections.length !== expandedSections.length) {
|
|
69
|
-
tag('substep').log(`Deduplicated ${expandedSections.length} → ${dedupedSections.length} extended sections`);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
if (dedupedSections.length > 0) {
|
|
73
|
-
result.text += `\n\n# Extended Research\n\n${dedupedSections.join('\n\n---\n\n')}`;
|
|
74
|
-
}
|
|
75
|
-
if (navigationLinks.length > 0) {
|
|
76
|
-
const links = navigationLinks.map((l) => `- \`${l.code}\` opens ${l.url}`).join('\n');
|
|
77
|
-
result.text += `\n\n## Navigation Links\n\n${links}`;
|
|
78
|
-
}
|
|
83
|
+
this._appendExtendedResearch(result, expandedSections, navigationLinks);
|
|
79
84
|
}
|
|
80
85
|
|
|
81
86
|
async researchOverlay(current: ActionResult, previous: ActionResult, pageStateHash: string): Promise<string | null> {
|
|
@@ -127,7 +132,70 @@ export function WithDeepAnalysis<T extends Constructor>(Base: T) {
|
|
|
127
132
|
return sectionMarkdown;
|
|
128
133
|
}
|
|
129
134
|
|
|
130
|
-
private
|
|
135
|
+
private _loadPreviousExtendedSections(hash: string): PreviousSection[] {
|
|
136
|
+
if (!hash) return [];
|
|
137
|
+
const previous = getPreviousResearch(hash);
|
|
138
|
+
if (!previous) return [];
|
|
139
|
+
|
|
140
|
+
const sections: PreviousSection[] = [];
|
|
141
|
+
for (const section of parseResearchSections(previous)) {
|
|
142
|
+
if (!section.isExtended) continue;
|
|
143
|
+
const code = extractCodeBlocks(section.rawMarkdown)[0];
|
|
144
|
+
if (!code) continue;
|
|
145
|
+
sections.push({ name: section.name, code });
|
|
146
|
+
}
|
|
147
|
+
return sections;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
private async _replayPreviousSections(state: WebPageState, prevSections: PreviousSection[], maxClicks: number): Promise<{ verified: string[]; verifiedCodes: string[]; missing: PreviousSection[] }> {
|
|
151
|
+
const originalAria = state.ariaSnapshot || '';
|
|
152
|
+
const verified: string[] = [];
|
|
153
|
+
const verifiedCodes: string[] = [];
|
|
154
|
+
const missing: PreviousSection[] = [];
|
|
155
|
+
|
|
156
|
+
for (const section of prevSections.slice(0, maxClicks)) {
|
|
157
|
+
if (executionController.isInterrupted()) break;
|
|
158
|
+
|
|
159
|
+
let outcome: ExpansionOutcome;
|
|
160
|
+
try {
|
|
161
|
+
outcome = await this._executeAndAnalyze([section.code], section.name, state, originalAria, this._summarizeExpanded(verified));
|
|
162
|
+
} catch (err) {
|
|
163
|
+
tag('warning').log(`Replay failed for "${section.name}": ${err instanceof Error ? err.message : err}`);
|
|
164
|
+
await this._restorePageState(state.url, originalAria).catch(() => {});
|
|
165
|
+
missing.push(section);
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (outcome.status === 'revealed') {
|
|
170
|
+
verified.push(outcome.sectionMarkdown);
|
|
171
|
+
verifiedCodes.push(section.code);
|
|
172
|
+
debugLog(`Replayed and verified section: ${section.name}`);
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
debugLog(`Could not replay previous section: ${section.name}`);
|
|
177
|
+
missing.push(section);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return { verified, verifiedCodes, missing };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
private _appendExtendedResearch(result: ResearchResult, expandedSections: string[], navigationLinks: Array<{ code: string; url: string }>): void {
|
|
184
|
+
const dedupedSections = this._deduplicateExpandedSections(expandedSections);
|
|
185
|
+
if (dedupedSections.length !== expandedSections.length) {
|
|
186
|
+
tag('substep').log(`Deduplicated ${expandedSections.length} → ${dedupedSections.length} extended sections`);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (dedupedSections.length > 0) {
|
|
190
|
+
result.text += `\n\n# Extended Research\n\n${dedupedSections.join('\n\n---\n\n')}`;
|
|
191
|
+
}
|
|
192
|
+
if (navigationLinks.length > 0) {
|
|
193
|
+
const links = navigationLinks.map((l) => `- \`${l.code}\` opens ${l.url}`).join('\n');
|
|
194
|
+
result.text += `\n\n## Navigation Links\n\n${links}`;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
private async _discoverExpandables(researchText: string, missing: PreviousSection[] = [], verifiedCodes: string[] = []): Promise<ExpandableElement[]> {
|
|
131
199
|
const allElements = new Map<string, ExpandableElement>();
|
|
132
200
|
for (const section of parseResearchSections(researchText)) {
|
|
133
201
|
for (const el of section.elements) {
|
|
@@ -138,6 +206,16 @@ export function WithDeepAnalysis<T extends Constructor>(Base: T) {
|
|
|
138
206
|
|
|
139
207
|
const eidxList = [...allElements.keys()].join(', ');
|
|
140
208
|
|
|
209
|
+
let missingHint = '';
|
|
210
|
+
if (missing.length > 0) {
|
|
211
|
+
const list = missing.map((s) => `- "${s.name}" (previously revealed via ${s.code})`).join('\n');
|
|
212
|
+
missingHint = dedent`
|
|
213
|
+
|
|
214
|
+
These sections were present on a previous visit but their trigger could not be replayed now — the element may have moved or been renamed. Prioritize finding the element that now reveals each:
|
|
215
|
+
${list}
|
|
216
|
+
`;
|
|
217
|
+
}
|
|
218
|
+
|
|
141
219
|
const textPrompt = dedent`
|
|
142
220
|
From this UI research, identify elements that could reveal hidden UI when clicked
|
|
143
221
|
(dropdown menus, popups, expandable panels, accordion sections, overflow menus, tab switches).
|
|
@@ -145,6 +223,7 @@ export function WithDeepAnalysis<T extends Constructor>(Base: T) {
|
|
|
145
223
|
Available eidx refs: ${eidxList}
|
|
146
224
|
|
|
147
225
|
${researchText}
|
|
226
|
+
${missingHint}
|
|
148
227
|
|
|
149
228
|
Rules:
|
|
150
229
|
- Only pick elements that HIDE content until clicked (menus, dropdowns, accordions, tabs)
|
|
@@ -168,6 +247,7 @@ export function WithDeepAnalysis<T extends Constructor>(Base: T) {
|
|
|
168
247
|
|
|
169
248
|
Look for: overflow/ellipsis menus, chevron dropdowns, hamburger menus,
|
|
170
249
|
gear/settings buttons, accordion toggles, tab switches, filter buttons.
|
|
250
|
+
${missingHint}
|
|
171
251
|
|
|
172
252
|
Rules:
|
|
173
253
|
- For repeated icons (same icon on every list row), pick only the FIRST one
|
|
@@ -302,53 +382,15 @@ export function WithDeepAnalysis<T extends Constructor>(Base: T) {
|
|
|
302
382
|
}
|
|
303
383
|
}
|
|
304
384
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
if (await action.attempt(cmd, undefined, false)) {
|
|
309
|
-
clickCode = cmd;
|
|
310
|
-
break;
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
if (!clickCode) {
|
|
314
|
-
debugLog(`Click failed: ${el.description.slice(0, 80)}`);
|
|
315
|
-
continue;
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
await new Promise((r) => setTimeout(r, 500));
|
|
319
|
-
|
|
320
|
-
let diff: Diff;
|
|
321
|
-
try {
|
|
322
|
-
await this.explorer.createAction().capturePageState();
|
|
323
|
-
const currAR = ActionResult.fromState(this.stateManager.getCurrentState()!);
|
|
324
|
-
diff = await currAR.diff(previousState);
|
|
325
|
-
await diff.calculate();
|
|
326
|
-
} catch (err) {
|
|
327
|
-
tag('warning').log(`State capture failed after click: ${err instanceof Error ? err.message : err}`);
|
|
328
|
-
await this._restorePageState(state.url, originalAria);
|
|
329
|
-
continue;
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
if (diff.urlHasChanged()) {
|
|
333
|
-
debugLog(`Click navigated to ${this.stateManager.getCurrentState()?.url}`);
|
|
334
|
-
navigationLinks.push({ code: clickCode, url: this.stateManager.getCurrentState()?.url || '' });
|
|
335
|
-
await (this as any).navigateTo(state.url);
|
|
336
|
-
continue;
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
const clickHtmlSize = diff.htmlParts.reduce((sum, p) => sum + p.subtree.length, 0);
|
|
340
|
-
if (!diff.ariaChanged && clickHtmlSize <= 150) {
|
|
341
|
-
debugLog(`No changes from: ${el.description.slice(0, 80)}`);
|
|
385
|
+
const outcome = await this._executeAndAnalyze(el.commands, el.description, state, originalAria, this._summarizeExpanded(expandedSections));
|
|
386
|
+
if (outcome.status === 'navigated') {
|
|
387
|
+
navigationLinks.push({ code: outcome.code, url: outcome.url });
|
|
342
388
|
continue;
|
|
343
389
|
}
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
if (sectionMarkdown) {
|
|
347
|
-
expandedSections.push(sectionMarkdown);
|
|
390
|
+
if (outcome.status === 'revealed') {
|
|
391
|
+
expandedSections.push(outcome.sectionMarkdown);
|
|
348
392
|
debugLog(`Captured section from: ${el.description.slice(0, 80)}`);
|
|
349
393
|
}
|
|
350
|
-
|
|
351
|
-
await this._restorePageState(state.url, originalAria);
|
|
352
394
|
} catch (err) {
|
|
353
395
|
tag('warning').log(`Expandable click failed for "${el.description.slice(0, 80)}": ${err instanceof Error ? err.message : err}`);
|
|
354
396
|
try {
|
|
@@ -358,6 +400,55 @@ export function WithDeepAnalysis<T extends Constructor>(Base: T) {
|
|
|
358
400
|
}
|
|
359
401
|
}
|
|
360
402
|
|
|
403
|
+
private async _executeAndAnalyze(commands: string[], description: string, state: WebPageState, originalAria: string, alreadyExpanded: string[]): Promise<ExpansionOutcome> {
|
|
404
|
+
const previousState = ActionResult.fromState(this.stateManager.getCurrentState()!);
|
|
405
|
+
|
|
406
|
+
let clickCode: string | null = null;
|
|
407
|
+
const action = this.explorer.createAction();
|
|
408
|
+
for (const cmd of commands) {
|
|
409
|
+
if (await action.attempt(cmd, undefined, false)) {
|
|
410
|
+
clickCode = cmd;
|
|
411
|
+
break;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
if (!clickCode) {
|
|
415
|
+
debugLog(`Click failed: ${description.slice(0, 80)}`);
|
|
416
|
+
return { status: 'failed' };
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
420
|
+
|
|
421
|
+
let diff: Diff;
|
|
422
|
+
try {
|
|
423
|
+
await this.explorer.createAction().capturePageState();
|
|
424
|
+
const currAR = ActionResult.fromState(this.stateManager.getCurrentState()!);
|
|
425
|
+
diff = await currAR.diff(previousState);
|
|
426
|
+
await diff.calculate();
|
|
427
|
+
} catch (err) {
|
|
428
|
+
tag('warning').log(`State capture failed after click: ${err instanceof Error ? err.message : err}`);
|
|
429
|
+
await this._restorePageState(state.url, originalAria);
|
|
430
|
+
return { status: 'failed' };
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if (diff.urlHasChanged()) {
|
|
434
|
+
const url = this.stateManager.getCurrentState()?.url || '';
|
|
435
|
+
debugLog(`Click navigated to ${url}`);
|
|
436
|
+
await (this as any).navigateTo(state.url);
|
|
437
|
+
return { status: 'navigated', code: clickCode, url };
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const clickHtmlSize = diff.htmlParts.reduce((sum, p) => sum + p.subtree.length, 0);
|
|
441
|
+
if (!diff.ariaChanged && clickHtmlSize <= 150) {
|
|
442
|
+
debugLog(`No changes from: ${description.slice(0, 80)}`);
|
|
443
|
+
return { status: 'none', code: clickCode };
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const sectionMarkdown = await this._analyzeExpandedAction(clickCode, description, diff, alreadyExpanded);
|
|
447
|
+
await this._restorePageState(state.url, originalAria);
|
|
448
|
+
if (!sectionMarkdown) return { status: 'none', code: clickCode };
|
|
449
|
+
return { status: 'revealed', code: clickCode, sectionMarkdown };
|
|
450
|
+
}
|
|
451
|
+
|
|
361
452
|
private async _restorePageState(url: string, originalAria: string): Promise<void> {
|
|
362
453
|
try {
|
|
363
454
|
await (this as any).cancelInUi();
|
|
@@ -484,6 +575,13 @@ interface ExpandableElement extends ResearchElement {
|
|
|
484
575
|
container: string | null;
|
|
485
576
|
}
|
|
486
577
|
|
|
578
|
+
interface PreviousSection {
|
|
579
|
+
name: string;
|
|
580
|
+
code: string;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
type ExpansionOutcome = { status: 'revealed'; code: string; sectionMarkdown: string } | { status: 'navigated'; code: string; url: string } | { status: 'none'; code: string } | { status: 'failed' };
|
|
584
|
+
|
|
487
585
|
export interface DeepAnalysisMethods {
|
|
488
586
|
performDeepAnalysis(state: WebPageState, result: ResearchResult): Promise<void>;
|
|
489
587
|
researchOverlay(current: ActionResult, previous: ActionResult, pageStateHash: string): Promise<string | null>;
|
|
@@ -80,7 +80,7 @@ export function WithLocators<T extends Constructor>(Base: T) {
|
|
|
80
80
|
}
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
tag('
|
|
83
|
+
tag('operation').log(`Validated ${locators.length} locators: ${locators.length - broken} valid, ${broken} broken`);
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
async fixBrokenSections(result: ResearchResult, conversation: Conversation): Promise<void> {
|
package/src/ai/researcher.ts
CHANGED
|
@@ -151,7 +151,7 @@ export class Researcher extends ResearcherBase implements Agent {
|
|
|
151
151
|
if (!deep && !force) {
|
|
152
152
|
const similar = await findSimilarResearch(combinedHtml);
|
|
153
153
|
if (similar) {
|
|
154
|
-
tag('
|
|
154
|
+
tag('operation').log('Similar research found, reusing cached result');
|
|
155
155
|
if (stateHash) saveResearch(stateHash, similar, combinedHtml);
|
|
156
156
|
tag('multiline').log(formatResearchSummary(similar));
|
|
157
157
|
tag('success').log('Research complete (reused)');
|
|
@@ -316,10 +316,10 @@ export class Researcher extends ResearcherBase implements Agent {
|
|
|
316
316
|
|
|
317
317
|
tag('multiline').log(formatResearchSummary(result.text, { visionUsed: this.hasScreenshotToAnalyze }));
|
|
318
318
|
tag('success').log('Research complete');
|
|
319
|
-
if (researchFile) tag('
|
|
319
|
+
if (researchFile) tag('operation').log(`Research file saved to: ${researchFile}`);
|
|
320
320
|
if (this.actionResult?.screenshotFile) {
|
|
321
321
|
const screenshotPath = outputPath('states', this.actionResult.screenshotFile);
|
|
322
|
-
tag('
|
|
322
|
+
tag('operation').log(`UI screenshot: file://${screenshotPath}`);
|
|
323
323
|
}
|
|
324
324
|
|
|
325
325
|
await this.hooksRunner.runAfterHook('researcher', state.url);
|
|
@@ -467,7 +467,7 @@ export class Researcher extends ResearcherBase implements Agent {
|
|
|
467
467
|
.filter((k) => !!k)
|
|
468
468
|
.join('\n\n');
|
|
469
469
|
|
|
470
|
-
tag('
|
|
470
|
+
tag('operation').log(`Found ${knowledgeFiles.length} relevant knowledge ${pluralize(knowledgeFiles.length, 'file')} for: ${this.actionResult.url}`);
|
|
471
471
|
knowledge = `
|
|
472
472
|
<hint>
|
|
473
473
|
Here is relevant knowledge for this page:
|
|
@@ -41,6 +41,9 @@ export class SessionAnalyst implements Agent {
|
|
|
41
41
|
|
|
42
42
|
Crucial distinction: "the app misbehaved" vs "the automation could not interact with the app". ONLY the first is a Defect. If the automation gives up before the app responds — timeout, retries exhausted, dead loop / loop detected, could not click or find an element — that is an Execution issue regardless of what the log calls it. Failure inside the automation ≠ failure inside the product.
|
|
43
43
|
|
|
44
|
+
The action log is more authoritative than the scenario title. If the actual submitted data, page state, or action sequence does not match the scenario title, classify it as Execution issue and do not list that scenario under What works. Do NOT infer a product Defect or UX issue from behavior caused by incorrect test data or an automation mismatch.
|
|
45
|
+
Negative test data is valid when it matches a negative scenario. Do not call intentionally invalid input wrong data when the scenario expects rejection or validation feedback.
|
|
46
|
+
|
|
44
47
|
A solitary failure where adjacent tests on the same feature passed → Execution, not Defect.
|
|
45
48
|
|
|
46
49
|
## Severity (defects only)
|
|
@@ -76,7 +79,7 @@ export class SessionAnalyst implements Agent {
|
|
|
76
79
|
|
|
77
80
|
## Brevity rules
|
|
78
81
|
|
|
79
|
-
- Headline: 2 sentences MAX. About the FEATURE, not the run. No counts, no "N tests", no "this session".
|
|
82
|
+
- Headline: 2 sentences MAX. About the FEATURE, not the run. No counts, no "N tests", no "this session". Never use these words: "exercised", "comprehensive", "notably", "this session", "module", "targeted", "covered creation".
|
|
80
83
|
- What works: feature name + test refs. NO parentheticals, NO caveats. If there's a caveat, the entry doesn't belong here.
|
|
81
84
|
- Defect title is the BUG ("Search returns non-matching results"), never the scenario name.
|
|
82
85
|
- Reproduce steps are imperative one-liners drawn from the log.
|
package/src/ai/task-agent.ts
CHANGED
|
@@ -44,7 +44,7 @@ export abstract class TaskAgent {
|
|
|
44
44
|
.filter((k) => !!k)
|
|
45
45
|
.join('\n\n');
|
|
46
46
|
|
|
47
|
-
tag('
|
|
47
|
+
tag('operation').log(`Found ${knowledgeFiles.length} relevant knowledge ${pluralize(knowledgeFiles.length, 'file')}`);
|
|
48
48
|
return dedent`
|
|
49
49
|
<knowledge>
|
|
50
50
|
Here is relevant knowledge for this page:
|
|
@@ -61,7 +61,7 @@ export abstract class TaskAgent {
|
|
|
61
61
|
|
|
62
62
|
const totalSections = toc.reduce((sum, entry) => sum + entry.sections.length, 0);
|
|
63
63
|
debugLog(`injecting experience TOC (${toc.length} files, ${totalSections} sections)`);
|
|
64
|
-
tag('
|
|
64
|
+
tag('operation').log(`Found ${toc.length} experience ${pluralize(toc.length, 'file')} (${totalSections} sections)`);
|
|
65
65
|
return renderExperienceToc(toc);
|
|
66
66
|
}
|
|
67
67
|
|
|
@@ -17,6 +17,6 @@ export class ContextAriaCommand extends BaseCommand {
|
|
|
17
17
|
throw new Error('No ARIA snapshot available for current page');
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
tag('multiline').log(`ARIA Snapshot:\n\n${ariaSnapshot}
|
|
20
|
+
tag('multiline').log(`ARIA Snapshot:\n\n${ariaSnapshot}`, { maxLines: 10 });
|
|
21
21
|
}
|
|
22
22
|
}
|