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.
- {wup-0.2.26/wup.egg-info → wup-0.2.27}/PKG-INFO +13 -12
- {wup-0.2.26 → wup-0.2.27}/README.md +12 -11
- {wup-0.2.26 → wup-0.2.27}/pyproject.toml +1 -1
- {wup-0.2.26 → wup-0.2.27}/tests/test_testql_monitor.py +40 -0
- {wup-0.2.26 → wup-0.2.27}/tests/test_testql_watcher.py +3 -1
- {wup-0.2.26 → wup-0.2.27}/wup/__init__.py +1 -1
- {wup-0.2.26 → wup-0.2.27}/wup/cli.py +21 -10
- {wup-0.2.26 → wup-0.2.27}/wup/config.py +10 -2
- {wup-0.2.26 → wup-0.2.27}/wup/models/config.py +7 -2
- {wup-0.2.26 → wup-0.2.27}/wup/testql_monitor.py +82 -19
- {wup-0.2.26 → wup-0.2.27}/wup/testql_watcher.py +72 -16
- {wup-0.2.26 → wup-0.2.27}/wup/visual_diff.py +26 -3
- {wup-0.2.26 → wup-0.2.27}/wup/web_client.py +8 -1
- {wup-0.2.26 → wup-0.2.27/wup.egg-info}/PKG-INFO +13 -12
- {wup-0.2.26 → wup-0.2.27}/LICENSE +0 -0
- {wup-0.2.26 → wup-0.2.27}/setup.cfg +0 -0
- {wup-0.2.26 → wup-0.2.27}/tests/test_e2e.py +0 -0
- {wup-0.2.26 → wup-0.2.27}/tests/test_monitoring_manifest.py +0 -0
- {wup-0.2.26 → wup-0.2.27}/tests/test_web_client.py +0 -0
- {wup-0.2.26 → wup-0.2.27}/tests/test_wup.py +0 -0
- {wup-0.2.26 → wup-0.2.27}/wup/_ast_detector.py +0 -0
- {wup-0.2.26 → wup-0.2.27}/wup/_hash_detector.py +0 -0
- {wup-0.2.26 → wup-0.2.27}/wup/_yaml_detector.py +0 -0
- {wup-0.2.26 → wup-0.2.27}/wup/anomaly_detector.py +0 -0
- {wup-0.2.26 → wup-0.2.27}/wup/anomaly_models.py +0 -0
- {wup-0.2.26 → wup-0.2.27}/wup/assistant.py +0 -0
- {wup-0.2.26 → wup-0.2.27}/wup/core.py +0 -0
- {wup-0.2.26 → wup-0.2.27}/wup/dependency_mapper.py +0 -0
- {wup-0.2.26 → wup-0.2.27}/wup/models/__init__.py +0 -0
- {wup-0.2.26 → wup-0.2.27}/wup/monitoring_manifest.py +0 -0
- {wup-0.2.26 → wup-0.2.27}/wup/testql_discovery.py +0 -0
- {wup-0.2.26 → wup-0.2.27}/wup.egg-info/SOURCES.txt +0 -0
- {wup-0.2.26 → wup-0.2.27}/wup.egg-info/dependency_links.txt +0 -0
- {wup-0.2.26 → wup-0.2.27}/wup.egg-info/entry_points.txt +0 -0
- {wup-0.2.26 → wup-0.2.27}/wup.egg-info/requires.txt +0 -0
- {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.
|
|
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
|
-
    
|
|
33
|
+
  
|
|
34
34
|
|
|
35
|
-
- 🤖 **LLM usage:** $
|
|
36
|
-
- 👤 **Human dev:** ~$
|
|
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
|
-
    
|
|
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
|
|
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
|
-
    
|
|
7
|
+
  
|
|
8
8
|
|
|
9
|
-
- 🤖 **LLM usage:** $
|
|
10
|
-
- 👤 **Human dev:** ~$
|
|
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
|
-
    
|
|
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
|
|
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
|
```
|
|
@@ -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.
|
|
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(
|
|
35
|
-
|
|
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
|
|
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
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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=
|
|
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",
|
|
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 . #
|
|
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 = "" #
|
|
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
|
|
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) <
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
257
|
-
if
|
|
326
|
+
probe = ProbeTarget(url=url, source="runtime")
|
|
327
|
+
if not is_monitoring_probe(probe):
|
|
258
328
|
continue
|
|
259
|
-
|
|
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(
|
|
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
|
|
197
|
-
endpoint_url = self.
|
|
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
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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
|
|
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
|
-
|
|
439
|
-
|
|
440
|
-
|
|
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=
|
|
512
|
+
message=summary[:500],
|
|
464
513
|
track_file=str(track_path),
|
|
465
514
|
)
|
|
466
|
-
self.
|
|
467
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
    
|
|
33
|
+
  
|
|
34
34
|
|
|
35
|
-
- 🤖 **LLM usage:** $
|
|
36
|
-
- 👤 **Human dev:** ~$
|
|
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
|
-
    
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|