wup 0.2.26__tar.gz → 0.2.27__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.26/wup.egg-info → wup-0.2.27}/PKG-INFO +13 -12
  2. {wup-0.2.26 → wup-0.2.27}/README.md +12 -11
  3. {wup-0.2.26 → wup-0.2.27}/pyproject.toml +1 -1
  4. {wup-0.2.26 → wup-0.2.27}/tests/test_testql_monitor.py +40 -0
  5. {wup-0.2.26 → wup-0.2.27}/tests/test_testql_watcher.py +3 -1
  6. {wup-0.2.26 → wup-0.2.27}/wup/__init__.py +1 -1
  7. {wup-0.2.26 → wup-0.2.27}/wup/cli.py +21 -10
  8. {wup-0.2.26 → wup-0.2.27}/wup/config.py +10 -2
  9. {wup-0.2.26 → wup-0.2.27}/wup/models/config.py +7 -2
  10. {wup-0.2.26 → wup-0.2.27}/wup/testql_monitor.py +82 -19
  11. {wup-0.2.26 → wup-0.2.27}/wup/testql_watcher.py +72 -16
  12. {wup-0.2.26 → wup-0.2.27}/wup/visual_diff.py +26 -3
  13. {wup-0.2.26 → wup-0.2.27}/wup/web_client.py +8 -1
  14. {wup-0.2.26 → wup-0.2.27/wup.egg-info}/PKG-INFO +13 -12
  15. {wup-0.2.26 → wup-0.2.27}/LICENSE +0 -0
  16. {wup-0.2.26 → wup-0.2.27}/setup.cfg +0 -0
  17. {wup-0.2.26 → wup-0.2.27}/tests/test_e2e.py +0 -0
  18. {wup-0.2.26 → wup-0.2.27}/tests/test_monitoring_manifest.py +0 -0
  19. {wup-0.2.26 → wup-0.2.27}/tests/test_web_client.py +0 -0
  20. {wup-0.2.26 → wup-0.2.27}/tests/test_wup.py +0 -0
  21. {wup-0.2.26 → wup-0.2.27}/wup/_ast_detector.py +0 -0
  22. {wup-0.2.26 → wup-0.2.27}/wup/_hash_detector.py +0 -0
  23. {wup-0.2.26 → wup-0.2.27}/wup/_yaml_detector.py +0 -0
  24. {wup-0.2.26 → wup-0.2.27}/wup/anomaly_detector.py +0 -0
  25. {wup-0.2.26 → wup-0.2.27}/wup/anomaly_models.py +0 -0
  26. {wup-0.2.26 → wup-0.2.27}/wup/assistant.py +0 -0
  27. {wup-0.2.26 → wup-0.2.27}/wup/core.py +0 -0
  28. {wup-0.2.26 → wup-0.2.27}/wup/dependency_mapper.py +0 -0
  29. {wup-0.2.26 → wup-0.2.27}/wup/models/__init__.py +0 -0
  30. {wup-0.2.26 → wup-0.2.27}/wup/monitoring_manifest.py +0 -0
  31. {wup-0.2.26 → wup-0.2.27}/wup/testql_discovery.py +0 -0
  32. {wup-0.2.26 → wup-0.2.27}/wup.egg-info/SOURCES.txt +0 -0
  33. {wup-0.2.26 → wup-0.2.27}/wup.egg-info/dependency_links.txt +0 -0
  34. {wup-0.2.26 → wup-0.2.27}/wup.egg-info/entry_points.txt +0 -0
  35. {wup-0.2.26 → wup-0.2.27}/wup.egg-info/requires.txt +0 -0
  36. {wup-0.2.26 → wup-0.2.27}/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.26
3
+ Version: 0.2.27
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.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)
32
+ ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.27-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-$2.13-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-16.3h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
34
34
 
35
- - 🤖 **LLM usage:** $1.9540 (36 commits)
36
- - 👤 **Human dev:** ~$1532 (15.3h @ $100/h, 30min dedup)
35
+ - 🤖 **LLM usage:** $2.1268 (37 commits)
36
+ - 👤 **Human dev:** ~$1632 (16.3h @ $100/h, 30min dedup)
37
37
 
38
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.26-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.27-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
 
@@ -83,7 +83,7 @@ wup assistant --quick --template fastapi
83
83
  # 3. Build dependency map (one-time setup)
84
84
  wup map-deps ./my-project
85
85
 
86
- # 4. Start watching for changes
86
+ # 4. Start watching (TestQL + live probes every 60s by default)
87
87
  wup watch ./my-project
88
88
 
89
89
  # 5. Start with live dashboard
@@ -110,14 +110,18 @@ wup map-deps ./my-project --output my-deps.json
110
110
  ### Watch Project
111
111
 
112
112
  ```bash
113
- # Basic watching (uses wup.yaml if present)
113
+ # Basic watching: TestQL mode + live HTTP probes every 60s (uses wup.yaml if present)
114
114
  wup watch ./my-project
115
115
 
116
+ # Legacy HTTP-only watcher (no TestQL, no periodic probes unless configured)
117
+ wup watch ./my-project --mode default --probe-interval 0
118
+
116
119
  # With custom settings
117
120
  wup watch ./my-project \
118
121
  --cpu-throttle 0.5 \
119
122
  --debounce 3 \
120
- --cooldown 600
123
+ --cooldown 600 \
124
+ --probe-interval 120
121
125
 
122
126
  # With live dashboard
123
127
  wup watch ./my-project --dashboard
@@ -125,9 +129,6 @@ wup watch ./my-project --dashboard
125
129
  # Use specific config file
126
130
  wup watch ./my-project --config custom-config.yaml
127
131
 
128
- # TestQL mode
129
- wup watch ./my-project --mode testql
130
-
131
132
  # Discover endpoints from TestQL scenarios
132
133
  wup testql-endpoints /path/to/scenarios --output testql-deps.json
133
134
  ```
@@ -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.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)
6
+ ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.27-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-$2.13-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-16.3h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
8
8
 
9
- - 🤖 **LLM usage:** $1.9540 (36 commits)
10
- - 👤 **Human dev:** ~$1532 (15.3h @ $100/h, 30min dedup)
9
+ - 🤖 **LLM usage:** $2.1268 (37 commits)
10
+ - 👤 **Human dev:** ~$1632 (16.3h @ $100/h, 30min dedup)
11
11
 
12
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.26-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.27-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
 
@@ -57,7 +57,7 @@ wup assistant --quick --template fastapi
57
57
  # 3. Build dependency map (one-time setup)
58
58
  wup map-deps ./my-project
59
59
 
60
- # 4. Start watching for changes
60
+ # 4. Start watching (TestQL + live probes every 60s by default)
61
61
  wup watch ./my-project
62
62
 
63
63
  # 5. Start with live dashboard
@@ -84,14 +84,18 @@ wup map-deps ./my-project --output my-deps.json
84
84
  ### Watch Project
85
85
 
86
86
  ```bash
87
- # Basic watching (uses wup.yaml if present)
87
+ # Basic watching: TestQL mode + live HTTP probes every 60s (uses wup.yaml if present)
88
88
  wup watch ./my-project
89
89
 
90
+ # Legacy HTTP-only watcher (no TestQL, no periodic probes unless configured)
91
+ wup watch ./my-project --mode default --probe-interval 0
92
+
90
93
  # With custom settings
91
94
  wup watch ./my-project \
92
95
  --cpu-throttle 0.5 \
93
96
  --debounce 3 \
94
- --cooldown 600
97
+ --cooldown 600 \
98
+ --probe-interval 120
95
99
 
96
100
  # With live dashboard
97
101
  wup watch ./my-project --dashboard
@@ -99,9 +103,6 @@ wup watch ./my-project --dashboard
99
103
  # Use specific config file
100
104
  wup watch ./my-project --config custom-config.yaml
101
105
 
102
- # TestQL mode
103
- wup watch ./my-project --mode testql
104
-
105
106
  # Discover endpoints from TestQL scenarios
106
107
  wup testql-endpoints /path/to/scenarios --output testql-deps.json
107
108
  ```
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "wup"
7
- version = "0.2.26"
7
+ version = "0.2.27"
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"
@@ -34,6 +34,24 @@ API[1]{method, endpoint, expected_status}:
34
34
  assert is_monitoring_probe(probes[0])
35
35
 
36
36
 
37
+ def test_connect_api_paths_on_8100_are_not_monitoring_probes():
38
+ probe = ProbeTarget(url="http://localhost:8100/api/id/health")
39
+ assert not is_monitoring_probe(probe)
40
+ assert assign_probe_to_service(
41
+ probe,
42
+ [ServiceConfig(name="backend", paths=["backend/**", "api/**"])],
43
+ ) != "backend"
44
+
45
+
46
+ def test_connect_health_on_8103_not_assigned_to_backend():
47
+ services = [
48
+ ServiceConfig(name="frontend", paths=["frontend/**"]),
49
+ ServiceConfig(name="backend", paths=["backend/**"]),
50
+ ]
51
+ probe = ProbeTarget(url="http://localhost:8103/api/id/health")
52
+ assert assign_probe_to_service(probe, services) is None
53
+
54
+
37
55
  def test_assign_firmware_service():
38
56
  services = [
39
57
  ServiceConfig(name="frontend", paths=["frontend/**"]),
@@ -87,6 +105,28 @@ def test_monitor_merges_config_and_service_map():
87
105
  assert "http://localhost:8100/firmware/api/v1/execution/logs" in urls
88
106
 
89
107
 
108
+ def test_probes_for_service_ignores_non_health_extra_paths():
109
+ with tempfile.TemporaryDirectory() as tmpdir:
110
+ root = Path(tmpdir)
111
+ cfg = WupConfig(
112
+ project=ProjectConfig(name="demo"),
113
+ services=[ServiceConfig(name="backend", paths=["backend/**"])],
114
+ watch=WatchConfig(),
115
+ testql=TestQLConfig(
116
+ base_url="http://localhost:8100",
117
+ api_base_url="http://localhost:8101",
118
+ endpoints_by_service={"backend": ["http://localhost:8101/api/v3/health"]},
119
+ ),
120
+ )
121
+ monitor = TestQLMonitor(root, cfg)
122
+ probes = monitor.probes_for_service(
123
+ "backend",
124
+ ["/connect-config", "http://localhost:8101/connect-config"],
125
+ )
126
+ urls = {p.url for p in probes}
127
+ assert urls == {"http://localhost:8101/api/v3/health"}
128
+
129
+
90
130
  def test_live_probe_failure_updates_health():
91
131
  with tempfile.TemporaryDirectory() as tmpdir:
92
132
  root = Path(tmpdir)
@@ -6,7 +6,7 @@ from pathlib import Path
6
6
  from subprocess import CompletedProcess
7
7
 
8
8
  from wup.testql_watcher import TestQLWatcher
9
- from wup.models.config import WupConfig, ProjectConfig, TestQLConfig, VisualDiffConfig
9
+ from wup.models.config import WupConfig, ProjectConfig, ServiceConfig, TestQLConfig, VisualDiffConfig
10
10
 
11
11
 
12
12
  def test_process_changed_file_creates_track_on_failure():
@@ -97,6 +97,7 @@ def test_config_endpoints_use_base_url_from_yaml_config():
97
97
  root = Path(tmpdir)
98
98
  cfg = WupConfig(
99
99
  project=ProjectConfig(name="demo"),
100
+ services=[ServiceConfig(name="connect-config", paths=["connect-config/**"])],
100
101
  testql=TestQLConfig(
101
102
  base_url="http://localhost:8100",
102
103
  explicit_endpoints=["/connect-config"],
@@ -122,6 +123,7 @@ def test_config_endpoints_use_base_url_from_env_when_yaml_missing():
122
123
  root = Path(tmpdir)
123
124
  cfg = WupConfig(
124
125
  project=ProjectConfig(name="demo"),
126
+ services=[ServiceConfig(name="connect-data", paths=["connect-data/**"])],
125
127
  testql=TestQLConfig(
126
128
  base_url="",
127
129
  base_url_env="WUP_BASE_URL",
@@ -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.26"
10
+ __version__ = "0.2.27"
11
11
  __author__ = "Tom Sapletta"
12
12
 
13
13
  from .config import load_config, save_config, get_default_config
@@ -31,8 +31,16 @@ def watch(
31
31
  debounce: int = typer.Option(2, "--debounce", "-b", help="Debounce time in seconds"),
32
32
  cooldown: int = typer.Option(300, "--cooldown", "-t", help="Test cooldown in seconds"),
33
33
  dashboard: bool = typer.Option(False, "--dashboard", help="Enable live dashboard"),
34
- mode: str = typer.Option("default", "--mode", help="Watcher mode: default or testql"),
35
- scenarios_dir: str = typer.Option("testql-scenarios", "--scenarios-dir", help="Directory with TestQL scenario files"),
34
+ mode: str = typer.Option(
35
+ "testql",
36
+ "--mode",
37
+ help="Watcher mode: testql (default) or default (HTTP-only, no TestQL)",
38
+ ),
39
+ scenarios_dir: Optional[str] = typer.Option(
40
+ None,
41
+ "--scenarios-dir",
42
+ help="TestQL scenarios directory (default: testql.scenario_dir from wup.yaml)",
43
+ ),
36
44
  testql_bin: str = typer.Option("testql", "--testql-bin", help="TestQL executable name/path"),
37
45
  browser_service_url: Optional[str] = typer.Option(None, "--browser-service-url", help="HTTP endpoint for browser notifications"),
38
46
  track_dir: str = typer.Option(".wup/tracks", "--track-dir", help="Directory where error track JSON files are written"),
@@ -40,17 +48,16 @@ def watch(
40
48
  probe_interval: Optional[int] = typer.Option(
41
49
  None,
42
50
  "--probe-interval",
43
- help="Periodic live HTTP/TestQL probes in seconds (overrides testql.probe_interval_s)",
51
+ help="Periodic live HTTP probes in seconds (default: 60 in testql mode, or testql.probe_interval_s from wup.yaml; use 0 to disable)",
44
52
  ),
45
53
  config: Optional[str] = typer.Option(None, "--config", "-C", help="Path to wup.yaml config file"),
46
54
  ):
47
55
  """
48
- Watch project for file changes and run intelligent regression tests.
49
-
50
- Uses a 3-layer approach:
51
- 1. Detection: File watching with heuristics
52
- 2. Priority: Quick tests of related services (3 endpoints max)
53
- 3. Detail: Full tests with blame reports (only on failure)
56
+ Watch project for file changes and run regression tests.
57
+
58
+ Defaults (no extra flags): ``--mode testql`` and live probes every **60s**
59
+ (unless ``testql.probe_interval_s`` is set in wup.yaml). Use
60
+ ``--mode default`` for the legacy HTTP-only watcher without TestQL.
54
61
  """
55
62
  project_path = Path(project).resolve()
56
63
 
@@ -63,6 +70,10 @@ def watch(
63
70
  wup_config = load_config(project_path, config_path)
64
71
  if probe_interval is not None:
65
72
  wup_config.testql.probe_interval_s = int(probe_interval)
73
+ elif mode.lower() == "testql" and not wup_config.testql.probe_interval_s:
74
+ wup_config.testql.probe_interval_s = 60
75
+
76
+ effective_scenarios_dir = scenarios_dir or wup_config.testql.scenario_dir
66
77
 
67
78
  console.print(f"[bold cyan]🚀 WUP Watcher[/bold cyan]")
68
79
  console.print(f"[dim]Project: {wup_config.project.name}[/dim]")
@@ -92,7 +103,7 @@ def watch(
92
103
  cpu_throttle=cpu_throttle,
93
104
  debounce_seconds=debounce,
94
105
  test_cooldown_seconds=cooldown,
95
- scenarios_dir=scenarios_dir,
106
+ scenarios_dir=effective_scenarios_dir,
96
107
  testql_bin=testql_bin,
97
108
  browser_service_url=browser_service_url,
98
109
  track_dir=track_dir,
@@ -178,9 +178,12 @@ def validate_config(raw: dict) -> WupConfig:
178
178
  endpoint_discovery=testql_raw.get("endpoint_discovery", True),
179
179
  probe_interval_s=int(testql_raw.get("probe_interval_s", 0) or 0),
180
180
  health_scenario=testql_raw.get("health_scenario", ""),
181
+ health_scenario_strict=bool(testql_raw.get("health_scenario_strict", False)),
181
182
  service_map_globs=testql_raw.get("service_map_globs", []),
182
183
  base_url=testql_raw.get("base_url", ""),
184
+ api_base_url=testql_raw.get("api_base_url", ""),
183
185
  base_url_env=testql_raw.get("base_url_env", "WUP_BASE_URL"),
186
+ service_base_urls=testql_raw.get("service_base_urls", {}),
184
187
  explicit_endpoints=testql_raw.get("explicit_endpoints", []),
185
188
  endpoints_by_service=testql_raw.get("endpoints_by_service", {})
186
189
  )
@@ -215,7 +218,8 @@ def validate_config(raw: dict) -> WupConfig:
215
218
  snapshot_dir=vd_raw.get("snapshot_dir", ".wup/visual-snapshots"),
216
219
  diff_dir=vd_raw.get("diff_dir", ".wup/visual-diffs"),
217
220
  pages=vd_raw.get("pages", []),
218
- pages_from_endpoints=vd_raw.get("pages_from_endpoints", True),
221
+ pages_from_endpoints=vd_raw.get("pages_from_endpoints", False),
222
+ max_pages=int(vd_raw.get("max_pages", 5)),
219
223
  threshold_added=int(vd_raw.get("threshold_added", 3)),
220
224
  threshold_removed=int(vd_raw.get("threshold_removed", 3)),
221
225
  threshold_changed=int(vd_raw.get("threshold_changed", 5)),
@@ -304,7 +308,7 @@ def save_config(config: WupConfig, output_path: Path):
304
308
  f"# wupbro (optional dashboard): pip install wupbro",
305
309
  f"#",
306
310
  f"# Quick Start:",
307
- f"# 1. wup watch . # Start watching",
311
+ f"# 1. wup watch . # TestQL + live probes every 60s",
308
312
  f"# 2. wup watch . --dashboard # With live dashboard",
309
313
  f"# 3. wup map-deps . # Build dependency map",
310
314
  f"#",
@@ -333,9 +337,12 @@ def save_config(config: WupConfig, output_path: Path):
333
337
  "endpoint_discovery": config.testql.endpoint_discovery,
334
338
  "probe_interval_s": config.testql.probe_interval_s,
335
339
  "health_scenario": config.testql.health_scenario,
340
+ "health_scenario_strict": config.testql.health_scenario_strict,
336
341
  "service_map_globs": config.testql.service_map_globs,
337
342
  "base_url": config.testql.base_url,
343
+ "api_base_url": config.testql.api_base_url,
338
344
  "base_url_env": config.testql.base_url_env,
345
+ "service_base_urls": config.testql.service_base_urls,
339
346
  "explicit_endpoints": config.testql.explicit_endpoints,
340
347
  "endpoints_by_service": config.testql.endpoints_by_service,
341
348
  },
@@ -349,6 +356,7 @@ def save_config(config: WupConfig, output_path: Path):
349
356
  "diff_dir": config.visual_diff.diff_dir,
350
357
  "pages": config.visual_diff.pages,
351
358
  "pages_from_endpoints": config.visual_diff.pages_from_endpoints,
359
+ "max_pages": config.visual_diff.max_pages,
352
360
  "threshold_added": config.visual_diff.threshold_added,
353
361
  "threshold_removed": config.visual_diff.threshold_removed,
354
362
  "threshold_changed": config.visual_diff.threshold_changed,
@@ -56,16 +56,20 @@ class TestStrategyConfig:
56
56
  @dataclass
57
57
  class TestQLConfig:
58
58
  """TestQL-specific configuration."""
59
+ __test__ = False
59
60
  scenario_dir: str = "scenarios/tests"
60
61
  smoke_scenario: str = "smoke.testql.toon.yaml"
61
62
  output_format: str = "json"
62
63
  extra_args: List[str] = field(default_factory=lambda: ["--timeout 10s"])
63
64
  endpoint_discovery: bool = True # Merge health probes from scenarios + service maps
64
65
  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
+ health_scenario: str = "" # Fleet TestQL scenario on each periodic probe cycle (live run)
67
+ health_scenario_strict: bool = False # If false, fleet scenario failure is logged but does not block per-service probes
66
68
  service_map_globs: List[str] = field(default_factory=list) # e.g. testql-testing/service-map/*.yaml
67
69
  base_url: str = ""
70
+ api_base_url: str = "" # Core API (c2004: http://localhost:8101) — used for backend probes
68
71
  base_url_env: str = "WUP_BASE_URL"
72
+ service_base_urls: Dict[str, str] = field(default_factory=dict) # optional per-service override
69
73
  explicit_endpoints: List[str] = field(default_factory=list)
70
74
  endpoints_by_service: Dict[str, List[str]] = field(default_factory=dict)
71
75
 
@@ -81,7 +85,8 @@ class VisualDiffConfig:
81
85
  snapshot_dir: str = ".wup/visual-snapshots"
82
86
  diff_dir: str = ".wup/visual-diffs"
83
87
  pages: List[str] = field(default_factory=list) # explicit page paths to scan
84
- pages_from_endpoints: bool = True # infer pages from explicit_endpoints
88
+ pages_from_endpoints: bool = True
89
+ max_pages: int = 5 # cap DOM scans per service per file change
85
90
  threshold_added: int = 3 # min added nodes to report
86
91
  threshold_removed: int = 3 # min removed nodes to report
87
92
  threshold_changed: int = 5 # min changed attrs to report
@@ -22,6 +22,18 @@ _HEALTH_HINT = re.compile(
22
22
  r"(/health|/healthz|/ready|/live|/status|/openapi\.json|/execution/status|/execution/logs)",
23
23
  re.IGNORECASE,
24
24
  )
25
+ # Connect module APIs live on :8103+ — not valid health probes on frontend proxy :8100
26
+ _CONNECT_API_PREFIXES = (
27
+ "/api/id",
28
+ "/api/manager",
29
+ "/api/scenario",
30
+ "/api/test",
31
+ "/api/template",
32
+ "/api/cql",
33
+ "/api/v1/data",
34
+ "/api/v2/menu",
35
+ )
36
+ _PATH_TOKEN_BLOCKLIST = frozenset({"api", "app", "src", "lib", "bin", "dist", "out"})
25
37
 
26
38
 
27
39
  @dataclass(frozen=True)
@@ -102,12 +114,28 @@ def parse_service_map_probes(map_path: Path) -> List[ProbeTarget]:
102
114
  return probes
103
115
 
104
116
 
117
+ def _connect_module_api_on_frontend_proxy(probe: ProbeTarget) -> bool:
118
+ """True when a connect-* API path would be wrongly probed via :8100."""
119
+ if not probe.url.startswith("http"):
120
+ return False
121
+ parsed = urlparse(probe.url)
122
+ if parsed.port not in (None, 8100):
123
+ return False
124
+ path = (parsed.path or "").lower()
125
+ return any(path.startswith(prefix) for prefix in _CONNECT_API_PREFIXES)
126
+
127
+
105
128
  def is_monitoring_probe(probe: ProbeTarget) -> bool:
106
129
  """True when this endpoint should be used for live service health checks."""
130
+ if _connect_module_api_on_frontend_proxy(probe):
131
+ return False
107
132
  if probe.url.startswith("http"):
108
133
  path = urlparse(probe.url).path or probe.url
109
134
  else:
110
135
  path = probe.url
136
+ path_lower = path.lower()
137
+ if any(path_lower.startswith(prefix) for prefix in _CONNECT_API_PREFIXES):
138
+ return False
111
139
  if _HEALTH_HINT.search(path):
112
140
  return True
113
141
  # Short GET smoke paths (/, /health) without heavy write APIs
@@ -128,14 +156,38 @@ def _service_path_patterns(services: Sequence[ServiceConfig]) -> Dict[str, List[
128
156
 
129
157
  def assign_probe_to_service(probe: ProbeTarget, services: Sequence[ServiceConfig]) -> Optional[str]:
130
158
  """Map a probe URL/path to a configured WUP service name."""
159
+ wup_names = {s.name.lower() for s in services}
131
160
  path = urlparse(probe.url).path if probe.url.startswith("http") else probe.url
132
161
  path_lower = path.lower()
133
162
 
163
+ if probe.url.startswith("http"):
164
+ parsed = urlparse(probe.url)
165
+ port = parsed.port
166
+ if port == 8101 and "backend" in wup_names:
167
+ return next(s.name for s in services if s.name.lower() == "backend")
168
+ if port == 8202:
169
+ for svc in services:
170
+ if "firmware" in svc.name.lower():
171
+ return svc.name
172
+ if port == 8100:
173
+ if path_lower.startswith("/firmware"):
174
+ for svc in services:
175
+ if "firmware" in svc.name.lower():
176
+ return svc.name
177
+ if "frontend" in wup_names:
178
+ return next(s.name for s in services if s.name.lower() == "frontend")
179
+ # Connect-* backends on 8103+ — only if a matching WUP service exists
180
+ for svc in services:
181
+ token = svc.name.lower().replace("_", "-")
182
+ if token.startswith("connect-") and token.replace("connect-", "") in path_lower:
183
+ return svc.name
184
+ return None
185
+
134
186
  best: Optional[str] = None
135
187
  best_len = -1
136
188
  for svc in services:
137
189
  for token in _service_path_patterns([svc]).get(svc.name, []):
138
- if len(token) < 3:
190
+ if len(token) < 4 or token in _PATH_TOKEN_BLOCKLIST:
139
191
  continue
140
192
  if token in path_lower and len(token) > best_len:
141
193
  best = svc.name
@@ -148,7 +200,7 @@ def assign_probe_to_service(probe: ProbeTarget, services: Sequence[ServiceConfig
148
200
  for svc in services:
149
201
  if "firmware" in svc.name.lower():
150
202
  return svc.name
151
- if path_lower.startswith("/api/"):
203
+ if path_lower.startswith("/api/v3"):
152
204
  for svc in services:
153
205
  if svc.name.lower() in {"backend", "api"}:
154
206
  return svc.name
@@ -193,23 +245,28 @@ class TestQLMonitor:
193
245
  seen[service].add(key)
194
246
  by_service[service].append(probe)
195
247
 
196
- # 1) Config-declared endpoints (paths or full URLs)
197
- base = self._resolve_base_url()
248
+ # 1) Config-declared endpoints (paths or full URLs) — per-service base URL
198
249
  for svc_name, paths in (self.config.testql.endpoints_by_service or {}).items():
250
+ base = self._resolve_base_url_for_service(svc_name)
199
251
  for path in paths:
200
252
  url = self._probeable_url(path, base)
201
253
  if not url:
202
254
  continue
203
255
  probe = ProbeTarget(url=url, source="wup.yaml:endpoints_by_service")
204
- add(svc_name, probe)
256
+ if is_monitoring_probe(probe):
257
+ add(svc_name, probe)
205
258
 
206
259
  for path in self.config.testql.explicit_endpoints or []:
260
+ probe = ProbeTarget(url=path, source="wup.yaml:explicit_endpoints")
261
+ assigned = assign_probe_to_service(probe, self.config.services)
262
+ if not assigned:
263
+ continue
264
+ base = self._resolve_base_url_for_service(assigned)
207
265
  url = self._probeable_url(path, base)
208
266
  if not url:
209
267
  continue
210
268
  probe = ProbeTarget(url=url, source="wup.yaml:explicit_endpoints")
211
- assigned = assign_probe_to_service(probe, self.config.services)
212
- if assigned:
269
+ if is_monitoring_probe(probe):
213
270
  add(assigned, probe)
214
271
 
215
272
  if not self.config.testql.endpoint_discovery:
@@ -235,6 +292,19 @@ class TestQLMonitor:
235
292
 
236
293
  return by_service
237
294
 
295
+ def _resolve_base_url_for_service(self, service: str) -> str:
296
+ tq = self.config.testql
297
+ overrides = getattr(tq, "service_base_urls", None) or {}
298
+ if isinstance(overrides, dict):
299
+ override = (overrides.get(service) or "").strip().rstrip("/")
300
+ if override:
301
+ return override
302
+ if service.lower() in {"backend", "api"}:
303
+ api_base = (getattr(tq, "api_base_url", None) or "").strip().rstrip("/")
304
+ if api_base:
305
+ return api_base
306
+ return self._resolve_base_url()
307
+
238
308
  def _probeable_url(self, path: str, base: str) -> Optional[str]:
239
309
  if path.startswith("http://") or path.startswith("https://"):
240
310
  return path
@@ -245,7 +315,7 @@ class TestQLMonitor:
245
315
  def probes_for_service(self, service: str, extra_paths: Iterable[str] = ()) -> List[ProbeTarget]:
246
316
  """Merged probe list for one service (discovery + config + caller extras)."""
247
317
  discovered = self.discover_probes_by_service().get(service, [])
248
- base = self._resolve_base_url()
318
+ base = self._resolve_base_url_for_service(service)
249
319
  merged: List[ProbeTarget] = list(discovered)
250
320
  keys = {f"{p.method}:{p.url}" for p in merged}
251
321
 
@@ -253,21 +323,14 @@ class TestQLMonitor:
253
323
  url = self._probeable_url(path, base)
254
324
  if not url:
255
325
  continue
256
- key = f"GET:{url}"
257
- if key in keys:
326
+ probe = ProbeTarget(url=url, source="runtime")
327
+ if not is_monitoring_probe(probe):
258
328
  continue
259
- keys.add(key)
260
- merged.append(ProbeTarget(url=url, source="runtime"))
261
-
262
- for path in self.config.testql.endpoints_by_service.get(service, []):
263
- url = self._probeable_url(path, base)
264
- if not url:
265
- continue
266
- key = f"GET:{url}"
329
+ key = f"{probe.method}:{probe.url}"
267
330
  if key in keys:
268
331
  continue
269
332
  keys.add(key)
270
- merged.append(ProbeTarget(url=url, source="wup.yaml"))
333
+ merged.append(probe)
271
334
 
272
335
  return [p for p in merged if p.url.startswith("http://") or p.url.startswith("https://")]
273
336
 
@@ -188,17 +188,51 @@ class TestQLWatcher(WupWatcher):
188
188
  return [token for token in raw_tokens if len(token) >= 3]
189
189
 
190
190
  def _get_config_endpoints_for_service(self, service: str) -> List[str]:
191
+ """Endpoints for *service* only — never attach all explicit_endpoints to every service."""
192
+ from .testql_monitor import ProbeTarget, assign_probe_to_service
193
+
191
194
  by_service = self.config.testql.endpoints_by_service or {}
192
195
  explicit = self.config.testql.explicit_endpoints or []
193
196
 
194
- service_specific = by_service.get(service, [])
195
197
  merged: List[str] = []
196
- for endpoint in [*service_specific, *explicit]:
197
- endpoint_url = self._to_full_url(endpoint)
198
- if endpoint_url not in merged:
198
+ for endpoint in by_service.get(service, []):
199
+ endpoint_url = self._to_full_url_for_service(service, endpoint)
200
+ if endpoint_url and endpoint_url not in merged:
199
201
  merged.append(endpoint_url)
202
+
203
+ for endpoint in explicit:
204
+ probe = ProbeTarget(
205
+ url=self._to_full_url_for_service(service, endpoint) or endpoint,
206
+ source="wup.yaml:explicit_endpoints",
207
+ )
208
+ if assign_probe_to_service(probe, self.config.services) == service:
209
+ if probe.url not in merged:
210
+ merged.append(probe.url)
200
211
  return merged
201
212
 
213
+ def _to_full_url_for_service(self, service: str, endpoint: str) -> str:
214
+ if endpoint.startswith("http://") or endpoint.startswith("https://"):
215
+ return endpoint
216
+ base = self._resolve_base_url_for_service(service)
217
+ if not base:
218
+ return endpoint
219
+ if endpoint.startswith("/"):
220
+ return f"{base}{endpoint}"
221
+ return f"{base}/{endpoint}"
222
+
223
+ def _resolve_base_url_for_service(self, service: str) -> str:
224
+ """Per-service base URL (e.g. backend API on :8101, frontend proxy on :8100)."""
225
+ overrides = getattr(self.config.testql, "service_base_urls", None) or {}
226
+ if isinstance(overrides, dict):
227
+ override = (overrides.get(service) or "").strip().rstrip("/")
228
+ if override:
229
+ return override
230
+ if service.lower() in {"backend", "api"}:
231
+ api_base = (getattr(self.config.testql, "api_base_url", None) or "").strip().rstrip("/")
232
+ if api_base:
233
+ return api_base
234
+ return self._resolve_base_url()
235
+
202
236
  def _resolve_base_url(self) -> str:
203
237
  base_url = (self.config.testql.base_url or "").strip()
204
238
  if base_url:
@@ -253,6 +287,8 @@ class TestQLWatcher(WupWatcher):
253
287
  score += 2
254
288
  if "infra" in name or "smoke" in name:
255
289
  score += 1
290
+ if "generated-api-smoke" in name:
291
+ score -= 5
256
292
  return score
257
293
 
258
294
  def _select_scenarios_for_service(self, service: str) -> List[Path]:
@@ -260,20 +296,28 @@ class TestQLWatcher(WupWatcher):
260
296
  if not all_scenarios:
261
297
  return []
262
298
 
299
+ svc_config = self.get_service_config(service)
300
+ limit = (svc_config.quick_tests.max_endpoints
301
+ if svc_config and svc_config.quick_tests else self.quick_limit)
302
+
263
303
  tokens = self._tokenize_service(service)
264
304
  scored = sorted(
265
305
  ((self._score_scenario(s, tokens), s) for s in all_scenarios),
266
306
  key=lambda item: (item[0], item[1].name),
267
307
  reverse=True,
268
308
  )
269
- selected = [s for score, s in scored if score > 0]
309
+ selected = [s for score, s in scored if score > 0][:limit]
270
310
  if selected:
271
311
  return selected
272
312
 
273
- svc_config = self.get_service_config(service)
274
- limit = (svc_config.quick_tests.max_endpoints
275
- if svc_config and svc_config.quick_tests else self.quick_limit)
276
- return all_scenarios if limit >= len(all_scenarios) else all_scenarios[:limit]
313
+ smoke_name = (self.config.testql.smoke_scenario or "").strip()
314
+ if smoke_name:
315
+ for base in (self.scenarios_dir, self.project_root):
316
+ candidate = base / smoke_name
317
+ if candidate.exists():
318
+ return [candidate]
319
+
320
+ return []
277
321
 
278
322
  def _run_testql(self, args: Sequence[str], timeout: int) -> subprocess.CompletedProcess:
279
323
  cmd = [self.testql_bin, *args]
@@ -396,7 +440,8 @@ class TestQLWatcher(WupWatcher):
396
440
  if not self.monitor:
397
441
  return True
398
442
 
399
- probes = self.monitor.probes_for_service(service, merged_endpoints)
443
+ # Health probes come only from wup.yaml + TestQL discovery — not deps.json page routes.
444
+ probes = self.monitor.probes_for_service(service)
400
445
  if not probes:
401
446
  return True
402
447
 
@@ -435,9 +480,12 @@ class TestQLWatcher(WupWatcher):
435
480
 
436
481
  scenario_path = Path(scenario_name)
437
482
  if not scenario_path.is_absolute():
438
- scenario_path = self.project_root / scenario_name
439
- if not scenario_path.exists():
440
- scenario_path = self.scenarios_dir / scenario_name
483
+ candidates = [
484
+ self.scenarios_dir / scenario_name,
485
+ self.project_root / scenario_name,
486
+ self.project_root / self.config.testql.scenario_dir / scenario_name,
487
+ ]
488
+ scenario_path = next((p for p in candidates if p.exists()), candidates[0])
441
489
  if not scenario_path.exists():
442
490
  self.console.print(f"[yellow]⚠ health_scenario not found: {scenario_name}[/yellow]")
443
491
  return True
@@ -455,16 +503,24 @@ class TestQLWatcher(WupWatcher):
455
503
  return True
456
504
 
457
505
  reason = result.stderr.strip() or result.stdout.strip() or "health_scenario failed"
506
+ summary = reason.splitlines()[-1] if reason else "health_scenario failed"
458
507
  track_path = self._write_track(service=fleet, stage="health_scenario", scenario=scenario_path, result=result)
459
508
  self._record_health_transition(
460
509
  service=fleet,
461
510
  status="down",
462
511
  stage="health_scenario",
463
- message=reason,
512
+ message=summary[:500],
464
513
  track_file=str(track_path),
465
514
  )
466
- self.console.print(f"[red]✗ Fleet health scenario failed: {reason}[/red]")
467
- return False
515
+ strict = bool(getattr(self.config.testql, "health_scenario_strict", False))
516
+ if strict:
517
+ self.console.print(f"[red]✗ Fleet health scenario failed: {summary}[/red]")
518
+ return False
519
+ self.console.print(
520
+ f"[yellow]⚠ Fleet health scenario incomplete: {summary} "
521
+ f"(per-service probes continue; set testql.health_scenario_strict: true to hard-fail)[/yellow]"
522
+ )
523
+ return True
468
524
 
469
525
  async def run_quick_test(self, service: str, endpoints: List[str]) -> bool:
470
526
  merged_endpoints = self._merge_endpoints(service, endpoints)
@@ -35,6 +35,7 @@ console = Console()
35
35
  # ---------------------------------------------------------------------------
36
36
 
37
37
  _PW_AVAILABLE: Optional[bool] = None
38
+ _PW_WARNED: bool = False
38
39
 
39
40
 
40
41
  def _playwright_available() -> bool:
@@ -48,6 +49,17 @@ def _playwright_available() -> bool:
48
49
  return _PW_AVAILABLE
49
50
 
50
51
 
52
+ def _warn_playwright_missing() -> None:
53
+ global _PW_WARNED
54
+ if _PW_WARNED:
55
+ return
56
+ _PW_WARNED = True
57
+ console.print(
58
+ "[yellow]visual_diff: playwright not installed — DOM scan skipped "
59
+ "(pip install playwright && playwright install chromium)[/yellow]"
60
+ )
61
+
62
+
51
63
  _DOM_SNAPSHOT_JS = """
52
64
  (maxDepth) => {
53
65
  function snapshot(node, depth) {
@@ -85,7 +97,7 @@ async def _fetch_dom_snapshot(
85
97
  ) -> Optional[Dict]:
86
98
  """Return a DOM structure dict for *url* using Playwright."""
87
99
  if not _playwright_available():
88
- console.print("[yellow]visual_diff: playwright not installed — skipping DOM scan[/yellow]")
100
+ _warn_playwright_missing()
89
101
  return None
90
102
  try:
91
103
  from playwright.async_api import async_playwright
@@ -273,7 +285,8 @@ class VisualDiffer:
273
285
  pages: List[str] = list(self.cfg.pages)
274
286
 
275
287
  if self.cfg.pages_from_endpoints and endpoints:
276
- pages.extend(endpoints)
288
+ for endpoint in endpoints:
289
+ pages.append(endpoint)
277
290
 
278
291
  if not pages:
279
292
  pages = [f"/{service}"]
@@ -297,10 +310,17 @@ class VisualDiffer:
297
310
  if not self.cfg.enabled:
298
311
  return []
299
312
 
313
+ if not _playwright_available():
314
+ _warn_playwright_missing()
315
+ return []
316
+
300
317
  if self.cfg.delay_seconds > 0:
301
318
  await asyncio.sleep(self.cfg.delay_seconds)
302
319
 
303
320
  pages = self._pages_for_service(service, endpoints)
321
+ max_pages = max(1, int(self.cfg.max_pages or 5))
322
+ if len(pages) > max_pages:
323
+ pages = pages[:max_pages]
304
324
  results = []
305
325
  for url in pages:
306
326
  result = await self._check_page(service, url)
@@ -321,7 +341,10 @@ class VisualDiffer:
321
341
  )
322
342
  elif result["diff"]["status"] == "new":
323
343
  console.print(f"[dim]📷 Baseline snapshot: {url}[/dim]")
324
- else:
344
+ elif result["diff"]["status"] == "error":
345
+ message = result["diff"].get("message", "scan failed")
346
+ console.print(f"[yellow]⚠ Visual diff skipped: {url} ({message})[/yellow]")
347
+ elif result["diff"]["status"] == "ok":
325
348
  console.print(f"[dim green]✓ No DOM change: {url}[/dim green]")
326
349
 
327
350
  return results
@@ -26,6 +26,7 @@ from .models.config import WebConfig
26
26
  _console = Console()
27
27
  _HTTPX_AVAILABLE: Optional[bool] = None
28
28
  _HTTPX_WARN_LOGGED: bool = False
29
+ _SEND_FAIL_WARNED: bool = False
29
30
 
30
31
 
31
32
  def _httpx_available() -> bool:
@@ -122,7 +123,13 @@ class WebClient:
122
123
  )
123
124
  return False
124
125
  except Exception as exc: # noqa: BLE001 — soft-fail by design
125
- _console.print(f"[yellow]wup.web_client: send_event failed ({exc})[/yellow]")
126
+ global _SEND_FAIL_WARNED
127
+ if not _SEND_FAIL_WARNED:
128
+ _SEND_FAIL_WARNED = True
129
+ _console.print(
130
+ f"[yellow]wup.web_client: send_event failed ({exc}) — "
131
+ f"set web.enabled=false or start wupbro at {self.endpoint}[/yellow]"
132
+ )
126
133
  return False
127
134
 
128
135
  async def send_regression(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wup
3
- Version: 0.2.26
3
+ Version: 0.2.27
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.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)
32
+ ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.27-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-$2.13-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-16.3h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
34
34
 
35
- - 🤖 **LLM usage:** $1.9540 (36 commits)
36
- - 👤 **Human dev:** ~$1532 (15.3h @ $100/h, 30min dedup)
35
+ - 🤖 **LLM usage:** $2.1268 (37 commits)
36
+ - 👤 **Human dev:** ~$1632 (16.3h @ $100/h, 30min dedup)
37
37
 
38
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.26-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.27-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
 
@@ -83,7 +83,7 @@ wup assistant --quick --template fastapi
83
83
  # 3. Build dependency map (one-time setup)
84
84
  wup map-deps ./my-project
85
85
 
86
- # 4. Start watching for changes
86
+ # 4. Start watching (TestQL + live probes every 60s by default)
87
87
  wup watch ./my-project
88
88
 
89
89
  # 5. Start with live dashboard
@@ -110,14 +110,18 @@ wup map-deps ./my-project --output my-deps.json
110
110
  ### Watch Project
111
111
 
112
112
  ```bash
113
- # Basic watching (uses wup.yaml if present)
113
+ # Basic watching: TestQL mode + live HTTP probes every 60s (uses wup.yaml if present)
114
114
  wup watch ./my-project
115
115
 
116
+ # Legacy HTTP-only watcher (no TestQL, no periodic probes unless configured)
117
+ wup watch ./my-project --mode default --probe-interval 0
118
+
116
119
  # With custom settings
117
120
  wup watch ./my-project \
118
121
  --cpu-throttle 0.5 \
119
122
  --debounce 3 \
120
- --cooldown 600
123
+ --cooldown 600 \
124
+ --probe-interval 120
121
125
 
122
126
  # With live dashboard
123
127
  wup watch ./my-project --dashboard
@@ -125,9 +129,6 @@ wup watch ./my-project --dashboard
125
129
  # Use specific config file
126
130
  wup watch ./my-project --config custom-config.yaml
127
131
 
128
- # TestQL mode
129
- wup watch ./my-project --mode testql
130
-
131
132
  # Discover endpoints from TestQL scenarios
132
133
  wup testql-endpoints /path/to/scenarios --output testql-deps.json
133
134
  ```
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes