selenium-python-ai-agent 0.2.1__py3-none-any.whl → 0.2.2__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.
- selenium_agent/__init__.py +1 -1
- selenium_agent/agents/coder.py +64 -1
- selenium_agent/agents/healer.py +20 -4
- selenium_agent/cli.py +1 -1
- selenium_agent/core/orchestrator.py +4 -2
- selenium_agent/scanner/project_scanner.py +32 -10
- {selenium_python_ai_agent-0.2.1.dist-info → selenium_python_ai_agent-0.2.2.dist-info}/METADATA +1 -1
- {selenium_python_ai_agent-0.2.1.dist-info → selenium_python_ai_agent-0.2.2.dist-info}/RECORD +12 -12
- {selenium_python_ai_agent-0.2.1.dist-info → selenium_python_ai_agent-0.2.2.dist-info}/WHEEL +0 -0
- {selenium_python_ai_agent-0.2.1.dist-info → selenium_python_ai_agent-0.2.2.dist-info}/entry_points.txt +0 -0
- {selenium_python_ai_agent-0.2.1.dist-info → selenium_python_ai_agent-0.2.2.dist-info}/licenses/LICENSE +0 -0
- {selenium_python_ai_agent-0.2.1.dist-info → selenium_python_ai_agent-0.2.2.dist-info}/top_level.txt +0 -0
selenium_agent/__init__.py
CHANGED
selenium_agent/agents/coder.py
CHANGED
|
@@ -245,6 +245,51 @@ Output valid JSON only, no markdown:
|
|
|
245
245
|
]}}
|
|
246
246
|
"""
|
|
247
247
|
|
|
248
|
+
PROJECT_NATIVE_SYSTEM_PROMPT = """
|
|
249
|
+
You are an expert Selenium Python engineer extending an EXISTING test
|
|
250
|
+
framework. The project profile below — especially its sample code — is the
|
|
251
|
+
ONLY architecture reference. You are a guest in this codebase: mirror its
|
|
252
|
+
patterns exactly, never import your own.
|
|
253
|
+
|
|
254
|
+
{project_context}
|
|
255
|
+
|
|
256
|
+
══ HARD RULES ══
|
|
257
|
+
|
|
258
|
+
1. FILE LOCATIONS — write ONLY into the project's own folders:
|
|
259
|
+
page objects → {pages_dir}/
|
|
260
|
+
test files → {tests_dir}/
|
|
261
|
+
FORBIDDEN: creating new top-level folders (generated_tests/, output/, ...).
|
|
262
|
+
|
|
263
|
+
2. ARCHITECTURE — copy the sample page object's style exactly: same base
|
|
264
|
+
class (or none), same helper/service/logger imports, same constructor
|
|
265
|
+
signature, same method style. Do NOT import selenium_agent modules
|
|
266
|
+
unless the samples themselves do.
|
|
267
|
+
|
|
268
|
+
3. DRIVER — the project's conftest.py already provides the fixture named
|
|
269
|
+
'{fixture_name}'. Test functions accept it as a parameter:
|
|
270
|
+
def test_something({fixture_name}):
|
|
271
|
+
NEVER define fixtures, NEVER create drivers, NEVER assume the fixture
|
|
272
|
+
is called 'driver' if the project calls it something else.
|
|
273
|
+
|
|
274
|
+
4. IMPORTS — follow the project's import style shown in the samples
|
|
275
|
+
(e.g. 'from pages.login_page import LoginPage').
|
|
276
|
+
|
|
277
|
+
══ QUALITY RULES (always apply) ══
|
|
278
|
+
- Use ONLY locators from the plan / DOM scan — never invent
|
|
279
|
+
- Unique runtime test data for entity-creating flows:
|
|
280
|
+
import uuid; unique = uuid.uuid4().hex[:8]
|
|
281
|
+
email = "qa." + unique + "@example.com"; password = "Xk9#" + unique + "!Qz"
|
|
282
|
+
- Fill EVERY form field the scan shows, not just the ones named
|
|
283
|
+
- Assert POST-action outcomes (what changes/appears), never pre-action text
|
|
284
|
+
- No time.sleep(), no pytest.skip() placeholders, no TODO steps
|
|
285
|
+
|
|
286
|
+
Output valid JSON only, no markdown:
|
|
287
|
+
{{"files": [
|
|
288
|
+
{{"filename": "{pages_dir}/<name>_page.py", "content": "..."}},
|
|
289
|
+
{{"filename": "{tests_dir}/test_<name>.py", "content": "..."}}
|
|
290
|
+
]}}
|
|
291
|
+
"""
|
|
292
|
+
|
|
248
293
|
REPAIR_PROMPT = """
|
|
249
294
|
The code you generated has validation errors. Fix them and return the
|
|
250
295
|
COMPLETE corrected set of files in the same JSON format.
|
|
@@ -270,7 +315,7 @@ class CoderAgent:
|
|
|
270
315
|
mode = plan.get("mode", "pytest")
|
|
271
316
|
logger.info(f"💻 Generating [{mode.upper()}] code (headless={plan.get('headless', False)})...")
|
|
272
317
|
|
|
273
|
-
system_prompt =
|
|
318
|
+
system_prompt = self._system_prompt_for(mode, project_profile)
|
|
274
319
|
user_prompt = self._build_user_prompt(plan, project_profile, mode)
|
|
275
320
|
max_tokens = self._token_budget(plan)
|
|
276
321
|
|
|
@@ -375,6 +420,24 @@ class CoderAgent:
|
|
|
375
420
|
|
|
376
421
|
# ── Prompt building ────────────────────────────────────────────────
|
|
377
422
|
|
|
423
|
+
@staticmethod
|
|
424
|
+
def _system_prompt_for(mode: str, project_profile=None) -> str:
|
|
425
|
+
"""
|
|
426
|
+
Standalone mode teaches the framework's own BasePage architecture.
|
|
427
|
+
Project mode REPLACES that entirely: the user's existing framework
|
|
428
|
+
(its samples, folders, fixture name) is the only reference —
|
|
429
|
+
supplementing a strong default architecture with a small 'fit in'
|
|
430
|
+
note demonstrably loses to the baked-in style.
|
|
431
|
+
"""
|
|
432
|
+
if project_profile is not None and mode != "bdd":
|
|
433
|
+
return PROJECT_NATIVE_SYSTEM_PROMPT.format(
|
|
434
|
+
project_context=project_profile.to_llm_context(),
|
|
435
|
+
pages_dir=project_profile.pages_dir.rstrip("/"),
|
|
436
|
+
tests_dir=project_profile.tests_dir.rstrip("/"),
|
|
437
|
+
fixture_name=project_profile.driver_fixture_name,
|
|
438
|
+
)
|
|
439
|
+
return CODER_SYSTEM_PROMPT_BDD if mode == "bdd" else CODER_SYSTEM_PROMPT_PYTEST
|
|
440
|
+
|
|
378
441
|
def _build_user_prompt(self, plan: dict, project_profile, mode: str) -> str:
|
|
379
442
|
project_context = ""
|
|
380
443
|
if project_profile:
|
selenium_agent/agents/healer.py
CHANGED
|
@@ -483,7 +483,8 @@ class HealerAgent:
|
|
|
483
483
|
|
|
484
484
|
# ── Main heal loop ─────────────────────────────────────────────────
|
|
485
485
|
|
|
486
|
-
def heal(self, saved_files: list[str], test_filter: str | None = None
|
|
486
|
+
def heal(self, saved_files: list[str], test_filter: str | None = None,
|
|
487
|
+
project_profile=None) -> dict:
|
|
487
488
|
resolved = self._resolve_paths(saved_files)
|
|
488
489
|
|
|
489
490
|
extra = self._auto_discover_related_files(resolved)
|
|
@@ -519,7 +520,8 @@ class HealerAgent:
|
|
|
519
520
|
|
|
520
521
|
logger.warning(f"❌ Failed on attempt {attempt}")
|
|
521
522
|
try:
|
|
522
|
-
applied = self._fix_once(resolved, output, test_filter, scan_cache
|
|
523
|
+
applied = self._fix_once(resolved, output, test_filter, scan_cache,
|
|
524
|
+
project_profile=project_profile)
|
|
523
525
|
except LLMJSONError as exc:
|
|
524
526
|
logger.error(f"💥 Healer LLM returned unusable JSON: {exc}")
|
|
525
527
|
applied = False
|
|
@@ -537,7 +539,8 @@ class HealerAgent:
|
|
|
537
539
|
return {"status": "failed", "attempts": self.max_retries, "output": output}
|
|
538
540
|
|
|
539
541
|
def _fix_once(self, resolved: dict[str, Path], output: str,
|
|
540
|
-
test_filter: str | None, scan_cache: dict[str, str]
|
|
542
|
+
test_filter: str | None, scan_cache: dict[str, str],
|
|
543
|
+
project_profile=None) -> bool:
|
|
541
544
|
"""One LLM fix round. Returns True if at least one file was updated."""
|
|
542
545
|
known_fix = SeleniumErrorMap.get_fix_summary(output)
|
|
543
546
|
|
|
@@ -582,6 +585,18 @@ class HealerAgent:
|
|
|
582
585
|
system_prompt = HEALER_SYSTEM_PROMPT
|
|
583
586
|
target_instruction = ""
|
|
584
587
|
|
|
588
|
+
project_instruction = ""
|
|
589
|
+
if project_profile is not None:
|
|
590
|
+
project_instruction = (
|
|
591
|
+
f"\n\n🏗️ EXISTING-PROJECT MODE — this codebase has its OWN "
|
|
592
|
+
f"architecture. The profile below OVERRIDES the default "
|
|
593
|
+
f"architecture/import rules above: mirror the project's "
|
|
594
|
+
f"patterns exactly, never convert files to the "
|
|
595
|
+
f"selenium_agent BasePage style, and tests use the fixture "
|
|
596
|
+
f"named '{project_profile.driver_fixture_name}'.\n\n"
|
|
597
|
+
f"{project_profile.to_llm_context()}\n"
|
|
598
|
+
)
|
|
599
|
+
|
|
585
600
|
raw = self.client.generate_text(
|
|
586
601
|
system_prompt=system_prompt,
|
|
587
602
|
user_prompt=(
|
|
@@ -589,7 +604,8 @@ class HealerAgent:
|
|
|
589
604
|
f"SELENIUM ERROR ANALYSIS:\n{known_fix}\n\n"
|
|
590
605
|
f"{locator_context}\n"
|
|
591
606
|
f"PYTEST OUTPUT:\n{self._trim_output(output)}\n"
|
|
592
|
-
f"{target_instruction}
|
|
607
|
+
f"{target_instruction}"
|
|
608
|
+
f"{project_instruction}\n"
|
|
593
609
|
f"CURRENT CODE:\n{files_text}"
|
|
594
610
|
),
|
|
595
611
|
max_tokens=8000,
|
selenium_agent/cli.py
CHANGED
|
@@ -201,7 +201,7 @@ Other examples:
|
|
|
201
201
|
parser.add_argument("--test", default=None, metavar="TEST_NAME",
|
|
202
202
|
help="Specific test function to run/heal. "
|
|
203
203
|
"e.g. --test test_login_locked_out_user")
|
|
204
|
-
parser.add_argument("--version", action="version", version="selenium-agent 0.2.
|
|
204
|
+
parser.add_argument("--version", action="version", version="selenium-agent 0.2.2")
|
|
205
205
|
|
|
206
206
|
args = parser.parse_args()
|
|
207
207
|
|
|
@@ -148,7 +148,8 @@ class Orchestrator:
|
|
|
148
148
|
return self.coder.code(plan, project_profile=self.project_profile)
|
|
149
149
|
|
|
150
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
|
|
151
|
+
return self.healer.heal(file_paths, test_filter=test_filter,
|
|
152
|
+
project_profile=self.project_profile)
|
|
152
153
|
|
|
153
154
|
def scan_only(self, project_root: str) -> str:
|
|
154
155
|
from selenium_agent.scanner.project_scanner import ProjectScanner
|
|
@@ -183,7 +184,8 @@ class Orchestrator:
|
|
|
183
184
|
heal_result = None
|
|
184
185
|
if self.auto_heal:
|
|
185
186
|
logger.info("\n🩺 STEP 3: HEALER")
|
|
186
|
-
heal_result = self.healer.heal(saved_files
|
|
187
|
+
heal_result = self.healer.heal(saved_files,
|
|
188
|
+
project_profile=self.project_profile)
|
|
187
189
|
else:
|
|
188
190
|
logger.info("\n⏭️ Auto-heal skipped")
|
|
189
191
|
return saved_files, heal_result
|
|
@@ -54,6 +54,7 @@ class ProjectProfile:
|
|
|
54
54
|
# Driver setup
|
|
55
55
|
driver_setup: str = "fixture" # fixture | setUp | class-level
|
|
56
56
|
driver_scope: str = "function" # function | class | module
|
|
57
|
+
driver_fixture_name: str = "driver" # actual fixture name (e.g. 'browser')
|
|
57
58
|
browser: str = "chrome"
|
|
58
59
|
headless: bool = False
|
|
59
60
|
|
|
@@ -91,7 +92,8 @@ class ProjectProfile:
|
|
|
91
92
|
f"BDD detected : {self.has_bdd}",
|
|
92
93
|
f"Base Page class : {self.base_page_class}",
|
|
93
94
|
f"Base Page import : {self.base_page_import or 'not detected'}",
|
|
94
|
-
f"Driver setup : {self.driver_setup} (scope={self.driver_scope}
|
|
95
|
+
f"Driver setup : {self.driver_setup} (scope={self.driver_scope}, "
|
|
96
|
+
f"fixture name='{self.driver_fixture_name}')",
|
|
95
97
|
f"Browser : {self.browser} (headless={self.headless})",
|
|
96
98
|
f"Test file pattern : {self.test_file_prefix}<name>.py",
|
|
97
99
|
f"Page file pattern : <name>{self.page_file_suffix}.py",
|
|
@@ -169,12 +171,18 @@ class ProjectScanner:
|
|
|
169
171
|
# ── Detection methods ─────────────────────────────────────────────
|
|
170
172
|
|
|
171
173
|
def _detect_folders(self):
|
|
172
|
-
"""Find pages, tests, features, steps directories.
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
174
|
+
"""Find pages, tests, features, steps directories.
|
|
175
|
+
|
|
176
|
+
Shallowest directory wins: a project's real `pages/` at the root
|
|
177
|
+
must beat any deeper `something/pages/` twin."""
|
|
178
|
+
all_dirs = sorted(
|
|
179
|
+
(d for d in self.root.rglob("*")
|
|
180
|
+
if d.is_dir() and not self._is_ignored(d)),
|
|
181
|
+
key=lambda d: len(d.parts),
|
|
182
|
+
)
|
|
183
|
+
dir_names: dict = {}
|
|
184
|
+
for d in all_dirs:
|
|
185
|
+
dir_names.setdefault(d.name.lower(), d)
|
|
178
186
|
|
|
179
187
|
for name in self.PAGES_DIRS:
|
|
180
188
|
if name in dir_names:
|
|
@@ -301,11 +309,19 @@ class ProjectScanner:
|
|
|
301
309
|
continue
|
|
302
310
|
|
|
303
311
|
# Fixture pattern
|
|
304
|
-
if "@pytest.fixture" in content and "driver" in content:
|
|
312
|
+
if "@pytest.fixture" in content and "driver" in content.lower():
|
|
305
313
|
self.profile.driver_setup = "fixture"
|
|
306
314
|
scope_match = re.search(r'@pytest\.fixture\s*\(\s*scope=["\'](\w+)["\']', content)
|
|
307
315
|
if scope_match:
|
|
308
316
|
self.profile.driver_scope = scope_match.group(1)
|
|
317
|
+
# The fixture NAME matters — generated tests must request the
|
|
318
|
+
# project's actual fixture ('browser', 'web_driver', ...),
|
|
319
|
+
# not assume it is called 'driver'.
|
|
320
|
+
name_match = re.search(
|
|
321
|
+
r"@pytest\.fixture(?:\([^)]*\))?\s*\ndef\s+(\w+)\s*\(", content
|
|
322
|
+
)
|
|
323
|
+
if name_match and f.name == "conftest.py":
|
|
324
|
+
self.profile.driver_fixture_name = name_match.group(1)
|
|
309
325
|
|
|
310
326
|
# unittest setUp pattern
|
|
311
327
|
elif "def setUp(self)" in content:
|
|
@@ -445,8 +461,14 @@ class ProjectScanner:
|
|
|
445
461
|
continue
|
|
446
462
|
|
|
447
463
|
def _is_ignored(self, path: Path) -> bool:
|
|
448
|
-
"""Skip venv, cache, git, node_modules etc.
|
|
464
|
+
"""Skip venv, cache, git, node_modules etc.
|
|
465
|
+
|
|
466
|
+
Also skips the agent's OWN output folders (generated_tests/, specs/):
|
|
467
|
+
scanning them would make the profile describe the agent's previous
|
|
468
|
+
output instead of the user's real framework — circular pollution.
|
|
469
|
+
"""
|
|
449
470
|
ignored = {".venv", "venv", "__pycache__", ".git", ".idea",
|
|
450
471
|
"node_modules", ".pytest_cache", "dist", "build",
|
|
451
|
-
".eggs", "*.egg-info", ".tox", ".mypy_cache"
|
|
472
|
+
".eggs", "*.egg-info", ".tox", ".mypy_cache",
|
|
473
|
+
"generated_tests", "specs"}
|
|
452
474
|
return any(part in ignored for part in path.parts)
|
{selenium_python_ai_agent-0.2.1.dist-info → selenium_python_ai_agent-0.2.2.dist-info}/RECORD
RENAMED
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
selenium_agent/__init__.py,sha256=
|
|
2
|
-
selenium_agent/cli.py,sha256
|
|
1
|
+
selenium_agent/__init__.py,sha256=Tz7CXR4KoDc5_KZv7geXV7zZ9UKAyEgE2uTbLPmcfic,603
|
|
2
|
+
selenium_agent/cli.py,sha256=l0MU95DYB_4zyihe5QhdrcvwsVDFXfXCj-TYLHBpsEA,15435
|
|
3
3
|
selenium_agent/agents/__init__.py,sha256=7QaYnHxDYyTrmXVdiMXy12FpDhtWpCOnj2UkR22-k1Y,216
|
|
4
|
-
selenium_agent/agents/coder.py,sha256
|
|
4
|
+
selenium_agent/agents/coder.py,sha256=-5WR9RSsAWBxB2R97JcUdiAuQqV_aZnEbTKUOgoNdc8,24313
|
|
5
5
|
selenium_agent/agents/definitions.py,sha256=RodYbSq6ZsUgwxmse3nPZRyAPJ5SW2kQONaZjaZL_pU,5366
|
|
6
|
-
selenium_agent/agents/healer.py,sha256=
|
|
6
|
+
selenium_agent/agents/healer.py,sha256=834aAEXprm6Rmdkgv9AcwzzkvndHEHqEUN0BUtbtfXQ,29151
|
|
7
7
|
selenium_agent/agents/planner.py,sha256=Qs3L0-5eagGAMOcicONjkVhayyvRMxX6F7G9OHo1FyI,10433
|
|
8
8
|
selenium_agent/bdd/__init__.py,sha256=0fsSGdNXcNy9Vjmt-KpG8Dx_IHE07UpmNT3abBEuwNk,358
|
|
9
9
|
selenium_agent/bdd/gherkin_advisor.py,sha256=sRnYOMWejCZ17xgpYNgh5nFnJ4hZ5LgGZii7zRKufVI,6873
|
|
10
10
|
selenium_agent/bdd/templates.py,sha256=yQ6ssK0cRpfOXz5mIuNNUY7yrBQtnQmGw7F8Kj8Vlu0,2105
|
|
11
11
|
selenium_agent/core/__init__.py,sha256=hdKa4Oxd99FGeHpeYsUoEkRgqdvyN6JxVX_i4SOVcVQ,86
|
|
12
|
-
selenium_agent/core/orchestrator.py,sha256=
|
|
12
|
+
selenium_agent/core/orchestrator.py,sha256=bBGtkQMKUSFHcOgJiZKoBBTt2a1GvRlvXlO9vSeFNzQ,8362
|
|
13
13
|
selenium_agent/scanner/__init__.py,sha256=RIsY5NnOEMC-2FBOTTeueA53ei2tIgiKb3oCP4LmSlw,130
|
|
14
|
-
selenium_agent/scanner/project_scanner.py,sha256=
|
|
14
|
+
selenium_agent/scanner/project_scanner.py,sha256=oUAMbYBIoOedGfUFmVSnyQ8KzOtzIowOuQ8y9Zbz7kw,18783
|
|
15
15
|
selenium_agent/selenium/__init__.py,sha256=ENE_t5NwiLbD1Hzry3f1DWnkMkoRGa0eop-UHmJMfos,329
|
|
16
16
|
selenium_agent/selenium/base_page.py,sha256=ChrTtXM35QjsQ74seyudHOWYaOeliXdOjCmODM54bwA,18105
|
|
17
17
|
selenium_agent/selenium/driver_factory.py,sha256=o6b1NgAbpPZHbmfFqy_Gk4DHGX3bcW2MepdxcfiMOPI,8927
|
|
@@ -27,9 +27,9 @@ selenium_agent/utils/logger.py,sha256=wIttutxhMXkADarPgad3PjyzD3hXwN7GzY3phsa07W
|
|
|
27
27
|
selenium_agent/utils/paths.py,sha256=UWz30G7H86gD5DHPfwaHky3iuw5kfD4nJj-PilU98so,1864
|
|
28
28
|
selenium_agent/utils/spec_writer.py,sha256=HT86mRKoe063lM9kR7xUb1R4GaBZEqE0VWi4L0uBc1o,4552
|
|
29
29
|
selenium_agent/utils/url_extractor.py,sha256=ibRUeQdmA8tMV89340WH9G8SVPppsOzX-l1QinMnV0I,1684
|
|
30
|
-
selenium_python_ai_agent-0.2.
|
|
31
|
-
selenium_python_ai_agent-0.2.
|
|
32
|
-
selenium_python_ai_agent-0.2.
|
|
33
|
-
selenium_python_ai_agent-0.2.
|
|
34
|
-
selenium_python_ai_agent-0.2.
|
|
35
|
-
selenium_python_ai_agent-0.2.
|
|
30
|
+
selenium_python_ai_agent-0.2.2.dist-info/licenses/LICENSE,sha256=MjRANRVeUVoTsw3zJoK-NiHLwTKjMHNl9QrG1l8u4ds,1071
|
|
31
|
+
selenium_python_ai_agent-0.2.2.dist-info/METADATA,sha256=B5FPPDEPtUEDN8LupKPLhuicdEBn2rRImxbyrWvdBws,20136
|
|
32
|
+
selenium_python_ai_agent-0.2.2.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
33
|
+
selenium_python_ai_agent-0.2.2.dist-info/entry_points.txt,sha256=fZIhpEO_RKRV-t-rahZgQ1XXzlQN2i9zas5CmJKNF5k,59
|
|
34
|
+
selenium_python_ai_agent-0.2.2.dist-info/top_level.txt,sha256=D7yM-RyAopuwik-nPbvgT6hXagm0aX0U5FQkFDW3yA8,15
|
|
35
|
+
selenium_python_ai_agent-0.2.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{selenium_python_ai_agent-0.2.1.dist-info → selenium_python_ai_agent-0.2.2.dist-info}/top_level.txt
RENAMED
|
File without changes
|