wup 0.2.24__tar.gz → 0.2.26__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.
Files changed (36) hide show
  1. {wup-0.2.24/wup.egg-info → wup-0.2.26}/PKG-INFO +7 -7
  2. {wup-0.2.24 → wup-0.2.26}/README.md +6 -6
  3. {wup-0.2.24 → wup-0.2.26}/pyproject.toml +1 -1
  4. wup-0.2.26/tests/test_monitoring_manifest.py +72 -0
  5. wup-0.2.26/tests/test_testql_monitor.py +121 -0
  6. {wup-0.2.24 → wup-0.2.26}/wup/__init__.py +1 -1
  7. {wup-0.2.24 → wup-0.2.26}/wup/cli.py +133 -2
  8. {wup-0.2.24 → wup-0.2.26}/wup/config.py +6 -0
  9. {wup-0.2.24 → wup-0.2.26}/wup/core.py +61 -32
  10. {wup-0.2.24 → wup-0.2.26}/wup/models/config.py +4 -1
  11. wup-0.2.26/wup/monitoring_manifest.py +306 -0
  12. wup-0.2.26/wup/testql_monitor.py +329 -0
  13. {wup-0.2.24 → wup-0.2.26}/wup/testql_watcher.py +129 -0
  14. {wup-0.2.24 → wup-0.2.26/wup.egg-info}/PKG-INFO +7 -7
  15. {wup-0.2.24 → wup-0.2.26}/wup.egg-info/SOURCES.txt +4 -0
  16. {wup-0.2.24 → wup-0.2.26}/LICENSE +0 -0
  17. {wup-0.2.24 → wup-0.2.26}/setup.cfg +0 -0
  18. {wup-0.2.24 → wup-0.2.26}/tests/test_e2e.py +0 -0
  19. {wup-0.2.24 → wup-0.2.26}/tests/test_testql_watcher.py +0 -0
  20. {wup-0.2.24 → wup-0.2.26}/tests/test_web_client.py +0 -0
  21. {wup-0.2.24 → wup-0.2.26}/tests/test_wup.py +0 -0
  22. {wup-0.2.24 → wup-0.2.26}/wup/_ast_detector.py +0 -0
  23. {wup-0.2.24 → wup-0.2.26}/wup/_hash_detector.py +0 -0
  24. {wup-0.2.24 → wup-0.2.26}/wup/_yaml_detector.py +0 -0
  25. {wup-0.2.24 → wup-0.2.26}/wup/anomaly_detector.py +0 -0
  26. {wup-0.2.24 → wup-0.2.26}/wup/anomaly_models.py +0 -0
  27. {wup-0.2.24 → wup-0.2.26}/wup/assistant.py +0 -0
  28. {wup-0.2.24 → wup-0.2.26}/wup/dependency_mapper.py +0 -0
  29. {wup-0.2.24 → wup-0.2.26}/wup/models/__init__.py +0 -0
  30. {wup-0.2.24 → wup-0.2.26}/wup/testql_discovery.py +0 -0
  31. {wup-0.2.24 → wup-0.2.26}/wup/visual_diff.py +0 -0
  32. {wup-0.2.24 → wup-0.2.26}/wup/web_client.py +0 -0
  33. {wup-0.2.24 → wup-0.2.26}/wup.egg-info/dependency_links.txt +0 -0
  34. {wup-0.2.24 → wup-0.2.26}/wup.egg-info/entry_points.txt +0 -0
  35. {wup-0.2.24 → wup-0.2.26}/wup.egg-info/requires.txt +0 -0
  36. {wup-0.2.24 → wup-0.2.26}/wup.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wup
3
- Version: 0.2.24
3
+ Version: 0.2.26
4
4
  Summary: WUP (What's Up) - Intelligent file watcher for regression testing in large projects
5
5
  Author-email: Tom Sapletta <tom@sapletta.com>
6
6
  License-Expression: Apache-2.0
@@ -29,17 +29,17 @@ Dynamic: license-file
29
29
 
30
30
  ## AI Cost Tracking
31
31
 
32
- ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.22-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
33
- ![AI Cost](https://img.shields.io/badge/AI%20Cost-$1.86-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-11.1h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
32
+ ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.26-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
33
+ ![AI Cost](https://img.shields.io/badge/AI%20Cost-$1.95-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-15.3h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
34
34
 
35
- - 🤖 **LLM usage:** $1.8607 (34 commits)
36
- - 👤 **Human dev:** ~$1113 (11.1h @ $100/h, 30min dedup)
35
+ - 🤖 **LLM usage:** $1.9540 (36 commits)
36
+ - 👤 **Human dev:** ~$1532 (15.3h @ $100/h, 30min dedup)
37
37
 
38
- Generated on 2026-05-12 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
38
+ Generated on 2026-05-16 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
39
39
 
40
40
  ---
41
41
 
42
- ![PyPI](https://img.shields.io/badge/pypi-wup-blue) ![Version](https://img.shields.io/badge/version-0.2.22-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
42
+ ![PyPI](https://img.shields.io/badge/pypi-wup-blue) ![Version](https://img.shields.io/badge/version-0.2.26-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
43
43
 
44
44
  **WUP (What's Up)** - Intelligent file watcher for regression testing in large projects.
45
45
 
@@ -3,17 +3,17 @@
3
3
 
4
4
  ## AI Cost Tracking
5
5
 
6
- ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.22-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
7
- ![AI Cost](https://img.shields.io/badge/AI%20Cost-$1.86-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-11.1h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
6
+ ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.26-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
7
+ ![AI Cost](https://img.shields.io/badge/AI%20Cost-$1.95-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-15.3h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
8
8
 
9
- - 🤖 **LLM usage:** $1.8607 (34 commits)
10
- - 👤 **Human dev:** ~$1113 (11.1h @ $100/h, 30min dedup)
9
+ - 🤖 **LLM usage:** $1.9540 (36 commits)
10
+ - 👤 **Human dev:** ~$1532 (15.3h @ $100/h, 30min dedup)
11
11
 
12
- Generated on 2026-05-12 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
12
+ Generated on 2026-05-16 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
13
13
 
14
14
  ---
15
15
 
16
- ![PyPI](https://img.shields.io/badge/pypi-wup-blue) ![Version](https://img.shields.io/badge/version-0.2.22-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
16
+ ![PyPI](https://img.shields.io/badge/pypi-wup-blue) ![Version](https://img.shields.io/badge/version-0.2.26-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
17
17
 
18
18
  **WUP (What's Up)** - Intelligent file watcher for regression testing in large projects.
19
19
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "wup"
7
- version = "0.2.24"
7
+ version = "0.2.26"
8
8
  description = "WUP (What's Up) - Intelligent file watcher for regression testing in large projects"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -0,0 +1,72 @@
1
+ import tempfile
2
+ from pathlib import Path
3
+
4
+ import yaml
5
+
6
+ from wup.models.config import ProjectConfig, ServiceConfig, TestQLConfig, WupConfig, WatchConfig
7
+ from wup.monitoring_manifest import (
8
+ MANIFEST_BEGIN,
9
+ MANIFEST_END,
10
+ build_monitoring_manifest,
11
+ discover_docker_compose_services,
12
+ load_monitoring_manifest_from_yaml,
13
+ patch_wup_yaml_monitoring,
14
+ )
15
+
16
+
17
+ def test_discover_docker_compose():
18
+ with tempfile.TemporaryDirectory() as tmpdir:
19
+ root = Path(tmpdir)
20
+ (root / "docker-compose.yml").write_text(
21
+ "services:\n"
22
+ " firmware:\n"
23
+ " container_name: test-simulator-firmware\n"
24
+ " ports:\n"
25
+ " - '8202:8202'\n"
26
+ " healthcheck:\n"
27
+ " test: ['CMD', 'curl', 'http://127.0.0.1:8202/health']\n",
28
+ encoding="utf-8",
29
+ )
30
+ found = discover_docker_compose_services(root)
31
+ assert len(found) == 1
32
+ assert found[0].compose_service == "firmware"
33
+ assert found[0].container_name == "test-simulator-firmware"
34
+
35
+
36
+ def test_patch_and_load_monitoring_block():
37
+ with tempfile.TemporaryDirectory() as tmpdir:
38
+ root = Path(tmpdir)
39
+ (root / "docker-compose.yml").write_text(
40
+ "services:\n firmware:\n container_name: test-simulator-firmware\n ports:\n - '8202:8202'\n",
41
+ encoding="utf-8",
42
+ )
43
+ cfg_path = root / "wup.yaml"
44
+ cfg_path.write_text(
45
+ "project:\n name: demo\n"
46
+ "services:\n - name: firmware\n paths: ['backend/firmware/**']\n"
47
+ "testql:\n base_url: http://localhost:8100\n"
48
+ " endpoints_by_service:\n firmware:\n - /firmware/api/v1/health\n",
49
+ encoding="utf-8",
50
+ )
51
+ wup_config = WupConfig(
52
+ project=ProjectConfig(name="demo"),
53
+ services=[ServiceConfig(name="firmware", paths=["backend/firmware/**"])],
54
+ watch=WatchConfig(),
55
+ testql=TestQLConfig(
56
+ base_url="http://localhost:8100",
57
+ endpoints_by_service={"firmware": ["/firmware/api/v1/health"]},
58
+ probe_interval_s=60,
59
+ ),
60
+ )
61
+ manifest = build_monitoring_manifest(root, wup_config)
62
+ patch_wup_yaml_monitoring(cfg_path, manifest)
63
+
64
+ text = cfg_path.read_text(encoding="utf-8")
65
+ assert MANIFEST_BEGIN in text
66
+ assert MANIFEST_END in text
67
+
68
+ loaded = load_monitoring_manifest_from_yaml(cfg_path)
69
+ assert loaded is not None
70
+ assert loaded["wup_services"]["firmware"]["live_probes"]
71
+ assert loaded["wup_services"]["firmware"]["live_probes"][0]["url"].endswith("/firmware/api/v1/health")
72
+ assert loaded["wup_services"]["firmware"]["docker"][0]["compose_service"] == "firmware"
@@ -0,0 +1,121 @@
1
+ import asyncio
2
+ import json
3
+ import tempfile
4
+ from pathlib import Path
5
+ from unittest.mock import patch
6
+
7
+ from wup.models.config import ProjectConfig, ServiceConfig, TestQLConfig, WupConfig, WatchConfig
8
+ from wup.testql_monitor import (
9
+ ProbeTarget,
10
+ TestQLMonitor,
11
+ assign_probe_to_service,
12
+ is_monitoring_probe,
13
+ parse_scenario_probes,
14
+ parse_service_map_probes,
15
+ )
16
+ from wup.testql_watcher import TestQLWatcher
17
+
18
+
19
+ def test_parse_scenario_probes_full_url():
20
+ content = """
21
+ API[1]{method, endpoint, expected_status}:
22
+ GET, http://localhost:8100/firmware/api/v1/health, 200
23
+ """
24
+ with tempfile.NamedTemporaryFile("w", suffix=".testql.toon.yaml", delete=False) as handle:
25
+ handle.write(content)
26
+ handle.flush()
27
+ path = Path(handle.name)
28
+
29
+ probes = parse_scenario_probes(path)
30
+ path.unlink(missing_ok=True)
31
+ assert len(probes) == 1
32
+ assert probes[0].url.endswith("/firmware/api/v1/health")
33
+ assert probes[0].expected_status == 200
34
+ assert is_monitoring_probe(probes[0])
35
+
36
+
37
+ def test_assign_firmware_service():
38
+ services = [
39
+ ServiceConfig(name="frontend", paths=["frontend/**"]),
40
+ ServiceConfig(name="firmware", paths=["backend/firmware/**"]),
41
+ ]
42
+ probe = ProbeTarget(url="http://localhost:8100/firmware/api/v1/health")
43
+ assert assign_probe_to_service(probe, services) == "firmware"
44
+
45
+
46
+ def test_monitor_merges_config_and_service_map():
47
+ with tempfile.TemporaryDirectory() as tmpdir:
48
+ root = Path(tmpdir)
49
+ scenario_dir = root / "testql-scenarios"
50
+ scenario_dir.mkdir()
51
+ scenario = scenario_dir / "fleet-health.testql.toon.yaml"
52
+ scenario.write_text(
53
+ "API[1]{method, endpoint, expected_status}:\n"
54
+ " GET, http://localhost:8100/firmware/api/v1/execution/status, 200\n",
55
+ encoding="utf-8",
56
+ )
57
+
58
+ service_map = root / "service-map.yaml"
59
+ service_map.write_text(
60
+ "service:\n base_url: http://localhost:8100\n"
61
+ "endpoints:\n"
62
+ " - { method: GET, path: /firmware/api/v1/health }\n",
63
+ encoding="utf-8",
64
+ )
65
+
66
+ cfg = WupConfig(
67
+ project=ProjectConfig(name="demo"),
68
+ services=[
69
+ ServiceConfig(name="firmware", paths=["backend/firmware/**"]),
70
+ ],
71
+ watch=WatchConfig(),
72
+ testql=TestQLConfig(
73
+ scenario_dir="testql-scenarios",
74
+ base_url="http://localhost:8100",
75
+ endpoint_discovery=True,
76
+ service_map_globs=["service-map.yaml"],
77
+ endpoints_by_service={
78
+ "firmware": ["/firmware/api/v1/execution/logs"],
79
+ },
80
+ ),
81
+ )
82
+ monitor = TestQLMonitor(root, cfg)
83
+ probes = monitor.probes_for_service("firmware")
84
+ urls = {p.url for p in probes}
85
+ assert "http://localhost:8100/firmware/api/v1/health" in urls
86
+ assert "http://localhost:8100/firmware/api/v1/execution/status" in urls
87
+ assert "http://localhost:8100/firmware/api/v1/execution/logs" in urls
88
+
89
+
90
+ def test_live_probe_failure_updates_health():
91
+ with tempfile.TemporaryDirectory() as tmpdir:
92
+ root = Path(tmpdir)
93
+ cfg = WupConfig(
94
+ project=ProjectConfig(name="demo"),
95
+ services=[ServiceConfig(name="firmware", paths=["backend/firmware/**"])],
96
+ watch=WatchConfig(),
97
+ testql=TestQLConfig(
98
+ scenario_dir="testql-scenarios",
99
+ base_url="http://localhost:8100",
100
+ endpoints_by_service={"firmware": ["/firmware/api/v1/health"]},
101
+ ),
102
+ )
103
+ watcher = TestQLWatcher(
104
+ project_root=str(root),
105
+ deps_file=str(root / "deps.json"),
106
+ scenarios_dir="testql-scenarios",
107
+ config=cfg,
108
+ )
109
+
110
+ failing = ProbeTarget(url="http://localhost:8100/firmware/api/v1/health")
111
+
112
+ def fake_probe(self, timeout_s=10.0):
113
+ return False, "HTTP 500 (expected 200)"
114
+
115
+ with patch.object(ProbeTarget, "probe", fake_probe):
116
+ ok = asyncio.run(watcher._run_live_http_probes("firmware", []))
117
+
118
+ assert ok is False
119
+ state = json.loads((root / ".wup" / "service-health.json").read_text(encoding="utf-8"))
120
+ assert state["firmware"]["status"] == "down"
121
+ assert state["firmware"]["stage"] == "probe"
@@ -7,7 +7,7 @@ WUP monitors file changes and runs intelligent regression tests using a 3-layer
7
7
  3. Detail Layer: Full tests with blame reports (only on failure)
8
8
  """
9
9
 
10
- __version__ = "0.2.22"
10
+ __version__ = "0.2.26"
11
11
  __author__ = "Tom Sapletta"
12
12
 
13
13
  from .config import load_config, save_config, get_default_config
@@ -9,7 +9,7 @@ from typing import Optional
9
9
  import typer
10
10
  from rich.console import Console
11
11
 
12
- from .config import load_config
12
+ from .config import find_config_file, load_config
13
13
  from .core import WupWatcher
14
14
  from .dependency_mapper import DependencyMapper
15
15
  from .models.config import WupConfig
@@ -37,6 +37,11 @@ def watch(
37
37
  browser_service_url: Optional[str] = typer.Option(None, "--browser-service-url", help="HTTP endpoint for browser notifications"),
38
38
  track_dir: str = typer.Option(".wup/tracks", "--track-dir", help="Directory where error track JSON files are written"),
39
39
  quick_limit: int = typer.Option(3, "--quick-limit", help="Maximum TestQL scenarios used in quick pass"),
40
+ probe_interval: Optional[int] = typer.Option(
41
+ None,
42
+ "--probe-interval",
43
+ help="Periodic live HTTP/TestQL probes in seconds (overrides testql.probe_interval_s)",
44
+ ),
40
45
  config: Optional[str] = typer.Option(None, "--config", "-C", help="Path to wup.yaml config file"),
41
46
  ):
42
47
  """
@@ -56,16 +61,30 @@ def watch(
56
61
  # Load configuration
57
62
  config_path = Path(config) if config else None
58
63
  wup_config = load_config(project_path, config_path)
59
-
64
+ if probe_interval is not None:
65
+ wup_config.testql.probe_interval_s = int(probe_interval)
66
+
60
67
  console.print(f"[bold cyan]🚀 WUP Watcher[/bold cyan]")
61
68
  console.print(f"[dim]Project: {wup_config.project.name}[/dim]")
62
69
  console.print(f"[dim]Description: {wup_config.project.description}[/dim]")
63
70
  console.print(f"[dim]CPU Throttle: {cpu_throttle * 100}%[/dim]")
64
71
  console.print(f"[dim]Debounce: {debounce}s[/dim]")
65
72
  console.print(f"[dim]Cooldown: {cooldown}s[/dim]")
73
+ if wup_config.testql.probe_interval_s:
74
+ console.print(f"[dim]Live probes: every {wup_config.testql.probe_interval_s}s[/dim]")
66
75
  console.print(f"[dim]Config: {config_path or 'auto-detected'}[/dim]")
67
76
  console.print()
68
77
 
78
+ cfg_path = config_path if config_path and config_path.exists() else find_config_file(project_path)
79
+ if cfg_path:
80
+ from .monitoring_manifest import build_monitoring_manifest, patch_wup_yaml_monitoring
81
+ try:
82
+ manifest = build_monitoring_manifest(project_path, wup_config)
83
+ patch_wup_yaml_monitoring(cfg_path, manifest)
84
+ console.print("[dim]Refreshed monitoring manifest in wup.yaml[/dim]")
85
+ except OSError as exc:
86
+ console.print(f"[yellow]Could not refresh monitoring manifest: {exc}[/yellow]")
87
+
69
88
  if mode.lower() == "testql":
70
89
  watcher = TestQLWatcher(
71
90
  project_root=str(project_path),
@@ -274,6 +293,33 @@ def status(
274
293
  if track_file:
275
294
  lines.append(Text.from_markup(f" [dim]track: {track_file}[/dim]"))
276
295
 
296
+ # --- monitoring manifest (from wup.yaml) ---
297
+ manifest_path = config_path if config_path and config_path.exists() else find_config_file(project_path)
298
+ if manifest_path:
299
+ from .monitoring_manifest import load_monitoring_manifest_from_yaml
300
+
301
+ manifest = load_monitoring_manifest_from_yaml(manifest_path)
302
+ if manifest:
303
+ lines.append(Text(""))
304
+ lines.append(Text.from_markup("[bold]Configured monitoring (wup.yaml):[/bold]"))
305
+ lines.append(Text.from_markup(
306
+ f" [dim]manifest {manifest.get('generated_at', '?')} · "
307
+ f"probe {manifest.get('probe_interval_s', 0)}s[/dim]"
308
+ ))
309
+ for svc, info in sorted((manifest.get("wup_services") or {}).items()):
310
+ probes = info.get("live_probes") or []
311
+ dockers = info.get("docker") or []
312
+ lines.append(Text.from_markup(
313
+ f" [cyan]{svc}[/cyan]: {len(probes)} probe(s), docker: "
314
+ + ", ".join(
315
+ d.get("compose_service", "?") for d in dockers[:4]
316
+ )
317
+ + ("…" if len(dockers) > 4 else "")
318
+ ))
319
+ lines.append(Text.from_markup(
320
+ " [dim]Pełna lista: sekcja monitoring: w wup.yaml (BEGIN WUP MONITORING MANIFEST)[/dim]"
321
+ ))
322
+
277
323
  # --- visual diff section ---
278
324
  if wup_config.visual_diff and wup_config.visual_diff.enabled:
279
325
  from .visual_diff import VisualDiffer
@@ -439,6 +485,91 @@ def map_deps(
439
485
  console.print(f"[dim]Files: {len(deps.get('files', {}))}[/dim]")
440
486
 
441
487
 
488
+ @app.command("sync-testql")
489
+ def sync_testql(
490
+ project: str = typer.Argument(".", help="Path to the project root directory"),
491
+ write: bool = typer.Option(False, "--write", "-w", help="Write monitoring manifest block into wup.yaml"),
492
+ merge_endpoints: bool = typer.Option(
493
+ False,
494
+ "--merge-endpoints",
495
+ help="Also merge discovered paths into testql.endpoints_by_service (rewrites YAML body)",
496
+ ),
497
+ config: Optional[str] = typer.Option(None, "--config", "-C", help="Path to wup.yaml config file"),
498
+ ):
499
+ """
500
+ Discover monitoring targets and document them in wup.yaml.
501
+
502
+ With ``--write``, appends/updates the auto-generated ``monitoring:`` block
503
+ (Docker Compose services, live HTTP probes, sources). Use this to verify
504
+ whether a failure is a WUP config gap vs a down container.
505
+
506
+ Use ``--merge-endpoints`` cautiously — it re-serializes wup.yaml (may drop comments).
507
+ """
508
+ import json
509
+
510
+ from .config import find_config_file, load_config
511
+ from .monitoring_manifest import (
512
+ MANIFEST_BEGIN,
513
+ build_monitoring_manifest,
514
+ format_manifest_summary,
515
+ patch_wup_yaml_monitoring,
516
+ )
517
+ from .testql_monitor import TestQLMonitor
518
+
519
+ project_path = Path(project).resolve()
520
+ if not project_path.exists():
521
+ console.print(f"[red]Error: Project path '{project}' does not exist[/red]")
522
+ raise typer.Exit(1)
523
+
524
+ config_path = Path(config) if config else find_config_file(project_path)
525
+ wup_config = load_config(project_path, config_path)
526
+ monitor = TestQLMonitor(project_path, wup_config)
527
+ suggested = monitor.suggested_endpoints_by_service()
528
+ manifest = build_monitoring_manifest(project_path, wup_config)
529
+
530
+ console.print("[bold]Monitoring manifest (preview):[/bold]")
531
+ console.print(format_manifest_summary(manifest))
532
+
533
+ if suggested:
534
+ console.print()
535
+ console.print("[bold]Suggested testql.endpoints_by_service additions:[/bold]")
536
+ console.print(json.dumps(suggested, indent=2))
537
+
538
+ if not write:
539
+ console.print()
540
+ console.print("[dim]Run: wup sync-testql . --write → dokumentacja w wup.yaml[/dim]")
541
+ return
542
+
543
+ if config_path is None:
544
+ console.print("[red]No wup.yaml found — run `wup init` first[/red]")
545
+ raise typer.Exit(1)
546
+
547
+ if merge_endpoints and suggested:
548
+ import yaml as pyyaml
549
+
550
+ merged = dict(wup_config.testql.endpoints_by_service or {})
551
+ for service, paths in suggested.items():
552
+ existing = set(merged.get(service, []))
553
+ existing.update(paths)
554
+ merged[service] = sorted(existing)
555
+ raw = pyyaml.safe_load(config_path.read_text(encoding="utf-8")) or {}
556
+ raw.setdefault("testql", {})["endpoints_by_service"] = merged
557
+ wup_config.testql.endpoints_by_service = merged
558
+ manifest = build_monitoring_manifest(project_path, wup_config)
559
+ body = pyyaml.safe_dump(
560
+ {k: v for k, v in raw.items() if k != "monitoring"},
561
+ sort_keys=False,
562
+ allow_unicode=True,
563
+ default_flow_style=False,
564
+ )
565
+ config_path.write_text(body.rstrip() + "\n\n", encoding="utf-8")
566
+ console.print("[yellow]Merged endpoints_by_service (review git diff for comment loss)[/yellow]")
567
+
568
+ patch_wup_yaml_monitoring(config_path, manifest)
569
+ console.print(f"[green]✓ monitoring manifest written to {config_path}[/green]")
570
+ console.print(f"[dim]Szukaj w pliku: {MANIFEST_BEGIN}[/dim]")
571
+
572
+
442
573
  @app.command()
443
574
  def assistant(
444
575
  quick: bool = typer.Option(False, "--quick", "-q", help="Non-interactive mode with auto-detected values"),
@@ -176,6 +176,9 @@ def validate_config(raw: dict) -> WupConfig:
176
176
  output_format=testql_raw.get("output_format", "json"),
177
177
  extra_args=testql_raw.get("extra_args", ["--timeout 10s"]),
178
178
  endpoint_discovery=testql_raw.get("endpoint_discovery", True),
179
+ probe_interval_s=int(testql_raw.get("probe_interval_s", 0) or 0),
180
+ health_scenario=testql_raw.get("health_scenario", ""),
181
+ service_map_globs=testql_raw.get("service_map_globs", []),
179
182
  base_url=testql_raw.get("base_url", ""),
180
183
  base_url_env=testql_raw.get("base_url_env", "WUP_BASE_URL"),
181
184
  explicit_endpoints=testql_raw.get("explicit_endpoints", []),
@@ -328,6 +331,9 @@ def save_config(config: WupConfig, output_path: Path):
328
331
  "output_format": config.testql.output_format,
329
332
  "extra_args": config.testql.extra_args,
330
333
  "endpoint_discovery": config.testql.endpoint_discovery,
334
+ "probe_interval_s": config.testql.probe_interval_s,
335
+ "health_scenario": config.testql.health_scenario,
336
+ "service_map_globs": config.testql.service_map_globs,
331
337
  "base_url": config.testql.base_url,
332
338
  "base_url_env": config.testql.base_url_env,
333
339
  "explicit_endpoints": config.testql.explicit_endpoints,
@@ -6,6 +6,8 @@ import asyncio
6
6
  import json
7
7
  import subprocess
8
8
  import time
9
+ import urllib.error
10
+ import urllib.request
9
11
  from collections import defaultdict, deque
10
12
  from pathlib import Path
11
13
  from typing import Dict, List, Optional, Set, Tuple
@@ -298,69 +300,96 @@ class WupWatcher:
298
300
  async def run_quick_test(self, service: str, endpoints: List[str]) -> bool:
299
301
  """
300
302
  Run a quick test for a service (smoke test).
301
-
303
+
302
304
  Args:
303
305
  service: Service name
304
306
  endpoints: List of endpoints to test
305
-
307
+
306
308
  Returns:
307
309
  True if all tests passed, False otherwise
308
310
  """
309
311
  self.console.print(f"[cyan]🧪 Quick testing {service} ({len(endpoints)} endpoints)[/cyan]")
310
-
311
- # This is a placeholder - integrate with TestQL or your test framework
312
- # For now, simulate a test
313
- await asyncio.sleep(1)
314
-
315
- # Simulate random failure for demo (10% chance)
316
- import random
317
- passed = random.random() > 0.1
318
-
312
+
313
+ if not endpoints:
314
+ self.console.print(f"[yellow]⚠ No endpoints configured for {service}, skipping quick test[/yellow]")
315
+ return True
316
+
317
+ passed = True
318
+ for endpoint in endpoints:
319
+ try:
320
+ req = urllib.request.Request(endpoint, method="HEAD")
321
+ with urllib.request.urlopen(req, timeout=10) as resp:
322
+ if resp.status >= 400:
323
+ self.console.print(f"[red]✗ {endpoint} → HTTP {resp.status}[/red]")
324
+ passed = False
325
+ else:
326
+ self.console.print(f"[green]✓ {endpoint} → HTTP {resp.status}[/green]")
327
+ except Exception as e:
328
+ self.console.print(f"[red]✗ {endpoint} → {e}[/red]")
329
+ passed = False
330
+
319
331
  if passed:
320
332
  self.console.print(f"[green]✓ Quick test passed for {service}[/green]")
321
333
  else:
322
334
  self.console.print(f"[red]✗ Quick test failed for {service}[/red]")
323
-
335
+
324
336
  return passed
325
337
 
326
338
  async def run_detail_test(self, service: str, endpoints: List[str]) -> Dict:
327
339
  """
328
340
  Run a detailed test for a service with blame report.
329
-
341
+
330
342
  Args:
331
343
  service: Service name
332
344
  endpoints: List of endpoints to test
333
-
345
+
334
346
  Returns:
335
347
  Dictionary with test results and blame information
336
348
  """
337
349
  self.console.print(f"[cyan]🔍 Detail testing {service} ({len(endpoints)} endpoints)[/cyan]")
338
-
339
- # This is a placeholder - integrate with TestQL or your test framework
340
- # For now, simulate a test
341
- await asyncio.sleep(3)
342
-
343
- # Simulate results
350
+
344
351
  results = {
345
352
  "service": service,
346
353
  "total_endpoints": len(endpoints),
347
- "passed": len(endpoints) - 1,
348
- "failed": 1,
349
- "failed_endpoint": endpoints[0] if endpoints else None,
350
- "blame": {
351
- "file": f"app/{service}/routes.py",
352
- "line": 42,
353
- "commit": "abc123",
354
- "author": "developer"
355
- }
354
+ "passed": 0,
355
+ "failed": 0,
356
+ "failed_endpoint": None,
357
+ "blame": {},
356
358
  }
357
-
359
+
360
+ for endpoint in endpoints:
361
+ try:
362
+ req = urllib.request.Request(endpoint, method="GET")
363
+ with urllib.request.urlopen(req, timeout=30) as resp:
364
+ if resp.status >= 400:
365
+ results["failed"] += 1
366
+ self.console.print(f"[red]✗ {endpoint} → HTTP {resp.status}[/red]")
367
+ else:
368
+ results["passed"] += 1
369
+ self.console.print(f"[green]✓ {endpoint} → HTTP {resp.status}[/green]")
370
+ except Exception as e:
371
+ results["failed"] += 1
372
+ self.console.print(f"[red]✗ {endpoint} → {e}[/red]")
373
+
358
374
  if results["failed"] > 0:
375
+ results["failed_endpoint"] = endpoints[0] if endpoints else None
376
+ try:
377
+ blame_result = subprocess.run(
378
+ ["git", "log", "--oneline", "-5", "--", f"*/{service}/*"],
379
+ cwd=str(self.project_root),
380
+ capture_output=True,
381
+ text=True,
382
+ )
383
+ if blame_result.returncode == 0:
384
+ lines = blame_result.stdout.strip().split("\n")
385
+ if lines and lines[0]:
386
+ results["blame"] = {"recent_commits": lines}
387
+ except Exception:
388
+ pass
359
389
  self.console.print(f"[red]✗ Detail test found {results['failed']} regression(s)[/red]")
360
- self.console.print(f"[red] Blame: {results['blame']['file']}:{results['blame']['line']}[/red]")
361
390
  else:
362
391
  self.console.print(f"[green]✓ Detail test passed for {service}[/green]")
363
-
392
+
364
393
  return results
365
394
 
366
395
  async def test_loop(self):
@@ -60,7 +60,10 @@ class TestQLConfig:
60
60
  smoke_scenario: str = "smoke.testql.toon.yaml"
61
61
  output_format: str = "json"
62
62
  extra_args: List[str] = field(default_factory=lambda: ["--timeout 10s"])
63
- endpoint_discovery: bool = True # Enable automatic endpoint discovery from scenarios
63
+ endpoint_discovery: bool = True # Merge health probes from scenarios + service maps
64
+ probe_interval_s: int = 0 # Periodic live probes for all services (0 = file-change only)
65
+ health_scenario: str = "" # Optional TestQL scenario run live (not --dry-run) on each quick pass
66
+ service_map_globs: List[str] = field(default_factory=list) # e.g. testql-testing/service-map/*.yaml
64
67
  base_url: str = ""
65
68
  base_url_env: str = "WUP_BASE_URL"
66
69
  explicit_endpoints: List[str] = field(default_factory=list)