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
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Schemas for the Semantic Vector Cache."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel, Field
|
|
10
|
+
|
|
11
|
+
from kernhell.core.models import SelectorStrategy
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class CacheEntry(BaseModel):
|
|
15
|
+
"""A verified selector healing record stored in semantic memory."""
|
|
16
|
+
|
|
17
|
+
id: str # Hash of selector + context
|
|
18
|
+
broken_selector: str
|
|
19
|
+
new_selector: str
|
|
20
|
+
strategy: SelectorStrategy = SelectorStrategy.CSS_FALLBACK
|
|
21
|
+
confidence: float = Field(ge=0.0, le=1.0)
|
|
22
|
+
context_snippet: str = ""
|
|
23
|
+
page_url: str = ""
|
|
24
|
+
test_file: str = ""
|
|
25
|
+
verified: bool = True
|
|
26
|
+
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
|
27
|
+
hit_count: int = 0
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class CacheStats(BaseModel):
|
|
31
|
+
"""Aggregate statistics for the semantic vector cache."""
|
|
32
|
+
|
|
33
|
+
total_entries: int = 0
|
|
34
|
+
hits: int = 0
|
|
35
|
+
misses: int = 0
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def hit_ratio(self) -> float:
|
|
39
|
+
total = self.hits + self.misses
|
|
40
|
+
if total == 0:
|
|
41
|
+
return 0.0
|
|
42
|
+
return round((self.hits / total) * 100, 1)
|
kernhell/cli/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""CLI package — Typer commands and Rich UI helpers."""
|
kernhell/cli/app.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Main CLI application entrypoint for KernHell."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
if sys.platform == "win32":
|
|
6
|
+
if hasattr(sys.stdout, "reconfigure"):
|
|
7
|
+
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
8
|
+
if hasattr(sys.stderr, "reconfigure"):
|
|
9
|
+
sys.stderr.reconfigure(encoding="utf-8", errors="replace")
|
|
10
|
+
|
|
11
|
+
import typer
|
|
12
|
+
from rich.console import Console
|
|
13
|
+
|
|
14
|
+
from kernhell import __app_name__, __version__
|
|
15
|
+
from kernhell.cli.commands.cache import cache_app
|
|
16
|
+
from kernhell.cli.commands.config import config_app
|
|
17
|
+
from kernhell.cli.commands.dashboard import dashboard_command
|
|
18
|
+
from kernhell.cli.commands.heal import heal_command
|
|
19
|
+
from kernhell.cli.commands.scan import scan_command
|
|
20
|
+
|
|
21
|
+
console = Console(stderr=True)
|
|
22
|
+
|
|
23
|
+
app = typer.Typer(
|
|
24
|
+
name=__app_name__,
|
|
25
|
+
help="KernHell — AI-Powered Autonomous Self-Healing Testing Engine.\n\n"
|
|
26
|
+
"'The Heart of Your System Where Innovation Meets Precision'",
|
|
27
|
+
add_completion=False,
|
|
28
|
+
no_args_is_help=True,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
# Register core commands
|
|
32
|
+
app.command(name="scan", help="Scan a test file to detect broken selectors and capture DOM snapshots.")(scan_command)
|
|
33
|
+
app.command(name="heal", help="Autonomously heal broken Playwright selectors in a test file.")(heal_command)
|
|
34
|
+
app.command(name="dashboard", help="Launch the KernHell Mission Control Dashboard and REST API.")(dashboard_command)
|
|
35
|
+
app.command(name="ui", help="Alias for 'dashboard' command.")(dashboard_command)
|
|
36
|
+
app.add_typer(cache_app, name="cache")
|
|
37
|
+
app.add_typer(config_app, name="config")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def version_callback(value: bool) -> None:
|
|
41
|
+
if value:
|
|
42
|
+
console.print(f"[bold red]KernHell[/bold red] version [cyan]{__version__}[/cyan]")
|
|
43
|
+
raise typer.Exit()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@app.callback()
|
|
47
|
+
def main(
|
|
48
|
+
version: bool = typer.Option(
|
|
49
|
+
False,
|
|
50
|
+
"--version",
|
|
51
|
+
"-V",
|
|
52
|
+
help="Show KernHell version and exit.",
|
|
53
|
+
callback=version_callback,
|
|
54
|
+
is_eager=True,
|
|
55
|
+
),
|
|
56
|
+
) -> None:
|
|
57
|
+
"""KernHell CLI entrypoint callback."""
|
|
58
|
+
pass
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
if __name__ == "__main__":
|
|
62
|
+
app()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""CLI commands package."""
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""CLI commands for managing the Semantic Vector Cache: `kernhell cache <subcommand>`."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Optional
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
from rich.table import Table
|
|
11
|
+
from rich import box
|
|
12
|
+
|
|
13
|
+
from kernhell.cache.cache_manager import CacheManager
|
|
14
|
+
from kernhell.core.config import load_config
|
|
15
|
+
from kernhell.utils.logger import setup_logging
|
|
16
|
+
|
|
17
|
+
console = Console(stderr=True)
|
|
18
|
+
|
|
19
|
+
cache_app = typer.Typer(
|
|
20
|
+
name="cache",
|
|
21
|
+
help="Inspect, query, and manage KernHell's Semantic Vector Cache.",
|
|
22
|
+
no_args_is_help=True,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@cache_app.command(name="list", help="List all verified healed selector mappings in cache.")
|
|
27
|
+
def list_cache(
|
|
28
|
+
config_path: Optional[Path] = typer.Option(None, "--config", "-c"),
|
|
29
|
+
) -> None:
|
|
30
|
+
cfg = load_config(config_path)
|
|
31
|
+
setup_logging(level=cfg.logging.level)
|
|
32
|
+
mgr = CacheManager(cfg)
|
|
33
|
+
|
|
34
|
+
entries = mgr.list_entries()
|
|
35
|
+
if not entries:
|
|
36
|
+
console.print("[yellow]Semantic Cache is currently empty. Run `kernhell heal` to populate.[/yellow]")
|
|
37
|
+
return
|
|
38
|
+
|
|
39
|
+
table = Table(title="🧠 KernHell Semantic Memory Cache", box=box.ROUNDED)
|
|
40
|
+
table.add_column("Original Selector", style="red")
|
|
41
|
+
table.add_column("Healed Selector", style="bold green")
|
|
42
|
+
table.add_column("Strategy", style="cyan")
|
|
43
|
+
table.add_column("Confidence", justify="right")
|
|
44
|
+
table.add_column("Hits", justify="right")
|
|
45
|
+
|
|
46
|
+
for e in entries:
|
|
47
|
+
table.add_row(
|
|
48
|
+
e.broken_selector,
|
|
49
|
+
e.new_selector,
|
|
50
|
+
e.strategy.value,
|
|
51
|
+
f"{int(e.confidence * 100)}%",
|
|
52
|
+
str(e.hit_count),
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
console.print(table)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@cache_app.command(name="stats", help="Show cache hit/miss metrics and efficiency ratio.")
|
|
59
|
+
def stats_cache(
|
|
60
|
+
config_path: Optional[Path] = typer.Option(None, "--config", "-c"),
|
|
61
|
+
) -> None:
|
|
62
|
+
cfg = load_config(config_path)
|
|
63
|
+
setup_logging(level=cfg.logging.level)
|
|
64
|
+
mgr = CacheManager(cfg)
|
|
65
|
+
|
|
66
|
+
stats = mgr.get_stats()
|
|
67
|
+
table = Table(title="📊 Semantic Memory Metrics", box=box.ROUNDED)
|
|
68
|
+
table.add_column("Metric", style="bold")
|
|
69
|
+
table.add_column("Value", justify="right")
|
|
70
|
+
|
|
71
|
+
table.add_row("Total Entries", str(stats.total_entries))
|
|
72
|
+
table.add_row("Cache Hits", f"[bold green]{stats.hits}[/bold green]")
|
|
73
|
+
table.add_row("Cache Misses", str(stats.misses))
|
|
74
|
+
table.add_row("Hit Efficiency", f"[bold cyan]{stats.hit_ratio}%[/bold cyan]")
|
|
75
|
+
table.add_row("Storage Path", str(mgr.storage_dir))
|
|
76
|
+
|
|
77
|
+
console.print(table)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@cache_app.command(name="clear", help="Purge all entries from the semantic cache.")
|
|
81
|
+
def clear_cache(
|
|
82
|
+
config_path: Optional[Path] = typer.Option(None, "--config", "-c"),
|
|
83
|
+
force: bool = typer.Option(False, "--force", "-f", help="Bypass confirmation prompt."),
|
|
84
|
+
) -> None:
|
|
85
|
+
cfg = load_config(config_path)
|
|
86
|
+
setup_logging(level=cfg.logging.level)
|
|
87
|
+
mgr = CacheManager(cfg)
|
|
88
|
+
|
|
89
|
+
if not force:
|
|
90
|
+
confirm = typer.confirm("Are you sure you want to purge the semantic vector cache?")
|
|
91
|
+
if not confirm:
|
|
92
|
+
console.print("[dim]Operation cancelled.[/dim]")
|
|
93
|
+
return
|
|
94
|
+
|
|
95
|
+
count = mgr.clear()
|
|
96
|
+
console.print(f"[bold green]✔ Successfully cleared {count} entries from semantic cache.[/bold green]")
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""CLI commands for configuration inspection and setup: `kernhell config <subcommand>`."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
from rich import box
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
from rich.table import Table
|
|
12
|
+
|
|
13
|
+
from kernhell.core.config import KernHellConfig, load_config
|
|
14
|
+
from kernhell.utils.logger import setup_logging
|
|
15
|
+
|
|
16
|
+
console = Console(stderr=True)
|
|
17
|
+
|
|
18
|
+
config_app = typer.Typer(
|
|
19
|
+
name="config",
|
|
20
|
+
help="View and configure KernHell engine parameters.",
|
|
21
|
+
no_args_is_help=True,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@config_app.command(name="show", help="Display active configuration settings and source paths.")
|
|
26
|
+
def show_config(
|
|
27
|
+
config_path: Optional[Path] = typer.Option(None, "--config", "-c"),
|
|
28
|
+
) -> None:
|
|
29
|
+
cfg = load_config(config_path)
|
|
30
|
+
setup_logging(level=cfg.logging.level)
|
|
31
|
+
|
|
32
|
+
table = Table(title="⚙️ KernHell Active Configuration", box=box.ROUNDED)
|
|
33
|
+
table.add_column("Section", style="bold cyan")
|
|
34
|
+
table.add_column("Setting", style="yellow")
|
|
35
|
+
table.add_column("Value")
|
|
36
|
+
|
|
37
|
+
# LLM
|
|
38
|
+
table.add_row("LLM", "Provider", cfg.llm.provider)
|
|
39
|
+
table.add_row("LLM", "Model", cfg.llm.model)
|
|
40
|
+
table.add_row("LLM", "API Key", "******" if cfg.llm.api_key else "[dim](not set, using local heuristic)[/dim]")
|
|
41
|
+
table.add_row("LLM", "Temperature", str(cfg.llm.temperature))
|
|
42
|
+
|
|
43
|
+
# Healing
|
|
44
|
+
table.add_row("Healing", "Confidence Threshold", f"{int(cfg.healing.confidence_threshold * 100)}%")
|
|
45
|
+
table.add_row("Healing", "Max Heals / File", str(cfg.healing.max_heals_per_file))
|
|
46
|
+
table.add_row("Healing", "Dry Run Default", str(cfg.healing.dry_run))
|
|
47
|
+
|
|
48
|
+
# Cache
|
|
49
|
+
table.add_row("Cache", "Enabled", str(cfg.cache.enabled))
|
|
50
|
+
table.add_row("Cache", "Similarity Threshold", f"{int(cfg.cache.similarity_threshold * 100)}%")
|
|
51
|
+
table.add_row("Cache", "Cache Directory", cfg.cache.cache_dir)
|
|
52
|
+
table.add_row("Cache", "Embedding Model", cfg.cache.embedding_model)
|
|
53
|
+
|
|
54
|
+
# Scanner
|
|
55
|
+
table.add_row("Scanner", "Browser", cfg.scanner.browser)
|
|
56
|
+
table.add_row("Scanner", "Timeout (ms)", str(cfg.scanner.timeout_ms))
|
|
57
|
+
table.add_row("Scanner", "Capture Screenshot", str(cfg.scanner.capture_screenshot))
|
|
58
|
+
|
|
59
|
+
console.print(table)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@config_app.command(name="init", help="Interactive setup wizard to initialize .kernhellrc.toml.")
|
|
63
|
+
def init_config(
|
|
64
|
+
target_path: Path = typer.Option(Path(".kernhellrc.toml"), "--path", "-p", help="Target config file path."),
|
|
65
|
+
) -> None:
|
|
66
|
+
console.print("[bold red]KernHell Engine Configuration Wizard[/bold red]\n")
|
|
67
|
+
|
|
68
|
+
provider = typer.prompt("Select LLM Provider (groq, openai, anthropic, ollama)", default="groq")
|
|
69
|
+
model = typer.prompt(
|
|
70
|
+
"Default Model Name",
|
|
71
|
+
default="llama-3.3-70b-versatile" if provider == "groq" else "gpt-4o",
|
|
72
|
+
)
|
|
73
|
+
api_key = typer.prompt("LLM API Key (press Enter to skip for local/env)", default="", hide_input=True)
|
|
74
|
+
confidence = typer.prompt("Confidence Threshold for auto-healing (0.0 to 1.0)", default=0.85, type=float)
|
|
75
|
+
browser = typer.prompt("Playwright Browser (chromium, firefox, webkit)", default="chromium")
|
|
76
|
+
|
|
77
|
+
toml_content = f"""# KernHell Configuration File
|
|
78
|
+
# Generated via `kernhell config init`
|
|
79
|
+
|
|
80
|
+
[llm]
|
|
81
|
+
provider = "{provider}"
|
|
82
|
+
model = "{model}"
|
|
83
|
+
{"api_key = \"" + api_key + "\"" if api_key else "# api_key = \"\""}
|
|
84
|
+
temperature = 0.1
|
|
85
|
+
max_tokens = 2048
|
|
86
|
+
|
|
87
|
+
[healing]
|
|
88
|
+
confidence_threshold = {confidence}
|
|
89
|
+
max_heals_per_file = 5
|
|
90
|
+
dry_run = false
|
|
91
|
+
auto_commit = false
|
|
92
|
+
|
|
93
|
+
[cache]
|
|
94
|
+
enabled = true
|
|
95
|
+
similarity_threshold = 0.92
|
|
96
|
+
cache_dir = "~/.kernhell/cache"
|
|
97
|
+
embedding_model = "all-MiniLM-L6-v2"
|
|
98
|
+
|
|
99
|
+
[scanner]
|
|
100
|
+
timeout_ms = 5000
|
|
101
|
+
capture_screenshot = true
|
|
102
|
+
browser = "{browser}"
|
|
103
|
+
headless = true
|
|
104
|
+
|
|
105
|
+
[logging]
|
|
106
|
+
level = "INFO"
|
|
107
|
+
json_output = false
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
target_path.write_text(toml_content.strip() + "\n", encoding="utf-8")
|
|
111
|
+
console.print(f"\n[bold green]✔ Configuration successfully written to {target_path.resolve()}[/bold green]")
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""CLI command: `kernhell dashboard` (or `kernhell ui`).
|
|
2
|
+
|
|
3
|
+
Launches the local KernHell Mission Control Dashboard and API service.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import webbrowser
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
import typer
|
|
13
|
+
import uvicorn
|
|
14
|
+
from rich.console import Console
|
|
15
|
+
from rich.panel import Panel
|
|
16
|
+
|
|
17
|
+
from kernhell.api.server import create_app
|
|
18
|
+
from kernhell.cli.ui import print_banner
|
|
19
|
+
from kernhell.core.config import load_config
|
|
20
|
+
from kernhell.utils.logger import setup_logging
|
|
21
|
+
|
|
22
|
+
console = Console(stderr=True)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def dashboard_command(
|
|
26
|
+
host: str = typer.Option("127.0.0.1", "--host", "-h", help="Bind host address."),
|
|
27
|
+
port: int = typer.Option(8000, "--port", "-p", help="Bind port number."),
|
|
28
|
+
no_open: bool = typer.Option(False, "--no-open", help="Do not automatically open browser."),
|
|
29
|
+
config_path: Optional[Path] = typer.Option(None, "--config", "-c"),
|
|
30
|
+
) -> None:
|
|
31
|
+
"""Launch the KernHell Mission Control Dashboard and REST API service."""
|
|
32
|
+
cfg = load_config(config_path)
|
|
33
|
+
setup_logging(level=cfg.logging.level)
|
|
34
|
+
|
|
35
|
+
print_banner()
|
|
36
|
+
|
|
37
|
+
url = f"http://{host}:{port}"
|
|
38
|
+
web_url = "http://localhost:3000"
|
|
39
|
+
|
|
40
|
+
panel_content = (
|
|
41
|
+
f"[bold cyan]Mission Control API:[/bold cyan] [underline]{url}[/underline]\n"
|
|
42
|
+
f"[bold cyan]Swagger API Docs:[/bold cyan] [underline]{url}/docs[/underline]\n"
|
|
43
|
+
f"[bold cyan]Web Dashboard UI:[/bold cyan] [underline]{web_url}[/underline]\n\n"
|
|
44
|
+
f"[dim]Press Ctrl+C to terminate the dashboard server.[/dim]"
|
|
45
|
+
)
|
|
46
|
+
console.print(Panel(panel_content, title="🚀 Mission Control Active", border_style="cyan"))
|
|
47
|
+
|
|
48
|
+
if not no_open:
|
|
49
|
+
try:
|
|
50
|
+
webbrowser.open(f"{url}/docs")
|
|
51
|
+
except Exception:
|
|
52
|
+
pass
|
|
53
|
+
|
|
54
|
+
app = create_app(cfg)
|
|
55
|
+
uvicorn.run(app, host=host, port=port, log_level="warning")
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""CLI command: `kernhell heal <test_file_or_directory>`.
|
|
2
|
+
|
|
3
|
+
Executes tests, intercepts selector failures, and runs the self-healing engine.
|
|
4
|
+
Supports single-file healing and directory-wide batch healing.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
import typer
|
|
13
|
+
from rich.console import Console
|
|
14
|
+
|
|
15
|
+
from kernhell.cli.ui import (
|
|
16
|
+
print_banner,
|
|
17
|
+
print_failure_context,
|
|
18
|
+
print_pruning_stats,
|
|
19
|
+
)
|
|
20
|
+
from kernhell.core.config import load_config
|
|
21
|
+
from kernhell.core.exceptions import (
|
|
22
|
+
NoFailuresDetectedError,
|
|
23
|
+
PlaywrightNotInstalledError,
|
|
24
|
+
ScannerError,
|
|
25
|
+
TestFileNotFoundError,
|
|
26
|
+
)
|
|
27
|
+
from kernhell.core.models import HealingReport
|
|
28
|
+
from kernhell.core.orchestrator import HealingOrchestrator
|
|
29
|
+
from kernhell.reporter.reporter import HealingReporter
|
|
30
|
+
from kernhell.utils.file_utils import read_file
|
|
31
|
+
from kernhell.utils.logger import setup_logging
|
|
32
|
+
|
|
33
|
+
console = Console(stderr=True)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def heal_command(
|
|
37
|
+
target: Path = typer.Argument(
|
|
38
|
+
...,
|
|
39
|
+
help="Path to a Playwright test file or directory of test files to heal.",
|
|
40
|
+
exists=False,
|
|
41
|
+
),
|
|
42
|
+
dry_run: bool = typer.Option(
|
|
43
|
+
False,
|
|
44
|
+
"--dry-run",
|
|
45
|
+
help="Preview healed selectors without mutating test files on disk.",
|
|
46
|
+
),
|
|
47
|
+
output_format: str = typer.Option(
|
|
48
|
+
"text",
|
|
49
|
+
"--output",
|
|
50
|
+
"-o",
|
|
51
|
+
help="Output format: 'text' (Rich terminal) or 'json' (machine-parseable for CI).",
|
|
52
|
+
),
|
|
53
|
+
output_file: Optional[Path] = typer.Option(
|
|
54
|
+
None,
|
|
55
|
+
"--output-file",
|
|
56
|
+
help="Optional path to save JSON healing report.",
|
|
57
|
+
),
|
|
58
|
+
config_path: Optional[Path] = typer.Option(
|
|
59
|
+
None,
|
|
60
|
+
"--config",
|
|
61
|
+
"-c",
|
|
62
|
+
help="Path to custom .kernhellrc.toml file.",
|
|
63
|
+
),
|
|
64
|
+
verbose: bool = typer.Option(
|
|
65
|
+
False,
|
|
66
|
+
"--verbose",
|
|
67
|
+
"-v",
|
|
68
|
+
help="Enable detailed debug logs.",
|
|
69
|
+
),
|
|
70
|
+
json_log: bool = typer.Option(
|
|
71
|
+
False,
|
|
72
|
+
"--json-log",
|
|
73
|
+
help="Output structured logs in JSON format.",
|
|
74
|
+
),
|
|
75
|
+
) -> None:
|
|
76
|
+
"""Autonomously detect and heal broken Playwright selectors."""
|
|
77
|
+
cfg = load_config(config_path)
|
|
78
|
+
log_level = "DEBUG" if verbose else cfg.logging.level
|
|
79
|
+
setup_logging(level=log_level, json_output=json_log or cfg.logging.json_output)
|
|
80
|
+
|
|
81
|
+
if output_format != "json":
|
|
82
|
+
print_banner()
|
|
83
|
+
|
|
84
|
+
if not target.exists():
|
|
85
|
+
console.print(f"[bold red]Error:[/bold red] Target path not found: {target}")
|
|
86
|
+
raise typer.Exit(code=1)
|
|
87
|
+
|
|
88
|
+
# Discover test files (single file vs batch directory mode)
|
|
89
|
+
if target.is_dir():
|
|
90
|
+
test_files = sorted(target.rglob("test_*.py"))
|
|
91
|
+
if not test_files:
|
|
92
|
+
console.print(f"[yellow]No 'test_*.py' files found in {target}[/yellow]")
|
|
93
|
+
raise typer.Exit(code=0)
|
|
94
|
+
else:
|
|
95
|
+
test_files = [target]
|
|
96
|
+
|
|
97
|
+
orchestrator = HealingOrchestrator(cfg)
|
|
98
|
+
reports: list[HealingReport] = []
|
|
99
|
+
|
|
100
|
+
for test_file in test_files:
|
|
101
|
+
original_code = read_file(test_file) if test_file.is_file() else ""
|
|
102
|
+
|
|
103
|
+
with console.status(f"[bold cyan]Scanning & healing [white]{test_file.name}[/white]..."):
|
|
104
|
+
try:
|
|
105
|
+
report = orchestrator.heal(test_file, dry_run=dry_run)
|
|
106
|
+
reports.append(report)
|
|
107
|
+
except NoFailuresDetectedError as exc:
|
|
108
|
+
if output_format != "json":
|
|
109
|
+
console.print(f"[bold green]✔[/bold green] {test_file.name}: {exc}")
|
|
110
|
+
continue
|
|
111
|
+
except (PlaywrightNotInstalledError, ScannerError, TestFileNotFoundError) as exc:
|
|
112
|
+
console.print(f"[bold red]Failed on {test_file.name}:[/bold red] {exc}")
|
|
113
|
+
continue
|
|
114
|
+
|
|
115
|
+
if output_format != "json" and report.results:
|
|
116
|
+
for i, result in enumerate(report.results, start=1):
|
|
117
|
+
print_failure_context(result.failure, index=i)
|
|
118
|
+
if result.pruned_dom:
|
|
119
|
+
print_pruning_stats(result.pruned_dom)
|
|
120
|
+
console.print()
|
|
121
|
+
|
|
122
|
+
# Render surgical AST diff if file was patched
|
|
123
|
+
if not dry_run and original_code and test_file.is_file():
|
|
124
|
+
patched_code = read_file(test_file)
|
|
125
|
+
if patched_code != original_code:
|
|
126
|
+
HealingReporter.render_diff(
|
|
127
|
+
old_code=original_code,
|
|
128
|
+
new_code=patched_code,
|
|
129
|
+
filename=test_file.name,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
HealingReporter.print_summary(report)
|
|
133
|
+
|
|
134
|
+
# JSON export handling
|
|
135
|
+
if output_format == "json" or output_file is not None:
|
|
136
|
+
combined_json = [json_str for report in reports for json_str in [HealingReporter.export_json(report)]]
|
|
137
|
+
payload = f"[\n{', '.join(combined_json)}\n]"
|
|
138
|
+
if output_file:
|
|
139
|
+
output_file.write_text(payload, encoding="utf-8")
|
|
140
|
+
if output_format == "json":
|
|
141
|
+
typer.echo(payload)
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""CLI command: `kernhell scan <test_file>`.
|
|
2
|
+
|
|
3
|
+
Detects broken selectors and captures DOM snapshots without modifying test files.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
import typer
|
|
12
|
+
from rich.console import Console
|
|
13
|
+
|
|
14
|
+
from kernhell.cli.ui import (
|
|
15
|
+
print_banner,
|
|
16
|
+
print_failure_context,
|
|
17
|
+
print_pruning_stats,
|
|
18
|
+
print_scan_summary,
|
|
19
|
+
)
|
|
20
|
+
from kernhell.core.config import load_config
|
|
21
|
+
from kernhell.core.exceptions import (
|
|
22
|
+
NoFailuresDetectedError,
|
|
23
|
+
PlaywrightNotInstalledError,
|
|
24
|
+
ScannerError,
|
|
25
|
+
TestFileNotFoundError,
|
|
26
|
+
)
|
|
27
|
+
from kernhell.core.orchestrator import HealingOrchestrator
|
|
28
|
+
from kernhell.utils.logger import setup_logging
|
|
29
|
+
|
|
30
|
+
console = Console(stderr=True)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def scan_command(
|
|
34
|
+
test_file: Path = typer.Argument(
|
|
35
|
+
...,
|
|
36
|
+
help="Path to the Playwright test file to scan for broken selectors.",
|
|
37
|
+
exists=False,
|
|
38
|
+
),
|
|
39
|
+
config_path: Optional[Path] = typer.Option(
|
|
40
|
+
None,
|
|
41
|
+
"--config",
|
|
42
|
+
"-c",
|
|
43
|
+
help="Path to custom .kernhellrc.toml file.",
|
|
44
|
+
),
|
|
45
|
+
verbose: bool = typer.Option(
|
|
46
|
+
False,
|
|
47
|
+
"--verbose",
|
|
48
|
+
"-v",
|
|
49
|
+
help="Enable detailed debug logs.",
|
|
50
|
+
),
|
|
51
|
+
json_log: bool = typer.Option(
|
|
52
|
+
False,
|
|
53
|
+
"--json-log",
|
|
54
|
+
help="Output structured logs in JSON format.",
|
|
55
|
+
),
|
|
56
|
+
) -> None:
|
|
57
|
+
"""Scan a Playwright test file, intercepting selector failures and capturing DOM."""
|
|
58
|
+
cfg = load_config(config_path)
|
|
59
|
+
log_level = "DEBUG" if verbose else cfg.logging.level
|
|
60
|
+
setup_logging(level=log_level, json_output=json_log or cfg.logging.json_output)
|
|
61
|
+
|
|
62
|
+
print_banner()
|
|
63
|
+
|
|
64
|
+
if not test_file.exists():
|
|
65
|
+
console.print(f"[bold red]Error:[/bold red] Test file not found: {test_file}")
|
|
66
|
+
raise typer.Exit(code=1)
|
|
67
|
+
|
|
68
|
+
orchestrator = HealingOrchestrator(cfg)
|
|
69
|
+
|
|
70
|
+
with console.status(f"[bold cyan]Scanning [white]{test_file.name}[/white] for selector failures..."):
|
|
71
|
+
try:
|
|
72
|
+
results = orchestrator.scan(test_file)
|
|
73
|
+
except NoFailuresDetectedError as exc:
|
|
74
|
+
console.print(f"[bold green]✔[/bold green] {exc}")
|
|
75
|
+
raise typer.Exit(code=0)
|
|
76
|
+
except (PlaywrightNotInstalledError, ScannerError, TestFileNotFoundError) as exc:
|
|
77
|
+
console.print(f"[bold red]Scan Failed:[/bold red] {exc}")
|
|
78
|
+
raise typer.Exit(code=1)
|
|
79
|
+
|
|
80
|
+
failures = [f for f, _ in results]
|
|
81
|
+
print_scan_summary(failures)
|
|
82
|
+
|
|
83
|
+
for i, (failure, pruned) in enumerate(results, start=1):
|
|
84
|
+
print_failure_context(failure, index=i)
|
|
85
|
+
if pruned:
|
|
86
|
+
print_pruning_stats(pruned)
|
|
87
|
+
console.print()
|