@capagents/forgeqe 2.2.0 → 2.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/HOW_TO_USE.md +133 -14
- package/dist/forgeqe.js +993 -906
- package/dist/template.html +36 -36
- package/docs/user-guide.html +126 -3
- package/package.json +2 -1
- package/prompts/README.md +1 -0
- package/prompts/brain.md +3 -1
- package/prompts/test-plan.md +61 -0
- package/py/forgeqe_engine/cli.py +44 -0
- package/py/forgeqe_engine/dynamic.py +149 -0
- package/py/forgeqe_engine/recordsession.py +908 -0
- package/py/forgeqe_engine/webrecord.py +589 -0
- package/py/forgeqe_engine/webrecord_page.py +530 -0
- package/reporters/playwright.mjs +83 -0
package/docs/user-guide.html
CHANGED
|
@@ -379,7 +379,7 @@ forgeqe docs --copy ./docs # copy the guide, the architecture page, and the re
|
|
|
379
379
|
</code></pre>
|
|
380
380
|
</div>
|
|
381
381
|
<h3 class="band-title">Every feature</h3>
|
|
382
|
-
<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>
|
|
382
|
+
<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><li><a class="tile" href="#cap-record-web"><b>Record a web test by hand</b><span>I'll show you the checkout steps in the shop: record them</span></a></li></ul></div>
|
|
383
383
|
<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>
|
|
384
384
|
<div class="area"><a class="area-name" href="#area-setup"><span class="mono">Setup</span></a><ul class="tiles"><li><a class="tile" href="#cap-test-apps"><b>Test environments and their apps</b><span>Place an order in the shop, then check its invoice in Billing, in UAT</span></a></li><li><a class="tile" href="#cap-generated-code"><b>What ForgeQE wrote</b><span>How much of the code ForgeQE wrote is still there?</span></a></li><li><a class="tile" href="#cap-forgeqe-commands"><b>Every forgeqe command from the shell</b><span>Show me the context budget</span></a></li><li><a class="tile" href="#cap-setup-help"><b>Setup, skills, and documentation</b><span>What can you do?</span></a></li></ul></div>
|
|
385
385
|
<div class="area"><a class="area-name" href="#area-desktop"><span class="mono">Desktop</span></a><ul class="tiles"><li><a class="tile" href="#cap-desktop-apps"><b>Automate Windows desktop apps</b><span>Create an invoice for Contoso in the Billing app and save it as a test</span></a></li></ul></div>
|
|
@@ -997,6 +997,99 @@ forgeqe memory stats # how often memory stood in for the
|
|
|
997
997
|
</section>
|
|
998
998
|
</div>
|
|
999
999
|
</article>
|
|
1000
|
+
<article class="spec feat" id="cap-record-web">
|
|
1001
|
+
<div class="hd"><span class="mono">Browser</span><h3>Record a web test by hand</h3></div>
|
|
1002
|
+
<p class="ds">Opens a browser with the Recorder panel docked in the page while the user does the steps of a web test themself, and saves their clicks, typing, choices and keys in order as a run, with the checks, kept values, test data and notes they add; the coding agent then turns it into a test in the repository's framework.</p>
|
|
1003
|
+
<div class="facts">
|
|
1004
|
+
<div class="fact"><span class="mono">Ask it</span><ul class="asks"><li>I'll show you the checkout steps in the shop: record them</li><li>Record my steps on the cart page and make a test of it</li></ul></div>
|
|
1005
|
+
<div class="fact"><span class="mono">What it needs</span><ul class="needs"><li><span class="need opt">Optional</span><span>The coding agent, Scribe: a live model profile (or code.model) and the agent runtime, which forgeqe setup installs</span></li></ul></div>
|
|
1006
|
+
<div class="fact"><span class="mono">Limits</span><ul class="plainlist"><li>Local Chrome or Chromium only, one recording at a time; drag and drop and steps inside iframes are not recorded yet</li></ul></div>
|
|
1007
|
+
<div class="fact"><span class="mono">Tools</span><p class="tools"><code>web_record</code></p></div>
|
|
1008
|
+
</div>
|
|
1009
|
+
<div class="detail"><span class="mono">In detail</span>
|
|
1010
|
+
<section class="doc" id="doc-how_to_use-recording-a-web-test-by-hand">
|
|
1011
|
+
<h4>Recording a web test by hand</h4>
|
|
1012
|
+
<div class="prose"><p>When you would rather show the steps than describe them, record them. ForgeQE opens Chrome on the app's page with the
|
|
1013
|
+
<strong>Recorder</strong> panel docked in a corner of the page, in the same window: you click, type and choose as usual, and each
|
|
1014
|
+
action is listed in the panel as a numbered step, in order. Drag the panel by its title to another corner, or make it
|
|
1015
|
+
small (a pill that shows the step count); it comes back where you left it on every page.</p>
|
|
1016
|
+
<pre><code class="language-bash">forgeqe record shop --name "Apply a coupon at checkout" # a web app from env.yaml
|
|
1017
|
+
forgeqe record --url https://staging.shop.test/cart # or any page
|
|
1018
|
+
</code></pre>
|
|
1019
|
+
<p>In the shell, say "I'll show you the checkout, record it" (or <code>/record shop</code>). What you can add while recording:</p>
|
|
1020
|
+
<div class="tbl"><table>
|
|
1021
|
+
<thead>
|
|
1022
|
+
<tr><th>In the panel</th><th>In the page: Ctrl+Shift+Space, then</th><th>What it adds</th></tr>
|
|
1023
|
+
</thead>
|
|
1024
|
+
<tbody>
|
|
1025
|
+
<tr><td>Check, then click an element</td><td><strong>A</strong></td><td>A check: its text, or its value for a field, or checked for a checkbox. Change the kind (text, visible, hidden, value) and mark <strong>live data</strong> (check the pattern, not this value) in the panel</td></tr>
|
|
1026
|
+
<tr><td>Variable, then click an element</td><td><strong>V</strong></td><td>Keeps the element's text for later steps: typing the same text later is written as <code>{{name}}</code></td></tr>
|
|
1027
|
+
<tr><td>Wait for, then click an element</td><td><strong>W</strong></td><td>The test waits for the element to show</td></tr>
|
|
1028
|
+
<tr><td>Test data</td><td><strong>D</strong></td><td>The last value you typed becomes test data the test reads from its data</td></tr>
|
|
1029
|
+
<tr><td><code>note: …</code> in the box</td><td><strong>N</strong></td><td>A note on the last step: "this is the cart page object"</td></tr>
|
|
1030
|
+
<tr><td>Pause / Undo / Stop, ✕ on a step</td><td><strong>P</strong> / <strong>Z</strong> / <strong>S</strong></td><td>Pause records nothing until you resume; ✕ removes that step</td></tr>
|
|
1031
|
+
</tbody>
|
|
1032
|
+
</table></div>
|
|
1033
|
+
<p>Type a check in words in the panel's box, or in the terminal: <code>the total shows "$40.00"</code>. Quoted text the page shows
|
|
1034
|
+
becomes a check on the element showing it; without quotes, one model call finds the text that shows it on the page. A
|
|
1035
|
+
check the page cannot show yet is kept in your words, and the coding agent writes it. In the terminal, <code>undo</code>, <code>pause</code>,
|
|
1036
|
+
<code>delete 4</code> and <code>stop</code> work too.</p>
|
|
1037
|
+
<p>What is recorded: clicks (a double click as one step), the final text of each field you typed in (not each key), choices
|
|
1038
|
+
in drop-downs, Enter and Escape, key chords, pages you open from the address bar, and Back. A click on a menu item that
|
|
1039
|
+
opened when the pointer went over its trigger gets a hover on the trigger first. Password fields are written as an
|
|
1040
|
+
environment variable (the saved login's <code>password_env</code>, or <code><SITE>_PASSWORD</code>), and text equal to a saved login's value
|
|
1041
|
+
as its variable: what you type in them is never stored. A file you choose for an upload is recorded by its path in the
|
|
1042
|
+
working folder; one outside it, a hidden file (<code>.env</code>, anything under <code>.git</code>), or one named like credentials or keys is
|
|
1043
|
+
listed as not recorded. Drag and drop is listed as not recorded. <code>--signed-in</code> starts with the saved sessions of
|
|
1044
|
+
<code>logins:</code>; without it, record the sign-in too.</p>
|
|
1045
|
+
<p><strong>Data that changes from run to run.</strong> A test that spells out this recording's price, date, or order number fails on
|
|
1046
|
+
the next run, so ForgeQE keeps what holds and makes the rest a pattern, a variable, or a value made fresh:</p>
|
|
1047
|
+
<div class="tbl"><table>
|
|
1048
|
+
<thead>
|
|
1049
|
+
<tr><th>In the recording</th><th>In the test</th></tr>
|
|
1050
|
+
</thead>
|
|
1051
|
+
<tbody>
|
|
1052
|
+
<tr><td>A check of a price, date, time, order number, or count ("$14.15", "2,463 found")</td><td>A check of its pattern: <code>/^\$\d[\d,]*\.\d{2}$/</code></td></tr>
|
|
1053
|
+
<tr><td>The same value checked again later (the product's price, then the cart total)</td><td>The first check keeps it as <code>{{price}}</code>; the later one checks <code>{{price}}</code></td></tr>
|
|
1054
|
+
<tr><td>A link or button named with such data ("Combination Pliers $14.15")</td><td>Found by the part that holds: <code>getByRole("link", { name: /Combination Pliers/ })</code></td></tr>
|
|
1055
|
+
<tr><td>A date you typed</td><td>The date the same number of days from today: <code>{{$date:+3:%Y-%m-%d}}</code></td></tr>
|
|
1056
|
+
<tr><td>A value the app refuses twice (a new account's email)</td><td>A part new on each run: <code>alice+{{$unique}}@example.com</code></td></tr>
|
|
1057
|
+
</tbody>
|
|
1058
|
+
</table></div>
|
|
1059
|
+
<p>Prices, dates, times, ids, and long numbers are found by their form; a small number on its own ("1" in the cart) stays
|
|
1060
|
+
exact, as it is usually what the check is about. When you stop, one model call sorts the values the forms cannot
|
|
1061
|
+
tell (a new account's email, "today's deal"); without a model, those stay as recorded. Each change shows in the panel
|
|
1062
|
+
with a tick box: untick it to keep the value as recorded.</p>
|
|
1063
|
+
<p><strong>Recording a test case.</strong> <code>forgeqe record --case SHOP-12</code> (any source ForgeQE reads cases from: Jira, Azure DevOps,
|
|
1064
|
+
Xray, TestRail, a file) starts on the case's page and shows its steps as a checklist in the panel. Click the step you
|
|
1065
|
+
are on (or <strong>Next case step</strong>): what you record belongs to it, and a check you add there takes its expected result.
|
|
1066
|
+
Each expected result shows <strong>✓ checked</strong> or <strong>no check yet</strong>; Stop warns once about the ones with no check, and a second
|
|
1067
|
+
Stop stops. The test is named and tagged for the case, and mapped to it once it passes.</p>
|
|
1068
|
+
<p>When you stop, the run folder holds <code>workflow.yaml</code> and <code>trace.md</code> as for any run. With a repository (<code>--repo</code>, or
|
|
1069
|
+
<code>code.repo</code>), ForgeQE asks whether to turn it into a test. First a planner reads the repository and shows the plan:</p>
|
|
1070
|
+
<pre><code>Test tests/add-to-cart.spec.ts › adds pliers to the cart
|
|
1071
|
+
Steps
|
|
1072
|
+
2. search: HomePage.searchFor()
|
|
1073
|
+
4. open the product: new HomePage.product(name)
|
|
1074
|
+
6. add to the cart: new ProductPage.addToCart()
|
|
1075
|
+
New code
|
|
1076
|
+
ProductPage in pages/ProductPage.ts
|
|
1077
|
+
Checks
|
|
1078
|
+
after step 5: expect(product.price).toHaveText(/^\$\d+\.\d{2}$/)
|
|
1079
|
+
after step 6: expect(product.cartQuantity).toHaveText("1")
|
|
1080
|
+
</code></pre>
|
|
1081
|
+
<p>It asks at most three questions where the repository allows two good answers (a new file, or the existing spec?), then
|
|
1082
|
+
<strong>Write it</strong>, <strong>Change the plan</strong> (say what to change, in your words), or <strong>Cancel</strong>. The coding agent replays the
|
|
1083
|
+
recording in Playwright, writes the test to the plan you agreed in the repository's framework, and asserts each check at
|
|
1084
|
+
the step where you added it. In the shell, "change the plan" comes back to ForgeQE's agent, which asks you what to
|
|
1085
|
+
change. <code>--no-plan</code> (or <code>--yes</code>) skips the plan. Later: <code>forgeqe code <run folder></code>.</p>
|
|
1086
|
+
<p>The recorder runs in an isolated world in each page, so the page's own scripts can neither see it nor add steps, and it
|
|
1087
|
+
records only real input: clicks and keys a script fakes are ignored. The panel's own clicks and keys never reach the page
|
|
1088
|
+
and are never recorded, and it stays above the page's dialogs. Recording uses local Chrome or Chromium.</p>
|
|
1089
|
+
</div>
|
|
1090
|
+
</section>
|
|
1091
|
+
</div>
|
|
1092
|
+
</article>
|
|
1000
1093
|
</section>
|
|
1001
1094
|
<section class="chapter" id="area-test-cases" aria-labelledby="area-test-cases-h">
|
|
1002
1095
|
<header class="chead"><span class="mono">05</span><h2 id="area-test-cases-h">Test cases</h2><p>Run, check, write, and explore manual test cases</p></header>
|
|
@@ -2653,11 +2746,26 @@ Run the tests added since 2026-09-01
|
|
|
2653
2746
|
<p>Through <code>forgeqe mcp</code>, ForgeQE cannot ask you, so <code>run_tests</code> returns the list and runs only when called again with <code>confirmed: true</code>; the MCP client should ask you first.</p>
|
|
2654
2747
|
</div>
|
|
2655
2748
|
</section>
|
|
2749
|
+
<section class="doc" id="doc-how_to_use-reports-for-tests-run-outside-forgeqe">
|
|
2750
|
+
<h4>Reports for tests run outside ForgeQE</h4>
|
|
2751
|
+
<div class="prose"><p>ForgeQE's HTML report is not only for runs it makes. <code>run_tests</code> and <code>pr-tests --run</code> write one, and so can any run in a terminal, an IDE's test runner, or CI:</p>
|
|
2752
|
+
<pre><code class="language-bash">forgeqe report test-results/junit.xml # from JUnit XML or TRX: any framework, after the run
|
|
2753
|
+
forgeqe report setup # a report after every Playwright or pytest run, automatically
|
|
2754
|
+
</code></pre>
|
|
2755
|
+
<p><code>forgeqe report</code> reads JUnit XML or TRX files, or the folder they are in (Playwright <code>--reporter=junit</code>, pytest <code>--junitxml=results.xml</code>, <code>dotnet test --logger trx</code>), and exits 1 when a test failed. Each report is saved in <code><out_dir>/results/<time>/</code>, so its Trends and Flaky tests pages cover the earlier runs, whoever ran them. JUnit carries one line per failure; a skipped test is counted, not shown.</p>
|
|
2756
|
+
<p><code>forgeqe report setup</code> sets up the framework the repository uses:</p>
|
|
2757
|
+
<ul>
|
|
2758
|
+
<li><strong>Playwright</strong>: it copies a reporter to <code>.forgeqe/playwright-reporter.mjs</code> (commit it) and prints the line to add to <code>playwright.config</code>: <code>reporter: [["list"], ["./.forgeqe/playwright-reporter.mjs"]]</code>. Every run then ends with a report, from <code>npx playwright test</code>, the VS Code Playwright extension, or CI. With <code>screenshot: "only-on-failure"</code> and <code>trace: "retain-on-failure"</code>, each failure shows its screenshot, the full error and stack, and the trace.</li>
|
|
2759
|
+
<li><strong>pytest</strong>: install <code>forgeqe-cli</code> in the environment that runs the tests (<code>pip install forgeqe-cli</code>). Its plugin loads by itself, from the terminal, VS Code's test explorer, or CI, in a folder with <code>forgeqe.yaml</code> or <code>.forgeqe/</code> (elsewhere with <code>pytest --forgeqe-report</code>). A test that passes when pytest-rerunfailures runs it again is reported as flaky.</li>
|
|
2760
|
+
</ul>
|
|
2761
|
+
<p><code>FORGEQE_REPORT=0</code> turns the reporters off for a run (ForgeQE's own <code>run_tests</code> does this, as it writes its own report). The reporters call <code>forgeqe</code> on PATH, or <code>FORGEQE_BIN</code>; without it they keep the results file and print the command that builds the report. Screenshots and traces are copied into the report only from the working folder, never from a hidden folder or a file named like a key.</p>
|
|
2762
|
+
</div>
|
|
2763
|
+
</section>
|
|
2656
2764
|
</div>
|
|
2657
2765
|
</article>
|
|
2658
2766
|
<article class="spec feat" id="cap-regression-packs">
|
|
2659
2767
|
<div class="hd"><span class="mono">Automated tests</span><h3>Regression packs for a release</h3></div>
|
|
2660
|
-
<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,
|
|
2768
|
+
<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, compares each run with the last, and triages each failure: flaky, environment, broken test code, heal, or an AI browser re-run to tell a test the app outgrew from an app bug.</p>
|
|
2661
2769
|
<div class="facts">
|
|
2662
2770
|
<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>
|
|
2663
2771
|
<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>
|
|
@@ -2697,7 +2805,22 @@ Build a pack for Jira fix version 2.4 and the commits since release/2.3 in ../sh
|
|
|
2697
2805
|
<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>
|
|
2698
2806
|
<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>
|
|
2699
2807
|
</ul>
|
|
2700
|
-
<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
|
|
2808
|
+
<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.</p>
|
|
2809
|
+
<p><strong>Triage: heal, re-run, or file a bug?</strong> After each run ForgeQE sorts every failed or flaky test into one route, from evidence that costs nothing first (the retry in the same run, the failure message, the test's history), so the AI is spent only where it decides something:</p>
|
|
2810
|
+
<div class="tbl"><table>
|
|
2811
|
+
<thead>
|
|
2812
|
+
<tr><th>Route</th><th>When</th><th>What happens</th></tr>
|
|
2813
|
+
</thead>
|
|
2814
|
+
<tbody>
|
|
2815
|
+
<tr><td>flaky</td><td>it passed when run again in the same run, or its history flip-flops</td><td>not healed: quarantine it, or fix the cause it flakes on</td></tr>
|
|
2816
|
+
<tr><td>environment</td><td>network errors, 5xx, a crashed browser, sign-in failing</td><td>run again once the environment works; no heal and no bug</td></tr>
|
|
2817
|
+
<tr><td>test code broken</td><td>a missing module, a compile error, a missing fixture</td><td>fix the test's code; healing against the page cannot fix it</td></tr>
|
|
2818
|
+
<tr><td>heal</td><td>an element the test uses is no longer found (high confidence when it passed its last runs)</td><td>the healing agent re-runs the flow in the browser and updates the steps and locators</td></tr>
|
|
2819
|
+
<tr><td>AI re-run decides</td><td>an expected value does not hold, a timeout, or something covers the element</td><td>the healing agent re-runs the flow in the browser: heals the test when the flow works, reports an app bug when it fails at the same step</td></tr>
|
|
2820
|
+
</tbody>
|
|
2821
|
+
</table></div>
|
|
2822
|
+
<p>Failures that name the same element or the same message are shown as <strong>one cause</strong> (a page object or a shared step changed), so one fix can heal them together. When most failures are the environment, ForgeQE says to run the pack again before healing anything, and <code>heal_tests</code> refuses until you name the tests.</p>
|
|
2823
|
+
<p>The triage is on the run's card and in <code>triage.md</code> and <code>triage.json</code> in the run's folder. To heal only the tests it sent to healing, ask "heal the failures of pack release-1.5", or run <code>forgeqe pack heal release-1.5</code> (<code>--tests</code> for some of them): ForgeQE builds a command for just those tests and the healing agent re-runs each flow in the browser before it changes anything. <code>forgeqe pack triage <name></code> shows the last run's triage again. Packs are saved in <code>.forgeqe/packs/<name>.json</code> and <code><name>.md</code>; run output and results in <code>.forgeqe/reports/packs/<name>/<time>/</code>.</p>
|
|
2701
2824
|
</div>
|
|
2702
2825
|
</section>
|
|
2703
2826
|
</div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@capagents/forgeqe",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.2",
|
|
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": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"type": "module",
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
},
|
|
11
11
|
"files": [
|
|
12
12
|
"bin/",
|
|
13
|
+
"reporters/",
|
|
13
14
|
"dist/",
|
|
14
15
|
"py/forgeqe_engine/*.py",
|
|
15
16
|
"py/forgeqe_desktop/*.py",
|
package/prompts/README.md
CHANGED
|
@@ -9,6 +9,7 @@ project; leave it as it is to keep getting ForgeQE's improvements when you updat
|
|
|
9
9
|
| test-agent.md | Scribe, the coding agent: turns a verified browser run into a test in your repository |
|
|
10
10
|
| test-rules.md | Rules for any test the agents write or change (included in test-agent, test-reviewer, heal) |
|
|
11
11
|
| test-reviewer.md | The reviewer that checks a passing test tests the right thing the right way |
|
|
12
|
+
| test-plan.md | Plans how a test you recorded by hand (`forgeqe record`) fits your repository, for you to agree before Scribe writes it |
|
|
12
13
|
| heal.md | Heals tests the application has outgrown, and tells them apart from real bugs |
|
|
13
14
|
| find-tests.md | Finds the automated tests that match a request |
|
|
14
15
|
| pr-review.md | Reviews pull requests |
|
package/prompts/brain.md
CHANGED
|
@@ -255,6 +255,8 @@ When the user asks for a regression pack, or which tests to run for a release, a
|
|
|
255
255
|
- 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.
|
|
256
256
|
- 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.
|
|
257
257
|
- After a run, you MUST name the failing tests with their messages, and the tests that fail now but passed in the last run.
|
|
258
|
+
- You MUST give the triage of the run: each failing test's route (flaky, environment, test code broken, heal, or an AI re-run decides) and why. You MUST offer only what the triage says. For heal and AI re-run tests, offer heal_tests with pack. When the environment was down, offer to run the pack again first. You MUST NOT offer heal_tests for flaky, environment, or test code failures.
|
|
259
|
+
- You MUST file a bug only for an app_bug that heal_tests reports, not for a failure the triage routed.
|
|
258
260
|
|
|
259
261
|
### Writing tests
|
|
260
262
|
When the user asks, or when the setup below says that passed runs become tests:
|
|
@@ -281,7 +283,7 @@ When the user asks, or when the setup below says that passed runs become tests:
|
|
|
281
283
|
|
|
282
284
|
### Repairing broken tests
|
|
283
285
|
When the user says that tests fail, broke after a release, or need healing:
|
|
284
|
-
- You MUST call heal_tests.
|
|
286
|
+
- You MUST call heal_tests. After a regression pack run, give pack, and tests when the user named some. Otherwise give the command when the user gave one or you know it. Otherwise use the test command below, or let the healing agent find it.
|
|
285
287
|
- You MUST report the verdict for each test: healed and confirmed, an application bug, or not healed. For an application bug, give what the browser run found.
|
|
286
288
|
- You MUST also report the files that changed, and heal.md.
|
|
287
289
|
- You MUST NOT delete, skip, or make tests weaker yourself to make a run pass.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: test-plan
|
|
3
|
+
description: "Plans how a test recorded by hand fits the repository, for the tester to agree before Scribe writes it."
|
|
4
|
+
used_by: "forgeqe record, write_test for a recording"
|
|
5
|
+
kind: structured
|
|
6
|
+
---
|
|
7
|
+
{{> conventions}}
|
|
8
|
+
|
|
9
|
+
## Role
|
|
10
|
+
You plan how a test that a tester recorded by hand fits a test repository. You do not write or change files.
|
|
11
|
+
|
|
12
|
+
## Objective
|
|
13
|
+
Tell the tester where the test goes and which code it uses. The tester agrees before Scribe writes it.
|
|
14
|
+
|
|
15
|
+
## Inputs
|
|
16
|
+
- The handoff: the recorded steps in order, their locators, the tester's checks, variables, test data, and notes.
|
|
17
|
+
- The repository's test setup: the framework, the page objects, the fixtures, and the test data.
|
|
18
|
+
- The tools code_outline, code_element, and code_references. They show what the page objects already locate and do.
|
|
19
|
+
|
|
20
|
+
## Rules
|
|
21
|
+
Reading:
|
|
22
|
+
- You MUST find the page-object members and methods that already locate each element of the steps. Use the code tools.
|
|
23
|
+
- You SHOULD read only the files that the plan needs.
|
|
24
|
+
|
|
25
|
+
Where the test goes:
|
|
26
|
+
- You MUST name the test file and the test name. Follow the names and folders of the tests that are there.
|
|
27
|
+
- When an existing test already does the first steps, you MAY propose to extend it. Name that test.
|
|
28
|
+
|
|
29
|
+
Steps:
|
|
30
|
+
- For each step, you MUST say which existing member or method does it, or which new one the test adds, and where.
|
|
31
|
+
- A new member MUST go in the page object of the page where the step is.
|
|
32
|
+
- You MUST NOT change what an existing member does for the other tests.
|
|
33
|
+
|
|
34
|
+
Checks and data:
|
|
35
|
+
- For each CHECK line, you MUST say the assertion and the step after which it comes.
|
|
36
|
+
- A check that the tester gave only in words MUST stay in the plan. Say what on the page will show it.
|
|
37
|
+
- For each value marked as test data, you MUST name its source: a fixture, a data file, or a parameter.
|
|
38
|
+
- You MUST keep the secrets as environment variables.
|
|
39
|
+
|
|
40
|
+
Questions:
|
|
41
|
+
- You MUST ask a question only when the repository allows two good answers and the tester's choice changes the code.
|
|
42
|
+
- You MUST NOT ask more than 3 questions. Give each question 2 to 4 short options, the recommended option first.
|
|
43
|
+
- You MUST NOT ask what the handoff or the repository already answers.
|
|
44
|
+
|
|
45
|
+
## Output
|
|
46
|
+
The structured result: test_file, test_name, mode, steps, new members, checks, test data, questions, and a short summary.
|
|
47
|
+
|
|
48
|
+
## Examples
|
|
49
|
+
### Example 1
|
|
50
|
+
Handoff: open the shop, search "pliers", open the first result, add it to the cart, CHECK the cart badge shows "1". The repository has pages/HomePage.ts with searchFor() and no product page object.
|
|
51
|
+
- test_file tests/add-to-cart.spec.ts, test_name "adds pliers to the cart", mode new.
|
|
52
|
+
- Steps: search with HomePage.searchFor(); open the product with a new HomePage.product(name); add to the cart with a new ProductPage.addToCart() in pages/ProductPage.ts.
|
|
53
|
+
- Checks: after the add, expect ProductPage.cartQuantity to have the text "1".
|
|
54
|
+
- Questions: none.
|
|
55
|
+
|
|
56
|
+
### Example 2
|
|
57
|
+
Handoff: sign in, type the coupon "SAVE10" (test data: coupon), CHECK the total shows text like "$40.00". The repository has tests/data/coupons.json and a fixture that signs in.
|
|
58
|
+
- Steps: sign in with the signedIn fixture; type the coupon with a new CartPage.applyCoupon(code).
|
|
59
|
+
- Test data: read the coupon from tests/data/coupons.json, with SAVE10 as the first entry.
|
|
60
|
+
- Checks: after the coupon, expect CartPage.total to match /^\$\d+\.\d{2}$/.
|
|
61
|
+
- Questions: "Add the coupon test to tests/cart.spec.ts, or a new file?" Options: "tests/cart.spec.ts", "A new file".
|
package/py/forgeqe_engine/cli.py
CHANGED
|
@@ -9,6 +9,7 @@ Engine process protocol, used by the Bun CLI:
|
|
|
9
9
|
|
|
10
10
|
python -m forgeqe_engine run
|
|
11
11
|
python -m forgeqe_engine login (stdin: {url, logins}, then "save")
|
|
12
|
+
python -m forgeqe_engine record (stdin: {start_url, out_dir, ...}, then commands; "stop" or EOF ends it)
|
|
12
13
|
python -m forgeqe_engine doctor
|
|
13
14
|
python -m forgeqe_engine token (stdin: an llms.json profile with auth: entra; prints {token, expires_on})
|
|
14
15
|
python -m forgeqe_engine probe (stdin: an llms.json profile; prints {ok, ms, structured, vision, error?})
|
|
@@ -155,6 +156,47 @@ def _login(out: TextIO) -> int:
|
|
|
155
156
|
return asyncio.run(main())
|
|
156
157
|
|
|
157
158
|
|
|
159
|
+
def _record(out: TextIO) -> int:
|
|
160
|
+
"""stdin line 1: the recording request (recordsession.py); later lines are commands ("stop", or JSON {"cmd": ...}); EOF stops."""
|
|
161
|
+
from .recordsession import record_web
|
|
162
|
+
|
|
163
|
+
line = sys.stdin.readline()
|
|
164
|
+
payload = json.loads(line) if line.strip() else {}
|
|
165
|
+
lock = threading.Lock()
|
|
166
|
+
|
|
167
|
+
def emit(event: Dict[str, Any]) -> None:
|
|
168
|
+
with lock:
|
|
169
|
+
out.write(json.dumps(event, ensure_ascii=False, default=str) + "\n")
|
|
170
|
+
out.flush()
|
|
171
|
+
|
|
172
|
+
async def main() -> int:
|
|
173
|
+
loop = asyncio.get_running_loop()
|
|
174
|
+
commands: asyncio.Queue = asyncio.Queue()
|
|
175
|
+
|
|
176
|
+
def watch_stdin() -> None:
|
|
177
|
+
for raw in sys.stdin:
|
|
178
|
+
text = raw.strip()
|
|
179
|
+
if not text:
|
|
180
|
+
continue
|
|
181
|
+
try:
|
|
182
|
+
cmd = json.loads(text) if text.startswith("{") else {"cmd": text.lower()}
|
|
183
|
+
except ValueError:
|
|
184
|
+
continue
|
|
185
|
+
if isinstance(cmd, dict):
|
|
186
|
+
loop.call_soon_threadsafe(commands.put_nowait, cmd)
|
|
187
|
+
loop.call_soon_threadsafe(commands.put_nowait, {"cmd": "stop"})
|
|
188
|
+
|
|
189
|
+
threading.Thread(target=watch_stdin, daemon=True).start()
|
|
190
|
+
try:
|
|
191
|
+
result = await record_web(payload, emit, commands)
|
|
192
|
+
except Exception as e:
|
|
193
|
+
result = {"success": False, "error": f"{type(e).__name__}: {e}"}
|
|
194
|
+
emit({"type": "done", "result": result})
|
|
195
|
+
return 0 if result.get("success") else 1
|
|
196
|
+
|
|
197
|
+
return asyncio.run(main())
|
|
198
|
+
|
|
199
|
+
|
|
158
200
|
def _token(out: TextIO) -> int:
|
|
159
201
|
"""stdin: an llms.json profile with auth: entra. Prints {token, expires_on} for its scope, or {error}."""
|
|
160
202
|
from .llm import entra_credential, entra_scope
|
|
@@ -204,6 +246,8 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
204
246
|
return _run(out)
|
|
205
247
|
if cmd == "login":
|
|
206
248
|
return _login(out)
|
|
249
|
+
if cmd == "record":
|
|
250
|
+
return _record(out)
|
|
207
251
|
if cmd == "token":
|
|
208
252
|
return _token(out)
|
|
209
253
|
if cmd == "probe":
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Data that changes from one run to the next, in what a recording keeps: prices, dates, times, ids and order numbers,
|
|
3
|
+
long numbers and counts in a sentence. A test that spells this run's value out fails on the next run, so the recorder
|
|
4
|
+
keeps the parts that hold and makes the rest a pattern, a variable, or a value made fresh on each run.
|
|
5
|
+
|
|
6
|
+
- ``parts(text)``: the spans of ``text`` that change, with their kind.
|
|
7
|
+
- ``pattern_of(text)``: a regular expression (JavaScript and Python alike) that the text matches on any run: the stable
|
|
8
|
+
parts as they are, each changing part by its kind.
|
|
9
|
+
- ``stable_part(text)``: the longest part that holds, for a locator that finds the element by it.
|
|
10
|
+
- ``relative_date(text, today)``: a typed date as days from today, for ``{{$date:+N:FORMAT}}``.
|
|
11
|
+
|
|
12
|
+
A small number on its own ("1" in a cart badge, "3" items) is not changing data: it is usually what the check is about.
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import re
|
|
17
|
+
from datetime import date, datetime, timedelta
|
|
18
|
+
from typing import List, Optional, Tuple
|
|
19
|
+
|
|
20
|
+
_MONTHS = "jan|feb|mar|apr|may|jun|jul|aug|sep|sept|oct|nov|dec|january|february|march|april|june|july|august|september|october|november|december"
|
|
21
|
+
|
|
22
|
+
# (kind, what finds it, the pattern a test asserts instead). Order matters: the first kind to claim a span keeps it.
|
|
23
|
+
_KINDS: List[Tuple[str, "re.Pattern[str]", str]] = [
|
|
24
|
+
("uuid", re.compile(r"\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b", re.I), r"[0-9a-fA-F-]{36}"),
|
|
25
|
+
("date", re.compile(r"\b\d{4}-\d{2}-\d{2}(?:[T ]\d{2}:\d{2}(?::\d{2})?(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})?)?\b"), r"\d{4}-\d{2}-\d{2}(?:[T ][\d:.]+(?:Z|[+-][\d:]+)?)?"),
|
|
26
|
+
("date", re.compile(r"\b\d{1,2}[/.]\d{1,2}[/.]\d{2,4}\b"), r"\d{1,2}[/.]\d{1,2}[/.]\d{2,4}"),
|
|
27
|
+
("date", re.compile(rf"\b(?:\d{{1,2}}\s+(?:{_MONTHS})\.?,?\s+\d{{4}}|(?:{_MONTHS})\.?\s+\d{{1,2}},?\s+\d{{4}})\b", re.I), r"[\w.]+\s+[\w.]+,?\s+\d{4}"),
|
|
28
|
+
("time", re.compile(r"\b\d{1,2}:\d{2}(?::\d{2})?\s?(?:[ap]\.?m\.?)?(?![\d:])", re.I), r"\d{1,2}:\d{2}(?::\d{2})?\s?(?:[aApP]\.?[mM]\.?)?"),
|
|
29
|
+
("money", re.compile(r"(?:[$€£¥₹]\s?|\b(?:USD|EUR|GBP|INR|JPY|AUD|CAD)\s?)\d[\d,]*(?:\.\d{1,2})?\b|\b\d[\d,]*(?:\.\d{1,2})?\s?(?:USD|EUR|GBP|INR|JPY|AUD|CAD|€|£)", re.I), r"(?:[$€£¥₹]\s?|[A-Z]{3}\s?)?\d[\d,]*(?:\.\d{1,2})?(?:\s?(?:[A-Z]{3}|€|£))?"),
|
|
30
|
+
("id", re.compile(r"(?<![\w-])(?=[A-Z0-9-]*\d)(?=[A-Z0-9-]*[A-Z])[A-Z0-9]{1,6}-[A-Z0-9]{2,}(?:-[A-Z0-9]+)*(?![\w-])"), r"[A-Z0-9]+(?:-[A-Z0-9]+)+"),
|
|
31
|
+
("id", re.compile(r"#\d{3,}\b"), r"#\d+"),
|
|
32
|
+
("id", re.compile(r"\b(?=[A-Za-z0-9]*\d)(?=[A-Za-z0-9]*[A-Za-z])[A-Za-z0-9]{10,}\b"), r"[A-Za-z0-9]{10,}"),
|
|
33
|
+
("number", re.compile(r"\b\d{1,3}(?:,\d{3})+(?:\.\d+)?\b|\b\d{4,}(?:\.\d+)?\b|\b\d+\.\d+\b"), r"\d[\d,]*(?:\.\d+)?"),
|
|
34
|
+
]
|
|
35
|
+
# A count in a sentence: "2,463 properties found", "12 results", "3 items in your cart".
|
|
36
|
+
_COUNT = re.compile(r"\b\d[\d,]*(?=\s+(?:results?|items?|products?|properties|matches|hits|records|rows|orders|reviews|comments|people|days|hours|minutes|seconds|left|available|found|in stock)\b)", re.I)
|
|
37
|
+
_COUNT_PATTERN = r"\d[\d,]*"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _spans(text: str) -> List[Tuple[int, int, str, str]]:
|
|
41
|
+
"""(start, end, kind, the pattern of the matcher that found it), in order."""
|
|
42
|
+
t = text or ""
|
|
43
|
+
found: List[Tuple[int, int, str, str]] = []
|
|
44
|
+
|
|
45
|
+
def free(a: int, b: int) -> bool:
|
|
46
|
+
return all(b <= s or a >= e for s, e, _, _ in found)
|
|
47
|
+
|
|
48
|
+
for kind, rx, pat in _KINDS:
|
|
49
|
+
for m in rx.finditer(t):
|
|
50
|
+
if free(m.start(), m.end()):
|
|
51
|
+
found.append((m.start(), m.end(), kind, pat))
|
|
52
|
+
for m in _COUNT.finditer(t):
|
|
53
|
+
if free(m.start(), m.end()):
|
|
54
|
+
found.append((m.start(), m.end(), "count", _COUNT_PATTERN))
|
|
55
|
+
return sorted(found)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def parts(text: str) -> List[Tuple[int, int, str]]:
|
|
59
|
+
"""The spans of `text` that change from run to run, in order, as (start, end, kind)."""
|
|
60
|
+
return [(s, e, k) for s, e, k, _ in _spans(text)]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def is_dynamic(text: str) -> bool:
|
|
64
|
+
return bool(parts(text))
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _token_pattern(kind: str, value: str, matched: str) -> str:
|
|
68
|
+
if kind == "date" and re.fullmatch(r"\d{4}-\d{2}-\d{2}", value):
|
|
69
|
+
return r"\d{4}-\d{2}-\d{2}"
|
|
70
|
+
if kind == "money":
|
|
71
|
+
# Keep the currency the run showed; the amount changes.
|
|
72
|
+
cur = re.match(r"^([$€£¥₹]|[A-Za-z]{3})\s?", value)
|
|
73
|
+
amount = r"\d[\d,]*(?:\.\d{1,2})?"
|
|
74
|
+
if cur:
|
|
75
|
+
return re.escape(cur.group(1)) + r"\s?" + amount
|
|
76
|
+
tail = re.search(r"\s?([A-Za-z]{3}|€|£)$", value)
|
|
77
|
+
return amount + (r"\s?" + re.escape(tail.group(1)) if tail else "")
|
|
78
|
+
return matched
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def pattern_of(text: str, anchored: bool = False) -> Optional[str]:
|
|
82
|
+
"""A regular expression the text matches on any run, or None when nothing in it changes."""
|
|
83
|
+
t = " ".join((text or "").split())
|
|
84
|
+
spans = _spans(t)
|
|
85
|
+
if not spans:
|
|
86
|
+
return None
|
|
87
|
+
out, at = [], 0
|
|
88
|
+
for s, e, kind, matched in spans:
|
|
89
|
+
out.append(re.escape(t[at:s]).replace("\\ ", r"\s+"))
|
|
90
|
+
out.append(_token_pattern(kind, t[s:e], matched))
|
|
91
|
+
at = e
|
|
92
|
+
out.append(re.escape(t[at:]).replace("\\ ", r"\s+"))
|
|
93
|
+
body = "".join(out)
|
|
94
|
+
return f"^{body}$" if anchored else body
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def stable_part(text: str, min_len: int = 3) -> str:
|
|
98
|
+
"""The longest stretch of `text` with nothing that changes in it, trimmed; "" when none is long enough."""
|
|
99
|
+
t = " ".join((text or "").split())
|
|
100
|
+
cuts, at = [], 0
|
|
101
|
+
for s, e, _ in parts(t):
|
|
102
|
+
cuts.append(t[at:s])
|
|
103
|
+
at = e
|
|
104
|
+
cuts.append(t[at:])
|
|
105
|
+
best = max((c.strip(" ,.:;|/-–·") for c in cuts), key=len, default="")
|
|
106
|
+
return best if len(best) >= min_len and re.search(r"[A-Za-z]", best) else ""
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def values(text: str) -> List[str]:
|
|
110
|
+
"""The changing values in `text`, as shown ("$14.15", "A-1042")."""
|
|
111
|
+
t = " ".join((text or "").split())
|
|
112
|
+
return [t[s:e] for s, e, _ in parts(t)]
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
_DATE_FORMATS = (("%Y-%m-%d", r"\d{4}-\d{2}-\d{2}"), ("%d/%m/%Y", r"\d{2}/\d{2}/\d{4}"), ("%m/%d/%Y", r"\d{2}/\d{2}/\d{4}"), ("%d.%m.%Y", r"\d{2}\.\d{2}\.\d{4}"))
|
|
116
|
+
MAX_DATE_DAYS = 3660
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def relative_date(text: str, today: Optional[date] = None) -> Optional[str]:
|
|
120
|
+
"""
|
|
121
|
+
A typed date, as ``{{$date:+N:FORMAT}}`` (N days from today) when it is the whole value and reads one way only;
|
|
122
|
+
None otherwise. 03/04/2026 could be either day-first or month-first: it is left as typed.
|
|
123
|
+
"""
|
|
124
|
+
t = (text or "").strip()
|
|
125
|
+
today = today or date.today()
|
|
126
|
+
hits = []
|
|
127
|
+
for fmt, rx in _DATE_FORMATS:
|
|
128
|
+
if not re.fullmatch(rx, t):
|
|
129
|
+
continue
|
|
130
|
+
try:
|
|
131
|
+
hits.append((fmt, datetime.strptime(t, fmt).date()))
|
|
132
|
+
except ValueError:
|
|
133
|
+
continue
|
|
134
|
+
if len({d for _, d in hits}) != 1:
|
|
135
|
+
return None
|
|
136
|
+
fmt, d = hits[0]
|
|
137
|
+
days = (d - today).days
|
|
138
|
+
if abs(days) > MAX_DATE_DAYS:
|
|
139
|
+
return None
|
|
140
|
+
return "{{$date:%s%d:%s}}" % ("+" if days >= 0 else "-", abs(days), fmt)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def date_from(template: str, today: Optional[date] = None) -> str:
|
|
144
|
+
"""The date a ``{{$date:+N:FORMAT}}`` stands for today (for tests of this module and the replay's own)."""
|
|
145
|
+
m = re.fullmatch(r"\{\{\$date:([+-])(\d+):([^}]+)\}\}", template)
|
|
146
|
+
if not m:
|
|
147
|
+
return template
|
|
148
|
+
d = (today or date.today()) + timedelta(days=int(m.group(2)) * (1 if m.group(1) == "+" else -1))
|
|
149
|
+
return d.strftime(m.group(3))
|