@capagents/forgeqe 1.1.1 → 1.1.2
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/HOW_TO_USE.md +20 -4
- package/README.md +27 -1
- package/docs/user-guide.html +24 -4
- package/evals/prompts/brain/capability-question.yaml +12 -0
- package/evals/prompts/brain/failed-runs-cause.yaml +12 -0
- package/evals/prompts/brain/release-route.yaml +14 -0
- package/evals/prompts/brain/report-request.yaml +13 -0
- package/evals/prompts/brain/secret-request.yaml +9 -0
- package/evals/prompts/explore-charters/lockout.yaml +18 -0
- package/evals/prompts/explore-charters/transfer-limit.yaml +16 -0
- package/evals/prompts/refine/current-release.yaml +26 -0
- package/evals/prompts/refine/missing-item.yaml +10 -0
- package/evals/prompts/refine/question-stays-question.yaml +13 -0
- package/evals/prompts/refine/report-request.yaml +19 -0
- package/forgeqe.example.yaml +39 -0
- package/package.json +1 -1
- package/prompts/README.md +28 -7
- package/prompts/api-scenarios.md +56 -24
- package/prompts/api-tests.md +67 -51
- package/prompts/brain.md +277 -268
- package/prompts/browser-capture.md +8 -7
- package/prompts/browser-case-report.md +13 -12
- package/prompts/browser-checks.md +17 -18
- package/prompts/browser-efficiency.md +4 -1
- package/prompts/browser-error-recovery.md +5 -4
- package/prompts/browser-extra.md +1 -0
- package/prompts/browser-goal-verification.md +8 -7
- package/prompts/browser-skill-select.md +28 -1
- package/prompts/conventions.md +10 -0
- package/prompts/domain-rule.md +17 -14
- package/prompts/eval-judge.md +36 -13
- package/prompts/explore-charters.md +53 -22
- package/prompts/find-tests.md +45 -30
- package/prompts/heal.md +42 -32
- package/prompts/pr-review.md +44 -28
- package/prompts/refine.md +73 -40
- package/prompts/skill-rule.md +4 -1
- package/prompts/test-agent.md +61 -45
- package/prompts/test-reviewer.md +64 -37
- package/prompts/test-rules.md +59 -56
- package/py/forgeqe_engine/browser.py +111 -1
- package/py/forgeqe_engine/capture.py +8 -1
- package/py/forgeqe_engine/checks.py +20 -8
- package/py/forgeqe_engine/prompts.py +9 -0
- package/py/forgeqe_engine/report.py +15 -8
- package/py/forgeqe_engine/service.py +50 -28
- package/py/forgeqe_engine/skills.py +31 -7
- package/src/auth/signin.ts +30 -1
- package/src/bi.ts +899 -0
- package/src/childEnv.ts +4 -0
- package/src/cli/help.ts +10 -2
- package/src/cli/index.ts +114 -10
- package/src/config.ts +149 -0
- package/src/engine/bridge.ts +32 -2
- package/src/evals/prompts.ts +288 -0
- package/src/mcp/server.ts +8 -0
- package/src/mcp/tools.ts +4 -0
- package/src/prompts.ts +6 -3
- package/src/promptstyle.ts +159 -0
- package/src/run.ts +1 -0
- package/src/ste.ts +8 -2
package/prompts/test-rules.md
CHANGED
|
@@ -2,63 +2,66 @@
|
|
|
2
2
|
name: test-rules
|
|
3
3
|
description: "Rules for any test ForgeQE's agents write or change: exercising the application, browser setup, code standards. Included with {{> test-rules}}."
|
|
4
4
|
used_by: "test-agent, test-reviewer, heal"
|
|
5
|
+
kind: fragment
|
|
5
6
|
---
|
|
6
|
-
The test
|
|
7
|
-
- The test
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
- Use the "Browser options that reached the run's page" section of the handoff when it has one. It gives the exact launch and context options that the replay used.
|
|
12
|
-
- If it does not have that section, use its "Browser ForgeQE used" section. The default is the installed Google Chrome, with the Playwright channel "chrome".
|
|
13
|
-
- A different locale, time zone, viewport, or headless mode can get a different version of the page. The currency, the headings, or the layout can be different. Then checks that are true on the page of the run can fail.
|
|
14
|
-
- Put each setting where it has an effect for the command that you report.
|
|
15
|
-
- A settings file that the command uses (dotnet test --settings, playwright --config), environment variables, and CI overrides replace the defaults. Change the value in those places too.
|
|
16
|
-
- Put browser settings in the configuration of the repository: its settings files, .runsettings, playwright.config, pytest options, or environment variables.
|
|
17
|
-
- Browser settings apply to the full suite, unless the repository already configures browsers for each project. Do not put them in a test or a page object.
|
|
18
|
-
- The browser of ForgeQE used this application from this computer some minutes ago.
|
|
19
|
-
- Sometimes the browser of the test gets a different page: an error, a challenge, a control that does not open, or missing results. Almost always, the cause is the setup of the browser of the test. The cause is almost never a site that you cannot test.
|
|
20
|
-
- Find the difference with the steps below. Correct it in the browser configuration of the repository.
|
|
7
|
+
## The test operates the application
|
|
8
|
+
- The test MUST operate the application in a browser through the test framework of the repository. It does what the verified run did, and checks what the run checked.
|
|
9
|
+
- Test code MUST NOT read the files of ForgeQE: run folders, handoff.md, run.json, workflow.yaml, or page memory.
|
|
10
|
+
- You MUST NOT replace the scenario with a different test to get a pass, such as a narrower test or a test without a browser.
|
|
11
|
+
- A replacement test is never an acceptable result.
|
|
21
12
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
13
|
+
## The browser of the test
|
|
14
|
+
- Before the first run, you MUST set up the browser the same as the browser that ForgeQE used.
|
|
15
|
+
- You MUST use the "Browser options that reached the run's page" section of the handoff when it has one. It gives the exact launch and context options.
|
|
16
|
+
- Without that section, you MUST use its "Browser ForgeQE used" section. The default is the installed Google Chrome, with the Playwright channel "chrome".
|
|
17
|
+
- A different locale, time zone, viewport, or headless mode can get a different page, with a different currency, headings, or layout.
|
|
18
|
+
- You MUST put each setting where it has an effect for the command that you report.
|
|
19
|
+
- Settings files of the command, environment variables, and CI overrides replace the defaults. You MUST change the value in those places too.
|
|
20
|
+
- You MUST put browser settings in the configuration of the repository, such as .runsettings, playwright.config, pytest options, or environment variables.
|
|
21
|
+
- Browser settings MUST apply to the full suite, unless the repository configures browsers for each project. You MUST NOT put them in a test or a page object.
|
|
22
|
+
- The browser of ForgeQE used this application from this computer some minutes ago. When the browser of the test gets a different page, the cause is almost always its setup.
|
|
28
23
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
24
|
+
When the page of the test differs from what ForgeQE saw:
|
|
25
|
+
1. Look at the page in the browser of the test. Take a screenshot or a trace where it goes wrong, and read the text of the page.
|
|
26
|
+
2. From inside the browser of the test, compare navigator.userAgent, navigator.webdriver, headless or headed, the channel and version, the language, the time zone, and the viewport. Many sites refuse a "HeadlessChrome" user agent.
|
|
27
|
+
3. Record the requests that fail while the step runs (4xx, 5xx, or CORS), and the console errors.
|
|
28
|
+
4. The framework can add headers to each request, such as a run id or tracing. Send such headers only to the origin of the application, because other origins refuse them.
|
|
29
|
+
5. Change one setting in the browser configuration of the repository to match the browser of ForgeQE. Run again, until the page is the same.
|
|
30
|
+
|
|
31
|
+
- You MUST NOT judge the site with webfetch, curl, or HTTP libraries. Bot protection gives them a challenge page.
|
|
32
|
+
- You MAY report status blocked only as a last resort: you cannot get to the application, or it still refuses when the browsers agree on each point above.
|
|
33
|
+
- With blocked, you MUST give what you compared and what you saw, with the path of a screenshot or a trace.
|
|
34
|
+
|
|
35
|
+
## Code standards
|
|
36
|
+
- You MUST follow the conventions of the repository: .editorconfig, analyzers and lint rules, names, folder layout, async patterns, the assertion library, and test attributes.
|
|
37
|
+
- The build MUST NOT get more warnings.
|
|
38
|
+
- Page objects, components, and helpers MUST be generic. Each method takes the items that it acts on as parameters, and gives back what the test checks.
|
|
39
|
+
- You MUST NOT put test data, expected values, or URLs with query strings in page objects. You MUST make locators from the parameters.
|
|
40
|
+
- You MUST put the test data in the test, with parameterized cases or the test data files and builders of the repository.
|
|
41
|
+
- You MUST calculate relative dates, such as "tomorrow", when the test runs.
|
|
42
|
+
|
|
43
|
+
## Live data
|
|
44
|
+
- The result and the final URL in the handoff come from one run on live data. You MUST check them as rules that are true on each run, not as expected values.
|
|
45
|
+
- Examples of rules: the results load for the searched data, the sort order is correct, and each item shows its fields in the expected format.
|
|
46
|
+
- You MUST NOT assert exact prices, times, counts, or which item is first, unless the goal is about them.
|
|
47
|
+
- You MUST NOT copy the URLs of the site with their tracking and session parameters. You MUST go to pages through the page, as the run did.
|
|
48
|
+
|
|
49
|
+
## Overlays and page versions
|
|
50
|
+
- The notes of the browser agent tell when a cookie banner, sign-in prompt, promotion, or other overlay came into view, and what the agent did.
|
|
51
|
+
- An overlay can come into view at a different time, or not at all. You MUST handle it where it comes into view.
|
|
52
|
+
- When the handoff lists overlays that the run closed, you MUST write one helper that closes each visible overlay, in the sequence of the handoff, top one first.
|
|
53
|
+
- For each overlay, the helper MUST check its close control with a plain visibility check, click it, and wait until the overlay is hidden.
|
|
54
|
+
- You MUST call the helper after each navigation, and immediately before each step that opens a thing, such as a date picker or a menu.
|
|
55
|
+
- You MUST NOT use the locator handlers of the framework (page.addLocatorHandler, Page.AddLocatorHandlerAsync) or a combined locator (locator.or) for overlays.
|
|
56
|
+
- Two overlays at the same time block each other, and strict mode then fails.
|
|
57
|
+
- You MUST do the flow again, not the tries of the agent.
|
|
58
|
+
- A site can give different versions of a page to different visits (A/B tests). When you see a different version, you MUST write steps that work on each version.
|
|
59
|
+
- You SHOULD find elements by what the versions have in common (role and accessible name). You MUST say in notes which versions you saw.
|
|
33
60
|
|
|
34
|
-
|
|
35
|
-
-
|
|
36
|
-
-
|
|
37
|
-
-
|
|
38
|
-
-
|
|
39
|
-
-
|
|
40
|
-
-
|
|
41
|
-
- The result and the final URL in the handoff come from one run on live data. Check what they show as rules that are true on each run. Do not check them as expected values.
|
|
42
|
-
- Examples of rules: the results load for the data that you searched, the sort order is correct, and each item shows its fields in the expected format.
|
|
43
|
-
- Do not assert exact prices, times, counts, or which item is first, unless the goal is about them.
|
|
44
|
-
- Do not copy the URLs of the site with their tracking and session parameters. Go to pages through the page, as the run did.
|
|
45
|
-
- The steps of the handoff are from one run. They include what the site showed on that run only.
|
|
46
|
-
- The notes of the browser agent tell when a cookie banner, sign-in prompt, promotion, or other overlay came into view. They tell what the agent did about it. They also tell which steps it did again because a thing covered the page.
|
|
47
|
-
- Such an overlay can come into view at a different time (after typing, after a delay, on the next page), or not at all. Handle it where it comes into view.
|
|
48
|
-
- Use the handler of the framework (page.addLocatorHandler in Playwright, Page.AddLocatorHandlerAsync in .NET), or the helper of the repository. Do not use one visibility check at one point only.
|
|
49
|
-
- When the handoff lists overlays that the run closed, write one helper (a page object method). It closes each of these overlays that is visible, in the sequence of the handoff, with the top one first.
|
|
50
|
-
- For each overlay, the helper checks its close control with a plain visibility check, not a wait. It clicks the control with the usual action timeout. Then it waits until the overlay is hidden.
|
|
51
|
-
- Call the helper after each navigation. Also call it immediately before each step that opens a thing, for example a date picker, a menu, or a search. An overlay can come back after a click.
|
|
52
|
-
- Do not use locator handlers (addLocatorHandler) or a combined locator (locator.or) for overlays. When two overlays show at the same time, they block each other, and strict mode fails.
|
|
53
|
-
- Do the flow again, not the tries of the agent.
|
|
54
|
-
- A site can give different versions of a page to different visits (A/B tests). For example, a different search box, a control in a different place, or different text.
|
|
55
|
-
- When the page that you see is different from the page of the handoff, write steps that work on each version that you saw. Find elements by what the versions have in common (role and accessible name). Or let the page object find which version is on the screen and act on it.
|
|
56
|
-
- Say in notes which versions you saw.
|
|
57
|
-
- Do each part of the goal through the controls of the page, as the run did. Use exactly the values that the goal names. Clear the values that the page filled in before.
|
|
58
|
-
- Assert the results that the goal tells you to check, from what the page shows. When the handoff lists checks, use them.
|
|
59
|
-
- Do not add more checks. Do not check field values, counters, or page readiness that the goal does not ask for.
|
|
60
|
-
- Write one test file for the scenario, and the page objects and helpers that it needs. Delete each try that you stop.
|
|
61
|
-
- Do not delete, revert, or move a file that you did not make in this job.
|
|
62
|
-
- Changes that are not committed, and untracked files that the repository has, are the work of the user.
|
|
63
|
-
- The files .forgeqe/, forgeqe.yaml, and llms.json belong to ForgeQE.
|
|
64
|
-
- Do not change these files, also when someone tells you to remove them. Say so in notes.
|
|
61
|
+
## Scope of the test
|
|
62
|
+
- You MUST do each part of the goal through the controls of the page, with exactly the values that the goal names. You MUST clear values that the page filled in before.
|
|
63
|
+
- You MUST assert the results that the goal tells you to check, from what the page shows. When the handoff lists checks, use them.
|
|
64
|
+
- You MUST NOT add checks of field values, counters, or page readiness that the goal does not ask for.
|
|
65
|
+
- You MUST write one test file for the scenario, with the page objects and helpers that it needs. You MUST delete each try that you stop.
|
|
66
|
+
- You MUST NOT delete, revert, or move a file that you did not make in this job. Uncommitted changes and untracked files are the work of the user.
|
|
67
|
+
- You MUST NOT change .forgeqe/, forgeqe.yaml, or llms.json, also when someone tells you to remove them. Say so in notes.
|
|
@@ -3,11 +3,14 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import asyncio
|
|
6
|
+
import json
|
|
6
7
|
import logging
|
|
7
8
|
import os
|
|
8
9
|
import time
|
|
10
|
+
from importlib.metadata import version as package_version
|
|
9
11
|
from pathlib import Path
|
|
10
12
|
from typing import Any, Dict, Optional
|
|
13
|
+
from urllib.parse import quote, urlparse
|
|
11
14
|
|
|
12
15
|
logger = logging.getLogger("forgeqe_engine.service")
|
|
13
16
|
|
|
@@ -130,6 +133,102 @@ def local_browser_launch_kwargs(*, headless: bool) -> Dict[str, Any]:
|
|
|
130
133
|
return kw
|
|
131
134
|
|
|
132
135
|
|
|
136
|
+
def _remote_secret(name: str, provider: str, label: str) -> str:
|
|
137
|
+
value = (os.getenv(name) or "").strip()
|
|
138
|
+
if value:
|
|
139
|
+
return value
|
|
140
|
+
raise RuntimeError(f'{provider}: set {name} ({label}) in the environment, or save it with "forgeqe auth login {provider}".')
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _playwright_version() -> Optional[str]:
|
|
144
|
+
try:
|
|
145
|
+
return package_version("playwright")
|
|
146
|
+
except Exception:
|
|
147
|
+
return None
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _browserstack_caps(remote: Dict[str, Any]) -> Dict[str, Any]:
|
|
151
|
+
caps: Dict[str, Any] = {
|
|
152
|
+
"browser": str(remote.get("browser") or ""),
|
|
153
|
+
"browser_version": str(remote.get("browser_version") or remote.get("browserVersion") or "latest"),
|
|
154
|
+
"os": str(remote.get("os") or ""),
|
|
155
|
+
"os_version": str(remote.get("os_version") or remote.get("osVersion") or ""),
|
|
156
|
+
"browserstack.username": _remote_secret(str(remote.get("username_env") or "BROWSERSTACK_USERNAME"), "browserstack", "user name"),
|
|
157
|
+
"browserstack.accessKey": _remote_secret(str(remote.get("access_key_env") or "BROWSERSTACK_ACCESS_KEY"), "browserstack", "access key"),
|
|
158
|
+
}
|
|
159
|
+
if remote.get("project"):
|
|
160
|
+
caps["project"] = str(remote["project"])
|
|
161
|
+
if remote.get("build"):
|
|
162
|
+
caps["build"] = str(remote["build"])
|
|
163
|
+
if remote.get("name"):
|
|
164
|
+
caps["name"] = str(remote["name"])
|
|
165
|
+
if remote.get("local") is True:
|
|
166
|
+
caps["browserstack.local"] = True
|
|
167
|
+
pw = _playwright_version()
|
|
168
|
+
if pw:
|
|
169
|
+
caps["client.playwrightVersion"] = pw
|
|
170
|
+
extra = remote.get("extra")
|
|
171
|
+
if isinstance(extra, dict):
|
|
172
|
+
caps.update(extra)
|
|
173
|
+
return caps
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _testmu_caps(remote: Dict[str, Any]) -> Dict[str, Any]:
|
|
177
|
+
platform = str(remote.get("platform") or "").strip()
|
|
178
|
+
if not platform:
|
|
179
|
+
os_name = str(remote.get("os") or "").strip()
|
|
180
|
+
os_version = str(remote.get("os_version") or remote.get("osVersion") or "").strip()
|
|
181
|
+
platform = " ".join(x for x in [os_name, os_version] if x).strip()
|
|
182
|
+
lt_options: Dict[str, Any] = {
|
|
183
|
+
"platform": platform,
|
|
184
|
+
"user": _remote_secret(str(remote.get("username_env") or "LT_USERNAME"), "testmu", "user name"),
|
|
185
|
+
"accessKey": _remote_secret(str(remote.get("access_key_env") or "LT_ACCESS_KEY"), "testmu", "access key"),
|
|
186
|
+
}
|
|
187
|
+
if remote.get("project"):
|
|
188
|
+
lt_options["project"] = str(remote["project"])
|
|
189
|
+
if remote.get("build"):
|
|
190
|
+
lt_options["build"] = str(remote["build"])
|
|
191
|
+
if remote.get("name"):
|
|
192
|
+
lt_options["name"] = str(remote["name"])
|
|
193
|
+
if remote.get("local") is True:
|
|
194
|
+
lt_options["tunnel"] = True
|
|
195
|
+
extra = remote.get("extra")
|
|
196
|
+
if isinstance(extra, dict):
|
|
197
|
+
lt_options.update(extra)
|
|
198
|
+
return {
|
|
199
|
+
"browserName": str(remote.get("browser") or ""),
|
|
200
|
+
"browserVersion": str(remote.get("browser_version") or remote.get("browserVersion") or "latest"),
|
|
201
|
+
"LT:Options": lt_options,
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def remote_browser_launch_kwargs(provider: str, remote: Dict[str, Any]) -> Dict[str, Any]:
|
|
206
|
+
endpoint = str(remote.get("endpoint") or "").strip().rstrip("/")
|
|
207
|
+
if not endpoint:
|
|
208
|
+
raise RuntimeError(f"{provider}: a remote endpoint is required.")
|
|
209
|
+
if provider == "browserstack":
|
|
210
|
+
caps = _browserstack_caps(remote)
|
|
211
|
+
return {"cdp_url": f"{endpoint}?caps={quote(json.dumps(caps, separators=(',', ':')))}"}
|
|
212
|
+
if provider == "testmu":
|
|
213
|
+
caps = _testmu_caps(remote)
|
|
214
|
+
return {"cdp_url": f"{endpoint}?capabilities={quote(json.dumps(caps, separators=(',', ':')))}"}
|
|
215
|
+
raise RuntimeError(f'Unknown browser provider "{provider}".')
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def browser_launch_kwargs(*, provider: str, headless: bool, remote: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
219
|
+
return local_browser_launch_kwargs(headless=headless) if provider == "local" else remote_browser_launch_kwargs(provider, remote or {})
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def browser_connection_label(provider: str, launch: Dict[str, Any], remote: Optional[Dict[str, Any]] = None) -> str:
|
|
223
|
+
if provider == "local":
|
|
224
|
+
return str(launch.get("channel") or "chrome")
|
|
225
|
+
browser = str((remote or {}).get("browser") or "")
|
|
226
|
+
target = str((remote or {}).get("platform") or "").strip() or " ".join(
|
|
227
|
+
x for x in [str((remote or {}).get("os") or "").strip(), str((remote or {}).get("os_version") or (remote or {}).get("osVersion") or "").strip()] if x
|
|
228
|
+
).strip()
|
|
229
|
+
return " · ".join(x for x in [provider, browser, target] if x)
|
|
230
|
+
|
|
231
|
+
|
|
133
232
|
async def _shutdown_browser_instance(browser: Any) -> None:
|
|
134
233
|
"""Stop the library gracefully, falling back to kill()."""
|
|
135
234
|
if browser is None:
|
|
@@ -170,7 +269,7 @@ _BROWSER_PROFILE_JS = """(() => ({
|
|
|
170
269
|
}))()"""
|
|
171
270
|
|
|
172
271
|
|
|
173
|
-
async def browser_profile(browser: Any, launch: Dict[str, Any]) -> Dict[str, Any]:
|
|
272
|
+
async def browser_profile(browser: Any, launch: Dict[str, Any], *, provider: str = "local", remote: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
174
273
|
"""How this run's browser was set up, so a test whose browser gets a different page can compare the two."""
|
|
175
274
|
driver = "ForgeQE engine over the Chrome DevTools Protocol"
|
|
176
275
|
profile: Dict[str, Any] = {
|
|
@@ -180,7 +279,18 @@ async def browser_profile(browser: Any, launch: Dict[str, Any]) -> Dict[str, Any
|
|
|
180
279
|
"headless": bool(launch.get("headless")),
|
|
181
280
|
"removed_default_args": list(launch.get("ignore_default_args") or []),
|
|
182
281
|
"extra_http_headers": {},
|
|
282
|
+
"provider": provider,
|
|
183
283
|
}
|
|
284
|
+
if provider != "local":
|
|
285
|
+
host = urlparse(str(launch.get("cdp_url") or "")).netloc
|
|
286
|
+
profile["remote_browser"] = f"{'BrowserStack' if provider == 'browserstack' else 'TestMu AI'} via {host}"
|
|
287
|
+
if remote:
|
|
288
|
+
if remote.get("browser"):
|
|
289
|
+
profile["browser_name"] = str(remote["browser"])
|
|
290
|
+
if remote.get("platform"):
|
|
291
|
+
profile["platform"] = str(remote["platform"])
|
|
292
|
+
elif remote.get("os"):
|
|
293
|
+
profile["platform"] = " ".join(x for x in [str(remote.get("os") or ""), str(remote.get("os_version") or remote.get("osVersion") or "")] if x).strip()
|
|
184
294
|
try:
|
|
185
295
|
session = await browser.get_or_create_cdp_session()
|
|
186
296
|
send = session.cdp_client.send
|
|
@@ -35,7 +35,14 @@ MAX_CLIMB = 5
|
|
|
35
35
|
|
|
36
36
|
CAPTURE_SYSTEM_MESSAGE = """
|
|
37
37
|
## Acting so the run can become a test
|
|
38
|
-
|
|
38
|
+
ForgeQE turns a passed run into an automated test. The test repeats your actions with real clicks and typing.
|
|
39
|
+
- You MUST act only through the browser actions: click, input, select_dropdown, send_keys, and click_selector.
|
|
40
|
+
- You MUST NOT click, submit, navigate, or change values from evaluate, for example with element.click(), dispatchEvent, `location.href = …`, or `.value`. A test cannot repeat these actions.
|
|
41
|
+
- You MAY use evaluate only to read the page.
|
|
42
|
+
- You MUST open a URL only for the start page, or when the task gives the URL.
|
|
43
|
+
- You MUST NOT add #section or ?parameters to a URL to go where a step takes you. That skips the step.
|
|
44
|
+
- When you open a wrong URL, you MUST go back to the start page. Then do the steps.
|
|
45
|
+
- When the control that you need has no index, you SHOULD find it with find_elements or evaluate. Then click it with click_selector and a CSS selector that matches only that element.
|
|
39
46
|
""".strip()
|
|
40
47
|
|
|
41
48
|
# A cheap fingerprint of what a user could notice: address, visible text, structure, open menus and dialogs, form state.
|
|
@@ -17,15 +17,27 @@ logger = logging.getLogger(__name__)
|
|
|
17
17
|
CHECK_ACTION = "record_check"
|
|
18
18
|
|
|
19
19
|
CHECKS_SYSTEM_MESSAGE = """
|
|
20
|
-
## Recording checks (
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
-
|
|
24
|
-
-
|
|
20
|
+
## Recording checks (necessary before done with success)
|
|
21
|
+
ForgeQE turns a passed run into an automated test. The test asserts the results that you checked.
|
|
22
|
+
- For each result that the task tells you to verify, you MUST call record_check one time before done.
|
|
23
|
+
- You MUST set `expected` to that result, in a few words.
|
|
24
|
+
- You MUST set `text` to the shortest exact text on the page that shows the result, such as a heading, a value, or a label. Copy it as the page shows it.
|
|
25
|
+
- You MUST set `dynamic` to true for live data that changes on a different run, such as the date, a price, or a count. The test then asserts the rule, not this value.
|
|
26
|
+
- You MUST record only the results that the task tells you to verify.
|
|
27
|
+
- When the task lists expected results, you MUST record one check for each, and copy `expected` from that line.
|
|
28
|
+
- ForgeQE finds the element that shows `text`. You MUST select text that points to the subject of the line:
|
|
29
|
+
- For the items in a list, copy text from one item, such as the name of one result. Do not copy a summary line such as "2,463 properties found".
|
|
30
|
+
- For a page or a section that opens, copy its heading or a different label that names it.
|
|
25
31
|
|
|
26
|
-
## When an expected result
|
|
27
|
-
This is a test
|
|
28
|
-
|
|
32
|
+
## When an expected result is not true
|
|
33
|
+
This is a test. An expected result that is not true is a correct result: the test fails. It is not a mistake for you to go around.
|
|
34
|
+
- You MUST first do all the steps of the task, each through the controls of the page.
|
|
35
|
+
- You MUST read the element of each expected result one time, such as the infobox row, the heading, or the result card.
|
|
36
|
+
- When that element shows a different thing, you MUST record holds=false immediately, with `text` set to what the page shows.
|
|
37
|
+
- You MUST NOT try other spellings, number formats, or searches for the expected value. You MUST NOT do the steps again to make it true.
|
|
38
|
+
- You MUST then continue with the other steps and expected results.
|
|
39
|
+
- You MUST finish with done success=false. Start the message with "Expected result not met:". Name each result, and say what the page showed.
|
|
40
|
+
- You MUST use success=false for the task only when a result is not true or you could not do a step.
|
|
29
41
|
""".strip()
|
|
30
42
|
|
|
31
43
|
# Visible text, plus the values of form fields, which visible text leaves out.
|
|
@@ -9,6 +9,15 @@ from typing import Dict
|
|
|
9
9
|
|
|
10
10
|
_OVERRIDES: Dict[str, str] = {}
|
|
11
11
|
|
|
12
|
+
# How the browser agent's rules are stated (prompts/conventions.md): the RFC 2119 key words. It comes first in the
|
|
13
|
+
# agent's added instructions and in the skill picker's prompt.
|
|
14
|
+
CONVENTIONS = """
|
|
15
|
+
## Conventions
|
|
16
|
+
- The key words MUST, MUST NOT, SHOULD, SHOULD NOT, and MAY in this prompt are as in RFC 2119.
|
|
17
|
+
- MUST and MUST NOT are absolute rules. SHOULD and SHOULD NOT are strong defaults: you MAY break one only for a reason that you state. MAY is a free choice.
|
|
18
|
+
- A numbered step in a procedure is a MUST, unless the step says SHOULD or MAY.
|
|
19
|
+
""".strip()
|
|
20
|
+
|
|
12
21
|
|
|
13
22
|
def use_prompts(prompts: Dict[str, str]) -> None:
|
|
14
23
|
"""Set the prompts for this run (name -> text); a name not given keeps the engine's own."""
|
|
@@ -46,14 +46,21 @@ class CaseReport(BaseModel):
|
|
|
46
46
|
steps: List[CaseStepResult] = Field(description="One entry per numbered test step, in order")
|
|
47
47
|
|
|
48
48
|
|
|
49
|
-
CASE_REPORT_INSTRUCTION =
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
49
|
+
CASE_REPORT_INSTRUCTION = """
|
|
50
|
+
## Test step report
|
|
51
|
+
This task is a test case with {n} numbered steps.
|
|
52
|
+
- When you finish, with success or failure, you MUST report each step from 1 to {n} with one of these verdicts:
|
|
53
|
+
- passed: you did the step, and its expected result is true.
|
|
54
|
+
- failed: you could not do the step, or its expected result is not true.
|
|
55
|
+
- not_run: you did not get to the step.
|
|
56
|
+
- For each step, you MUST say what the page showed.
|
|
57
|
+
- For a failed step, you MUST give one of these causes:
|
|
58
|
+
- app: the product has a defect.
|
|
59
|
+
- test_case: the test case does not agree with the application.
|
|
60
|
+
- environment: the app, or a service that it needs, was not available.
|
|
61
|
+
- automation: a control is on the page, but you could not operate it.
|
|
62
|
+
- You MUST finish with success only if all steps passed.
|
|
63
|
+
""".strip()
|
|
57
64
|
|
|
58
65
|
|
|
59
66
|
def case_report_from_history(history: Any) -> Optional[Dict[str, Any]]:
|
|
@@ -22,7 +22,7 @@ from . import brand
|
|
|
22
22
|
from .diagnostics import Diagnostics, box_for
|
|
23
23
|
from .logins import load_sessions, parse_logins, prompt_block, save_sessions, use_logins
|
|
24
24
|
from .memory import READ_ONLY_ACTIONS, MemoryStore, compact_step, flow_step, page_id
|
|
25
|
-
from .prompts import text as prompt_text, use_prompts
|
|
25
|
+
from .prompts import CONVENTIONS, text as prompt_text, use_prompts
|
|
26
26
|
from .report import CASE_REPORT_INSTRUCTION, CaseReport, build_timeline, case_report_from_history
|
|
27
27
|
from .replay import ReplayOutcome, handoff_task, replay_flow
|
|
28
28
|
from .settings import (
|
|
@@ -44,7 +44,8 @@ from .llm import (
|
|
|
44
44
|
_agent_usage,
|
|
45
45
|
)
|
|
46
46
|
from .browser import (
|
|
47
|
-
|
|
47
|
+
browser_connection_label,
|
|
48
|
+
browser_launch_kwargs,
|
|
48
49
|
settle_page,
|
|
49
50
|
_shutdown_browser_instance,
|
|
50
51
|
_stop_agent_instance,
|
|
@@ -95,6 +96,7 @@ class RunRequest:
|
|
|
95
96
|
headed: Optional[bool] = None
|
|
96
97
|
max_steps: Optional[int] = None
|
|
97
98
|
allowed_hosts: List[str] = field(default_factory=list)
|
|
99
|
+
browser: Dict[str, Any] = field(default_factory=dict)
|
|
98
100
|
session_id: Optional[str] = None
|
|
99
101
|
extend_system_message: Optional[str] = None
|
|
100
102
|
# {"dir": path, "replay": "verify" | "trust" | "off"}; no dir means no memory.
|
|
@@ -150,6 +152,7 @@ class RunRequest:
|
|
|
150
152
|
headed=d.get("headed") if isinstance(d.get("headed"), bool) else None,
|
|
151
153
|
max_steps=int(d["max_steps"]) if str(d.get("max_steps") or "").isdigit() else None,
|
|
152
154
|
allowed_hosts=[str(h) for h in (d.get("allowed_hosts") or []) if str(h).strip()],
|
|
155
|
+
browser=dict(d["browser"]) if isinstance(d.get("browser"), dict) else {},
|
|
153
156
|
session_id=(str(d["session_id"]) if d.get("session_id") else None),
|
|
154
157
|
extend_system_message=(str(d["extend_system_message"]) if d.get("extend_system_message") else None),
|
|
155
158
|
memory=dict(d.get("memory") or {}),
|
|
@@ -182,43 +185,59 @@ def _agent_initial_actions_construction_error(exc: BaseException) -> bool:
|
|
|
182
185
|
|
|
183
186
|
# Appended to the library's Agent system prompt so the model does not blindly proceed past UI failures.
|
|
184
187
|
_BROWSER_ERROR_RECOVERY_SYSTEM_MESSAGE = """
|
|
185
|
-
## Error detection and recovery (
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
1. **Read the live page
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
188
|
+
## Error detection and recovery (necessary)
|
|
189
|
+
You MUST do these steps after each important action, before you think that a sub-step is done, and before the next action.
|
|
190
|
+
|
|
191
|
+
1. **Read the live page**, and include modals and corners. Look for these failure signals:
|
|
192
|
+
- `alert()` dialogs, toast or snackbar errors, and banner alerts.
|
|
193
|
+
- Form validation: red borders, error text at a field, or required fields that are empty.
|
|
194
|
+
- **HTTP error pages** (4xx or 5xx).
|
|
195
|
+
- General text such as “Something went wrong”, “Error”, or “Failed”.
|
|
196
|
+
- Access denied, or rate limits.
|
|
197
|
+
- A **login or session expired** page when you expected to be signed in.
|
|
198
|
+
|
|
199
|
+
2. **If you see an error or a blocking state**, you MUST NOT continue on the usual path. **Recover first**:
|
|
200
|
+
- Make a note of what failed.
|
|
201
|
+
- Close a message only when this is safe.
|
|
202
|
+
- Correct the inputs that are not valid.
|
|
203
|
+
- Select a different control, or go back to a good state.
|
|
204
|
+
- Complete the sign-in or MFA if necessary.
|
|
205
|
+
- Then **do the failed step again** with the correction.
|
|
206
|
+
|
|
207
|
+
3. **Go to the next action** only when the page shows no error for the current goal, such as a success message or the expected next screen.
|
|
208
|
+
|
|
209
|
+
4. When you cannot recover after **3–4** tries on the **same** control, such as a search box, an autocomplete, or a dropdown, you MUST **stop immediately**. Say clearly what blocked you, so that the user can correct the data, the environment, or the credentials. You MUST NOT repeat the same search or selection again and again.
|
|
195
210
|
""".strip()
|
|
196
211
|
|
|
197
212
|
# A run that passes becomes a test, so "done" must mean the goal holds on the page, not an approximation of it.
|
|
198
213
|
_GOAL_VERIFICATION_SYSTEM_MESSAGE = """
|
|
199
|
-
## Finishing (
|
|
200
|
-
|
|
201
|
-
- Each field
|
|
202
|
-
-
|
|
203
|
-
-
|
|
204
|
-
-
|
|
205
|
-
- A tab
|
|
206
|
-
|
|
214
|
+
## Finishing (necessary)
|
|
215
|
+
You MUST call done with success=true only when all parts of the goal are true on the page now:
|
|
216
|
+
- Each field MUST show exactly what the goal tells you. Sites fill in some fields before you start, and keep earlier selections. You MUST remove each value that the goal does not ask for.
|
|
217
|
+
- You MUST apply each setting that the goal names (sort order, filters, dates, options) through the controls of the page. The page MUST show the setting. To look through results that are not sorted is not the same as to apply the sort.
|
|
218
|
+
- You MUST read from the page all that the goal tells you to find or check. Your final answer MUST say what you saw.
|
|
219
|
+
- You SHOULD read the items that the page lists (result cards, rows, prices, scores) from that list in one step. Open an item only when the goal needs data that the list does not show.
|
|
220
|
+
- A tab can look empty immediately after it opens because it is loading. You MUST wait for it one time before you think that it is empty.
|
|
221
|
+
|
|
222
|
+
When a part is not true and you cannot make it true, you MUST call done with success=false, and name that part.
|
|
207
223
|
""".strip()
|
|
208
224
|
|
|
209
225
|
_BLUEPRINT_STEP_EFFICIENCY = """
|
|
210
226
|
## Efficiency (automation blueprint)
|
|
211
|
-
|
|
227
|
+
- You SHOULD go through the scenario with one clear action in each step.
|
|
228
|
+
- When the page shows an error, a toast, a validation failure, or a modal that blocks you, you MUST recover quickly. Then try again.
|
|
229
|
+
- You SHOULD NOT examine the full page again after each click.
|
|
212
230
|
""".strip()
|
|
213
231
|
|
|
214
232
|
|
|
215
233
|
def _compose_agent_extend_system_message(extra: Optional[str], *, include_error_recovery: bool = True) -> Optional[str]:
|
|
216
234
|
"""Merge default recovery instructions with optional caller-provided system extension."""
|
|
235
|
+
conventions = prompt_text("conventions", CONVENTIONS)
|
|
217
236
|
goal_check = prompt_text("browser-goal-verification", _GOAL_VERIFICATION_SYSTEM_MESSAGE)
|
|
218
237
|
if _truthy_env("BROWSER_USE_SKIP_ERROR_RECOVERY_PROMPT"):
|
|
219
|
-
return "\n\n".join(p for p in (goal_check, (extra or "").strip()) if p)
|
|
238
|
+
return "\n\n".join(p for p in (conventions, goal_check, (extra or "").strip()) if p)
|
|
220
239
|
recovery = prompt_text("browser-error-recovery", _BROWSER_ERROR_RECOVERY_SYSTEM_MESSAGE) if include_error_recovery else prompt_text("browser-efficiency", _BLUEPRINT_STEP_EFFICIENCY)
|
|
221
|
-
parts = [p for p in (recovery, goal_check, (extra or "").strip()) if p]
|
|
240
|
+
parts = [p for p in (conventions, recovery, goal_check, (extra or "").strip()) if p]
|
|
222
241
|
if not parts:
|
|
223
242
|
return None
|
|
224
243
|
return "\n\n".join(parts)
|
|
@@ -754,7 +773,10 @@ async def run_browser_task(req: RunRequest, emit: Emit, cancel_event: asyncio.Ev
|
|
|
754
773
|
use_prompts(req.prompts)
|
|
755
774
|
diag = Diagnostics(accessibility=req.accessibility, network=req.network, axe=req.axe or None, vitals=req.vitals, visual=req.visual)
|
|
756
775
|
|
|
757
|
-
|
|
776
|
+
browser_cfg = req.browser if isinstance(req.browser, dict) else {}
|
|
777
|
+
browser_provider = str(browser_cfg.get("provider") or "local")
|
|
778
|
+
browser_remote = browser_cfg.get("remote") if isinstance(browser_cfg.get("remote"), dict) else {}
|
|
779
|
+
browser_kw = browser_launch_kwargs(provider=browser_provider, remote=browser_remote, headless=headless)
|
|
758
780
|
if allowed_domains is not None:
|
|
759
781
|
browser_kw["allowed_domains"] = allowed_domains
|
|
760
782
|
logins = parse_logins(req.logins)
|
|
@@ -770,7 +792,7 @@ async def run_browser_task(req: RunRequest, emit: Emit, cancel_event: asyncio.Ev
|
|
|
770
792
|
{
|
|
771
793
|
"type": "log",
|
|
772
794
|
"level": "info",
|
|
773
|
-
"text": f"ForgeQE engine · {browser_kw
|
|
795
|
+
"text": f"ForgeQE engine · {browser_connection_label(browser_provider, browser_kw, browser_remote)} · {'headless' if headless else 'visible'}"
|
|
774
796
|
+ (f" · opening {normalized_initial}" if normalized_initial else " · agent picks the site"),
|
|
775
797
|
}
|
|
776
798
|
)
|
|
@@ -929,7 +951,7 @@ async def run_browser_task(req: RunRequest, emit: Emit, cancel_event: asyncio.Ev
|
|
|
929
951
|
"usage": {"input": 0, "output": 0},
|
|
930
952
|
"steps": replay.emitted,
|
|
931
953
|
"step_events": collected_steps,
|
|
932
|
-
"browser": await browser_profile(browser, browser_kw),
|
|
954
|
+
"browser": await browser_profile(browser, browser_kw, provider=browser_provider, remote=browser_remote),
|
|
933
955
|
}
|
|
934
956
|
_save(out, "memory_replay")
|
|
935
957
|
return out
|
|
@@ -1118,7 +1140,7 @@ async def run_browser_task(req: RunRequest, emit: Emit, cancel_event: asyncio.Ev
|
|
|
1118
1140
|
emit({"type": "log", "level": "warn", "text": budget_state["message"]})
|
|
1119
1141
|
agent_ref.stop()
|
|
1120
1142
|
if not launch_profile:
|
|
1121
|
-
launch_profile.update(await browser_profile(browser, browser_kw))
|
|
1143
|
+
launch_profile.update(await browser_profile(browser, browser_kw, provider=browser_provider, remote=browser_remote))
|
|
1122
1144
|
await diag.sample(browser)
|
|
1123
1145
|
with contextlib.suppress(Exception):
|
|
1124
1146
|
await _save_logins()
|
|
@@ -1273,7 +1295,7 @@ async def run_browser_task(req: RunRequest, emit: Emit, cancel_event: asyncio.Ev
|
|
|
1273
1295
|
"usage": _usage_from_history(history),
|
|
1274
1296
|
"steps": len(history.history or []) + replay_offset,
|
|
1275
1297
|
"step_events": collected_steps,
|
|
1276
|
-
"browser": dict(launch_profile) if launch_profile else await browser_profile(browser, browser_kw),
|
|
1298
|
+
"browser": dict(launch_profile) if launch_profile else await browser_profile(browser, browser_kw, provider=browser_provider, remote=browser_remote),
|
|
1277
1299
|
}
|
|
1278
1300
|
if judge is not None:
|
|
1279
1301
|
out["judge"] = judge
|