@capagents/forgeqe 1.2.0 → 1.3.0

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.
Files changed (75) hide show
  1. package/HOW_TO_USE.md +54 -3
  2. package/README.md +1 -1
  3. package/docs/COST_ARCHITECTURE.md +280 -0
  4. package/docs/feature-test-prompts.md +95 -0
  5. package/docs/user-guide.html +66 -8
  6. package/forgeqe.example.yaml +15 -0
  7. package/package.json +1 -1
  8. package/prompts/brain.md +36 -4
  9. package/prompts/browser-api.md +13 -0
  10. package/prompts/browser-capture.md +2 -2
  11. package/prompts/browser-checks.md +2 -0
  12. package/py/forgeqe_engine/api_step.py +130 -0
  13. package/py/forgeqe_engine/capture.py +53 -2
  14. package/py/forgeqe_engine/checks.py +8 -2
  15. package/py/forgeqe_engine/llm.py +33 -5
  16. package/py/forgeqe_engine/memory.py +70 -2
  17. package/py/forgeqe_engine/observe.py +123 -0
  18. package/py/forgeqe_engine/replay.py +55 -2
  19. package/py/forgeqe_engine/service.py +177 -5
  20. package/src/api/fromSteps.ts +111 -0
  21. package/src/capabilities/registry.ts +19 -5
  22. package/src/cases/ado.ts +1 -1
  23. package/src/cases/journey.ts +61 -0
  24. package/src/cases/quality.ts +3 -1
  25. package/src/cases/readiness.ts +35 -6
  26. package/src/cases/save.ts +241 -48
  27. package/src/cases/types.ts +11 -0
  28. package/src/cli/help.ts +8 -0
  29. package/src/cli/index.ts +64 -1
  30. package/src/code/coverage.ts +22 -3
  31. package/src/code/decisions.ts +73 -0
  32. package/src/code/find.ts +3 -1
  33. package/src/code/graph/graph.ts +33 -2
  34. package/src/code/handoff.ts +20 -0
  35. package/src/code/loop.ts +27 -1
  36. package/src/code/notes.ts +48 -1
  37. package/src/code/prompt.ts +31 -9
  38. package/src/code/protect.ts +103 -0
  39. package/src/code/replay.ts +33 -0
  40. package/src/code/review.ts +2 -0
  41. package/src/code/runtime.ts +35 -3
  42. package/src/code/shortlist.ts +309 -0
  43. package/src/code/testrun.ts +3 -1
  44. package/src/code/traceability.ts +2 -0
  45. package/src/commands.ts +6 -1
  46. package/src/config.ts +46 -0
  47. package/src/engine/bridge.ts +47 -5
  48. package/src/mcp/casetools.ts +107 -11
  49. package/src/mcp/commands.ts +74 -20
  50. package/src/mcp/packtools.ts +187 -0
  51. package/src/mcp/tools.ts +238 -29
  52. package/src/pack/collect.ts +251 -0
  53. package/src/pack/index.ts +134 -0
  54. package/src/pack/init.ts +45 -0
  55. package/src/pack/pack.ts +327 -0
  56. package/src/pack/pipeline.ts +373 -0
  57. package/src/pack/run.ts +155 -0
  58. package/src/registry/db.ts +249 -0
  59. package/src/registry/index.ts +72 -0
  60. package/src/registry/resolve.ts +57 -0
  61. package/src/registry/tier0.ts +81 -0
  62. package/src/report/data.ts +7 -0
  63. package/src/report/html.ts +14 -0
  64. package/src/report/template.html +52 -48
  65. package/src/report/usage.ts +113 -0
  66. package/src/run.ts +32 -2
  67. package/src/suite/report.ts +22 -4
  68. package/src/suite/run.ts +13 -5
  69. package/src/summary.ts +6 -0
  70. package/src/tokenomics/budget.ts +172 -0
  71. package/src/tokenomics/ledger.ts +287 -0
  72. package/src/tui/App.tsx +90 -2
  73. package/src/tui/inspector.tsx +40 -35
  74. package/src/tui/transcript.tsx +5 -4
  75. package/src/types.ts +11 -2
@@ -378,7 +378,7 @@ forgeqe docs --copy ./docs # copy the guide, the architecture page, and the re
378
378
  <h3 class="band-title">Every feature</h3>
379
379
  <div class="area"><a class="area-name" href="#area-browser"><span class="mono">Browser</span></a><ul class="tiles"><li><a class="tile" href="#cap-browser-goals"><b>Run any goal in a real browser</b><span>Go to booking.com and search hotels in Mumbai for next weekend</span></a></li><li><a class="tile" href="#cap-logins"><b>Saved logins for the application under test</b><span>Sign in to the admin site and keep the session</span></a></li></ul></div>
380
380
  <div class="area"><a class="area-name" href="#area-test-cases"><span class="mono">Test cases</span></a><ul class="tiles"><li><a class="tile" href="#cap-run-test-cases"><b>Run manual test cases in browsers</b><span>Run SHOP-12 and SHOP-14 from Jira, two browsers at once</span></a></li><li><a class="tile" href="#cap-test-case-quality"><b>Check and improve test cases</b><span>Are the cases in test plan 12 good enough to run?</span></a></li><li><a class="tile" href="#cap-write-test-cases"><b>Write test cases from a story</b><span>Write test cases for story 1234 and save them</span></a></li><li><a class="tile" href="#cap-domain-compliance"><b>Domain and regulatory test design</b><span>Write test cases for story 1234, covering our DORA and PSD2 obligations</span></a></li><li><a class="tile" href="#cap-exploratory-testing"><b>Exploratory testing of a story</b><span>Explore story 1234 and find bugs</span></a></li></ul></div>
381
- <div class="area"><a class="area-name" href="#area-automated-tests"><span class="mono">Automated tests</span></a><ul class="tiles"><li><a class="tile" href="#cap-write-tests"><b>Turn a passed run into an automated test</b><span>Write a Playwright test for that run in ../app</span></a></li><li><a class="tile" href="#cap-heal-tests"><b>Heal failing tests</b><span>Heal the failing tests in ../app</span></a></li><li><a class="tile" href="#cap-find-run-tests"><b>Find and run existing tests</b><span>Run the checkout tests</span></a></li><li><a class="tile" href="#cap-flaky-tests"><b>Flaky test detection and quarantine</b><span>Which tests are flaky?</span></a></li></ul></div>
381
+ <div class="area"><a class="area-name" href="#area-automated-tests"><span class="mono">Automated tests</span></a><ul class="tiles"><li><a class="tile" href="#cap-write-tests"><b>Turn a passed run into an automated test</b><span>Write a Playwright test for that run in ../app</span></a></li><li><a class="tile" href="#cap-heal-tests"><b>Heal failing tests</b><span>Heal the failing tests in ../app</span></a></li><li><a class="tile" href="#cap-find-run-tests"><b>Find and run existing tests</b><span>Run the checkout tests</span></a></li><li><a class="tile" href="#cap-regression-packs"><b>Regression packs for a release</b><span>Build a regression pack for sprint 12 and run it on TestMu</span></a></li><li><a class="tile" href="#cap-flaky-tests"><b>Flaky test detection and quarantine</b><span>Which tests are flaky?</span></a></li></ul></div>
382
382
  <div class="area"><a class="area-name" href="#area-pull-requests"><span class="mono">Pull requests</span></a><ul class="tiles"><li><a class="tile" href="#cap-pr-review"><b>Review pull requests</b><span>Review PR 42 and focus on error handling</span></a></li><li><a class="tile" href="#cap-pr-impact"><b>Pull request impact and test selection</b><span>What does PR 42 impact?</span></a></li></ul></div>
383
383
  <div class="area"><a class="area-name" href="#area-api"><span class="mono">API</span></a><ul class="tiles"><li><a class="tile" href="#cap-api-testing"><b>API and contract testing</b><span>Turn the last run's API calls into tests</span></a></li></ul></div>
384
384
  <div class="area"><a class="area-name" href="#area-quality"><span class="mono">Quality</span></a><ul class="tiles"><li><a class="tile" href="#cap-accessibility-performance"><b>Accessibility and page performance</b><span>Is the checkout page accessible?</span></a></li><li><a class="tile" href="#cap-visual-comparison"><b>Visual comparison with baselines</b><span>What changed visually since the last run?</span></a></li><li><a class="tile" href="#cap-defect-intelligence"><b>Defect intelligence: trends and prediction</b><span>Which defects keep coming back?</span></a></li></ul></div>
@@ -590,6 +590,7 @@ forgeqe auth logout jira
590
590
  </table></div>
591
591
  <p>A card says which it did and why (<em>Extending PracticeTests.Flow · covers 12 of 15; only 3 are missing</em>). Say how when you know (&quot;extend the login test&quot;, &quot;write a separate test&quot;, &quot;update it&quot;) and ForgeQE's agent passes that on without asking. In every mode Scribe adds page-object members rather than changing what an existing member does for the other tests that use it.</p>
592
592
  <p><strong>The same flow asked again.</strong> When a session writes tests and you ask for a flow an earlier job already wrote a test for, ForgeQE runs that test first, before any browser run. If it passes in a real browser, that is the answer: no browser run, no Scribe, and no new test file, in the seconds the test takes (<em>✓ Existing test passed · 19s · no browser run or new test needed</em>). If it fails, the flow runs in the browser and Scribe updates that test instead of writing another. ForgeQE's agent rewords a goal a little each time, so goals count as the same flow when they share their content words (endings, case, and phrasing aside) and the new goal keeps everything the earlier one checked; the same steps with a different final check, a shorter flow, or another site are a new flow. The agent also compares the earlier goal with your request and runs fresh when you ask for a check the earlier test does not have. Ask for &quot;a fresh run&quot; or &quot;a new test&quot; to skip the reuse.</p>
593
+ <p><strong>When the flow cannot be reproduced.</strong> If ForgeQE's own replay of the run could not reach the run's final page and Scribe's first test fails too, the job stops there and says which replayed step failed and why, instead of spending its remaining attempts on a flow no script can repeat as recorded.</p>
593
594
  <p><strong>Why a test failed.</strong> When ForgeQE's run of Scribe's test fails, the ✗ line and the next attempt say why in the test's own words: dotnet test's Error Message, pytest's <code>E</code> lines, or the error with its Expected and Received values (Playwright, Jest, JUnit), not the runner's summary line.</p>
594
595
  <p><strong>Keeping Scribe's cost down.</strong> ForgeQE reads the repository's test setup itself before Scribe starts (stack, settings files and their browser values, the test id attribute, base classes, page objects and their methods, one test command) and hands it over, so Scribe does not discover it with dozens of file reads. Each attempt after the first starts a fresh session from a summary of the job (handoff, files changed, what each attempt did, the latest failure), so later attempts cost about what the first did. <code>code.max_attempts</code> (default 3) and <code>code.max_tokens</code> (default 1,500,000; <code>0</code> turns it off) cap a job, and the token budget is checked during an attempt too, so one long attempt is stopped when the total passes it. The reviewer sorts its issues into correctness (the test does not test what the goal says, or not reliably) and coding standards (test data or credentials in a page object, naming, hard waits, leftovers). Coding standards never fail a test that works: it is accepted, and the coding agent gets one more round (not counted in <code>max_attempts</code>) to fix them; if that round breaks the test, ForgeQE puts back the version that passed and lists the standards issues in the notes (<code>code.tidy: false</code> skips the round). A test the reviewer rejects twice for correctness stops the job with its issues, since that usually means the test case and the page disagree. Under each step, the handoff shows that step's part of the page (its section and neighbours, from the replay's page snapshot) instead of whole snapshots. When a test fails, ForgeQE shows the failing element on the test's page at the failure and on the replay's page, so most failures are fixed without opening a browser. Scribe's live browser is switched on only when the same failure repeats, and it does not send a page snapshot after every click or keystroke. <code>code.max_steps</code> (default 50) caps the tool calls in one of Scribe's turns. <code>code.review_profile</code> runs the reviewer on a cheaper llms.json profile.</p>
595
596
  <p><code>forgeqe.yaml</code> selects the active profile:</p>
@@ -917,6 +918,17 @@ forgeqe memory clear booking.com --yes
917
918
  <h4>Test cases and suites</h4>
918
919
  <div class="prose"><p>Test cases you already have can be run two ways. In the shell or with <code>forgeqe run</code>, ask in plain words (&quot;run SHOP-12 and SHOP-14&quot;, &quot;run test plan 12 suite 34&quot;, &quot;run the cases in smoke.yaml&quot;) or paste the cases, and the agent works out the source. <code>forgeqe suite</code> takes explicit references and runs the same way every time, with no agent involved, which is what pipelines call. Each case is one goal: ForgeQE tells the agent to do the steps in order and to check every expected result, and the agent's verdict decides whether the case passed. A case with no steps (plain text, a Jira story) is run from its description.</p>
919
920
  </div>
921
+ <section class="doc sub" id="doc-how_to_use-api-steps-in-a-test-case"><h5>API steps in a test case</h5><div class="prose"><p>A test case can mix API and UI steps (&quot;add three products with POST /add_to_cart/{id}, then remove the first one in the cart, then pay&quot;). The browser agent makes each API step with its <code>api_request</code> action, from the page, in the browser's own session: the logged-in user's cookies go with the call, and the CSRF token comes from the cookie or the page's form when the site needs one. So what an API step changes (a cart, an order) is what the next UI step sees.</p>
922
+ <ul>
923
+ <li><strong>The verdict</strong> of an API step rests on the status the server answered, not on the agent's account of it.</li>
924
+ <li><strong>The run records it</strong> as an <code>api</code> step in <code>workflow.yaml</code>: method, URL, body, CSRF source, and the status and final URL the server gave.</li>
925
+ <li><strong>ForgeQE's replay repeats it</strong> the same way, and fails that step when the server answers differently (403 where the run got 200), so the replay and the test get past it instead of stalling on an empty cart.</li>
926
+ <li><strong>Scribe's handoff</strong> lists each API step with what was sent and answered, and tells Scribe to make it through the framework's API request context that shares the browser's cookies (Playwright's <code>page.request</code>), asserting the status.</li>
927
+ <li><strong>Kept for reuse</strong>: the calls are saved as an API scenario, one per test case (<code>out/api/case-ado-2287/</code>, updated by each run), which <code>forgeqe api</code> and the API tools run again without a browser. The run's card says where (<em>API 2 calls saved as a reusable API scenario</em>).</li>
928
+ </ul>
929
+ <p>URLs the case gives are opened exactly as given: when the model adds a trailing slash (<code>/login/</code>, a 404 on many sites), ForgeQE opens and records the URL from the case.</p>
930
+ <p>When a run finds a test case does not fit the application, the agent drafts the steps as they should be, from what the run found, and offers to save them. <code>update_test_case</code> shows the current and the new steps and asks before it changes the Azure DevOps Test Case; the reason goes into the work item's history, where the old steps stay.</p>
931
+ </div></section>
920
932
  <section class="doc sub" id="doc-how_to_use-when-a-case-fails"><h5>When a case fails</h5><div class="prose"><p>The browser run of a test case is the test, so when a case does not pass, ForgeQE works out what kind of failure it was and acts on it. The browser agent reports a cause for each failed step (it saw the page); clear evidence in the run, such as an unreachable site or a 503 page, overrides &quot;I could not operate it&quot;.</p>
921
933
  <div class="tbl"><table>
922
934
  <thead>
@@ -1115,10 +1127,10 @@ forgeqe suite cases.yaml --no-code --no-write-back --out .forgeqe/reports/smoke
1115
1127
  </article>
1116
1128
  <article class="spec feat" id="cap-test-case-quality">
1117
1129
  <div class="hd"><span class="mono">Test cases</span><h3>Check and improve test cases</h3></div>
1118
- <p class="ds">Checks test cases before they run (vague or missing expected results, missing test data, placeholders, duplicates) against the story, feature, and epic they test, then rewrites Azure DevOps cases once the user agrees.</p>
1130
+ <p class="ds">Checks test cases before they run (vague or missing expected results, missing test data, placeholders, duplicates) against the story, feature, and epic they test, then rewrites Azure DevOps or Jira cases in place once the user agrees.</p>
1119
1131
  <div class="facts">
1120
1132
  <div class="fact"><span class="mono">Ask it</span><ul class="asks"><li>Are the cases in test plan 12 good enough to run?</li><li>Fix the expected results of case 4512</li></ul></div>
1121
- <div class="fact"><span class="mono">What it needs</span><ul class="needs"><li><span class="need opt">Optional</span><span>Azure DevOps, to rewrite cases in place: set up under sources: in forgeqe.yaml, or sign in with /auth</span></li></ul></div>
1133
+ <div class="fact"><span class="mono">What it needs</span><ul class="needs"><li><span class="need opt">Optional</span><span>Azure DevOps or Jira, to rewrite cases in place: set up under sources: in forgeqe.yaml, or sign in with /auth</span></li></ul></div>
1122
1134
  <div class="fact"><span class="mono">Tools</span><p class="tools"><code>check_test_cases</code> <code>update_test_case</code></p></div>
1123
1135
  </div>
1124
1136
  <div class="detail"><span class="mono">In detail</span>
@@ -1148,19 +1160,18 @@ forgeqe suite cases.yaml --no-code --no-write-back --out .forgeqe/reports/smoke
1148
1160
  # command: [&quot;python&quot;, &quot;rag/search.py&quot;]
1149
1161
  </code></pre>
1150
1162
  <p>The answer can also be an object with <code>results</code>, <code>passages</code>, <code>documents</code>, or <code>hits</code>, and a passage's text can be <code>text</code>, <code>content</code>, or <code>page_content</code> (LangChain's shape). A RAG server with an MCP interface works through <code>mcp:</code> instead: ForgeQE's agent calls its tools.</p>
1151
- <p><code>suite.case_check</code> decides what happens before a run: <code>warn</code> (the default) shows the findings and runs every case, <code>block</code> leaves out the cases that cannot run as written, <code>off</code> skips the check.</p>
1152
- <p>When a run finds a test case does not fit the application, the agent drafts the steps as they should be, from what the run found, and offers to save them. <code>update_test_case</code> shows the current and the new steps and asks before it changes the Azure DevOps Test Case; the reason goes into the work item's history, where the old steps stay.</p>
1163
+ <p><code>suite.case_check</code> decides what happens before a run: <code>warn</code> (the default) shows the findings and runs every case, <code>block</code> leaves out the cases that cannot run as written, <code>off</code> skips the check. Ask to run a case and it runs, whatever the check found; ForgeQE's agent reports the findings with the result instead of refusing. A case whose steps have no expected results runs too: it passes when every step can be done, and the check lists it <strong>to review</strong>, since it checks no result the page should show.</p>
1153
1164
  </div>
1154
1165
  </section>
1155
1166
  </div>
1156
1167
  </article>
1157
1168
  <article class="spec feat" id="cap-write-test-cases">
1158
1169
  <div class="hd"><span class="mono">Test cases</span><h3>Write test cases from a story</h3></div>
1159
- <p class="ds">Writes test cases from a story's acceptance criteria and saves them in Azure DevOps or Jira, linked to the story.</p>
1170
+ <p class="ds">Writes test cases from a story's acceptance criteria and saves them in Azure DevOps or Jira, linked to the story. Reads the cases already linked first, so asking again adds only what is missing and updates cases in place instead of duplicating them; plans end-to-end journeys from the existing cases.</p>
1160
1171
  <div class="facts">
1161
- <div class="fact"><span class="mono">Ask it</span><ul class="asks"><li>Write test cases for story 1234 and save them</li></ul></div>
1172
+ <div class="fact"><span class="mono">Ask it</span><ul class="asks"><li>Write test cases for story 1234 and save them</li><li>Build an end-to-end journey from story 1234's cases</li></ul></div>
1162
1173
  <div class="fact"><span class="mono">What it needs</span><ul class="needs"><li><span class="need req">Required</span><span>Azure DevOps or Jira: set up under sources: in forgeqe.yaml, or sign in with /auth</span></li></ul></div>
1163
- <div class="fact"><span class="mono">Tools</span><p class="tools"><code>save_test_cases</code></p></div>
1174
+ <div class="fact"><span class="mono">Tools</span><p class="tools"><code>story_test_cases</code> <code>save_test_cases</code> <code>e2e_journey</code></p></div>
1164
1175
  </div>
1165
1176
  <div class="detail"><span class="mono">In detail</span>
1166
1177
  <p class="see">See <a href="#doc-how_to_use-where-cases-come-from">Where cases come from</a>.</p>
@@ -1491,6 +1502,53 @@ Run the tests added since 2026-09-01
1491
1502
  </section>
1492
1503
  </div>
1493
1504
  </article>
1505
+ <article class="spec feat" id="cap-regression-packs">
1506
+ <div class="hd"><span class="mono">Automated tests</span><h3>Regression packs for a release</h3></div>
1507
+ <p class="ds">Builds a regression pack from what goes into a release (an Azure DevOps sprint or work items, a commit range, pull requests, or changes read from any connected tool): the tests those changes can break, each with why, plus risk-area and smoke tests, and the changes no test covers. Runs it locally, on TestMu AI, or in a pipeline, and compares each run with the last.</p>
1508
+ <div class="facts">
1509
+ <div class="fact"><span class="mono">Ask it</span><ul class="asks"><li>Build a regression pack for sprint 12 and run it on TestMu</li><li>Which tests should we run for the PRs merged this week?</li></ul></div>
1510
+ <div class="fact"><span class="mono">What it needs</span><ul class="needs"><li><span class="need input">You give</span><span>a repository with tests</span></li><li><span class="need opt">Optional</span><span>Azure DevOps or Jira, to read a sprint's or release's work items: set up under sources: in forgeqe.yaml, or sign in with /auth</span></li></ul></div>
1511
+ <div class="fact"><span class="mono">Tools</span><p class="tools"><code>build_regression_pack</code> <code>run_regression_pack</code> <code>list_regression_packs</code></p></div>
1512
+ </div>
1513
+ <div class="detail"><span class="mono">In detail</span>
1514
+ <section class="doc" id="doc-how_to_use-regression-packs-for-a-release">
1515
+ <h4>Regression packs for a release</h4>
1516
+ <div class="prose"><p>A regression pack is the set of your repository's tests that a release's changes can break, saved under a name so you can run it again (here, on TestMu AI, or in a pipeline) and compare runs. Ask in your own words:</p>
1517
+ <pre><code class="language-text">Build a regression pack for sprint 12 and run it on TestMu
1518
+ Which tests should we run for the PRs merged this week?
1519
+ Build a pack for Jira fix version 2.4 and the commits since release/2.3 in ../shop-app
1520
+ </code></pre>
1521
+ <p><strong>Where the changes come from.</strong> Any mix of:</p>
1522
+ <ul>
1523
+ <li>an Azure DevOps sprint, area, epic or feature, or work item ids: their stories and bugs, the test cases each is tested by, and the files their linked pull requests changed;</li>
1524
+ <li>the commits between two refs of a repository (<code>from</code> and <code>to</code>): the product's repository, or the test repository's own; work items the commit messages name (<code>AB#123</code>, <code>#123</code>, <code>PAY-12</code>) are read too;</li>
1525
+ <li>pull requests by number or URL, or the ones merged in the last N days (Azure DevOps or GitHub);</li>
1526
+ <li>anything ForgeQE's agent can read through its connected tools (a Jira release, GitHub issues, a Confluence release note): it reads the items and passes them in with their text and links.</li>
1527
+ </ul>
1528
+ <p><strong>Why each test is in.</strong> A test can be in for more than one reason, and the card and <code>&lt;name&gt;.md</code> say each one:</p>
1529
+ <div class="tbl"><table>
1530
+ <thead>
1531
+ <tr><th>Reason</th><th>Meaning</th></tr>
1532
+ </thead>
1533
+ <tbody>
1534
+ <tr><td>linked</td><td>it automates a test case that tests a changed work item (<code>.forgeqe/test-cases.json</code>, or a case id in the test's attributes)</td></tr>
1535
+ <tr><td>test code changed</td><td>the change is in the test repository, in code the test uses</td></tr>
1536
+ <tr><td>matches a change</td><td>the work item's or pull request's words name what the test does: the elements it uses, its page, its name</td></tr>
1537
+ <tr><td>risk area</td><td>the release touches an area with defects or failures before, and the test works there</td></tr>
1538
+ <tr><td>smoke</td><td>the test is tagged smoke, so it is in every pack</td></tr>
1539
+ </tbody>
1540
+ </table></div>
1541
+ <p><strong>Gaps.</strong> Changes no test is in for are listed as <em>not covered</em>, each with the closest existing test and what it lacks, so you can extend that test rather than write a new one.</p>
1542
+ <p><strong>Running it.</strong> ForgeQE asks before it runs anything.</p>
1543
+ <ul>
1544
+ <li><em>This machine</em>: ForgeQE builds one command for exactly the pack's tests, the way the repository already runs a test (for .NET, a <code>--filter</code> with each test, the settings an earlier job used, TRX results), runs it, and reads each result. A failed test runs once more; one that then passes is flaky.</li>
1545
+ <li><em>TestMu AI</em>: the same command, with the variable <code>FORGEQE_PW_WS_ENDPOINT</code> set to TestMu AI's Playwright endpoint for the browser and platform in <code>testmu.yaml</code> (the access key goes in the variable, never in the command, and is removed from the output). The tests must connect to it when it is set: for NUnit, override <code>ConnectOptionsAsync</code> in the test base class; for Playwright Test, set <code>connectOptions</code> from it in <code>playwright.config</code>. ForgeQE refuses a TestMu run when the tests do not read the variable, so a run never looks remote while it ran locally.</li>
1546
+ </ul>
1547
+ <p>Each run is kept with the pack: the card says which tests fail now that passed in the last run, and which were fixed. Packs are saved in <code>.forgeqe/packs/&lt;name&gt;.json</code> and <code>&lt;name&gt;.md</code>; run output and results in <code>.forgeqe/reports/packs/&lt;name&gt;/&lt;time&gt;/</code>.</p>
1548
+ </div>
1549
+ </section>
1550
+ </div>
1551
+ </article>
1494
1552
  <article class="spec feat" id="cap-flaky-tests">
1495
1553
  <div class="hd"><span class="mono">Automated tests</span><h3>Flaky test detection and quarantine</h3></div>
1496
1554
  <p class="ds">Finds tests that flip between passing and failing in the repository's run history, with their likely cause, and quarantines them once the user agrees.</p>
@@ -75,6 +75,13 @@ memory:
75
75
  dir: ./.forgeqe/memory # pages, elements, locators, and recorded flows
76
76
  replay: verify # verify | trust | off
77
77
 
78
+ # Cost-aware execution (docs/COST_ARCHITECTURE.md), on by default. on: a rerun of a passed run (the rerun link,
79
+ # /rerun, "run it again", the same goal word for word) runs as recorded without the agent, and a replay whose
80
+ # recorded checks all hold passes with no model; anything else, or any check that fails, goes to the agent.
81
+ # shadow: only records what it would have done (.forgeqe/registry). off: every request goes to ForgeQE's agent.
82
+ registry:
83
+ mode: on # on | shadow | off
84
+
78
85
  code:
79
86
  # repo: ../my-app # where passed runs become tests (default: the current folder; --no-code turns it off)
80
87
  # test_command: npx playwright test # the repo's test command, for forgeqe heal; default: the agent finds it
@@ -114,6 +121,14 @@ report:
114
121
  screenshots: all # all | failures | off: step screenshots in report.html
115
122
  accessibility: true # check each page the agent visits (names, labels, image text, contrast, lang, zoom)
116
123
 
124
+ # Model spending budgets, in the price currency. They only warn (at 80%, 90% and 100%) and never stop a run.
125
+ # Day and month (UTC) count every ForgeQE process on this machine, from the usage ledger in ~/.forgeqe/usage.
126
+ # budget:
127
+ # run: 2 # one browser run
128
+ # day: 25
129
+ # month: 400
130
+ # # currency: USD # default: the currency of the model's price
131
+
117
132
  # Offline BI with Evidence: ForgeQE exports CSV datasets and runs Evidence (@evidence-dev/evidence 40.x, needs Node.js)
118
133
  # to build a static site, no always-on server. forgeqe bi init scaffolds the Evidence project, forgeqe bi sync refreshes
119
134
  # the data, forgeqe bi build does both and builds the site; without Node.js, or with --static, it writes ForgeQE's own
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capagents/forgeqe",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
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",
package/prompts/brain.md CHANGED
@@ -26,9 +26,11 @@ A message can also be a task specification with the parts Role, Objective, Conte
26
26
  **Tools** (ForgeQE tools; the client adds the forgeqe_ prefix):
27
27
  - browser_run: does one goal in a real browser, and verifies it. Use it for all work on a website. Set purpose to test for a flow of the application under test. Set purpose to task for all other work, such as finding, linking, or checking work items, or reading data from a tool. A task never becomes an automated test.
28
28
  - find_test_cases: reads test cases from Azure DevOps, Jira, Xray, TestRail, Zephyr Scale, qTest, GitHub issues, files, or text. It does not run them.
29
- - save_test_cases: saves test cases that you wrote from a story into Azure DevOps (Test Case work items) or Jira, linked to the story. It gives a link for each case.
29
+ - story_test_cases: gives the test cases already linked to an Azure DevOps or Jira story, with their steps. It changes nothing.
30
+ - save_test_cases: saves test cases that you wrote from a story into Azure DevOps (Test Case work items) or Jira, linked to the story. It never duplicates a linked case: it gives each case a status (created, unchanged, differs, similar, failed) and a link.
30
31
  - check_test_cases: checks test cases before they run. It finds expected results that are missing or not clear, missing test data, placeholders, and duplicates. It runs nothing.
31
- - update_test_case: writes the steps of an Azure DevOps Test Case again, after the user agrees. It asks the user.
32
+ - update_test_case: writes the steps of an Azure DevOps Test Case or a Jira test issue again, in place, after the user agrees. It asks the user.
33
+ - e2e_journey: plans end-to-end journeys from test cases that exist (the cases of a story, or any sources). It keeps the steps they share once, and gives each case as a section. It changes nothing.
32
34
  - release_readiness: gives requirement coverage and a GO / GO WITH RISKS / NO-GO verdict for a sprint, area, epic, or stories. It uses test cases, their results, bugs, and flaky tests. It can publish to the wiki. Examples: "are we ready to release?", "coverage of sprint 12".
33
35
  - run_test_cases: runs test cases in real browsers, with a verdict for each step. It writes report.html (steps, screenshots, timings) and junit.xml. It sends results back to the source system. It files or updates bugs for failures when bugs are set up. When this session writes tests, it gives each passed case to the coding agent immediately after the browser run of that case, before the next case starts. It gives the test of each case.
34
36
  - read_confluence: reads Confluence pages (requirements, specifications) as text, by id, link, or CQL.
@@ -43,6 +45,9 @@ A message can also be a task specification with the parts Role, Objective, Conte
43
45
  - flaky_tests: gives the flaky tests from that history, with their probable cause.
44
46
  - defect_trends: gives how the defects of the saved suites went over time: still there, gone, or back after a fix, the time to fix, the most seen, and defects by area. It runs nothing.
45
47
  - predict_defects: gives the areas where a change in the product repository can cause defects, with the reasons and the cases to run first. It is a heuristic from the history. It runs nothing.
48
+ - build_regression_pack: builds a regression pack for a release from an Azure DevOps sprint or work items, a commit range, pull requests, or changes you read from other connected tools: the tests the changes can break, each with why, and the changes no test covers. It runs nothing.
49
+ - run_regression_pack: runs a saved pack after the user confirms, on this machine, on TestMu AI, or in a pipeline, and compares the run with the last one.
50
+ - list_regression_packs: gives the saved packs and their last runs.
46
51
  - quarantine_tests: puts flaky tests in quarantine, or takes them out, after the user agrees. The failures of a test in quarantine do not fail runs or CI.
47
52
  - review_pull_request: Scribe reviews an Azure DevOps or GitHub pull request from the local clone (bugs, security, requirements, tests). The user sees the findings. It posts nothing.
48
53
  - post_pr_review: posts a review_pull_request review to the pull request after the user confirms.
@@ -160,10 +165,12 @@ Read data from Azure DevOps, Jira, Xray, Confluence, and other systems in this s
160
165
  - run_test_cases files bugs for failed cases itself when "This session" says so, only for functional defects. You MUST NOT file them again. Its write_back list names each bug.
161
166
  - run_test_cases checks the cases first (case_check in its result). When a case needs a review or cannot run as written, say so briefly, with the correction.
162
167
  - When the user asks if cases are good, ready, clear, or complete, you MUST call check_test_cases.
168
+ - When the user asks to run cases, you MUST call run_test_cases, also when check_test_cases or its card found problems. run_test_cases skips a case only when the setup says so (suite.case_check: block). You MUST NOT refuse a run because of a check verdict. Report what the check found with the result, and offer to fix the case with update_test_case.
163
169
  - check_test_cases also reads what each case tests: the story, the feature, the epic, linked Confluence pages, the RAG service, and page memory.
164
170
  - You MUST judge each case against that. Find criteria that no step covers, steps that no requirement asks for, wrong expected results, and controls that the application does not have.
165
171
  - For a case classified "test case needs updating", you MUST write the correct steps from run_details. Show them, and offer update_test_case.
166
172
  - You MUST NOT change a test case without update_test_case, which asks the user first.
173
+ - When the user reviews a saved case and asks for a change, you MUST call update_test_case with the id of that case. You MUST NOT save the changed case again with save_test_cases, and you MUST NOT write a new case for it.
167
174
  - To link cases to a story, or to check a link, you MUST use the MCP tools or query_api, not the browser. When the browser is the only way, call browser_run with purpose task.
168
175
  - A browser run of a test case is the test. When a case does not pass, run_test_cases gives the type of failure (failure.type). You MUST report each failed case by that type, in plain words:
169
176
  - A functional defect: the application is wrong. Give the bug, and the test for the correct behaviour, marked as an expected failure.
@@ -182,17 +189,27 @@ Domain applies when you write and check test cases, test from requirements, do e
182
189
  ### Testing from requirements
183
190
  For a Confluence page, a Jira or Azure DevOps story, or a specification file:
184
191
  - You MUST read the requirements with read_confluence or the Confluence MCP tools, the MCP tools of the story or query_api, or the file.
192
+ - For an Azure DevOps or Jira story, you MUST call story_test_cases before you write a case. Write cases only for the acceptance criteria, rules, and risks that no linked case covers. For a linked case that no longer agrees with the story, propose its new steps with update_test_case. You MUST NOT write a linked case again. When the linked cases cover everything, say so and write nothing.
185
193
  - You MUST write one case for each acceptance criterion or rule, and the main negative paths.
186
194
  - You MUST then write one case for each domain risk and obligation on your list that the story does not cover.
187
195
  - Each case MUST have a title, the start page when you know it, and numbered steps with an expected result, specific enough for a browser.
188
196
  - When the story is in Azure DevOps or Jira, and "This session" says that ForgeQE saves cases there, you MUST save them with save_test_cases before they run.
189
197
  - Give save_test_cases the system, the story, and each case with its steps and expected results. You MUST NOT create the cases with the MCP tools or the browser.
190
- - In the reply, you MUST list each saved case as a markdown link to its work item, with its title, and without its full steps.
198
+ - save_test_cases writes nothing for a case with status differs or similar. You MUST NOT say that such a case was saved or updated. Show how it differs from linked_case, and offer update_test_case. Use force_new only when the user says that it is a different case.
199
+ - In the reply, you MUST list each saved case as a markdown link to its work item, with its title and its status, and without its full steps.
191
200
  - When ForgeQE does not save cases there, or the user says not to, you MUST show them as a short markdown list with numbered titles, steps, and expected results.
192
201
  - You MUST then run them with run_test_cases, unless the user asked only for the cases.
193
202
  - You MUST run saved Azure DevOps cases by their ids (save_test_cases gives run_with), so that the results go back. Run other cases as text sources, one entry for each case.
194
203
  - You MUST save cases from other requirements into a test management system only when the user asks. Use save_test_cases when there is a story to link them to. Give the new ids as links.
195
204
 
205
+ ### End-to-end journeys from test cases
206
+ For "an end-to-end test for story 1234", "combine these cases into one flow", or "E2E from the test plan":
207
+ - You MUST build on the cases that exist. Call e2e_journey with the story, or with the sources. You MUST NOT write the cases again first.
208
+ - When the story has no linked cases yet, write and save them by "Testing from requirements" first, then call e2e_journey.
209
+ - Order the sections so that each one leaves the application in the state that the next one needs. A negative section goes last, or stays its own case.
210
+ - Show each journey with the cases that it covers. When the user agrees, save it with save_test_cases as one case titled "E2E: …", with a description that names the cases it covers. Keep the original cases.
211
+ - For end-to-end API scenarios, use api_scenarios.
212
+
196
213
  ### Signing in
197
214
  - The browser agent signs in to the sites under "Saved logins" with stored credentials and a saved session. You MUST NOT ask the user for those passwords or codes.
198
215
  - You MUST NOT put credentials in goals.
@@ -217,10 +234,25 @@ For a Confluence page, a Jira or Azure DevOps story, or a specification file:
217
234
  - You MUST name the files that it made: report.html in the run folder, and the test that write_test wrote. Say if the run of that test passed. When ForgeQE wrote no test, say so and why.
218
235
  - When the user wants logs or details to share for debugging, tell them to type /logs, and add shots for screenshots. Outside the shell, forgeqe logs bundle writes one archive with secrets replaced.
219
236
 
237
+ ### Regression packs
238
+ When the user asks for a regression pack, or which tests to run for a release, a sprint, or recent pull requests:
239
+ - You MUST give build_regression_pack every source the user names. Give an Azure DevOps sprint, area, epic, or work items directly. Give a commit range as from and to, with range_repo when it is the product repository. Give pull requests as prs or merged_days.
240
+ - When the changes are in another connected tool (a Jira release or fix version, GitHub issues, a release note in Confluence), you MUST read them with that tool first. Then you MUST pass each one in items with its id, title, text, url, and the test cases and work items it links to. You MUST NOT shorten their text: the words find the tests.
241
+ - You MUST report the tests by why they are in, the changes no test covers with the closest test for each, and the risk areas.
242
+ - When the user did not say where to run the pack, you MUST ask one question: on this machine, on TestMu AI, or in a pipeline. You MUST NOT run a pack until the user agrees.
243
+ - After a run, you MUST name the failing tests with their messages, and the tests that fail now but passed in the last run.
244
+
220
245
  ### Writing tests
221
246
  When the user asks, or when the setup below says that passed runs become tests:
222
247
  - You MUST call write_test with the folder of the passed run. Give the repository when it is not the working directory.
223
- - When an earlier job wrote a test for the same flow, browser_run runs that test first. When it passes, browser_run returns reused_test, and written_for (the goal the test was written for), and does not run the browser. You MUST compare written_for with the request of the user. When the request asks for a check that written_for does not have, you MUST call browser_run again with fresh set to true. Otherwise, you MUST NOT call write_test, and you MUST report the test file and that it passed. When it fails, browser_run runs the flow, and write_test updates that test. You MUST set fresh to true only when the user asks for a new browser run or a new test.
248
+ - When an earlier job wrote a test for the same flow, browser_run runs that test first. When it passes, browser_run returns reused_test, and written_for (the goal the test was written for), and does not run the browser. You MUST compare written_for with the request of the user. When the request asks for a step or a check that written_for does not have, you MUST call browser_run again with existing_test set to reused_test and existing_mode extend. Otherwise, you MUST NOT call write_test, and you MUST report the test file and that it passed. When it fails, browser_run runs the flow, and write_test updates that test. You MUST set fresh to true only when the user asks for a new browser run or a new test.
249
+ - Before a browser run for a test, browser_run searches the tests of the repository. When some are close to the request but not the same flow, it returns status existing_tests_found and does not start the browser. For each test it gives its steps, the elements of the request that it covers and lacks, and written_for when an earlier job wrote it. You MUST compare them with the request of the user before any browser run. Then you MUST call browser_run again with the same goal and one of these:
250
+ - existing_test (the test, Class.Method) and existing_mode reuse, when that test already does and checks everything the request asks.
251
+ - existing_mode extend, when the test covers the start of the journey and the request adds a few steps or checks to it.
252
+ - existing_mode sibling, when the request is a variant of the test: another path, other data, more than a few added steps, or a test that is long already. A new test goes beside it in its class and reuses its setup and steps.
253
+ - fresh set to true, when no test is this journey. The new test still reuses the page objects and helpers of the repository.
254
+ When existing_tests_found has sent_as task, you sent a check of the application as a task, and tests of the repository are close to it. When one of them does what the check asks, you MUST answer the check with it: call browser_run again with the same goal and purpose, existing_test set to that test, and existing_mode reuse. When the user asked for a test, you MUST use purpose test and one of the choices above. You MUST set fresh to true only when no test does what the check asks, or when the check needs data that a passing test does not give, such as a value to read.
255
+ You MUST NOT write a new test file for a journey that an existing test covers in part. After extend or sibling, write_test does what you chose. You MUST NOT give it a mode.
224
256
  - Cases that run_test_cases ran in a session that writes tests already have their test. You MUST call write_test only for a case that has none, when the user asks.
225
257
  - You MUST NOT write that test yourself. write_test gives the run to the coding agent of ForgeQE, and the user watches it.
226
258
  - Reuse before you rebuild, and extend before you duplicate. By default (mode auto), write_test measures which tests of the repository already cover the steps and checks of the run. It reuses a test that covers all of them, extends a test that covers most of them with only the missing steps, or writes a new test. When this is not clear, it shows the options to the user and the user selects one. You MUST NOT ask the user the same question again.
@@ -0,0 +1,13 @@
1
+ ---
2
+ name: browser-api
3
+ description: "How the browser agent makes the API steps of a test case, in the browser's session, so they are recorded and repeated."
4
+ used_by: "the browser agent (every browser run)"
5
+ kind: fragment
6
+ ---
7
+ ## API steps
8
+ - For a step that calls an API (an HTTP method, an endpoint, or a payload), you MUST use api_request. You MUST NOT call fetch or XMLHttpRequest from evaluate.
9
+ - Before the first api_request, you MUST open a page of the site. api_request runs in the browser's session and sends its cookies.
10
+ - When the site needs a CSRF token, you MUST set csrf to "cookie" or "form". Use "cookie" for a csrftoken or XSRF-TOKEN cookie, and "form" for a token in the page's form.
11
+ - For a form payload, you MUST give body as name=value pairs joined with &, and content_type application/x-www-form-urlencoded. For JSON, you MUST give the JSON text and content_type application/json.
12
+ - You MUST judge an API step on the server's answer: its status, or a value in the response. You MUST NOT judge it on what the page shows. You MUST NOT record an API answer with record_check. The api_request result is the evidence of an API step. To see the effect of the call in the UI, you SHOULD open or reload the page after it.
13
+ - You MUST use every URL exactly as the task gives it. You MUST NOT add or remove a trailing slash, path segments, or parameters. When a URL that you changed fails, you MUST use the URL as given.
@@ -6,10 +6,10 @@ kind: fragment
6
6
  ---
7
7
  ## Acting so the run can become a test
8
8
  ForgeQE turns a passed run into an automated test. The test repeats your actions with real clicks and typing.
9
- - You MUST act only through the browser actions: click, input, select_dropdown, send_keys, and click_selector.
9
+ - You MUST act only through the browser actions: click, input, select_dropdown, send_keys, and click_selector, and through api_request for a step that calls an API.
10
10
  - 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.
11
11
  - You MAY use evaluate only to read the page.
12
- - You MUST open a URL only for the start page, or when the task gives the URL.
12
+ - You MUST open a URL only for the start page, or when the task gives the URL. Open it exactly as given: you MUST NOT add a trailing slash or change its path.
13
13
  - You MUST NOT add #section or ?parameters to a URL to go where a step takes you. That skips the step.
14
14
  - When you open a wrong URL, you MUST go back to the start page. Then do the steps.
15
15
  - 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.
@@ -12,6 +12,8 @@ ForgeQE turns a passed run into an automated test. The test asserts the results
12
12
  - 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.
13
13
  - You MUST record only the results that the task tells you to verify.
14
14
  - When the task lists expected results, you MUST record one check for each, and copy `expected` from that line.
15
+ - When the task is a test case with numbered steps, you MUST set `step` to the number of the step that the result belongs to.
16
+ - You MUST record the check of a step as soon as its expected result shows, before you start the next step.
15
17
  - ForgeQE finds the element that shows `text`. You MUST select text that points to the subject of the line:
16
18
  - 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".
17
19
  - For a page or a section that opens, copy its heading or a different label that names it.
@@ -0,0 +1,130 @@
1
+ """
2
+ API steps in a browser run: api_request calls an HTTP endpoint from the page, in the browser's own session (its
3
+ cookies go with the call), so a test case can mix API and UI steps on one logged-in user. Each call is recorded as an
4
+ "api" step with its method, URL, body, and the response's status, so the verdict rests on the status the server
5
+ gave, the replay and the written test repeat the call, and ForgeQE keeps it as a reusable API scenario.
6
+ """
7
+ # No `from __future__ import annotations`: the library matches the injected browser_session parameter by its real type.
8
+ import asyncio
9
+ import json
10
+ import logging
11
+ from typing import Any, Dict
12
+
13
+ from browser_use import ActionResult, BrowserSession
14
+
15
+ logger = logging.getLogger("forgeqe_engine.api_step")
16
+
17
+ # An endpoint that does not answer in this time fails the step instead of holding the run.
18
+ CALL_TIMEOUT = 30.0
19
+
20
+ API_SYSTEM_MESSAGE = """
21
+ ## API steps
22
+ - For a step that calls an API (an HTTP method, an endpoint, or a payload), you MUST use api_request. You MUST NOT call fetch or XMLHttpRequest from evaluate.
23
+ - Before the first api_request, you MUST open a page of the site. api_request runs in the browser's session and sends its cookies.
24
+ - When the site needs a CSRF token, you MUST set csrf to "cookie" or "form". Use "cookie" for a csrftoken or XSRF-TOKEN cookie, and "form" for a token in the page's form.
25
+ - For a form payload, you MUST give body as name=value pairs joined with &, and content_type application/x-www-form-urlencoded. For JSON, you MUST give the JSON text and content_type application/json.
26
+ - You MUST judge an API step on the server's answer: its status, or a value in the response. You MUST NOT judge it on what the page shows. You MUST NOT record an API answer with record_check. The api_request result is the evidence of an API step. To see the effect of the call in the UI, you SHOULD open or reload the page after it.
27
+ - You MUST use every URL exactly as the task gives it. You MUST NOT add or remove a trailing slash, path segments, or parameters. When a URL that you changed fails, you MUST use the URL as given.
28
+ """.strip()
29
+
30
+ # Runs in the page: the call carries the page's cookies; a CSRF token is read where the site keeps it.
31
+ _FETCH_JS = r"""async (a) => {
32
+ const cookie = (n) => { const c = document.cookie.split('; ').find((x) => x.startsWith(n + '=')); return c ? decodeURIComponent(c.slice(n.length + 1)) : '' };
33
+ let token = '';
34
+ if (a.csrf === 'cookie') token = cookie('csrftoken') || cookie('XSRF-TOKEN') || cookie('csrf_token') || cookie('_csrf');
35
+ if (a.csrf === 'form') {
36
+ const f = document.querySelector('input[name="csrfmiddlewaretoken"], input[name="_csrf"], input[name="csrf_token"], input[name="authenticity_token"], input[name="__RequestVerificationToken"]');
37
+ const m = document.querySelector('meta[name="csrf-token"], meta[name="_csrf"]');
38
+ token = (f && f.value) || (m && m.content) || '';
39
+ }
40
+ const headers = {};
41
+ if (a.contentType) headers['Content-Type'] = a.contentType;
42
+ let body = a.body || undefined;
43
+ if (token) {
44
+ headers['X-CSRFToken'] = token;
45
+ headers['X-XSRF-TOKEN'] = token;
46
+ headers['X-CSRF-Token'] = token;
47
+ // A form post carries the token in the form too, as the site's own form would.
48
+ if (/x-www-form-urlencoded/i.test(a.contentType || '') && !/(^|&)(csrfmiddlewaretoken|_csrf|authenticity_token)=/.test(body || '')) body = (body ? body + '&' : '') + 'csrfmiddlewaretoken=' + encodeURIComponent(token);
49
+ }
50
+ const method = (a.method || 'GET').toUpperCase();
51
+ try {
52
+ const r = await fetch(a.url, { method, headers, body: method === 'GET' || method === 'HEAD' ? undefined : body, credentials: 'include', redirect: 'follow' });
53
+ const text = await r.text();
54
+ return { status: r.status, ok: r.ok, final_url: r.url, redirected: r.redirected, content_type: r.headers.get('content-type') || '', body: text.slice(0, 4000), csrf_sent: Boolean(token) };
55
+ } catch (e) {
56
+ return { error: String(e && e.message || e), page: location.href };
57
+ }
58
+ }"""
59
+
60
+
61
+ def _snippet(text: str, n: int = 300) -> str:
62
+ """The start of a response body on one line: JSON as it is, HTML without its tags."""
63
+ s = text.strip()
64
+ if s.startswith("<"):
65
+ import re
66
+
67
+ title = re.search(r"<title[^>]*>(.*?)</title>", s, re.I | re.S)
68
+ s = (f"[HTML: {title.group(1).strip()}] " if title else "[HTML] ") + re.sub(r"\s+", " ", re.sub(r"<[^>]+>", " ", s))
69
+ return " ".join(s.split())[:n]
70
+
71
+
72
+ def describe(call: Dict[str, Any]) -> str:
73
+ """"POST https://shop.test/payment → 200, ended at https://shop.test/payment_done/1 · response: …" for the agent and the trace."""
74
+ if call.get("error"):
75
+ return f"{call['method']} {call['url']} failed: {call['error']}"
76
+ end = f", ended at {call['final_url']}" if call.get("redirected") and call.get("final_url") else ""
77
+ csrf = " (CSRF token sent)" if call.get("csrf_sent") else ""
78
+ return f"{call['method']} {call['url']}{csrf} → {call['status']}{end} · response: {call.get('snippet') or '(empty)'}"
79
+
80
+
81
+ def register_api_request(tools: Any, captured: Dict[str, Any]) -> None:
82
+ """api_request: an HTTP call from the page's session; the call and its answer are captured for the step."""
83
+
84
+ @tools.action(
85
+ "Call an HTTP API endpoint from this browser's session (the session's cookies are sent) and get the status and "
86
+ "the start of the response. Use it for every step that calls an API; never fetch() from evaluate. "
87
+ "method: GET, POST, PUT, PATCH, or DELETE. url: exactly as the task gives it. body: name=value pairs joined with & "
88
+ "for a form, or JSON text. content_type: application/x-www-form-urlencoded or application/json. "
89
+ "csrf: empty, cookie, or form."
90
+ )
91
+ async def api_request(
92
+ method: str,
93
+ url: str,
94
+ browser_session: BrowserSession,
95
+ body: str = "",
96
+ content_type: str = "",
97
+ csrf: str = "",
98
+ ) -> ActionResult:
99
+ args = {"method": (method or "GET").upper(), "url": url, "body": body, "contentType": content_type, "csrf": (csrf or "").lower()}
100
+ try:
101
+ cdp = await browser_session.get_or_create_cdp_session()
102
+ res = await asyncio.wait_for(
103
+ cdp.cdp_client.send.Runtime.evaluate(
104
+ params={"expression": f"({_FETCH_JS})({json.dumps(args)})", "awaitPromise": True, "returnByValue": True},
105
+ session_id=cdp.session_id,
106
+ ),
107
+ timeout=CALL_TIMEOUT,
108
+ )
109
+ value = ((res or {}).get("result") or {}).get("value") or {}
110
+ except Exception as ex: # The page went away, or the call could not be made.
111
+ value = {"error": str(ex)}
112
+ call: Dict[str, Any] = {"method": args["method"], "url": url, "body": body, "content_type": content_type, "csrf": args["csrf"]}
113
+ if value.get("error"):
114
+ call["error"] = value["error"]
115
+ captured["api"] = call
116
+ hint = " Open a page of this site first: the call is made from the page." if str(value.get("page", "")).startswith("about:") else ""
117
+ return ActionResult(error=f"api_request: {describe(call)}.{hint}")
118
+ call.update(
119
+ status=int(value.get("status") or 0),
120
+ ok=bool(value.get("ok")),
121
+ final_url=str(value.get("final_url") or url),
122
+ redirected=bool(value.get("redirected")),
123
+ content_type_received=str(value.get("content_type") or ""),
124
+ csrf_sent=bool(value.get("csrf_sent")),
125
+ snippet=_snippet(str(value.get("body") or "")),
126
+ )
127
+ captured["api"] = call
128
+ line = describe(call)
129
+ # A 4xx or 5xx is the server's answer, not a failed action: the agent judges it against what the step expects.
130
+ return ActionResult(extracted_content=line, long_term_memory=line)
@@ -12,6 +12,7 @@ Live capture for the test a passed run becomes, done while the page is still the
12
12
  import asyncio
13
13
  import json
14
14
  import logging
15
+ import re
15
16
  import time
16
17
  from typing import Any, Dict, List, Optional
17
18
 
@@ -36,10 +37,10 @@ MAX_CLIMB = 5
36
37
  CAPTURE_SYSTEM_MESSAGE = """
37
38
  ## Acting so the run can become a test
38
39
  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 act only through the browser actions: click, input, select_dropdown, send_keys, and click_selector, and through api_request for a step that calls an API.
40
41
  - 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
42
  - 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 open a URL only for the start page, or when the task gives the URL. Open it exactly as given: you MUST NOT add a trailing slash or change its path.
43
44
  - You MUST NOT add #section or ?parameters to a URL to go where a step takes you. That skips the step.
44
45
  - When you open a wrong URL, you MUST go back to the start page. Then do the steps.
45
46
  - 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.
@@ -114,6 +115,45 @@ def action_name(action: Any) -> str:
114
115
  return next((k for k, v in data.items() if v is not None), "")
115
116
 
116
117
 
118
+ _URL = re.compile(r"https?://[^\s\"'<>)\]]+")
119
+
120
+
121
+ def task_urls(task: str) -> List[str]:
122
+ """The URLs the task gives, without the punctuation that ends a sentence around them."""
123
+ return [u.rstrip(".,;:!?") for u in _URL.findall(task or "")]
124
+
125
+
126
+ def exact_task_url(url: str, given: Optional[List[str]]) -> Optional[str]:
127
+ """
128
+ The task's own URL when `url` differs from it only by a trailing slash. Models add one (/login/), and on many
129
+ sites that is a 404: the URL the task gave is the one to open. None when `url` is fine as it is.
130
+ """
131
+ if not url or not given or url in given:
132
+ return None
133
+ return next((t for t in given if t != url and t.rstrip("/") == url.rstrip("/")), None)
134
+
135
+
136
+ def correct_task_urls(payload: Dict[str, Any], given: Optional[List[str]]) -> None:
137
+ """
138
+ A step's actions, as shown and recorded, with the task's own URL where the model added a slash: what install_effects
139
+ opens. The log, the trace, page memory, and the written test then all say the URL that was used.
140
+ """
141
+ actions = payload.get("actions") or []
142
+ for i, action in enumerate(actions):
143
+ for name in ("navigate", "api_request"):
144
+ params = action.get(name) if isinstance(action, dict) else None
145
+ if not isinstance(params, dict) or not params.get("url"):
146
+ continue
147
+ fixed = exact_task_url(str(params["url"]), given)
148
+ if not fixed:
149
+ continue
150
+ for key in ("action_lines", "plain_actions"):
151
+ lines = payload.get(key) or []
152
+ if i < len(lines) and isinstance(lines[i], str):
153
+ lines[i] = lines[i].replace(str(params["url"]), fixed)
154
+ params["url"] = fixed
155
+
156
+
117
157
  def install_effects(tools: Any, effects: List[Dict[str, Any]], captured: Dict[str, Any]) -> None:
118
158
  """
119
159
  Wrap tools.act so each action call leaves {action, effect} in `effects`, in call order; click_selector also leaves
@@ -124,14 +164,25 @@ def install_effects(tools: Any, effects: List[Dict[str, Any]], captured: Dict[st
124
164
  async def act(action: Any, browser_session: Any = None, *args: Any, **kwargs: Any):
125
165
  session = browser_session if browser_session is not None else kwargs.get("browser_session")
126
166
  name = action_name(action)
167
+ # A URL the task gave, opened with a slash it does not have: open it exactly as given.
168
+ if name in ("navigate", "api_request"):
169
+ # The library's ActionModel is a root model: the action's parameters sit under .root.
170
+ params = getattr(getattr(action, "root", action), name, None)
171
+ fixed = exact_task_url(str(getattr(params, "url", "") or ""), captured.get("task_urls"))
172
+ if fixed and params is not None:
173
+ logger.info("%s: using the task's URL %s instead of %s", name, fixed, params.url)
174
+ params.url = fixed
127
175
  before = await page_state(session) if name in EFFECT_ACTIONS and session is not None else None
128
176
  captured.pop("target", None)
177
+ captured.pop("api", None)
129
178
  result = await original(action, browser_session, *args, **kwargs)
130
179
  entry: Dict[str, Any] = {"action": name, "effect": None}
131
180
  if name in EFFECT_ACTIONS and session is not None and not getattr(result, "error", None):
132
181
  entry["effect"] = await effect_after(session, before)
133
182
  if captured.get("target") is not None:
134
183
  entry["target"] = captured.pop("target")
184
+ if captured.get("api") is not None:
185
+ entry["api"] = captured.pop("api")
135
186
  effects.append(entry)
136
187
  return result
137
188
 
@@ -25,6 +25,8 @@ ForgeQE turns a passed run into an automated test. The test asserts the results
25
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
26
  - You MUST record only the results that the task tells you to verify.
27
27
  - When the task lists expected results, you MUST record one check for each, and copy `expected` from that line.
28
+ - When the task is a test case with numbered steps, you MUST set `step` to the number of the step that the result belongs to.
29
+ - You MUST record the check of a step as soon as its expected result shows, before you start the next step.
28
30
  - ForgeQE finds the element that shows `text`. You MUST select text that points to the subject of the line:
29
31
  - 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
32
  - For a page or a section that opens, copy its heading or a different label that names it.
@@ -72,9 +74,10 @@ def register_checks(tools: Any, checks: List[Dict[str, Any]]) -> None:
72
74
  "Record one result the task asks you to verify, before done: expected (the result in a few words), "
73
75
  "text (the shortest exact text on the page that shows it, copied as shown), and dynamic=true when that text "
74
76
  "is live data that will differ on another run (today's date, a price, a count, today's featured item). "
75
- "holds=false when the page does not show the expected result: then text is what the page shows instead."
77
+ "holds=false when the page does not show the expected result: then text is what the page shows instead. "
78
+ "step: when the task is a test case with numbered steps, the number of the step whose expected result this is."
76
79
  )
77
- async def record_check(expected: str, text: str, browser_session: BrowserSession, dynamic: bool = False, holds: bool = True) -> ActionResult:
80
+ async def record_check(expected: str, text: str, browser_session: BrowserSession, dynamic: bool = False, holds: bool = True, step: int = 0) -> ActionResult:
78
81
  expected, text = " ".join(str(expected or "").split())[:200], " ".join(str(text or "").split())[:200]
79
82
  if not text:
80
83
  return ActionResult(error="Give the text on the page that shows the result.")
@@ -91,6 +94,9 @@ def register_checks(tools: Any, checks: List[Dict[str, Any]]) -> None:
91
94
  if not holds:
92
95
  # A failed expectation is a test result: `text` is what the page shows instead.
93
96
  check["holds"] = False
97
+ if isinstance(step, int) and step > 0:
98
+ # Which test step this checks: memory records where that step ended, to replay it after the case is edited.
99
+ check["step"] = step
94
100
  # The element showing the text and its section, with locators checked on this page, for the test's assertion.
95
101
  found = await locate_check(browser_session, text, dynamic=dynamic)
96
102
  if found: