explorbot 0.1.9 → 0.1.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -1
- package/bin/explorbot-cli.ts +86 -15
- package/boat/api-tester/src/ai/curler-tools.ts +3 -3
- package/boat/api-tester/src/ai/curler.ts +1 -1
- package/boat/api-tester/src/apibot.ts +2 -2
- package/boat/api-tester/src/config.ts +1 -1
- package/dist/bin/explorbot-cli.js +85 -14
- package/dist/boat/api-tester/src/ai/curler-tools.js +2 -2
- package/dist/boat/api-tester/src/apibot.js +2 -2
- package/dist/package.json +2 -2
- package/dist/rules/navigator/output.md +9 -0
- package/dist/rules/navigator/verification-actions.md +2 -0
- package/dist/src/action-result.js +23 -1
- package/dist/src/action.js +46 -38
- package/dist/src/ai/bosun.js +16 -2
- package/dist/src/ai/conversation.js +39 -0
- package/dist/src/ai/experience-compactor.js +235 -50
- package/dist/src/ai/historian/codeceptjs.js +109 -0
- package/dist/src/ai/historian/experience.js +320 -0
- package/dist/src/ai/historian/mixin.js +2 -0
- package/dist/src/ai/historian/playwright.js +145 -0
- package/dist/src/ai/historian/utils.js +18 -0
- package/dist/src/ai/historian.js +19 -398
- package/dist/src/ai/navigator.js +133 -80
- package/dist/src/ai/pilot.js +254 -13
- package/dist/src/ai/planner/subpages.js +1 -30
- package/dist/src/ai/planner.js +33 -13
- package/dist/src/ai/provider.js +55 -18
- package/dist/src/ai/rerunner.js +3 -3
- package/dist/src/ai/researcher/deep-analysis.js +1 -1
- package/dist/src/ai/researcher/fingerprint-worker.js +1 -1
- package/dist/src/ai/researcher/locators.js +1 -1
- package/dist/src/ai/researcher/sections.js +8 -1
- package/dist/src/ai/researcher.js +43 -41
- package/dist/src/ai/rules.js +26 -14
- package/dist/src/ai/tester.js +90 -26
- package/dist/src/ai/tools.js +18 -10
- package/dist/src/api/request-store.js +20 -0
- package/dist/src/api/xhr-capture.js +19 -3
- package/dist/src/browser-server.js +16 -3
- package/dist/src/command-handler.js +1 -1
- package/dist/src/commands/add-rule-command.js +12 -9
- package/dist/src/commands/base-command.js +20 -0
- package/dist/src/commands/clean-command.js +3 -2
- package/dist/src/commands/compact-command.js +138 -0
- package/dist/src/commands/context-command.js +7 -1
- package/dist/src/commands/drill-command.js +4 -1
- package/dist/src/commands/experience-command.js +104 -0
- package/dist/src/commands/explore-command.js +54 -19
- package/dist/src/commands/freesail-command.js +2 -0
- package/dist/src/commands/index.js +7 -3
- package/dist/src/commands/init-command.js +11 -10
- package/dist/src/commands/learn-command.js +1 -1
- package/dist/src/commands/navigate-command.js +4 -1
- package/dist/src/commands/plan-clear-command.js +4 -1
- package/dist/src/commands/plan-command.js +43 -4
- package/dist/src/commands/plan-edit-command.js +1 -1
- package/dist/src/commands/plan-load-command.js +4 -1
- package/dist/src/commands/plan-reload-command.js +4 -1
- package/dist/src/commands/plan-save-command.js +20 -8
- package/dist/src/commands/rerun-command.js +4 -0
- package/dist/src/commands/research-command.js +5 -2
- package/dist/src/commands/start-command.js +5 -1
- package/dist/src/commands/test-command.js +7 -1
- package/dist/src/components/App.js +15 -5
- package/dist/src/execution-controller.js +13 -2
- package/dist/src/experience-tracker.js +174 -83
- package/dist/src/explorbot.js +31 -22
- package/dist/src/explorer.js +12 -5
- package/dist/src/observability.js +50 -99
- package/dist/src/playwright-recorder.js +309 -0
- package/dist/src/reporter.js +17 -2
- package/dist/src/stats.js +2 -0
- package/dist/src/suite.js +1 -1
- package/dist/src/test-plan.js +12 -0
- package/dist/src/utils/aria.js +37 -1
- package/dist/src/utils/error-page.js +30 -7
- package/dist/src/utils/logger.js +1 -1
- package/dist/src/utils/next-steps.js +37 -0
- package/dist/src/utils/rules-loader.js +1 -1
- package/dist/src/utils/test-files.js +1 -1
- package/dist/src/utils/url-matcher.js +50 -0
- package/package.json +2 -2
- package/rules/navigator/output.md +9 -0
- package/rules/navigator/verification-actions.md +2 -0
- package/src/action-result.ts +26 -1
- package/src/action.ts +44 -37
- package/src/ai/bosun.ts +16 -2
- package/src/ai/conversation.ts +37 -0
- package/src/ai/experience-compactor.ts +270 -63
- package/src/ai/historian/codeceptjs.ts +130 -0
- package/src/ai/historian/experience.ts +383 -0
- package/src/ai/historian/mixin.ts +4 -0
- package/src/ai/historian/playwright.ts +169 -0
- package/src/ai/historian/utils.ts +23 -0
- package/src/ai/historian.ts +35 -468
- package/src/ai/navigator.ts +140 -85
- package/src/ai/pilot.ts +259 -14
- package/src/ai/planner/subpages.ts +1 -24
- package/src/ai/planner.ts +34 -14
- package/src/ai/provider.ts +52 -18
- package/src/ai/rerunner.ts +3 -3
- package/src/ai/researcher/deep-analysis.ts +1 -1
- package/src/ai/researcher/fingerprint-worker.ts +1 -1
- package/src/ai/researcher/locators.ts +2 -2
- package/src/ai/researcher/sections.ts +7 -1
- package/src/ai/researcher.ts +47 -42
- package/src/ai/rules.ts +27 -14
- package/src/ai/task-agent.ts +1 -1
- package/src/ai/tester.ts +94 -26
- package/src/ai/tools.ts +53 -29
- package/src/api/request-store.ts +22 -0
- package/src/api/xhr-capture.ts +21 -3
- package/src/browser-server.ts +17 -3
- package/src/command-handler.ts +1 -1
- package/src/commands/add-rule-command.ts +13 -9
- package/src/commands/base-command.ts +26 -1
- package/src/commands/clean-command.ts +4 -3
- package/src/commands/compact-command.ts +156 -0
- package/src/commands/context-command.ts +8 -2
- package/src/commands/drill-command.ts +5 -2
- package/src/commands/experience-command.ts +125 -0
- package/src/commands/explore-command.ts +58 -21
- package/src/commands/freesail-command.ts +2 -0
- package/src/commands/index.ts +7 -3
- package/src/commands/init-command.ts +11 -10
- package/src/commands/learn-command.ts +2 -2
- package/src/commands/navigate-command.ts +5 -2
- package/src/commands/plan-clear-command.ts +5 -2
- package/src/commands/plan-command.ts +47 -5
- package/src/commands/plan-edit-command.ts +2 -2
- package/src/commands/plan-load-command.ts +5 -2
- package/src/commands/plan-reload-command.ts +5 -2
- package/src/commands/plan-save-command.ts +20 -9
- package/src/commands/rerun-command.ts +5 -0
- package/src/commands/research-command.ts +6 -3
- package/src/commands/start-command.ts +6 -2
- package/src/commands/test-command.ts +8 -2
- package/src/components/App.tsx +16 -5
- package/src/config.ts +6 -1
- package/src/execution-controller.ts +14 -3
- package/src/experience-tracker.ts +198 -100
- package/src/explorbot.ts +33 -23
- package/src/explorer.ts +14 -5
- package/src/observability.ts +50 -109
- package/src/playwright-recorder.ts +305 -0
- package/src/reporter.ts +17 -3
- package/src/stats.ts +4 -0
- package/src/suite.ts +1 -1
- package/src/test-plan.ts +12 -0
- package/src/utils/aria.ts +38 -1
- package/src/utils/error-page.ts +32 -7
- package/src/utils/logger.ts +1 -1
- package/src/utils/next-steps.ts +51 -0
- package/src/utils/rules-loader.ts +1 -1
- package/src/utils/test-files.ts +1 -1
- package/src/utils/url-matcher.ts +43 -0
package/dist/src/ai/pilot.js
CHANGED
|
@@ -6,6 +6,7 @@ import { ConfigParser } from "../config.js";
|
|
|
6
6
|
import { renderExperienceToc } from "../experience-tracker.js";
|
|
7
7
|
import { TestResult } from "../test-plan.js";
|
|
8
8
|
import { collectInteractiveNodes, detectFocusArea, extractFocusedElement } from "../utils/aria.js";
|
|
9
|
+
import { ErrorPageError } from "../utils/error-page.js";
|
|
9
10
|
import { createDebug, tag } from "../utils/logger.js";
|
|
10
11
|
const debugLog = createDebug('explorbot:pilot');
|
|
11
12
|
import { truncateJson } from "../utils/strings.js";
|
|
@@ -42,22 +43,27 @@ export class Pilot {
|
|
|
42
43
|
return null;
|
|
43
44
|
return this.conversation.getLastMessage() || null;
|
|
44
45
|
}
|
|
45
|
-
async reviewStop(task, currentState, testerConversation) {
|
|
46
|
-
return this.reviewDecision('stop', task, currentState, testerConversation);
|
|
46
|
+
async reviewStop(task, currentState, testerConversation, navigator) {
|
|
47
|
+
return this.reviewDecision('stop', task, currentState, testerConversation, navigator);
|
|
47
48
|
}
|
|
48
|
-
async reviewFinish(task, currentState, testerConversation) {
|
|
49
|
-
return this.reviewDecision('finish', task, currentState, testerConversation);
|
|
49
|
+
async reviewFinish(task, currentState, testerConversation, navigator) {
|
|
50
|
+
return this.reviewDecision('finish', task, currentState, testerConversation, navigator);
|
|
50
51
|
}
|
|
51
|
-
async reviewCompletion(task, currentState, testerConversation) {
|
|
52
|
+
async reviewCompletion(task, currentState, testerConversation, navigator) {
|
|
52
53
|
const verdictType = task.hasAchievedAny() ? 'finish' : 'stop';
|
|
53
|
-
return this.reviewDecision(verdictType, task, currentState, testerConversation);
|
|
54
|
+
return this.reviewDecision(verdictType, task, currentState, testerConversation, navigator);
|
|
54
55
|
}
|
|
55
|
-
async finalReview(task, currentState, testerConversation) {
|
|
56
|
+
async finalReview(task, currentState, testerConversation, navigator) {
|
|
56
57
|
if (task.hasFinished)
|
|
57
58
|
return false;
|
|
58
|
-
return this.reviewCompletion(task, currentState, testerConversation);
|
|
59
|
+
return this.reviewCompletion(task, currentState, testerConversation, navigator);
|
|
59
60
|
}
|
|
60
|
-
async
|
|
61
|
+
async reviewReset(task, currentState, reason, testerConversation) {
|
|
62
|
+
return this.reviewResetDecision(task, currentState, reason, testerConversation);
|
|
63
|
+
}
|
|
64
|
+
async reviewDecision(type, task, currentState, testerConversation, navigator) {
|
|
65
|
+
if (task.hasFinished)
|
|
66
|
+
return false;
|
|
61
67
|
tag('substep').log(`Pilot reviewing ${type} verdict...`);
|
|
62
68
|
const sessionLog = this.formatSessionLog(testerConversation);
|
|
63
69
|
const stateContext = this.buildStateContext(currentState);
|
|
@@ -79,6 +85,10 @@ export class Pilot {
|
|
|
79
85
|
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'),
|
|
80
86
|
reason: z.string().describe('What happened and why (1-2 sentences). Do NOT repeat the decision status (e.g. "scenario goal achieved/not achieved") — just explain the evidence. For continue: explain why rejected and suggest alternatives.'),
|
|
81
87
|
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.'),
|
|
88
|
+
requestVerification: z
|
|
89
|
+
.string()
|
|
90
|
+
.nullable()
|
|
91
|
+
.describe('REQUIRED whenever decision is "pass" — provide a specific assertion that proves the scenario goal on the current page (e.g., "New test suite \\"Foo\\" is visible in the suites list"). The system runs it and bakes the resulting assertion into the generated test file; without it the test file has no verifiable expect(). Also use when evidence is insufficient before deciding pass/fail. Leave null for "continue", "fail", or "skipped".'),
|
|
82
92
|
});
|
|
83
93
|
const userContent = dedent `
|
|
84
94
|
Tester wants to ${type} the test.
|
|
@@ -106,6 +116,12 @@ export class Pilot {
|
|
|
106
116
|
- "continue" if tester hasn't completed the scenario goal yet — even if milestones were checked
|
|
107
117
|
- If evidence is mixed, but final state indicates goal completion, choose "pass"
|
|
108
118
|
- If evidence is mixed and final state is unclear, prefer "continue" over "fail"
|
|
119
|
+
|
|
120
|
+
When deciding "pass", you MUST also set requestVerification to a CodeceptJS assertion that
|
|
121
|
+
proves the scenario goal on the current page. Choose the strongest single evidence (a unique
|
|
122
|
+
element/text that exists ONLY because the scenario succeeded). The assertion is executed and
|
|
123
|
+
then converted into the spec file's expect() — without it the generated test has nothing to
|
|
124
|
+
assert and is worthless.
|
|
109
125
|
`;
|
|
110
126
|
const messages = [
|
|
111
127
|
{
|
|
@@ -124,6 +140,30 @@ export class Pilot {
|
|
|
124
140
|
task.finish(TestResult.FAILED);
|
|
125
141
|
return false;
|
|
126
142
|
}
|
|
143
|
+
if (result.requestVerification && navigator) {
|
|
144
|
+
tag('substep').log(`Pilot requesting verification: ${result.requestVerification}`);
|
|
145
|
+
try {
|
|
146
|
+
const verifyResult = await navigator.verifyState(result.requestVerification, currentState);
|
|
147
|
+
if (verifyResult.verified) {
|
|
148
|
+
if (verifyResult.assertionSteps?.length) {
|
|
149
|
+
this.explorer.getPlaywrightRecorder().recordVerification(verifyResult.assertionSteps);
|
|
150
|
+
}
|
|
151
|
+
tag('substep').log(`Pilot verified: ${result.requestVerification}`);
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
tag('substep').log(`Pilot verification failed: ${result.requestVerification}`);
|
|
155
|
+
if (result.decision === 'pass') {
|
|
156
|
+
const flipMessage = `Verification "${result.requestVerification}" did not match the page. Adjust approach and re-verify before finishing.`;
|
|
157
|
+
result.decision = 'continue';
|
|
158
|
+
result.reason = flipMessage;
|
|
159
|
+
result.guidance = result.guidance ?? flipMessage;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
catch (verifyErr) {
|
|
164
|
+
tag('warning').log(`Pilot verification errored: ${verifyErr.message}`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
127
167
|
tag('info').log(`Pilot: ${result.decision} — ${result.reason}`);
|
|
128
168
|
task.summary = result.reason;
|
|
129
169
|
if (result.decision === 'pass') {
|
|
@@ -152,6 +192,131 @@ export class Pilot {
|
|
|
152
192
|
return false;
|
|
153
193
|
}
|
|
154
194
|
}
|
|
195
|
+
async reviewResetDecision(task, currentState, reason, testerConversation) {
|
|
196
|
+
if (task.hasFinished)
|
|
197
|
+
return false;
|
|
198
|
+
tag('substep').log(`Pilot reviewing reset (count=${task.resetCount})...`);
|
|
199
|
+
const sessionLog = this.formatSessionLog(testerConversation);
|
|
200
|
+
const stateContext = this.buildStateContext(currentState);
|
|
201
|
+
const notes = task.notesToString() || 'No notes recorded.';
|
|
202
|
+
const schema = z.object({
|
|
203
|
+
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'),
|
|
204
|
+
reason: z.string().describe('What evidence justifies this decision (1-2 sentences). Do not restate the decision.'),
|
|
205
|
+
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).'),
|
|
206
|
+
});
|
|
207
|
+
const userContent = dedent `
|
|
208
|
+
Tester requested reset. Previous reset count: ${task.resetCount - 1}.
|
|
209
|
+
|
|
210
|
+
Reason given by tester: ${reason || '(none)'}
|
|
211
|
+
|
|
212
|
+
<state>
|
|
213
|
+
${stateContext}
|
|
214
|
+
</state>
|
|
215
|
+
|
|
216
|
+
${this.formatExpectations(task)}
|
|
217
|
+
|
|
218
|
+
<notes>
|
|
219
|
+
${notes}
|
|
220
|
+
</notes>
|
|
221
|
+
|
|
222
|
+
<session_log>
|
|
223
|
+
${sessionLog || 'No actions recorded'}
|
|
224
|
+
</session_log>
|
|
225
|
+
|
|
226
|
+
Decide:
|
|
227
|
+
- "allow" — the reset is legitimate (navigation dead-end, wrong page, irrecoverable error on current page).
|
|
228
|
+
- "continue" — veto the reset; something on the current page can still be used to progress or verify. Provide guidance.
|
|
229
|
+
- "fail" — reset-looping: tester has already reset and the underlying obstacle will not change. Stop the test as failed.
|
|
230
|
+
- "skipped" — the scenario is inapplicable to this application or cannot be executed here.
|
|
231
|
+
`;
|
|
232
|
+
const messages = [
|
|
233
|
+
{
|
|
234
|
+
role: 'system',
|
|
235
|
+
content: this.buildResetSystemPrompt(task),
|
|
236
|
+
},
|
|
237
|
+
{ role: 'user', content: userContent },
|
|
238
|
+
];
|
|
239
|
+
try {
|
|
240
|
+
const response = await this.provider.generateObject(messages, schema, this.provider.getAgenticModel('pilot'), {
|
|
241
|
+
agentName: 'pilot',
|
|
242
|
+
experimental_telemetry: { functionId: 'pilot.reviewReset' },
|
|
243
|
+
});
|
|
244
|
+
const result = response?.object;
|
|
245
|
+
if (!result) {
|
|
246
|
+
return true;
|
|
247
|
+
}
|
|
248
|
+
tag('info').log(`Pilot reset verdict: ${result.decision} — ${result.reason}`);
|
|
249
|
+
if (result.decision === 'allow') {
|
|
250
|
+
tag('substep').log(`Pilot allowed reset: ${result.reason}`);
|
|
251
|
+
return true;
|
|
252
|
+
}
|
|
253
|
+
if (result.decision === 'fail') {
|
|
254
|
+
task.addNote(`Pilot: reset refused — ${result.reason}`, TestResult.FAILED);
|
|
255
|
+
task.finish(TestResult.FAILED);
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
if (result.decision === 'skipped') {
|
|
259
|
+
task.addNote(`Pilot: skipped — ${result.reason}`, TestResult.SKIPPED);
|
|
260
|
+
task.finish(TestResult.SKIPPED);
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
tag('substep').log(`Pilot vetoed reset: ${result.reason}`);
|
|
264
|
+
const guidanceText = result.guidance ? `\n\nWhat to do instead: ${result.guidance}` : '';
|
|
265
|
+
testerConversation.addUserText(`Pilot vetoed reset: ${result.reason}${guidanceText}`);
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
catch (error) {
|
|
269
|
+
tag('warning').log(`Pilot reset review failed: ${error.message}`);
|
|
270
|
+
return true;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
buildResetSystemPrompt(task) {
|
|
274
|
+
return dedent `
|
|
275
|
+
You are Pilot — the supervisor that decides whether a reset is legitimate.
|
|
276
|
+
Tester wants to reset (navigate back to the start URL and discard progress).
|
|
277
|
+
|
|
278
|
+
SCENARIO: ${task.scenario}
|
|
279
|
+
|
|
280
|
+
Reset is DESTRUCTIVE. It abandons all work done in this iteration. In stateful apps, any
|
|
281
|
+
side effects (records created, forms submitted) persist on the server — resetting does not
|
|
282
|
+
undo them. Unnecessary resets create duplicate data and loop forever.
|
|
283
|
+
|
|
284
|
+
LEGITIMATE RESET (decide "allow"):
|
|
285
|
+
- The current page is unrelated to the scenario and no path leads back.
|
|
286
|
+
- Navigation is stuck in an error state with no recoverable action.
|
|
287
|
+
- The tester arrived on a page that cannot host the scenario at all.
|
|
288
|
+
|
|
289
|
+
ILLEGITIMATE RESET (decide "continue"):
|
|
290
|
+
- The previous action already succeeded (URL changed to a success/detail page, record visible,
|
|
291
|
+
confirmation shown) and tester wants to redo it because an assertion did not match.
|
|
292
|
+
The work is done — verify, record, or finish instead of restarting.
|
|
293
|
+
- A single expectation / milestone does not match app reality but the scenario goal may still
|
|
294
|
+
have been achieved. Do not redo — instruct the tester to verify the actual outcome.
|
|
295
|
+
- Tester wants to "try again with different input" after a form was submitted. Submitting
|
|
296
|
+
again creates a duplicate; guide toward editing the existing record or accepting the state.
|
|
297
|
+
|
|
298
|
+
RESET-LOOP (decide "fail"):
|
|
299
|
+
- resetCount >= 2 and the previous resets did not change the underlying situation.
|
|
300
|
+
- The same flow has been attempted twice with the same failure mode.
|
|
301
|
+
- Repeating the reset cannot produce new information.
|
|
302
|
+
|
|
303
|
+
SCENARIO INAPPLICABLE (decide "skipped"):
|
|
304
|
+
- The feature the scenario targets does not exist on this app, or prerequisites cannot be met.
|
|
305
|
+
|
|
306
|
+
PRIORITY:
|
|
307
|
+
1) Evidence of successful side effects in session_log (URL transition, new record visible).
|
|
308
|
+
If present, almost never allow the reset — the work is done.
|
|
309
|
+
2) resetCount. Each prior reset raises the bar for allowing another.
|
|
310
|
+
3) Tester's stated reason. Weigh it against the observed evidence, do not trust it blindly.
|
|
311
|
+
|
|
312
|
+
GUIDANCE FIELD (required when decision is "continue"):
|
|
313
|
+
Give a specific next action on the current page: which tool to call, what to verify, or how to
|
|
314
|
+
record the outcome. Do not suggest repeating actions that already succeeded.
|
|
315
|
+
|
|
316
|
+
EXPECTED RESULTS (milestones, not the goal):
|
|
317
|
+
${task.expected.map((e) => `- ${e}`).join('\n')}
|
|
318
|
+
`;
|
|
319
|
+
}
|
|
155
320
|
buildVerdictSystemPrompt(type, task) {
|
|
156
321
|
return dedent `
|
|
157
322
|
You are Pilot — the final decision maker for test pass/fail.
|
|
@@ -248,8 +413,12 @@ export class Pilot {
|
|
|
248
413
|
the elements needed for the scenario. The page summary does not list every element.
|
|
249
414
|
Prefer interacting with the current page over navigating away.
|
|
250
415
|
|
|
416
|
+
If you load a recipe via learn_experience, do NOT rewrite its code in your plan — the
|
|
417
|
+
raw recipe is forwarded to Tester automatically. Reference it by step ("apply recipe
|
|
418
|
+
steps 1–3, then…") and call out anywhere your scenario diverges from it.
|
|
419
|
+
|
|
251
420
|
Be concise and specific. Tester will follow your plan.
|
|
252
|
-
`, 'pilot.planTest', { tools: true, maxToolRoundtrips: 3, task });
|
|
421
|
+
`, 'pilot.planTest', { tools: true, planningOnly: true, maxToolRoundtrips: 3, task });
|
|
253
422
|
}
|
|
254
423
|
async reviewNewPage(task, currentState) {
|
|
255
424
|
if (!this.conversation)
|
|
@@ -329,7 +498,10 @@ export class Pilot {
|
|
|
329
498
|
}
|
|
330
499
|
}
|
|
331
500
|
this.conversation.addUserText(finalUserText);
|
|
332
|
-
let tools
|
|
501
|
+
let tools;
|
|
502
|
+
if (opts.tools) {
|
|
503
|
+
tools = opts.planningOnly ? this.pickPlanningTools() : this.agentTools;
|
|
504
|
+
}
|
|
333
505
|
if (opts.tools && opts.task) {
|
|
334
506
|
tools = { ...tools, ...this.buildPreconditionTool(opts.task) };
|
|
335
507
|
}
|
|
@@ -338,7 +510,20 @@ export class Pilot {
|
|
|
338
510
|
agentName: 'pilot',
|
|
339
511
|
experimental_telemetry: { functionId },
|
|
340
512
|
});
|
|
341
|
-
|
|
513
|
+
const text = result?.response?.text || '';
|
|
514
|
+
const learned = (result?.toolExecutions || []).filter((e) => e.toolName === 'learn_experience' && e.output?.content).map((e) => e.output.content);
|
|
515
|
+
if (learned.length === 0)
|
|
516
|
+
return text;
|
|
517
|
+
return dedent `
|
|
518
|
+
${text}
|
|
519
|
+
|
|
520
|
+
<applied_experience>
|
|
521
|
+
Recipes from prior successful runs that Pilot judged relevant. Locators worked then; the page may have changed since.
|
|
522
|
+
Treat code blocks below as a starting hypothesis. If a locator misses, fall back to ARIA/UI-map.
|
|
523
|
+
|
|
524
|
+
${learned.join('\n\n')}
|
|
525
|
+
</applied_experience>
|
|
526
|
+
`;
|
|
342
527
|
}
|
|
343
528
|
getExperienceToc() {
|
|
344
529
|
if (!this.experienceTracker)
|
|
@@ -350,6 +535,25 @@ export class Pilot {
|
|
|
350
535
|
const toc = this.experienceTracker.getExperienceTableOfContents(actionResult);
|
|
351
536
|
return renderExperienceToc(toc);
|
|
352
537
|
}
|
|
538
|
+
pickPlanningTools() {
|
|
539
|
+
const { see, context, verify, research, getVisitedStates, xpathCheck, learn_experience } = this.agentTools ?? {};
|
|
540
|
+
const planning = {};
|
|
541
|
+
if (see)
|
|
542
|
+
planning.see = see;
|
|
543
|
+
if (context)
|
|
544
|
+
planning.context = context;
|
|
545
|
+
if (verify)
|
|
546
|
+
planning.verify = verify;
|
|
547
|
+
if (research)
|
|
548
|
+
planning.research = research;
|
|
549
|
+
if (getVisitedStates)
|
|
550
|
+
planning.getVisitedStates = getVisitedStates;
|
|
551
|
+
if (xpathCheck)
|
|
552
|
+
planning.xpathCheck = xpathCheck;
|
|
553
|
+
if (learn_experience)
|
|
554
|
+
planning.learn_experience = learn_experience;
|
|
555
|
+
return planning;
|
|
556
|
+
}
|
|
353
557
|
buildPreconditionTool(task) {
|
|
354
558
|
return {
|
|
355
559
|
precondition: tool({
|
|
@@ -418,6 +622,28 @@ export class Pilot {
|
|
|
418
622
|
const verifyLines = verifications.map(([a, v]) => `${v ? 'PASS' : 'FAIL'}: ${a}`);
|
|
419
623
|
lines.push(`verifications: ${verifyLines.join(', ')}`);
|
|
420
624
|
}
|
|
625
|
+
const consoleErrors = (state.browserLogs ?? []).filter((l) => (l.type || l.level) === 'error');
|
|
626
|
+
if (consoleErrors.length > 0) {
|
|
627
|
+
const sample = consoleErrors
|
|
628
|
+
.slice(0, 3)
|
|
629
|
+
.map((e) => e.text || e.message || String(e))
|
|
630
|
+
.join(' | ');
|
|
631
|
+
lines.push(`console errors: ${consoleErrors.length} (${sample})`);
|
|
632
|
+
}
|
|
633
|
+
else {
|
|
634
|
+
lines.push('console errors: none');
|
|
635
|
+
}
|
|
636
|
+
const failedRequests = this.explorer.getRequestStore()?.getFailedRequests() ?? [];
|
|
637
|
+
if (failedRequests.length > 0) {
|
|
638
|
+
const sample = failedRequests
|
|
639
|
+
.slice(-5)
|
|
640
|
+
.map((r) => `${r.method} ${r.path} → ${r.status}`)
|
|
641
|
+
.join(', ');
|
|
642
|
+
lines.push(`network errors: ${sample}`);
|
|
643
|
+
}
|
|
644
|
+
else {
|
|
645
|
+
lines.push('network errors: none');
|
|
646
|
+
}
|
|
421
647
|
const interactiveNodes = collectInteractiveNodes(state.ariaSnapshot);
|
|
422
648
|
const disabledButtons = interactiveNodes.filter((n) => n.role === 'button' && n.disabled === true && n.name).map((n) => n.name);
|
|
423
649
|
lines.push(`disabled buttons: ${disabledButtons.length > 0 ? disabledButtons.join(', ') : 'none'}`);
|
|
@@ -461,7 +687,15 @@ export class Pilot {
|
|
|
461
687
|
}
|
|
462
688
|
}
|
|
463
689
|
if (text.includes('ATTACH_UI_MAP')) {
|
|
464
|
-
|
|
690
|
+
let uiMap = '';
|
|
691
|
+
try {
|
|
692
|
+
uiMap = await this.researcher.research(currentState);
|
|
693
|
+
}
|
|
694
|
+
catch (err) {
|
|
695
|
+
if (!(err instanceof ErrorPageError))
|
|
696
|
+
throw err;
|
|
697
|
+
tag('warning').log(`Pilot UI map skipped: ${err.message}`);
|
|
698
|
+
}
|
|
465
699
|
if (uiMap) {
|
|
466
700
|
parts.push(dedent `
|
|
467
701
|
<page_ui_map>
|
|
@@ -613,6 +847,13 @@ export class Pilot {
|
|
|
613
847
|
- If the goal was achieved by a previous action (SUCCESS in recent_actions with confirming ariaDiff): instruct Tester to verify() the result and finish(). Do NOT repeat the same action.
|
|
614
848
|
- If Tester keeps re-opening the same panel and re-submitting the same data — STOP. The action was already completed.
|
|
615
849
|
|
|
850
|
+
Action-goal alignment — classify every recent successful action:
|
|
851
|
+
- GOAL-ADVANCING: creates, edits, removes, submits, or verifies the scenario's subject data (the object the scenario actually changes).
|
|
852
|
+
- VIEW-ONLY: toggles layout, filters, tabs, segment controls, sort orders, collapse/expand — changes which data is shown without modifying it.
|
|
853
|
+
- A single VIEW-ONLY action is legitimate when needed to reveal a target element for the next GOAL-ADVANCING action.
|
|
854
|
+
- A run of two or more consecutive successful VIEW-ONLY actions with no interleaved GOAL-ADVANCING action is thrashing — Tester is exploring UI instead of executing the scenario. Redirect Tester to the specific mutation or verification the scenario requires.
|
|
855
|
+
- VIEW-ONLY actions also tend to produce large page diffs with many htmlParts; if you see that pattern repeatedly in recent_actions, treat it as evidence of thrashing.
|
|
856
|
+
|
|
616
857
|
Navigation awareness — always compare current page url to START URL:
|
|
617
858
|
- subpage navigation (deeper path from START URL) — OK, scenario may need sub-pages
|
|
618
859
|
- outer-page navigation (parent/sibling path from START URL) — SUSPICIOUS. The scenario target is on the START page. Do NOT rationalize leaving it. Instruct Tester to back() or reset().
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import dedent from 'dedent';
|
|
2
2
|
import { z } from 'zod';
|
|
3
|
-
import { ConfigParser } from "../../config.js";
|
|
4
3
|
import { normalizeUrl } from "../../state-manager.js";
|
|
4
|
+
import { isDynamicSegment } from "../../utils/url-matcher.js";
|
|
5
5
|
const planRegistry = new Map();
|
|
6
6
|
export function registerPlan(url, plan, feature, stateHash) {
|
|
7
7
|
const key = buildKey(url, feature);
|
|
@@ -27,35 +27,6 @@ function buildKey(url, feature) {
|
|
|
27
27
|
return `${normalized}::${feature}`;
|
|
28
28
|
return normalized;
|
|
29
29
|
}
|
|
30
|
-
export function isDynamicSegment(segment) {
|
|
31
|
-
try {
|
|
32
|
-
const configRegex = ConfigParser.getInstance().getConfig().dynamicPageRegex;
|
|
33
|
-
if (configRegex)
|
|
34
|
-
return new RegExp(configRegex, 'i').test(segment);
|
|
35
|
-
}
|
|
36
|
-
catch {
|
|
37
|
-
/* config not loaded yet */
|
|
38
|
-
}
|
|
39
|
-
// numeric: /users/123
|
|
40
|
-
if (/^\d+$/.test(segment))
|
|
41
|
-
return true;
|
|
42
|
-
// UUID: /items/550e8400-e29b-41d4-a716-446655440000
|
|
43
|
-
if (/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i.test(segment))
|
|
44
|
-
return true;
|
|
45
|
-
// ULID: /items/01ARZ3NDEKTSV4RRFFQ69G5FAV
|
|
46
|
-
if (/^[0-9A-HJKMNP-TV-Z]{26}$/.test(segment))
|
|
47
|
-
return true;
|
|
48
|
-
// hex ID (4+ chars): /suite/70dae98a
|
|
49
|
-
if (/^[a-f0-9]{4,}$/i.test(segment))
|
|
50
|
-
return true;
|
|
51
|
-
// hex-prefixed slug (8+ hex before dash): /suite/95ef0c94-mobile
|
|
52
|
-
if (/^[a-f0-9]{8,}-/i.test(segment))
|
|
53
|
-
return true;
|
|
54
|
-
// short mixed alphanumeric (digits + letters, ≤8 chars, no dash): /item/x7f2
|
|
55
|
-
if (segment.length <= 8 && !segment.includes('-') && /\d/.test(segment) && /[a-z]/i.test(segment))
|
|
56
|
-
return true;
|
|
57
|
-
return false;
|
|
58
|
-
}
|
|
59
30
|
export function isTemplateMatch(urlA, urlB) {
|
|
60
31
|
const partsA = normalizeUrl(urlA).split('/');
|
|
61
32
|
const partsB = normalizeUrl(urlB).split('/');
|
package/dist/src/ai/planner.js
CHANGED
|
@@ -6,19 +6,19 @@ import { ConfigParser } from "../config.js";
|
|
|
6
6
|
import { ExperienceTracker } from "../experience-tracker.js";
|
|
7
7
|
import { Observability } from "../observability.js";
|
|
8
8
|
import { Stats } from "../stats.js";
|
|
9
|
+
import { Suite } from "../suite.js";
|
|
9
10
|
import { Plan, Test } from "../test-plan.js";
|
|
10
|
-
import { planToCompactAiContext } from "../utils/test-plan-markdown.js";
|
|
11
11
|
import { createDebug, tag } from '../utils/logger.js';
|
|
12
12
|
import { jsonToTable } from "../utils/markdown-parser.js";
|
|
13
13
|
import { mdq } from '../utils/markdown-query.js';
|
|
14
|
+
import { planToCompactAiContext } from "../utils/test-plan-markdown.js";
|
|
14
15
|
import { Conversation } from "./conversation.js";
|
|
15
|
-
import { getActiveStyle, getStyles } from "./planner/styles.js";
|
|
16
16
|
import { WithSessionDedup } from "./planner/session-dedup.js";
|
|
17
|
+
import { getActiveStyle, getStyles } from "./planner/styles.js";
|
|
17
18
|
import { WithSubPages, getPlannedByStateHash, getRegisteredPlan, registerPlan } from "./planner/subpages.js";
|
|
19
|
+
import { POSSIBLE_SECTIONS, Researcher } from "./researcher.js";
|
|
18
20
|
import { findSimilarStateHash } from "./researcher/cache.js";
|
|
19
21
|
import { hasFocusedSection } from "./researcher/focus.js";
|
|
20
|
-
import { POSSIBLE_SECTIONS, Researcher } from "./researcher.js";
|
|
21
|
-
import { Suite } from "../suite.js";
|
|
22
22
|
import { fileUploadRule, protectionRule } from "./rules.js";
|
|
23
23
|
const debugLog = createDebug('explorbot:planner');
|
|
24
24
|
const TasksSchema = z.object({
|
|
@@ -400,6 +400,34 @@ export class Planner extends PlannerBase {
|
|
|
400
400
|
const allTests = this.currentPlan.getAllTests();
|
|
401
401
|
const titleListing = allTests.map((t) => `- "${t.scenario}" [${t.result || 'pending'}]`).join('\n');
|
|
402
402
|
const compactContext = planToCompactAiContext(this.currentPlan);
|
|
403
|
+
let planningStrategy;
|
|
404
|
+
if (feature) {
|
|
405
|
+
planningStrategy = dedent `
|
|
406
|
+
<planning_strategy>
|
|
407
|
+
Stay strictly inside the "${feature}" feature area. Do NOT switch to a different, unrelated feature even if it has no coverage.
|
|
408
|
+
Propose ${this.MIN_TASKS}-${this.MAX_TASKS} additional scenarios for "${feature}" that are not already in the tested list.
|
|
409
|
+
Use the <approach> above to decide which new angles to explore — different controls, inputs, states, outcome categories, or combinations — all within "${feature}".
|
|
410
|
+
Return an empty scenarios array only when no genuinely new scenario for "${feature}" remains.
|
|
411
|
+
</planning_strategy>
|
|
412
|
+
`;
|
|
413
|
+
}
|
|
414
|
+
else {
|
|
415
|
+
let extendedResearchHint = '';
|
|
416
|
+
if (mdq(plannerResearch).query('section("Extended Research")').count() > 0) {
|
|
417
|
+
extendedResearchHint = 'IMPORTANT: The research contains "Extended Research" sections with dropdowns, modals, and panels. Prioritize testing features from Extended Research that have no coverage yet.';
|
|
418
|
+
}
|
|
419
|
+
planningStrategy = dedent `
|
|
420
|
+
<planning_strategy>
|
|
421
|
+
Find a feature area in the research that has NO or minimal test coverage.
|
|
422
|
+
Pick that ONE feature and propose ${this.MIN_TASKS}-${this.MAX_TASKS} tests for it.
|
|
423
|
+
${extendedResearchHint}
|
|
424
|
+
|
|
425
|
+
Follow the <approach> described above when proposing tests for this feature.
|
|
426
|
+
|
|
427
|
+
If ALL features across ALL research sections are covered, return empty scenarios array.
|
|
428
|
+
</planning_strategy>
|
|
429
|
+
`;
|
|
430
|
+
}
|
|
403
431
|
conversation.addUserText(dedent `
|
|
404
432
|
CRITICAL: This plan already has tests.
|
|
405
433
|
|
|
@@ -419,15 +447,7 @@ export class Planner extends PlannerBase {
|
|
|
419
447
|
${compactContext}
|
|
420
448
|
</tested_scenarios>
|
|
421
449
|
|
|
422
|
-
|
|
423
|
-
Find a feature area in the research that has NO or minimal test coverage.
|
|
424
|
-
Pick that ONE feature and propose ${this.MIN_TASKS}-${this.MAX_TASKS} tests for it.
|
|
425
|
-
${mdq(plannerResearch).query('section("Extended Research")').count() > 0 ? 'IMPORTANT: The research contains "Extended Research" sections with dropdowns, modals, and panels. Prioritize testing features from Extended Research that have no coverage yet.' : ''}
|
|
426
|
-
|
|
427
|
-
Follow the <approach> described above when proposing tests for this feature.
|
|
428
|
-
|
|
429
|
-
If ALL features across ALL research sections are covered, return empty scenarios array.
|
|
430
|
-
</planning_strategy>
|
|
450
|
+
${planningStrategy}
|
|
431
451
|
|
|
432
452
|
<context_from_previous_tests>
|
|
433
453
|
During testing, the following pages were visited:
|
package/dist/src/ai/provider.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { LangfuseSpanProcessor } from '@langfuse/otel';
|
|
2
2
|
import { NodeSDK } from '@opentelemetry/sdk-node';
|
|
3
|
-
import { generateObject, generateText } from 'ai';
|
|
3
|
+
import { generateObject, generateText, stepCountIs } from 'ai';
|
|
4
4
|
import { clearActivity, setActivity } from "../activity.js";
|
|
5
|
-
import { RulesLoader } from "../utils/rules-loader.js";
|
|
6
5
|
import { executionController } from "../execution-controller.js";
|
|
7
6
|
import { Observability } from "../observability.js";
|
|
8
7
|
import { Stats } from "../stats.js";
|
|
9
8
|
import { createDebug, tag } from '../utils/logger.js';
|
|
10
9
|
import { withRetry } from '../utils/retry.js';
|
|
10
|
+
import { RulesLoader } from "../utils/rules-loader.js";
|
|
11
11
|
import { Conversation } from './conversation.js';
|
|
12
12
|
const debugLog = createDebug('explorbot:provider');
|
|
13
13
|
const promptLog = createDebug('explorbot:provider:out');
|
|
@@ -16,6 +16,20 @@ class AiError extends Error {
|
|
|
16
16
|
}
|
|
17
17
|
export class ContextLengthError extends Error {
|
|
18
18
|
}
|
|
19
|
+
function rejectAfterIdle(ms, signal) {
|
|
20
|
+
return new Promise((_, reject) => {
|
|
21
|
+
const tick = () => {
|
|
22
|
+
if (signal.cancelled)
|
|
23
|
+
return;
|
|
24
|
+
if (executionController.isAwaitingInput()) {
|
|
25
|
+
setTimeout(tick, ms);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
reject(new Error('AI request timeout'));
|
|
29
|
+
};
|
|
30
|
+
setTimeout(tick, ms);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
19
33
|
export class Provider {
|
|
20
34
|
config;
|
|
21
35
|
telemetryEnabled = false;
|
|
@@ -247,13 +261,19 @@ export class Provider {
|
|
|
247
261
|
promptLog('Available tools:', toolNames);
|
|
248
262
|
promptLog(messages[messages.length - 1].content);
|
|
249
263
|
const telemetry = this.getTelemetry(options);
|
|
264
|
+
const maxRoundtrips = options.maxToolRoundtrips ?? 5;
|
|
265
|
+
const extraStop = options.stopWhen;
|
|
266
|
+
const stopConditions = [stepCountIs(maxRoundtrips)];
|
|
267
|
+
if (extraStop)
|
|
268
|
+
stopConditions.push(extraStop);
|
|
269
|
+
const { stopWhen: _ignoredStopWhen, ...optionsWithoutStop } = options;
|
|
250
270
|
const config = this.mergeProviderOptions({
|
|
251
271
|
tools,
|
|
252
272
|
maxTokens: 16384,
|
|
253
|
-
maxToolRoundtrips: options.maxToolRoundtrips ?? 5,
|
|
254
273
|
toolChoice: 'auto',
|
|
255
274
|
...(this.config.config || {}),
|
|
256
|
-
...
|
|
275
|
+
...optionsWithoutStop,
|
|
276
|
+
stopWhen: stopConditions,
|
|
257
277
|
...(telemetry ? { experimental_telemetry: telemetry } : {}),
|
|
258
278
|
model,
|
|
259
279
|
abortSignal: executionController.getAbortSignal(),
|
|
@@ -261,13 +281,24 @@ export class Provider {
|
|
|
261
281
|
try {
|
|
262
282
|
const response = await withRetry(async () => {
|
|
263
283
|
const timeout = config.timeout || 30000;
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
284
|
+
const cancel = { cancelled: false };
|
|
285
|
+
try {
|
|
286
|
+
const result = (await Promise.race([
|
|
287
|
+
generateText({
|
|
288
|
+
messages,
|
|
289
|
+
...config,
|
|
290
|
+
}),
|
|
291
|
+
rejectAfterIdle(timeout, cancel),
|
|
292
|
+
]));
|
|
293
|
+
const hasToolCall = (result.toolCalls?.length || 0) > 0;
|
|
294
|
+
if (!result.text && !hasToolCall && result.finishReason === 'length') {
|
|
295
|
+
throw new ContextLengthError('AI response empty: output truncated at maxTokens. Increase maxTokens in config or use a model with higher output capacity.');
|
|
296
|
+
}
|
|
297
|
+
return result;
|
|
298
|
+
}
|
|
299
|
+
finally {
|
|
300
|
+
cancel.cancelled = true;
|
|
301
|
+
}
|
|
271
302
|
}, this.getRetryOptions(options));
|
|
272
303
|
clearActivity();
|
|
273
304
|
// Log tool usage summary
|
|
@@ -330,13 +361,19 @@ export class Provider {
|
|
|
330
361
|
promptLog(messages[messages.length - 1].content);
|
|
331
362
|
const response = await withRetry(async () => {
|
|
332
363
|
const timeout = config.timeout || 30000;
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
364
|
+
const cancel = { cancelled: false };
|
|
365
|
+
try {
|
|
366
|
+
return (await Promise.race([
|
|
367
|
+
generateObject({
|
|
368
|
+
messages,
|
|
369
|
+
...config,
|
|
370
|
+
}),
|
|
371
|
+
rejectAfterIdle(timeout, cancel),
|
|
372
|
+
]));
|
|
373
|
+
}
|
|
374
|
+
finally {
|
|
375
|
+
cancel.cancelled = true;
|
|
376
|
+
}
|
|
340
377
|
}, this.getRetryOptions(options));
|
|
341
378
|
clearActivity();
|
|
342
379
|
responseLog(response.object);
|
package/dist/src/ai/rerunner.js
CHANGED
|
@@ -7,8 +7,8 @@ import { highlight } from 'cli-highlight';
|
|
|
7
7
|
import * as codeceptjs from 'codeceptjs';
|
|
8
8
|
import heal from 'codeceptjs/lib/heal';
|
|
9
9
|
import aiTracePlugin from 'codeceptjs/lib/plugin/aiTrace';
|
|
10
|
-
import figureSet from 'figures';
|
|
11
10
|
import dedent from 'dedent';
|
|
11
|
+
import figureSet from 'figures';
|
|
12
12
|
import { z } from 'zod';
|
|
13
13
|
import { ActionResult } from "../action-result.js";
|
|
14
14
|
import { setActivity } from "../activity.js";
|
|
@@ -16,11 +16,11 @@ import { Stats } from "../stats.js";
|
|
|
16
16
|
import { Task, Test, TestResult } from "../test-plan.js";
|
|
17
17
|
import { createDebug, tag } from "../utils/logger.js";
|
|
18
18
|
import { loop } from "../utils/loop.js";
|
|
19
|
+
import { RulesLoader } from "../utils/rules-loader.js";
|
|
19
20
|
import { loadTestSuites, printTestList } from "../utils/test-files.js";
|
|
20
21
|
import { toolExecutionLabel } from "./conversation.js";
|
|
21
|
-
import {
|
|
22
|
+
import { actionRule, locatorRule, sectionContextRule } from "./rules.js";
|
|
22
23
|
import { TaskAgent } from "./task-agent.js";
|
|
23
|
-
import { RulesLoader } from "../utils/rules-loader.js";
|
|
24
24
|
import { createCodeceptJSTools } from "./tools.js";
|
|
25
25
|
const debugLog = createDebug('explorbot:rerunner');
|
|
26
26
|
export class Rerunner extends TaskAgent {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import dedent from 'dedent';
|
|
2
2
|
import { ActionResult } from '../../action-result.js';
|
|
3
|
-
import { detectFocusArea, diffAriaSnapshots } from "../../utils/aria.js";
|
|
4
3
|
import { executionController } from "../../execution-controller.js";
|
|
4
|
+
import { detectFocusArea, diffAriaSnapshots } from "../../utils/aria.js";
|
|
5
5
|
import { tag } from '../../utils/logger.js';
|
|
6
6
|
import { mdq } from "../../utils/markdown-query.js";
|
|
7
7
|
import { getCachedResearch, saveResearch } from "./cache.js";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync,
|
|
1
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { parentPort } from 'node:worker_threads';
|
|
4
4
|
import { computeHtmlFingerprint } from "../../utils/html-diff.js";
|
|
@@ -4,8 +4,8 @@ import { parseAriaLocator } from "../../utils/aria.js";
|
|
|
4
4
|
import { tag } from '../../utils/logger.js';
|
|
5
5
|
import { mdq } from "../../utils/markdown-query.js";
|
|
6
6
|
import { WebElement } from "../../utils/web-element.js";
|
|
7
|
-
import { FOCUSED_MARKER } from "./focus.js";
|
|
8
7
|
import { locatorRule as generalLocatorRuleText } from '../rules.js';
|
|
8
|
+
import { FOCUSED_MARKER } from "./focus.js";
|
|
9
9
|
import { debugLog } from "./mixin.js";
|
|
10
10
|
import { parseResearchSections } from "./parser.js";
|
|
11
11
|
function firstCssSegment(css) {
|