fusiontest 0.2.7__tar.gz → 0.2.8__tar.gz
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.
- fusiontest-0.2.8/PKG-INFO +201 -0
- fusiontest-0.2.8/README.md +127 -0
- {fusiontest-0.2.7 → fusiontest-0.2.8}/fusiontest/cli.py +15 -9
- {fusiontest-0.2.7 → fusiontest-0.2.8}/fusiontest/core/runner.py +0 -26
- {fusiontest-0.2.7 → fusiontest-0.2.8}/fusiontest/desktop/playwright_adapter.py +0 -45
- {fusiontest-0.2.7 → fusiontest-0.2.8}/fusiontest/reporting/reporter.py +7 -0
- {fusiontest-0.2.7 → fusiontest-0.2.8}/fusiontest/training/__init__.py +5 -0
- fusiontest-0.2.8/fusiontest.egg-info/PKG-INFO +201 -0
- {fusiontest-0.2.7 → fusiontest-0.2.8}/fusiontest.egg-info/SOURCES.txt +0 -2
- {fusiontest-0.2.7 → fusiontest-0.2.8}/pyproject.toml +1 -1
- fusiontest-0.2.7/PKG-INFO +0 -266
- fusiontest-0.2.7/README.md +0 -192
- fusiontest-0.2.7/fusiontest/recording/__init__.py +0 -19
- fusiontest-0.2.7/fusiontest/recording/recorder.py +0 -309
- fusiontest-0.2.7/fusiontest.egg-info/PKG-INFO +0 -266
- {fusiontest-0.2.7 → fusiontest-0.2.8}/fusiontest/__init__.py +0 -0
- {fusiontest-0.2.7 → fusiontest-0.2.8}/fusiontest/core/__init__.py +0 -0
- {fusiontest-0.2.7 → fusiontest-0.2.8}/fusiontest/core/action_model.py +0 -0
- {fusiontest-0.2.7 → fusiontest-0.2.8}/fusiontest/core/goal_verifier.py +0 -0
- {fusiontest-0.2.7 → fusiontest-0.2.8}/fusiontest/core/netguard.py +0 -0
- {fusiontest-0.2.7 → fusiontest-0.2.8}/fusiontest/core/replay.py +0 -0
- {fusiontest-0.2.7 → fusiontest-0.2.8}/fusiontest/core/screen_parser.py +0 -0
- {fusiontest-0.2.7 → fusiontest-0.2.8}/fusiontest/core/secrets.py +0 -0
- {fusiontest-0.2.7 → fusiontest-0.2.8}/fusiontest/core/tokens.py +0 -0
- {fusiontest-0.2.7 → fusiontest-0.2.8}/fusiontest/desktop/__init__.py +0 -0
- {fusiontest-0.2.7 → fusiontest-0.2.8}/fusiontest/desktop/macos_adapter.py +0 -0
- {fusiontest-0.2.7 → fusiontest-0.2.8}/fusiontest/desktop/windows_adapter.py +0 -0
- {fusiontest-0.2.7 → fusiontest-0.2.8}/fusiontest/discovery/__init__.py +0 -0
- {fusiontest-0.2.7 → fusiontest-0.2.8}/fusiontest/discovery/goal_generator.py +0 -0
- {fusiontest-0.2.7 → fusiontest-0.2.8}/fusiontest/guardrails/__init__.py +0 -0
- {fusiontest-0.2.7 → fusiontest-0.2.8}/fusiontest/guardrails/engine.py +0 -0
- {fusiontest-0.2.7 → fusiontest-0.2.8}/fusiontest/mobile/__init__.py +0 -0
- {fusiontest-0.2.7 → fusiontest-0.2.8}/fusiontest/mobile/android_adapter.py +0 -0
- {fusiontest-0.2.7 → fusiontest-0.2.8}/fusiontest/mobile/ios_adapter.py +0 -0
- {fusiontest-0.2.7 → fusiontest-0.2.8}/fusiontest/reporting/__init__.py +0 -0
- {fusiontest-0.2.7 → fusiontest-0.2.8}/fusiontest/training/data_collector.py +0 -0
- {fusiontest-0.2.7 → fusiontest-0.2.8}/fusiontest/training/dataset_builder.py +0 -0
- {fusiontest-0.2.7 → fusiontest-0.2.8}/fusiontest/training/trainer.py +0 -0
- {fusiontest-0.2.7 → fusiontest-0.2.8}/fusiontest.egg-info/dependency_links.txt +0 -0
- {fusiontest-0.2.7 → fusiontest-0.2.8}/fusiontest.egg-info/entry_points.txt +0 -0
- {fusiontest-0.2.7 → fusiontest-0.2.8}/fusiontest.egg-info/not-zip-safe +0 -0
- {fusiontest-0.2.7 → fusiontest-0.2.8}/fusiontest.egg-info/requires.txt +0 -0
- {fusiontest-0.2.7 → fusiontest-0.2.8}/fusiontest.egg-info/top_level.txt +0 -0
- {fusiontest-0.2.7 → fusiontest-0.2.8}/setup.cfg +0 -0
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fusiontest
|
|
3
|
+
Version: 0.2.8
|
|
4
|
+
Summary: AI-powered UI testing for mobile and desktop — by FusionLeap.io
|
|
5
|
+
Author-email: FusionLeap <hello@fusionleap.io>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://fusionleap.io
|
|
8
|
+
Project-URL: Repository, https://github.com/mohigo/fusion-test
|
|
9
|
+
Project-URL: Issues, https://github.com/mohigo/fusion-test/issues
|
|
10
|
+
Keywords: testing,AI,mobile,desktop,LLM,automation,QA
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Topic :: Software Development :: Testing
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
Requires-Dist: click>=8.1
|
|
21
|
+
Requires-Dist: pyyaml>=6.0
|
|
22
|
+
Requires-Dist: httpx>=0.27
|
|
23
|
+
Requires-Dist: python-dotenv>=1.0
|
|
24
|
+
Requires-Dist: rich>=13.0
|
|
25
|
+
Requires-Dist: pydantic>=2.0
|
|
26
|
+
Requires-Dist: openai>=1.58
|
|
27
|
+
Provides-Extra: mobile
|
|
28
|
+
Requires-Dist: Appium-Python-Client>=3.1; extra == "mobile"
|
|
29
|
+
Requires-Dist: selenium>=4.18; extra == "mobile"
|
|
30
|
+
Provides-Extra: desktop
|
|
31
|
+
Requires-Dist: playwright>=1.43; extra == "desktop"
|
|
32
|
+
Provides-Extra: windows
|
|
33
|
+
Requires-Dist: Appium-Python-Client>=3.1; extra == "windows"
|
|
34
|
+
Requires-Dist: selenium>=4.18; extra == "windows"
|
|
35
|
+
Provides-Extra: mpnet
|
|
36
|
+
Requires-Dist: sentence-transformers>=2.7; extra == "mpnet"
|
|
37
|
+
Requires-Dist: torch>=2.2; extra == "mpnet"
|
|
38
|
+
Provides-Extra: api
|
|
39
|
+
Requires-Dist: fastapi>=0.111; extra == "api"
|
|
40
|
+
Requires-Dist: uvicorn[standard]>=0.29; extra == "api"
|
|
41
|
+
Requires-Dist: pydantic[email]>=2.0; extra == "api"
|
|
42
|
+
Requires-Dist: python-jose[cryptography]>=3.3; extra == "api"
|
|
43
|
+
Requires-Dist: cryptography>=42; extra == "api"
|
|
44
|
+
Requires-Dist: passlib[bcrypt]>=1.7; extra == "api"
|
|
45
|
+
Requires-Dist: psycopg2-binary>=2.9; extra == "api"
|
|
46
|
+
Provides-Extra: training
|
|
47
|
+
Requires-Dist: sentence-transformers>=2.7; extra == "training"
|
|
48
|
+
Requires-Dist: torch>=2.2; extra == "training"
|
|
49
|
+
Requires-Dist: datasets>=2.19; extra == "training"
|
|
50
|
+
Requires-Dist: accelerate>=0.30; extra == "training"
|
|
51
|
+
Requires-Dist: scikit-learn>=1.4; extra == "training"
|
|
52
|
+
Requires-Dist: pandas>=2.2; extra == "training"
|
|
53
|
+
Requires-Dist: numpy>=1.26; extra == "training"
|
|
54
|
+
Requires-Dist: tqdm>=4.66; extra == "training"
|
|
55
|
+
Requires-Dist: wandb>=0.17; extra == "training"
|
|
56
|
+
Provides-Extra: openai
|
|
57
|
+
Requires-Dist: openai>=1.58; extra == "openai"
|
|
58
|
+
Provides-Extra: groq
|
|
59
|
+
Requires-Dist: openai>=1.58; extra == "groq"
|
|
60
|
+
Provides-Extra: claude
|
|
61
|
+
Requires-Dist: anthropic>=0.25; extra == "claude"
|
|
62
|
+
Provides-Extra: gemini
|
|
63
|
+
Requires-Dist: google-genai>=1.0; extra == "gemini"
|
|
64
|
+
Provides-Extra: dev
|
|
65
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
66
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
|
|
67
|
+
Requires-Dist: pytest-cov>=5.0; extra == "dev"
|
|
68
|
+
Requires-Dist: ruff>=0.4; extra == "dev"
|
|
69
|
+
Requires-Dist: mypy>=1.10; extra == "dev"
|
|
70
|
+
Requires-Dist: pre-commit>=3.7; extra == "dev"
|
|
71
|
+
Requires-Dist: fusiontest[desktop,mobile,openai]; extra == "dev"
|
|
72
|
+
Provides-Extra: all
|
|
73
|
+
Requires-Dist: fusiontest[api,claude,desktop,gemini,groq,mobile,mpnet,openai,training,windows]; extra == "all"
|
|
74
|
+
|
|
75
|
+
# 🧪 FusionTest
|
|
76
|
+
|
|
77
|
+
> AI-powered UI testing for web, desktop and mobile — built by [FusionLeap.io](https://fusionleap.io)
|
|
78
|
+
|
|
79
|
+
FusionTest runs UI tests written as plain-English goals. An LLM reads the
|
|
80
|
+
screen and decides what to do, a verifier confirms each goal against what's
|
|
81
|
+
actually on screen, and goals that pass are recorded so later runs replay them
|
|
82
|
+
without calling the AI at all.
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## ✨ Features
|
|
87
|
+
|
|
88
|
+
| Feature | Notes |
|
|
89
|
+
|---|---|
|
|
90
|
+
| Natural-language goals | `"Open the Pricing page and verify all three plans are shown"` |
|
|
91
|
+
| Goal discovery | Crawls a site and proposes goals grounded in its real content |
|
|
92
|
+
| Record & replay | Passing goals replay with no LLM calls; the AI repairs them when the page changes |
|
|
93
|
+
| Honest results | Capacity or infrastructure problems are reported as **errors**, not test failures |
|
|
94
|
+
| Multi-provider LLMs | Groq, Claude, Gemini, OpenAI or local Ollama, with automatic fallback |
|
|
95
|
+
| Safe secrets | `{{PASSWORD}}` placeholders — the AI never sees values, which are only typed on allowed sites |
|
|
96
|
+
| Guardrails | Invalid-action checks, loop detection, backtracking, no-progress detection |
|
|
97
|
+
| Reporting | JSON reports, dashboard uploads, Slack, GitHub Actions annotations, webhooks |
|
|
98
|
+
| Platforms | Web (Playwright), Electron, Android & iOS (Appium), Windows, macOS |
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## 🏗 Architecture
|
|
103
|
+
|
|
104
|
+
```
|
|
105
|
+
fusiontest/
|
|
106
|
+
├── core/ # Runner, LLM action model + fallback chain, goal verifier,
|
|
107
|
+
│ # screen parser, secrets, replay, SSRF guard (netguard)
|
|
108
|
+
├── desktop/ # Playwright (web/Electron), Windows, macOS adapters
|
|
109
|
+
├── mobile/ # Android (UIAutomator2) and iOS (XCUITest) adapters via Appium
|
|
110
|
+
├── discovery/ # Goal discovery from a site's pages
|
|
111
|
+
├── guardrails/ # Action validation, loop detection, backtracking
|
|
112
|
+
├── reporting/ # JSON reports, Slack, GitHub annotations
|
|
113
|
+
└── training/ # Experimental MPNet fine-tuning (not wired into runs)
|
|
114
|
+
api/ # SaaS backend (FastAPI + PostgreSQL) — see docs/api.md
|
|
115
|
+
dashboard/ # React dashboard
|
|
116
|
+
website/ # Marketing site (Next.js)
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Design decisions are recorded in [docs/adr](docs/adr) — start with
|
|
120
|
+
[ADR-007](docs/adr/ADR-007-llm-capacity-and-execution.md).
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## 🚀 Quickstart
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
pip install "fusiontest[desktop]"
|
|
128
|
+
playwright install chromium
|
|
129
|
+
|
|
130
|
+
export GROQ_API_KEY=gsk_... # or ANTHROPIC_API_KEY / GEMINI_API_KEY / OPENAI_API_KEY
|
|
131
|
+
|
|
132
|
+
fusiontest run --url https://example.com \
|
|
133
|
+
--goal "Verify the page heading says Example Domain" --headless
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Set `FUSIONTEST_API_KEY` (from the dashboard's API Keys page) to upload each
|
|
137
|
+
run to your FusionTest dashboard.
|
|
138
|
+
|
|
139
|
+
Exit codes: `0` all goals passed · `1` a goal failed · `2` some goals couldn't
|
|
140
|
+
be evaluated (LLM capacity or infrastructure) — not a test failure.
|
|
141
|
+
|
|
142
|
+
> **Full guide:** [docs/getting-started.md](docs/getting-started.md) — platforms,
|
|
143
|
+
> goal files, configuration, secrets, replay, dashboard and CI.
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## 📝 Writing goals
|
|
148
|
+
|
|
149
|
+
```yaml
|
|
150
|
+
# tests/goals/smoke.yaml
|
|
151
|
+
name: Marketing site smoke test
|
|
152
|
+
platform: playwright
|
|
153
|
+
url: https://example.com
|
|
154
|
+
goals:
|
|
155
|
+
- "Open the Pricing page and verify three plan tiers are shown"
|
|
156
|
+
- "Open the Contact page, submit the form empty, and verify required-field errors appear"
|
|
157
|
+
- name: Log in
|
|
158
|
+
goal: "Log in with {{EMAIL}} and {{PASSWORD}} and verify the dashboard loads"
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
fusiontest run --file tests/goals/smoke.yaml --headless
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Tips: say what to verify, not just where to go; one outcome per goal; don't
|
|
166
|
+
submit real forms on production sites.
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## 🛠 Configuration
|
|
171
|
+
|
|
172
|
+
`fusiontest init` creates `fusiontest.config.yaml`:
|
|
173
|
+
|
|
174
|
+
```yaml
|
|
175
|
+
backend: groq # unset = auto-detect from API keys
|
|
176
|
+
guardrails:
|
|
177
|
+
loop_detection_window: 5
|
|
178
|
+
max_retries: 3
|
|
179
|
+
replay:
|
|
180
|
+
enabled: true
|
|
181
|
+
trace_dir: .fusiontest/traces
|
|
182
|
+
reverify_every: 10
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## 🔌 CI/CD
|
|
188
|
+
|
|
189
|
+
Run `fusiontest run --headless` in any CI system and cache
|
|
190
|
+
`.fusiontest/traces` so runs can replay. Guides for GitHub Actions, Bitrise,
|
|
191
|
+
Fastlane and Jenkins: [docs/ci-integrations.md](docs/ci-integrations.md).
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
## 🤝 Contributing
|
|
196
|
+
|
|
197
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) and [ROADMAP.md](ROADMAP.md).
|
|
198
|
+
|
|
199
|
+
## 📄 License
|
|
200
|
+
|
|
201
|
+
MIT © [FusionLeap.io](https://fusionleap.io)
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# 🧪 FusionTest
|
|
2
|
+
|
|
3
|
+
> AI-powered UI testing for web, desktop and mobile — built by [FusionLeap.io](https://fusionleap.io)
|
|
4
|
+
|
|
5
|
+
FusionTest runs UI tests written as plain-English goals. An LLM reads the
|
|
6
|
+
screen and decides what to do, a verifier confirms each goal against what's
|
|
7
|
+
actually on screen, and goals that pass are recorded so later runs replay them
|
|
8
|
+
without calling the AI at all.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## ✨ Features
|
|
13
|
+
|
|
14
|
+
| Feature | Notes |
|
|
15
|
+
|---|---|
|
|
16
|
+
| Natural-language goals | `"Open the Pricing page and verify all three plans are shown"` |
|
|
17
|
+
| Goal discovery | Crawls a site and proposes goals grounded in its real content |
|
|
18
|
+
| Record & replay | Passing goals replay with no LLM calls; the AI repairs them when the page changes |
|
|
19
|
+
| Honest results | Capacity or infrastructure problems are reported as **errors**, not test failures |
|
|
20
|
+
| Multi-provider LLMs | Groq, Claude, Gemini, OpenAI or local Ollama, with automatic fallback |
|
|
21
|
+
| Safe secrets | `{{PASSWORD}}` placeholders — the AI never sees values, which are only typed on allowed sites |
|
|
22
|
+
| Guardrails | Invalid-action checks, loop detection, backtracking, no-progress detection |
|
|
23
|
+
| Reporting | JSON reports, dashboard uploads, Slack, GitHub Actions annotations, webhooks |
|
|
24
|
+
| Platforms | Web (Playwright), Electron, Android & iOS (Appium), Windows, macOS |
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## 🏗 Architecture
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
fusiontest/
|
|
32
|
+
├── core/ # Runner, LLM action model + fallback chain, goal verifier,
|
|
33
|
+
│ # screen parser, secrets, replay, SSRF guard (netguard)
|
|
34
|
+
├── desktop/ # Playwright (web/Electron), Windows, macOS adapters
|
|
35
|
+
├── mobile/ # Android (UIAutomator2) and iOS (XCUITest) adapters via Appium
|
|
36
|
+
├── discovery/ # Goal discovery from a site's pages
|
|
37
|
+
├── guardrails/ # Action validation, loop detection, backtracking
|
|
38
|
+
├── reporting/ # JSON reports, Slack, GitHub annotations
|
|
39
|
+
└── training/ # Experimental MPNet fine-tuning (not wired into runs)
|
|
40
|
+
api/ # SaaS backend (FastAPI + PostgreSQL) — see docs/api.md
|
|
41
|
+
dashboard/ # React dashboard
|
|
42
|
+
website/ # Marketing site (Next.js)
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Design decisions are recorded in [docs/adr](docs/adr) — start with
|
|
46
|
+
[ADR-007](docs/adr/ADR-007-llm-capacity-and-execution.md).
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## 🚀 Quickstart
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
pip install "fusiontest[desktop]"
|
|
54
|
+
playwright install chromium
|
|
55
|
+
|
|
56
|
+
export GROQ_API_KEY=gsk_... # or ANTHROPIC_API_KEY / GEMINI_API_KEY / OPENAI_API_KEY
|
|
57
|
+
|
|
58
|
+
fusiontest run --url https://example.com \
|
|
59
|
+
--goal "Verify the page heading says Example Domain" --headless
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Set `FUSIONTEST_API_KEY` (from the dashboard's API Keys page) to upload each
|
|
63
|
+
run to your FusionTest dashboard.
|
|
64
|
+
|
|
65
|
+
Exit codes: `0` all goals passed · `1` a goal failed · `2` some goals couldn't
|
|
66
|
+
be evaluated (LLM capacity or infrastructure) — not a test failure.
|
|
67
|
+
|
|
68
|
+
> **Full guide:** [docs/getting-started.md](docs/getting-started.md) — platforms,
|
|
69
|
+
> goal files, configuration, secrets, replay, dashboard and CI.
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## 📝 Writing goals
|
|
74
|
+
|
|
75
|
+
```yaml
|
|
76
|
+
# tests/goals/smoke.yaml
|
|
77
|
+
name: Marketing site smoke test
|
|
78
|
+
platform: playwright
|
|
79
|
+
url: https://example.com
|
|
80
|
+
goals:
|
|
81
|
+
- "Open the Pricing page and verify three plan tiers are shown"
|
|
82
|
+
- "Open the Contact page, submit the form empty, and verify required-field errors appear"
|
|
83
|
+
- name: Log in
|
|
84
|
+
goal: "Log in with {{EMAIL}} and {{PASSWORD}} and verify the dashboard loads"
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
fusiontest run --file tests/goals/smoke.yaml --headless
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Tips: say what to verify, not just where to go; one outcome per goal; don't
|
|
92
|
+
submit real forms on production sites.
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## 🛠 Configuration
|
|
97
|
+
|
|
98
|
+
`fusiontest init` creates `fusiontest.config.yaml`:
|
|
99
|
+
|
|
100
|
+
```yaml
|
|
101
|
+
backend: groq # unset = auto-detect from API keys
|
|
102
|
+
guardrails:
|
|
103
|
+
loop_detection_window: 5
|
|
104
|
+
max_retries: 3
|
|
105
|
+
replay:
|
|
106
|
+
enabled: true
|
|
107
|
+
trace_dir: .fusiontest/traces
|
|
108
|
+
reverify_every: 10
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## 🔌 CI/CD
|
|
114
|
+
|
|
115
|
+
Run `fusiontest run --headless` in any CI system and cache
|
|
116
|
+
`.fusiontest/traces` so runs can replay. Guides for GitHub Actions, Bitrise,
|
|
117
|
+
Fastlane and Jenkins: [docs/ci-integrations.md](docs/ci-integrations.md).
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## 🤝 Contributing
|
|
122
|
+
|
|
123
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) and [ROADMAP.md](ROADMAP.md).
|
|
124
|
+
|
|
125
|
+
## 📄 License
|
|
126
|
+
|
|
127
|
+
MIT © [FusionLeap.io](https://fusionleap.io)
|
|
@@ -263,8 +263,9 @@ def run(goal, goal_file, platform, app, url, locale, backend, headless, output,
|
|
|
263
263
|
finally:
|
|
264
264
|
progress.stop()
|
|
265
265
|
|
|
266
|
-
# Print results table
|
|
266
|
+
# Print results table, then Slack / GitHub annotations if configured
|
|
267
267
|
_print_results(result)
|
|
268
|
+
_notify(result)
|
|
268
269
|
|
|
269
270
|
# Save JSON report locally
|
|
270
271
|
if output:
|
|
@@ -473,22 +474,19 @@ def init():
|
|
|
473
474
|
detected_backend = "groq" # recommended default (free)
|
|
474
475
|
|
|
475
476
|
config = {
|
|
476
|
-
# backend: which LLM to use — groq | claude | gemini | gpt4o | ollama
|
|
477
|
+
# backend: which LLM to use — groq | claude | gemini | gpt4o | ollama
|
|
477
478
|
# Free options: groq (recommended), gemini, ollama
|
|
478
479
|
# Paid options: claude, gpt4o
|
|
479
480
|
# Leave unset to auto-detect from whichever API key is present.
|
|
480
481
|
"backend": detected_backend,
|
|
481
|
-
"runner": {
|
|
482
|
-
"max_steps_per_goal": 25,
|
|
483
|
-
},
|
|
484
482
|
"guardrails": {
|
|
485
|
-
"loop_detection": True,
|
|
486
483
|
"loop_detection_window": 5,
|
|
487
484
|
"max_retries": 3,
|
|
488
|
-
"backtrack_on_invalid": True,
|
|
489
485
|
},
|
|
490
|
-
"
|
|
491
|
-
"
|
|
486
|
+
"replay": {
|
|
487
|
+
"enabled": True,
|
|
488
|
+
"trace_dir": DEFAULT_TRACE_DIR,
|
|
489
|
+
"reverify_every": 10,
|
|
492
490
|
},
|
|
493
491
|
}
|
|
494
492
|
|
|
@@ -594,6 +592,14 @@ def _exit_code(result) -> int:
|
|
|
594
592
|
_MODE_LABELS = {"llm": "AI", "replayed": "[cyan]replay[/cyan]", "repaired": "[blue]repaired[/blue]"}
|
|
595
593
|
|
|
596
594
|
|
|
595
|
+
def _notify(result) -> None:
|
|
596
|
+
"""Slack (FUSIONTEST_SLACK_WEBHOOK) and GitHub Actions annotations, when configured."""
|
|
597
|
+
import os
|
|
598
|
+
|
|
599
|
+
from fusiontest.reporting.reporter import Reporter
|
|
600
|
+
Reporter(github_annotations=os.getenv("GITHUB_ACTIONS") == "true").notify(result)
|
|
601
|
+
|
|
602
|
+
|
|
597
603
|
def _print_results(result) -> None:
|
|
598
604
|
if result.success:
|
|
599
605
|
status = "[green]PASS ✓[/green]"
|
|
@@ -99,7 +99,6 @@ class RunResult:
|
|
|
99
99
|
steps: list[StepResult] = field(default_factory=list)
|
|
100
100
|
total_duration_seconds: float = 0.0
|
|
101
101
|
bugs_detected: list[str] = field(default_factory=list)
|
|
102
|
-
video_path: str = ""
|
|
103
102
|
cancelled: bool = False
|
|
104
103
|
# Set when the run stopped early because it could not be evaluated
|
|
105
104
|
# (e.g. no LLM backend available). Remaining goals were not run.
|
|
@@ -160,9 +159,6 @@ class RunnerConfig:
|
|
|
160
159
|
max_goal_seconds: int = 180
|
|
161
160
|
# Path to write step-level JSONL logs (used by the training pipeline)
|
|
162
161
|
step_log_path: str | None = None
|
|
163
|
-
# Video recording
|
|
164
|
-
record_video: bool = False
|
|
165
|
-
video_output_dir: str = "fusiontest-reports/videos"
|
|
166
162
|
# Named secrets for {{VAR}} template resolution in goal strings.
|
|
167
163
|
# Keys are variable names, values are secret strings.
|
|
168
164
|
# Env vars are also searched when a key is missing here, unless
|
|
@@ -221,14 +217,6 @@ class FusionTestRunner:
|
|
|
221
217
|
pathlib.Path(self.config.step_log_path).parent.mkdir(parents=True, exist_ok=True)
|
|
222
218
|
self._step_log = open(self.config.step_log_path, "a") # noqa: SIM115
|
|
223
219
|
|
|
224
|
-
self._recorder = None
|
|
225
|
-
if self.config.record_video:
|
|
226
|
-
from fusiontest.recording.recorder import Recorder, RecordingConfig
|
|
227
|
-
self._recorder = Recorder(
|
|
228
|
-
platform=getattr(adapter, "_platform", "unknown"),
|
|
229
|
-
config=RecordingConfig(output_dir=self.config.video_output_dir),
|
|
230
|
-
)
|
|
231
|
-
|
|
232
220
|
def run(
|
|
233
221
|
self,
|
|
234
222
|
goals: list[str],
|
|
@@ -255,12 +243,6 @@ class FusionTestRunner:
|
|
|
255
243
|
run_start = time.time()
|
|
256
244
|
result = RunResult(name=name, platform=platform, locale=locale, success=True, url=url)
|
|
257
245
|
|
|
258
|
-
if self._recorder:
|
|
259
|
-
try:
|
|
260
|
-
self._recorder.start(getattr(self.adapter, "_driver", self.adapter))
|
|
261
|
-
except Exception as e:
|
|
262
|
-
logger.warning(f"Could not start recorder: {e}")
|
|
263
|
-
|
|
264
246
|
logger.info(f"Starting run: {name} ({len(goals)} goals)")
|
|
265
247
|
|
|
266
248
|
# Goals keep their {{VAR}} placeholders everywhere — model prompts, logs,
|
|
@@ -390,14 +372,6 @@ class FusionTestRunner:
|
|
|
390
372
|
)
|
|
391
373
|
break
|
|
392
374
|
|
|
393
|
-
if self._recorder:
|
|
394
|
-
try:
|
|
395
|
-
video_path = self._recorder.stop()
|
|
396
|
-
if video_path:
|
|
397
|
-
result.video_path = str(video_path)
|
|
398
|
-
except Exception as e:
|
|
399
|
-
logger.warning(f"Could not stop recorder: {e}")
|
|
400
|
-
|
|
401
375
|
result.total_duration_seconds = time.time() - run_start
|
|
402
376
|
logger.info(
|
|
403
377
|
f"Run complete: {'PASS' if result.success else 'FAIL'} "
|
|
@@ -14,7 +14,6 @@ Supports:
|
|
|
14
14
|
from __future__ import annotations
|
|
15
15
|
|
|
16
16
|
import asyncio
|
|
17
|
-
import base64
|
|
18
17
|
import logging
|
|
19
18
|
import os
|
|
20
19
|
import re
|
|
@@ -636,47 +635,3 @@ class PlaywrightAdapter:
|
|
|
636
635
|
).first
|
|
637
636
|
|
|
638
637
|
raise ValueError(f"Cannot resolve locator for action: {action}")
|
|
639
|
-
|
|
640
|
-
# ── Vision fallback ────────────────────────────────────────────────────────
|
|
641
|
-
|
|
642
|
-
async def get_vision_description(self) -> str:
|
|
643
|
-
"""
|
|
644
|
-
Fallback for pages with poor accessibility trees.
|
|
645
|
-
Takes a screenshot and returns a GPT-4V/Claude description of the UI.
|
|
646
|
-
"""
|
|
647
|
-
import os
|
|
648
|
-
|
|
649
|
-
screenshot = await self.async_get_screenshot()
|
|
650
|
-
b64 = base64.b64encode(screenshot).decode()
|
|
651
|
-
|
|
652
|
-
# Use whichever vision LLM is configured
|
|
653
|
-
api_key = os.getenv("OPENAI_API_KEY")
|
|
654
|
-
if not api_key:
|
|
655
|
-
return "=== SCREEN STATE ===\n-- Vision fallback not configured --"
|
|
656
|
-
|
|
657
|
-
try:
|
|
658
|
-
from openai import OpenAI
|
|
659
|
-
client = OpenAI(api_key=api_key)
|
|
660
|
-
resp = client.chat.completions.create(
|
|
661
|
-
model="gpt-4o",
|
|
662
|
-
messages=[{
|
|
663
|
-
"role": "user",
|
|
664
|
-
"content": [
|
|
665
|
-
{
|
|
666
|
-
"type": "text",
|
|
667
|
-
"text": (
|
|
668
|
-
"Describe all interactive UI elements visible on this screen. "
|
|
669
|
-
"For each element, note: type (button, input, link, etc.), "
|
|
670
|
-
"visible label or text, and approximate position (top/center/bottom, left/center/right). "
|
|
671
|
-
"Format as a list."
|
|
672
|
-
),
|
|
673
|
-
},
|
|
674
|
-
{"type": "image_url", "image_url": {"url": f"data:image/png;base64,{b64}"}},
|
|
675
|
-
],
|
|
676
|
-
}],
|
|
677
|
-
max_tokens=512,
|
|
678
|
-
)
|
|
679
|
-
return f"=== SCREEN STATE (vision) ===\n{resp.choices[0].message.content}"
|
|
680
|
-
except Exception as e:
|
|
681
|
-
logger.warning(f"Vision fallback failed: {e}")
|
|
682
|
-
return "=== SCREEN STATE ===\n-- Vision fallback error --"
|
|
@@ -64,6 +64,13 @@ class Reporter:
|
|
|
64
64
|
|
|
65
65
|
return report_path
|
|
66
66
|
|
|
67
|
+
def notify(self, result: RunResult) -> None:
|
|
68
|
+
"""Send notifications only (Slack, GitHub annotations) — no files written."""
|
|
69
|
+
if self.slack_webhook:
|
|
70
|
+
self._notify_slack(result)
|
|
71
|
+
if self.github_annotations:
|
|
72
|
+
self._emit_github_annotations(result)
|
|
73
|
+
|
|
67
74
|
# ── JSON ───────────────────────────────────────────────────────────────────
|
|
68
75
|
|
|
69
76
|
def _save_json(self, result: RunResult) -> Path:
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
"""
|
|
2
2
|
fusiontest.training — MPNet fine-tuning pipeline for FusionTest action ranking.
|
|
3
3
|
|
|
4
|
+
EXPERIMENTAL — not wired into runs. The action strings it learns from step
|
|
5
|
+
logs don't match the candidates ActionModel(backend="mpnet") ranks at run
|
|
6
|
+
time, so a model trained here won't help yet. Kept for the distillation plan
|
|
7
|
+
in ADR-001 (amended by ADR-007, which makes replay the main cost lever).
|
|
8
|
+
|
|
4
9
|
Workflow:
|
|
5
10
|
1. Collect — gather (screen_text, goal, action, outcome) tuples from live runs
|
|
6
11
|
2. Build — convert raw logs into contrastive training pairs
|