selenium-python-ai-agent 0.2.0__py3-none-any.whl

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 (35) hide show
  1. selenium_agent/__init__.py +20 -0
  2. selenium_agent/agents/__init__.py +5 -0
  3. selenium_agent/agents/coder.py +505 -0
  4. selenium_agent/agents/definitions.py +152 -0
  5. selenium_agent/agents/healer.py +638 -0
  6. selenium_agent/agents/planner.py +279 -0
  7. selenium_agent/bdd/__init__.py +15 -0
  8. selenium_agent/bdd/gherkin_advisor.py +189 -0
  9. selenium_agent/bdd/templates.py +98 -0
  10. selenium_agent/cli.py +314 -0
  11. selenium_agent/core/__init__.py +3 -0
  12. selenium_agent/core/orchestrator.py +189 -0
  13. selenium_agent/scanner/__init__.py +3 -0
  14. selenium_agent/scanner/project_scanner.py +452 -0
  15. selenium_agent/selenium/__init__.py +6 -0
  16. selenium_agent/selenium/base_page.py +447 -0
  17. selenium_agent/selenium/driver_factory.py +222 -0
  18. selenium_agent/selenium/error_map.py +235 -0
  19. selenium_agent/selenium/locator_advisor.py +247 -0
  20. selenium_agent/selenium/locator_scanner.py +502 -0
  21. selenium_agent/utils/__init__.py +26 -0
  22. selenium_agent/utils/code_validator.py +96 -0
  23. selenium_agent/utils/config_manager.py +81 -0
  24. selenium_agent/utils/json_utils.py +103 -0
  25. selenium_agent/utils/llm.py +240 -0
  26. selenium_agent/utils/logger.py +37 -0
  27. selenium_agent/utils/paths.py +57 -0
  28. selenium_agent/utils/spec_writer.py +126 -0
  29. selenium_agent/utils/url_extractor.py +52 -0
  30. selenium_python_ai_agent-0.2.0.dist-info/METADATA +412 -0
  31. selenium_python_ai_agent-0.2.0.dist-info/RECORD +35 -0
  32. selenium_python_ai_agent-0.2.0.dist-info/WHEEL +5 -0
  33. selenium_python_ai_agent-0.2.0.dist-info/entry_points.txt +2 -0
  34. selenium_python_ai_agent-0.2.0.dist-info/licenses/LICENSE +21 -0
  35. selenium_python_ai_agent-0.2.0.dist-info/top_level.txt +1 -0
selenium_agent/cli.py ADDED
@@ -0,0 +1,314 @@
1
+ """CLI Entry Point for Selenium AI Agent"""
2
+
3
+ import argparse
4
+ import json
5
+ import sys
6
+
7
+ # Auto-load .env file if present — so you never need to pass --api-key manually
8
+ try:
9
+ from dotenv import load_dotenv
10
+ load_dotenv()
11
+ except ImportError:
12
+ pass
13
+
14
+ from selenium_agent.core.orchestrator import Orchestrator
15
+ from selenium_agent.utils.logger import setup_logger
16
+ from selenium_agent.utils import config_manager
17
+
18
+ logger = setup_logger("CLI")
19
+
20
+
21
+ def _handle_config(argv):
22
+ """Handle `selenium-agent config [flags]` before main argparse."""
23
+ p = argparse.ArgumentParser(prog="selenium-agent config")
24
+ p.add_argument("--provider", choices=["anthropic", "openai"])
25
+ p.add_argument("--model", help="e.g. gpt-4o-mini, claude-sonnet-4-20250514")
26
+ p.add_argument("--base-url", dest="base_url", help="Default base URL for all tests")
27
+ p.add_argument("--headless", dest="headless", action="store_true", default=None)
28
+ p.add_argument("--no-headless", dest="headless", action="store_false", default=None)
29
+ p.add_argument("--mode", choices=["pytest", "bdd"])
30
+ p.add_argument("--show", action="store_true", help="Print current config")
31
+ args = p.parse_args(argv)
32
+
33
+ if args.show:
34
+ print("\nšŸ“‹ Current config:")
35
+ print(json.dumps(config_manager.load(), indent=2))
36
+ return
37
+
38
+ updates = {k: v for k, v in vars(args).items() if v is not None and k != "show"}
39
+ if not updates:
40
+ p.print_help()
41
+ return
42
+
43
+ path = config_manager.save(updates)
44
+ print(f"\nāœ… Config saved to {path}")
45
+ print(json.dumps(config_manager.load(), indent=2))
46
+
47
+
48
+ def _handle_init_agents(argv):
49
+ """Handle `selenium-agent init-agents [--project PATH]`."""
50
+ p = argparse.ArgumentParser(prog="selenium-agent init-agents")
51
+ p.add_argument("--project", default=".", metavar="PATH",
52
+ help="Project to install the agent definitions into (default: .)")
53
+ args = p.parse_args(argv)
54
+
55
+ from selenium_agent.agents.definitions import write_agent_definitions
56
+ written = write_agent_definitions(args.project)
57
+ print("\nāœ… Claude Code agent definitions installed:")
58
+ for path in written:
59
+ print(f" šŸ“„ {path}")
60
+ print("\nOpen Claude Code in this project and ask e.g.:")
61
+ print(' "use selenium-test-planner to plan tests for https://www.saucedemo.com"')
62
+
63
+
64
+ def _handle_help():
65
+ """Print a friendly cheatsheet of all commands."""
66
+ print("""
67
+ ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
68
+ │ šŸ¤– Selenium Python AI Agent — Command Reference │
69
+ ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜
70
+
71
+ šŸ”§ ONE-TIME SETUP
72
+ selenium-agent config --provider openai --model gpt-4o-mini
73
+ selenium-agent config --base-url https://www.yoursite.com
74
+ selenium-agent config --headless (--no-headless to turn off)
75
+ selenium-agent config --show ← verify saved config
76
+
77
+ šŸš€ GENERATE TESTS (plan + code + heal)
78
+ selenium-agent "test the login page" ← uses saved config & URL
79
+ selenium-agent "test login of saucedemo.com" ← URL auto-detected
80
+ selenium-agent "test login" --url https://myapp.com ← override URL once
81
+ selenium-agent "test login page" --no-heal ← generate only, skip heal
82
+ selenium-agent "test login page" --mode bdd ← BDD/Gherkin output
83
+ selenium-agent "test login page" --headless ← headless browser
84
+ selenium-agent "test login page" --explore 3 ← scan 3 extra pages for locators
85
+ selenium-agent "test login page" --output-dir my_tests/
86
+ selenium-agent "test login page" --max-retries 5
87
+ selenium-agent "test login page" --project /path/to/existing/project
88
+
89
+ šŸ’» PLAN ONLY (saved to specs/<slug>.md + .json for review)
90
+ selenium-agent --plan-only "test the login page"
91
+
92
+ šŸ“‹ GENERATE FROM A SAVED PLAN (review/edit the plan first)
93
+ selenium-agent --from-plan specs/test-the-login-page.json
94
+
95
+ 🩺 HEAL EXISTING TESTS
96
+ selenium-agent --heal-only generated_tests/tests/test_login.py
97
+ selenium-agent --heal-only generated_tests/tests/test_login.py \\
98
+ --test test_login_locked_out_user ← specific test only
99
+ selenium-agent --heal-only generated_tests/tests/test_login.py \\
100
+ --test "locked_out or invalid_password" ← multiple (-k syntax)
101
+
102
+ šŸ” SCAN PROJECT
103
+ selenium-agent --scan /path/to/project
104
+
105
+ šŸ¤ CLAUDE CODE INTEGRATION (like `playwright init-agents`)
106
+ selenium-agent init-agents ← installs .claude/agents/*.md
107
+ selenium-agent init-agents --project /path/to/project
108
+
109
+ āš™ļø CONFIG OPTIONS
110
+ --provider anthropic | openai (default: from config)
111
+ --model e.g. gpt-4o-mini (default: from config)
112
+ --api-key your key (prefer .env instead)
113
+ --url override base URL once (auto-saved to config)
114
+ --mode pytest | bdd (default: pytest)
115
+ --headless headless browser
116
+ --explore N scan N extra same-origin pages while planning
117
+ --output-dir where to save files (default: generated_tests)
118
+ --project path to existing project
119
+ --max-retries healer retries (default: 3)
120
+ --no-heal skip healing
121
+ --plan-only preview plan only (saved to specs/)
122
+ --from-plan generate from saved plan JSON
123
+ --test target specific test (use with --heal-only)
124
+ --version show version
125
+
126
+ šŸ“‚ GENERATED STRUCTURE
127
+ specs/<slug>.md ← reviewable test plan (planner output)
128
+ specs/<slug>.json ← machine-readable plan (generator input)
129
+ generated_tests/
130
+ ā”œā”€ā”€ pages/login_page.py ← Page Object (locators live here)
131
+ ā”œā”€ā”€ tests/test_login.py ← pytest tests (no raw locators)
132
+ └── conftest.py ← driver fixture
133
+ """)
134
+
135
+
136
+ def main():
137
+ # ── subcommands handled before argparse to avoid positional conflicts ──
138
+ if len(sys.argv) > 1 and sys.argv[1] == "config":
139
+ _handle_config(sys.argv[2:])
140
+ return
141
+
142
+ if len(sys.argv) > 1 and sys.argv[1] == "init-agents":
143
+ _handle_init_agents(sys.argv[2:])
144
+ return
145
+
146
+ if len(sys.argv) > 1 and sys.argv[1] == "help":
147
+ _handle_help()
148
+ return
149
+
150
+ parser = argparse.ArgumentParser(
151
+ prog="selenium-agent",
152
+ description="šŸ¤– Selenium Python AI Agent — Plan, Code & Heal tests automatically",
153
+ formatter_class=argparse.RawDescriptionHelpFormatter,
154
+ epilog="""
155
+ One-time setup:
156
+ selenium-agent config --provider openai --model gpt-4o-mini
157
+ selenium-agent config --base-url https://saucedemo.com
158
+ selenium-agent config --show
159
+
160
+ After setup, just run:
161
+ selenium-agent "test the login page"
162
+ selenium-agent "test the checkout flow" ← base_url reused automatically
163
+
164
+ Other examples:
165
+ selenium-agent "test login" --url https://myapp.com ← override URL once
166
+ selenium-agent "test login page" --mode bdd
167
+ selenium-agent --plan-only "test login page"
168
+ selenium-agent --from-plan specs/test-login-page.json
169
+ selenium-agent --heal-only generated_tests/tests/test_login.py
170
+ selenium-agent init-agents ← Claude Code agents (planner/generator/healer)
171
+ """
172
+ )
173
+
174
+ parser.add_argument("instruction", nargs="?",
175
+ help="What to test. E.g: 'test the login page'")
176
+ parser.add_argument("--api-key", default=None,
177
+ help="LLM API key (or set in .env)")
178
+ parser.add_argument("--provider", default=None, choices=["anthropic", "openai"],
179
+ help="LLM provider (default from config)")
180
+ parser.add_argument("--model", default=None, help="Override model")
181
+ parser.add_argument("--mode", default=None, choices=["pytest", "bdd"])
182
+ parser.add_argument("--headless", action="store_true", default=False)
183
+ parser.add_argument("--url", default=None,
184
+ help="Override base URL for this run (also saves to config)")
185
+ parser.add_argument("--project", default=None, metavar="PATH")
186
+ parser.add_argument("--output-dir", default="generated_tests")
187
+ parser.add_argument("--max-retries", type=int, default=None)
188
+ parser.add_argument("--explore", type=int, default=0, metavar="N",
189
+ help="Scan up to N extra same-origin pages while planning")
190
+ parser.add_argument("--no-heal", action="store_true")
191
+ parser.add_argument("--plan-only", action="store_true")
192
+ parser.add_argument("--from-plan", default=None, metavar="FILE",
193
+ help="Generate code from a saved specs/<slug>.json plan")
194
+ parser.add_argument("--scan", default=None, metavar="PATH")
195
+ parser.add_argument("--heal-only", nargs="+", metavar="FILE")
196
+ parser.add_argument("--test", default=None, metavar="TEST_NAME",
197
+ help="Specific test function to run/heal. "
198
+ "e.g. --test test_login_locked_out_user")
199
+ parser.add_argument("--version", action="version", version="selenium-agent 0.2.0")
200
+
201
+ args = parser.parse_args()
202
+
203
+ # ── Scan only ────────────────────────────────────────────────────
204
+ if args.scan:
205
+ from selenium_agent.scanner.project_scanner import ProjectScanner
206
+ print(f"\nšŸ” Scanning: {args.scan}\n")
207
+ try:
208
+ profile = ProjectScanner(args.scan).scan()
209
+ print(profile.to_llm_context())
210
+ except Exception as e:
211
+ print(f"āŒ Scan failed: {e}")
212
+ sys.exit(1)
213
+ sys.exit(0)
214
+
215
+ # ── Merge config + CLI args ───────────────────────────────────────
216
+ cfg = config_manager.get_effective({
217
+ "provider": args.provider,
218
+ "model": args.model,
219
+ "headless": args.headless,
220
+ "mode": args.mode,
221
+ })
222
+
223
+ # URL priority: --url flag > extracted from instruction > saved base_url
224
+ from selenium_agent.utils.url_extractor import extract_url
225
+ override_url = args.url
226
+ if not override_url and args.instruction:
227
+ override_url = extract_url(args.instruction)
228
+ if not override_url:
229
+ override_url = cfg.get("base_url")
230
+ if override_url:
231
+ logger.info(f"🌐 Using saved base_url: {override_url}")
232
+
233
+ # If new URL given via --url, persist it for future runs
234
+ if args.url and args.url != cfg.get("base_url"):
235
+ config_manager.save({"base_url": args.url})
236
+ logger.info(f"šŸ’¾ base_url saved: {args.url}")
237
+
238
+ # ── Build orchestrator ────────────────────────────────────────────
239
+ try:
240
+ agent = Orchestrator(
241
+ api_key=args.api_key,
242
+ provider=cfg["provider"],
243
+ model=cfg.get("model"),
244
+ output_dir=args.output_dir,
245
+ max_heal_retries=args.max_retries or 5,
246
+ auto_heal=not args.no_heal,
247
+ mode=cfg["mode"],
248
+ project_root=args.project,
249
+ headless=cfg["headless"],
250
+ explore_pages=args.explore,
251
+ )
252
+ except ValueError as exc:
253
+ print(f"\nāŒ {exc}\n")
254
+ sys.exit(1)
255
+
256
+ try:
257
+ if args.heal_only:
258
+ result = agent.heal_only(args.heal_only, test_filter=args.test)
259
+ emoji = "āœ…" if result["status"] == "passed" else "āŒ"
260
+ print(f"\n{emoji} {result['status']} (attempts: {result['attempts']})")
261
+ sys.exit(0 if result["status"] == "passed" else 1)
262
+
263
+ if args.from_plan:
264
+ result = agent.run_from_plan(args.from_plan)
265
+ _print_run_result(result)
266
+ sys.exit(0)
267
+
268
+ if not args.instruction:
269
+ parser.print_help()
270
+ sys.exit(1)
271
+
272
+ if args.plan_only:
273
+ plan = agent.plan_only(args.instruction, override_url=override_url)
274
+ spec_files = plan.pop("_spec_files", {})
275
+ print("\nšŸ“‹ TEST PLAN:")
276
+ print(json.dumps(plan, indent=2))
277
+ if spec_files:
278
+ print(f"\nšŸ“„ Saved: {spec_files.get('markdown')}")
279
+ print(f"šŸ“„ Saved: {spec_files.get('json')}")
280
+ print(f"\nReview/edit the plan, then generate with:")
281
+ print(f" selenium-agent --from-plan {spec_files.get('json')}")
282
+ sys.exit(0)
283
+
284
+ result = agent.run(args.instruction, override_url=override_url)
285
+ _print_run_result(result)
286
+ sys.exit(0)
287
+
288
+ except KeyboardInterrupt:
289
+ print("\n\nāš ļø Interrupted")
290
+ sys.exit(1)
291
+ except Exception as e:
292
+ logger.error(f"Fatal: {e}")
293
+ print(f"\nāŒ Error: {e}")
294
+ sys.exit(1)
295
+
296
+
297
+ def _print_run_result(result: dict):
298
+ print(f"\nāœ… Generated {len(result['files'])} files:")
299
+ for f in result["files"]:
300
+ print(f" šŸ“„ {f}")
301
+
302
+ spec = result.get("spec") or {}
303
+ if spec.get("markdown"):
304
+ print(f"\nšŸ“‹ Test plan: {spec['markdown']}")
305
+
306
+ if result.get("heal_result"):
307
+ status = result["heal_result"]["status"]
308
+ attempts = result["heal_result"]["attempts"]
309
+ emoji = "āœ…" if status == "passed" else "āŒ"
310
+ print(f"\n{emoji} Tests {status} after {attempts} attempt(s)")
311
+
312
+
313
+ if __name__ == "__main__":
314
+ main()
@@ -0,0 +1,3 @@
1
+ from selenium_agent.core.orchestrator import Orchestrator
2
+
3
+ __all__ = ["Orchestrator"]
@@ -0,0 +1,189 @@
1
+ """ORCHESTRATOR — Coordinates Planner → Coder (Generator) → Healer
2
+
3
+ Mirrors the Playwright agents workflow:
4
+ plan → specs/<slug>.md + specs/<slug>.json (reviewable artifacts)
5
+ generate → Page Object Model code from a plan (fresh or saved)
6
+ heal → run, live-DOM re-scan, fix, verify — until green
7
+ """
8
+
9
+ from pathlib import Path
10
+
11
+ from selenium_agent.agents.planner import PlannerAgent
12
+ from selenium_agent.agents.coder import CoderAgent
13
+ from selenium_agent.agents.healer import HealerAgent
14
+ from selenium_agent.utils.logger import setup_logger
15
+ from selenium_agent.utils.url_extractor import extract_url
16
+ from selenium_agent.utils.spec_writer import save_spec, load_plan
17
+ from selenium_agent.utils.llm import (
18
+ DEFAULT_PROVIDER, format_missing_api_key_error,
19
+ get_default_model, normalize_provider, resolve_api_key,
20
+ )
21
+
22
+ logger = setup_logger("Orchestrator")
23
+
24
+
25
+ class Orchestrator:
26
+ def __init__(
27
+ self,
28
+ api_key: str = None,
29
+ output_dir: str = "generated_tests",
30
+ max_heal_retries: int = 5,
31
+ auto_heal: bool = True,
32
+ provider: str = DEFAULT_PROVIDER,
33
+ model: str | None = None,
34
+ mode: str = "pytest",
35
+ project_root: str | None = None,
36
+ headless: bool = False,
37
+ explore_pages: int = 0,
38
+ ):
39
+ self.provider = normalize_provider(provider)
40
+ self.model = model or get_default_model(self.provider)
41
+ self.api_key = resolve_api_key(provider=self.provider, api_key=api_key)
42
+ if not self.api_key:
43
+ raise ValueError(format_missing_api_key_error(self.provider))
44
+
45
+ self.output_dir = output_dir
46
+ self.auto_heal = auto_heal
47
+ self.mode = mode if mode in ("pytest", "bdd") else "pytest"
48
+ self.headless = headless
49
+ self.project_root = project_root
50
+ self.explore_pages = explore_pages
51
+
52
+ self.project_profile = None
53
+ if project_root:
54
+ self._scan_project(project_root)
55
+
56
+ self.planner = PlannerAgent(api_key=self.api_key, provider=self.provider, model=self.model)
57
+ self.coder = CoderAgent(api_key=self.api_key, output_dir=self.output_dir,
58
+ provider=self.provider, model=self.model)
59
+ self.healer = HealerAgent(api_key=self.api_key, output_dir=self.output_dir,
60
+ max_retries=max_heal_retries,
61
+ provider=self.provider, model=self.model)
62
+
63
+ def _scan_project(self, project_root: str):
64
+ from selenium_agent.scanner.project_scanner import ProjectScanner
65
+ logger.info(f"šŸ” Scanning: {project_root}")
66
+ try:
67
+ self.project_profile = ProjectScanner(project_root).scan()
68
+ logger.info(
69
+ f"āœ… Detected: {self.project_profile.test_framework} | "
70
+ f"pages={self.project_profile.pages_dir} | "
71
+ f"base={self.project_profile.base_page_class}"
72
+ )
73
+ if self.output_dir == "generated_tests":
74
+ self.output_dir = project_root
75
+ self.coder.output_dir = project_root
76
+ self.healer.output_dir = str(Path(project_root).resolve())
77
+ except Exception as e:
78
+ logger.warning(f"āš ļø Scan failed: {e} — using defaults")
79
+
80
+ # ── Spec persistence (Playwright-planner style) ──────────────────────
81
+
82
+ def _specs_dir(self) -> Path:
83
+ root = Path(self.project_root) if self.project_root else Path.cwd()
84
+ return root / "specs"
85
+
86
+ def _save_spec(self, plan: dict, instruction: str) -> dict:
87
+ try:
88
+ paths = save_spec(plan, instruction, specs_dir=self._specs_dir())
89
+ logger.info(f"šŸ“„ Plan saved: {paths['markdown']} (+ .json twin)")
90
+ return paths
91
+ except Exception as e:
92
+ logger.warning(f"āš ļø Could not save spec files: {e}")
93
+ return {}
94
+
95
+ # ── Pipelines ────────────────────────────────────────────────────────
96
+
97
+ def run(self, instruction: str, override_url: str | None = None) -> dict:
98
+ logger.info("=" * 60)
99
+ logger.info(f"šŸš€ Selenium AI Agent | mode={self.mode.upper()} | headless={self.headless}")
100
+ logger.info(f"šŸ“Œ Task: {instruction}")
101
+ logger.info("=" * 60)
102
+
103
+ # URL priority: --url flag > extracted from instruction > LLM decides
104
+ target_url = override_url or extract_url(instruction)
105
+ if override_url:
106
+ logger.info(f"🌐 URL override (--url flag): {target_url}")
107
+ elif target_url:
108
+ logger.info(f"🌐 Target URL detected: {target_url}")
109
+ else:
110
+ logger.warning("āš ļø No URL detected — LLM will infer from instruction")
111
+
112
+ logger.info("\n🧠 STEP 1: PLANNER")
113
+ plan = self._plan(instruction, target_url)
114
+ spec_paths = self._save_spec(plan, instruction)
115
+
116
+ saved_files, heal_result = self._generate_and_heal(plan)
117
+
118
+ logger.info("\n" + "=" * 60)
119
+ logger.info("šŸŽ‰ DONE!")
120
+ logger.info(f"šŸ“ Files in: {self.output_dir}/")
121
+ logger.info("=" * 60)
122
+
123
+ return {
124
+ "plan": plan,
125
+ "spec": spec_paths,
126
+ "files": saved_files,
127
+ "heal_result": heal_result,
128
+ }
129
+
130
+ def run_from_plan(self, plan_file: str) -> dict:
131
+ """Generate (and heal) directly from a saved specs/<slug>.json plan."""
132
+ plan = load_plan(plan_file)
133
+ if self.headless:
134
+ plan["headless"] = True
135
+ logger.info(f"šŸ“‹ Loaded plan: {plan_file} "
136
+ f"({len(plan.get('test_scenarios') or plan.get('scenarios', []))} scenario(s))")
137
+ saved_files, heal_result = self._generate_and_heal(plan)
138
+ return {"plan": plan, "files": saved_files, "heal_result": heal_result}
139
+
140
+ def plan_only(self, instruction: str, override_url: str | None = None) -> dict:
141
+ target_url = override_url or extract_url(instruction)
142
+ plan = self._plan(instruction, target_url)
143
+ spec_paths = self._save_spec(plan, instruction)
144
+ plan["_spec_files"] = spec_paths
145
+ return plan
146
+
147
+ def code_only(self, plan: dict) -> list:
148
+ return self.coder.code(plan, project_profile=self.project_profile)
149
+
150
+ def heal_only(self, file_paths: list, test_filter: str | None = None) -> dict:
151
+ return self.healer.heal(file_paths, test_filter=test_filter)
152
+
153
+ def scan_only(self, project_root: str) -> str:
154
+ from selenium_agent.scanner.project_scanner import ProjectScanner
155
+ return ProjectScanner(project_root).scan().to_llm_context()
156
+
157
+ # ── Internals ────────────────────────────────────────────────────────
158
+
159
+ def _plan(self, instruction: str, target_url: str | None) -> dict:
160
+ plan = self.planner.plan(
161
+ instruction, mode=self.mode,
162
+ project_profile=self.project_profile,
163
+ headless=self.headless,
164
+ target_url=target_url,
165
+ explore_pages=self.explore_pages,
166
+ )
167
+ # Force URL and headless into plan — LLM cannot override these
168
+ if target_url:
169
+ plan["url"] = target_url
170
+ plan["headless"] = self.headless
171
+ return plan
172
+
173
+ def _generate_and_heal(self, plan: dict) -> tuple[list, dict | None]:
174
+ logger.info("\nšŸ’» STEP 2: GENERATOR")
175
+ try:
176
+ saved_files = self.coder.code(plan, project_profile=self.project_profile)
177
+ except ValueError as e:
178
+ logger.error(f"šŸ’„ Generator failed: {e}")
179
+ logger.error("šŸ’” Tip: Complex multi-page flows may exceed token limits.")
180
+ logger.error(" Try: simplifying the instruction or splitting the plan.")
181
+ raise
182
+
183
+ heal_result = None
184
+ if self.auto_heal:
185
+ logger.info("\n🩺 STEP 3: HEALER")
186
+ heal_result = self.healer.heal(saved_files)
187
+ else:
188
+ logger.info("\nā­ļø Auto-heal skipped")
189
+ return saved_files, heal_result
@@ -0,0 +1,3 @@
1
+ from selenium_agent.scanner.project_scanner import ProjectScanner, ProjectProfile
2
+
3
+ __all__ = ["ProjectScanner", "ProjectProfile"]