explorbot 0.4.7 → 0.4.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/boat/api-tester/src/ai/chief.ts +3 -1
- package/boat/api-tester/src/ai/curler.ts +4 -0
- package/dist/boat/api-tester/src/ai/chief.js +3 -1
- package/dist/boat/api-tester/src/ai/curler.js +4 -0
- package/dist/package.json +1 -1
- package/dist/rules/chief/general.md +2 -0
- package/dist/rules/researcher/pagination.md +1 -0
- package/dist/src/action.js +3 -2
- package/dist/src/ai/navigator.js +1 -4
- package/dist/src/ai/pilot.js +8 -12
- package/dist/src/ai/planner/session-dedup.d.ts +2 -1
- package/dist/src/ai/planner/session-dedup.js +18 -1
- package/dist/src/ai/planner.js +8 -4
- package/dist/src/ai/provider.js +18 -4
- package/dist/src/ai/rules.js +1 -0
- package/dist/src/ai/tester.js +1 -1
- package/dist/src/ai/tools.js +2 -1
- package/dist/src/utils/code-extractor.js +6 -2
- package/package.json +1 -1
- package/rules/chief/general.md +2 -0
- package/rules/researcher/pagination.md +1 -0
- package/src/action.ts +3 -2
- package/src/ai/navigator.ts +1 -4
- package/src/ai/pilot.ts +8 -12
- package/src/ai/planner/session-dedup.ts +16 -2
- package/src/ai/planner.ts +8 -4
- package/src/ai/provider.ts +18 -3
- package/src/ai/rules.ts +1 -0
- package/src/ai/tester.ts +1 -1
- package/src/ai/tools.ts +2 -1
- package/src/utils/code-extractor.ts +6 -2
|
@@ -261,7 +261,9 @@ export class Chief extends ChiefBase {
|
|
|
261
261
|
- Use real enum values discovered in the data
|
|
262
262
|
- Each test MUST use DIFFERENT data — never reuse the same field values across tests
|
|
263
263
|
- For "create" tests: base payload on a real record but change field values to create new unique data
|
|
264
|
-
-
|
|
264
|
+
- Treat records and IDs from sample_data as read-only. Never update, patch, delete, archive, or otherwise mutate them
|
|
265
|
+
- For update/delete tests: the same scenario must first create its own target, then mutate only that target
|
|
266
|
+
- For negative or unsupported-method tests that could mutate data if accepted: create a scenario-owned target first; if that setup is impossible, do not send the destructive request
|
|
265
267
|
- For tests needing parent references: use real _id field values from sample_data
|
|
266
268
|
`);
|
|
267
269
|
}
|
|
@@ -284,6 +284,10 @@ export class Curler {
|
|
|
284
284
|
- Record important findings as you go
|
|
285
285
|
- Be precise about what you expect vs what you observe
|
|
286
286
|
- If a test requires data from another endpoint, use schemaFor to look it up before guessing
|
|
287
|
+
- Treat existing records, sample data, and IDs supplied by the plan as read-only
|
|
288
|
+
- Before PUT, PATCH, DELETE, archive, or another destructive request, create the target inside the current scenario and mutate only that target
|
|
289
|
+
- This also applies when testing an unsupported method: the server may unexpectedly accept it, so never probe destructively against pre-existing data
|
|
290
|
+
- If a scenario-owned target cannot be created, use stop rather than risking existing data
|
|
287
291
|
</rules>
|
|
288
292
|
`;
|
|
289
293
|
}
|
|
@@ -230,7 +230,9 @@ export class Chief extends ChiefBase {
|
|
|
230
230
|
- Use real enum values discovered in the data
|
|
231
231
|
- Each test MUST use DIFFERENT data — never reuse the same field values across tests
|
|
232
232
|
- For "create" tests: base payload on a real record but change field values to create new unique data
|
|
233
|
-
-
|
|
233
|
+
- Treat records and IDs from sample_data as read-only. Never update, patch, delete, archive, or otherwise mutate them
|
|
234
|
+
- For update/delete tests: the same scenario must first create its own target, then mutate only that target
|
|
235
|
+
- For negative or unsupported-method tests that could mutate data if accepted: create a scenario-owned target first; if that setup is impossible, do not send the destructive request
|
|
234
236
|
- For tests needing parent references: use real _id field values from sample_data
|
|
235
237
|
`);
|
|
236
238
|
}
|
|
@@ -243,6 +243,10 @@ export class Curler {
|
|
|
243
243
|
- Record important findings as you go
|
|
244
244
|
- Be precise about what you expect vs what you observe
|
|
245
245
|
- If a test requires data from another endpoint, use schemaFor to look it up before guessing
|
|
246
|
+
- Treat existing records, sample data, and IDs supplied by the plan as read-only
|
|
247
|
+
- Before PUT, PATCH, DELETE, archive, or another destructive request, create the target inside the current scenario and mutate only that target
|
|
248
|
+
- This also applies when testing an unsupported method: the server may unexpectedly accept it, so never probe destructively against pre-existing data
|
|
249
|
+
- If a scenario-owned target cannot be created, use stop rather than risking existing data
|
|
246
250
|
</rules>
|
|
247
251
|
`;
|
|
248
252
|
}
|
package/dist/package.json
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
- Steps should specify exact HTTP methods, paths, and key payload details
|
|
3
3
|
- Expected outcomes should be specific and verifiable (status codes, response fields, error messages)
|
|
4
4
|
- For CRUD operations, each test should handle its own setup and teardown
|
|
5
|
+
- Treat existing records and IDs discovered from the API, knowledge, or sample data as read-only
|
|
6
|
+
- A scenario that updates, patches, deletes, archives, or otherwise mutates a record must create that target inside the same scenario first; omit the scenario if safe setup is impossible
|
|
5
7
|
- Expect standard REST conventions: 200 OK, 201 Created, 204 No Content, 400 Bad Request, 404 Not Found, 422 Unprocessable Entity
|
|
6
8
|
- NEVER propose scenarios that test the same thing. "Create a basic suite" and "Successful creation of a simple suite" are DUPLICATES. Each scenario must test a DISTINCT behavior or aspect.
|
|
7
9
|
- Before finalizing, review all scenarios and remove any that overlap in what they actually verify.
|
|
@@ -2,5 +2,6 @@
|
|
|
2
2
|
When a section is a list that continues beyond what is shown, add one line under its `> Container:` line:
|
|
3
3
|
`> Pagination: controls` — it has page numbers (1, 2, 3), prev/next arrows, or a "load more" button.
|
|
4
4
|
`> Pagination: infinite` — it has none of those and loads more as it is scrolled.
|
|
5
|
+
Omit the line when the items already shown are the whole collection.
|
|
5
6
|
Sorting, filtering and switching tabs are not pagination — omit the line then.
|
|
6
7
|
</pagination>
|
package/dist/src/action.js
CHANGED
|
@@ -552,10 +552,11 @@ export const attachStepLogger = (target, assertionsTarget) => {
|
|
|
552
552
|
}
|
|
553
553
|
tag('step').log(step);
|
|
554
554
|
};
|
|
555
|
-
|
|
555
|
+
const onPassed = (step) => listener(step);
|
|
556
|
+
codeceptjs.event.dispatcher.on(codeceptjs.event.step.passed, onPassed);
|
|
556
557
|
codeceptjs.event.dispatcher.on(codeceptjs.event.step.failed, listener);
|
|
557
558
|
return () => {
|
|
558
|
-
codeceptjs.event.dispatcher.off(codeceptjs.event.step.passed,
|
|
559
|
+
codeceptjs.event.dispatcher.off(codeceptjs.event.step.passed, onPassed);
|
|
559
560
|
codeceptjs.event.dispatcher.off(codeceptjs.event.step.failed, listener);
|
|
560
561
|
};
|
|
561
562
|
};
|
package/dist/src/ai/navigator.js
CHANGED
|
@@ -630,7 +630,7 @@ class Navigator {
|
|
|
630
630
|
const cachedVerification = actionResult.getVerification(message);
|
|
631
631
|
if (cachedVerification !== null) {
|
|
632
632
|
tag('operation').log(`Reusing cached verification: ${cachedVerification ? 'PASS' : 'FAIL'}`);
|
|
633
|
-
return { verified: cachedVerification, successfulCodes: [], assertionSteps: [], totalAttempted: 0 };
|
|
633
|
+
return { verified: cachedVerification, inexpressible: false, results: [], successfulCodes: [], assertionSteps: [], totalAttempted: 0 };
|
|
634
634
|
}
|
|
635
635
|
const knowledge = this.knowledgeTracker.renderRelevantContext(actionResult);
|
|
636
636
|
let experience = '';
|
|
@@ -741,9 +741,6 @@ class Navigator {
|
|
|
741
741
|
observability: {
|
|
742
742
|
agent: 'navigator',
|
|
743
743
|
},
|
|
744
|
-
catch: async (error) => {
|
|
745
|
-
debugLog(error);
|
|
746
|
-
},
|
|
747
744
|
});
|
|
748
745
|
}
|
|
749
746
|
finally {
|
package/dist/src/ai/pilot.js
CHANGED
|
@@ -126,14 +126,6 @@ export class Pilot {
|
|
|
126
126
|
${sessionLog || 'No actions recorded'}
|
|
127
127
|
</session_log>
|
|
128
128
|
|
|
129
|
-
Decide and commit. "continue" extends the loop and burns iterations — choose it only when
|
|
130
|
-
evidence is genuinely insufficient to call pass/fail, not as a safety hedge.
|
|
131
|
-
- "pass" if final state proves the SCENARIO GOAL is accomplished. Set requestVerification.
|
|
132
|
-
- "fail" if scenario was attempted but goal not achieved.
|
|
133
|
-
- "skipped" if scenario is irrelevant/inapplicable, OR systematic infrastructure failures.
|
|
134
|
-
- "continue" only when a concrete missing piece of evidence (a verify/see) would change your verdict.
|
|
135
|
-
- Mixed evidence + final state shows success → pass. Mixed + final state unclear → continue with guidance.
|
|
136
|
-
|
|
137
129
|
When deciding "pass", you MUST also set requestVerification to a one-sentence natural-language
|
|
138
130
|
claim about the current page (e.g., "New item Foo is visible in the items list"). NOT
|
|
139
131
|
code — do not write I.*, expect(), .then(), or any JavaScript. Choose the strongest single
|
|
@@ -352,7 +344,7 @@ export class Pilot {
|
|
|
352
344
|
buildVerdictSystemPrompt(task) {
|
|
353
345
|
return dedent `
|
|
354
346
|
You are Pilot — final decision maker for test pass/fail. Review the evidence and commit to a
|
|
355
|
-
verdict
|
|
347
|
+
verdict.
|
|
356
348
|
|
|
357
349
|
${capabilityGroundingRule}
|
|
358
350
|
|
|
@@ -366,10 +358,11 @@ export class Pilot {
|
|
|
366
358
|
DOM assertion can't be made.
|
|
367
359
|
Do not pass when Tester achieved only a related navigation/filter/tab/status outcome instead of the
|
|
368
360
|
requested action, workflow, or entity detail goal.
|
|
369
|
-
- "fail":
|
|
361
|
+
- "fail": goal not achieved and no further step toward it is available on the current page.
|
|
370
362
|
- "skipped": scenario is irrelevant to the app, OR systematic infrastructure failures (LLM errors,
|
|
371
363
|
crashes) prevented testing. NOT for "test failed to interact" — that's "fail" or "continue".
|
|
372
|
-
- "continue":
|
|
364
|
+
- "continue": goal incomplete but the control for the NEXT step is present on the current page, or a
|
|
365
|
+
concrete missing check would change your verdict. Guidance must name that step.
|
|
373
366
|
If a verify() asserted a state that was ALREADY TRUE before the test, it proves nothing — reject.
|
|
374
367
|
|
|
375
368
|
reason field: one short sentence, maximum 120 characters. Do NOT restate the decision
|
|
@@ -1030,7 +1023,9 @@ export class Pilot {
|
|
|
1030
1023
|
${interactive ? '- Use askUser() only as last resort.' : ''}
|
|
1031
1024
|
|
|
1032
1025
|
Diagnostic patterns (use <state>, executed/element/skipped fields, ariaDiff):
|
|
1033
|
-
-
|
|
1026
|
+
- Scenario's target control in "disabled buttons" → a precondition is unmet; identify which before acting.
|
|
1027
|
+
Other disabled controls often name the unsatisfied constraint; "active form" marks [required] fields.
|
|
1028
|
+
Aim Tester at the constraint the page names, not the one the scenario assumed — note the difference in PROGRESS.
|
|
1034
1029
|
- "overlay: none" but Tester targets an overlay → overlay closed; re-trigger.
|
|
1035
1030
|
- "region:" in <state> → a large area appeared in place without navigation (subview, wizard step, panel). Direct Tester to act inside it; the rest of the page is still usable.
|
|
1036
1031
|
- Action SUCCESS but ariaDiff empty → may have worked without visible DOM change; check result message.
|
|
@@ -1052,6 +1047,7 @@ export class Pilot {
|
|
|
1052
1047
|
Tester tools: click, pressKey, form, see, verify, interact, context, research, xpathCheck,
|
|
1053
1048
|
visualClick, back, getVisitedStates, reset, stop, finish, record.
|
|
1054
1049
|
Use tool names exactly as listed. Do not invent combined names or aliases.
|
|
1050
|
+
Reloading is not a tool: to re-read a page from the server, instruct Tester to run I.reloadPage() through form.
|
|
1055
1051
|
|
|
1056
1052
|
${capabilityGroundingRule}
|
|
1057
1053
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type Plan, type Test } from '../../test-plan.js';
|
|
2
2
|
import type { Constructor } from '../researcher/mixin.js';
|
|
3
3
|
export declare function WithSessionDedup<T extends Constructor>(Base: T): {
|
|
4
4
|
new (...args: any[]): {
|
|
@@ -9,4 +9,5 @@ export declare function WithSessionDedup<T extends Constructor>(Base: T): {
|
|
|
9
9
|
getPreviousSessionScenariosExcluding(plan: Plan): Set<string>;
|
|
10
10
|
};
|
|
11
11
|
} & T;
|
|
12
|
+
export declare function formatSessionTest(plan: Plan, test: Test): string;
|
|
12
13
|
export declare function clearSessionDedup(): void;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { TestResult } from "../../test-plan.js";
|
|
1
2
|
const previousPlans = [];
|
|
2
3
|
export function WithSessionDedup(Base) {
|
|
3
4
|
return class extends Base {
|
|
@@ -12,7 +13,7 @@ export function WithSessionDedup(Base) {
|
|
|
12
13
|
if (plan === this.currentPlan)
|
|
13
14
|
continue;
|
|
14
15
|
for (const test of plan.tests) {
|
|
15
|
-
lines.push(
|
|
16
|
+
lines.push(formatSessionTest(plan, test));
|
|
16
17
|
}
|
|
17
18
|
}
|
|
18
19
|
return lines.join('\n');
|
|
@@ -25,6 +26,22 @@ export function WithSessionDedup(Base) {
|
|
|
25
26
|
}
|
|
26
27
|
};
|
|
27
28
|
}
|
|
29
|
+
export function formatSessionTest(plan, test) {
|
|
30
|
+
const lastNote = Object.values(test.notes)
|
|
31
|
+
.filter((note) => note.message)
|
|
32
|
+
.pop();
|
|
33
|
+
let outcome = test.result;
|
|
34
|
+
if (!outcome)
|
|
35
|
+
outcome = 'pending';
|
|
36
|
+
if (!test.result && lastNote)
|
|
37
|
+
outcome = 'unfinished';
|
|
38
|
+
const line = `${plan.url || '/'} | ${test.style || 'default'} | ${outcome} | ${test.scenario}`;
|
|
39
|
+
if (!lastNote)
|
|
40
|
+
return line;
|
|
41
|
+
if (outcome !== TestResult.FAILED && outcome !== 'unfinished')
|
|
42
|
+
return line;
|
|
43
|
+
return `${line} — ${lastNote.message.slice(0, 140)}`;
|
|
44
|
+
}
|
|
28
45
|
export function clearSessionDedup() {
|
|
29
46
|
previousPlans.length = 0;
|
|
30
47
|
}
|
package/dist/src/ai/planner.js
CHANGED
|
@@ -316,8 +316,10 @@ export class Planner extends PlannerBase {
|
|
|
316
316
|
You can suggest scenarios that can be tested only through web interface.
|
|
317
317
|
You can't test emails, database, SMS, or any external services.
|
|
318
318
|
Suggest scenarios that can be potentially verified by UI.
|
|
319
|
-
|
|
320
|
-
|
|
319
|
+
Prefer outcomes grounded in observed interface behavior.
|
|
320
|
+
Every expected outcome must be verifiable through the web interface.
|
|
321
|
+
If a page or subpage has not been observed, describe the expected visible result generically instead of inventing interface details.
|
|
322
|
+
Persistency after a reload counts only when the persisted state can be confirmed through the interface.
|
|
321
323
|
If there are subpages (pages with same URL path) plan testing of those subpages as well
|
|
322
324
|
Plan CRUD operations in order: create, read, update, delete.
|
|
323
325
|
Do not invent specific route names, success messages, validation texts, badge counts, or welcome messages unless they are visible in research, visited pages, or prior observed flows.
|
|
@@ -328,7 +330,7 @@ export class Planner extends PlannerBase {
|
|
|
328
330
|
If a scenario needs existing records, recipients, results, notifications, or other target data, propose it only when that data is visible, API preconditions can create it, or the scenario itself creates the record as its setup.
|
|
329
331
|
If the page appears read-only, degraded, demo-limited, maintenance-like, or lacks write controls, prefer read-only scenarios such as opening panels, inspecting visible lists, filtering, searching, or verifying current state.
|
|
330
332
|
Do not assume hidden data exists just because a control is present.
|
|
331
|
-
|
|
333
|
+
Do not put record IDs or unique record names in test plans. Describe which record is needed and let Pilot choose it during execution; name a specific record only when research shows a small, complete list of available records.
|
|
332
334
|
If the list is empty or no concrete item names are visible, do not invent "known" or "existing" items. Prefer empty-state, no-match search, clear-search, or read-only list behavior scenarios.
|
|
333
335
|
Search, filter, sorting, tab, and list scenarios must start from a stable page where those controls are visible; avoid transient create/edit/new URLs unless the scenario tests that form.
|
|
334
336
|
For option values and list items, use only visible or previously observed data; do not add create/update/delete setup unless the user explicitly requests that workflow.
|
|
@@ -540,7 +542,9 @@ export class Planner extends PlannerBase {
|
|
|
540
542
|
const sessionTests = this.getSessionTestsSummary();
|
|
541
543
|
if (sessionTests) {
|
|
542
544
|
conversation.addUserText(dedent `
|
|
543
|
-
Tests already planned in this session across all pages. DO NOT duplicate any of these
|
|
545
|
+
Tests already planned in this session across all pages, with how each one ended. DO NOT duplicate any of these.
|
|
546
|
+
A failed test means the app or the harness could not do what it tried: do not re-propose the same behavior on another page unless you can name what makes it work this time.
|
|
547
|
+
A failed or unfinished test carries the last thing it observed after the dash — read it before deciding that the behavior is worth trying again.
|
|
544
548
|
|
|
545
549
|
<session_tests>
|
|
546
550
|
${sessionTests}
|
package/dist/src/ai/provider.js
CHANGED
|
@@ -32,6 +32,11 @@ function createHarmonyChannelFallbackTool() {
|
|
|
32
32
|
execute: async () => ({ message: 'Noted. Continue with your next action.' }),
|
|
33
33
|
});
|
|
34
34
|
}
|
|
35
|
+
function withHarmonyChannelFallback(tools) {
|
|
36
|
+
if (tools?.commentary)
|
|
37
|
+
return tools;
|
|
38
|
+
return { ...tools, commentary: createHarmonyChannelFallbackTool() };
|
|
39
|
+
}
|
|
35
40
|
let telemetryRegistered = false;
|
|
36
41
|
let beforeExitFlushHooked = false;
|
|
37
42
|
let activeOtelSdk = null;
|
|
@@ -367,8 +372,8 @@ export class Provider {
|
|
|
367
372
|
setActivity(`🤖 Asking ${modelName} with dynamic tools`, 'ai');
|
|
368
373
|
promptLog(`Using model: ${modelName}`);
|
|
369
374
|
let toolsWithCommentary = tools;
|
|
370
|
-
if (
|
|
371
|
-
toolsWithCommentary =
|
|
375
|
+
if (options.toolChoice !== 'required')
|
|
376
|
+
toolsWithCommentary = withHarmonyChannelFallback(tools);
|
|
372
377
|
const toolNames = Object.keys(toolsWithCommentary || {});
|
|
373
378
|
tag('debug').log(`Tools enabled: [${toolNames.join(', ')}]`);
|
|
374
379
|
promptLog('Available tools:', toolNames);
|
|
@@ -378,9 +383,10 @@ export class Provider {
|
|
|
378
383
|
const stopConditions = [isStepCount(maxRoundtrips)];
|
|
379
384
|
if (extraStop)
|
|
380
385
|
stopConditions.push(extraStop);
|
|
381
|
-
|
|
386
|
+
let config = this.buildGenerateConfig({ tools: toolsWithCommentary, maxOutputTokens: 16384, toolChoice: 'auto', experimental_repairToolCall: repairToolCall }, { stopWhen: stopConditions, model }, options);
|
|
382
387
|
let attemptMessages = messages;
|
|
383
388
|
let invalidRequestFeedbackAdded = false;
|
|
389
|
+
let requiredToolChoiceRelaxed = false;
|
|
384
390
|
const executedStepMessages = [];
|
|
385
391
|
try {
|
|
386
392
|
let response = await this.withModelRequestSlot(() => withRetry(async () => {
|
|
@@ -399,6 +405,11 @@ export class Provider {
|
|
|
399
405
|
invalidRequestFeedbackAdded = amended !== attemptMessages;
|
|
400
406
|
attemptMessages = amended;
|
|
401
407
|
}
|
|
408
|
+
if (!requiredToolChoiceRelaxed && isRequiredToolChoiceError(error)) {
|
|
409
|
+
requiredToolChoiceRelaxed = true;
|
|
410
|
+
config = { ...config, tools: withHarmonyChannelFallback(tools), toolChoice: 'auto' };
|
|
411
|
+
tag('warning').log('Provider rejected required tool choice — retrying with automatic tool choice and channel fallback');
|
|
412
|
+
}
|
|
402
413
|
throw error;
|
|
403
414
|
}));
|
|
404
415
|
this.recordUsage(options.agentName || 'unknown', modelName, result.usage);
|
|
@@ -422,7 +433,7 @@ export class Provider {
|
|
|
422
433
|
}
|
|
423
434
|
catch (error) {
|
|
424
435
|
clearActivity();
|
|
425
|
-
if (error
|
|
436
|
+
if (isRequiredToolChoiceError(error)) {
|
|
426
437
|
return { text: '', toolCalls: [], toolResults: [], responseMessages: executedStepMessages, usage: null };
|
|
427
438
|
}
|
|
428
439
|
if (error?.name === 'AbortError')
|
|
@@ -659,6 +670,9 @@ function withInvalidRequestFeedback(messages, error) {
|
|
|
659
670
|
},
|
|
660
671
|
];
|
|
661
672
|
}
|
|
673
|
+
function isRequiredToolChoiceError(error) {
|
|
674
|
+
return error instanceof Error && error.message.includes('Tool choice is required');
|
|
675
|
+
}
|
|
662
676
|
function repairChannelMarker({ toolCall, tools }) {
|
|
663
677
|
const markerIndex = toolCall.toolName.indexOf('<|channel|>');
|
|
664
678
|
if (markerIndex <= 0)
|
package/dist/src/ai/rules.js
CHANGED
|
@@ -177,6 +177,7 @@ export const capabilityGroundingRule = dedent `
|
|
|
177
177
|
When a scenario depends on a named action, menu item, status, option, workflow, or feature,
|
|
178
178
|
that capability must be visible or explicitly confirmed in the current research/page context
|
|
179
179
|
for the same target entity type.
|
|
180
|
+
Ground on the scenario's outcome, not a planned step's control label — a missing label is not a missing capability.
|
|
180
181
|
|
|
181
182
|
Do not transfer capabilities between similar entities, rows, lists, detail pages, or menus.
|
|
182
183
|
Do not replace a requested action with a synonym or related action unless the UI explicitly
|
package/dist/src/ai/tester.js
CHANGED
|
@@ -29,7 +29,7 @@ const SAMPLE_FILES = {
|
|
|
29
29
|
};
|
|
30
30
|
export class Tester extends TaskAgent {
|
|
31
31
|
ACTION_TOOLS = ['click', 'hover', 'pressKey', 'form'];
|
|
32
|
-
DELEGATED_ACTION_TOOLS = ['interact'];
|
|
32
|
+
DELEGATED_ACTION_TOOLS = ['interact', 'visualClick'];
|
|
33
33
|
SPECIAL_CONTEXT_ACTION_TOOLS = ['exitIframe'];
|
|
34
34
|
emoji = '🧪';
|
|
35
35
|
requestStore;
|
package/dist/src/ai/tools.js
CHANGED
|
@@ -312,6 +312,7 @@ export function createCodeceptJSTools({ explorer, stateManager }, task) {
|
|
|
312
312
|
- Performing multiple form actions in a single batch
|
|
313
313
|
- Complex interactions requiring sequential commands
|
|
314
314
|
- Reaching items further down a list (I.scrollTo)
|
|
315
|
+
- Reloading the page to prove a change outlived it (I.reloadPage)
|
|
315
316
|
|
|
316
317
|
Example - filling a form with context (PREFERRED):
|
|
317
318
|
I.fillField('Username', 'John', '.login-form')
|
|
@@ -324,7 +325,7 @@ export function createCodeceptJSTools({ explorer, stateManager }, task) {
|
|
|
324
325
|
I.selectOption({"role":"combobox","text":"Category"}, 'Technology')
|
|
325
326
|
|
|
326
327
|
Do not submit form - use verify() first to check fields were filled correctly, then click() to submit.
|
|
327
|
-
Do not use: wait functions, amOnPage,
|
|
328
|
+
Do not use: wait functions, amOnPage, saveScreenshot
|
|
328
329
|
`,
|
|
329
330
|
inputSchema: z.object({
|
|
330
331
|
codeBlock: z.string().describe('Valid CodeceptJS code starting with I. Can contain multiple commands separated by newlines.'),
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import { createDebug } from './logger.js';
|
|
2
2
|
const debugLog = createDebug('explorbot:code-extractor');
|
|
3
|
+
const JS_LANGUAGES = new Set(['', 'js', 'javascript']);
|
|
3
4
|
export function extractCodeBlocks(aiResponse) {
|
|
4
|
-
const codeBlockRegex = /```(
|
|
5
|
+
const codeBlockRegex = /```([^\n`]*)\n([\s\S]*?)\n```/g;
|
|
5
6
|
const codeBlocks = [];
|
|
6
7
|
let match = null;
|
|
7
8
|
while ((match = codeBlockRegex.exec(aiResponse))) {
|
|
8
|
-
const
|
|
9
|
+
const language = match[1].trim().toLowerCase();
|
|
10
|
+
if (!JS_LANGUAGES.has(language))
|
|
11
|
+
continue;
|
|
12
|
+
const code = match[2].trim();
|
|
9
13
|
if (!code)
|
|
10
14
|
continue;
|
|
11
15
|
try {
|
package/package.json
CHANGED
package/rules/chief/general.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
- Steps should specify exact HTTP methods, paths, and key payload details
|
|
3
3
|
- Expected outcomes should be specific and verifiable (status codes, response fields, error messages)
|
|
4
4
|
- For CRUD operations, each test should handle its own setup and teardown
|
|
5
|
+
- Treat existing records and IDs discovered from the API, knowledge, or sample data as read-only
|
|
6
|
+
- A scenario that updates, patches, deletes, archives, or otherwise mutates a record must create that target inside the same scenario first; omit the scenario if safe setup is impossible
|
|
5
7
|
- Expect standard REST conventions: 200 OK, 201 Created, 204 No Content, 400 Bad Request, 404 Not Found, 422 Unprocessable Entity
|
|
6
8
|
- NEVER propose scenarios that test the same thing. "Create a basic suite" and "Successful creation of a simple suite" are DUPLICATES. Each scenario must test a DISTINCT behavior or aspect.
|
|
7
9
|
- Before finalizing, review all scenarios and remove any that overlap in what they actually verify.
|
|
@@ -2,5 +2,6 @@
|
|
|
2
2
|
When a section is a list that continues beyond what is shown, add one line under its `> Container:` line:
|
|
3
3
|
`> Pagination: controls` — it has page numbers (1, 2, 3), prev/next arrows, or a "load more" button.
|
|
4
4
|
`> Pagination: infinite` — it has none of those and loads more as it is scrolled.
|
|
5
|
+
Omit the line when the items already shown are the whole collection.
|
|
5
6
|
Sorting, filtering and switching tabs are not pagination — omit the line then.
|
|
6
7
|
</pagination>
|
package/src/action.ts
CHANGED
|
@@ -592,10 +592,11 @@ export const attachStepLogger = (target: ExecutedStep[], assertionsTarget?: Arra
|
|
|
592
592
|
}
|
|
593
593
|
tag('step').log(step);
|
|
594
594
|
};
|
|
595
|
-
|
|
595
|
+
const onPassed: StepListener = (step) => listener(step);
|
|
596
|
+
codeceptjs.event.dispatcher.on(codeceptjs.event.step.passed, onPassed);
|
|
596
597
|
codeceptjs.event.dispatcher.on(codeceptjs.event.step.failed, listener);
|
|
597
598
|
return () => {
|
|
598
|
-
codeceptjs.event.dispatcher.off(codeceptjs.event.step.passed,
|
|
599
|
+
codeceptjs.event.dispatcher.off(codeceptjs.event.step.passed, onPassed);
|
|
599
600
|
codeceptjs.event.dispatcher.off(codeceptjs.event.step.failed, listener);
|
|
600
601
|
};
|
|
601
602
|
};
|
package/src/ai/navigator.ts
CHANGED
|
@@ -703,7 +703,7 @@ class Navigator implements Agent {
|
|
|
703
703
|
const cachedVerification = actionResult.getVerification(message);
|
|
704
704
|
if (cachedVerification !== null) {
|
|
705
705
|
tag('operation').log(`Reusing cached verification: ${cachedVerification ? 'PASS' : 'FAIL'}`);
|
|
706
|
-
return { verified: cachedVerification, successfulCodes: [], assertionSteps: [], totalAttempted: 0 };
|
|
706
|
+
return { verified: cachedVerification, inexpressible: false, results: [], successfulCodes: [], assertionSteps: [], totalAttempted: 0 };
|
|
707
707
|
}
|
|
708
708
|
|
|
709
709
|
const knowledge = this.knowledgeTracker.renderRelevantContext(actionResult);
|
|
@@ -832,9 +832,6 @@ class Navigator implements Agent {
|
|
|
832
832
|
observability: {
|
|
833
833
|
agent: 'navigator',
|
|
834
834
|
},
|
|
835
|
-
catch: async (error) => {
|
|
836
|
-
debugLog(error);
|
|
837
|
-
},
|
|
838
835
|
}
|
|
839
836
|
);
|
|
840
837
|
} finally {
|
package/src/ai/pilot.ts
CHANGED
|
@@ -152,14 +152,6 @@ export class Pilot implements Agent {
|
|
|
152
152
|
${sessionLog || 'No actions recorded'}
|
|
153
153
|
</session_log>
|
|
154
154
|
|
|
155
|
-
Decide and commit. "continue" extends the loop and burns iterations — choose it only when
|
|
156
|
-
evidence is genuinely insufficient to call pass/fail, not as a safety hedge.
|
|
157
|
-
- "pass" if final state proves the SCENARIO GOAL is accomplished. Set requestVerification.
|
|
158
|
-
- "fail" if scenario was attempted but goal not achieved.
|
|
159
|
-
- "skipped" if scenario is irrelevant/inapplicable, OR systematic infrastructure failures.
|
|
160
|
-
- "continue" only when a concrete missing piece of evidence (a verify/see) would change your verdict.
|
|
161
|
-
- Mixed evidence + final state shows success → pass. Mixed + final state unclear → continue with guidance.
|
|
162
|
-
|
|
163
155
|
When deciding "pass", you MUST also set requestVerification to a one-sentence natural-language
|
|
164
156
|
claim about the current page (e.g., "New item Foo is visible in the items list"). NOT
|
|
165
157
|
code — do not write I.*, expect(), .then(), or any JavaScript. Choose the strongest single
|
|
@@ -401,7 +393,7 @@ export class Pilot implements Agent {
|
|
|
401
393
|
private buildVerdictSystemPrompt(task: Test): string {
|
|
402
394
|
return dedent`
|
|
403
395
|
You are Pilot — final decision maker for test pass/fail. Review the evidence and commit to a
|
|
404
|
-
verdict
|
|
396
|
+
verdict.
|
|
405
397
|
|
|
406
398
|
${capabilityGroundingRule}
|
|
407
399
|
|
|
@@ -415,10 +407,11 @@ export class Pilot implements Agent {
|
|
|
415
407
|
DOM assertion can't be made.
|
|
416
408
|
Do not pass when Tester achieved only a related navigation/filter/tab/status outcome instead of the
|
|
417
409
|
requested action, workflow, or entity detail goal.
|
|
418
|
-
- "fail":
|
|
410
|
+
- "fail": goal not achieved and no further step toward it is available on the current page.
|
|
419
411
|
- "skipped": scenario is irrelevant to the app, OR systematic infrastructure failures (LLM errors,
|
|
420
412
|
crashes) prevented testing. NOT for "test failed to interact" — that's "fail" or "continue".
|
|
421
|
-
- "continue":
|
|
413
|
+
- "continue": goal incomplete but the control for the NEXT step is present on the current page, or a
|
|
414
|
+
concrete missing check would change your verdict. Guidance must name that step.
|
|
422
415
|
If a verify() asserted a state that was ALREADY TRUE before the test, it proves nothing — reject.
|
|
423
416
|
|
|
424
417
|
reason field: one short sentence, maximum 120 characters. Do NOT restate the decision
|
|
@@ -1134,7 +1127,9 @@ export class Pilot implements Agent {
|
|
|
1134
1127
|
${interactive ? '- Use askUser() only as last resort.' : ''}
|
|
1135
1128
|
|
|
1136
1129
|
Diagnostic patterns (use <state>, executed/element/skipped fields, ariaDiff):
|
|
1137
|
-
-
|
|
1130
|
+
- Scenario's target control in "disabled buttons" → a precondition is unmet; identify which before acting.
|
|
1131
|
+
Other disabled controls often name the unsatisfied constraint; "active form" marks [required] fields.
|
|
1132
|
+
Aim Tester at the constraint the page names, not the one the scenario assumed — note the difference in PROGRESS.
|
|
1138
1133
|
- "overlay: none" but Tester targets an overlay → overlay closed; re-trigger.
|
|
1139
1134
|
- "region:" in <state> → a large area appeared in place without navigation (subview, wizard step, panel). Direct Tester to act inside it; the rest of the page is still usable.
|
|
1140
1135
|
- Action SUCCESS but ariaDiff empty → may have worked without visible DOM change; check result message.
|
|
@@ -1156,6 +1151,7 @@ export class Pilot implements Agent {
|
|
|
1156
1151
|
Tester tools: click, pressKey, form, see, verify, interact, context, research, xpathCheck,
|
|
1157
1152
|
visualClick, back, getVisitedStates, reset, stop, finish, record.
|
|
1158
1153
|
Use tool names exactly as listed. Do not invent combined names or aliases.
|
|
1154
|
+
Reloading is not a tool: to re-read a page from the server, instruct Tester to run I.reloadPage() through form.
|
|
1159
1155
|
|
|
1160
1156
|
${capabilityGroundingRule}
|
|
1161
1157
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type Plan, type Test, TestResult } from '../../test-plan.ts';
|
|
2
2
|
import type { Constructor } from '../researcher/mixin.ts';
|
|
3
3
|
|
|
4
4
|
const previousPlans: Plan[] = [];
|
|
@@ -18,7 +18,7 @@ export function WithSessionDedup<T extends Constructor>(Base: T) {
|
|
|
18
18
|
for (const plan of previousPlans) {
|
|
19
19
|
if (plan === this.currentPlan) continue;
|
|
20
20
|
for (const test of plan.tests) {
|
|
21
|
-
lines.push(
|
|
21
|
+
lines.push(formatSessionTest(plan, test));
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
24
|
return lines.join('\n');
|
|
@@ -34,6 +34,20 @@ export function WithSessionDedup<T extends Constructor>(Base: T) {
|
|
|
34
34
|
};
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
export function formatSessionTest(plan: Plan, test: Test): string {
|
|
38
|
+
const lastNote = Object.values(test.notes)
|
|
39
|
+
.filter((note) => note.message)
|
|
40
|
+
.pop();
|
|
41
|
+
let outcome: string | null = test.result;
|
|
42
|
+
if (!outcome) outcome = 'pending';
|
|
43
|
+
if (!test.result && lastNote) outcome = 'unfinished';
|
|
44
|
+
|
|
45
|
+
const line = `${plan.url || '/'} | ${test.style || 'default'} | ${outcome} | ${test.scenario}`;
|
|
46
|
+
if (!lastNote) return line;
|
|
47
|
+
if (outcome !== TestResult.FAILED && outcome !== 'unfinished') return line;
|
|
48
|
+
return `${line} — ${lastNote.message.slice(0, 140)}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
37
51
|
export function clearSessionDedup(): void {
|
|
38
52
|
previousPlans.length = 0;
|
|
39
53
|
}
|
package/src/ai/planner.ts
CHANGED
|
@@ -358,8 +358,10 @@ export class Planner extends PlannerBase implements Agent {
|
|
|
358
358
|
You can suggest scenarios that can be tested only through web interface.
|
|
359
359
|
You can't test emails, database, SMS, or any external services.
|
|
360
360
|
Suggest scenarios that can be potentially verified by UI.
|
|
361
|
-
|
|
362
|
-
|
|
361
|
+
Prefer outcomes grounded in observed interface behavior.
|
|
362
|
+
Every expected outcome must be verifiable through the web interface.
|
|
363
|
+
If a page or subpage has not been observed, describe the expected visible result generically instead of inventing interface details.
|
|
364
|
+
Persistency after a reload counts only when the persisted state can be confirmed through the interface.
|
|
363
365
|
If there are subpages (pages with same URL path) plan testing of those subpages as well
|
|
364
366
|
Plan CRUD operations in order: create, read, update, delete.
|
|
365
367
|
Do not invent specific route names, success messages, validation texts, badge counts, or welcome messages unless they are visible in research, visited pages, or prior observed flows.
|
|
@@ -370,7 +372,7 @@ export class Planner extends PlannerBase implements Agent {
|
|
|
370
372
|
If a scenario needs existing records, recipients, results, notifications, or other target data, propose it only when that data is visible, API preconditions can create it, or the scenario itself creates the record as its setup.
|
|
371
373
|
If the page appears read-only, degraded, demo-limited, maintenance-like, or lacks write controls, prefer read-only scenarios such as opening panels, inspecting visible lists, filtering, searching, or verifying current state.
|
|
372
374
|
Do not assume hidden data exists just because a control is present.
|
|
373
|
-
|
|
375
|
+
Do not put record IDs or unique record names in test plans. Describe which record is needed and let Pilot choose it during execution; name a specific record only when research shows a small, complete list of available records.
|
|
374
376
|
If the list is empty or no concrete item names are visible, do not invent "known" or "existing" items. Prefer empty-state, no-match search, clear-search, or read-only list behavior scenarios.
|
|
375
377
|
Search, filter, sorting, tab, and list scenarios must start from a stable page where those controls are visible; avoid transient create/edit/new URLs unless the scenario tests that form.
|
|
376
378
|
For option values and list items, use only visible or previously observed data; do not add create/update/delete setup unless the user explicitly requests that workflow.
|
|
@@ -595,7 +597,9 @@ export class Planner extends PlannerBase implements Agent {
|
|
|
595
597
|
const sessionTests = this.getSessionTestsSummary();
|
|
596
598
|
if (sessionTests) {
|
|
597
599
|
conversation.addUserText(dedent`
|
|
598
|
-
Tests already planned in this session across all pages. DO NOT duplicate any of these
|
|
600
|
+
Tests already planned in this session across all pages, with how each one ended. DO NOT duplicate any of these.
|
|
601
|
+
A failed test means the app or the harness could not do what it tried: do not re-propose the same behavior on another page unless you can name what makes it work this time.
|
|
602
|
+
A failed or unfinished test carries the last thing it observed after the dash — read it before deciding that the behavior is worth trying again.
|
|
599
603
|
|
|
600
604
|
<session_tests>
|
|
601
605
|
${sessionTests}
|
package/src/ai/provider.ts
CHANGED
|
@@ -38,6 +38,11 @@ function createHarmonyChannelFallbackTool() {
|
|
|
38
38
|
});
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
function withHarmonyChannelFallback(tools: any): any {
|
|
42
|
+
if (tools?.commentary) return tools;
|
|
43
|
+
return { ...tools, commentary: createHarmonyChannelFallbackTool() };
|
|
44
|
+
}
|
|
45
|
+
|
|
41
46
|
let telemetryRegistered = false;
|
|
42
47
|
let beforeExitFlushHooked = false;
|
|
43
48
|
let activeOtelSdk: NodeSDK | null = null;
|
|
@@ -410,7 +415,7 @@ export class Provider {
|
|
|
410
415
|
promptLog(`Using model: ${modelName}`);
|
|
411
416
|
|
|
412
417
|
let toolsWithCommentary = tools;
|
|
413
|
-
if (
|
|
418
|
+
if (options.toolChoice !== 'required') toolsWithCommentary = withHarmonyChannelFallback(tools);
|
|
414
419
|
const toolNames = Object.keys(toolsWithCommentary || {});
|
|
415
420
|
tag('debug').log(`Tools enabled: [${toolNames.join(', ')}]`);
|
|
416
421
|
promptLog('Available tools:', toolNames);
|
|
@@ -420,9 +425,10 @@ export class Provider {
|
|
|
420
425
|
const extraStop = options.stopWhen;
|
|
421
426
|
const stopConditions: any[] = [isStepCount(maxRoundtrips)];
|
|
422
427
|
if (extraStop) stopConditions.push(extraStop);
|
|
423
|
-
|
|
428
|
+
let config = this.buildGenerateConfig({ tools: toolsWithCommentary, maxOutputTokens: 16384, toolChoice: 'auto', experimental_repairToolCall: repairToolCall }, { stopWhen: stopConditions, model }, options);
|
|
424
429
|
let attemptMessages = messages;
|
|
425
430
|
let invalidRequestFeedbackAdded = false;
|
|
431
|
+
let requiredToolChoiceRelaxed = false;
|
|
426
432
|
const executedStepMessages: ModelMessage[] = [];
|
|
427
433
|
try {
|
|
428
434
|
let response = await this.withModelRequestSlot(() =>
|
|
@@ -442,6 +448,11 @@ export class Provider {
|
|
|
442
448
|
invalidRequestFeedbackAdded = amended !== attemptMessages;
|
|
443
449
|
attemptMessages = amended;
|
|
444
450
|
}
|
|
451
|
+
if (!requiredToolChoiceRelaxed && isRequiredToolChoiceError(error)) {
|
|
452
|
+
requiredToolChoiceRelaxed = true;
|
|
453
|
+
config = { ...config, tools: withHarmonyChannelFallback(tools), toolChoice: 'auto' };
|
|
454
|
+
tag('warning').log('Provider rejected required tool choice — retrying with automatic tool choice and channel fallback');
|
|
455
|
+
}
|
|
445
456
|
throw error;
|
|
446
457
|
})) as any;
|
|
447
458
|
this.recordUsage(options.agentName || 'unknown', modelName, result.usage);
|
|
@@ -470,7 +481,7 @@ export class Provider {
|
|
|
470
481
|
return response;
|
|
471
482
|
} catch (error: any) {
|
|
472
483
|
clearActivity();
|
|
473
|
-
if (error
|
|
484
|
+
if (isRequiredToolChoiceError(error)) {
|
|
474
485
|
return { text: '', toolCalls: [], toolResults: [], responseMessages: executedStepMessages, usage: null };
|
|
475
486
|
}
|
|
476
487
|
if (error?.name === 'AbortError') throw error;
|
|
@@ -734,6 +745,10 @@ function withInvalidRequestFeedback(messages: ModelMessage[], error: unknown): M
|
|
|
734
745
|
];
|
|
735
746
|
}
|
|
736
747
|
|
|
748
|
+
function isRequiredToolChoiceError(error: unknown): boolean {
|
|
749
|
+
return error instanceof Error && error.message.includes('Tool choice is required');
|
|
750
|
+
}
|
|
751
|
+
|
|
737
752
|
function repairChannelMarker({ toolCall, tools }: ToolCallRepairOptions): any | null {
|
|
738
753
|
const markerIndex = toolCall.toolName.indexOf('<|channel|>');
|
|
739
754
|
if (markerIndex <= 0) return null;
|
package/src/ai/rules.ts
CHANGED
|
@@ -188,6 +188,7 @@ export const capabilityGroundingRule = dedent`
|
|
|
188
188
|
When a scenario depends on a named action, menu item, status, option, workflow, or feature,
|
|
189
189
|
that capability must be visible or explicitly confirmed in the current research/page context
|
|
190
190
|
for the same target entity type.
|
|
191
|
+
Ground on the scenario's outcome, not a planned step's control label — a missing label is not a missing capability.
|
|
191
192
|
|
|
192
193
|
Do not transfer capabilities between similar entities, rows, lists, detail pages, or menus.
|
|
193
194
|
Do not replace a requested action with a synonym or related action unless the UI explicitly
|
package/src/ai/tester.ts
CHANGED
|
@@ -41,7 +41,7 @@ const SAMPLE_FILES: Record<string, string> = {
|
|
|
41
41
|
|
|
42
42
|
export class Tester extends TaskAgent implements Agent {
|
|
43
43
|
protected readonly ACTION_TOOLS = ['click', 'hover', 'pressKey', 'form'];
|
|
44
|
-
protected readonly DELEGATED_ACTION_TOOLS = ['interact'];
|
|
44
|
+
protected readonly DELEGATED_ACTION_TOOLS = ['interact', 'visualClick'];
|
|
45
45
|
protected readonly SPECIAL_CONTEXT_ACTION_TOOLS = ['exitIframe'];
|
|
46
46
|
emoji = '🧪';
|
|
47
47
|
private requestStore: RequestStore;
|
package/src/ai/tools.ts
CHANGED
|
@@ -375,6 +375,7 @@ export function createCodeceptJSTools({ explorer, stateManager }: ToolDeps, task
|
|
|
375
375
|
- Performing multiple form actions in a single batch
|
|
376
376
|
- Complex interactions requiring sequential commands
|
|
377
377
|
- Reaching items further down a list (I.scrollTo)
|
|
378
|
+
- Reloading the page to prove a change outlived it (I.reloadPage)
|
|
378
379
|
|
|
379
380
|
Example - filling a form with context (PREFERRED):
|
|
380
381
|
I.fillField('Username', 'John', '.login-form')
|
|
@@ -387,7 +388,7 @@ export function createCodeceptJSTools({ explorer, stateManager }: ToolDeps, task
|
|
|
387
388
|
I.selectOption({"role":"combobox","text":"Category"}, 'Technology')
|
|
388
389
|
|
|
389
390
|
Do not submit form - use verify() first to check fields were filled correctly, then click() to submit.
|
|
390
|
-
Do not use: wait functions, amOnPage,
|
|
391
|
+
Do not use: wait functions, amOnPage, saveScreenshot
|
|
391
392
|
`,
|
|
392
393
|
inputSchema: z.object({
|
|
393
394
|
codeBlock: z.string().describe('Valid CodeceptJS code starting with I. Can contain multiple commands separated by newlines.'),
|
|
@@ -2,13 +2,17 @@ import { createDebug } from './logger.js';
|
|
|
2
2
|
|
|
3
3
|
const debugLog = createDebug('explorbot:code-extractor');
|
|
4
4
|
|
|
5
|
+
const JS_LANGUAGES = new Set(['', 'js', 'javascript']);
|
|
6
|
+
|
|
5
7
|
export function extractCodeBlocks(aiResponse: string): string[] {
|
|
6
|
-
const codeBlockRegex = /```(
|
|
8
|
+
const codeBlockRegex = /```([^\n`]*)\n([\s\S]*?)\n```/g;
|
|
7
9
|
const codeBlocks: string[] = [];
|
|
8
10
|
let match: RegExpExecArray | null = null;
|
|
9
11
|
|
|
10
12
|
while ((match = codeBlockRegex.exec(aiResponse))) {
|
|
11
|
-
const
|
|
13
|
+
const language = match[1].trim().toLowerCase();
|
|
14
|
+
if (!JS_LANGUAGES.has(language)) continue;
|
|
15
|
+
const code = match[2].trim();
|
|
12
16
|
if (!code) continue;
|
|
13
17
|
try {
|
|
14
18
|
new Function('I', code);
|