wup 0.2.27__tar.gz → 0.2.29__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.27/wup.egg-info → wup-0.2.29}/PKG-INFO +7 -7
- {wup-0.2.27 → wup-0.2.29}/README.md +6 -6
- {wup-0.2.27 → wup-0.2.29}/pyproject.toml +1 -1
- {wup-0.2.27 → wup-0.2.29}/tests/test_testql_monitor.py +7 -0
- {wup-0.2.27 → wup-0.2.29}/tests/test_testql_watcher.py +78 -1
- {wup-0.2.27 → wup-0.2.29}/wup/__init__.py +1 -1
- {wup-0.2.27 → wup-0.2.29}/wup/models/config.py +1 -0
- {wup-0.2.27 → wup-0.2.29}/wup/testql_monitor.py +31 -1
- {wup-0.2.27 → wup-0.2.29}/wup/testql_watcher.py +56 -4
- {wup-0.2.27 → wup-0.2.29/wup.egg-info}/PKG-INFO +7 -7
- {wup-0.2.27 → wup-0.2.29}/LICENSE +0 -0
- {wup-0.2.27 → wup-0.2.29}/setup.cfg +0 -0
- {wup-0.2.27 → wup-0.2.29}/tests/test_e2e.py +0 -0
- {wup-0.2.27 → wup-0.2.29}/tests/test_monitoring_manifest.py +0 -0
- {wup-0.2.27 → wup-0.2.29}/tests/test_web_client.py +0 -0
- {wup-0.2.27 → wup-0.2.29}/tests/test_wup.py +0 -0
- {wup-0.2.27 → wup-0.2.29}/wup/_ast_detector.py +0 -0
- {wup-0.2.27 → wup-0.2.29}/wup/_hash_detector.py +0 -0
- {wup-0.2.27 → wup-0.2.29}/wup/_yaml_detector.py +0 -0
- {wup-0.2.27 → wup-0.2.29}/wup/anomaly_detector.py +0 -0
- {wup-0.2.27 → wup-0.2.29}/wup/anomaly_models.py +0 -0
- {wup-0.2.27 → wup-0.2.29}/wup/assistant.py +0 -0
- {wup-0.2.27 → wup-0.2.29}/wup/cli.py +0 -0
- {wup-0.2.27 → wup-0.2.29}/wup/config.py +0 -0
- {wup-0.2.27 → wup-0.2.29}/wup/core.py +0 -0
- {wup-0.2.27 → wup-0.2.29}/wup/dependency_mapper.py +0 -0
- {wup-0.2.27 → wup-0.2.29}/wup/models/__init__.py +0 -0
- {wup-0.2.27 → wup-0.2.29}/wup/monitoring_manifest.py +0 -0
- {wup-0.2.27 → wup-0.2.29}/wup/testql_discovery.py +0 -0
- {wup-0.2.27 → wup-0.2.29}/wup/visual_diff.py +0 -0
- {wup-0.2.27 → wup-0.2.29}/wup/web_client.py +0 -0
- {wup-0.2.27 → wup-0.2.29}/wup.egg-info/SOURCES.txt +0 -0
- {wup-0.2.27 → wup-0.2.29}/wup.egg-info/dependency_links.txt +0 -0
- {wup-0.2.27 → wup-0.2.29}/wup.egg-info/entry_points.txt +0 -0
- {wup-0.2.27 → wup-0.2.29}/wup.egg-info/requires.txt +0 -0
- {wup-0.2.27 → wup-0.2.29}/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.29
|
|
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:** $2.
|
|
36
|
-
- 👤 **Human dev:** ~$
|
|
35
|
+
- 🤖 **LLM usage:** $2.2757 (39 commits)
|
|
36
|
+
- 👤 **Human dev:** ~$1732 (17.3h @ $100/h, 30min dedup)
|
|
37
37
|
|
|
38
|
-
Generated on 2026-05-
|
|
38
|
+
Generated on 2026-05-17 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
|
|
|
@@ -3,17 +3,17 @@
|
|
|
3
3
|
|
|
4
4
|
## AI Cost Tracking
|
|
5
5
|
|
|
6
|
-
    
|
|
7
|
+
  
|
|
8
8
|
|
|
9
|
-
- 🤖 **LLM usage:** $2.
|
|
10
|
-
- 👤 **Human dev:** ~$
|
|
9
|
+
- 🤖 **LLM usage:** $2.2757 (39 commits)
|
|
10
|
+
- 👤 **Human dev:** ~$1732 (17.3h @ $100/h, 30min dedup)
|
|
11
11
|
|
|
12
|
-
Generated on 2026-05-
|
|
12
|
+
Generated on 2026-05-17 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
|
|
|
@@ -34,6 +34,13 @@ API[1]{method, endpoint, expected_status}:
|
|
|
34
34
|
assert is_monitoring_probe(probes[0])
|
|
35
35
|
|
|
36
36
|
|
|
37
|
+
def test_firmware_plugin_health_on_8202_not_live_probe():
|
|
38
|
+
probe = ProbeTarget(url="http://localhost:8202/api/v1/plugins/modbus-io/health")
|
|
39
|
+
assert not is_monitoring_probe(probe)
|
|
40
|
+
direct = ProbeTarget(url="http://localhost:8202/health")
|
|
41
|
+
assert is_monitoring_probe(direct)
|
|
42
|
+
|
|
43
|
+
|
|
37
44
|
def test_connect_api_paths_on_8100_are_not_monitoring_probes():
|
|
38
45
|
probe = ProbeTarget(url="http://localhost:8100/api/id/health")
|
|
39
46
|
assert not is_monitoring_probe(probe)
|
|
@@ -6,7 +6,14 @@ 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
|
|
9
|
+
from wup.models.config import (
|
|
10
|
+
ProjectConfig,
|
|
11
|
+
ServiceConfig,
|
|
12
|
+
TestQLConfig,
|
|
13
|
+
VisualDiffConfig,
|
|
14
|
+
WatchConfig,
|
|
15
|
+
WupConfig,
|
|
16
|
+
)
|
|
10
17
|
|
|
11
18
|
|
|
12
19
|
def test_process_changed_file_creates_track_on_failure():
|
|
@@ -203,6 +210,76 @@ def test_service_health_transitions_are_persisted():
|
|
|
203
210
|
assert "up" in statuses
|
|
204
211
|
|
|
205
212
|
|
|
213
|
+
def test_normalize_fleet_health_entry_down_to_degraded():
|
|
214
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
215
|
+
root = Path(tmpdir)
|
|
216
|
+
health_path = root / ".wup" / "service-health.json"
|
|
217
|
+
health_path.parent.mkdir(parents=True)
|
|
218
|
+
health_path.write_text(
|
|
219
|
+
json.dumps(
|
|
220
|
+
{
|
|
221
|
+
"demo": {
|
|
222
|
+
"status": "down",
|
|
223
|
+
"stage": "health_scenario",
|
|
224
|
+
"message": "partial",
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
),
|
|
228
|
+
encoding="utf-8",
|
|
229
|
+
)
|
|
230
|
+
cfg = WupConfig(
|
|
231
|
+
project=ProjectConfig(name="demo"),
|
|
232
|
+
services=[ServiceConfig(name="frontend", paths=["frontend/**"])],
|
|
233
|
+
watch=WatchConfig(),
|
|
234
|
+
testql=TestQLConfig(health_scenario_strict=False),
|
|
235
|
+
)
|
|
236
|
+
TestQLWatcher(
|
|
237
|
+
project_root=str(root),
|
|
238
|
+
deps_file=str(root / "deps.json"),
|
|
239
|
+
scenarios_dir="testql-scenarios",
|
|
240
|
+
track_dir=".wup/tracks",
|
|
241
|
+
config=cfg,
|
|
242
|
+
)
|
|
243
|
+
state = json.loads(health_path.read_text(encoding="utf-8"))
|
|
244
|
+
assert state["demo"]["status"] == "degraded"
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def test_fleet_health_scenario_non_strict_records_degraded_not_down():
|
|
248
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
249
|
+
root = Path(tmpdir)
|
|
250
|
+
scenario_dir = root / "testql-scenarios"
|
|
251
|
+
scenario_dir.mkdir(parents=True, exist_ok=True)
|
|
252
|
+
(scenario_dir / "fleet.testql.toon.yaml").write_text("name: fleet\n", encoding="utf-8")
|
|
253
|
+
|
|
254
|
+
cfg = WupConfig(
|
|
255
|
+
project=ProjectConfig(name="demo"),
|
|
256
|
+
services=[ServiceConfig(name="frontend", paths=["frontend/**"])],
|
|
257
|
+
watch=WatchConfig(),
|
|
258
|
+
testql=TestQLConfig(
|
|
259
|
+
scenario_dir="testql-scenarios",
|
|
260
|
+
health_scenario="fleet.testql.toon.yaml",
|
|
261
|
+
health_scenario_strict=False,
|
|
262
|
+
),
|
|
263
|
+
)
|
|
264
|
+
watcher = TestQLWatcher(
|
|
265
|
+
project_root=str(root),
|
|
266
|
+
deps_file=str(root / "deps.json"),
|
|
267
|
+
scenarios_dir="testql-scenarios",
|
|
268
|
+
track_dir=".wup/tracks",
|
|
269
|
+
config=cfg,
|
|
270
|
+
)
|
|
271
|
+
watcher._run_testql = lambda args, timeout: CompletedProcess( # type: ignore[method-assign]
|
|
272
|
+
args=args,
|
|
273
|
+
returncode=1,
|
|
274
|
+
stdout='{"passed": 1, "failed": 1, "errors": ["L1: bad"]}',
|
|
275
|
+
stderr="",
|
|
276
|
+
)
|
|
277
|
+
assert asyncio.run(watcher._run_fleet_health_scenario()) is True
|
|
278
|
+
state = json.loads((root / ".wup" / "service-health.json").read_text(encoding="utf-8"))
|
|
279
|
+
assert state["demo"]["status"] == "degraded"
|
|
280
|
+
assert state["demo"]["stage"] == "health_scenario"
|
|
281
|
+
|
|
282
|
+
|
|
206
283
|
def test_visual_differ_disabled_by_default():
|
|
207
284
|
"""visual_differ exists but is disabled (no-op) when visual_diff.enabled=False."""
|
|
208
285
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
@@ -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.29"
|
|
11
11
|
__author__ = "Tom Sapletta"
|
|
12
12
|
|
|
13
13
|
from .config import load_config, save_config, get_default_config
|
|
@@ -49,6 +49,7 @@ class WatchConfig:
|
|
|
49
49
|
@dataclass
|
|
50
50
|
class TestStrategyConfig:
|
|
51
51
|
"""Global test strategy configuration."""
|
|
52
|
+
__test__ = False
|
|
52
53
|
quick: Dict = field(default_factory=lambda: {"debounce_s": 2, "max_queue": 5, "timeout_s": 10})
|
|
53
54
|
detail: Dict = field(default_factory=lambda: {"debounce_s": 10, "max_queue": 1, "timeout_s": 30})
|
|
54
55
|
|
|
@@ -125,10 +125,23 @@ def _connect_module_api_on_frontend_proxy(probe: ProbeTarget) -> bool:
|
|
|
125
125
|
return any(path.startswith(prefix) for prefix in _CONNECT_API_PREFIXES)
|
|
126
126
|
|
|
127
127
|
|
|
128
|
+
def _firmware_plugin_probe_without_runtime(probe: ProbeTarget) -> bool:
|
|
129
|
+
"""Plugin health on :8202 requires loaded plugins — skip for bare simulator live probes."""
|
|
130
|
+
if not probe.url.startswith("http"):
|
|
131
|
+
return False
|
|
132
|
+
parsed = urlparse(probe.url)
|
|
133
|
+
if parsed.port != 8202:
|
|
134
|
+
return False
|
|
135
|
+
path = (parsed.path or "").lower()
|
|
136
|
+
return "/api/v1/plugins/" in path and path.endswith("/health")
|
|
137
|
+
|
|
138
|
+
|
|
128
139
|
def is_monitoring_probe(probe: ProbeTarget) -> bool:
|
|
129
140
|
"""True when this endpoint should be used for live service health checks."""
|
|
130
141
|
if _connect_module_api_on_frontend_proxy(probe):
|
|
131
142
|
return False
|
|
143
|
+
if _firmware_plugin_probe_without_runtime(probe):
|
|
144
|
+
return False
|
|
132
145
|
if probe.url.startswith("http"):
|
|
133
146
|
path = urlparse(probe.url).path or probe.url
|
|
134
147
|
else:
|
|
@@ -214,6 +227,8 @@ def assign_probe_to_service(probe: ProbeTarget, services: Sequence[ServiceConfig
|
|
|
214
227
|
class TestQLMonitor:
|
|
215
228
|
"""Build and run live probes from TestQL scenarios + WUP config."""
|
|
216
229
|
|
|
230
|
+
__test__ = False
|
|
231
|
+
|
|
217
232
|
def __init__(self, project_root: Path, config: WupConfig):
|
|
218
233
|
self.project_root = project_root
|
|
219
234
|
self.config = config
|
|
@@ -334,6 +349,20 @@ class TestQLMonitor:
|
|
|
334
349
|
|
|
335
350
|
return [p for p in merged if p.url.startswith("http://") or p.url.startswith("https://")]
|
|
336
351
|
|
|
352
|
+
@staticmethod
|
|
353
|
+
def _sort_probes_for_live(probes: Sequence[ProbeTarget]) -> List[ProbeTarget]:
|
|
354
|
+
"""Prefer wup.yaml endpoints before scenario discovery for pass/fail."""
|
|
355
|
+
|
|
356
|
+
def rank(probe: ProbeTarget) -> Tuple[int, str]:
|
|
357
|
+
source = probe.source or ""
|
|
358
|
+
if source.startswith("wup.yaml:endpoints_by_service"):
|
|
359
|
+
return (0, probe.url)
|
|
360
|
+
if source.startswith("wup.yaml:explicit_endpoints"):
|
|
361
|
+
return (1, probe.url)
|
|
362
|
+
return (2, probe.url)
|
|
363
|
+
|
|
364
|
+
return sorted(probes, key=rank)
|
|
365
|
+
|
|
337
366
|
def run_probes(
|
|
338
367
|
self,
|
|
339
368
|
service: str,
|
|
@@ -347,7 +376,8 @@ class TestQLMonitor:
|
|
|
347
376
|
return True, ""
|
|
348
377
|
|
|
349
378
|
failed: List[str] = []
|
|
350
|
-
|
|
379
|
+
ordered = self._sort_probes_for_live(probes)
|
|
380
|
+
for probe in ordered[:max_count]:
|
|
351
381
|
ok, detail = probe.probe(timeout_s=timeout_s)
|
|
352
382
|
if ok:
|
|
353
383
|
continue
|
|
@@ -101,6 +101,27 @@ class TestQLWatcher(WupWatcher):
|
|
|
101
101
|
self.visual_differ = VisualDiffer(project_root, config.visual_diff) if config and config.visual_diff else None
|
|
102
102
|
self.web_client = WebClient(config.web) if config and getattr(config, "web", None) else WebClient()
|
|
103
103
|
self._probe_thread = None
|
|
104
|
+
self._normalize_fleet_health_entry()
|
|
105
|
+
|
|
106
|
+
def _normalize_fleet_health_entry(self) -> None:
|
|
107
|
+
"""Upgrade stale fleet ``down`` to ``degraded`` when health_scenario is non-strict."""
|
|
108
|
+
if not self.config:
|
|
109
|
+
return
|
|
110
|
+
strict = bool(getattr(self.config.testql, "health_scenario_strict", False))
|
|
111
|
+
if strict:
|
|
112
|
+
return
|
|
113
|
+
fleet = self.config.project.name
|
|
114
|
+
entry = self.service_health.get(fleet)
|
|
115
|
+
if not isinstance(entry, dict):
|
|
116
|
+
return
|
|
117
|
+
if entry.get("stage") != "health_scenario":
|
|
118
|
+
return
|
|
119
|
+
if str(entry.get("status", "")).lower() != "down":
|
|
120
|
+
return
|
|
121
|
+
entry = dict(entry)
|
|
122
|
+
entry["status"] = "degraded"
|
|
123
|
+
self.service_health[fleet] = entry
|
|
124
|
+
self._save_service_health()
|
|
104
125
|
|
|
105
126
|
def _load_service_health(self) -> Dict[str, Dict]:
|
|
106
127
|
if not self.health_state_path.exists():
|
|
@@ -472,6 +493,36 @@ class TestQLWatcher(WupWatcher):
|
|
|
472
493
|
)
|
|
473
494
|
return False
|
|
474
495
|
|
|
496
|
+
@staticmethod
|
|
497
|
+
def _summarize_health_scenario_failure(result: subprocess.CompletedProcess) -> str:
|
|
498
|
+
"""Extract a short human summary from TestQL --output json (avoid trailing '}')."""
|
|
499
|
+
blob = "\n".join(part for part in (result.stdout or "", result.stderr or "") if part).strip()
|
|
500
|
+
if not blob:
|
|
501
|
+
return "health_scenario failed"
|
|
502
|
+
|
|
503
|
+
start = blob.rfind("{")
|
|
504
|
+
if start >= 0:
|
|
505
|
+
try:
|
|
506
|
+
data = json.loads(blob[start:])
|
|
507
|
+
except json.JSONDecodeError:
|
|
508
|
+
data = None
|
|
509
|
+
if isinstance(data, dict):
|
|
510
|
+
passed = data.get("passed")
|
|
511
|
+
failed = data.get("failed")
|
|
512
|
+
if isinstance(passed, int) and isinstance(failed, int):
|
|
513
|
+
total = passed + failed
|
|
514
|
+
errors = data.get("errors") or []
|
|
515
|
+
hint = f" — {errors[0]}" if errors else ""
|
|
516
|
+
return f"{passed}/{total} passed, {failed} failed{hint}"
|
|
517
|
+
|
|
518
|
+
for line in reversed(blob.splitlines()):
|
|
519
|
+
stripped = line.strip()
|
|
520
|
+
if not stripped or stripped in {"}", "{"}:
|
|
521
|
+
continue
|
|
522
|
+
if "passed" in stripped.lower() or "failed" in stripped.lower() or "❌" in stripped:
|
|
523
|
+
return stripped
|
|
524
|
+
return "health_scenario failed"
|
|
525
|
+
|
|
475
526
|
async def _run_fleet_health_scenario(self) -> bool:
|
|
476
527
|
"""Optional full TestQL run (not dry-run) for fleet-wide health scenarios."""
|
|
477
528
|
scenario_name = (self.config.testql.health_scenario or "").strip()
|
|
@@ -502,17 +553,18 @@ class TestQLWatcher(WupWatcher):
|
|
|
502
553
|
)
|
|
503
554
|
return True
|
|
504
555
|
|
|
505
|
-
|
|
506
|
-
summary = reason.splitlines()[-1] if reason else "health_scenario failed"
|
|
556
|
+
summary = self._summarize_health_scenario_failure(result)
|
|
507
557
|
track_path = self._write_track(service=fleet, stage="health_scenario", scenario=scenario_path, result=result)
|
|
558
|
+
strict = bool(getattr(self.config.testql, "health_scenario_strict", False))
|
|
559
|
+
# Non-strict: informational only — must not mark fleet "down" (koru reads service-health.json).
|
|
560
|
+
fleet_status = "down" if strict else "degraded"
|
|
508
561
|
self._record_health_transition(
|
|
509
562
|
service=fleet,
|
|
510
|
-
status=
|
|
563
|
+
status=fleet_status,
|
|
511
564
|
stage="health_scenario",
|
|
512
565
|
message=summary[:500],
|
|
513
566
|
track_file=str(track_path),
|
|
514
567
|
)
|
|
515
|
-
strict = bool(getattr(self.config.testql, "health_scenario_strict", False))
|
|
516
568
|
if strict:
|
|
517
569
|
self.console.print(f"[red]✗ Fleet health scenario failed: {summary}[/red]")
|
|
518
570
|
return False
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: wup
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.29
|
|
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:** $2.
|
|
36
|
-
- 👤 **Human dev:** ~$
|
|
35
|
+
- 🤖 **LLM usage:** $2.2757 (39 commits)
|
|
36
|
+
- 👤 **Human dev:** ~$1732 (17.3h @ $100/h, 30min dedup)
|
|
37
37
|
|
|
38
|
-
Generated on 2026-05-
|
|
38
|
+
Generated on 2026-05-17 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
|
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|