@capagents/forgeqe 1.1.3 → 1.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # ForgeQE
2
2
 
3
- Browser agent CLI. Say what you want in plain language: a goal to carry out in a browser, test cases to run from Jira, Azure DevOps, Xray or a file, a test to write, or a CI pipeline to set up. ForgeQE's agent works out what that needs and calls ForgeQE's tools. The ForgeQE engine drives Chrome locally, or BrowserStack and TestMu AI/LambdaTest through their Playwright CDP endpoints, and a run that passes becomes a test in your repo's own language and framework, written by **Scribe**, ForgeQE's coding agent. Skills (`SKILL.md` folders, built in or in `.forgeqe/skills`) teach the agents your repo's conventions and the apps you test.
3
+ Browser agent CLI. Say what you want in plain language: a goal to carry out in a browser, test cases to run from Jira, Azure DevOps, Xray or a file, a test to write, or a CI pipeline to set up. ForgeQE's agent works out what that needs and calls ForgeQE's tools. The ForgeQE engine drives Chrome locally, or BrowserStack and TestMu AI/LambdaTest through their Chrome DevTools Protocol (CDP) endpoints, and a run that passes becomes a test in your repo's own language and framework, written by **Scribe**, ForgeQE's coding agent. Skills (`SKILL.md` folders, built in or in `.forgeqe/skills`) teach the agents your repo's conventions and the apps you test.
4
4
 
5
5
  The engine is the browser service from test-agent-nexus, copied into this package (`py/forgeqe_engine`). It keeps the nexus behaviour: error-recovery system prompt, fast-mode agent tuning, Chrome launch flags, the search/select loop breaker, the 600 s run timeout, compact workflow YAML with semantic locators. There is no dependency on test-agent-nexus, FastAPI, or a database.
6
6
 
@@ -68,7 +68,7 @@ ForgeQE sends no telemetry and turns it off in everything it starts:
68
68
 
69
69
  An unchanged copy keeps following ForgeQE's updates; once you edit it, yours wins.
70
70
 
71
- For remote browsers, `forgeqe.yaml` can now point at provider-specific YAML files under `browser.providers` such as `browserstack` and `testmu`, so the BrowserStack or TestMu AI connection details do not have to live inline under `browser.remote`. `forgeqe init` drops sample files at `.forgeqe/browserstack.yaml` and `.forgeqe/testmu.yaml`. When both are present, the separate file supplies the provider defaults and the inline `browser.remote` values override only the active provider. ForgeQE's agent can also switch providers per request by passing `browser_provider` to `browser_run` or `run_test_cases`, and the CLI can pin one with `--browser-provider`.
71
+ For remote browsers, `forgeqe.yaml` can now point at provider-specific YAML files under `browser.providers` such as `browserstack` and `testmu`, so the BrowserStack or TestMu AI connection details do not have to live inline under `browser.remote`. `forgeqe init` drops sample files at `.forgeqe/browserstack.yaml` and `.forgeqe/testmu.yaml`. When both are present, the separate file supplies the provider defaults and the inline `browser.remote` values override only the active provider. ForgeQE's agent can also switch providers per request by passing `browser_provider` to `browser_run` or `run_test_cases`, and the CLI can pin one with `--browser-provider`. ForgeQE drives remote browsers over CDP, so `remote.browser` must be Chrome or Edge, and the default endpoints are the providers' CDP (`/puppeteer`) ones. On a remote grid, the browser stops taking real clicks and keystrokes on a tab once a password form has been submitted (most likely the browser's password manager), so after a sign-in leaves the login page ForgeQE reopens the page it landed on in a fresh tab, with the session intact, and the run log says so.
72
72
 
73
73
  ```bash
74
74
  forgeqe models
@@ -240,7 +240,7 @@ Every suite writes `report.html`, `junit.xml`, and `suite.json` to `.forgeqe/rep
240
240
 
241
241
  ### Static BI
242
242
 
243
- `forgeqe bi` adds an offline BI layer on top of those reports. It exports curated CSV datasets from ForgeQE's own suite history and release-readiness reports, plus optional Azure DevOps WIQL and Jira JQL datasets from `bi.datasets` in `forgeqe.yaml`, scaffolds an [Evidence](https://github.com/evidence-dev/evidence) project, and runs `evidence build` to produce static HTML. No backend service stays up between runs: the refresh happens during `forgeqe bi sync` or `forgeqe bi build`, and the output is a static site in the configured build directory.
243
+ `forgeqe bi` adds an offline BI layer on top of those reports. It exports curated CSV datasets from ForgeQE's own suite history and release-readiness reports, plus optional Azure DevOps WIQL and Jira JQL datasets from `bi.datasets` in `forgeqe.yaml`, into an [Evidence](https://github.com/evidence-dev/evidence) project that reads them through its CSV source, then runs `evidence sources` and `evidence build` to produce a static site in the configured build directory. ForgeQE uses the open-source Evidence line (`@evidence-dev/evidence` 40.x, pinned in the generated `package.json`), because the newer Evidence Studio CLI has no local build and keeps data in its cloud. Building it needs Node.js 18 or later; serve the folder to view it (`npx serve`), since it cannot load its data when opened from disk. When Node.js is missing or the Evidence build fails, `forgeqe bi build` writes ForgeQE's own self-contained page from the same data instead, which opens straight from disk, and says why; `--static` always does that, and `--evidence` fails instead of falling back. Dataset ids become table names (`forgeqe.<id>`), so they use letters, digits, and `_`. Pages ForgeQE generates carry a "Managed by forgeqe bi" comment in their front matter; a page without it is yours and is never overwritten. Tables with no rows yet stay out of the Evidence project (Evidence cannot build over an empty table), and their sections read "None yet." No backend service stays up between runs.
244
244
 
245
245
  ```yaml
246
246
  bi:
@@ -260,8 +260,9 @@ bi:
260
260
  ```
261
261
 
262
262
  ```bash
263
- forgeqe bi init
264
- forgeqe bi build
263
+ forgeqe bi init # scaffold the Evidence project and install its packages
264
+ forgeqe bi build # refresh the data and build the site (ForgeQE's own page if Evidence cannot build)
265
+ forgeqe bi build --static # ForgeQE's own page only, no Node.js needed
265
266
  ```
266
267
 
267
268
  ## Signing in
@@ -1,6 +1,6 @@
1
1
  provider: browserstack
2
2
  remote:
3
- browser: playwright-chromium
3
+ browser: chrome
4
4
  browser_version: latest
5
5
  os: osx
6
6
  os_version: sequoia
@@ -20,7 +20,7 @@ browser:
20
20
  overlay: true # visible browser: ForgeQE start page, edge glow, a bar with the current step, your mouse and keyboard locked while the agent works
21
21
  # start_url: https://staging.example.com # optional; omit to let the agent choose the site
22
22
  # remote: # inline settings for the selected remote provider; override the provider file when both are set
23
- # browser: playwright-chromium # BrowserStack: playwright-chromium / playwright-firefox / playwright-webkit
23
+ # browser: chrome # BrowserStack: chrome or edge (ForgeQE drives the browser over CDP)
24
24
  # browser_version: latest
25
25
  # os: osx
26
26
  # os_version: sequoia
@@ -75,14 +75,6 @@ memory:
75
75
  dir: ./.forgeqe/memory # pages, elements, locators, and recorded flows
76
76
  replay: verify # verify | trust | off
77
77
 
78
- # Redis cache for deterministic external data fetches, such as BI datasets. Live browser runs still execute.
79
- # cache:
80
- # redis_url: redis://127.0.0.1:6379
81
- # key_prefix: forgeqe
82
- # default_ttl_seconds: 3600
83
- # bi_ttl_seconds: 900
84
- # connect_timeout_ms: 3000
85
-
86
78
  code:
87
79
  # repo: ../my-app # where passed runs become tests (default: the current folder; --no-code turns it off)
88
80
  # test_command: npx playwright test # the repo's test command, for forgeqe heal; default: the agent finds it
@@ -121,13 +113,15 @@ report:
121
113
  screenshots: all # all | failures | off: step screenshots in report.html
122
114
  accessibility: true # check each page the agent visits (names, labels, image text, contrast, lang, zoom)
123
115
 
124
- # Offline BI with Evidence: ForgeQE exports CSV datasets and runs Evidence to build static HTML, no always-on server.
125
- # forgeqe bi init scaffolds the Evidence project, forgeqe bi sync refreshes the data, forgeqe bi build does both
126
- # and runs evidence build. The datasets below are optional: local ForgeQE suite/readiness data exports either way.
116
+ # Offline BI with Evidence: ForgeQE exports CSV datasets and runs Evidence (@evidence-dev/evidence 40.x, needs Node.js)
117
+ # to build a static site, no always-on server. forgeqe bi init scaffolds the Evidence project, forgeqe bi sync refreshes
118
+ # the data, forgeqe bi build does both and builds the site; without Node.js, or with --static, it writes ForgeQE's own
119
+ # page instead. The datasets below are optional: local ForgeQE suite/readiness data exports either way.
120
+ # Dataset ids are table names (forgeqe.<id>): letters, digits, and _.
127
121
  # bi:
128
122
  # title: ForgeQE BI
129
123
  # # evidence_dir: ./evidence # the managed Evidence project
130
- # # data_dir: ./evidence/sources/forgeqe/data # CSV exports that the Evidence project reads
124
+ # # data_dir: ./evidence/sources/forgeqe # CSV exports; copied there when set elsewhere
131
125
  # # build_dir: ./.forgeqe/reports/bi # static HTML output from evidence build
132
126
  # datasets:
133
127
  # ado_current_sprint:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capagents/forgeqe",
3
- "version": "1.1.3",
3
+ "version": "1.1.4",
4
4
  "description": "ForgeQE: a browser agent CLI that runs goals and test cases in local Chrome or Chromium, or BrowserStack and TestMu AI/LambdaTest when configured, and turns passing runs into tests in your repo",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -55,7 +55,6 @@
55
55
  "pixelmatch": "8.0.0",
56
56
  "pngjs": "7.0.0",
57
57
  "react": "^19.3.0",
58
- "redis": "^4.7.1",
59
58
  "yaml": "^2.9.1"
60
59
  },
61
60
  "devDependencies": {
@@ -3,11 +3,11 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import asyncio
6
+ import contextlib
6
7
  import json
7
8
  import logging
8
9
  import os
9
10
  import time
10
- from importlib.metadata import version as package_version
11
11
  from pathlib import Path
12
12
  from typing import Any, Dict, Optional
13
13
  from urllib.parse import quote, urlparse
@@ -140,13 +140,6 @@ def _remote_secret(name: str, provider: str, label: str) -> str:
140
140
  raise RuntimeError(f'{provider}: set {name} ({label}) in the environment, or save it with "forgeqe auth login {provider}".')
141
141
 
142
142
 
143
- def _playwright_version() -> Optional[str]:
144
- try:
145
- return package_version("playwright")
146
- except Exception:
147
- return None
148
-
149
-
150
143
  def _browserstack_caps(remote: Dict[str, Any]) -> Dict[str, Any]:
151
144
  caps: Dict[str, Any] = {
152
145
  "browser": str(remote.get("browser") or ""),
@@ -164,9 +157,6 @@ def _browserstack_caps(remote: Dict[str, Any]) -> Dict[str, Any]:
164
157
  caps["name"] = str(remote["name"])
165
158
  if remote.get("local") is True:
166
159
  caps["browserstack.local"] = True
167
- pw = _playwright_version()
168
- if pw:
169
- caps["client.playwrightVersion"] = pw
170
160
  extra = remote.get("extra")
171
161
  if isinstance(extra, dict):
172
162
  caps.update(extra)
@@ -229,6 +219,83 @@ def browser_connection_label(provider: str, launch: Dict[str, Any], remote: Opti
229
219
  return " · ".join(x for x in [provider, browser, target] if x)
230
220
 
231
221
 
222
+ _PAGE_PASSWORD_JS = """(() => ({
223
+ url: location.href,
224
+ filled: Array.from(document.querySelectorAll('input[type=password]')).some((e) => (e.value || '').length > 0),
225
+ }))()"""
226
+
227
+
228
+ def typed_into_password(history_item: Any) -> bool:
229
+ """Whether a step typed into a password field: its actions line up with the elements they touched."""
230
+ actions = getattr(getattr(history_item, "model_output", None), "action", None) or []
231
+ elements = getattr(getattr(history_item, "state", None), "interacted_element", None) or []
232
+ for action, element in zip(actions, elements):
233
+ attributes = (getattr(element, "attributes", None) or {}) if element is not None else {}
234
+ if str(attributes.get("type") or "").lower() != "password":
235
+ continue
236
+ try:
237
+ name = next(iter(action.model_dump(exclude_unset=True)), "")
238
+ except Exception:
239
+ name = ""
240
+ if any(word in name for word in ("input", "type", "fill")):
241
+ return True
242
+ return False
243
+
244
+
245
+ class FreshTabAfterPassword:
246
+ """
247
+ Remote grids only: after a password form is submitted, TestMu AI's Chrome stops taking real mouse and keyboard
248
+ input on that tab (most likely its password manager's bubble), while the page itself still works: clicks report
249
+ success and change nothing. The same URL in a fresh tab takes input again, with the session's cookies intact.
250
+ Armed when a step types into a password field (or one is filled when a step ends); fires once the page has left
251
+ that URL with no filled password field, so a failed sign-in that stays on the form does not move tabs.
252
+ """
253
+
254
+ def __init__(self) -> None:
255
+ self.armed_at: Optional[str] = None
256
+
257
+ async def after_step(self, browser: Any, history_item: Any) -> Optional[str]:
258
+ """Returns the URL reopened in a fresh tab, or None."""
259
+ if typed_into_password(history_item):
260
+ self.armed_at = str(getattr(getattr(history_item, "state", None), "url", "") or "")
261
+ try:
262
+ session = await asyncio.wait_for(browser.get_or_create_cdp_session(), timeout=2)
263
+ r = await asyncio.wait_for(
264
+ session.cdp_client.send.Runtime.evaluate(
265
+ params={"expression": _PAGE_PASSWORD_JS, "returnByValue": True}, session_id=session.session_id
266
+ ),
267
+ timeout=2,
268
+ )
269
+ state = ((r or {}).get("result") or {}).get("value") or {}
270
+ except Exception:
271
+ return None
272
+ url = str(state.get("url") or "")
273
+ if self.armed_at is None:
274
+ if state.get("filled"):
275
+ self.armed_at = url
276
+ return None
277
+ if not url.startswith("http") or url == self.armed_at or state.get("filled"):
278
+ return None
279
+ self.armed_at = None
280
+ await reopen_in_fresh_tab(browser, url)
281
+ return url
282
+
283
+
284
+ async def reopen_in_fresh_tab(browser: Any, url: str) -> None:
285
+ """Open url in a new tab, which the agent then works in, and close the tab it was on."""
286
+ from browser_use.browser.events import CloseTabEvent, NavigateToUrlEvent
287
+
288
+ old = getattr(browser, "agent_focus_target_id", None)
289
+ event = browser.event_bus.dispatch(NavigateToUrlEvent(url=url, new_tab=True))
290
+ await event
291
+ await event.event_result(raise_if_any=True, raise_if_none=False)
292
+ if old and old != getattr(browser, "agent_focus_target_id", None):
293
+ with contextlib.suppress(Exception):
294
+ close = browser.event_bus.dispatch(CloseTabEvent(target_id=old))
295
+ await close
296
+ await close.event_result(raise_if_any=False, raise_if_none=False)
297
+
298
+
232
299
  async def _shutdown_browser_instance(browser: Any) -> None:
233
300
  """Stop the library gracefully, falling back to kill()."""
234
301
  if browser is None:
@@ -44,6 +44,7 @@ from .llm import (
44
44
  _agent_usage,
45
45
  )
46
46
  from .browser import (
47
+ FreshTabAfterPassword,
47
48
  browser_connection_label,
48
49
  browser_launch_kwargs,
49
50
  local_browser_launch_kwargs,
@@ -1105,6 +1106,14 @@ async def run_browser_task(req: RunRequest, emit: Emit, cancel_event: asyncio.Ev
1105
1106
  last = agent_ref.history.history[-1]
1106
1107
  except Exception:
1107
1108
  return
1109
+ if fresh_tab is not None:
1110
+ try:
1111
+ reopened = await fresh_tab.after_step(browser, last)
1112
+ except Exception as ex:
1113
+ reopened = None
1114
+ logger.warning("could not reopen the page in a fresh tab after sign-in: %s", ex)
1115
+ if reopened:
1116
+ emit({"type": "log", "level": "info", "text": f"Signed in on {browser_provider}: reopened {reopened} in a fresh tab, so the page takes input again"})
1108
1117
  results = getattr(last, "result", None) or []
1109
1118
  if mem is not None:
1110
1119
  mem.llm_steps = len(agent_steps(agent_ref.history.history))
@@ -1153,6 +1162,9 @@ async def run_browser_task(req: RunRequest, emit: Emit, cancel_event: asyncio.Ev
1153
1162
  if overlay is not None:
1154
1163
  await overlay.thinking()
1155
1164
 
1165
+ # Remote grids stop taking input on a tab after a password form is submitted: see FreshTabAfterPassword.
1166
+ fresh_tab = FreshTabAfterPassword() if browser_provider != "local" else None
1167
+
1156
1168
  async def _on_step_start(agent_ref: Any) -> None:
1157
1169
  if not collected_steps:
1158
1170
  await diag.start(browser)