explorbot 0.1.21 → 0.1.22
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 +3 -3
- package/dist/src/action-result.js +12 -0
- package/dist/src/ai/navigator.js +97 -37
- package/dist/src/ai/pilot.js +6 -0
- package/dist/src/commands/init-command.js +9 -0
- package/dist/src/reporter.js +5 -7
- package/package.json +3 -3
- package/src/action-result.ts +11 -0
- package/src/ai/navigator.ts +104 -39
- package/src/ai/pilot.ts +6 -0
- package/src/commands/init-command.ts +9 -0
- package/src/config.ts +2 -0
- package/src/reporter.ts +4 -7
package/dist/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "explorbot",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.22",
|
|
4
4
|
"description": "CLI app built with React Ink, CodeceptJS, and Playwright",
|
|
5
5
|
"license": "Elastic-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -82,7 +82,7 @@
|
|
|
82
82
|
"@opentelemetry/sdk-trace-base": "^2.2.0",
|
|
83
83
|
"@opentelemetry/semantic-conventions": "^1.38.0",
|
|
84
84
|
"@scalar/openapi-parser": "^0.25.6",
|
|
85
|
-
"@testomatio/reporter": "^2.
|
|
85
|
+
"@testomatio/reporter": "^2.8.4",
|
|
86
86
|
"ai": "^6.0.6",
|
|
87
87
|
"axe-core": "^4.11.1",
|
|
88
88
|
"bash-tool": "^1.3.15",
|
|
@@ -108,7 +108,7 @@
|
|
|
108
108
|
"micromatch": "^4.0.8",
|
|
109
109
|
"ora-classic": "^5.4.2",
|
|
110
110
|
"parse5": "^8.0.0",
|
|
111
|
-
"playwright": "^1.
|
|
111
|
+
"playwright": "^1.60",
|
|
112
112
|
"react": "^19.1.1",
|
|
113
113
|
"strip-ansi": "^7.1.2",
|
|
114
114
|
"turndown": "^7.2.1",
|
|
@@ -151,6 +151,18 @@ export class ActionResult {
|
|
|
151
151
|
this.verifications ??= {};
|
|
152
152
|
this.verifications[assertion] = passed;
|
|
153
153
|
}
|
|
154
|
+
getVerification(message) {
|
|
155
|
+
if (!this.verifications)
|
|
156
|
+
return null;
|
|
157
|
+
if (typeof message === 'string') {
|
|
158
|
+
return this.verifications[message] ?? null;
|
|
159
|
+
}
|
|
160
|
+
for (const [assertion, passed] of Object.entries(this.verifications)) {
|
|
161
|
+
if (message.test(assertion))
|
|
162
|
+
return passed;
|
|
163
|
+
}
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
154
166
|
isSameUrl(state) {
|
|
155
167
|
if (!this.url || this.url === '') {
|
|
156
168
|
return false;
|
package/dist/src/ai/navigator.js
CHANGED
|
@@ -70,6 +70,12 @@ class Navigator {
|
|
|
70
70
|
this.experienceTracker = experienceTracker || new ExperienceTracker();
|
|
71
71
|
this.hooksRunner = new HooksRunner(explorer, explorer.getConfig());
|
|
72
72
|
}
|
|
73
|
+
get verifyAttempts() {
|
|
74
|
+
return this.explorer.getConfig().ai?.agents?.navigator?.verifyAttempts ?? 3;
|
|
75
|
+
}
|
|
76
|
+
get verifyTimeout() {
|
|
77
|
+
return this.explorer.getConfig().ai?.agents?.navigator?.verifyTimeout ?? 1500;
|
|
78
|
+
}
|
|
73
79
|
getBaseOrigin() {
|
|
74
80
|
const baseUrl = this.explorer.getConfig().playwright.url;
|
|
75
81
|
try {
|
|
@@ -565,6 +571,11 @@ class Navigator {
|
|
|
565
571
|
async verifyState(message, actionResult) {
|
|
566
572
|
tag('info').log('AI Navigator verifying state at', actionResult.url);
|
|
567
573
|
debugLog('Verification message:', message);
|
|
574
|
+
const cachedVerification = actionResult.getVerification(message);
|
|
575
|
+
if (cachedVerification !== null) {
|
|
576
|
+
tag('substep').log(`Reusing cached verification: ${cachedVerification ? 'PASS' : 'FAIL'}`);
|
|
577
|
+
return { verified: cachedVerification, successfulCodes: [], assertionSteps: [], totalAttempted: 0 };
|
|
578
|
+
}
|
|
568
579
|
let knowledge = '';
|
|
569
580
|
let experience = '';
|
|
570
581
|
const relevantKnowledge = this.knowledgeTracker.getRelevantKnowledge(actionResult);
|
|
@@ -584,6 +595,20 @@ class Navigator {
|
|
|
584
595
|
experience = renderExperienceToc(toc);
|
|
585
596
|
}
|
|
586
597
|
}
|
|
598
|
+
const priorVerifications = Object.entries(actionResult.verifications ?? {});
|
|
599
|
+
let verificationContext = '';
|
|
600
|
+
if (priorVerifications.length > 0) {
|
|
601
|
+
const lines = priorVerifications.map(([claim, passed]) => `- "${claim}" → ${passed ? 'passed' : 'failed'}`).join('\n');
|
|
602
|
+
verificationContext = dedent `
|
|
603
|
+
<already_verified>
|
|
604
|
+
These claims were already checked on this page:
|
|
605
|
+
${lines}
|
|
606
|
+
|
|
607
|
+
If the claim to verify has the same meaning as one above (even if worded differently), do NOT write any assertion code.
|
|
608
|
+
Respond with a single line and nothing else: ALREADY_VERIFIED: <exact text of the matching claim>
|
|
609
|
+
</already_verified>
|
|
610
|
+
`;
|
|
611
|
+
}
|
|
587
612
|
const prompt = dedent `
|
|
588
613
|
<message>
|
|
589
614
|
${message}
|
|
@@ -597,11 +622,13 @@ class Navigator {
|
|
|
597
622
|
</page_html>
|
|
598
623
|
</page>
|
|
599
624
|
|
|
625
|
+
${verificationContext}
|
|
626
|
+
|
|
600
627
|
<task>
|
|
601
628
|
Identify what assertion the user wants to verify on the page.
|
|
602
|
-
Propose
|
|
629
|
+
Propose 2-3 strong, distinct CodeceptJS assertion code blocks that each directly prove the claim.
|
|
603
630
|
Use only data from the <page> context to plan the verification.
|
|
604
|
-
|
|
631
|
+
Prefer the fewest, most specific assertions over many variants of the same locator.
|
|
605
632
|
|
|
606
633
|
IMPORTANT: Each code block must verify the SPECIFIC claim in the message, not just a generic aspect of it.
|
|
607
634
|
Bad: I.seeElement({"role":"button","aria-pressed":"true"}) — matches ANY button, not the specific one
|
|
@@ -620,50 +647,83 @@ class Navigator {
|
|
|
620
647
|
tag('debug').log('Prompt:', prompt);
|
|
621
648
|
const conversation = this.provider.startConversation(this.systemPrompt, 'navigator');
|
|
622
649
|
conversation.addUserText(prompt);
|
|
650
|
+
let alreadyVerified = false;
|
|
623
651
|
const tools = this.buildExperienceTools();
|
|
624
652
|
let codeBlocks = [];
|
|
625
653
|
const successfulCodes = [];
|
|
626
654
|
const assertionSteps = [];
|
|
627
655
|
const action = this.explorer.createAction();
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
656
|
+
let failures = 0;
|
|
657
|
+
const page = this.explorer.playwrightHelper?.page;
|
|
658
|
+
const originalTimeout = this.explorer.playwrightHelper?.options?.timeout ?? 3000;
|
|
659
|
+
page?.setDefaultTimeout(this.verifyTimeout);
|
|
660
|
+
try {
|
|
661
|
+
await loop(async ({ stop, iteration }) => {
|
|
662
|
+
if (codeBlocks.length === 0) {
|
|
663
|
+
const result = await this.provider.invokeConversation(conversation, tools);
|
|
664
|
+
if (!result)
|
|
665
|
+
return;
|
|
666
|
+
const aiResponse = result?.response?.text ?? '';
|
|
667
|
+
debugLog('Received AI response:', aiResponse.length, 'characters');
|
|
668
|
+
tag('step').log('Verifying assertion...');
|
|
669
|
+
if (this.checkAlreadyVerified(aiResponse, actionResult)) {
|
|
670
|
+
alreadyVerified = true;
|
|
671
|
+
stop();
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
codeBlocks = extractCodeBlocks(aiResponse);
|
|
675
|
+
}
|
|
676
|
+
if (codeBlocks.length === 0) {
|
|
632
677
|
return;
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
678
|
+
}
|
|
679
|
+
const codeBlock = codeBlocks[iteration - 1];
|
|
680
|
+
if (!codeBlock) {
|
|
681
|
+
stop();
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
await this.explorer.switchToMainFrame();
|
|
685
|
+
const verified = await action.attempt(codeBlock, message, false);
|
|
686
|
+
if (verified) {
|
|
687
|
+
tag('success').log('Verification passed');
|
|
688
|
+
successfulCodes.push(codeBlock);
|
|
689
|
+
assertionSteps.push(...action.assertionSteps);
|
|
690
|
+
}
|
|
691
|
+
else {
|
|
692
|
+
failures++;
|
|
693
|
+
}
|
|
694
|
+
const target = Math.min(codeBlocks.length, this.verifyAttempts);
|
|
695
|
+
const majorityNeeded = Math.floor(target / 2) + 1;
|
|
696
|
+
if (successfulCodes.length >= majorityNeeded || failures > target - majorityNeeded) {
|
|
697
|
+
stop();
|
|
698
|
+
}
|
|
699
|
+
}, {
|
|
700
|
+
maxAttempts: this.verifyAttempts,
|
|
701
|
+
observability: {
|
|
702
|
+
agent: 'navigator',
|
|
703
|
+
},
|
|
704
|
+
catch: async (error) => {
|
|
705
|
+
debugLog(error);
|
|
706
|
+
},
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
finally {
|
|
710
|
+
page?.setDefaultTimeout(originalTimeout);
|
|
711
|
+
}
|
|
712
|
+
const totalAttempted = Math.min(codeBlocks.length, this.verifyAttempts);
|
|
713
|
+
const majorityNeeded = Math.floor(totalAttempted / 2) + 1;
|
|
714
|
+
let verified = successfulCodes.length >= majorityNeeded;
|
|
715
|
+
if (alreadyVerified)
|
|
716
|
+
verified = true;
|
|
664
717
|
actionResult.addVerification(message, verified);
|
|
665
718
|
this.explorer.getStateManager().updateState(actionResult);
|
|
666
719
|
return { verified, successfulCodes, assertionSteps, totalAttempted };
|
|
667
720
|
}
|
|
721
|
+
checkAlreadyVerified(aiResponse, actionResult) {
|
|
722
|
+
const verifiedMatch = aiResponse.match(/ALREADY_VERIFIED:\s*(.+)/i);
|
|
723
|
+
if (!verifiedMatch)
|
|
724
|
+
return false;
|
|
725
|
+
const claim = verifiedMatch[1].trim().replace(/^["']|["']$/g, '');
|
|
726
|
+
return actionResult.getVerification(claim) === true;
|
|
727
|
+
}
|
|
668
728
|
}
|
|
669
729
|
export { Navigator };
|
package/dist/src/ai/pilot.js
CHANGED
|
@@ -279,6 +279,12 @@ export class Pilot {
|
|
|
279
279
|
- "Delete X" → X must be gone. Clicking delete is NOT enough.
|
|
280
280
|
- "Edit X" → updated value must be persisted (visible in list/detail). Opening edit is NOT enough; redirect after save with the new value visible IS enough.
|
|
281
281
|
- Negative tests ("without a name", "invalid", "duplicate", "unauthorized") → success means the system PREVENTED the action with validation/error.
|
|
282
|
+
- Navigation-prefixed titles ("Access/Open/Go to X to <do Y>") → the goal is <do Y>; reaching X is
|
|
283
|
+
only a milestone. A satisfied milestone (tab active, panel/prompt visible, list shown) is NEVER a pass
|
|
284
|
+
if <do Y> did not occur this run.
|
|
285
|
+
- If the page reveals the goal cannot be performed here — required control absent, integration not
|
|
286
|
+
connected, or only a setup/connect/empty-state prompt is shown — vote "skipped" (prerequisites unmet),
|
|
287
|
+
never "pass".
|
|
282
288
|
|
|
283
289
|
PROVENANCE: the entity you cite as proof must appear by name in <notes> or
|
|
284
290
|
<session_log> tool inputs for THIS run. Name absent from tester activity = stale
|
|
@@ -29,6 +29,15 @@ const config = {
|
|
|
29
29
|
// agentic model for decision making
|
|
30
30
|
agenticModel: openrouter('minimax/minimax-m2.5:nitro'),
|
|
31
31
|
},
|
|
32
|
+
|
|
33
|
+
reporter: {
|
|
34
|
+
// Save a local HTML report after each run.
|
|
35
|
+
html: true,
|
|
36
|
+
// Save a local markdown report after each run.
|
|
37
|
+
markdown: true,
|
|
38
|
+
// Group runs by title in Testomat.io / HTML reports. Defaults to today's date — customize or remove.
|
|
39
|
+
runGroup: new Date().toISOString().slice(0, 10),
|
|
40
|
+
},
|
|
32
41
|
};
|
|
33
42
|
|
|
34
43
|
export default config;
|
package/dist/src/reporter.js
CHANGED
|
@@ -11,7 +11,7 @@ export class Reporter {
|
|
|
11
11
|
constructor(config, stateManager) {
|
|
12
12
|
this.reporterEnabled = Reporter.resolveEnabled(config);
|
|
13
13
|
this.stateManager = stateManager;
|
|
14
|
-
if (this.reporterEnabled &&
|
|
14
|
+
if (this.reporterEnabled && config?.html) {
|
|
15
15
|
this.configureHtmlPipe();
|
|
16
16
|
}
|
|
17
17
|
if (this.reporterEnabled && config?.markdown) {
|
|
@@ -45,6 +45,8 @@ export class Reporter {
|
|
|
45
45
|
return true;
|
|
46
46
|
if (config?.enabled === false)
|
|
47
47
|
return false;
|
|
48
|
+
if (config?.html || config?.markdown)
|
|
49
|
+
return true;
|
|
48
50
|
return Boolean(process.env.TESTOMATIO);
|
|
49
51
|
}
|
|
50
52
|
configureHtmlPipe() {
|
|
@@ -68,13 +70,9 @@ export class Reporter {
|
|
|
68
70
|
configureRunGroup(runGroup) {
|
|
69
71
|
if (process.env.TESTOMATIO_RUNGROUP_TITLE)
|
|
70
72
|
return;
|
|
71
|
-
if (runGroup
|
|
72
|
-
return;
|
|
73
|
-
if (runGroup) {
|
|
74
|
-
process.env.TESTOMATIO_RUNGROUP_TITLE = runGroup;
|
|
73
|
+
if (!runGroup)
|
|
75
74
|
return;
|
|
76
|
-
|
|
77
|
-
process.env.TESTOMATIO_RUNGROUP_TITLE = `Explorbot ${new Date().toISOString().slice(0, 10)}`;
|
|
75
|
+
process.env.TESTOMATIO_RUNGROUP_TITLE = runGroup;
|
|
78
76
|
}
|
|
79
77
|
async startRun() {
|
|
80
78
|
if (this.isRunStarted) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "explorbot",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.22",
|
|
4
4
|
"description": "CLI app built with React Ink, CodeceptJS, and Playwright",
|
|
5
5
|
"license": "Elastic-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -82,7 +82,7 @@
|
|
|
82
82
|
"@opentelemetry/sdk-trace-base": "^2.2.0",
|
|
83
83
|
"@opentelemetry/semantic-conventions": "^1.38.0",
|
|
84
84
|
"@scalar/openapi-parser": "^0.25.6",
|
|
85
|
-
"@testomatio/reporter": "^2.
|
|
85
|
+
"@testomatio/reporter": "^2.8.4",
|
|
86
86
|
"ai": "^6.0.6",
|
|
87
87
|
"axe-core": "^4.11.1",
|
|
88
88
|
"bash-tool": "^1.3.15",
|
|
@@ -108,7 +108,7 @@
|
|
|
108
108
|
"micromatch": "^4.0.8",
|
|
109
109
|
"ora-classic": "^5.4.2",
|
|
110
110
|
"parse5": "^8.0.0",
|
|
111
|
-
"playwright": "^1.
|
|
111
|
+
"playwright": "^1.60",
|
|
112
112
|
"react": "^19.1.1",
|
|
113
113
|
"strip-ansi": "^7.1.2",
|
|
114
114
|
"turndown": "^7.2.1",
|
package/src/action-result.ts
CHANGED
|
@@ -207,6 +207,17 @@ export class ActionResult implements ActionResultData {
|
|
|
207
207
|
this.verifications[assertion] = passed;
|
|
208
208
|
}
|
|
209
209
|
|
|
210
|
+
getVerification(message: string | RegExp): boolean | null {
|
|
211
|
+
if (!this.verifications) return null;
|
|
212
|
+
if (typeof message === 'string') {
|
|
213
|
+
return this.verifications[message] ?? null;
|
|
214
|
+
}
|
|
215
|
+
for (const [assertion, passed] of Object.entries(this.verifications)) {
|
|
216
|
+
if (message.test(assertion)) return passed;
|
|
217
|
+
}
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
|
|
210
221
|
isSameUrl(state: WebPageState): boolean {
|
|
211
222
|
if (!this.url || this.url === '') {
|
|
212
223
|
return false;
|
package/src/ai/navigator.ts
CHANGED
|
@@ -82,6 +82,14 @@ class Navigator implements Agent {
|
|
|
82
82
|
this.hooksRunner = new HooksRunner(explorer, explorer.getConfig());
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
private get verifyAttempts(): number {
|
|
86
|
+
return this.explorer.getConfig().ai?.agents?.navigator?.verifyAttempts ?? 3;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
private get verifyTimeout(): number {
|
|
90
|
+
return this.explorer.getConfig().ai?.agents?.navigator?.verifyTimeout ?? 1500;
|
|
91
|
+
}
|
|
92
|
+
|
|
85
93
|
private getBaseOrigin(): string | null {
|
|
86
94
|
const baseUrl = this.explorer.getConfig().playwright.url;
|
|
87
95
|
try {
|
|
@@ -623,6 +631,12 @@ class Navigator implements Agent {
|
|
|
623
631
|
tag('info').log('AI Navigator verifying state at', actionResult.url);
|
|
624
632
|
debugLog('Verification message:', message);
|
|
625
633
|
|
|
634
|
+
const cachedVerification = actionResult.getVerification(message);
|
|
635
|
+
if (cachedVerification !== null) {
|
|
636
|
+
tag('substep').log(`Reusing cached verification: ${cachedVerification ? 'PASS' : 'FAIL'}`);
|
|
637
|
+
return { verified: cachedVerification, successfulCodes: [], assertionSteps: [], totalAttempted: 0 };
|
|
638
|
+
}
|
|
639
|
+
|
|
626
640
|
let knowledge = '';
|
|
627
641
|
let experience = '';
|
|
628
642
|
|
|
@@ -645,6 +659,21 @@ class Navigator implements Agent {
|
|
|
645
659
|
}
|
|
646
660
|
}
|
|
647
661
|
|
|
662
|
+
const priorVerifications = Object.entries(actionResult.verifications ?? {});
|
|
663
|
+
let verificationContext = '';
|
|
664
|
+
if (priorVerifications.length > 0) {
|
|
665
|
+
const lines = priorVerifications.map(([claim, passed]) => `- "${claim}" → ${passed ? 'passed' : 'failed'}`).join('\n');
|
|
666
|
+
verificationContext = dedent`
|
|
667
|
+
<already_verified>
|
|
668
|
+
These claims were already checked on this page:
|
|
669
|
+
${lines}
|
|
670
|
+
|
|
671
|
+
If the claim to verify has the same meaning as one above (even if worded differently), do NOT write any assertion code.
|
|
672
|
+
Respond with a single line and nothing else: ALREADY_VERIFIED: <exact text of the matching claim>
|
|
673
|
+
</already_verified>
|
|
674
|
+
`;
|
|
675
|
+
}
|
|
676
|
+
|
|
648
677
|
const prompt = dedent`
|
|
649
678
|
<message>
|
|
650
679
|
${message}
|
|
@@ -658,11 +687,13 @@ class Navigator implements Agent {
|
|
|
658
687
|
</page_html>
|
|
659
688
|
</page>
|
|
660
689
|
|
|
690
|
+
${verificationContext}
|
|
691
|
+
|
|
661
692
|
<task>
|
|
662
693
|
Identify what assertion the user wants to verify on the page.
|
|
663
|
-
Propose
|
|
694
|
+
Propose 2-3 strong, distinct CodeceptJS assertion code blocks that each directly prove the claim.
|
|
664
695
|
Use only data from the <page> context to plan the verification.
|
|
665
|
-
|
|
696
|
+
Prefer the fewest, most specific assertions over many variants of the same locator.
|
|
666
697
|
|
|
667
698
|
IMPORTANT: Each code block must verify the SPECIFIC claim in the message, not just a generic aspect of it.
|
|
668
699
|
Bad: I.seeElement({"role":"button","aria-pressed":"true"}) — matches ANY button, not the specific one
|
|
@@ -684,6 +715,7 @@ class Navigator implements Agent {
|
|
|
684
715
|
const conversation = this.provider.startConversation(this.systemPrompt, 'navigator');
|
|
685
716
|
conversation.addUserText(prompt);
|
|
686
717
|
|
|
718
|
+
let alreadyVerified = false;
|
|
687
719
|
const tools = this.buildExperienceTools();
|
|
688
720
|
|
|
689
721
|
let codeBlocks: string[] = [];
|
|
@@ -691,57 +723,90 @@ class Navigator implements Agent {
|
|
|
691
723
|
const assertionSteps: Array<{ name: string; args: any[] }> = [];
|
|
692
724
|
|
|
693
725
|
const action = this.explorer.createAction();
|
|
726
|
+
let failures = 0;
|
|
694
727
|
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
const result = await this.provider.invokeConversation(conversation, tools);
|
|
699
|
-
if (!result) return;
|
|
700
|
-
const aiResponse = result?.response?.text;
|
|
701
|
-
debugLog('Received AI response:', aiResponse?.length ?? 0, 'characters');
|
|
702
|
-
tag('step').log('Verifying assertion...');
|
|
703
|
-
codeBlocks = extractCodeBlocks(aiResponse ?? '');
|
|
704
|
-
}
|
|
728
|
+
const page = this.explorer.playwrightHelper?.page;
|
|
729
|
+
const originalTimeout = this.explorer.playwrightHelper?.options?.timeout ?? 3000;
|
|
730
|
+
page?.setDefaultTimeout(this.verifyTimeout);
|
|
705
731
|
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
}
|
|
732
|
+
try {
|
|
733
|
+
await loop(
|
|
734
|
+
async ({ stop, iteration }) => {
|
|
735
|
+
if (codeBlocks.length === 0) {
|
|
736
|
+
const result = await this.provider.invokeConversation(conversation, tools);
|
|
737
|
+
if (!result) return;
|
|
738
|
+
const aiResponse = result?.response?.text ?? '';
|
|
739
|
+
debugLog('Received AI response:', aiResponse.length, 'characters');
|
|
740
|
+
tag('step').log('Verifying assertion...');
|
|
741
|
+
|
|
742
|
+
if (this.checkAlreadyVerified(aiResponse, actionResult)) {
|
|
743
|
+
alreadyVerified = true;
|
|
744
|
+
stop();
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
709
747
|
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
stop();
|
|
713
|
-
return;
|
|
714
|
-
}
|
|
748
|
+
codeBlocks = extractCodeBlocks(aiResponse);
|
|
749
|
+
}
|
|
715
750
|
|
|
716
|
-
|
|
751
|
+
if (codeBlocks.length === 0) {
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
717
754
|
|
|
718
|
-
|
|
755
|
+
const codeBlock = codeBlocks[iteration - 1];
|
|
756
|
+
if (!codeBlock) {
|
|
757
|
+
stop();
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
719
760
|
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
761
|
+
await this.explorer.switchToMainFrame();
|
|
762
|
+
|
|
763
|
+
const verified = await action.attempt(codeBlock, message, false);
|
|
764
|
+
|
|
765
|
+
if (verified) {
|
|
766
|
+
tag('success').log('Verification passed');
|
|
767
|
+
successfulCodes.push(codeBlock);
|
|
768
|
+
assertionSteps.push(...action.assertionSteps);
|
|
769
|
+
} else {
|
|
770
|
+
failures++;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
const target = Math.min(codeBlocks.length, this.verifyAttempts);
|
|
774
|
+
const majorityNeeded = Math.floor(target / 2) + 1;
|
|
775
|
+
if (successfulCodes.length >= majorityNeeded || failures > target - majorityNeeded) {
|
|
776
|
+
stop();
|
|
777
|
+
}
|
|
733
778
|
},
|
|
734
|
-
|
|
735
|
-
|
|
779
|
+
{
|
|
780
|
+
maxAttempts: this.verifyAttempts,
|
|
781
|
+
observability: {
|
|
782
|
+
agent: 'navigator',
|
|
783
|
+
},
|
|
784
|
+
catch: async (error) => {
|
|
785
|
+
debugLog(error);
|
|
786
|
+
},
|
|
787
|
+
}
|
|
788
|
+
);
|
|
789
|
+
} finally {
|
|
790
|
+
page?.setDefaultTimeout(originalTimeout);
|
|
791
|
+
}
|
|
736
792
|
|
|
737
|
-
const totalAttempted = Math.min(codeBlocks.length, this.
|
|
738
|
-
const
|
|
793
|
+
const totalAttempted = Math.min(codeBlocks.length, this.verifyAttempts);
|
|
794
|
+
const majorityNeeded = Math.floor(totalAttempted / 2) + 1;
|
|
795
|
+
let verified = successfulCodes.length >= majorityNeeded;
|
|
796
|
+
if (alreadyVerified) verified = true;
|
|
739
797
|
|
|
740
798
|
actionResult.addVerification(message, verified);
|
|
741
799
|
this.explorer.getStateManager().updateState(actionResult);
|
|
742
800
|
|
|
743
801
|
return { verified, successfulCodes, assertionSteps, totalAttempted };
|
|
744
802
|
}
|
|
803
|
+
|
|
804
|
+
private checkAlreadyVerified(aiResponse: string, actionResult: ActionResult): boolean {
|
|
805
|
+
const verifiedMatch = aiResponse.match(/ALREADY_VERIFIED:\s*(.+)/i);
|
|
806
|
+
if (!verifiedMatch) return false;
|
|
807
|
+
const claim = verifiedMatch[1].trim().replace(/^["']|["']$/g, '');
|
|
808
|
+
return actionResult.getVerification(claim) === true;
|
|
809
|
+
}
|
|
745
810
|
}
|
|
746
811
|
|
|
747
812
|
export { Navigator };
|
package/src/ai/pilot.ts
CHANGED
|
@@ -322,6 +322,12 @@ export class Pilot implements Agent {
|
|
|
322
322
|
- "Delete X" → X must be gone. Clicking delete is NOT enough.
|
|
323
323
|
- "Edit X" → updated value must be persisted (visible in list/detail). Opening edit is NOT enough; redirect after save with the new value visible IS enough.
|
|
324
324
|
- Negative tests ("without a name", "invalid", "duplicate", "unauthorized") → success means the system PREVENTED the action with validation/error.
|
|
325
|
+
- Navigation-prefixed titles ("Access/Open/Go to X to <do Y>") → the goal is <do Y>; reaching X is
|
|
326
|
+
only a milestone. A satisfied milestone (tab active, panel/prompt visible, list shown) is NEVER a pass
|
|
327
|
+
if <do Y> did not occur this run.
|
|
328
|
+
- If the page reveals the goal cannot be performed here — required control absent, integration not
|
|
329
|
+
connected, or only a setup/connect/empty-state prompt is shown — vote "skipped" (prerequisites unmet),
|
|
330
|
+
never "pass".
|
|
325
331
|
|
|
326
332
|
PROVENANCE: the entity you cite as proof must appear by name in <notes> or
|
|
327
333
|
<session_log> tool inputs for THIS run. Name absent from tester activity = stale
|
|
@@ -30,6 +30,15 @@ const config = {
|
|
|
30
30
|
// agentic model for decision making
|
|
31
31
|
agenticModel: openrouter('minimax/minimax-m2.5:nitro'),
|
|
32
32
|
},
|
|
33
|
+
|
|
34
|
+
reporter: {
|
|
35
|
+
// Save a local HTML report after each run.
|
|
36
|
+
html: true,
|
|
37
|
+
// Save a local markdown report after each run.
|
|
38
|
+
markdown: true,
|
|
39
|
+
// Group runs by title in Testomat.io / HTML reports. Defaults to today's date — customize or remove.
|
|
40
|
+
runGroup: new Date().toISOString().slice(0, 10),
|
|
41
|
+
},
|
|
33
42
|
};
|
|
34
43
|
|
|
35
44
|
export default config;
|
package/src/config.ts
CHANGED
|
@@ -84,6 +84,8 @@ interface PilotAgentConfig extends AgentConfig {
|
|
|
84
84
|
interface NavigatorAgentConfig extends AgentConfig {
|
|
85
85
|
addHtmlOnTry?: number;
|
|
86
86
|
maxAttempts?: number;
|
|
87
|
+
verifyAttempts?: number;
|
|
88
|
+
verifyTimeout?: number;
|
|
87
89
|
}
|
|
88
90
|
|
|
89
91
|
type HealFn = (ctx: { I: any }) => Promise<void> | void;
|
package/src/reporter.ts
CHANGED
|
@@ -29,7 +29,7 @@ export class Reporter {
|
|
|
29
29
|
this.reporterEnabled = Reporter.resolveEnabled(config);
|
|
30
30
|
this.stateManager = stateManager;
|
|
31
31
|
|
|
32
|
-
if (this.reporterEnabled &&
|
|
32
|
+
if (this.reporterEnabled && config?.html) {
|
|
33
33
|
this.configureHtmlPipe();
|
|
34
34
|
}
|
|
35
35
|
|
|
@@ -63,6 +63,7 @@ export class Reporter {
|
|
|
63
63
|
static resolveEnabled(config?: ReporterConfig): boolean {
|
|
64
64
|
if (config?.enabled === true) return true;
|
|
65
65
|
if (config?.enabled === false) return false;
|
|
66
|
+
if (config?.html || config?.markdown) return true;
|
|
66
67
|
return Boolean(process.env.TESTOMATIO);
|
|
67
68
|
}
|
|
68
69
|
|
|
@@ -88,12 +89,8 @@ export class Reporter {
|
|
|
88
89
|
|
|
89
90
|
private configureRunGroup(runGroup: string | null | undefined): void {
|
|
90
91
|
if (process.env.TESTOMATIO_RUNGROUP_TITLE) return;
|
|
91
|
-
if (runGroup
|
|
92
|
-
|
|
93
|
-
process.env.TESTOMATIO_RUNGROUP_TITLE = runGroup;
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
process.env.TESTOMATIO_RUNGROUP_TITLE = `Explorbot ${new Date().toISOString().slice(0, 10)}`;
|
|
92
|
+
if (!runGroup) return;
|
|
93
|
+
process.env.TESTOMATIO_RUNGROUP_TITLE = runGroup;
|
|
97
94
|
}
|
|
98
95
|
|
|
99
96
|
async startRun(): Promise<void> {
|