wup 0.2.25__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.25/wup.egg-info → wup-0.2.27}/PKG-INFO +14 -13
  2. {wup-0.2.25 → wup-0.2.27}/README.md +13 -12
  3. {wup-0.2.25 → wup-0.2.27}/pyproject.toml +1 -1
  4. wup-0.2.27/tests/test_monitoring_manifest.py +72 -0
  5. wup-0.2.27/tests/test_testql_monitor.py +161 -0
  6. {wup-0.2.25 → wup-0.2.27}/tests/test_testql_watcher.py +3 -1
  7. {wup-0.2.25 → wup-0.2.27}/wup/__init__.py +1 -1
  8. {wup-0.2.25 → wup-0.2.27}/wup/cli.py +153 -11
  9. {wup-0.2.25 → wup-0.2.27}/wup/config.py +16 -2
  10. {wup-0.2.25 → wup-0.2.27}/wup/models/config.py +10 -2
  11. wup-0.2.27/wup/monitoring_manifest.py +306 -0
  12. wup-0.2.27/wup/testql_monitor.py +392 -0
  13. {wup-0.2.25 → wup-0.2.27}/wup/testql_watcher.py +194 -9
  14. {wup-0.2.25 → wup-0.2.27}/wup/visual_diff.py +26 -3
  15. {wup-0.2.25 → wup-0.2.27}/wup/web_client.py +8 -1
  16. {wup-0.2.25 → wup-0.2.27/wup.egg-info}/PKG-INFO +14 -13
  17. {wup-0.2.25 → wup-0.2.27}/wup.egg-info/SOURCES.txt +4 -0
  18. {wup-0.2.25 → wup-0.2.27}/LICENSE +0 -0
  19. {wup-0.2.25 → wup-0.2.27}/setup.cfg +0 -0
  20. {wup-0.2.25 → wup-0.2.27}/tests/test_e2e.py +0 -0
  21. {wup-0.2.25 → wup-0.2.27}/tests/test_web_client.py +0 -0
  22. {wup-0.2.25 → wup-0.2.27}/tests/test_wup.py +0 -0
  23. {wup-0.2.25 → wup-0.2.27}/wup/_ast_detector.py +0 -0
  24. {wup-0.2.25 → wup-0.2.27}/wup/_hash_detector.py +0 -0
  25. {wup-0.2.25 → wup-0.2.27}/wup/_yaml_detector.py +0 -0
  26. {wup-0.2.25 → wup-0.2.27}/wup/anomaly_detector.py +0 -0
  27. {wup-0.2.25 → wup-0.2.27}/wup/anomaly_models.py +0 -0
  28. {wup-0.2.25 → wup-0.2.27}/wup/assistant.py +0 -0
  29. {wup-0.2.25 → wup-0.2.27}/wup/core.py +0 -0
  30. {wup-0.2.25 → wup-0.2.27}/wup/dependency_mapper.py +0 -0
  31. {wup-0.2.25 → wup-0.2.27}/wup/models/__init__.py +0 -0
  32. {wup-0.2.25 → wup-0.2.27}/wup/testql_discovery.py +0 -0
  33. {wup-0.2.25 → wup-0.2.27}/wup.egg-info/dependency_links.txt +0 -0
  34. {wup-0.2.25 → wup-0.2.27}/wup.egg-info/entry_points.txt +0 -0
  35. {wup-0.2.25 → wup-0.2.27}/wup.egg-info/requires.txt +0 -0
  36. {wup-0.2.25 → 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.25
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.25-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
33
- ![AI Cost](https://img.shields.io/badge/AI%20Cost-$1.86-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-14.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.8622 (35 commits)
36
- - 👤 **Human dev:** ~$1432 (14.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
- Generated on 2026-05-15 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
38
+ Generated on 2026-05-16 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
39
39
 
40
40
  ---
41
41
 
42
- ![PyPI](https://img.shields.io/badge/pypi-wup-blue) ![Version](https://img.shields.io/badge/version-0.2.25-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.25-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
7
- ![AI Cost](https://img.shields.io/badge/AI%20Cost-$1.86-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-14.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.8622 (35 commits)
10
- - 👤 **Human dev:** ~$1432 (14.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
- Generated on 2026-05-15 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
12
+ Generated on 2026-05-16 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
13
13
 
14
14
  ---
15
15
 
16
- ![PyPI](https://img.shields.io/badge/pypi-wup-blue) ![Version](https://img.shields.io/badge/version-0.2.25-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.25"
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"
@@ -0,0 +1,72 @@
1
+ import tempfile
2
+ from pathlib import Path
3
+
4
+ import yaml
5
+
6
+ from wup.models.config import ProjectConfig, ServiceConfig, TestQLConfig, WupConfig, WatchConfig
7
+ from wup.monitoring_manifest import (
8
+ MANIFEST_BEGIN,
9
+ MANIFEST_END,
10
+ build_monitoring_manifest,
11
+ discover_docker_compose_services,
12
+ load_monitoring_manifest_from_yaml,
13
+ patch_wup_yaml_monitoring,
14
+ )
15
+
16
+
17
+ def test_discover_docker_compose():
18
+ with tempfile.TemporaryDirectory() as tmpdir:
19
+ root = Path(tmpdir)
20
+ (root / "docker-compose.yml").write_text(
21
+ "services:\n"
22
+ " firmware:\n"
23
+ " container_name: test-simulator-firmware\n"
24
+ " ports:\n"
25
+ " - '8202:8202'\n"
26
+ " healthcheck:\n"
27
+ " test: ['CMD', 'curl', 'http://127.0.0.1:8202/health']\n",
28
+ encoding="utf-8",
29
+ )
30
+ found = discover_docker_compose_services(root)
31
+ assert len(found) == 1
32
+ assert found[0].compose_service == "firmware"
33
+ assert found[0].container_name == "test-simulator-firmware"
34
+
35
+
36
+ def test_patch_and_load_monitoring_block():
37
+ with tempfile.TemporaryDirectory() as tmpdir:
38
+ root = Path(tmpdir)
39
+ (root / "docker-compose.yml").write_text(
40
+ "services:\n firmware:\n container_name: test-simulator-firmware\n ports:\n - '8202:8202'\n",
41
+ encoding="utf-8",
42
+ )
43
+ cfg_path = root / "wup.yaml"
44
+ cfg_path.write_text(
45
+ "project:\n name: demo\n"
46
+ "services:\n - name: firmware\n paths: ['backend/firmware/**']\n"
47
+ "testql:\n base_url: http://localhost:8100\n"
48
+ " endpoints_by_service:\n firmware:\n - /firmware/api/v1/health\n",
49
+ encoding="utf-8",
50
+ )
51
+ wup_config = WupConfig(
52
+ project=ProjectConfig(name="demo"),
53
+ services=[ServiceConfig(name="firmware", paths=["backend/firmware/**"])],
54
+ watch=WatchConfig(),
55
+ testql=TestQLConfig(
56
+ base_url="http://localhost:8100",
57
+ endpoints_by_service={"firmware": ["/firmware/api/v1/health"]},
58
+ probe_interval_s=60,
59
+ ),
60
+ )
61
+ manifest = build_monitoring_manifest(root, wup_config)
62
+ patch_wup_yaml_monitoring(cfg_path, manifest)
63
+
64
+ text = cfg_path.read_text(encoding="utf-8")
65
+ assert MANIFEST_BEGIN in text
66
+ assert MANIFEST_END in text
67
+
68
+ loaded = load_monitoring_manifest_from_yaml(cfg_path)
69
+ assert loaded is not None
70
+ assert loaded["wup_services"]["firmware"]["live_probes"]
71
+ assert loaded["wup_services"]["firmware"]["live_probes"][0]["url"].endswith("/firmware/api/v1/health")
72
+ assert loaded["wup_services"]["firmware"]["docker"][0]["compose_service"] == "firmware"
@@ -0,0 +1,161 @@
1
+ import asyncio
2
+ import json
3
+ import tempfile
4
+ from pathlib import Path
5
+ from unittest.mock import patch
6
+
7
+ from wup.models.config import ProjectConfig, ServiceConfig, TestQLConfig, WupConfig, WatchConfig
8
+ from wup.testql_monitor import (
9
+ ProbeTarget,
10
+ TestQLMonitor,
11
+ assign_probe_to_service,
12
+ is_monitoring_probe,
13
+ parse_scenario_probes,
14
+ parse_service_map_probes,
15
+ )
16
+ from wup.testql_watcher import TestQLWatcher
17
+
18
+
19
+ def test_parse_scenario_probes_full_url():
20
+ content = """
21
+ API[1]{method, endpoint, expected_status}:
22
+ GET, http://localhost:8100/firmware/api/v1/health, 200
23
+ """
24
+ with tempfile.NamedTemporaryFile("w", suffix=".testql.toon.yaml", delete=False) as handle:
25
+ handle.write(content)
26
+ handle.flush()
27
+ path = Path(handle.name)
28
+
29
+ probes = parse_scenario_probes(path)
30
+ path.unlink(missing_ok=True)
31
+ assert len(probes) == 1
32
+ assert probes[0].url.endswith("/firmware/api/v1/health")
33
+ assert probes[0].expected_status == 200
34
+ assert is_monitoring_probe(probes[0])
35
+
36
+
37
+ def test_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
+
55
+ def test_assign_firmware_service():
56
+ services = [
57
+ ServiceConfig(name="frontend", paths=["frontend/**"]),
58
+ ServiceConfig(name="firmware", paths=["backend/firmware/**"]),
59
+ ]
60
+ probe = ProbeTarget(url="http://localhost:8100/firmware/api/v1/health")
61
+ assert assign_probe_to_service(probe, services) == "firmware"
62
+
63
+
64
+ def test_monitor_merges_config_and_service_map():
65
+ with tempfile.TemporaryDirectory() as tmpdir:
66
+ root = Path(tmpdir)
67
+ scenario_dir = root / "testql-scenarios"
68
+ scenario_dir.mkdir()
69
+ scenario = scenario_dir / "fleet-health.testql.toon.yaml"
70
+ scenario.write_text(
71
+ "API[1]{method, endpoint, expected_status}:\n"
72
+ " GET, http://localhost:8100/firmware/api/v1/execution/status, 200\n",
73
+ encoding="utf-8",
74
+ )
75
+
76
+ service_map = root / "service-map.yaml"
77
+ service_map.write_text(
78
+ "service:\n base_url: http://localhost:8100\n"
79
+ "endpoints:\n"
80
+ " - { method: GET, path: /firmware/api/v1/health }\n",
81
+ encoding="utf-8",
82
+ )
83
+
84
+ cfg = WupConfig(
85
+ project=ProjectConfig(name="demo"),
86
+ services=[
87
+ ServiceConfig(name="firmware", paths=["backend/firmware/**"]),
88
+ ],
89
+ watch=WatchConfig(),
90
+ testql=TestQLConfig(
91
+ scenario_dir="testql-scenarios",
92
+ base_url="http://localhost:8100",
93
+ endpoint_discovery=True,
94
+ service_map_globs=["service-map.yaml"],
95
+ endpoints_by_service={
96
+ "firmware": ["/firmware/api/v1/execution/logs"],
97
+ },
98
+ ),
99
+ )
100
+ monitor = TestQLMonitor(root, cfg)
101
+ probes = monitor.probes_for_service("firmware")
102
+ urls = {p.url for p in probes}
103
+ assert "http://localhost:8100/firmware/api/v1/health" in urls
104
+ assert "http://localhost:8100/firmware/api/v1/execution/status" in urls
105
+ assert "http://localhost:8100/firmware/api/v1/execution/logs" in urls
106
+
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
+
130
+ def test_live_probe_failure_updates_health():
131
+ with tempfile.TemporaryDirectory() as tmpdir:
132
+ root = Path(tmpdir)
133
+ cfg = WupConfig(
134
+ project=ProjectConfig(name="demo"),
135
+ services=[ServiceConfig(name="firmware", paths=["backend/firmware/**"])],
136
+ watch=WatchConfig(),
137
+ testql=TestQLConfig(
138
+ scenario_dir="testql-scenarios",
139
+ base_url="http://localhost:8100",
140
+ endpoints_by_service={"firmware": ["/firmware/api/v1/health"]},
141
+ ),
142
+ )
143
+ watcher = TestQLWatcher(
144
+ project_root=str(root),
145
+ deps_file=str(root / "deps.json"),
146
+ scenarios_dir="testql-scenarios",
147
+ config=cfg,
148
+ )
149
+
150
+ failing = ProbeTarget(url="http://localhost:8100/firmware/api/v1/health")
151
+
152
+ def fake_probe(self, timeout_s=10.0):
153
+ return False, "HTTP 500 (expected 200)"
154
+
155
+ with patch.object(ProbeTarget, "probe", fake_probe):
156
+ ok = asyncio.run(watcher._run_live_http_probes("firmware", []))
157
+
158
+ assert ok is False
159
+ state = json.loads((root / ".wup" / "service-health.json").read_text(encoding="utf-8"))
160
+ assert state["firmware"]["status"] == "down"
161
+ assert state["firmware"]["stage"] == "probe"
@@ -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.25"
10
+ __version__ = "0.2.27"
11
11
  __author__ = "Tom Sapletta"
12
12
 
13
13
  from .config import load_config, save_config, get_default_config
@@ -9,7 +9,7 @@ from typing import Optional
9
9
  import typer
10
10
  from rich.console import Console
11
11
 
12
- from .config import load_config
12
+ from .config import find_config_file, load_config
13
13
  from .core import WupWatcher
14
14
  from .dependency_mapper import DependencyMapper
15
15
  from .models.config import WupConfig
@@ -31,21 +31,33 @@ 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"),
39
47
  quick_limit: int = typer.Option(3, "--quick-limit", help="Maximum TestQL scenarios used in quick pass"),
48
+ probe_interval: Optional[int] = typer.Option(
49
+ None,
50
+ "--probe-interval",
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)",
52
+ ),
40
53
  config: Optional[str] = typer.Option(None, "--config", "-C", help="Path to wup.yaml config file"),
41
54
  ):
42
55
  """
43
- Watch project for file changes and run intelligent regression tests.
44
-
45
- Uses a 3-layer approach:
46
- 1. Detection: File watching with heuristics
47
- 2. Priority: Quick tests of related services (3 endpoints max)
48
- 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.
49
61
  """
50
62
  project_path = Path(project).resolve()
51
63
 
@@ -56,16 +68,34 @@ def watch(
56
68
  # Load configuration
57
69
  config_path = Path(config) if config else None
58
70
  wup_config = load_config(project_path, config_path)
59
-
71
+ if probe_interval is not None:
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
77
+
60
78
  console.print(f"[bold cyan]🚀 WUP Watcher[/bold cyan]")
61
79
  console.print(f"[dim]Project: {wup_config.project.name}[/dim]")
62
80
  console.print(f"[dim]Description: {wup_config.project.description}[/dim]")
63
81
  console.print(f"[dim]CPU Throttle: {cpu_throttle * 100}%[/dim]")
64
82
  console.print(f"[dim]Debounce: {debounce}s[/dim]")
65
83
  console.print(f"[dim]Cooldown: {cooldown}s[/dim]")
84
+ if wup_config.testql.probe_interval_s:
85
+ console.print(f"[dim]Live probes: every {wup_config.testql.probe_interval_s}s[/dim]")
66
86
  console.print(f"[dim]Config: {config_path or 'auto-detected'}[/dim]")
67
87
  console.print()
68
88
 
89
+ cfg_path = config_path if config_path and config_path.exists() else find_config_file(project_path)
90
+ if cfg_path:
91
+ from .monitoring_manifest import build_monitoring_manifest, patch_wup_yaml_monitoring
92
+ try:
93
+ manifest = build_monitoring_manifest(project_path, wup_config)
94
+ patch_wup_yaml_monitoring(cfg_path, manifest)
95
+ console.print("[dim]Refreshed monitoring manifest in wup.yaml[/dim]")
96
+ except OSError as exc:
97
+ console.print(f"[yellow]Could not refresh monitoring manifest: {exc}[/yellow]")
98
+
69
99
  if mode.lower() == "testql":
70
100
  watcher = TestQLWatcher(
71
101
  project_root=str(project_path),
@@ -73,7 +103,7 @@ def watch(
73
103
  cpu_throttle=cpu_throttle,
74
104
  debounce_seconds=debounce,
75
105
  test_cooldown_seconds=cooldown,
76
- scenarios_dir=scenarios_dir,
106
+ scenarios_dir=effective_scenarios_dir,
77
107
  testql_bin=testql_bin,
78
108
  browser_service_url=browser_service_url,
79
109
  track_dir=track_dir,
@@ -274,6 +304,33 @@ def status(
274
304
  if track_file:
275
305
  lines.append(Text.from_markup(f" [dim]track: {track_file}[/dim]"))
276
306
 
307
+ # --- monitoring manifest (from wup.yaml) ---
308
+ manifest_path = config_path if config_path and config_path.exists() else find_config_file(project_path)
309
+ if manifest_path:
310
+ from .monitoring_manifest import load_monitoring_manifest_from_yaml
311
+
312
+ manifest = load_monitoring_manifest_from_yaml(manifest_path)
313
+ if manifest:
314
+ lines.append(Text(""))
315
+ lines.append(Text.from_markup("[bold]Configured monitoring (wup.yaml):[/bold]"))
316
+ lines.append(Text.from_markup(
317
+ f" [dim]manifest {manifest.get('generated_at', '?')} · "
318
+ f"probe {manifest.get('probe_interval_s', 0)}s[/dim]"
319
+ ))
320
+ for svc, info in sorted((manifest.get("wup_services") or {}).items()):
321
+ probes = info.get("live_probes") or []
322
+ dockers = info.get("docker") or []
323
+ lines.append(Text.from_markup(
324
+ f" [cyan]{svc}[/cyan]: {len(probes)} probe(s), docker: "
325
+ + ", ".join(
326
+ d.get("compose_service", "?") for d in dockers[:4]
327
+ )
328
+ + ("…" if len(dockers) > 4 else "")
329
+ ))
330
+ lines.append(Text.from_markup(
331
+ " [dim]Pełna lista: sekcja monitoring: w wup.yaml (BEGIN WUP MONITORING MANIFEST)[/dim]"
332
+ ))
333
+
277
334
  # --- visual diff section ---
278
335
  if wup_config.visual_diff and wup_config.visual_diff.enabled:
279
336
  from .visual_diff import VisualDiffer
@@ -439,6 +496,91 @@ def map_deps(
439
496
  console.print(f"[dim]Files: {len(deps.get('files', {}))}[/dim]")
440
497
 
441
498
 
499
+ @app.command("sync-testql")
500
+ def sync_testql(
501
+ project: str = typer.Argument(".", help="Path to the project root directory"),
502
+ write: bool = typer.Option(False, "--write", "-w", help="Write monitoring manifest block into wup.yaml"),
503
+ merge_endpoints: bool = typer.Option(
504
+ False,
505
+ "--merge-endpoints",
506
+ help="Also merge discovered paths into testql.endpoints_by_service (rewrites YAML body)",
507
+ ),
508
+ config: Optional[str] = typer.Option(None, "--config", "-C", help="Path to wup.yaml config file"),
509
+ ):
510
+ """
511
+ Discover monitoring targets and document them in wup.yaml.
512
+
513
+ With ``--write``, appends/updates the auto-generated ``monitoring:`` block
514
+ (Docker Compose services, live HTTP probes, sources). Use this to verify
515
+ whether a failure is a WUP config gap vs a down container.
516
+
517
+ Use ``--merge-endpoints`` cautiously — it re-serializes wup.yaml (may drop comments).
518
+ """
519
+ import json
520
+
521
+ from .config import find_config_file, load_config
522
+ from .monitoring_manifest import (
523
+ MANIFEST_BEGIN,
524
+ build_monitoring_manifest,
525
+ format_manifest_summary,
526
+ patch_wup_yaml_monitoring,
527
+ )
528
+ from .testql_monitor import TestQLMonitor
529
+
530
+ project_path = Path(project).resolve()
531
+ if not project_path.exists():
532
+ console.print(f"[red]Error: Project path '{project}' does not exist[/red]")
533
+ raise typer.Exit(1)
534
+
535
+ config_path = Path(config) if config else find_config_file(project_path)
536
+ wup_config = load_config(project_path, config_path)
537
+ monitor = TestQLMonitor(project_path, wup_config)
538
+ suggested = monitor.suggested_endpoints_by_service()
539
+ manifest = build_monitoring_manifest(project_path, wup_config)
540
+
541
+ console.print("[bold]Monitoring manifest (preview):[/bold]")
542
+ console.print(format_manifest_summary(manifest))
543
+
544
+ if suggested:
545
+ console.print()
546
+ console.print("[bold]Suggested testql.endpoints_by_service additions:[/bold]")
547
+ console.print(json.dumps(suggested, indent=2))
548
+
549
+ if not write:
550
+ console.print()
551
+ console.print("[dim]Run: wup sync-testql . --write → dokumentacja w wup.yaml[/dim]")
552
+ return
553
+
554
+ if config_path is None:
555
+ console.print("[red]No wup.yaml found — run `wup init` first[/red]")
556
+ raise typer.Exit(1)
557
+
558
+ if merge_endpoints and suggested:
559
+ import yaml as pyyaml
560
+
561
+ merged = dict(wup_config.testql.endpoints_by_service or {})
562
+ for service, paths in suggested.items():
563
+ existing = set(merged.get(service, []))
564
+ existing.update(paths)
565
+ merged[service] = sorted(existing)
566
+ raw = pyyaml.safe_load(config_path.read_text(encoding="utf-8")) or {}
567
+ raw.setdefault("testql", {})["endpoints_by_service"] = merged
568
+ wup_config.testql.endpoints_by_service = merged
569
+ manifest = build_monitoring_manifest(project_path, wup_config)
570
+ body = pyyaml.safe_dump(
571
+ {k: v for k, v in raw.items() if k != "monitoring"},
572
+ sort_keys=False,
573
+ allow_unicode=True,
574
+ default_flow_style=False,
575
+ )
576
+ config_path.write_text(body.rstrip() + "\n\n", encoding="utf-8")
577
+ console.print("[yellow]Merged endpoints_by_service (review git diff for comment loss)[/yellow]")
578
+
579
+ patch_wup_yaml_monitoring(config_path, manifest)
580
+ console.print(f"[green]✓ monitoring manifest written to {config_path}[/green]")
581
+ console.print(f"[dim]Szukaj w pliku: {MANIFEST_BEGIN}[/dim]")
582
+
583
+
442
584
  @app.command()
443
585
  def assistant(
444
586
  quick: bool = typer.Option(False, "--quick", "-q", help="Non-interactive mode with auto-detected values"),