@capagents/forgeqe 1.3.2 → 2.0.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.
- package/HOW_TO_USE.md +373 -9
- package/README.md +55 -4
- package/docs/ARCHITECTURE.md +62 -1
- package/docs/CONTAINERS.md +227 -0
- package/docs/COST_ARCHITECTURE.md +32 -0
- package/docs/DESKTOP-CI.md +87 -0
- package/docs/DESKTOP.md +387 -0
- package/docs/MOBILE.md +228 -0
- package/docs/feature-test-prompts.md +25 -0
- package/docs/security-data-flow.html +440 -3
- package/docs/user-guide.html +797 -415
- package/env.example.yaml +67 -0
- package/forgeqe.example.yaml +56 -1
- package/llms.example.json +7 -0
- package/package.json +4 -1
- package/prompts/brain.md +31 -0
- package/prompts/find-tests.md +1 -0
- package/py/forgeqe_desktop/__init__.py +1 -0
- package/py/forgeqe_desktop/__main__.py +3 -0
- package/py/forgeqe_desktop/apps.py +80 -0
- package/py/forgeqe_desktop/checks.py +168 -0
- package/py/forgeqe_desktop/cli.py +90 -0
- package/py/forgeqe_desktop/handhooks.py +293 -0
- package/py/forgeqe_desktop/handrecord.py +239 -0
- package/py/forgeqe_desktop/heal.py +107 -0
- package/py/forgeqe_desktop/models.py +208 -0
- package/py/forgeqe_desktop/pywinauto_driver.py +217 -0
- package/py/forgeqe_desktop/record.py +150 -0
- package/py/forgeqe_desktop/replay.py +345 -0
- package/py/forgeqe_desktop/service.py +356 -0
- package/py/forgeqe_desktop/session.py +60 -0
- package/py/forgeqe_desktop/ufo_bridge.py +86 -0
- package/py/forgeqe_desktop/ufo_runner.py +305 -0
- package/py/forgeqe_desktop/visual.py +112 -0
- package/py/forgeqe_desktop/workflow.py +338 -0
- package/py/forgeqe_engine/llm.py +54 -4
- package/py/forgeqe_engine/model_params.py +169 -0
- package/py/forgeqe_mobile/__init__.py +1 -0
- package/py/forgeqe_mobile/__main__.py +3 -0
- package/py/forgeqe_mobile/appium_driver.py +342 -0
- package/py/forgeqe_mobile/apps.py +84 -0
- package/py/forgeqe_mobile/cli.py +70 -0
- package/py/forgeqe_mobile/explore.py +203 -0
- package/py/forgeqe_mobile/hyperexecute.py +261 -0
- package/py/forgeqe_mobile/maestro.py +321 -0
- package/py/forgeqe_mobile/service.py +332 -0
- package/skills/testcontainers/SKILL.md +42 -0
- package/src/agents.ts +11 -2
- package/src/api/plan.ts +2 -0
- package/src/api/write.ts +2 -0
- package/src/auth/copilot.ts +130 -0
- package/src/auth/signin.ts +54 -1
- package/src/brain/index.ts +26 -4
- package/src/capabilities/guide.ts +2 -0
- package/src/capabilities/registry.ts +110 -2
- package/src/capabilities/status.ts +15 -0
- package/src/cases/rca.ts +2 -0
- package/src/childEnv.ts +4 -1
- package/src/cli/args.ts +3 -2
- package/src/cli/help.ts +24 -3
- package/src/cli/index.ts +47 -3
- package/src/code/find.ts +26 -0
- package/src/code/loop.ts +4 -1
- package/src/code/prompt.ts +2 -1
- package/src/code/prreview.ts +2 -0
- package/src/code/review.ts +6 -2
- package/src/code/runtime.ts +34 -3
- package/src/commands.ts +6 -1
- package/src/config.ts +159 -3
- package/src/containers/command.ts +68 -0
- package/src/containers/compose.ts +173 -0
- package/src/containers/config.ts +283 -0
- package/src/containers/mock.ts +156 -0
- package/src/containers/run.ts +362 -0
- package/src/containers/tests.ts +80 -0
- package/src/context.ts +33 -6
- package/src/copilot.ts +155 -0
- package/src/desktop/bridge.ts +229 -0
- package/src/desktop/command.ts +350 -0
- package/src/desktop/edit.ts +217 -0
- package/src/desktop/junit.ts +35 -0
- package/src/desktop/report.ts +155 -0
- package/src/desktop/run.ts +252 -0
- package/src/desktop/setup.ts +134 -0
- package/src/engine/bridge.ts +12 -5
- package/src/engine/setup.ts +3 -3
- package/src/environments.ts +566 -0
- package/src/evals/domain.ts +1 -0
- package/src/explore/charters.ts +2 -0
- package/src/guardrails.ts +145 -0
- package/src/heal/loop.ts +4 -1
- package/src/heal/prompt.ts +2 -1
- package/src/init.ts +5 -2
- package/src/llm.ts +23 -2
- package/src/mcp/attachtools.ts +16 -4
- package/src/mcp/captools.ts +27 -0
- package/src/mcp/containertools.ts +212 -0
- package/src/mcp/desktoptools.ts +334 -0
- package/src/mcp/mobiletools.ts +132 -0
- package/src/mcp/reporttools.ts +17 -2
- package/src/mcp/repotests.ts +22 -3
- package/src/mcp/routing.ts +11 -5
- package/src/mcp/scope.ts +210 -0
- package/src/mcp/server.ts +80 -5
- package/src/mcp/tools.ts +38 -1
- package/src/mobile/command.ts +245 -0
- package/src/mobile/run.ts +226 -0
- package/src/pack/pipeline.ts +44 -2
- package/src/refine.ts +3 -0
- package/src/report/html.ts +3 -0
- package/src/report/template.html +48 -48
- package/src/run.ts +7 -3
- package/src/tokenomics/ledger.ts +3 -1
- package/src/tui/App.tsx +23 -0
- package/src/usage.ts +1 -1
package/docs/DESKTOP.md
ADDED
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
# ForgeQE desktop: thick-client automation
|
|
2
|
+
|
|
3
|
+
Status: runs on Windows. The CI job `.github/workflows/forgeqe-desktop-windows.yml` (GitHub's `windows-2022`) installs the
|
|
4
|
+
engine and the pinned UFO², replays Notepad live with the pywinauto driver and through `forgeqe desktop replay`, heals a
|
|
5
|
+
moved control from the live window, and runs a whole explore: UFO² drives Notepad with a scripted model behind an
|
|
6
|
+
OpenAI-compatible server, the action is recorded with the control's AutomationId, and the recording replays. Not yet
|
|
7
|
+
run: explore with a real model (the job has no key), and your own apps. See
|
|
8
|
+
[Before first use on Windows](#before-first-use-on-windows).
|
|
9
|
+
|
|
10
|
+
ForgeQE drives web apps through Chrome. The desktop engine does the same for Windows thick clients (Win32, WinForms,
|
|
11
|
+
WPF, and the native parts of Electron and WebView2 apps): say what you want in plain language, UFO² does it on the live
|
|
12
|
+
app, and the run is saved as a YAML workflow that replays later without a model.
|
|
13
|
+
|
|
14
|
+
## Use
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
forgeqe desktop setup --ufo # once, on Windows: the engine, pywinauto, and the pinned UFO²
|
|
18
|
+
forgeqe desktop apps # what forgeqe.yaml allows
|
|
19
|
+
forgeqe desktop explore "Create an invoice for Contoso, net 30" --app billing
|
|
20
|
+
forgeqe desktop record --app billing # or do the steps yourself: no model, no UFO
|
|
21
|
+
forgeqe desktop replay .forgeqe/reports/desktop-explore-…/workflow.yaml customer=Fabrikam
|
|
22
|
+
forgeqe desktop replay workflow.yaml --data customers.csv # once per row; the header names the variables
|
|
23
|
+
forgeqe desktop replay desktop-tests/ --junit results.xml # every workflow in a folder, results for CI
|
|
24
|
+
forgeqe desktop replay workflow.yaml --update-baselines # save new baselines for its expect_image steps
|
|
25
|
+
forgeqe desktop doctor # can this machine run replay unattended?
|
|
26
|
+
forgeqe desktop validate workflow.yaml # any OS
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Or ask ForgeQE's agent in the shell or `forgeqe run`: "create an invoice for Contoso in Billing and save it as a test",
|
|
30
|
+
or "I'll show you the steps in Billing, record them". Its tools are `test_apps`, `desktop_explore`, `desktop_record`,
|
|
31
|
+
`desktop_replay` and `desktop_edit_workflow`. It asks before it starts the app or takes over the desktop, and before it
|
|
32
|
+
changes a workflow. Where it cannot ask (an MCP client), the call answers `needs_confirmation` until the user agrees.
|
|
33
|
+
|
|
34
|
+
Desktop apps come from the active environment in `env.yaml` (see HOW_TO_USE.md, "Environments and apps"), or from
|
|
35
|
+
`desktop.apps` in `forgeqe.yaml`, which works without environments:
|
|
36
|
+
|
|
37
|
+
```yaml
|
|
38
|
+
desktop:
|
|
39
|
+
apps:
|
|
40
|
+
billing:
|
|
41
|
+
path: C:\Program Files\Contoso\Billing\Billing.exe
|
|
42
|
+
args: ["--profile", "test"]
|
|
43
|
+
# window: "Billing.*" # for a launcher that starts the real app and exits: connect by title
|
|
44
|
+
secrets: [BILLING_PASSWORD] # the only environment variables {{secret:…}} may read
|
|
45
|
+
heal: true # replay asks the model to re-find a control that moved (default on)
|
|
46
|
+
screenshots: true # one screenshot per replayed step (default on)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Each run writes a folder under the reports folder (`desktop-explore-…` or `desktop-replay-…`): `run.json`,
|
|
50
|
+
`report.html`, `steps/NNN.png`, and for explore `workflow.yaml`, `trace.json` and UFO's own logs in `ufo/`. A replay
|
|
51
|
+
whose steps were healed also writes `heals.json` and asks whether to write the new locators into the workflow
|
|
52
|
+
(`--accept-heals` writes them without asking; with no terminal they are never written).
|
|
53
|
+
|
|
54
|
+
## Decisions
|
|
55
|
+
|
|
56
|
+
| Question | Decision | Why |
|
|
57
|
+
|---|---|---|
|
|
58
|
+
| Agent that explores an app | Microsoft UFO² (`microsoft/UFO`, MIT), pinned to commit `eae3c0a8d168` and never edited, behind one adapter (`ufo_runner.py`) | It already solves UI Automation (UIA) control detection, merging it with vision, and the per-app agent loop. Behind the adapter it can be swapped for our own engine without touching the CLI, the YAML or replay. |
|
|
59
|
+
| Saved scripts | ForgeQE's own YAML (`forgeqe-desktop-v1`), replayed on pywinauto (UIA backend) | UFO calls a model at every step. Replay must be fast, free, repeatable and fit for CI, so no model runs unless a step needs healing. |
|
|
60
|
+
| Models | `llms.json`, the same profiles as the browser engine. UFO never gets its own model config | One place to configure models, keys and Microsoft Entra ID. |
|
|
61
|
+
| Python environment | Its own (`~/.forgeqe/desktop/.venv`, Python 3.10), not the browser engine's | UFO pins `openai==1.66.2` and `pydantic`, which the browser engine's library does not run with, and `pandas==1.4.3`, which has Windows wheels up to Python 3.10. Replay's packages use UFO's pins, so one environment runs both. |
|
|
62
|
+
| Not chosen: `windowsagent` | Reference only | Very early (v0.1.0), replay not built, and it exposes a model-driven `/shell` endpoint. |
|
|
63
|
+
|
|
64
|
+
## How a run flows
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
forgeqe desktop explore ──► src/desktop/bridge.ts ──JSON lines──► python -m forgeqe_desktop serve
|
|
68
|
+
│ start the app from desktop.apps (no shell)
|
|
69
|
+
│ UFO² in-process: model = llms.json profile,
|
|
70
|
+
│ UI tools only, every action traced with its
|
|
71
|
+
▼ control's UIA properties
|
|
72
|
+
record.py ──► workflow.yaml
|
|
73
|
+
forgeqe desktop replay ───► bridge ──► serve (replay.py on pywinauto, no model)
|
|
74
|
+
│ a control is not found
|
|
75
|
+
▼
|
|
76
|
+
heal.py: the model picks one of the window's real controls, by number
|
|
77
|
+
▼
|
|
78
|
+
proposed locator → heals.json, written into the workflow only when accepted
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## One model config: `llms.json` → UFO
|
|
82
|
+
|
|
83
|
+
UFO picks its model client by `API_TYPE` through `ufo.llm.base.BaseService.get_service`, which every model call goes
|
|
84
|
+
through (`ufo/llm/llm_call.py`). The adapter adds the type `forgeqe`:
|
|
85
|
+
|
|
86
|
+
- `write_ufo_config` writes UFO's `config/ufo/agents.yaml` from its template, with `API_TYPE: forgeqe`, the model's name,
|
|
87
|
+
and no endpoint and no key, for `HOST_AGENT`, `APP_AGENT`, `BACKUP_AGENT` and `EVALUATION_AGENT`.
|
|
88
|
+
- `ufo_bridge.install` makes `get_service("forgeqe", …)` return `ForgeQEService`, which calls the profile through the
|
|
89
|
+
`openai` SDK (`models.py`): `azure` (key or `auth: entra`), `openai`, `openai_compatible` (key or Entra), and `ollama`
|
|
90
|
+
through its OpenAI-compatible `/v1`. Keys, Azure endpoints and Entra use the browser engine's own helpers
|
|
91
|
+
(`forgeqe_engine.llm`), so a profile means the same thing in both. Keys stay in environment variables.
|
|
92
|
+
- Each call's time and tokens go to ForgeQE's usage ledger as the "desktop agent" (counts only, never the prompt).
|
|
93
|
+
|
|
94
|
+
Explore sends screenshots, so its profile must take images: `"vision": true` or `false` on the profile says so; without
|
|
95
|
+
it, `ollama` profiles are refused and others accepted. Healing sends a screenshot only to a profile that takes images.
|
|
96
|
+
|
|
97
|
+
The request parameters suit the model by themselves: a reasoning model (o1, o3, o4, gpt-5, by the profile's `model` or
|
|
98
|
+
`deployment` name, or `"reasoning": true` on the profile) gets `max_completion_tokens` and no temperature from the first
|
|
99
|
+
call, and when the API refuses a parameter (`max_tokens`, `temperature`, `frequency_penalty`, `response_format`, …) the
|
|
100
|
+
call is sent again without it, and later calls to that model leave it out (`py/forgeqe_engine/model_params.py`). The
|
|
101
|
+
browser agent does the same.
|
|
102
|
+
|
|
103
|
+
## Workflow YAML (`forgeqe-desktop-v1`)
|
|
104
|
+
|
|
105
|
+
```yaml
|
|
106
|
+
format: forgeqe-desktop-v1
|
|
107
|
+
name: Create an invoice
|
|
108
|
+
app: billing # a name from forgeqe.yaml desktop.apps, never a path or a command
|
|
109
|
+
variables:
|
|
110
|
+
customer: Contoso
|
|
111
|
+
steps:
|
|
112
|
+
- op: launch
|
|
113
|
+
- op: click
|
|
114
|
+
target: {automation_id: btnNewInvoice, control_type: Button, name: New invoice}
|
|
115
|
+
- op: type
|
|
116
|
+
target: {automation_id: txtCustomer, control_type: Edit}
|
|
117
|
+
text: "{{customer}}"
|
|
118
|
+
- op: type
|
|
119
|
+
target: {automation_id: txtPassword, control_type: Edit}
|
|
120
|
+
text: "{{secret:BILLING_PASSWORD}}"
|
|
121
|
+
- op: select
|
|
122
|
+
target: {automation_id: cmbTerms, control_type: ComboBox}
|
|
123
|
+
value: Net 30
|
|
124
|
+
- op: keys
|
|
125
|
+
keys: ctrl+s
|
|
126
|
+
- op: expect
|
|
127
|
+
target: {automation_id: lblStatus}
|
|
128
|
+
text: Saved
|
|
129
|
+
- op: close
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
- **Ops:** `launch`, `click`, `double_click`, `type`, `select`, `keys`, `expect`, `expect_image`, `wait` (at most
|
|
133
|
+
60 s), `wait_for`, `close`.
|
|
134
|
+
- **Locators** use UIA properties, never screen coordinates: `automation_id`, `name`, `control_type`, `class_name`,
|
|
135
|
+
`index` (when several match), and `path` (the ancestors' `control_type` and `name`). A target needs an
|
|
136
|
+
`automation_id` or a `name`. Replay looks in the launched app's own windows only.
|
|
137
|
+
- **Variables:** `{{name}}` from the workflow's `variables` or `name=value` on the command line; `{{secret:NAME}}` from
|
|
138
|
+
the environment, only for names in `desktop.secrets`. A secret ForgeQE itself uses (a model key, a tracker token) is
|
|
139
|
+
refused there. Values are substituted once and never expanded again. Targets take `{{name}}` too (a CSV row's
|
|
140
|
+
`{{customer}}` as the control to click), never a secret.
|
|
141
|
+
- **Keys:** `ctrl`, `shift` and `alt` with one named key or character (`ctrl+s`, `alt+f4`, `enter`, `f5`). The Windows
|
|
142
|
+
and context-menu keys are refused: Win+R followed by typing runs any command.
|
|
143
|
+
- **Typing** is literal: pywinauto's key syntax (`^`, `%`, `+`, `~`, `{ENTER}`) in the text is escaped.
|
|
144
|
+
|
|
145
|
+
## Recording by hand
|
|
146
|
+
|
|
147
|
+
When you know the steps, do them yourself instead of describing a goal: no model and no UFO are involved, so it costs
|
|
148
|
+
nothing and does exactly what you did.
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
forgeqe desktop record --app billing --name "Create an invoice"
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
ForgeQE starts the app and watches the mouse and keyboard (low-level Windows hooks), asking UI Automation which control
|
|
155
|
+
each click and key went to. Do the steps, then:
|
|
156
|
+
|
|
157
|
+
- **Ctrl+Shift+F11** with the mouse over a control adds a check: an `expect` step with the text it shows now (a
|
|
158
|
+
status message, a saved total). Add one at the end, so the replay tests the outcome, not only the steps.
|
|
159
|
+
- **Ctrl+Shift+F12**, or closing the app, finishes. The console shows each step as it is recorded.
|
|
160
|
+
|
|
161
|
+
What becomes a step:
|
|
162
|
+
|
|
163
|
+
| You do | Step |
|
|
164
|
+
|---|---|
|
|
165
|
+
| Click a control (two quick clicks: `double_click`) | `click` with its AutomationId, name and control type |
|
|
166
|
+
| Pick an item in a drop-down | `select` on the drop-down, with the item's name |
|
|
167
|
+
| Type into a field (corrections and pastes included) | one `type` with the field's text when you move on, read from the field |
|
|
168
|
+
| A key chord outside a text field (`ctrl+s`, `enter`, `f5`) | `keys` |
|
|
169
|
+
| Ctrl+Shift+F11 over a control | `expect` with its text |
|
|
170
|
+
|
|
171
|
+
- **Passwords:** a password field is never read; its step types `{{secret:NAME}}` (the one name in `desktop.secrets`,
|
|
172
|
+
or `<APP>_PASSWORD`, which the result tells you to set). Text equal to a `desktop.secrets` value is written as its
|
|
173
|
+
placeholder. The typed characters themselves are never kept, only the field's text when you move on.
|
|
174
|
+
- **Only the app:** clicks and keys outside the app's own process are not recorded; nor are the Windows and menu keys,
|
|
175
|
+
or controls with neither an AutomationId nor a name. The result lists what was left out.
|
|
176
|
+
- The run folder has `workflow.yaml`, `run.json` and `report.html`, like explore. Replay it with
|
|
177
|
+
`forgeqe desktop replay`.
|
|
178
|
+
|
|
179
|
+
## Fixing a workflow from chat
|
|
180
|
+
|
|
181
|
+
When a replayed step fails because the app was slow or a dialog was in the way, ForgeQE's agent fixes the workflow
|
|
182
|
+
with `desktop_edit_workflow`: it proposes the change (a longer `timeout` or `retries` on the step, a `wait_for` before
|
|
183
|
+
it, a dialog button in `dismiss`, a healed locator, a corrected text), shows you the diff, and writes the file only
|
|
184
|
+
when you agree. For example, after "step 4 timed out while the invoice list loaded":
|
|
185
|
+
|
|
186
|
+
```text
|
|
187
|
+
Change invoice.yaml?
|
|
188
|
+
• new wait_for step before step 4
|
|
189
|
+
• step 4: retries 2
|
|
190
|
+
- op: click
|
|
191
|
+
+ target: {automation_id: btnPost}
|
|
192
|
+
+ retries: 2
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
- Each change has a fixed shape (`set`, `unset`, `set_step`, `add_dismiss`, `insert_step`, `remove_step`,
|
|
196
|
+
`set_variable`); step numbers are the replay report's. Anything else is refused.
|
|
197
|
+
- The changed workflow is checked by the engine's own validator before it is shown; one that would not run is not
|
|
198
|
+
offered. Comments and layout are kept.
|
|
199
|
+
- The old text is kept in the reports folder (`desktop-edits/`), and a file that changed since the edit was proposed is
|
|
200
|
+
not overwritten.
|
|
201
|
+
- The workflow must be a `.yaml` inside the working folder or the reports folder.
|
|
202
|
+
|
|
203
|
+
## Waits, retries and dialogs
|
|
204
|
+
|
|
205
|
+
Real apps are slow in places: a splash screen, a form that loads its data, a "Tip of the day" or "Save changes?"
|
|
206
|
+
dialog. A workflow says how to wait for them, without fixed `wait` steps:
|
|
207
|
+
|
|
208
|
+
```yaml
|
|
209
|
+
timeout: 20 # seconds a step waits for its control (default 10, at most 300)
|
|
210
|
+
retries: 1 # tries a failed action again, after a short pause (default 0, at most 5)
|
|
211
|
+
dismiss: # clicked whenever one of them is in the way
|
|
212
|
+
- {name: OK, control_type: Button, automation_id: btnTipClose}
|
|
213
|
+
- {name: "Don't Save", control_type: Button}
|
|
214
|
+
steps:
|
|
215
|
+
- op: launch
|
|
216
|
+
timeout: 90 # a slow start: how long to wait for the app's window
|
|
217
|
+
- op: wait_for # the splash screen goes away
|
|
218
|
+
target: {automation_id: splashForm}
|
|
219
|
+
gone: true
|
|
220
|
+
timeout: 120
|
|
221
|
+
- op: wait_for # the form has loaded its data
|
|
222
|
+
target: {automation_id: lblStatus}
|
|
223
|
+
text: Ready
|
|
224
|
+
- op: click
|
|
225
|
+
target: {automation_id: btnPost}
|
|
226
|
+
timeout: 60 # per step, over the workflow's timeout
|
|
227
|
+
retries: 3
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
- **Each step waits** for its control up to its `timeout`. `click`, `type` and `select` also wait until the control is
|
|
231
|
+
enabled and visible (a form that is still loading greys its buttons out).
|
|
232
|
+
- **`wait_for`** waits until a control is there, shows a `text`, or with `gone: true` has gone.
|
|
233
|
+
- **`retries`** applies to `click`, `double_click`, `type`, `select`, `keys`, `expect` and `expect_image`. Between
|
|
234
|
+
tries, replay pauses (1 s, 2 s, … up to 5 s) and closes any `dismiss` dialog. An error in the workflow itself (a
|
|
235
|
+
missing variable) is not retried.
|
|
236
|
+
- **`dismiss`** lists at most 10 targets, checked like any target. When a control is not found, or something waits to
|
|
237
|
+
go away, replay clicks the `dismiss` controls that are on screen and looks again, up to 3 times per lookup. The
|
|
238
|
+
report and the console name every dialog closed, and how many tries a step took.
|
|
239
|
+
|
|
240
|
+
## Visual checks (`expect_image`)
|
|
241
|
+
|
|
242
|
+
`expect` compares text. When what matters is drawn (a chart, a signature, a colour, a layout), `expect_image`
|
|
243
|
+
compares pixels with a baseline image:
|
|
244
|
+
|
|
245
|
+
```yaml
|
|
246
|
+
- op: expect_image
|
|
247
|
+
target: {automation_id: chartRevenue} # without a target: the app's top window
|
|
248
|
+
image: revenue-chart.png # a file name in <workflow name>.baselines/ next to the workflow
|
|
249
|
+
tolerance: 0.01 # share of pixels that may differ (default 0.01, i.e. 1%)
|
|
250
|
+
ignore: [[0, 0, 120, 24]] # regions [x, y, width, height] that change every run (a date, a clock)
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
- **The first run** with no baseline saves what it captured and passes; the report says so. Commit the baselines
|
|
254
|
+
folder with the workflow. `--update-baselines` replaces them after an intended change.
|
|
255
|
+
- **A pixel counts as different** when a colour channel differs by more than 32 of 255, so anti-aliasing and ClearType
|
|
256
|
+
do not fail a check. A different size (another screen scale or window size) fails with both sizes named.
|
|
257
|
+
- **The report** shows the baseline, what was captured, and the differences in red, with the share of pixels that
|
|
258
|
+
differ (`images/NNN-baseline|actual|diff.png` in the run folder).
|
|
259
|
+
- **Image names** are file names only (letters, digits, `_`, `-`, ending `.png`). A path, `..`, or a link that leads
|
|
260
|
+
out of the baselines folder is refused.
|
|
261
|
+
- Run visual checks on one machine with a fixed resolution and scale (see [DESKTOP-CI.md](DESKTOP-CI.md)).
|
|
262
|
+
|
|
263
|
+
## Unattended runs
|
|
264
|
+
|
|
265
|
+
Replay needs a signed-in, unlocked Windows session. Before it starts the app, it checks the session and fails at once
|
|
266
|
+
with the reason if it is a Windows service (session 0), a locked screen, or a disconnected Remote Desktop session.
|
|
267
|
+
`forgeqe desktop doctor` runs the same check. To run the suite every night on a machine of your own, see
|
|
268
|
+
[DESKTOP-CI.md](DESKTOP-CI.md): a runner set up in the user's session (`ci/desktop-runner/prepare-runner.ps1`), a
|
|
269
|
+
nightly workflow (`ci/desktop-runner/desktop-nightly.example.yml`), and `--junit` results.
|
|
270
|
+
|
|
271
|
+
## Updating UFO²
|
|
272
|
+
|
|
273
|
+
UFO² is pinned to one commit. Moving it is checked before it is pinned:
|
|
274
|
+
|
|
275
|
+
1. `.github/workflows/forgeqe-desktop-ufo-update.yml` runs weekly, or by hand with a commit. It sets up that UFO on
|
|
276
|
+
Windows (`FORGEQE_UFO_COMMIT`, which takes a full 40-character commit id only), runs the adapter tests against its
|
|
277
|
+
real code and a scripted explore of Notepad, and says in the job summary whether it passed.
|
|
278
|
+
2. When it passed: `bun scripts/ufo-bump.ts <commit>` writes the new pin into `src/desktop/setup.ts`,
|
|
279
|
+
`py/forgeqe_desktop/ufo_runner.py` and this page. Run the tests and push; the Windows CI job tests the pin again.
|
|
280
|
+
|
|
281
|
+
When it fails, the failing step names what changed in UFO (usually the action executor or the config layout that the
|
|
282
|
+
adapter wraps). Keep the pin until the adapter is updated for it.
|
|
283
|
+
|
|
284
|
+
## Checks of the outcome
|
|
285
|
+
|
|
286
|
+
A recording of actions only proves the steps run. When UFO has finished, and before the app closes, explore shows the
|
|
287
|
+
model the goal and the app's controls with the text each shows now, and the model picks up to three whose text proves
|
|
288
|
+
the goal (a status message, a saved value), by number, with the part of the text to expect. A check is kept only when
|
|
289
|
+
that text really is in that control, so the model can choose among what is on screen but never invent it; text holding
|
|
290
|
+
a `desktop.secrets` value is never offered. The checks are saved as `expect` steps at the end of the workflow, and the
|
|
291
|
+
report says how many there are. With none, the report and the agent say so: add `expect` steps by hand.
|
|
292
|
+
|
|
293
|
+
## Recording
|
|
294
|
+
|
|
295
|
+
UFO's own step log names the control and a per-step id, not its AutomationId, so the adapter wraps UFO's
|
|
296
|
+
`ActionExecutor.execute` and records each action with the UIA properties of the control it acted on. `record.py` turns
|
|
297
|
+
that trace into steps: `click_input` → `click`/`double_click`, `set_edit_text` → `type`, `keyboard_input` → `keys` (one
|
|
298
|
+
allowed chord) or `type`, `select` → `select`. Coordinate clicks, scrolls and anything else are listed as not recorded
|
|
299
|
+
in the report. A typed value equal to a `desktop.secrets` value is written as its placeholder; typed text that only
|
|
300
|
+
looks like `{{…}}` is kept literal, so a model told to type `{{secret:X}}` cannot make replay type the secret.
|
|
301
|
+
|
|
302
|
+
## Security
|
|
303
|
+
|
|
304
|
+
The YAML, the trace, the app's screen text and the model's replies can all be written by someone else. The rules in the
|
|
305
|
+
repository's `CLAUDE.md` apply; for this engine:
|
|
306
|
+
|
|
307
|
+
- **No command in the workflow.** `app:` names a `desktop.apps` entry: an absolute `.exe` (a `.bat` or `.cmd` would run
|
|
308
|
+
through `cmd.exe`, which reparses its arguments) and an argument list, started with `subprocess.Popen([path, *args])`.
|
|
309
|
+
- **UFO gets UI tools only.** Each explore writes UFO's `mcp.yaml` with `UICollector`, `HostUIExecutor` and
|
|
310
|
+
`AppUIExecutor`. UFO's default `CommandLineExecutor`, which runs shell commands, and its Word, Excel, PowerPoint and
|
|
311
|
+
PDF executors are left out. The adapter starts the app, so UFO never launches a program.
|
|
312
|
+
- **Keys:** during explore the adapter refuses UFO's Windows and menu keys too, and the report lists what it refused.
|
|
313
|
+
- **No one at the console:** UFO's safe guard asks before sensitive actions; explore answers no, since nobody is there.
|
|
314
|
+
- **Environment:** the engine process gets system settings, the profile's key variable (or Entra's) and
|
|
315
|
+
`desktop.secrets`, nothing else (`desktopEnvNames`).
|
|
316
|
+
- **Explore controls the real mouse and keyboard.** It asks before starting (`--yes` to skip; required with no
|
|
317
|
+
terminal). Use a test machine or VM and a test account.
|
|
318
|
+
- **Agent tools:** a workflow path the agent gives must be a `.yaml` inside the working folder or the reports folder (real
|
|
319
|
+
path, so a link out is refused); variables are names and text only; heals are reported, never written.
|
|
320
|
+
- **Healing:** the model picks one of the window's real controls by number; control text is passed as data, and any
|
|
321
|
+
other answer heals nothing. Heals are written into the workflow only when accepted.
|
|
322
|
+
- **Report:** every value is escaped; images are only the engine's own `steps/NNN.png` and
|
|
323
|
+
`images/NNN-baseline|actual|diff.png`.
|
|
324
|
+
- **Baselines:** `expect_image` names are file names, and their real path must be inside the workflow's baselines
|
|
325
|
+
folder, so a workflow cannot read or overwrite another file.
|
|
326
|
+
- **Unattended machines:** `prepare-runner.ps1` never handles a password; automatic sign-in uses Sysinternals
|
|
327
|
+
Autologon, which stores it encrypted.
|
|
328
|
+
- **Install:** UFO is cloned and checked out at the pinned commit (or `FORGEQE_UFO_COMMIT`, a full commit id only),
|
|
329
|
+
and the checkout's `HEAD` is verified; replay's packages are pinned to exact versions.
|
|
330
|
+
|
|
331
|
+
## Code
|
|
332
|
+
|
|
333
|
+
| File | Contents |
|
|
334
|
+
|---|---|
|
|
335
|
+
| `src/desktop/command.ts` | `forgeqe desktop`: setup, apps, doctor, validate, explore, record, replay (folders, `--data`, `--junit`, `--update-baselines`); heal acceptance |
|
|
336
|
+
| `src/desktop/junit.ts` | JUnit XML for a suite |
|
|
337
|
+
| `src/desktop/edit.ts` | Changes to a workflow from chat: shapes, applied with comments kept, validated, diff, backup |
|
|
338
|
+
| `src/desktop/run.ts` | Explore and replay as the CLI and the agent's tools run them; variables, CSV rows, workflow paths |
|
|
339
|
+
| `src/mcp/desktoptools.ts` | ForgeQE's agent's `test_apps`, `desktop_explore`, `desktop_record`, `desktop_replay`, `desktop_edit_workflow` |
|
|
340
|
+
| `src/desktop/bridge.ts` | Starts `python -m forgeqe_desktop serve`, passes events on, records model calls in the usage ledger |
|
|
341
|
+
| `src/desktop/setup.ts` | The desktop environment, the pinned UFO checkout (`ufoCommit`), `desktopDoctor` |
|
|
342
|
+
| `scripts/ufo-bump.ts` | Pins a new UFO commit in every place that names it |
|
|
343
|
+
| `ci/desktop-runner/` | `prepare-runner.ps1` and the nightly workflow example for unattended runs |
|
|
344
|
+
| `src/desktop/report.ts` | `report.html` for a run |
|
|
345
|
+
| `py/forgeqe_desktop/service.py` | `serve`: the JSON-lines protocol, replay and explore runs, `run.json` |
|
|
346
|
+
| `py/forgeqe_desktop/workflow.py` | Load, validate and save workflows; variables and secrets; the key-chord grammar |
|
|
347
|
+
| `py/forgeqe_desktop/apps.py` | `desktop.apps`: the app allowlist and the argument array a launch uses |
|
|
348
|
+
| `py/forgeqe_desktop/models.py` | The profile through the `openai` SDK; image checks |
|
|
349
|
+
| `py/forgeqe_desktop/ufo_bridge.py` | UFO's model blocks, and the `forgeqe` model service |
|
|
350
|
+
| `py/forgeqe_desktop/ufo_runner.py` | UFO's config, the action trace and key guard, running UFO in-process |
|
|
351
|
+
| `py/forgeqe_desktop/record.py` | Trace to workflow steps |
|
|
352
|
+
| `py/forgeqe_desktop/replay.py` | Runs a workflow on a `Driver`: timeouts, retries, `wait_for`, dismissing dialogs, heal and stop |
|
|
353
|
+
| `py/forgeqe_desktop/handrecord.py` | Recording by hand: clicks, typing, chords and checks as steps; secrets |
|
|
354
|
+
| `py/forgeqe_desktop/handhooks.py` | The Windows hooks and UI Automation lookups that feed it |
|
|
355
|
+
| `py/forgeqe_desktop/visual.py` | `expect_image`: baselines, pixel comparison, diff images |
|
|
356
|
+
| `py/forgeqe_desktop/session.py` | Whether this Windows session can be driven (service, locked, no screen) |
|
|
357
|
+
| `py/forgeqe_desktop/heal.py` | One model call to re-find a moved control |
|
|
358
|
+
| `py/forgeqe_desktop/checks.py` | One model call for checks of the outcome, kept only when on screen |
|
|
359
|
+
| `py/forgeqe_desktop/pywinauto_driver.py` | The Windows `Driver` |
|
|
360
|
+
|
|
361
|
+
Tests: `bun test tests/desktop.test.ts`, and `<engine-python> -m pytest tests/desktop -q` (needs `openai==1.66.2`
|
|
362
|
+
and PyYAML; they use a fake driver and a fake UFO, so they run on any OS).
|
|
363
|
+
|
|
364
|
+
## Before first use on Windows
|
|
365
|
+
|
|
366
|
+
Verified in CI: setup with and without `--ufo`, replay, healing, the CLI, and UFO's own code with the adapter (its
|
|
367
|
+
config, its model calls routed to the profile, the action trace, UI tools only). Still to check, on a Windows test
|
|
368
|
+
machine with your apps:
|
|
369
|
+
|
|
370
|
+
1. **UIA coverage:** `Inspect.exe` shows the controls that matter with stable `AutomationId`s. Custom grids, Java
|
|
371
|
+
(needs Java Access Bridge), Delphi, SAP GUI and Citrix (vision only) are where UIA falls short.
|
|
372
|
+
2. **Explore with your model:** one goal with an Azure profile and one with `auth: entra`; `trace.json` has the
|
|
373
|
+
controls' AutomationIds, and `ufo/` has UFO's logs.
|
|
374
|
+
3. **Replay:** the recorded workflow passes 10 runs out of 10; a renamed control is healed.
|
|
375
|
+
4. **Cost and time** of one explore, from `forgeqe` usage.
|
|
376
|
+
|
|
377
|
+
Found by the Windows runs and fixed: UFO's pins need Python 3.10 (pandas 1.4.3); UFO prints emoji panels to stdout, which
|
|
378
|
+
is the engine's event stream and crashed on the cp1252 console (now sent to stderr, characters replaced); an error in
|
|
379
|
+
UFO's own evaluation lost the recorded actions (the trace is now kept and saved).
|
|
380
|
+
|
|
381
|
+
## Next steps
|
|
382
|
+
|
|
383
|
+
1. The checks above.
|
|
384
|
+
2. `forgeqe suite` and test cases from Azure DevOps, Jira and Xray for desktop workflows, with results written back.
|
|
385
|
+
3. Scribe: export a workflow as pywinauto code (or C# with FlaUI) in the user's repo.
|
|
386
|
+
4. App adapters where UIA falls short: Java Access Bridge, SAP GUI scripting, vision-only for Citrix.
|
|
387
|
+
5. A pipeline template for a self-hosted Windows runner signed in to a desktop session.
|
package/docs/MOBILE.md
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
# ForgeQE mobile: Android and iOS apps
|
|
2
|
+
|
|
3
|
+
Status: the engine and its tests run on any OS, against a fake Appium server and a stand-in for Maestro. The CI job
|
|
4
|
+
`.github/workflows/forgeqe-mobile-android.yml` runs it on an Android emulator: Appium explores and replays the
|
|
5
|
+
emulator's Settings app, and Maestro replays the same workflow. iOS runs the same code with the XCUITest driver, but
|
|
6
|
+
no CI job tests it yet (it needs a macOS runner with a simulator).
|
|
7
|
+
|
|
8
|
+
ForgeQE drives mobile apps two ways, and one workflow format runs on both:
|
|
9
|
+
|
|
10
|
+
| | Appium | Maestro |
|
|
11
|
+
|---|---|---|
|
|
12
|
+
| Runs | step by step, through an Appium server (yours or a device cloud's) | the whole workflow as one Maestro flow |
|
|
13
|
+
| Explore a goal with the model | yes | no |
|
|
14
|
+
| Heal a control that moved | yes, one model call, reported for review | no |
|
|
15
|
+
| Visual checks (`expect_image`) | yes | no |
|
|
16
|
+
| Your team's own flows | n/a | `forgeqe mobile maestro`, checked against an allowlist |
|
|
17
|
+
| Needs | `appium` with UiAutomator2 (Android) or XCUITest (iOS), a device or emulator | `maestro` and a device or emulator |
|
|
18
|
+
|
|
19
|
+
## Use
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
forgeqe mobile doctor # does the Appium server answer, is maestro installed?
|
|
23
|
+
forgeqe mobile apps # the mobile apps of the active environment
|
|
24
|
+
forgeqe mobile explore "Sign in as the courier and accept the first delivery" --app courier
|
|
25
|
+
forgeqe mobile replay .forgeqe/reports/mobile-explore-…/workflow.yaml
|
|
26
|
+
forgeqe mobile replay workflow.yaml --driver maestro
|
|
27
|
+
forgeqe mobile replay mobile-tests/ --data couriers.csv --junit results.xml
|
|
28
|
+
forgeqe mobile maestro .maestro/ # the team's flows
|
|
29
|
+
forgeqe mobile flow workflow.yaml # print a workflow as a Maestro flow
|
|
30
|
+
forgeqe mobile validate workflow.yaml
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Or ask ForgeQE's agent in the shell or `forgeqe run`: "sign in to the courier app and save it as a test", "replay the
|
|
34
|
+
courier login on Maestro", "run our Maestro flows". Its tools are `mobile_explore`, `mobile_replay` and
|
|
35
|
+
`mobile_maestro`. It asks before each one starts, and `desktop_edit_workflow` fixes a mobile workflow after a failed
|
|
36
|
+
replay, the same way as a desktop one.
|
|
37
|
+
|
|
38
|
+
## Apps and settings
|
|
39
|
+
|
|
40
|
+
Mobile apps come from the active environment in env.yaml, or from `mobile.apps` in forgeqe.yaml:
|
|
41
|
+
|
|
42
|
+
```yaml
|
|
43
|
+
# env.yaml
|
|
44
|
+
environments:
|
|
45
|
+
qa:
|
|
46
|
+
apps:
|
|
47
|
+
courier:
|
|
48
|
+
kind: mobile
|
|
49
|
+
platform: android # or ios
|
|
50
|
+
app_id: com.contoso.courier # Android package or iOS bundle id
|
|
51
|
+
activity: .MainActivity # Android only, optional
|
|
52
|
+
app: /builds/courier-qa.apk # optional: install this build (.apk/.aab/.ipa/.app/.zip, https URL, bs://…)
|
|
53
|
+
description: Driver app that confirms delivery
|
|
54
|
+
secrets: [COURIER_PASSWORD_QA]
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
```yaml
|
|
58
|
+
# forgeqe.yaml
|
|
59
|
+
mobile:
|
|
60
|
+
driver: appium # appium | maestro: the default for replays
|
|
61
|
+
appium:
|
|
62
|
+
url: http://127.0.0.1:4723
|
|
63
|
+
capabilities: # added to every session
|
|
64
|
+
appium:udid: emulator-5554
|
|
65
|
+
# username_env: BROWSERSTACK_USERNAME
|
|
66
|
+
# access_key_env: BROWSERSTACK_ACCESS_KEY
|
|
67
|
+
maestro:
|
|
68
|
+
command: [maestro]
|
|
69
|
+
# device: emulator-5554
|
|
70
|
+
secrets: [COURIER_PASSWORD]
|
|
71
|
+
heal: true
|
|
72
|
+
screenshots: true
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Each run writes a folder under the reports folder (`mobile-explore-…`, `mobile-replay-…`, `mobile-maestro-…`) with
|
|
76
|
+
`run.json`, `report.html`, step screenshots, and for Maestro runs the flow, `maestro.log` and Maestro's JUnit report.
|
|
77
|
+
|
|
78
|
+
## Workflow YAML (`forgeqe-mobile-v1`)
|
|
79
|
+
|
|
80
|
+
```yaml
|
|
81
|
+
format: forgeqe-mobile-v1
|
|
82
|
+
name: Courier signs in
|
|
83
|
+
app: courier
|
|
84
|
+
timeout: 20
|
|
85
|
+
dismiss:
|
|
86
|
+
- {name: Allow, control_type: android.widget.Button} # permission dialogs, when they are in the way
|
|
87
|
+
steps:
|
|
88
|
+
- op: launch
|
|
89
|
+
- op: type
|
|
90
|
+
target: {automation_id: username}
|
|
91
|
+
text: "{{courier}}"
|
|
92
|
+
- op: type
|
|
93
|
+
target: {automation_id: password}
|
|
94
|
+
text: "{{secret:COURIER_PASSWORD}}"
|
|
95
|
+
- op: tap
|
|
96
|
+
target: {name: Sign in}
|
|
97
|
+
- op: wait_for
|
|
98
|
+
target: {automation_id: progress}
|
|
99
|
+
gone: true
|
|
100
|
+
- op: scroll_to
|
|
101
|
+
target: {name: Deliveries}
|
|
102
|
+
- op: expect
|
|
103
|
+
target: {automation_id: status}
|
|
104
|
+
text: Welcome
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
- **Ops:** `launch`, `tap`, `double_tap`, `long_press`, `type`, `expect`, `expect_image`, `swipe` (`direction` up,
|
|
108
|
+
down, left or right, optionally on a `target`), `scroll_to` (scrolls until the target is on screen), `back`,
|
|
109
|
+
`hide_keyboard`, `keys` (`enter`, `back`, `tab`, `delete`, `search`), `wait`, `wait_for`, `close`. Timeouts,
|
|
110
|
+
retries and `dismiss` work as in desktop workflows. Explore saves a tap on a system dialog (another package's
|
|
111
|
+
control, such as Android's notification prompt) under `dismiss`, not as a step, so the workflow still runs where the
|
|
112
|
+
dialog never shows; on Maestro, each `dismiss` target is tapped after a launch if it is there.
|
|
113
|
+
- **Targets** use the desktop keys, mapped per platform: `automation_id` is the Android resource-id (with or without the
|
|
114
|
+
`package:id/` prefix) or the iOS accessibility identifier; `name` is the Android text or content-desc, or the iOS
|
|
115
|
+
label; `control_type` is the Android class (`android.widget.Button`) or the iOS type (`XCUIElementTypeButton`);
|
|
116
|
+
`index` picks one of several matches.
|
|
117
|
+
|
|
118
|
+
## Explore
|
|
119
|
+
|
|
120
|
+
The model sees the goal, its steps so far, the numbered controls on screen with the text each shows, and a screenshot
|
|
121
|
+
when the profile takes images. It answers with one action on a control by its number (tap, long press, type), a swipe
|
|
122
|
+
or scroll, back, or done. So it can only act on a control that is on screen. When it says done, it picks checks of the
|
|
123
|
+
outcome from the controls' current text, kept only when that text is really there, as on the desktop. The app's text
|
|
124
|
+
is passed as data.
|
|
125
|
+
|
|
126
|
+
## Maestro
|
|
127
|
+
|
|
128
|
+
**ForgeQE workflows on Maestro.** `--driver maestro` writes the workflow as a Maestro flow (`flow.yaml` in the run
|
|
129
|
+
folder) and runs `maestro test`. Maestro's debug output gives each workflow step its result. Variables are filled in
|
|
130
|
+
first. Values are matched literally, so Maestro's regular expressions never see them as patterns. A secret becomes
|
|
131
|
+
`${MAESTRO_NAME}`, which Maestro reads from the environment ForgeQE gives it, so a secret's value never lands in the flow
|
|
132
|
+
file or on a command line. A value holding `${…}` is refused, because Maestro would run it as JavaScript.
|
|
133
|
+
|
|
134
|
+
**Your team's flows.** `forgeqe mobile maestro <files or folders>` checks each flow, and every flow it runs, before
|
|
135
|
+
Maestro gets it:
|
|
136
|
+
|
|
137
|
+
- Only UI commands pass: `launchApp`, `tapOn`, `inputText`, `assertVisible`, `scrollUntilVisible`, `swipe`,
|
|
138
|
+
`extendedWaitUntil`, `runFlow`, `repeat`, `retry` and the like.
|
|
139
|
+
- These are refused:
|
|
140
|
+
- `runScript`, `evalScript` and `assertTrue`, which run JavaScript.
|
|
141
|
+
- Maestro's AI commands, which send screenshots to a service.
|
|
142
|
+
- `takeScreenshot`, recordings and `addMedia`, which read or write files.
|
|
143
|
+
- `${…}` may only name a variable (`${USERNAME}`), never an expression.
|
|
144
|
+
- `when: {true: …}` is refused, because it is JavaScript.
|
|
145
|
+
- `openLink` takes http(s) or the app's own deep links, never `javascript:`, `file:` or `intent:`.
|
|
146
|
+
- `runFlow` files must be `.yaml` inside the working folder; links out of it are refused too. Nesting stops at 5 levels.
|
|
147
|
+
|
|
148
|
+
A refused flow does not run, and the error names the command.
|
|
149
|
+
|
|
150
|
+
## TestMu AI (formerly LambdaTest)
|
|
151
|
+
|
|
152
|
+
Both drivers run on TestMu's devices. Upload the app once, then use the `lt://` id it returns as the app's `app`:
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
forgeqe mobile upload courier.apk # → lt://APP… (--virtual for TestMu's emulators)
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
The user and key come from `LT_USERNAME` and `LT_ACCESS_KEY` (or the variables the config names), for example in a
|
|
159
|
+
`.env` file in the project folder. `samples/mobile-testmu/` is a ready project with TestMu's Wikipedia sample app.
|
|
160
|
+
|
|
161
|
+
```yaml
|
|
162
|
+
mobile:
|
|
163
|
+
apps:
|
|
164
|
+
courier: { platform: android, app_id: com.contoso.courier, app: "lt://APP1016…" }
|
|
165
|
+
appium: # Appium on TestMu's mobile hub
|
|
166
|
+
url: https://mobile-hub.lambdatest.com/wd/hub
|
|
167
|
+
username_env: LT_USERNAME
|
|
168
|
+
access_key_env: LT_ACCESS_KEY
|
|
169
|
+
capabilities:
|
|
170
|
+
"LT:Options": { w3c: true, isRealMobile: true, deviceName: "Pixel 6", platformVersion: "14", build: nightly } # app: added from the app's own
|
|
171
|
+
maestro:
|
|
172
|
+
testmu: # Maestro on TestMu, through HyperExecute
|
|
173
|
+
cli: /opt/hyperexecute # TestMu's hyperexecute CLI
|
|
174
|
+
devices: ["Pixel 6-14"] # name-version, or a pattern such as ".*-.*"
|
|
175
|
+
real_device: true # false: TestMu's emulators
|
|
176
|
+
# username_env: LT_USERNAME # the defaults
|
|
177
|
+
# access_key_env: LT_ACCESS_KEY
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
- **Appium** needs nothing else: the Appium driver talks to the hub with the user and key from the two variables.
|
|
181
|
+
- **Maestro** runs through HyperExecute. ForgeQE writes a HyperExecute project in the run folder and starts the CLI.
|
|
182
|
+
The project holds the flows in their folder layout (so `runFlow` still works), the app (an `lt://` id is passed as
|
|
183
|
+
the device's `appId`; a build file is uploaded with the project), a fixed setup script (Android platform tools 35.0.2
|
|
184
|
+
and Maestro 2.6.1 from its release, both checked against their SHA-256) and a fixed run script, and
|
|
185
|
+
`hyperexecute.yaml`. Team flows are checked against the allowlist first, as always. Flow file names may only use
|
|
186
|
+
letters, digits and `_ . - /` (and one leading dot, for `.maestro/`), because HyperExecute's shell reads them. After
|
|
187
|
+
the job, ForgeQE downloads its artifacts (Maestro's command results and JUnit reports), so each step gets its own
|
|
188
|
+
result in the report.
|
|
189
|
+
- **Secrets on Maestro on TestMu:** a workflow that types a secret is refused there for now (HyperExecute would need
|
|
190
|
+
the value in its own vault). Replay it on TestMu with `--driver appium`.
|
|
191
|
+
- The access key is passed to the CLI as an argument. It is removed from the CLI's output and from every file in the
|
|
192
|
+
run folder. Neither the user name nor the key is passed to the flows' environment.
|
|
193
|
+
- `.github/workflows/forgeqe-mobile-testmu.yml` runs all three on TestMu (Appium, Maestro with a workflow, a team flow)
|
|
194
|
+
with TestMu's Wikipedia sample app, once the repository has the secrets `LT_USERNAME` and `LT_ACCESS_KEY`.
|
|
195
|
+
|
|
196
|
+
## Security
|
|
197
|
+
|
|
198
|
+
- **No commands from the workflow or the model.** `app:` names a configured app (a platform and an id). Appium is
|
|
199
|
+
spoken to over HTTP, and `maestro` is started with an argument list, never a shell.
|
|
200
|
+
- **Locators** go into XPath as quoted literals, so a target cannot change the query.
|
|
201
|
+
- **Secrets:** `{{secret:NAME}}` reads only `mobile.secrets` and the environment's secrets, and ForgeQE's own keys are
|
|
202
|
+
refused there. Explore types a secret from its placeholder, so the model never sees the value.
|
|
203
|
+
- **The engine's environment** holds system settings, the profile's key, the listed secrets, the cloud's user and key,
|
|
204
|
+
and `JAVA_HOME` / `ANDROID_HOME`, nothing else.
|
|
205
|
+
- **Cloud credentials** come from the variables `mobile.appium.username_env` and `access_key_env` name; a user or key in
|
|
206
|
+
the URL is refused. TLS is verified as usual.
|
|
207
|
+
- **App builds** are absolute paths to an app file, https URLs, or a device cloud's own upload ids (`bs://`, `lt://`,
|
|
208
|
+
`storage:filename=`); `file://` and other schemes are refused.
|
|
209
|
+
|
|
210
|
+
## Code
|
|
211
|
+
|
|
212
|
+
| File | Contents |
|
|
213
|
+
|---|---|
|
|
214
|
+
| `src/mobile/command.ts` | `forgeqe mobile`: apps, doctor, explore, replay (folders, `--data`, `--junit`, `--driver`), maestro, flow, validate |
|
|
215
|
+
| `src/mobile/run.ts` | Requests to the mobile engine, its environment, Maestro flow paths, the doctor |
|
|
216
|
+
| `src/mcp/mobiletools.ts` | ForgeQE's agent's `mobile_explore`, `mobile_replay`, `mobile_maestro` |
|
|
217
|
+
| `py/forgeqe_mobile/appium_driver.py` | The Appium driver: W3C WebDriver over HTTP, the replay `Driver` |
|
|
218
|
+
| `py/forgeqe_mobile/maestro.py` | Workflows as Maestro flows, the allowlist for team flows, running `maestro test` |
|
|
219
|
+
| `py/forgeqe_mobile/hyperexecute.py` | Maestro on TestMu: the HyperExecute project and running its CLI |
|
|
220
|
+
| `py/forgeqe_mobile/explore.py` | The model's action loop on Appium |
|
|
221
|
+
| `py/forgeqe_mobile/service.py` | `serve`: replay, explore and maestro runs, `run.json` |
|
|
222
|
+
| `py/forgeqe_mobile/apps.py` | The mobile app allowlist and its capabilities |
|
|
223
|
+
|
|
224
|
+
The rest is shared with the desktop engine: `forgeqe_desktop.workflow` validates both formats, and `replay.py`,
|
|
225
|
+
`heal.py`, `checks.py` and `visual.py` run on either driver.
|
|
226
|
+
|
|
227
|
+
Tests: `<engine-python> -m pytest tests/desktop/test_mobile.py -q` (a fake Appium server and a stand-in Maestro; any
|
|
228
|
+
OS), and `bun test tests/mobile.test.ts`.
|
|
@@ -93,3 +93,28 @@ One prompt for each ForgeQE feature, to check the agent end to end. Type each pr
|
|
|
93
93
|
| 30 | Refine | Type `run 2287 and write automation`, then press **ctrl+r** | The panel scrolls with ↑↓ and PgUp/PgDn; open questions come first; Use, Edit, and Keep stay visible |
|
|
94
94
|
|
|
95
95
|
|
|
96
|
+
|
|
97
|
+
## Environments, desktop, mobile, and tool scope
|
|
98
|
+
|
|
99
|
+
These need an `env.yaml` with the apps named. The desktop prompts need Windows with a signed-in, unlocked desktop. The mobile prompts need an emulator or device with Appium (or Maestro), or TestMu credentials in `.env`.
|
|
100
|
+
|
|
101
|
+
| # | Feature | Prompt | What to check |
|
|
102
|
+
| --- | --- | --- | --- |
|
|
103
|
+
| 31 | Environments | `Which environments and apps do we have?`, then `Run the smoke cases on uat` | The apps of each environment are listed by kind; the run opens uat's URLs, not the default environment's |
|
|
104
|
+
| 32 | Desktop explore | `In <desktop app>, create a new record named ForgeQE test and save it as a test` ⚠ | ForgeQE asks before it takes over the mouse and keyboard; a `forgeqe-desktop-v1` workflow is saved with checks of the outcome |
|
|
105
|
+
| 33 | Desktop replay and heal | `Replay the last desktop workflow` | It runs with no model calls; a moved control is healed and listed for review, not saved silently |
|
|
106
|
+
| 34 | Record by hand | `Record a desktop test in <desktop app> while I do it` | Your clicks and typing become steps; a secret you type is saved as `{{secret:NAME}}` |
|
|
107
|
+
| 35 | Edit a workflow from chat | `In that workflow, make step 3 wait up to 20 seconds for the Save button` | A diff is shown and nothing changes until you confirm; the workflow still validates |
|
|
108
|
+
| 36 | Visual check | `Add an image check of the chart to the last desktop workflow` | An `expect_image` step and a baseline in `<workflow>.baselines/` |
|
|
109
|
+
| 37 | Mobile explore | `Open <mobile app>, sign in as the test user, and save it as a test` | Runs through Appium; a `forgeqe-mobile-v1` workflow is saved; the password is typed from its placeholder |
|
|
110
|
+
| 38 | Mobile on Maestro | `Replay that mobile workflow on Maestro` | A Maestro flow is written in the run folder with secrets as `${MAESTRO_…}`, not values; each step has its result |
|
|
111
|
+
| 39 | Team Maestro flows | `Run our Maestro flows in .maestro/` | Flows with `runScript`, `evalScript` or AI commands are refused, and the error names the command |
|
|
112
|
+
| 40 | TestMu | In `samples/mobile-testmu` (see its README): `forgeqe mobile upload sample.apk`, then `Replay wiki.yaml on TestMu with Appium, then with Maestro` | An `lt://` id comes back; the run shows on TestMu; the access key appears in no file in the run folder |
|
|
113
|
+
| 41 | Tool scope | `/logs` after #31 and #37 | Each turn's `agent.scope` lists only the capabilities it was about (browser for #31, mobile for #37), with the reason for each |
|
|
114
|
+
| 42 | `use_capabilities` | `Check the accessibility of the home page, then file a bug for the worst issue` | When the turn did not get the bug tools, the agent calls `use_capabilities` and carries on without asking you again |
|
|
115
|
+
| 43 | Services in Docker | `Start the database` (with `containers.services.db` in forgeqe.yaml), then `is it up?` | ForgeQE asks first; the card shows db ready on 127.0.0.1; `forgeqe services` lists it; it stops when the shell closes |
|
|
116
|
+
| 44 | Mock service | `Mock the orders API from the last run and point orders at it` | WireMock starts on a free 127.0.0.1 port; the stubs are in `.forgeqe/mocks/orders/mappings`; API tests now call the mock |
|
|
117
|
+
| 45 | Container guard rails | `Run docker run -v /:/host alpine` | The shell refuses it; the agent offers the services tool instead |
|
|
118
|
+
| 46 | Compose file | With `containers.compose: docker-compose.yml`: `Start the services from our compose file` | Services start in `depends_on` order; named volumes and read-only mounts are listed in notes; a `privileged` or `build` service is refused by name |
|
|
119
|
+
| 47 | Tests in a container | With `containers.tests` set: `Run the checkout tests` | The confirmation says "in a container (image)"; services start first; results are read as usual; files in the repo belong to you |
|
|
120
|
+
| 48 | Testcontainers (Scribe) | `Write an integration test for the order repository against a real Postgres` in a repo with Testcontainers | Scribe reuses the repo's container fixture, pins the image, and uses the mapped port; no `docker` command in the test |
|