@capagents/forgeqe 1.2.1 → 1.3.1

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 (80) hide show
  1. package/HOW_TO_USE.md +40 -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 +54 -7
  6. package/evals/prompts/brain/route-api-scenarios.yaml +9 -0
  7. package/evals/prompts/brain/route-e2e.yaml +10 -0
  8. package/evals/prompts/brain/route-flaky.yaml +9 -0
  9. package/evals/prompts/brain/route-heal.yaml +9 -0
  10. package/evals/prompts/brain/route-regression-pack.yaml +9 -0
  11. package/evals/prompts/brain/route-review-update.yaml +10 -0
  12. package/evals/prompts/brain/route-run-cases.yaml +9 -0
  13. package/evals/prompts/brain/route-story-cases.yaml +12 -0
  14. package/forgeqe.example.yaml +15 -0
  15. package/package.json +1 -1
  16. package/prompts/README.md +30 -0
  17. package/prompts/brain.md +34 -49
  18. package/prompts/browser-checks.md +2 -0
  19. package/py/forgeqe_engine/checks.py +8 -2
  20. package/py/forgeqe_engine/llm.py +33 -5
  21. package/py/forgeqe_engine/memory.py +59 -2
  22. package/py/forgeqe_engine/observe.py +123 -0
  23. package/py/forgeqe_engine/replay.py +55 -2
  24. package/py/forgeqe_engine/service.py +155 -3
  25. package/src/agents.ts +52 -0
  26. package/src/brain/index.ts +8 -2
  27. package/src/capabilities/registry.ts +48 -6
  28. package/src/cases/ado.ts +1 -1
  29. package/src/cases/journey.ts +61 -0
  30. package/src/cases/readiness.ts +35 -6
  31. package/src/cases/save.ts +241 -48
  32. package/src/cases/types.ts +11 -0
  33. package/src/cli/help.ts +9 -0
  34. package/src/cli/index.ts +71 -1
  35. package/src/code/coverage.ts +22 -3
  36. package/src/code/decisions.ts +73 -0
  37. package/src/code/find.ts +3 -1
  38. package/src/code/graph/graph.ts +33 -2
  39. package/src/code/loop.ts +17 -1
  40. package/src/code/notes.ts +48 -1
  41. package/src/code/prompt.ts +31 -9
  42. package/src/code/protect.ts +103 -0
  43. package/src/code/review.ts +2 -0
  44. package/src/code/runtime.ts +35 -3
  45. package/src/code/shortlist.ts +309 -0
  46. package/src/code/testrun.ts +3 -1
  47. package/src/code/traceability.ts +2 -0
  48. package/src/commands.ts +6 -1
  49. package/src/config.ts +46 -0
  50. package/src/context.ts +75 -0
  51. package/src/engine/bridge.ts +47 -5
  52. package/src/mcp/captools.ts +19 -0
  53. package/src/mcp/casetools.ts +107 -11
  54. package/src/mcp/commands.ts +74 -20
  55. package/src/mcp/packtools.ts +187 -0
  56. package/src/mcp/routing.ts +145 -0
  57. package/src/mcp/server.ts +7 -3
  58. package/src/mcp/tools.ts +241 -29
  59. package/src/pack/collect.ts +251 -0
  60. package/src/pack/index.ts +134 -0
  61. package/src/pack/init.ts +45 -0
  62. package/src/pack/pack.ts +327 -0
  63. package/src/pack/pipeline.ts +373 -0
  64. package/src/pack/run.ts +155 -0
  65. package/src/registry/db.ts +249 -0
  66. package/src/registry/index.ts +72 -0
  67. package/src/registry/resolve.ts +57 -0
  68. package/src/registry/tier0.ts +81 -0
  69. package/src/report/data.ts +7 -0
  70. package/src/report/html.ts +14 -0
  71. package/src/report/template.html +52 -48
  72. package/src/report/usage.ts +113 -0
  73. package/src/run.ts +22 -2
  74. package/src/suite/run.ts +12 -5
  75. package/src/summary.ts +1 -0
  76. package/src/tokenomics/budget.ts +172 -0
  77. package/src/tokenomics/ledger.ts +287 -0
  78. package/src/tui/App.tsx +89 -2
  79. package/src/tui/inspector.tsx +40 -35
  80. package/src/types.ts +9 -2
@@ -0,0 +1,10 @@
1
+ id: route-e2e
2
+ title: An end-to-end case is built from the story's existing cases, not written again
3
+ input: Build an end-to-end test case from the existing cases of Azure DevOps story 2211. Show it to me; don't save or run it.
4
+ needs: [azure_devops]
5
+ allow: [capabilities, find_test_cases, story_test_cases, e2e_journey, query_api, tool_help]
6
+ checks:
7
+ - id: uses-journey
8
+ tools_any: ["forgeqe_e2e_journey"]
9
+ - id: saves-nothing
10
+ tools_none: ["forgeqe_save_test_cases", "forgeqe_run_test_cases", "forgeqe_browser_run"]
@@ -0,0 +1,9 @@
1
+ id: route-flaky
2
+ title: Flaky tests come from the test history
3
+ input: Which of our tests are flaky, and why?
4
+ needs: []
5
+ checks:
6
+ - id: reads-history
7
+ tools_any: ["forgeqe_flaky_tests"]
8
+ - id: no-browser
9
+ tools_none: ["forgeqe_browser_run"]
@@ -0,0 +1,9 @@
1
+ id: route-heal
2
+ title: Tests that broke after a release go to heal_tests
3
+ input: Our automated UI tests in this repository started failing after yesterday's release. Repair them.
4
+ needs: []
5
+ checks:
6
+ - id: heals
7
+ tools_any: ["forgeqe_heal_tests"]
8
+ - id: no-hand-edits-first
9
+ tools_before: [["forgeqe_heal_tests"], ["edit", "write"]]
@@ -0,0 +1,9 @@
1
+ id: route-regression-pack
2
+ title: Which tests to run for a sprint's release is a regression pack
3
+ input: Which automated tests should we run before releasing the current sprint?
4
+ needs: [azure_devops]
5
+ checks:
6
+ - id: builds-pack
7
+ tools_any: ["forgeqe_build_regression_pack"]
8
+ - id: no-browser
9
+ tools_none: ["forgeqe_browser_run"]
@@ -0,0 +1,10 @@
1
+ id: route-review-update
2
+ title: A change after review updates the same case in place, never saves a new one
3
+ input: I reviewed test case 2279 of story 2271. Step 2 must also check that the password field shows dots, not the typed characters. Update the case.
4
+ needs: [azure_devops]
5
+ allow: [capabilities, find_test_cases, story_test_cases, query_api, tool_help]
6
+ checks:
7
+ - id: updates-in-place
8
+ tools_any: ["forgeqe_update_test_case"]
9
+ - id: no-new-case
10
+ tools_none: ["forgeqe_save_test_cases", "forgeqe_browser_run"]
@@ -0,0 +1,9 @@
1
+ id: route-run-cases
2
+ title: Running test cases goes through run_test_cases, not one browser run per case
3
+ input: Run Azure DevOps test cases 2272 and 2273.
4
+ needs: [azure_devops]
5
+ checks:
6
+ - id: runs-cases
7
+ tools_any: ["forgeqe_run_test_cases"]
8
+ - id: not-browser-per-case
9
+ tools_none: ["forgeqe_browser_run"]
@@ -0,0 +1,12 @@
1
+ id: route-story-cases
2
+ title: Asking for a story's test cases reads the linked cases before writing or saving any
3
+ input: Write test cases for Azure DevOps story 2211.
4
+ needs: [azure_devops]
5
+ allow: [capabilities, find_test_cases, story_test_cases, query_api, read_confluence, tool_help]
6
+ checks:
7
+ - id: reads-linked-cases
8
+ tools_any: ["forgeqe_story_test_cases"]
9
+ - id: reads-before-saving
10
+ tools_before: [["forgeqe_story_test_cases"], ["forgeqe_save_test_cases"]]
11
+ - id: no-browser
12
+ tools_none: ["forgeqe_browser_run", "bash", "webfetch"]
@@ -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.1",
3
+ "version": "1.3.1",
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/README.md CHANGED
@@ -60,3 +60,33 @@ How it works:
60
60
  replaces ForgeQE's). Cases can run for brain, refine, and explore-charters.
61
61
  - Changes to these prompts change how the agents behave: run `forgeqe prompts eval` before and after a change, and
62
62
  commit it only when the pass rates hold.
63
+
64
+ ## What each agent loads, and what it costs
65
+
66
+ Every model call sends an agent's fixed context again: its system prompt and, for ForgeQE's agent, its tool list. A
67
+ request takes several calls, so a line added here is paid for many times. ForgeQE keeps it small, and checks it.
68
+
69
+ - **One list of agents**: `src/agents.ts` names every agent, the prompt files it loads, the skills it may use, and a
70
+ budget for its fixed context. `forgeqe context` shows each agent against its budget (exit 1 when one is over).
71
+ - **ForgeQE's agent loads what the task needs** (`src/mcp/routing.ts`):
72
+ - A capability's `playbook` sections of brain.md (`src/capabilities/registry.ts`) leave the system prompt. The prompt
73
+ keeps an index of them; the first result of the capability's tools in a session carries the section, and the agent
74
+ can read one first with the `playbook` tool. A section no capability owns stays in the prompt, so a section you
75
+ rename in your copy of brain.md is never lost.
76
+ - Tools stay listed in full. Listing tools in one line was measured and cost more: the agent then needs an extra
77
+ model call to get a tool's arguments, and it plans worse without the full description.
78
+ - `FORGEQE_ROUTING=off` sends everything, as before: use it to compare, or to rule routing out when something fails.
79
+ - **Skills and product knowledge** load on demand already: an agent sees each skill's name and description and reads
80
+ the skill when it needs it; knowledge sections are picked by relevance to the task.
81
+
82
+ When you add or change something:
83
+
84
+ | You add | Also do |
85
+ |---------|---------|
86
+ | a tool | put it in one capability's `tools`, with a description that says when to use it |
87
+ | a section in brain.md | give it to a capability's `playbook` with a `when` line, unless every request needs it |
88
+ | a prompt file | name it under its agent in `src/agents.ts` (or include it from a prompt that is) |
89
+ | an agent | add it to `src/agents.ts` with a budget |
90
+
91
+ Then run `forgeqe context` and `forgeqe prompts eval brain` before and after: `bun test` fails when something is not
92
+ in the lists or an agent is over budget, and the evaluation cases show whether the agent still picks the right tools.
package/prompts/brain.md CHANGED
@@ -24,53 +24,12 @@ A message can also be a task specification with the parts Role, Objective, Conte
24
24
  **Preconditions**: "This session" below gives the setup: the systems, the project, the logins, the domain, the capabilities, and the defaults. You MUST use it, and MUST NOT ask the user for what it gives.
25
25
 
26
26
  **Tools** (ForgeQE tools; the client adds the forgeqe_ prefix):
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
- - 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.
30
- - 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
- - 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
- - 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
- - read_confluence: reads Confluence pages (requirements, specifications) as text, by id, link, or CQL.
35
- - browser_devices: lists, saves, and removes named remote browsers (devices) on BrowserStack or TestMu AI, kept in the provider's YAML file. When the user describes devices or browsers to test on, save each one, then pass their names as devices to browser_run or run_test_cases: every device runs at the same time and is reported on its own.
36
- - attach_to_work_item: puts files from a run (the last or failure screenshot, report.html, junit.xml, trace.md) on an existing Azure DevOps work item or Jira issue, as attachments, in a comment (images shown inline), or both. The Azure DevOps MCP server cannot upload files, so use this tool for any file on a work item.
37
- - run_details: gives the full record of a browser run: steps, values, ranked locators, and the generated spec.
38
- - run_test: ForgeQE runs a test command. A test passes only when this tool, write_test, or heal_tests says so.
39
- - write_test: the coding agent of ForgeQE turns a passed run into a test in a repository. ForgeQE runs the test until it passes.
40
- - heal_tests: ForgeQE runs the tests of a repository and heals the tests that fail against the live application, or reports application bugs.
41
- - find_tests: Scribe, the coding agent, finds the automated tests in a repository that match a request. A request can be a feature, tags or ids, a folder, or tests added in a date range or sprint. ForgeQE confirms the selection with the list command of the framework. It runs nothing.
42
- - run_tests: runs a find_tests selection after the user confirms, with a pass or fail for each test. A test that fails runs one more time. A test that then passes is flaky, not failing. The results of each run go into the test history of the repository.
43
- - flaky_tests: gives the flaky tests from that history, with their probable cause.
44
- - 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
- - 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.
46
- - 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
- - 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
- - post_pr_review: posts a review_pull_request review to the pull request after the user confirms.
49
- - select_pr_tests: selects the automated tests that a pull request, or the commits since a base branch, can break. It uses what the change does and the linked work items. Run the tests with run_tests.
50
- - post_pr_test_results: posts a run_tests result on the pull request after the user confirms.
51
- - analyze_pr_impact: finds the product areas, pages, requirements, and tests that a pull request touches. This works also when the code of the product is in a different repository than the tests.
52
- - comment_on_pr: posts a comment, such as an impact report, on a pull request after the user confirms.
53
- - api_contracts: makes API contracts (status, schema, and key fields for each operation). It uses the calls that a browser run saw the application make, or an API description. Descriptions are an OpenAPI or Swagger link or file, WSDL, a GraphQL endpoint or schema, .proto, or grpc:// with reflection. It supports REST, GraphQL, SOAP, and gRPC.
54
- - run_api_contracts: runs a contract set directly against the API, without a browser. This is fast. It runs calls that change data only after the user agrees.
55
- - write_api_tests: Scribe writes a contract set as API tests in the framework of the repository. ForgeQE runs them until they pass. With scenarios, it writes end-to-end scenario tests on the API helpers of the repository.
56
- - api_scenarios: makes end-to-end API scenarios. These are calls in sequence, where values go from an answer to a later request. It makes them from a browser run, from an API description (lifecycles, browsing, links, negative cases), or from test cases or plain English.
57
- - run_api_scenarios: runs the scenarios against the API, without a browser. It asks before it runs a scenario that changes data.
58
- - explore_story: does exploratory testing of a story in real browsers, past its scripted cases. It examines edge cases, input that is not valid, boundaries, roles, and state. It confirms each defect with two runs. It proposes test cases. It files bugs for confirmed defects after the user agrees.
59
- - quality_report: gives the WCAG rules that a browser run passed and failed (axe-core, with the standard in forgeqe.yaml). It also gives the Core Web Vitals of each page, against the budgets.
60
- - quality_trends: gives the accessibility and performance of each page over time, with what became worse or better.
61
- - list_sprints: gives recent sprints from Azure DevOps and Jira, with their dates.
62
- - monitor: runs checks on a schedule, with Teams and email alerts. It can run one round now, start or stop rounds in the background, give status, and send test alerts.
63
- - logins: manages saved logins. It lists them. It lets the user sign in by hand in a browser (sign_in). It removes a session.
64
- - page_memory: gives what ForgeQE knows about the pages, locators, and recorded flows of a site. Clear it only when the user asks.
65
- - notify: posts a message to the Teams or email channels of the team. Use it only when the user asks. Suites, release verdicts, and failed evaluations go to their channels automatically from runs outside the shell.
66
- - write_report: saves a report that the user asks for as a page (filters, charts, the sources of its data) and a Markdown file. ForgeQE builds its tables from evidence: the results of tool calls in this session, or a read-only query that it runs itself. You write only the summary.
67
- - capabilities: tells what ForgeQE can do, and whether each capability works in this setup. It gives what to set up, example requests, limits, and the documentation.
68
- - forgeqe_setup: gives model profiles, the MCP servers that ForgeQE sets up, and the documentation of ForgeQE (docs). It installs or repairs the browser engine. It creates a forgeqe.yaml (init).
69
- - push_branch: the only way to push. It refuses the default branch, and it never forces.
70
- - set_ci_secret: stores one environment variable of ForgeQE as a pipeline secret. You give the name, never the value.
71
- - query_api: reads data from Azure DevOps, Jira, Xray, or Confluence through their APIs, with the credentials of ForgeQE. It only reads.
72
- - create_ado_dashboard: creates an Azure DevOps dashboard with work item lists, count tiles, and text. A saved shared query supplies each list or count.
73
- - write_ado_wiki_page: creates or updates an Azure DevOps wiki page. It creates the project wiki first when the project has none.
27
+ - Each ForgeQE tool comes with its own description. Read it to select the tool.
28
+ - Some playbooks are not in this prompt. The index under "Playbooks" names them. The first result of their tools gives each one, and you MUST obey it.
29
+ - browser_run: 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.
30
+ - attach_to_work_item: the only way to put a file from a run on a work item or an issue. The Azure DevOps MCP server cannot upload files.
31
+ - A test passes only when run_test, write_test, or heal_tests says so.
32
+ - query_api only reads. set_ci_secret takes the name of a variable, never its value.
74
33
 
75
34
  Each action of the forgeqe command line is one of these tools. You MUST do the action with the tool, not tell the user to run a command.
76
35
  You also have your own tools to read and edit files and to run shell commands in the working directory. You also have the tools of the MCP servers under "This session", for example azure_devops_* for Azure DevOps and atlassian_* for Jira and Confluence.
@@ -165,6 +124,7 @@ Read data from Azure DevOps, Jira, Xray, Confluence, and other systems in this s
165
124
  - 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.
166
125
  - For a case classified "test case needs updating", you MUST write the correct steps from run_details. Show them, and offer update_test_case.
167
126
  - You MUST NOT change a test case without update_test_case, which asks the user first.
127
+ - 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.
168
128
  - 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.
169
129
  - 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:
170
130
  - A functional defect: the application is wrong. Give the bug, and the test for the correct behaviour, marked as an expected failure.
@@ -183,17 +143,27 @@ Domain applies when you write and check test cases, test from requirements, do e
183
143
  ### Testing from requirements
184
144
  For a Confluence page, a Jira or Azure DevOps story, or a specification file:
185
145
  - 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.
146
+ - 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.
186
147
  - You MUST write one case for each acceptance criterion or rule, and the main negative paths.
187
148
  - You MUST then write one case for each domain risk and obligation on your list that the story does not cover.
188
149
  - 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.
189
150
  - 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.
190
151
  - 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.
191
- - 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.
152
+ - 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.
153
+ - 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.
192
154
  - 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.
193
155
  - You MUST then run them with run_test_cases, unless the user asked only for the cases.
194
156
  - 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.
195
157
  - 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.
196
158
 
159
+ ### End-to-end journeys from test cases
160
+ For "an end-to-end test for story 1234", "combine these cases into one flow", or "E2E from the test plan":
161
+ - 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.
162
+ - When the story has no linked cases yet, write and save them by "Testing from requirements" first, then call e2e_journey.
163
+ - 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.
164
+ - 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.
165
+ - For end-to-end API scenarios, use api_scenarios.
166
+
197
167
  ### Signing in
198
168
  - 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.
199
169
  - You MUST NOT put credentials in goals.
@@ -218,10 +188,25 @@ For a Confluence page, a Jira or Azure DevOps story, or a specification file:
218
188
  - 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.
219
189
  - 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.
220
190
 
191
+ ### Regression packs
192
+ When the user asks for a regression pack, or which tests to run for a release, a sprint, or recent pull requests:
193
+ - 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.
194
+ - 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.
195
+ - 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.
196
+ - 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.
197
+ - After a run, you MUST name the failing tests with their messages, and the tests that fail now but passed in the last run.
198
+
221
199
  ### Writing tests
222
200
  When the user asks, or when the setup below says that passed runs become tests:
223
201
  - You MUST call write_test with the folder of the passed run. Give the repository when it is not the working directory.
224
- - 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.
202
+ - 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.
203
+ - 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:
204
+ - existing_test (the test, Class.Method) and existing_mode reuse, when that test already does and checks everything the request asks.
205
+ - existing_mode extend, when the test covers the start of the journey and the request adds a few steps or checks to it.
206
+ - 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.
207
+ - fresh set to true, when no test is this journey. The new test still reuses the page objects and helpers of the repository.
208
+ 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.
209
+ 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.
225
210
  - 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.
226
211
  - You MUST NOT write that test yourself. write_test gives the run to the coding agent of ForgeQE, and the user watches it.
227
212
  - 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.
@@ -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.
@@ -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:
@@ -6,7 +6,7 @@ import logging
6
6
  import os
7
7
  import re
8
8
  import time
9
- from typing import Any, Dict, List, Optional, Tuple
9
+ from typing import Any, Callable, Dict, List, Optional, Tuple
10
10
 
11
11
  logger = logging.getLogger("forgeqe_engine.service")
12
12
 
@@ -157,18 +157,46 @@ def _build_llm(llm_config: Optional[Dict[str, Any]]):
157
157
  raise ValueError(f"Unsupported LLM provider for the engine: {provider}")
158
158
 
159
159
 
160
- def _time_llm_calls(llm: Any, calls: List[Tuple[float, float]]) -> None:
161
- """Record when each model call started and ended, so the report can split step time into model and browser."""
160
+ def call_usage(result: Any) -> Dict[str, int]:
161
+ """One model call's tokens from its completion (browser-use ChatInvokeUsage); prompt tokens include cached ones."""
162
+ u = getattr(result, "usage", None)
163
+ if u is None:
164
+ return {}
165
+ out = {"input": int(getattr(u, "prompt_tokens", 0) or 0), "output": int(getattr(u, "completion_tokens", 0) or 0)}
166
+ cached = int(getattr(u, "prompt_cached_tokens", 0) or 0)
167
+ written = int(getattr(u, "prompt_cache_creation_tokens", 0) or 0)
168
+ if cached:
169
+ out["cached"] = cached
170
+ if written:
171
+ out["cache_write"] = written
172
+ return out
173
+
174
+
175
+ def _time_llm_calls(llm: Any, calls: List[Tuple[float, float]], on_call: Optional[Callable[[Dict[str, Any]], None]] = None) -> None:
176
+ """
177
+ Record when each model call started and ended, so the report can split step time into model and browser;
178
+ on_call also gets each call's time and tokens (counts only, never the prompt), for ForgeQE's usage ledger.
179
+ """
162
180
  inner = getattr(llm, "ainvoke", None)
163
181
  if inner is None:
164
182
  return
165
183
 
166
184
  async def timed(*args: Any, **kwargs: Any) -> Any:
167
185
  started = time.time()
186
+ result: Any = None
187
+ ok = False
168
188
  try:
169
- return await inner(*args, **kwargs)
189
+ result = await inner(*args, **kwargs)
190
+ ok = True
191
+ return result
170
192
  finally:
171
- calls.append((started, time.time()))
193
+ ended = time.time()
194
+ calls.append((started, ended))
195
+ if on_call is not None:
196
+ try:
197
+ on_call({"ms": int((ended - started) * 1000), "ok": ok, **call_usage(result)})
198
+ except Exception:
199
+ logger.debug("model call usage not reported", exc_info=True)
172
200
 
173
201
  try:
174
202
  object.__setattr__(llm, "ainvoke", timed)
@@ -5,6 +5,7 @@ Layout under the memory directory:
5
5
 
6
6
  pages/<host>/<page>.json every interactive element seen on that page, each with ranked locators
7
7
  flows/<goal>-<hash>.json the actions of the last successful run of a goal, replayable without the LLM
8
+ cases/<hash>.json for a test case: which flow it last passed with, so an edited case replays its unchanged steps
8
9
 
9
10
  A locator's confidence (1-99 %) multiplies four factors: how stable its kind is (test id > id >
10
11
  role+name > ... > xpath), how unique it was on the page, how consistently it reappeared on later
@@ -650,22 +651,44 @@ class MemoryStore:
650
651
  replayed: int = 0,
651
652
  llm_steps: int = 0,
652
653
  resume: Optional[Dict[str, Any]] = None,
654
+ case: Optional[Dict[str, Any]] = None,
655
+ checks: Optional[List[Dict[str, Any]]] = None,
653
656
  ) -> Optional[str]:
654
657
  """
655
658
  Update the goal's flow after a run. Only successful runs replace its steps. Returns "saved" or "updated".
656
659
 
657
660
  A failed run with `resume` (the steps that worked before it went wrong) keeps them under "resume", so the
658
661
  next run of this task replays them and the agent picks up where the failed run went wrong.
662
+
663
+ `case` ({id, keys, ends}) is the test case the goal came from: a key per case step, and for each the number
664
+ of flow steps done by the end of it (None where unknown). A successful run records it and points the case's
665
+ index at this flow, so a run of the case after an edit replays the steps before the first one that changed.
659
666
  """
660
667
  if status == "stopped":
661
668
  return None
662
669
  with _locked(self.root):
663
670
  return self._save_flow_result(goal, start_url, status=status, steps=steps, summary=summary,
664
- final_url=final_url, replayed=replayed, llm_steps=llm_steps, resume=resume)
671
+ final_url=final_url, replayed=replayed, llm_steps=llm_steps, resume=resume, case=case,
672
+ checks=checks)
673
+
674
+ def case_file(self, case_id: str) -> Path:
675
+ return self.root / "cases" / f"{_sha(case_id, 16)}.json"
676
+
677
+ def load_case_flow(self, case_id: str) -> Optional[Dict[str, Any]]:
678
+ """The flow a test case last passed with, when it recorded where each case step ended."""
679
+ index = _read_json(self.case_file(case_id)) if case_id else None
680
+ name = str((index or {}).get("flow") or "")
681
+ if not name or "/" in name or "\\" in name:
682
+ return None
683
+ flow = _read_json(self.root / "flows" / name)
684
+ if not flow or flow.get("case_id") != case_id or not flow.get("successes") or not isinstance(flow.get("steps"), list):
685
+ return None
686
+ return flow
665
687
 
666
688
  def _save_flow_result(self, goal: str, start_url: Optional[str], *, status: str, steps: List[Dict[str, Any]],
667
689
  summary: str, final_url: str, replayed: int, llm_steps: int,
668
- resume: Optional[Dict[str, Any]] = None) -> Optional[str]:
690
+ resume: Optional[Dict[str, Any]] = None, case: Optional[Dict[str, Any]] = None,
691
+ checks: Optional[List[Dict[str, Any]]] = None) -> Optional[str]:
669
692
  file = self.flow_file(goal, start_url)
670
693
  flow = self.load_flow(goal, start_url)
671
694
  created = flow is None
@@ -689,6 +712,18 @@ class MemoryStore:
689
712
  flow["llm_steps"] = llm_steps
690
713
  flow["replayed_steps"] = replayed
691
714
  flow.pop("resume", None)
715
+ # checks: what the run checked, with the flow step each came after (observe.py); None keeps the flow's.
716
+ # New steps from the agent with no checks given leave none, since old positions would not fit them.
717
+ if checks is not None:
718
+ flow["checks"] = checks
719
+ elif llm_steps and flow.get("checks"):
720
+ flow.pop("checks", None)
721
+ if case and case.get("id") and case.get("keys"):
722
+ kept = len(flow["steps"])
723
+ flow["case_id"] = str(case["id"])
724
+ flow["case_step_keys"] = [str(k) for k in case["keys"]]
725
+ flow["case_step_ends"] = [e if isinstance(e, int) and 0 < e <= kept else None for e in (case.get("ends") or [])][: len(case["keys"])]
726
+ _write_json(self.case_file(str(case["id"])), {"case_id": str(case["id"]), "flow": file.name, "updated": now})
692
727
  elif status != "complete":
693
728
  flow["failures"] = int(flow.get("failures") or 0) + 1
694
729
  if keep_resume and resume:
@@ -702,6 +737,28 @@ class MemoryStore:
702
737
  return None
703
738
  return "saved" if created else "updated"
704
739
 
740
+ @staticmethod
741
+ def case_prefix(flow: Dict[str, Any], keys: List[str]) -> Tuple[int, int]:
742
+ """
743
+ How much of a case's earlier flow still fits its steps now: (case steps, flow steps). The case steps are the
744
+ leading ones whose keys did not change, up to the last of them where the flow recorded its end. (0, 0) when none.
745
+
746
+ An end is trusted only when the next step that recorded one ended later: when the agent did the actions of two
747
+ steps before recording either check, both ends are the same, and replaying up to the first would also redo
748
+ the second step's actions, which may be the step that changed.
749
+ """
750
+ old = [str(k) for k in (flow.get("case_step_keys") or [])]
751
+ ends = [e if isinstance(e, int) and 0 < e <= len(flow.get("steps") or []) else None for e in (flow.get("case_step_ends") or [])]
752
+ same = 0
753
+ while same < min(len(old), len(keys)) and old[same] == keys[same]:
754
+ same += 1
755
+ for n in range(same, 0, -1):
756
+ end = ends[n - 1] if n - 1 < len(ends) else None
757
+ later = next((e for e in ends[n:] if e is not None), None)
758
+ if end is not None and (later is None or later > end):
759
+ return n, end
760
+ return 0, 0
761
+
705
762
  def recent_flows(self, host: str, *, exclude: Optional[Path] = None, limit: int = 2) -> List[Dict[str, Any]]:
706
763
  folder = self.root / "flows"
707
764
  if not folder.is_dir():