kernhell 0.1.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.
Potentially problematic release.
This version of kernhell might be problematic. Click here for more details.
- kernhell/__init__.py +8 -0
- kernhell/__main__.py +6 -0
- kernhell/analyzer/analyzer.py +178 -0
- kernhell/analyzer/healer.py +249 -0
- kernhell/analyzer/prompts.py +89 -0
- kernhell/api/__init__.py +5 -0
- kernhell/api/server.py +367 -0
- kernhell/cache/__init__.py +13 -0
- kernhell/cache/cache_manager.py +221 -0
- kernhell/cache/embedder.py +104 -0
- kernhell/cache/schemas.py +42 -0
- kernhell/cli/__init__.py +1 -0
- kernhell/cli/app.py +62 -0
- kernhell/cli/commands/__init__.py +1 -0
- kernhell/cli/commands/cache.py +96 -0
- kernhell/cli/commands/config.py +111 -0
- kernhell/cli/commands/dashboard.py +55 -0
- kernhell/cli/commands/heal.py +141 -0
- kernhell/cli/commands/scan.py +87 -0
- kernhell/cli/ui.py +176 -0
- kernhell/core/__init__.py +1 -0
- kernhell/core/config.py +219 -0
- kernhell/core/exceptions.py +104 -0
- kernhell/core/models.py +174 -0
- kernhell/core/orchestrator.py +245 -0
- kernhell/patcher/patcher.py +215 -0
- kernhell/patcher/verifier.py +82 -0
- kernhell/pruner/__init__.py +1 -0
- kernhell/pruner/dom_pruner.py +240 -0
- kernhell/reporter/__init__.py +5 -0
- kernhell/reporter/reporter.py +124 -0
- kernhell/scanner/__init__.py +1 -0
- kernhell/scanner/parser.py +276 -0
- kernhell/scanner/runner.py +242 -0
- kernhell/utils/__init__.py +1 -0
- kernhell/utils/file_utils.py +117 -0
- kernhell/utils/logger.py +78 -0
- kernhell-0.1.0.dist-info/METADATA +245 -0
- kernhell-0.1.0.dist-info/RECORD +42 -0
- kernhell-0.1.0.dist-info/WHEEL +4 -0
- kernhell-0.1.0.dist-info/entry_points.txt +2 -0
- kernhell-0.1.0.dist-info/licenses/LICENSE +21 -0
kernhell/__init__.py
ADDED
kernhell/__main__.py
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"""Failure Analyzer — classifies Playwright test failures using LLM.
|
|
2
|
+
|
|
3
|
+
Determines whether a selector failure is:
|
|
4
|
+
- RENAME: Element exists, selector refactored (safe to heal).
|
|
5
|
+
- MISSING: Element genuinely absent / application bug (do not heal!).
|
|
6
|
+
- STRUCTURAL: Major UI/flow refactor.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import re
|
|
13
|
+
from typing import Optional
|
|
14
|
+
|
|
15
|
+
import structlog
|
|
16
|
+
|
|
17
|
+
from kernhell.analyzer.prompts import (
|
|
18
|
+
DIAGNOSIS_SYSTEM_PROMPT,
|
|
19
|
+
DIAGNOSIS_USER_PROMPT_TEMPLATE,
|
|
20
|
+
)
|
|
21
|
+
from kernhell.core.config import KernHellConfig
|
|
22
|
+
from kernhell.core.exceptions import AnalyzerError
|
|
23
|
+
from kernhell.core.models import Diagnosis, FailureContext, FailureType, PrunedDOM
|
|
24
|
+
|
|
25
|
+
logger = structlog.get_logger()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _clean_json_response(raw_text: str) -> str:
|
|
29
|
+
"""Extract raw JSON substring from potentially markdown-wrapped LLM output."""
|
|
30
|
+
raw = raw_text.strip()
|
|
31
|
+
# Strip markdown ```json ... ``` wrappers if present
|
|
32
|
+
match = re.search(r"```(?:json)?\s*([\s\S]*?)\s*```", raw)
|
|
33
|
+
if match:
|
|
34
|
+
return match.group(1).strip()
|
|
35
|
+
return raw
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class FailureAnalyzer:
|
|
39
|
+
"""Diagnoses test failure causes using configured LLM provider."""
|
|
40
|
+
|
|
41
|
+
def __init__(self, config: KernHellConfig) -> None:
|
|
42
|
+
self.config = config
|
|
43
|
+
self.llm_cfg = config.llm
|
|
44
|
+
|
|
45
|
+
def diagnose(
|
|
46
|
+
self,
|
|
47
|
+
failure: FailureContext,
|
|
48
|
+
pruned_dom: Optional[PrunedDOM] = None,
|
|
49
|
+
) -> Diagnosis:
|
|
50
|
+
"""Analyze failure context and pruned DOM to diagnose the failure type.
|
|
51
|
+
|
|
52
|
+
Parameters
|
|
53
|
+
----------
|
|
54
|
+
failure : FailureContext
|
|
55
|
+
Failure metadata captured by the scanner.
|
|
56
|
+
pruned_dom : Optional[PrunedDOM]
|
|
57
|
+
Pruned live DOM snapshot of the page.
|
|
58
|
+
|
|
59
|
+
Returns
|
|
60
|
+
-------
|
|
61
|
+
Diagnosis
|
|
62
|
+
Classification (RENAME | MISSING | STRUCTURAL) with confidence & rationale.
|
|
63
|
+
"""
|
|
64
|
+
dom_text = pruned_dom.html if pruned_dom else failure.live_dom
|
|
65
|
+
|
|
66
|
+
logger.info(
|
|
67
|
+
"analyzer.diagnose.start",
|
|
68
|
+
selector=failure.broken_selector,
|
|
69
|
+
dom_length=len(dom_text),
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
# Guardrail: If no DOM was captured, we cannot reliably inspect elements
|
|
73
|
+
if not dom_text:
|
|
74
|
+
return Diagnosis(
|
|
75
|
+
failure_type=FailureType.UNKNOWN,
|
|
76
|
+
explanation="No DOM captured at the time of failure.",
|
|
77
|
+
confidence=0.0,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
prompt_content = DIAGNOSIS_USER_PROMPT_TEMPLATE.format(
|
|
81
|
+
broken_selector=failure.broken_selector,
|
|
82
|
+
selector_type=failure.selector_type.value,
|
|
83
|
+
line_number=failure.line_number,
|
|
84
|
+
test_file=failure.test_file.name,
|
|
85
|
+
surrounding_code=failure.surrounding_code or "# (surrounding code unavailable)",
|
|
86
|
+
pruned_dom=dom_text,
|
|
87
|
+
error_message=failure.error_message,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
try:
|
|
91
|
+
raw_response = self._call_llm(
|
|
92
|
+
system_prompt=DIAGNOSIS_SYSTEM_PROMPT,
|
|
93
|
+
user_prompt=prompt_content,
|
|
94
|
+
)
|
|
95
|
+
clean_json = _clean_json_response(raw_response)
|
|
96
|
+
data = json.loads(clean_json)
|
|
97
|
+
|
|
98
|
+
failure_type_str = data.get("failure_type", "unknown").lower()
|
|
99
|
+
try:
|
|
100
|
+
failure_type = FailureType(failure_type_str)
|
|
101
|
+
except ValueError:
|
|
102
|
+
failure_type = FailureType.UNKNOWN
|
|
103
|
+
|
|
104
|
+
diagnosis = Diagnosis(
|
|
105
|
+
failure_type=failure_type,
|
|
106
|
+
explanation=data.get("explanation", "No explanation provided."),
|
|
107
|
+
confidence=float(data.get("confidence", 0.5)),
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
logger.info(
|
|
111
|
+
"analyzer.diagnose.complete",
|
|
112
|
+
failure_type=diagnosis.failure_type.value,
|
|
113
|
+
confidence=diagnosis.confidence,
|
|
114
|
+
)
|
|
115
|
+
return diagnosis
|
|
116
|
+
|
|
117
|
+
except Exception as exc:
|
|
118
|
+
logger.error("analyzer.diagnose.error", error=str(exc))
|
|
119
|
+
raise AnalyzerError(f"Failed to diagnose test failure via LLM: {exc}") from exc
|
|
120
|
+
|
|
121
|
+
def _call_llm(self, system_prompt: str, user_prompt: str) -> str:
|
|
122
|
+
"""Call the LLM using litellm with fallback for local test execution."""
|
|
123
|
+
# Check if litellm is available
|
|
124
|
+
try:
|
|
125
|
+
import litellm
|
|
126
|
+
except ImportError:
|
|
127
|
+
# Fallback heuristic mode when running offline or in unit tests
|
|
128
|
+
logger.warning("analyzer.litellm_not_installed.using_heuristic")
|
|
129
|
+
return self._heuristic_diagnosis_fallback(user_prompt)
|
|
130
|
+
|
|
131
|
+
# Resolve API key from config or standard provider environment variables (e.g. GEMINI_API_KEY)
|
|
132
|
+
api_key = self.llm_cfg.get_resolved_api_key()
|
|
133
|
+
if not api_key and self.llm_cfg.provider not in ("ollama", "local"):
|
|
134
|
+
logger.warning("analyzer.no_api_key.using_heuristic")
|
|
135
|
+
return self._heuristic_diagnosis_fallback(user_prompt)
|
|
136
|
+
|
|
137
|
+
model_name = self.llm_cfg.get_litellm_model_name()
|
|
138
|
+
|
|
139
|
+
response = litellm.completion(
|
|
140
|
+
model=model_name,
|
|
141
|
+
api_key=api_key,
|
|
142
|
+
temperature=self.llm_cfg.temperature,
|
|
143
|
+
max_tokens=self.llm_cfg.max_tokens,
|
|
144
|
+
messages=[
|
|
145
|
+
{"role": "system", "content": system_prompt},
|
|
146
|
+
{"role": "user", "content": user_prompt},
|
|
147
|
+
],
|
|
148
|
+
)
|
|
149
|
+
return response.choices[0].message.content or ""
|
|
150
|
+
|
|
151
|
+
def _heuristic_diagnosis_fallback(self, user_prompt: str) -> str:
|
|
152
|
+
"""Deterministic heuristic for offline testing without LLM API key."""
|
|
153
|
+
prompt_lower = user_prompt.lower()
|
|
154
|
+
|
|
155
|
+
# If DOM has order/purchase button but test used buy-now -> RENAME
|
|
156
|
+
if ("buy-now" in prompt_lower or "cart" in prompt_lower) and (
|
|
157
|
+
"purchase" in prompt_lower or "place your order" in prompt_lower or "complete-order-btn" in prompt_lower
|
|
158
|
+
):
|
|
159
|
+
return json.dumps({
|
|
160
|
+
"failure_type": "rename",
|
|
161
|
+
"explanation": "Target action exists in DOM but selector changed from #buy-now to a refactored button element.",
|
|
162
|
+
"confidence": 0.95,
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
# If DOM indicates unavailable / missing / error banner -> MISSING
|
|
166
|
+
if "out of stock" in prompt_lower or "unavailable" in prompt_lower or "error" in prompt_lower:
|
|
167
|
+
return json.dumps({
|
|
168
|
+
"failure_type": "missing",
|
|
169
|
+
"explanation": "Element is genuinely absent from page due to out of stock status. This represents an application state change, not a selector rename.",
|
|
170
|
+
"confidence": 0.92,
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
# Default fallback
|
|
174
|
+
return json.dumps({
|
|
175
|
+
"failure_type": "rename",
|
|
176
|
+
"explanation": "Heuristic fallback analysis.",
|
|
177
|
+
"confidence": 0.85,
|
|
178
|
+
})
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
"""Selector Healer — suggests resilient replacement selectors via LLM.
|
|
2
|
+
|
|
3
|
+
Enforces:
|
|
4
|
+
1. Resilience Hierarchy: get_by_role > get_by_text > get_by_test_id > CSS.
|
|
5
|
+
2. DOM Presence Check: Rejects or penalizes hallucinated selectors not present in DOM.
|
|
6
|
+
3. Confidence Threshold Gating: Ensures fixes meet quality standards before application.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import re
|
|
13
|
+
from typing import Optional
|
|
14
|
+
|
|
15
|
+
import structlog
|
|
16
|
+
|
|
17
|
+
from kernhell.analyzer.prompts import (
|
|
18
|
+
HEALING_SYSTEM_PROMPT,
|
|
19
|
+
HEALING_USER_PROMPT_TEMPLATE,
|
|
20
|
+
)
|
|
21
|
+
from kernhell.core.config import KernHellConfig
|
|
22
|
+
from kernhell.core.exceptions import HealerError, LowConfidenceError
|
|
23
|
+
from kernhell.core.models import (
|
|
24
|
+
Diagnosis,
|
|
25
|
+
FailureContext,
|
|
26
|
+
FailureType,
|
|
27
|
+
PrunedDOM,
|
|
28
|
+
SelectorFix,
|
|
29
|
+
SelectorStrategy,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
logger = structlog.get_logger()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _clean_json_response(raw_text: str) -> str:
|
|
36
|
+
"""Extract raw JSON substring from potentially markdown-wrapped LLM output."""
|
|
37
|
+
raw = raw_text.strip()
|
|
38
|
+
match = re.search(r"```(?:json)?\s*([\s\S]*?)\s*```", raw)
|
|
39
|
+
if match:
|
|
40
|
+
return match.group(1).strip()
|
|
41
|
+
return raw
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def verify_dom_presence(new_selector: str, dom_html: str) -> bool:
|
|
45
|
+
"""Check if the proposed selector has matching evidence in the pruned DOM.
|
|
46
|
+
|
|
47
|
+
Anti-Hallucination Guardrail:
|
|
48
|
+
Prevents applying selectors referencing imaginary IDs, classes, or texts.
|
|
49
|
+
"""
|
|
50
|
+
if not dom_html:
|
|
51
|
+
return False
|
|
52
|
+
|
|
53
|
+
dom_lower = dom_html.lower()
|
|
54
|
+
|
|
55
|
+
# 1. Check get_by_test_id("foo") or [data-testid="foo"]
|
|
56
|
+
testid_match = re.search(r"""(?:get_by_test_id|data-testid[=:])\s*\(?["']?([^"'\)]+)""", new_selector)
|
|
57
|
+
if testid_match:
|
|
58
|
+
testid = testid_match.group(1).lower()
|
|
59
|
+
return testid in dom_lower
|
|
60
|
+
|
|
61
|
+
# 2. Check get_by_role("button", name="...")
|
|
62
|
+
role_match = re.search(r"""get_by_role\(\s*["'](\w+)["'](?:\s*,\s*name\s*=\s*["']([^"']+)["'])?""", new_selector)
|
|
63
|
+
if role_match:
|
|
64
|
+
role = role_match.group(1).lower()
|
|
65
|
+
name = role_match.group(2).lower() if role_match.group(2) else ""
|
|
66
|
+
if name:
|
|
67
|
+
return name in dom_lower and (role in dom_lower or f"<{role}" in dom_lower)
|
|
68
|
+
return role in dom_lower or f"<{role}" in dom_lower
|
|
69
|
+
|
|
70
|
+
# 3. Check get_by_text("...")
|
|
71
|
+
text_match = re.search(r"""get_by_text\(\s*["']([^"']+)["']""", new_selector)
|
|
72
|
+
if text_match:
|
|
73
|
+
text = text_match.group(1).lower()
|
|
74
|
+
return text in dom_lower
|
|
75
|
+
|
|
76
|
+
# 4. Check CSS ID selector (#foo)
|
|
77
|
+
if new_selector.startswith("#"):
|
|
78
|
+
element_id = new_selector[1:].strip().lower()
|
|
79
|
+
return f'id="{element_id}"' in dom_lower or f"id='{element_id}'" in dom_lower
|
|
80
|
+
|
|
81
|
+
# 5. Check CSS Class selector (.foo)
|
|
82
|
+
if new_selector.startswith("."):
|
|
83
|
+
class_name = new_selector[1:].strip().lower()
|
|
84
|
+
return class_name in dom_lower
|
|
85
|
+
|
|
86
|
+
# Fallback check: ensure significant selector tokens exist in DOM
|
|
87
|
+
tokens = [t for t in re.split(r"[#.\s\[\]\"'()=]", new_selector) if len(t) > 2]
|
|
88
|
+
return any(token.lower() in dom_lower for token in tokens)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class SelectorHealer:
|
|
92
|
+
"""Proposes resilient replacement Playwright selectors."""
|
|
93
|
+
|
|
94
|
+
def __init__(self, config: KernHellConfig) -> None:
|
|
95
|
+
self.config = config
|
|
96
|
+
self.llm_cfg = config.llm
|
|
97
|
+
self.healing_cfg = config.healing
|
|
98
|
+
|
|
99
|
+
def suggest_fix(
|
|
100
|
+
self,
|
|
101
|
+
failure: FailureContext,
|
|
102
|
+
diagnosis: Diagnosis,
|
|
103
|
+
pruned_dom: Optional[PrunedDOM] = None,
|
|
104
|
+
) -> SelectorFix:
|
|
105
|
+
"""Generate a replacement selector with confidence score.
|
|
106
|
+
|
|
107
|
+
Raises
|
|
108
|
+
------
|
|
109
|
+
HealerError
|
|
110
|
+
If selector generation fails or diagnosis is MISSING.
|
|
111
|
+
LowConfidenceError
|
|
112
|
+
If proposed selector confidence falls below confidence_threshold.
|
|
113
|
+
"""
|
|
114
|
+
# Guardrail: Never heal genuine missing features (real bugs)
|
|
115
|
+
if diagnosis.failure_type == FailureType.MISSING:
|
|
116
|
+
raise HealerError(
|
|
117
|
+
f"Element was diagnosed as genuinely MISSING ({diagnosis.explanation}). "
|
|
118
|
+
"Healing skipped to prevent masking application bugs."
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
dom_text = pruned_dom.html if pruned_dom else failure.live_dom
|
|
122
|
+
|
|
123
|
+
logger.info(
|
|
124
|
+
"healer.suggest_fix.start",
|
|
125
|
+
old_selector=failure.broken_selector,
|
|
126
|
+
strategy_target="resilient_locators",
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
prompt_content = HEALING_USER_PROMPT_TEMPLATE.format(
|
|
130
|
+
broken_selector=failure.broken_selector,
|
|
131
|
+
surrounding_code=failure.surrounding_code or "# (surrounding code unavailable)",
|
|
132
|
+
pruned_dom=dom_text,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
try:
|
|
136
|
+
raw_response = self._call_llm(
|
|
137
|
+
system_prompt=HEALING_SYSTEM_PROMPT,
|
|
138
|
+
user_prompt=prompt_content,
|
|
139
|
+
)
|
|
140
|
+
clean_json = _clean_json_response(raw_response)
|
|
141
|
+
data = json.loads(clean_json)
|
|
142
|
+
|
|
143
|
+
new_selector = data.get("new_selector", "").strip()
|
|
144
|
+
strategy_str = data.get("strategy", "css_fallback").lower()
|
|
145
|
+
confidence = float(data.get("confidence", 0.0))
|
|
146
|
+
reasoning = data.get("reasoning", "")
|
|
147
|
+
|
|
148
|
+
try:
|
|
149
|
+
strategy = SelectorStrategy(strategy_str)
|
|
150
|
+
except ValueError:
|
|
151
|
+
strategy = SelectorStrategy.CSS_FALLBACK
|
|
152
|
+
|
|
153
|
+
# DOM Presence Check Guardrail
|
|
154
|
+
is_present = verify_dom_presence(new_selector, dom_text)
|
|
155
|
+
if not is_present:
|
|
156
|
+
logger.warning(
|
|
157
|
+
"healer.dom_presence_check.failed",
|
|
158
|
+
new_selector=new_selector,
|
|
159
|
+
)
|
|
160
|
+
confidence = max(0.0, confidence - 0.4)
|
|
161
|
+
reasoning += " [Warning: Proposed selector failed strict DOM presence check.]"
|
|
162
|
+
|
|
163
|
+
fix = SelectorFix(
|
|
164
|
+
old_selector=failure.broken_selector,
|
|
165
|
+
new_selector=new_selector,
|
|
166
|
+
strategy=strategy,
|
|
167
|
+
confidence=confidence,
|
|
168
|
+
reasoning=reasoning,
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
# Confidence Threshold Gating
|
|
172
|
+
if fix.confidence < self.healing_cfg.confidence_threshold:
|
|
173
|
+
logger.warning(
|
|
174
|
+
"healer.confidence_below_threshold",
|
|
175
|
+
confidence=fix.confidence,
|
|
176
|
+
threshold=self.healing_cfg.confidence_threshold,
|
|
177
|
+
)
|
|
178
|
+
raise LowConfidenceError(fix.confidence, self.healing_cfg.confidence_threshold)
|
|
179
|
+
|
|
180
|
+
logger.info(
|
|
181
|
+
"healer.suggest_fix.complete",
|
|
182
|
+
old_selector=fix.old_selector,
|
|
183
|
+
new_selector=fix.new_selector,
|
|
184
|
+
confidence=fix.confidence,
|
|
185
|
+
)
|
|
186
|
+
return fix
|
|
187
|
+
|
|
188
|
+
except LowConfidenceError:
|
|
189
|
+
raise
|
|
190
|
+
except Exception as exc:
|
|
191
|
+
logger.error("healer.suggest_fix.error", error=str(exc))
|
|
192
|
+
raise HealerError(f"Failed to generate replacement selector: {exc}") from exc
|
|
193
|
+
|
|
194
|
+
def _call_llm(self, system_prompt: str, user_prompt: str) -> str:
|
|
195
|
+
"""Call LLM via litellm or execute deterministic heuristic fallback."""
|
|
196
|
+
try:
|
|
197
|
+
import litellm
|
|
198
|
+
except ImportError:
|
|
199
|
+
logger.warning("healer.litellm_not_installed.using_heuristic")
|
|
200
|
+
return self._heuristic_healing_fallback(user_prompt)
|
|
201
|
+
|
|
202
|
+
# Resolve API key from config or standard provider environment variables (e.g. GEMINI_API_KEY)
|
|
203
|
+
api_key = self.llm_cfg.get_resolved_api_key()
|
|
204
|
+
if not api_key and self.llm_cfg.provider not in ("ollama", "local"):
|
|
205
|
+
logger.warning("healer.no_api_key.using_heuristic")
|
|
206
|
+
return self._heuristic_healing_fallback(user_prompt)
|
|
207
|
+
|
|
208
|
+
model_name = self.llm_cfg.get_litellm_model_name()
|
|
209
|
+
|
|
210
|
+
response = litellm.completion(
|
|
211
|
+
model=model_name,
|
|
212
|
+
api_key=api_key,
|
|
213
|
+
temperature=self.llm_cfg.temperature,
|
|
214
|
+
max_tokens=self.llm_cfg.max_tokens,
|
|
215
|
+
messages=[
|
|
216
|
+
{"role": "system", "content": system_prompt},
|
|
217
|
+
{"role": "user", "content": user_prompt},
|
|
218
|
+
],
|
|
219
|
+
)
|
|
220
|
+
return response.choices[0].message.content or ""
|
|
221
|
+
|
|
222
|
+
def _heuristic_healing_fallback(self, user_prompt: str) -> str:
|
|
223
|
+
"""Deterministic heuristic fallback when running offline."""
|
|
224
|
+
prompt_lower = user_prompt.lower()
|
|
225
|
+
|
|
226
|
+
# If DOM has purchase-btn or complete-order-btn, return semantic locator
|
|
227
|
+
if "complete-order-btn" in prompt_lower:
|
|
228
|
+
return json.dumps({
|
|
229
|
+
"new_selector": "page.get_by_test_id(\"complete-order-btn\")",
|
|
230
|
+
"strategy": "test_id_match",
|
|
231
|
+
"confidence": 0.96,
|
|
232
|
+
"reasoning": "Element has explicit data-testid='complete-order-btn' in DOM.",
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
if "purchase-btn" in prompt_lower or "place your order" in prompt_lower:
|
|
236
|
+
return json.dumps({
|
|
237
|
+
"new_selector": "page.get_by_role(\"button\", name=\"Place Your Order\")",
|
|
238
|
+
"strategy": "role_match",
|
|
239
|
+
"confidence": 0.95,
|
|
240
|
+
"reasoning": "Element matches button role with visible accessible label 'Place Your Order'.",
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
# Generic fallback
|
|
244
|
+
return json.dumps({
|
|
245
|
+
"new_selector": "#purchase-btn",
|
|
246
|
+
"strategy": "css_fallback",
|
|
247
|
+
"confidence": 0.88,
|
|
248
|
+
"reasoning": "Mapped selector to matching button ID found in DOM.",
|
|
249
|
+
})
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""Prompt engineering templates for KernHell Analyzer and Healer.
|
|
2
|
+
|
|
3
|
+
All prompts are versioned, strictly typed, and instruct the LLM to output
|
|
4
|
+
clean JSON with structured fields so we can validate directly with Pydantic.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
# Current prompt version for telemetry and cache invalidation
|
|
10
|
+
PROMPTS_VERSION = "1.0.0"
|
|
11
|
+
|
|
12
|
+
DIAGNOSIS_SYSTEM_PROMPT = """You are KernHell's Principal Automated Testing Diagnostic Engine.
|
|
13
|
+
Your mission is to examine a failed Playwright automated UI test and determine WHY a selector failed.
|
|
14
|
+
|
|
15
|
+
You must categorize the failure into EXACTLY ONE of the following three categories:
|
|
16
|
+
1. "rename": The element or button still exists in the page, but its CSS class, ID, wrapper, tag, or locator strategy changed during refactoring. This is an intended UI change and SAFE to heal.
|
|
17
|
+
2. "missing": The element is genuinely absent from the page because of a functional change, out-of-stock state, missing API data, or a real application bug. This should NOT be healed — doing so would hide a real bug!
|
|
18
|
+
3. "structural": The layout or workflow changed substantially (e.g., checkout is now a multi-step modal rather than an inline form).
|
|
19
|
+
|
|
20
|
+
You MUST respond ONLY with a valid JSON object matching this schema:
|
|
21
|
+
{
|
|
22
|
+
"failure_type": "rename" | "missing" | "structural",
|
|
23
|
+
"explanation": "concise explanation of why this category was selected",
|
|
24
|
+
"confidence": 0.0 to 1.0
|
|
25
|
+
}
|
|
26
|
+
Do not include any conversational filler or markdown code blocks other than the raw JSON.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
DIAGNOSIS_USER_PROMPT_TEMPLATE = """A Playwright test failed while waiting for selector:
|
|
30
|
+
Broken Selector: {broken_selector}
|
|
31
|
+
Selector Type: {selector_type}
|
|
32
|
+
Failing Test Line: {line_number} in {test_file}
|
|
33
|
+
|
|
34
|
+
Surrounding Test Code:
|
|
35
|
+
```python
|
|
36
|
+
{surrounding_code}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Pruned Live DOM Snapshot (Scripts, styles, and noise stripped):
|
|
40
|
+
```html
|
|
41
|
+
{pruned_dom}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Error Message:
|
|
45
|
+
{error_message}
|
|
46
|
+
|
|
47
|
+
Classify this failure as "rename", "missing", or "structural". Provide confidence score and reasoning.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
HEALING_SYSTEM_PROMPT = """You are KernHell's Surgical Selector Healing Engine.
|
|
51
|
+
Your task is to analyze a broken Playwright selector and the live pruned DOM of the page, then propose the BEST, most resilient replacement Playwright selector.
|
|
52
|
+
|
|
53
|
+
Selector Resilience Hierarchy (prioritize in this exact order):
|
|
54
|
+
1. `page.get_by_role("button" | "link" | "textbox", name="Visible Text")` — Most resilient to CSS/DOM refactors.
|
|
55
|
+
2. `page.get_by_text("Text", exact=True)` — Good for unique buttons or links.
|
|
56
|
+
3. `page.get_by_test_id("...")` or `[data-testid="..."]` — Explicit test hooks.
|
|
57
|
+
4. `page.get_by_label("...")` — For form inputs.
|
|
58
|
+
5. Semantic CSS selector (e.g. `button.order-btn`, `button[type='submit']`) — Last resort when semantic locators are unavailable.
|
|
59
|
+
|
|
60
|
+
CRITICAL RULES:
|
|
61
|
+
- The proposed selector MUST match an element that actually exists in the provided pruned DOM.
|
|
62
|
+
- Never invent nonexistent IDs or attributes.
|
|
63
|
+
- Avoid fragile CSS paths like `div > div:nth-child(3) > span > button`.
|
|
64
|
+
- Return confidence score between 0.0 and 1.0.
|
|
65
|
+
|
|
66
|
+
You MUST respond ONLY with a valid JSON object matching this schema:
|
|
67
|
+
{
|
|
68
|
+
"new_selector": "the replacement selector expression (e.g. page.get_by_role('button', name='Place Your Order') or #purchase-btn)",
|
|
69
|
+
"strategy": "role_match" | "text_match" | "test_id_match" | "aria_match" | "data_attr" | "css_fallback",
|
|
70
|
+
"confidence": 0.0 to 1.0,
|
|
71
|
+
"reasoning": "why this replacement selector was chosen and which element in the DOM it targets"
|
|
72
|
+
}
|
|
73
|
+
Do not include any markdown code fences or conversational text outside of the JSON.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
HEALING_USER_PROMPT_TEMPLATE = """The selector `{broken_selector}` failed and was diagnosed as a selector rename/refactor.
|
|
77
|
+
|
|
78
|
+
Original Intent from Test Code:
|
|
79
|
+
```python
|
|
80
|
+
{surrounding_code}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Live Pruned DOM of the target page:
|
|
84
|
+
```html
|
|
85
|
+
{pruned_dom}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Propose the best resilient replacement selector. Return strictly JSON.
|
|
89
|
+
"""
|