explorbot 0.2.3 → 0.2.4
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 +1 -1
- package/bin/explorbot-cli.ts +7 -1
- package/boat/api-tester/src/config.ts +4 -2
- package/boat/doc-collector/bin/doc-collector-cli.ts +2 -0
- package/boat/doc-collector/src/ai/documentarian.ts +61 -31
- package/boat/doc-collector/src/config.ts +4 -2
- package/boat/prima/bin/prima-cli.ts +0 -0
- package/boat/prima/src/activity-line.ts +33 -0
- package/boat/prima/src/cli.ts +115 -86
- package/boat/prima/src/envelope.ts +80 -52
- package/boat/prima/src/prima.ts +547 -128
- package/boat/prima/src/pw-parser.ts +11 -1
- package/boat/prima/src/pw-registry.ts +4 -5
- package/boat/prima/src/session-log.ts +126 -0
- package/dist/bin/explorbot-cli.js +7 -1
- package/dist/boat/api-tester/bin/apibot-cli.js +2 -0
- package/dist/boat/api-tester/src/config.js +4 -2
- package/dist/boat/doc-collector/bin/doc-collector-cli.js +2 -0
- package/dist/boat/doc-collector/src/ai/documentarian.js +44 -19
- package/dist/boat/doc-collector/src/config.js +4 -2
- package/dist/boat/prima/src/activity-line.js +30 -0
- package/dist/boat/prima/src/cli.js +97 -77
- package/dist/boat/prima/src/envelope.js +70 -44
- package/dist/boat/prima/src/prima.js +515 -119
- package/dist/boat/prima/src/pw-parser.js +13 -1
- package/dist/boat/prima/src/pw-registry.js +4 -5
- package/dist/boat/prima/src/session-log.js +108 -0
- package/dist/package.json +3 -2
- package/dist/rules/navigator/verification-actions.md +20 -0
- package/dist/src/action-result.d.ts +7 -0
- package/dist/src/action-result.js +4 -0
- package/dist/src/action.d.ts +2 -0
- package/dist/src/action.js +36 -1
- package/dist/src/ai/captain/web-mode.js +6 -3
- package/dist/src/ai/captain.js +2 -0
- package/dist/src/ai/navigator.d.ts +7 -0
- package/dist/src/ai/navigator.js +10 -6
- package/dist/src/ai/pilot.d.ts +4 -0
- package/dist/src/ai/pilot.js +48 -2
- package/dist/src/ai/provider.js +2 -2
- package/dist/src/ai/rerunner.js +2 -1
- package/dist/src/ai/researcher.js +1 -1
- package/dist/src/ai/rules.js +17 -10
- package/dist/src/ai/task-agent.js +4 -1
- package/dist/src/ai/tester.d.ts +1 -1
- package/dist/src/ai/tester.js +33 -33
- package/dist/src/ai/tools.d.ts +14 -0
- package/dist/src/ai/tools.js +113 -36
- package/dist/src/config.d.ts +1 -0
- package/dist/src/config.js +13 -4
- package/dist/src/execution-controller.d.ts +2 -0
- package/dist/src/execution-controller.js +6 -0
- package/dist/src/explorbot.d.ts +2 -1
- package/dist/src/explorbot.js +3 -1
- package/dist/src/explorer.js +2 -3
- package/dist/src/playwright-recorder.js +30 -0
- package/dist/src/remote.d.ts +54 -0
- package/dist/src/remote.js +229 -0
- package/dist/src/reporter.d.ts +1 -0
- package/dist/src/reporter.js +7 -1
- package/dist/src/state-manager.d.ts +2 -1
- package/dist/src/stats.d.ts +1 -0
- package/dist/src/stats.js +1 -0
- package/dist/src/test-plan.d.ts +2 -0
- package/dist/src/test-plan.js +7 -0
- package/dist/src/utils/aria.d.ts +2 -8
- package/dist/src/utils/aria.js +69 -40
- package/dist/src/utils/html.js +1 -0
- package/dist/src/utils/logger.d.ts +6 -0
- package/dist/src/utils/logger.js +24 -0
- package/dist/src/utils/page-readiness.js +18 -1
- package/dist/src/utils/url-matcher.js +3 -0
- package/dist/src/utils/web-element.d.ts +2 -0
- package/dist/src/utils/web-element.js +8 -0
- package/dist/src/utils/web-sandbox.d.ts +1 -1
- package/dist/src/utils/web-sandbox.js +2 -3
- package/docs/api-testing/basics.md +90 -0
- package/docs/api-testing/planning.md +57 -0
- package/docs/api-testing/running-tests.md +55 -0
- package/docs/assets/cloud-report.png +0 -0
- package/docs/assets/html-report.png +0 -0
- package/docs/assets/langfuse-trace.png +0 -0
- package/docs/assets/successful-explore-run.png +0 -0
- package/docs/basics/getting-started.md +140 -0
- package/docs/basics/prerequisites.md +63 -0
- package/docs/basics/providers.md +362 -0
- package/docs/basics/running.md +78 -0
- package/docs/contributing/ai-integration-tests.md +57 -0
- package/docs/contributing/contributing.md +90 -0
- package/docs/contributing/demo-videos.md +36 -0
- package/docs/contributing/npm-package.md +138 -0
- package/docs/contributing/observability.md +227 -0
- package/docs/contributing/regression-tests.md +103 -0
- package/docs/contributing/testing.md +95 -0
- package/docs/doc-collection/basics.md +128 -0
- package/docs/doc-collection/crawling.md +67 -0
- package/docs/doc-collection/interactive-mode.md +99 -0
- package/docs/index.json +86 -0
- package/docs/reference/commands.md +994 -0
- package/docs/reference/configuration.md +569 -0
- package/docs/reference/scripting.md +303 -0
- package/docs/superpowers/plans/2026-08-01-actor-boat.md +925 -0
- package/docs/superpowers/plans/2026-08-01-prima-boat.md +1120 -0
- package/docs/superpowers/reviews/2026-08-06-prima-vs-playwright-cli.md +268 -0
- package/docs/superpowers/specs/2026-08-01-actor-boat-design.md +204 -0
- package/docs/superpowers/specs/2026-08-01-prima-boat-design.md +242 -0
- package/docs/superpowers/specs/2026-08-03-global-config-design.md +138 -0
- package/docs/superpowers/specs/2026-08-07-prima-fixes-design.md +394 -0
- package/docs/web-testing/agents.md +158 -0
- package/docs/web-testing/automated-tests.md +134 -0
- package/docs/web-testing/basics.md +91 -0
- package/docs/web-testing/customization.md +131 -0
- package/docs/web-testing/hooks.md +238 -0
- package/docs/web-testing/page-interaction.md +84 -0
- package/docs/web-testing/planner.md +122 -0
- package/docs/web-testing/rerun.md +164 -0
- package/docs/web-testing/researcher.md +380 -0
- package/docs/workflow/agentic-usage.md +233 -0
- package/docs/workflow/application-spec.md +73 -0
- package/docs/workflow/ci.md +202 -0
- package/docs/workflow/knowledge.md +310 -0
- package/docs/workflow/planning-styles.md +67 -0
- package/docs/workflow/reporting.md +133 -0
- package/docs/workflow/test-plans.md +90 -0
- package/package.json +3 -2
- package/rules/navigator/verification-actions.md +20 -0
- package/src/action-result.ts +11 -0
- package/src/action.ts +38 -2
- package/src/ai/captain/web-mode.ts +6 -3
- package/src/ai/captain.ts +3 -0
- package/src/ai/navigator.ts +14 -8
- package/src/ai/pilot.ts +53 -2
- package/src/ai/provider.ts +2 -4
- package/src/ai/rerunner.ts +2 -1
- package/src/ai/researcher.ts +1 -1
- package/src/ai/rules.ts +17 -10
- package/src/ai/task-agent.ts +3 -1
- package/src/ai/tester.ts +32 -33
- package/src/ai/tools.ts +132 -36
- package/src/config.ts +15 -4
- package/src/execution-controller.ts +8 -0
- package/src/explorbot.ts +4 -2
- package/src/explorer.ts +2 -2
- package/src/playwright-recorder.ts +23 -0
- package/src/remote.ts +238 -0
- package/src/reporter.ts +7 -1
- package/src/state-manager.ts +2 -1
- package/src/stats.ts +1 -0
- package/src/test-plan.ts +9 -0
- package/src/utils/aria.ts +65 -45
- package/src/utils/html.ts +1 -0
- package/src/utils/logger.ts +24 -1
- package/src/utils/page-readiness.ts +24 -1
- package/src/utils/url-matcher.ts +3 -0
- package/src/utils/web-element.ts +9 -0
- package/src/utils/web-sandbox.ts +3 -4
package/src/action.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { join } from 'node:path';
|
|
|
3
3
|
import { context, trace } from '@opentelemetry/api';
|
|
4
4
|
import { container, recorder } from 'codeceptjs';
|
|
5
5
|
import * as codeceptjs from 'codeceptjs';
|
|
6
|
-
import { ActionResult } from './action-result.js';
|
|
6
|
+
import { ActionResult, type FocusedElement } from './action-result.js';
|
|
7
7
|
import { clearActivity, setActivity } from './activity.ts';
|
|
8
8
|
import { ConfigParser, outputPath } from './config.js';
|
|
9
9
|
import type { ExplorbotConfig } from './config.js';
|
|
@@ -19,6 +19,8 @@ import { codeceptJSSandbox, hasPlaywrightCommands, playwrightSandbox, sanitizeCo
|
|
|
19
19
|
|
|
20
20
|
const debugLog = createDebug('explorbot:action');
|
|
21
21
|
const CAPTURE_NAVIGATION_TRANSITION_ATTEMPTS = 3;
|
|
22
|
+
const DEFAULT_ACTION_TIMEOUT = 3000;
|
|
23
|
+
const DEFAULT_PAGE_TIMEOUT = 3000;
|
|
22
24
|
|
|
23
25
|
class Action {
|
|
24
26
|
private actor: CodeceptJS.I;
|
|
@@ -32,6 +34,7 @@ class Action {
|
|
|
32
34
|
public playwrightHelper: any;
|
|
33
35
|
public playwrightGroupId: string | null = null;
|
|
34
36
|
public assertionSteps: Array<{ name: string; args: any[] }> = [];
|
|
37
|
+
public lastValue: unknown;
|
|
35
38
|
private recorder?: PlaywrightRecorder;
|
|
36
39
|
private recovery: RecoveryRunner;
|
|
37
40
|
private mainDocumentStatus: number | undefined = undefined;
|
|
@@ -133,10 +136,12 @@ class Action {
|
|
|
133
136
|
|
|
134
137
|
let ariaSnapshot: string | null = null;
|
|
135
138
|
let ariaSnapshotFile: string | undefined = undefined;
|
|
139
|
+
let focusedElement: FocusedElement | null = null;
|
|
136
140
|
|
|
137
141
|
try {
|
|
138
142
|
const page = this.playwrightHelper.page;
|
|
139
143
|
ariaSnapshot = await page.locator('body').ariaSnapshot();
|
|
144
|
+
focusedElement = await page.evaluate(readFocusedElement);
|
|
140
145
|
} catch (err) {
|
|
141
146
|
debugLog('ARIA snapshot failed:', err instanceof Error ? `${err.message}\n${err.stack}` : err);
|
|
142
147
|
}
|
|
@@ -160,6 +165,7 @@ class Action {
|
|
|
160
165
|
iframeSnapshots,
|
|
161
166
|
ariaSnapshot,
|
|
162
167
|
ariaSnapshotFile,
|
|
168
|
+
focusedElement,
|
|
163
169
|
iframeURL: frame ? frame.url?.() || 'iframe' : undefined,
|
|
164
170
|
});
|
|
165
171
|
this.stateManager.updateState(result, codeBlock);
|
|
@@ -297,16 +303,21 @@ class Action {
|
|
|
297
303
|
throw new Error('No valid I.* or page.* commands found in code block');
|
|
298
304
|
}
|
|
299
305
|
|
|
306
|
+
this.playwrightHelper?.page?.setDefaultTimeout(this.config.action?.timeout ?? DEFAULT_ACTION_TIMEOUT);
|
|
307
|
+
|
|
300
308
|
if (isPlaywright) {
|
|
301
309
|
const page = this.playwrightHelper.page;
|
|
302
310
|
await playwrightSandbox(page, sanitizedCode);
|
|
303
311
|
await sleep(this.config.action?.delay || 500);
|
|
304
312
|
} else {
|
|
305
|
-
codeceptJSSandbox(this.actor, sanitizedCode);
|
|
313
|
+
const returned = codeceptJSSandbox(this.actor, sanitizedCode);
|
|
306
314
|
await recorder.add(() => sleep(this.config.action?.delay || 500));
|
|
307
315
|
await recorder.promise();
|
|
316
|
+
this.lastValue = await returned;
|
|
308
317
|
}
|
|
309
318
|
|
|
319
|
+
this.restorePageTimeout();
|
|
320
|
+
|
|
310
321
|
if (executedSteps.length > 0) {
|
|
311
322
|
codeString = executedSteps.join('\n');
|
|
312
323
|
}
|
|
@@ -325,6 +336,7 @@ class Action {
|
|
|
325
336
|
this.assertionSteps = [];
|
|
326
337
|
throw err;
|
|
327
338
|
} finally {
|
|
339
|
+
this.restorePageTimeout();
|
|
328
340
|
detachMainDocumentResponse();
|
|
329
341
|
if (groupId) await this.recorder!.endAction();
|
|
330
342
|
detachStepLogger(stepListener);
|
|
@@ -374,6 +386,10 @@ class Action {
|
|
|
374
386
|
return this.actionResult;
|
|
375
387
|
}
|
|
376
388
|
|
|
389
|
+
private restorePageTimeout(): void {
|
|
390
|
+
this.playwrightHelper?.page?.setDefaultTimeout(this.config.playwright.timeout ?? DEFAULT_PAGE_TIMEOUT);
|
|
391
|
+
}
|
|
392
|
+
|
|
377
393
|
private async waitForPageReadiness(page: any): Promise<void> {
|
|
378
394
|
await waitForPageReadiness(page, {
|
|
379
395
|
timeout: this.config.playwright.waitForTimeout,
|
|
@@ -442,3 +458,23 @@ const detachStepLogger = (listener: StepListener) => {
|
|
|
442
458
|
codeceptjs.event.dispatcher.off(codeceptjs.event.step.passed, listener);
|
|
443
459
|
codeceptjs.event.dispatcher.off(codeceptjs.event.step.failed, listener);
|
|
444
460
|
};
|
|
461
|
+
|
|
462
|
+
const readFocusedElement = () => {
|
|
463
|
+
const el = document.activeElement as any;
|
|
464
|
+
if (!el || el === document.body) return null;
|
|
465
|
+
|
|
466
|
+
const tag = el.tagName.toLowerCase();
|
|
467
|
+
const textish = new Set(['text', 'search', 'email', 'password', 'url', 'tel', 'number']);
|
|
468
|
+
let role = el.getAttribute('role') || tag;
|
|
469
|
+
if (tag === 'textarea' || el.isContentEditable) role = 'textbox';
|
|
470
|
+
if (tag === 'input' && textish.has(el.type)) role = 'textbox';
|
|
471
|
+
if (tag === 'select') role = 'combobox';
|
|
472
|
+
if (tag === 'a') role = 'link';
|
|
473
|
+
|
|
474
|
+
const label = el.getAttribute('aria-label') || el.getAttribute('placeholder') || el.labels?.[0]?.textContent || el.textContent || '';
|
|
475
|
+
const focused: { role: string; name: string; value?: string } = { role, name: label.trim().slice(0, 80) };
|
|
476
|
+
|
|
477
|
+
const value = el.value ?? el.textContent;
|
|
478
|
+
if (typeof value === 'string' && value) focused.value = value.slice(0, 200);
|
|
479
|
+
return focused;
|
|
480
|
+
};
|
|
@@ -18,7 +18,7 @@ export function WithWebMode<T extends Constructor>(Base: T) {
|
|
|
18
18
|
});
|
|
19
19
|
const { see, context, visualClick, learnExperience } = agentTools;
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
const tools: Record<string, any> = {
|
|
22
22
|
navigate: tool({
|
|
23
23
|
description: 'Navigate to a URL or page description using AI-powered navigation.',
|
|
24
24
|
inputSchema: z.object({
|
|
@@ -123,11 +123,14 @@ export function WithWebMode<T extends Constructor>(Base: T) {
|
|
|
123
123
|
}),
|
|
124
124
|
|
|
125
125
|
...codeceptTools,
|
|
126
|
-
see,
|
|
127
126
|
context,
|
|
128
|
-
visualClick,
|
|
129
127
|
learnExperience,
|
|
130
128
|
};
|
|
129
|
+
|
|
130
|
+
if (see) tools.see = see;
|
|
131
|
+
if (visualClick) tools.visualClick = visualClick;
|
|
132
|
+
|
|
133
|
+
return tools;
|
|
131
134
|
}
|
|
132
135
|
|
|
133
136
|
webModePrompt(): string {
|
package/src/ai/captain.ts
CHANGED
|
@@ -21,6 +21,7 @@ import type { Navigator } from './navigator.ts';
|
|
|
21
21
|
import type { Provider } from './provider.ts';
|
|
22
22
|
import { Researcher } from './researcher.ts';
|
|
23
23
|
import { TaskAgent } from './task-agent.ts';
|
|
24
|
+
import { withdrawVisionTools } from './tools.ts';
|
|
24
25
|
|
|
25
26
|
const MAX_STEPS = 15;
|
|
26
27
|
|
|
@@ -437,6 +438,8 @@ export class Captain extends CaptainBase implements Agent {
|
|
|
437
438
|
return;
|
|
438
439
|
}
|
|
439
440
|
|
|
441
|
+
withdrawVisionTools(tools);
|
|
442
|
+
|
|
440
443
|
const currentState = stateManager.getCurrentState();
|
|
441
444
|
if (!currentState && this.getMode() !== 'idle') {
|
|
442
445
|
stop();
|
package/src/ai/navigator.ts
CHANGED
|
@@ -8,6 +8,7 @@ import type { ExperienceTracker } from '../experience-tracker.js';
|
|
|
8
8
|
import Explorer from '../explorer.ts';
|
|
9
9
|
import type { KnowledgeTracker } from '../knowledge-tracker.js';
|
|
10
10
|
import { type StateManager, normalizeUrl } from '../state-manager.js';
|
|
11
|
+
import { renderAssertion } from '../playwright-recorder.ts';
|
|
11
12
|
import { extractCodeBlocks } from '../utils/code-extractor.js';
|
|
12
13
|
import { HooksRunner } from '../utils/hooks-runner.ts';
|
|
13
14
|
import { createDebug, pluralize, tag } from '../utils/logger.js';
|
|
@@ -620,7 +621,7 @@ class Navigator implements Agent {
|
|
|
620
621
|
return suggestion;
|
|
621
622
|
}
|
|
622
623
|
|
|
623
|
-
async verifyState(message: string, actionResult: ActionResult): Promise<{ verified: boolean; successfulCodes: string[]; assertionSteps: Array<{ name: string; args: any[] }>; totalAttempted: number }> {
|
|
624
|
+
async verifyState(message: string, actionResult: ActionResult): Promise<{ verified: boolean; inexpressible: boolean; results: AssertionResult[]; successfulCodes: string[]; assertionSteps: Array<{ name: string; args: any[] }>; totalAttempted: number }> {
|
|
624
625
|
tag('info').log('AI Navigator verifying state at', actionResult.url);
|
|
625
626
|
debugLog('Verification message:', message);
|
|
626
627
|
|
|
@@ -698,6 +699,7 @@ class Navigator implements Agent {
|
|
|
698
699
|
|
|
699
700
|
let codeBlocks: string[] = [];
|
|
700
701
|
const successfulCodes: string[] = [];
|
|
702
|
+
const results: AssertionResult[] = [];
|
|
701
703
|
const assertionSteps: Array<{ name: string; args: any[] }> = [];
|
|
702
704
|
|
|
703
705
|
const action = this.explorer.action();
|
|
@@ -739,6 +741,8 @@ class Navigator implements Agent {
|
|
|
739
741
|
await action.exitIframe();
|
|
740
742
|
|
|
741
743
|
const verified = await action.attempt(codeBlock, message);
|
|
744
|
+
const proof = action.assertionSteps.map(renderAssertion).filter(Boolean);
|
|
745
|
+
results.push({ code: codeBlock, passed: verified, proof });
|
|
742
746
|
|
|
743
747
|
if (verified) {
|
|
744
748
|
tag('success').log('Verification passed');
|
|
@@ -747,12 +751,6 @@ class Navigator implements Agent {
|
|
|
747
751
|
} else {
|
|
748
752
|
failures++;
|
|
749
753
|
}
|
|
750
|
-
|
|
751
|
-
const target = Math.min(codeBlocks.length, this.verifyAttempts);
|
|
752
|
-
const majorityNeeded = Math.floor(target / 2) + 1;
|
|
753
|
-
if (successfulCodes.length >= majorityNeeded || failures > target - majorityNeeded) {
|
|
754
|
-
stop();
|
|
755
|
-
}
|
|
756
754
|
},
|
|
757
755
|
{
|
|
758
756
|
maxAttempts: this.verifyAttempts,
|
|
@@ -773,10 +771,16 @@ class Navigator implements Agent {
|
|
|
773
771
|
let verified = successfulCodes.length >= majorityNeeded;
|
|
774
772
|
if (alreadyVerified) verified = true;
|
|
775
773
|
|
|
774
|
+
const inexpressible = !alreadyVerified && totalAttempted === 0;
|
|
775
|
+
if (inexpressible) {
|
|
776
|
+
tag('warning').log('No assertion could express this claim');
|
|
777
|
+
return { verified: false, inexpressible, results, successfulCodes, assertionSteps, totalAttempted };
|
|
778
|
+
}
|
|
779
|
+
|
|
776
780
|
actionResult.addVerification(message, verified);
|
|
777
781
|
this.stateManager.updateState(actionResult);
|
|
778
782
|
|
|
779
|
-
return { verified, successfulCodes, assertionSteps, totalAttempted };
|
|
783
|
+
return { verified, inexpressible, results, successfulCodes, assertionSteps, totalAttempted };
|
|
780
784
|
}
|
|
781
785
|
|
|
782
786
|
private checkAlreadyVerified(aiResponse: string, actionResult: ActionResult): boolean {
|
|
@@ -787,4 +791,6 @@ class Navigator implements Agent {
|
|
|
787
791
|
}
|
|
788
792
|
}
|
|
789
793
|
|
|
794
|
+
export type AssertionResult = { code: string; passed: boolean; proof: string[] };
|
|
795
|
+
|
|
790
796
|
export { Navigator };
|
package/src/ai/pilot.ts
CHANGED
|
@@ -8,7 +8,7 @@ import type Explorer from '../explorer.ts';
|
|
|
8
8
|
import type { PlaywrightRecorder } from '../playwright-recorder.ts';
|
|
9
9
|
import type { StateManager } from '../state-manager.ts';
|
|
10
10
|
import { type Test, TestResult } from '../test-plan.ts';
|
|
11
|
-
import { collectInteractiveNodes, detectFocusArea
|
|
11
|
+
import { collectInteractiveNodes, detectFocusArea } from '../utils/aria.ts';
|
|
12
12
|
import { ErrorPageError } from '../utils/error-page.ts';
|
|
13
13
|
import { createDebug, tag } from '../utils/logger.ts';
|
|
14
14
|
|
|
@@ -22,6 +22,7 @@ import type { Provider } from './provider.ts';
|
|
|
22
22
|
import type { Researcher } from './researcher.ts';
|
|
23
23
|
import { capabilityGroundingRule, dataProtectionRules } from './rules.ts';
|
|
24
24
|
import { isInteractive } from './task-agent.ts';
|
|
25
|
+
import { withdrawVisionTools } from './tools.ts';
|
|
25
26
|
|
|
26
27
|
const CHECK_TOOLS = ['verify', 'see', 'research', 'context'];
|
|
27
28
|
const META_TOOLS = ['record', 'reset', 'stop', 'finish'];
|
|
@@ -561,6 +562,55 @@ export class Pilot implements Agent {
|
|
|
561
562
|
return text;
|
|
562
563
|
}
|
|
563
564
|
|
|
565
|
+
async settleExpectations(task: Test): Promise<Array<{ text: string; status: 'passed' | 'failed' | 'unverified' }>> {
|
|
566
|
+
const undecided = task.expected.filter((text) => !task.getCheckedExpectations().includes(text));
|
|
567
|
+
const decided = (text: string): 'passed' | 'failed' => {
|
|
568
|
+
if (task.hasAchievedAny() && !task.getRemainingExpectations().includes(text)) return 'passed';
|
|
569
|
+
return 'failed';
|
|
570
|
+
};
|
|
571
|
+
|
|
572
|
+
if (!undecided.length) return task.expected.map((text) => ({ text, status: decided(text) }));
|
|
573
|
+
|
|
574
|
+
const schema = z.object({
|
|
575
|
+
outcomes: z.array(
|
|
576
|
+
z.object({
|
|
577
|
+
expectation: z.string().describe('The expected outcome, repeated exactly as it was given'),
|
|
578
|
+
status: z.enum(['passed', 'failed', 'unverified']).describe('passed = the log shows it happened, failed = the log shows it did not, unverified = the run never established either way'),
|
|
579
|
+
})
|
|
580
|
+
),
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
const userContent = dedent`
|
|
584
|
+
A test run has finished. Decide, for each expected outcome, what the run established about it.
|
|
585
|
+
|
|
586
|
+
<expected_outcomes>
|
|
587
|
+
${undecided.map((text) => `- ${text}`).join('\n')}
|
|
588
|
+
</expected_outcomes>
|
|
589
|
+
|
|
590
|
+
<run_log>
|
|
591
|
+
${task.notesToString() || 'No steps recorded.'}
|
|
592
|
+
</run_log>
|
|
593
|
+
|
|
594
|
+
The log is written in the tester's own words, so an outcome can be satisfied by a step that describes it
|
|
595
|
+
differently. Judge by what the steps show happened, not by whether the wording matches.
|
|
596
|
+
Choose "unverified" only when the log neither shows the outcome happening nor shows it failing —
|
|
597
|
+
that is a statement about the run, not about the application.
|
|
598
|
+
`;
|
|
599
|
+
|
|
600
|
+
const response = await this.provider
|
|
601
|
+
.generateObject([{ role: 'user' as const, content: userContent }], schema, this.provider.getAgenticModel('pilot'), {
|
|
602
|
+
agentName: 'pilot',
|
|
603
|
+
telemetry: { functionId: 'pilot.settleExpectations' },
|
|
604
|
+
})
|
|
605
|
+
.catch(() => null);
|
|
606
|
+
|
|
607
|
+
const judged = new Map((response?.object?.outcomes || []).map((outcome: any) => [outcome.expectation, outcome.status]));
|
|
608
|
+
return task.expected.map((text) => {
|
|
609
|
+
if (!undecided.includes(text)) return { text, status: decided(text) };
|
|
610
|
+
return { text, status: (judged.get(text) as 'passed' | 'failed' | 'unverified') || 'unverified' };
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
|
|
564
614
|
private formatExpectations(task: Test): string {
|
|
565
615
|
const checked = task.getCheckedExpectations();
|
|
566
616
|
const remaining = task.getRemainingExpectations();
|
|
@@ -620,6 +670,7 @@ export class Pilot implements Agent {
|
|
|
620
670
|
if (xpathCheck) planning.xpathCheck = xpathCheck;
|
|
621
671
|
if (learnExperience) planning.learnExperience = learnExperience;
|
|
622
672
|
if (askUser) planning.askUser = askUser;
|
|
673
|
+
withdrawVisionTools(planning);
|
|
623
674
|
return planning;
|
|
624
675
|
}
|
|
625
676
|
|
|
@@ -703,7 +754,7 @@ export class Pilot implements Agent {
|
|
|
703
754
|
lines.push(`url: ${state.url}`);
|
|
704
755
|
lines.push(`title: ${state.title || 'unknown'}`);
|
|
705
756
|
|
|
706
|
-
const focused =
|
|
757
|
+
const focused = state.focusedElement;
|
|
707
758
|
if (focused) {
|
|
708
759
|
const valuePart = focused.value ? ` (value: "${focused.value}")` : '';
|
|
709
760
|
lines.push(`focused: ${focused.role} "${focused.name}"${valuePart}`);
|
package/src/ai/provider.ts
CHANGED
|
@@ -326,6 +326,7 @@ export class Provider {
|
|
|
326
326
|
try {
|
|
327
327
|
const response = await withRetry(async () => {
|
|
328
328
|
const result = await generateText({ messages, ...config });
|
|
329
|
+
this.recordUsage(options.agentName || 'unknown', modelName, result.usage);
|
|
329
330
|
if (!result.text) {
|
|
330
331
|
debugLog(result);
|
|
331
332
|
if (result.finishReason === 'length') {
|
|
@@ -342,8 +343,6 @@ export class Provider {
|
|
|
342
343
|
clearActivity();
|
|
343
344
|
responseLog(response.text);
|
|
344
345
|
|
|
345
|
-
this.recordUsage(options.agentName || 'unknown', modelName, response.usage);
|
|
346
|
-
|
|
347
346
|
return response;
|
|
348
347
|
} catch (error: any) {
|
|
349
348
|
clearActivity();
|
|
@@ -378,6 +377,7 @@ export class Provider {
|
|
|
378
377
|
try {
|
|
379
378
|
const response = await withRetry(async () => {
|
|
380
379
|
const result = (await this.raceWithIdleTimeout((signal) => generateText({ messages, ...config, abortSignal: signal }), config.timeout || 30000)) as any;
|
|
380
|
+
this.recordUsage(options.agentName || 'unknown', modelName, result.usage);
|
|
381
381
|
const hasToolCall = (result.toolCalls?.length || 0) > 0;
|
|
382
382
|
if (!result.text && !hasToolCall && result.finishReason === 'length') {
|
|
383
383
|
throw new ContextLengthError('AI response empty: output truncated at maxTokens. Increase maxOutputTokens in config or use a model with higher output capacity.');
|
|
@@ -397,8 +397,6 @@ export class Provider {
|
|
|
397
397
|
|
|
398
398
|
responseLog(response.text);
|
|
399
399
|
|
|
400
|
-
this.recordUsage(options.agentName || 'unknown', modelName, response.usage);
|
|
401
|
-
|
|
402
400
|
return response;
|
|
403
401
|
} catch (error: any) {
|
|
404
402
|
clearActivity();
|
package/src/ai/rerunner.ts
CHANGED
|
@@ -24,7 +24,7 @@ import type { Navigator } from './navigator.ts';
|
|
|
24
24
|
import { Provider } from './provider.ts';
|
|
25
25
|
import { actionRule, locatorRule, sectionContextRule } from './rules.ts';
|
|
26
26
|
import { TaskAgent } from './task-agent.ts';
|
|
27
|
-
import { createCodeceptJSTools } from './tools.ts';
|
|
27
|
+
import { createCodeceptJSTools, withdrawVisionTools } from './tools.ts';
|
|
28
28
|
|
|
29
29
|
const debugLog = createDebug('explorbot:rerunner');
|
|
30
30
|
|
|
@@ -361,6 +361,7 @@ export class Rerunner extends TaskAgent implements Agent {
|
|
|
361
361
|
},
|
|
362
362
|
}),
|
|
363
363
|
};
|
|
364
|
+
withdrawVisionTools(tools);
|
|
364
365
|
|
|
365
366
|
const conversation = this.provider.startConversation(this.getHealSystemPrompt(), 'rerunner');
|
|
366
367
|
conversation.addUserText(this.getHealUserPrompt(failedCode, error));
|
package/src/ai/researcher.ts
CHANGED
|
@@ -102,7 +102,7 @@ export class Researcher extends ResearcherBase implements Agent {
|
|
|
102
102
|
const cached = getCachedResearch(stateHash);
|
|
103
103
|
if (cached) {
|
|
104
104
|
debugLog('Previous research result found');
|
|
105
|
-
return
|
|
105
|
+
return cached;
|
|
106
106
|
}
|
|
107
107
|
}
|
|
108
108
|
|
package/src/ai/rules.ts
CHANGED
|
@@ -4,13 +4,18 @@ export const recommendedCodeceptCommands = ['I.click', 'I.type', 'I.fillField',
|
|
|
4
4
|
|
|
5
5
|
const locatorPriorityRule = dedent`
|
|
6
6
|
<locator_priority>
|
|
7
|
+
When the page context shows the element a ref, such as [ref=e14], there is no locator to select: click it with clickRef
|
|
8
|
+
and that ref. A ref names one exact element, so it never matches the wrong one and never has to be narrowed. Everything
|
|
9
|
+
below is for elements the context gives no ref for.
|
|
10
|
+
|
|
7
11
|
Use the following priority when selecting locators:
|
|
8
12
|
|
|
9
13
|
1. ARIA locators (first choice) - target browser's accessibility tree, most reliable
|
|
10
14
|
Use JSON format: { "role": "button", "text": "Login" }
|
|
11
|
-
|
|
15
|
+
Copy role and text VERBATIM from the ARIA snapshot or UI map — never guess the pair.
|
|
16
|
+
If the element is absent from the snapshot, do not invent one; use text or CSS instead.
|
|
12
17
|
|
|
13
|
-
2. Text locators (second choice) - use only when
|
|
18
|
+
2. Text locators (second choice) - exact visible text, use only when unique on the page
|
|
14
19
|
Example: 'Login', 'Submit', 'Username'
|
|
15
20
|
Skip if the same text appears multiple times on the page
|
|
16
21
|
|
|
@@ -26,10 +31,10 @@ const locatorPriorityRule = dedent`
|
|
|
26
31
|
|
|
27
32
|
const contextSimplificationRule = dedent`
|
|
28
33
|
<context_simplification>
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
-
|
|
32
|
-
-
|
|
34
|
+
- Add a container when the target may match several elements, or the UI map gives a verified
|
|
35
|
+
section container: I.click('Save', '.modal')
|
|
36
|
+
- Skip the container when the locator is already unique (XPath, unique ID, unique text)
|
|
37
|
+
- A wrong or stale container guarantees failure: always add one fallback command WITHOUT a container
|
|
33
38
|
- No need for complex ARIA when container narrows scope sufficiently
|
|
34
39
|
</context_simplification>
|
|
35
40
|
`;
|
|
@@ -59,7 +64,7 @@ const locatorStrategyRule = dedent`
|
|
|
59
64
|
|
|
60
65
|
<good_aria_locator_example>
|
|
61
66
|
{ "role": "button", "text": "Login" },
|
|
62
|
-
{ "role": "
|
|
67
|
+
{ "role": "textbox", "text": "Name" },
|
|
63
68
|
{ "role": "link", "text": "Forgot your password?" },
|
|
64
69
|
{ "role": "link", "text": "Sign Up" },
|
|
65
70
|
{ "role": "button", "text": "Sign In" },
|
|
@@ -105,7 +110,7 @@ const locatorStrategyRule = dedent`
|
|
|
105
110
|
</xpath_rules>
|
|
106
111
|
|
|
107
112
|
<good locator example>
|
|
108
|
-
'
|
|
113
|
+
'input[placeholder="Name"]'
|
|
109
114
|
'[aria-label="Name"]'
|
|
110
115
|
'form#user_form input[name="name"]'
|
|
111
116
|
'#content-top #user_name'
|
|
@@ -245,7 +250,8 @@ export const unexpectedPopupRule = dedent`
|
|
|
245
250
|
|
|
246
251
|
export const sectionContextRule = dedent`
|
|
247
252
|
<section_context_rule>
|
|
248
|
-
|
|
253
|
+
Use a container from UI map sections when the target may match several elements. A container that is
|
|
254
|
+
wrong or stale guarantees failure, so prefer a verified one and keep a containerless fallback.
|
|
249
255
|
|
|
250
256
|
1. Identify which section contains the target element
|
|
251
257
|
2. Get the Context Locator from that section in the UI map
|
|
@@ -263,9 +269,10 @@ export const sectionContextRule = dedent`
|
|
|
263
269
|
For CSS locators - prepend section context:
|
|
264
270
|
- I.click('.main button.submit') // instead of I.click('button.submit')
|
|
265
271
|
|
|
266
|
-
|
|
272
|
+
Omit context when the locator already resolves to one element:
|
|
267
273
|
- Locator is XPath (already includes path context)
|
|
268
274
|
- Locator is a unique ID (#specific-element)
|
|
275
|
+
- Text or ARIA that appears only once on the page
|
|
269
276
|
</section_context_rule>
|
|
270
277
|
|
|
271
278
|
${unexpectedPopupRule}
|
package/src/ai/task-agent.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ActionResult } from '../action-result.js';
|
|
2
2
|
import type { ExplorbotConfig } from '../config.ts';
|
|
3
|
+
import { executionController } from '../execution-controller.ts';
|
|
3
4
|
import type { ExperienceTracker } from '../experience-tracker.js';
|
|
4
5
|
import type Explorer from '../explorer.ts';
|
|
5
6
|
import type { KnowledgeTracker } from '../knowledge-tracker.js';
|
|
@@ -12,7 +13,8 @@ import type { Provider } from './provider.js';
|
|
|
12
13
|
import { Quartermaster } from './quartermaster.js';
|
|
13
14
|
|
|
14
15
|
export function isInteractive(): boolean {
|
|
15
|
-
|
|
16
|
+
if (process.env.INK_RUNNING === 'true') return true;
|
|
17
|
+
return executionController.hasInputCallback();
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
function createNullProxy<T extends object>(): T {
|
package/src/ai/tester.ts
CHANGED
|
@@ -11,7 +11,7 @@ import { Observability } from '../observability.ts';
|
|
|
11
11
|
import type { StateTransition } from '../state-manager.ts';
|
|
12
12
|
import { Stats } from '../stats.ts';
|
|
13
13
|
import { type Test, TestResult, type TestResultType } from '../test-plan.ts';
|
|
14
|
-
import {
|
|
14
|
+
import { compactAriaSnapshot, detectFocusArea } from '../utils/aria.ts';
|
|
15
15
|
import { ErrorPageError, isErrorPage } from '../utils/error-page.ts';
|
|
16
16
|
import { createDebug, tag } from '../utils/logger.ts';
|
|
17
17
|
import { loop } from '../utils/loop.ts';
|
|
@@ -24,7 +24,7 @@ import { Provider } from './provider.ts';
|
|
|
24
24
|
import { Researcher } from './researcher.ts';
|
|
25
25
|
import { actionRule, capabilityGroundingRule, dataProtectionRules, focusedElementRule, formRequirementsRule, locatorRule, multipleTabsRule, sectionContextRule } from './rules.ts';
|
|
26
26
|
import { TaskAgent } from './task-agent.ts';
|
|
27
|
-
import { createCodeceptJSTools, createIframeTools } from './tools.ts';
|
|
27
|
+
import { createCodeceptJSTools, createIframeTools, withdrawVisionTools } from './tools.ts';
|
|
28
28
|
|
|
29
29
|
const debugLog = createDebug('explorbot:tester');
|
|
30
30
|
|
|
@@ -62,7 +62,6 @@ export class Tester extends TaskAgent implements Agent {
|
|
|
62
62
|
private seenUiMapUrls = new Set<string>();
|
|
63
63
|
private lastAnalyzedStateHash: string | null = null;
|
|
64
64
|
private stalledIterations = 0;
|
|
65
|
-
private hasSuccessfulAssertion = false;
|
|
66
65
|
private readonly MAX_STALLED_ITERATIONS = 3;
|
|
67
66
|
|
|
68
67
|
constructor(deps: AgentDeps, researcher: Researcher, navigator: Navigator, agentTools?: any) {
|
|
@@ -111,7 +110,6 @@ export class Tester extends TaskAgent implements Agent {
|
|
|
111
110
|
this.seenUiMapUrls.clear();
|
|
112
111
|
this.lastAnalyzedStateHash = null;
|
|
113
112
|
this.stalledIterations = 0;
|
|
114
|
-
this.hasSuccessfulAssertion = false;
|
|
115
113
|
this.stateManager.clearHistory();
|
|
116
114
|
this.resetFailureCount();
|
|
117
115
|
this.pilot?.reset();
|
|
@@ -119,7 +117,7 @@ export class Tester extends TaskAgent implements Agent {
|
|
|
119
117
|
const requestStore = this.requestStore;
|
|
120
118
|
requestStore.clear();
|
|
121
119
|
const offFailedRequest = requestStore.onFailedRequest((r) => {
|
|
122
|
-
task.
|
|
120
|
+
task.addObservation(`Network error: ${r.method} ${r.path} → ${r.status}`);
|
|
123
121
|
});
|
|
124
122
|
|
|
125
123
|
const initialState = ActionResult.fromState(state);
|
|
@@ -247,6 +245,7 @@ export class Tester extends TaskAgent implements Agent {
|
|
|
247
245
|
if (currentState.isInsideIframe) {
|
|
248
246
|
Object.assign(tools, createIframeTools(this.toolDeps));
|
|
249
247
|
}
|
|
248
|
+
withdrawVisionTools(tools);
|
|
250
249
|
|
|
251
250
|
debugLog(`Test ${task.scenario} iteration ${iteration}`);
|
|
252
251
|
|
|
@@ -314,17 +313,9 @@ export class Tester extends TaskAgent implements Agent {
|
|
|
314
313
|
const allToolNames = result?.toolExecutions?.map((execution: any) => execution.toolName) || [];
|
|
315
314
|
const successfulToolNames = result?.toolExecutions?.filter((execution: any) => execution.wasSuccessful)?.map((execution: any) => execution.toolName) || [];
|
|
316
315
|
const actionPerformed = !!allToolNames.find((toolName: string) => this.ACTION_TOOLS.includes(toolName));
|
|
317
|
-
const successfulActionPerformed = !!successfulToolNames.find((toolName: string) => this.ACTION_TOOLS.includes(toolName));
|
|
318
316
|
assertionPerformed = !!successfulToolNames.find((toolName: string) => this.ASSERTION_TOOLS.includes(toolName));
|
|
319
317
|
const wasSuccessful = result?.toolExecutions?.every((execution: any) => execution.wasSuccessful);
|
|
320
318
|
|
|
321
|
-
if (successfulActionPerformed) {
|
|
322
|
-
this.hasSuccessfulAssertion = false;
|
|
323
|
-
}
|
|
324
|
-
if (assertionPerformed) {
|
|
325
|
-
this.hasSuccessfulAssertion = true;
|
|
326
|
-
}
|
|
327
|
-
|
|
328
319
|
this.trackToolExecutions(result?.toolExecutions || []);
|
|
329
320
|
|
|
330
321
|
if (this.consecutiveEmptyResults >= 5) {
|
|
@@ -416,6 +407,7 @@ export class Tester extends TaskAgent implements Agent {
|
|
|
416
407
|
if (extensions >= this.MAX_EXTENSIONS) break;
|
|
417
408
|
|
|
418
409
|
extensions++;
|
|
410
|
+
this.stalledIterations = 0;
|
|
419
411
|
tag('info').log(`Pilot extending test (${extensions}/${this.MAX_EXTENSIONS})`);
|
|
420
412
|
conversation.cleanupTag('page_aria', '...trimmed...', 1);
|
|
421
413
|
conversation.cleanupTag('page_html', '...trimmed...', 0);
|
|
@@ -474,13 +466,7 @@ export class Tester extends TaskAgent implements Agent {
|
|
|
474
466
|
this.stalledIterations++;
|
|
475
467
|
if (this.stalledIterations < this.MAX_STALLED_ITERATIONS) return false;
|
|
476
468
|
|
|
477
|
-
|
|
478
|
-
task.addNote('No further browser progress after successful verification; requesting final review');
|
|
479
|
-
return true;
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
task.addNote('No browser progress after repeated attempts on unchanged page', TestResult.FAILED);
|
|
483
|
-
task.finish(TestResult.FAILED);
|
|
469
|
+
task.addNote('No further browser progress on unchanged page; requesting final review');
|
|
484
470
|
return true;
|
|
485
471
|
}
|
|
486
472
|
|
|
@@ -500,7 +486,8 @@ export class Tester extends TaskAgent implements Agent {
|
|
|
500
486
|
`;
|
|
501
487
|
|
|
502
488
|
if (task.getPrintableNotes()) {
|
|
503
|
-
outcomeStatus
|
|
489
|
+
outcomeStatus += dedent`
|
|
490
|
+
|
|
504
491
|
Your current log:
|
|
505
492
|
<notes>
|
|
506
493
|
${task.notesToString()}
|
|
@@ -529,7 +516,7 @@ export class Tester extends TaskAgent implements Agent {
|
|
|
529
516
|
|
|
530
517
|
const focusArea = detectFocusArea(currentState.ariaSnapshot);
|
|
531
518
|
|
|
532
|
-
const focusedElement =
|
|
519
|
+
const focusedElement = currentState.focusedElement;
|
|
533
520
|
if (focusedElement) {
|
|
534
521
|
const isTextInput = ['textbox', 'combobox', 'searchbox'].includes(focusedElement.role);
|
|
535
522
|
context += dedent`
|
|
@@ -611,7 +598,7 @@ export class Tester extends TaskAgent implements Agent {
|
|
|
611
598
|
</page>
|
|
612
599
|
|
|
613
600
|
<page_aria>
|
|
614
|
-
${
|
|
601
|
+
${await this.interactiveAriaWithRefs(currentState)}
|
|
615
602
|
</page_aria>
|
|
616
603
|
${uiMapSection}
|
|
617
604
|
|
|
@@ -648,25 +635,32 @@ export class Tester extends TaskAgent implements Agent {
|
|
|
648
635
|
</page>
|
|
649
636
|
|
|
650
637
|
<page_aria>
|
|
651
|
-
${
|
|
638
|
+
${await this.interactiveAriaWithRefs(currentState)}
|
|
652
639
|
</page_aria>
|
|
653
640
|
`;
|
|
654
641
|
}
|
|
655
642
|
|
|
643
|
+
private async interactiveAriaWithRefs(state: ActionResult): Promise<string> {
|
|
644
|
+
const withRefs = await Promise.resolve(this.explorer?.withPage?.((page: any) => page.locator('body').ariaSnapshot({ mode: 'ai' }))).catch(() => null);
|
|
645
|
+
if (!withRefs) return state.getInteractiveARIA();
|
|
646
|
+
return compactAriaSnapshot(withRefs, false);
|
|
647
|
+
}
|
|
648
|
+
|
|
656
649
|
private finishTest(task: Test): void {
|
|
657
|
-
if (!task.
|
|
658
|
-
task.finish(TestResult.
|
|
650
|
+
if (!task.result) {
|
|
651
|
+
if (task.hasAchievedAll()) task.finish(TestResult.PASSED);
|
|
652
|
+
else task.finish(TestResult.FAILED);
|
|
659
653
|
}
|
|
660
654
|
|
|
661
655
|
if (task.isSuccessful) {
|
|
662
656
|
tag('success').log(`Successful test: ${task.scenario}`);
|
|
663
|
-
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
if (task.isSkipped) {
|
|
664
660
|
tag('warning').log(`Skipped test: ${task.scenario}`);
|
|
665
|
-
|
|
666
|
-
tag('error').log(`Failed test: ${task.scenario}`);
|
|
667
|
-
} else {
|
|
668
|
-
tag('warning').log(`Test with no result: ${task.scenario}`);
|
|
661
|
+
return;
|
|
669
662
|
}
|
|
663
|
+
tag('error').log(`Failed test: ${task.scenario}`);
|
|
670
664
|
}
|
|
671
665
|
|
|
672
666
|
private async abortStartedTestOnErrorPage(task: Test, actionResult: ActionResult): Promise<{ success: boolean }> {
|
|
@@ -810,6 +804,9 @@ export class Tester extends TaskAgent implements Agent {
|
|
|
810
804
|
${task.expected.map((e) => `- ${e}`).join('\n')}
|
|
811
805
|
</expected_results>
|
|
812
806
|
|
|
807
|
+
An expected result counts as settled only when you record it back word for word as it is written above.
|
|
808
|
+
A note in your own wording is a general note and leaves that result unsettled.
|
|
809
|
+
|
|
813
810
|
Your goal is to perform actions on the web page and verify the expected outcomes.
|
|
814
811
|
Try to achieve as many goals as possible.
|
|
815
812
|
If goal is not achievable, log that and skip to next one.
|
|
@@ -1059,6 +1056,9 @@ export class Tester extends TaskAgent implements Agent {
|
|
|
1059
1056
|
- You unsuccessfully tried multiple iterations and failed
|
|
1060
1057
|
- If the expected result was expected to fail, use status="success" instead
|
|
1061
1058
|
|
|
1059
|
+
When a note settles one of the expected results, that note must repeat the expected result word
|
|
1060
|
+
for word. Paraphrasing it leaves the expected result unsettled and it is reported as unverified.
|
|
1061
|
+
|
|
1062
1062
|
Example:
|
|
1063
1063
|
- record({ notes: ["clicked login button", "login form appeared", "fill credentials"], status: "success" })
|
|
1064
1064
|
`,
|
|
@@ -1143,8 +1143,7 @@ export class Tester extends TaskAgent implements Agent {
|
|
|
1143
1143
|
this.stalledIterations++;
|
|
1144
1144
|
if (this.stalledIterations < this.MAX_STALLED_ITERATIONS) return false;
|
|
1145
1145
|
|
|
1146
|
-
task.addNote('No browser progress after repeated execution errors'
|
|
1147
|
-
task.finish(TestResult.FAILED);
|
|
1146
|
+
task.addNote('No browser progress after repeated execution errors; requesting final review');
|
|
1148
1147
|
return true;
|
|
1149
1148
|
}
|
|
1150
1149
|
|