forgeqe-cli 2.1.1__py3-none-any.whl → 2.2.2__py3-none-any.whl

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.
forgeqe_cli/__init__.py CHANGED
@@ -4,4 +4,4 @@ The agent itself is the npm package ``@capagents/forgeqe`` (OpenTUI + Bun).
4
4
  This distribution only puts a ``forgeqe`` command on PATH.
5
5
  """
6
6
 
7
- __version__ = "2.1.1"
7
+ __version__ = "2.2.2"
@@ -0,0 +1,132 @@
1
+ """ForgeQE plugin for pytest: an HTML report after every run, from the terminal, an IDE's test runner, or CI.
2
+
3
+ pytest loads it by itself from the forgeqe-cli wheel (the pytest11 entry point). It is on in a folder with
4
+ forgeqe.yaml or .forgeqe/, or with ``pytest --forgeqe-report`` or FORGEQE_REPORT=1; FORGEQE_REPORT=0 turns it off.
5
+ At the end of the run it writes the results, with each failure's message and traceback, to
6
+ .pytest_cache/forgeqe/results.json and runs ``forgeqe report`` on it.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import os
13
+ import re
14
+ import subprocess
15
+ import sys
16
+ import tempfile
17
+ import time
18
+ from datetime import datetime, timezone
19
+ from pathlib import Path
20
+
21
+ ANSI = re.compile(r"\x1b\[[0-9;]*[A-Za-z]")
22
+
23
+
24
+ def _clean(text: str) -> str:
25
+ return ANSI.sub("", text or "")
26
+
27
+
28
+ def pytest_addoption(parser):
29
+ parser.getgroup("forgeqe").addoption(
30
+ "--forgeqe-report", action="store_true", default=False, help="write a ForgeQE HTML report after the run"
31
+ )
32
+
33
+
34
+ def _active(config) -> bool:
35
+ flag = os.environ.get("FORGEQE_REPORT")
36
+ if flag == "0":
37
+ return False
38
+ if flag == "1" or config.getoption("forgeqe_report", False):
39
+ return True
40
+ root = Path(str(config.rootpath))
41
+ return (root / "forgeqe.yaml").exists() or (root / ".forgeqe").is_dir()
42
+
43
+
44
+ def pytest_configure(config):
45
+ # An xdist worker sends its results to the controller, which writes the one report.
46
+ if hasattr(config, "workerinput") or not _active(config):
47
+ return
48
+ config.pluginmanager.register(_Collector(config), "forgeqe-collector")
49
+
50
+
51
+ def _message(report) -> str:
52
+ crash = getattr(getattr(report, "longrepr", None), "reprcrash", None)
53
+ text = crash.message if crash is not None else str(getattr(report, "longrepr", "") or "")
54
+ first = next((line.strip() for line in _clean(text).splitlines() if line.strip()), "")
55
+ return first[:300]
56
+
57
+
58
+ class _Collector:
59
+ def __init__(self, config):
60
+ self.config = config
61
+ self.tests: dict[str, dict] = {}
62
+ self.started = datetime.now(timezone.utc)
63
+ self.t0 = time.monotonic()
64
+ self.line = ""
65
+
66
+ def pytest_runtest_logreport(self, report):
67
+ nodeid = report.nodeid
68
+ test = self.tests.setdefault(
69
+ nodeid, {"name": " › ".join(nodeid.split("::")), "status": "passed", "seconds": 0.0, "file": report.location[0]}
70
+ )
71
+ if report.outcome == "rerun":
72
+ # pytest-rerunfailures: a failure that is run again; passing later makes the test flaky, not failed.
73
+ test["rerun"] = True
74
+ test.setdefault("message", _message(report))
75
+ test.setdefault("details", _clean(report.longreprtext))
76
+ return
77
+ test["seconds"] = round(test["seconds"] + (report.duration or 0), 3)
78
+ if report.failed:
79
+ if test["status"] != "failed":
80
+ test["message"] = _message(report)
81
+ test["details"] = _clean(report.longreprtext)
82
+ test["status"] = "failed"
83
+ elif report.skipped and test["status"] == "passed" and report.when in ("setup", "call"):
84
+ test["status"] = "skipped"
85
+
86
+ def pytest_sessionfinish(self, session):
87
+ if not self.tests:
88
+ return
89
+ tests = []
90
+ for test in self.tests.values():
91
+ rerun = test.pop("rerun", False)
92
+ if test["status"] == "passed" and rerun:
93
+ test["flaky"] = True
94
+ elif test["status"] != "failed" and not test.get("flaky"):
95
+ test.pop("message", None)
96
+ test.pop("details", None)
97
+ tests.append({k: v for k, v in test.items() if v not in ("", None)})
98
+ doc = {
99
+ "format": "forgeqe-results",
100
+ "version": 1,
101
+ "framework": "pytest",
102
+ "startedAt": self.started.isoformat(),
103
+ "ms": round((time.monotonic() - self.t0) * 1000),
104
+ "command": " ".join(["pytest", *self.config.invocation_params.args])[:500],
105
+ "tests": tests,
106
+ }
107
+ cache = getattr(self.config, "cache", None)
108
+ folder = Path(cache.mkdir("forgeqe")) if cache is not None else Path(tempfile.mkdtemp(prefix="forgeqe-"))
109
+ path = folder / "results.json"
110
+ path.write_text(json.dumps(doc, indent=2) + "\n", encoding="utf-8")
111
+ self.line = _report(path, Path(str(self.config.rootpath)))
112
+
113
+ def pytest_terminal_summary(self, terminalreporter):
114
+ if self.line:
115
+ terminalreporter.write_line(self.line)
116
+
117
+
118
+ def _report(path: Path, root: Path) -> str:
119
+ """Run forgeqe report on the results; the line to show."""
120
+ # This wheel's own launcher, by the Python running pytest: no PATH lookup and no .cmd shim on Windows.
121
+ bin_ = os.environ.get("FORGEQE_BIN")
122
+ cmd = [bin_, "report", str(path)] if bin_ else [sys.executable, "-m", "forgeqe_cli", "report", str(path)]
123
+ try:
124
+ run = subprocess.run(cmd, cwd=str(root), capture_output=True, text=True, timeout=300)
125
+ except (OSError, subprocess.TimeoutExpired) as exc:
126
+ return f"ForgeQE: no report ({exc.__class__.__name__}). Build it with: forgeqe report {path}"
127
+ # forgeqe report exits 1 when a test failed: that is a report, not an error.
128
+ line = next((l for l in _clean(run.stdout).splitlines() if l.startswith("Report")), "") if run.returncode <= 1 else ""
129
+ if line:
130
+ return "ForgeQE report: " + line[len("Report"):].strip()
131
+ why = (_clean(run.stderr).strip().splitlines() or ["it did not finish"])[-1]
132
+ return f"ForgeQE: no report ({why}). Build it with: forgeqe report {path}"
@@ -1,14 +1,16 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: forgeqe-cli
3
- Version: 2.1.1
3
+ Version: 2.2.2
4
4
  Summary: pip launcher for the ForgeQE browser agent (npm package: @capagents/forgeqe)
5
5
  Author: CLI-Agents
6
6
  License: MIT
7
7
  Keywords: forgeqe,browser,agent,cli,opentui
8
8
  Requires-Python: >=3.9
9
9
  Description-Content-Type: text/markdown
10
+ License-File: LICENSE
10
11
  Provides-Extra: dev
11
12
  Requires-Dist: pytest>=8.0; extra == "dev"
13
+ Dynamic: license-file
12
14
 
13
15
  # ForgeQE
14
16
 
@@ -132,7 +134,7 @@ ForgeQE's agent needs the agent runtime (downloaded automatically, see Install)
132
134
 
133
135
  Every live run records each page it visits in `.forgeqe/memory` next to `forgeqe.yaml` (`~/.forgeqe/memory` when there is no config file): every interactive element, with all of its locators (test id, id, role and name, label, placeholder, alt, title, href, text, CSS, XPath), a confidence % for each, and when it was first seen, last seen, and last used. Confidence rises when a locator keeps matching exactly one element and works when used, and falls when it goes missing or fails.
134
136
 
135
- A goal that passed before is replayed from memory with no model calls. Each step finds its element by the highest-confidence locator that still matches, so a renamed button or a changed id heals itself. The agent then checks the result with one model call (`replay: verify`), or the run finishes without the model (`replay: trust`). If a step cannot be found, the agent takes over from that point. Generated specs use the best locator from memory.
137
+ A goal that passed before is replayed from memory with no model calls. Each step finds its element by the highest-confidence locator that still matches, so a renamed button or a changed id heals itself. The agent then checks the result with one model call (`replay: verify`), or the run finishes without the model (`replay: trust`). If a step cannot be found, the agent takes over from that point. A run that failed because an expected result did not hold keeps the steps up to that check, so the next run of the case replays them and the agent checks the result again there, instead of exploring the whole case again. On every page memory knows, the agent is told which controls earlier runs used, with the index each has on the page right now, so it acts on them in one step. Generated specs use the best locator from memory.
136
138
 
137
139
  ```bash
138
140
  forgeqe memory # hosts, pages, elements, flows
@@ -140,9 +142,12 @@ forgeqe memory show booking.com # a host's pages
140
142
  forgeqe memory show https://app.test/login --locators # every locator with confidence
141
143
  forgeqe memory flows --steps # recorded goals and their steps
142
144
  forgeqe memory clear booking.com --yes
145
+ forgeqe memory stats # how often memory stood in for the model, per area
143
146
  forgeqe run "..." --replay trust # or --replay off, --no-memory
144
147
  ```
145
148
 
149
+ A new goal on a site ForgeQE knows replays the start it shares with an earlier goal there: a sign-in with a saved login, a cookie banner, and the steps whose controls or values the new goal names. Mobile and desktop explores do the same from app memory. A goal that passed before replays with no model call when its checks still hold, a failed explore is picked up where it went wrong, and a control a heal found once is used again without the model. Saved API calls replay inside a browser flow when the server answers as before, and API scenarios planned from the same test cases or text are reused. A rerun of any of these runs as recorded, without ForgeQE's agent.
150
+
146
151
  ## Skills
147
152
 
148
153
  Skills teach the agents what a prompt can't know: your repo's test conventions, a framework, or how to drive an app. A skill is a folder with a `SKILL.md` (the Agent Skills format: `name`, `description`, then instructions), plus ForgeQE's `agents: [forgeqe, scribe, browser]` to say who uses it. ForgeQE ships `playwright-typescript`, `dotnet-nunit-playwright` and `testcontainers` for Scribe, `dynamics-365` for the browser agent, and `writing-skills` for ForgeQE's agent. Your own go in `.forgeqe/skills` next to `forgeqe.yaml` (commit them) or `~/.forgeqe/skills`; a project skill replaces a built-in one with the same name. Skills in `~/.claude/skills` and `~/.agents/skills` are used only with `skills.external: true`.
@@ -0,0 +1,10 @@
1
+ forgeqe_cli/__init__.py,sha256=P8v4jSY_ROSVD98F0zQcv49wFe88He6hTop3NqlqJrg,200
2
+ forgeqe_cli/__main__.py,sha256=frkRQzq1Cd7HI3aiw0W0eJ-w1r8VkXOJrv0wIR-on9Y,46
3
+ forgeqe_cli/launcher.py,sha256=GKmbo1W4xpNz2I3smM0CXGbnAYRdwhrX8Pe48Gr4kgs,6413
4
+ forgeqe_cli/pytest_plugin.py,sha256=f_hH2JXv059oJwHBBdR-TZAhuFYqM2LMr6vAAcRpC5o,5570
5
+ forgeqe_cli-2.2.2.dist-info/licenses/LICENSE,sha256=Z_3_KODt79ZZl4nLelIVBSsVsL6AhEypl7HouMkKjYA,3079
6
+ forgeqe_cli-2.2.2.dist-info/METADATA,sha256=LB2sIwF4wFAVpWvZgpbo42iftHYyk4fXcQDbnDBkQ84,50190
7
+ forgeqe_cli-2.2.2.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
8
+ forgeqe_cli-2.2.2.dist-info/entry_points.txt,sha256=KphefM5TY2MOIbABNg2UJJQsEZUZgrNM4ZD3lrIphVg,139
9
+ forgeqe_cli-2.2.2.dist-info/top_level.txt,sha256=YrCSJdE_QY2GlHw_tgRYsQpZ1cSiIejC9swAUcRLpD8,12
10
+ forgeqe_cli-2.2.2.dist-info/RECORD,,
@@ -1,3 +1,6 @@
1
1
  [console_scripts]
2
2
  forgeqe = forgeqe_cli.launcher:main
3
3
  webpilot = forgeqe_cli.launcher:main
4
+
5
+ [pytest11]
6
+ forgeqe = forgeqe_cli.pytest_plugin
@@ -0,0 +1,29 @@
1
+ ForgeQE Software Licence
2
+
3
+ Copyright (c) 2026 Javed Khan. All rights reserved.
4
+
5
+ ForgeQE, its documentation, prompts, skills, and accompanying files (together, "the Software") are proprietary and confidential. The Software is licensed, not sold.
6
+
7
+ 1. Grant. Subject to a written agreement with the licensor, the licensee may install and use the Software on its own systems, for its own internal business purposes, for the term and in the quantities that agreement states. No other right is granted.
8
+
9
+ 2. Restrictions. Except to the extent that applicable law expressly permits it despite this restriction, the licensee shall not, and shall not allow anyone else to:
10
+ (a) copy the Software, other than one copy for backup;
11
+ (b) modify, translate, adapt, or create derivative works of the Software;
12
+ (c) reverse engineer, decompile, disassemble, deobfuscate, or otherwise attempt to derive the source code, prompts, algorithms, structure, or operation of the Software;
13
+ (d) sell, rent, lease, lend, sublicense, distribute, publish, host for others, or otherwise make the Software available to any third party;
14
+ (e) remove, hide, or alter any proprietary notice in the Software;
15
+ (f) use the Software, or knowledge of its operation, to build a product or service that competes with it.
16
+
17
+ 3. Third-party software. The Software contains and installs open source components, listed with their licences in THIRD_PARTY_NOTICES.md. Each such component is governed by its own licence, and nothing in this licence restricts a right that licence grants.
18
+
19
+ 4. Ownership. The licensor and its licensors own all right, title, and interest in the Software, including all intellectual property rights. The licensee's feedback may be used by the licensor without restriction.
20
+
21
+ 5. Confidentiality. The Software, including its prompts and documentation, is confidential information of the licensor. The licensee shall protect it with at least the care it uses for its own confidential information, and shall disclose it only to those of its staff who need it to use the Software under this licence.
22
+
23
+ 6. No warranty. The Software is provided "as is", without warranty of any kind, express or implied, including the implied warranties of merchantability, fitness for a particular purpose, and non-infringement, except where a written agreement with the licensor says otherwise.
24
+
25
+ 7. Limitation of liability. To the maximum extent permitted by law, the licensor shall not be liable for any indirect, incidental, special, consequential, or punitive damages, or for loss of profits, data, or business, arising out of or related to the Software, and its total liability shall not exceed the fees the licensee paid for the Software in the twelve months before the claim.
26
+
27
+ 8. Termination. This licence ends automatically if the licensee breaches it. On termination, the licensee shall stop using the Software and destroy every copy. Sections 2 to 7 survive termination.
28
+
29
+ 9. Governing law. The written agreement between the licensor and the licensee states the governing law and the forum for disputes.
@@ -1,8 +0,0 @@
1
- forgeqe_cli/__init__.py,sha256=Px_1pNrFTrZYc8pozzZ2dPZt2jKxs76rdbY3RdnxpCQ,200
2
- forgeqe_cli/__main__.py,sha256=frkRQzq1Cd7HI3aiw0W0eJ-w1r8VkXOJrv0wIR-on9Y,46
3
- forgeqe_cli/launcher.py,sha256=GKmbo1W4xpNz2I3smM0CXGbnAYRdwhrX8Pe48Gr4kgs,6413
4
- forgeqe_cli-2.1.1.dist-info/METADATA,sha256=kBFmJ9HbTPlTJAMpTINK9lPGCNs-_7Ah35Xx5kE1pp8,49004
5
- forgeqe_cli-2.1.1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
6
- forgeqe_cli-2.1.1.dist-info/entry_points.txt,sha256=1IHZSsCnhKZfoCBsdScd7pRKwRzQ5bruc1Nk_FA5SI4,91
7
- forgeqe_cli-2.1.1.dist-info/top_level.txt,sha256=YrCSJdE_QY2GlHw_tgRYsQpZ1cSiIejC9swAUcRLpD8,12
8
- forgeqe_cli-2.1.1.dist-info/RECORD,,