wup 0.2.68__tar.gz → 0.2.70__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.68/wup.egg-info → wup-0.2.70}/PKG-INFO +31 -7
- {wup-0.2.68 → wup-0.2.70}/README.md +30 -6
- {wup-0.2.68 → wup-0.2.70}/pyproject.toml +1 -1
- wup-0.2.70/tests/test_multi_project.py +117 -0
- wup-0.2.70/tests/test_planfile_reporter_dedupe.py +78 -0
- {wup-0.2.68 → wup-0.2.70}/tests/test_testql_watcher.py +13 -4
- wup-0.2.70/tests/test_watch_no_paths_exit.py +30 -0
- {wup-0.2.68 → wup-0.2.70}/tests/test_wup.py +17 -8
- {wup-0.2.68 → wup-0.2.70}/wup/__init__.py +1 -1
- {wup-0.2.68 → wup-0.2.70}/wup/cli.py +175 -41
- {wup-0.2.68 → wup-0.2.70}/wup/config.py +24 -4
- {wup-0.2.68 → wup-0.2.70}/wup/core.py +71 -21
- {wup-0.2.68 → wup-0.2.70}/wup/models/__init__.py +2 -0
- wup-0.2.70/wup/models/target.py +23 -0
- {wup-0.2.68 → wup-0.2.70}/wup/monitoring_manifest.py +44 -36
- wup-0.2.70/wup/multi.py +81 -0
- {wup-0.2.68 → wup-0.2.70}/wup/planfile_reporter.py +68 -24
- {wup-0.2.68 → wup-0.2.70}/wup/status_data.py +27 -16
- {wup-0.2.68 → wup-0.2.70}/wup/testql_watcher.py +35 -35
- {wup-0.2.68 → wup-0.2.70}/wup/visual_diff.py +8 -4
- {wup-0.2.68 → wup-0.2.70/wup.egg-info}/PKG-INFO +31 -7
- {wup-0.2.68 → wup-0.2.70}/wup.egg-info/SOURCES.txt +5 -0
- {wup-0.2.68 → wup-0.2.70}/LICENSE +0 -0
- {wup-0.2.68 → wup-0.2.70}/setup.cfg +0 -0
- {wup-0.2.68 → wup-0.2.70}/tests/test_assistant.py +0 -0
- {wup-0.2.68 → wup-0.2.70}/tests/test_auto_detection.py +0 -0
- {wup-0.2.68 → wup-0.2.70}/tests/test_cli_bridge.py +0 -0
- {wup-0.2.68 → wup-0.2.70}/tests/test_cli_filtering.py +0 -0
- {wup-0.2.68 → wup-0.2.70}/tests/test_control.py +0 -0
- {wup-0.2.68 → wup-0.2.70}/tests/test_e2e.py +0 -0
- {wup-0.2.68 → wup-0.2.70}/tests/test_endpoints_init_cli.py +0 -0
- {wup-0.2.68 → wup-0.2.70}/tests/test_health_summary_passed.py +0 -0
- {wup-0.2.68 → wup-0.2.70}/tests/test_monitoring_manifest.py +0 -0
- {wup-0.2.68 → wup-0.2.70}/tests/test_probe_mutex.py +0 -0
- {wup-0.2.68 → wup-0.2.70}/tests/test_service_inference.py +0 -0
- {wup-0.2.68 → wup-0.2.70}/tests/test_status_data.py +0 -0
- {wup-0.2.68 → wup-0.2.70}/tests/test_sync.py +0 -0
- {wup-0.2.68 → wup-0.2.70}/tests/test_testql_monitor.py +0 -0
- {wup-0.2.68 → wup-0.2.70}/tests/test_visual_diff_periodic_skip.py +0 -0
- {wup-0.2.68 → wup-0.2.70}/tests/test_visual_diff_progress.py +0 -0
- {wup-0.2.68 → wup-0.2.70}/tests/test_watch_exclude.py +0 -0
- {wup-0.2.68 → wup-0.2.70}/tests/test_web_client.py +0 -0
- {wup-0.2.68 → wup-0.2.70}/tests/test_wup_generate.py +0 -0
- {wup-0.2.68 → wup-0.2.70}/wup/_ast_detector.py +0 -0
- {wup-0.2.68 → wup-0.2.70}/wup/_base_detector.py +0 -0
- {wup-0.2.68 → wup-0.2.70}/wup/_hash_detector.py +0 -0
- {wup-0.2.68 → wup-0.2.70}/wup/_yaml_detector.py +0 -0
- {wup-0.2.68 → wup-0.2.70}/wup/anomaly_detector.py +0 -0
- {wup-0.2.68 → wup-0.2.70}/wup/anomaly_models.py +0 -0
- {wup-0.2.68 → wup-0.2.70}/wup/assistant.py +0 -0
- {wup-0.2.68 → wup-0.2.70}/wup/assistant_discovery.py +0 -0
- {wup-0.2.68 → wup-0.2.70}/wup/assistant_validator.py +0 -0
- {wup-0.2.68 → wup-0.2.70}/wup/bus.py +0 -0
- {wup-0.2.68 → wup-0.2.70}/wup/cli_bridge.py +0 -0
- {wup-0.2.68 → wup-0.2.70}/wup/cli_config_generator.py +0 -0
- {wup-0.2.68 → wup-0.2.70}/wup/cli_scanner.py +0 -0
- {wup-0.2.68 → wup-0.2.70}/wup/control.py +0 -0
- {wup-0.2.68 → wup-0.2.70}/wup/dependency_mapper.py +0 -0
- {wup-0.2.68 → wup-0.2.70}/wup/endpoints.py +0 -0
- {wup-0.2.68 → wup-0.2.70}/wup/event_store.py +0 -0
- {wup-0.2.68 → wup-0.2.70}/wup/file_watcher/events/file_events.py +0 -0
- {wup-0.2.68 → wup-0.2.70}/wup/generate.py +0 -0
- {wup-0.2.68 → wup-0.2.70}/wup/init_cli.py +0 -0
- {wup-0.2.68 → wup-0.2.70}/wup/models/config.py +0 -0
- {wup-0.2.68 → wup-0.2.70}/wup/paths.py +0 -0
- {wup-0.2.68 → wup-0.2.70}/wup/sync.py +0 -0
- {wup-0.2.68 → wup-0.2.70}/wup/testing/events/health_events.py +0 -0
- {wup-0.2.68 → wup-0.2.70}/wup/testing/events/test_results.py +0 -0
- {wup-0.2.68 → wup-0.2.70}/wup/testing/handlers/event_handlers.py +0 -0
- {wup-0.2.68 → wup-0.2.70}/wup/testing/handlers/health_handlers.py +0 -0
- {wup-0.2.68 → wup-0.2.70}/wup/testing/queries/health_queries.py +0 -0
- {wup-0.2.68 → wup-0.2.70}/wup/testql_cli_generator.py +0 -0
- {wup-0.2.68 → wup-0.2.70}/wup/testql_discovery.py +0 -0
- {wup-0.2.68 → wup-0.2.70}/wup/testql_monitor.py +0 -0
- {wup-0.2.68 → wup-0.2.70}/wup/validate.py +0 -0
- {wup-0.2.68 → wup-0.2.70}/wup/web_client.py +0 -0
- {wup-0.2.68 → wup-0.2.70}/wup.egg-info/dependency_links.txt +0 -0
- {wup-0.2.68 → wup-0.2.70}/wup.egg-info/entry_points.txt +0 -0
- {wup-0.2.68 → wup-0.2.70}/wup.egg-info/requires.txt +0 -0
- {wup-0.2.68 → wup-0.2.70}/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.70
|
|
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
|
|
@@ -42,17 +42,17 @@ Dynamic: license-file
|
|
|
42
42
|
|
|
43
43
|
## AI Cost Tracking
|
|
44
44
|
|
|
45
|
-
    
|
|
46
|
+
  
|
|
47
47
|
|
|
48
|
-
- 🤖 **LLM usage:** $
|
|
49
|
-
- 👤 **Human dev:** ~$
|
|
48
|
+
- 🤖 **LLM usage:** $6.5152 (98 commits)
|
|
49
|
+
- 👤 **Human dev:** ~$5115 (51.1h @ $100/h, 30min dedup)
|
|
50
50
|
|
|
51
|
-
Generated on 2026-07-
|
|
51
|
+
Generated on 2026-07-17 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
|
|
52
52
|
|
|
53
53
|
---
|
|
54
54
|
|
|
55
|
-
    
|
|
56
56
|
|
|
57
57
|
**WUP (What's Up)** - Intelligent file watcher for regression testing in large projects.
|
|
58
58
|
|
|
@@ -147,6 +147,30 @@ wup watch ./my-project --config custom-config.yaml
|
|
|
147
147
|
wup testql-endpoints /path/to/scenarios --output testql-deps.json
|
|
148
148
|
```
|
|
149
149
|
|
|
150
|
+
### Watch Multiple Projects Simultaneously
|
|
151
|
+
|
|
152
|
+
Pass several project roots to watch them all at once in a single process. Each
|
|
153
|
+
project keeps its own `wup.yaml`, dependency map, file observer and test queue.
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
# Watch several projects at the same time
|
|
157
|
+
wup watch ./service-a ./service-b ./service-c
|
|
158
|
+
|
|
159
|
+
# Discover: expand a monorepo root into every sub-project that has a wup.yaml
|
|
160
|
+
wup watch --discover ./my-monorepo
|
|
161
|
+
|
|
162
|
+
# Discover from the current directory
|
|
163
|
+
wup watch -D .
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
`--discover` walks the immediate sub-directories of each given path and watches
|
|
167
|
+
those that already contain a `wup.yaml` (hidden and vendor folders such as
|
|
168
|
+
`node_modules`, `.venv`, `dist` and `build` are skipped). A relative `--deps`
|
|
169
|
+
path is resolved per project, so each project uses its own `deps.json`.
|
|
170
|
+
|
|
171
|
+
> The live `--dashboard` and single `--config` options apply to single-project
|
|
172
|
+
> runs only; when watching multiple projects each uses its own `wup.yaml`.
|
|
173
|
+
|
|
150
174
|
### Initialize Configuration
|
|
151
175
|
|
|
152
176
|
```bash
|
|
@@ -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:** $6.5152 (98 commits)
|
|
10
|
+
- 👤 **Human dev:** ~$5115 (51.1h @ $100/h, 30min dedup)
|
|
11
11
|
|
|
12
|
-
Generated on 2026-07-
|
|
12
|
+
Generated on 2026-07-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
|
|
|
@@ -108,6 +108,30 @@ wup watch ./my-project --config custom-config.yaml
|
|
|
108
108
|
wup testql-endpoints /path/to/scenarios --output testql-deps.json
|
|
109
109
|
```
|
|
110
110
|
|
|
111
|
+
### Watch Multiple Projects Simultaneously
|
|
112
|
+
|
|
113
|
+
Pass several project roots to watch them all at once in a single process. Each
|
|
114
|
+
project keeps its own `wup.yaml`, dependency map, file observer and test queue.
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
# Watch several projects at the same time
|
|
118
|
+
wup watch ./service-a ./service-b ./service-c
|
|
119
|
+
|
|
120
|
+
# Discover: expand a monorepo root into every sub-project that has a wup.yaml
|
|
121
|
+
wup watch --discover ./my-monorepo
|
|
122
|
+
|
|
123
|
+
# Discover from the current directory
|
|
124
|
+
wup watch -D .
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
`--discover` walks the immediate sub-directories of each given path and watches
|
|
128
|
+
those that already contain a `wup.yaml` (hidden and vendor folders such as
|
|
129
|
+
`node_modules`, `.venv`, `dist` and `build` are skipped). A relative `--deps`
|
|
130
|
+
path is resolved per project, so each project uses its own `deps.json`.
|
|
131
|
+
|
|
132
|
+
> The live `--dashboard` and single `--config` options apply to single-project
|
|
133
|
+
> runs only; when watching multiple projects each uses its own `wup.yaml`.
|
|
134
|
+
|
|
111
135
|
### Initialize Configuration
|
|
112
136
|
|
|
113
137
|
```bash
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""Tests for simultaneous multi-project watching."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from wup.cli import _discover_projects, _resolve_project_paths
|
|
8
|
+
from wup.config import detect_watch_paths, get_default_config
|
|
9
|
+
from wup.core import WupWatcher
|
|
10
|
+
from wup.models.config import ProjectConfig, WatchConfig, WupConfig
|
|
11
|
+
from wup.multi import MultiProjectWatcher
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _make_project(root: Path, name: str, src: str = "src") -> Path:
|
|
15
|
+
"""Create a project dir with a wup.yaml and one watchable source folder."""
|
|
16
|
+
proj = root / name
|
|
17
|
+
(proj / src).mkdir(parents=True)
|
|
18
|
+
(proj / src / "main.py").write_text("print('hi')\n")
|
|
19
|
+
(proj / "wup.yaml").write_text(
|
|
20
|
+
f"project:\n name: {name}\nwatch:\n paths:\n - {src}/**\n"
|
|
21
|
+
)
|
|
22
|
+
return proj
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _watcher(project_root: Path, paths: list[str]) -> WupWatcher:
|
|
26
|
+
cfg = WupConfig(
|
|
27
|
+
project=ProjectConfig(name=project_root.name),
|
|
28
|
+
watch=WatchConfig(paths=paths),
|
|
29
|
+
)
|
|
30
|
+
return WupWatcher(
|
|
31
|
+
project_root=str(project_root),
|
|
32
|
+
deps_file=str(project_root / "deps.json"),
|
|
33
|
+
config=cfg,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_detect_watch_paths_uses_existing_dirs(tmp_path: Path) -> None:
|
|
38
|
+
(tmp_path / "services").mkdir()
|
|
39
|
+
(tmp_path / "lib").mkdir()
|
|
40
|
+
paths = detect_watch_paths(tmp_path)
|
|
41
|
+
assert "services/**" in paths
|
|
42
|
+
assert "lib/**" in paths
|
|
43
|
+
# A non-existent common dir is not emitted.
|
|
44
|
+
assert "app/**" not in paths
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_detect_watch_paths_falls_back_when_nothing_matches(tmp_path: Path) -> None:
|
|
48
|
+
assert detect_watch_paths(tmp_path) == ["app/**", "src/**", "routes/**"]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_default_config_watches_only_real_dirs(tmp_path: Path) -> None:
|
|
52
|
+
(tmp_path / "services").mkdir()
|
|
53
|
+
cfg = get_default_config(tmp_path)
|
|
54
|
+
assert cfg.watch.paths == ["services/**"]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_discover_finds_subprojects(tmp_path: Path) -> None:
|
|
58
|
+
_make_project(tmp_path, "alpha")
|
|
59
|
+
_make_project(tmp_path, "beta")
|
|
60
|
+
(tmp_path / "not_a_project").mkdir() # no wup.yaml -> skipped
|
|
61
|
+
|
|
62
|
+
discovered = {p.name for p in _discover_projects(tmp_path)}
|
|
63
|
+
assert discovered == {"alpha", "beta"}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_discover_skips_vendor_and_hidden(tmp_path: Path) -> None:
|
|
67
|
+
_make_project(tmp_path, "alpha")
|
|
68
|
+
_make_project(tmp_path, "node_modules") # vendored -> skipped
|
|
69
|
+
_make_project(tmp_path, ".hidden") # hidden -> skipped
|
|
70
|
+
|
|
71
|
+
discovered = {p.name for p in _discover_projects(tmp_path)}
|
|
72
|
+
assert discovered == {"alpha"}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_resolve_paths_dedupes(tmp_path: Path) -> None:
|
|
76
|
+
_make_project(tmp_path, "alpha")
|
|
77
|
+
resolved = _resolve_project_paths(
|
|
78
|
+
[str(tmp_path / "alpha"), str(tmp_path / "alpha")], discover=False
|
|
79
|
+
)
|
|
80
|
+
assert len(resolved) == 1
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_prepare_observer_none_when_no_valid_paths(tmp_path: Path) -> None:
|
|
84
|
+
watcher = _watcher(tmp_path, paths=["does-not-exist/**"])
|
|
85
|
+
assert watcher.prepare_observer() is None
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def test_multi_watcher_returns_false_when_all_invalid(tmp_path: Path) -> None:
|
|
89
|
+
w1 = _watcher(tmp_path, paths=["missing-a/**"])
|
|
90
|
+
w2 = _watcher(tmp_path, paths=["missing-b/**"])
|
|
91
|
+
assert MultiProjectWatcher([w1, w2]).start_watching() is False
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def test_multi_watcher_starts_observers_for_valid_projects(tmp_path: Path) -> None:
|
|
95
|
+
alpha = _make_project(tmp_path, "alpha")
|
|
96
|
+
beta = _make_project(tmp_path, "beta")
|
|
97
|
+
w1 = _watcher(alpha, paths=["src/**"])
|
|
98
|
+
w2 = _watcher(beta, paths=["src/**"])
|
|
99
|
+
|
|
100
|
+
multi = MultiProjectWatcher([w1, w2])
|
|
101
|
+
observers = []
|
|
102
|
+
active = []
|
|
103
|
+
for watcher in multi.watchers:
|
|
104
|
+
watcher.start_background_tasks()
|
|
105
|
+
obs = watcher.prepare_observer()
|
|
106
|
+
if obs is not None:
|
|
107
|
+
observers.append(obs)
|
|
108
|
+
active.append(watcher)
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
assert len(active) == 2
|
|
112
|
+
assert all(obs.is_alive() for obs in observers)
|
|
113
|
+
finally:
|
|
114
|
+
for obs in observers:
|
|
115
|
+
obs.stop()
|
|
116
|
+
for obs in observers:
|
|
117
|
+
obs.join()
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Regression: dedupe registry must not mute recurrences of CLOSED tickets.
|
|
2
|
+
|
|
3
|
+
A stale `.wup/planfile-tickets.json` entry whose ticket was already
|
|
4
|
+
done/canceled used to suppress every future ticket for the same
|
|
5
|
+
failure fingerprint — a regression of an old outage became silently
|
|
6
|
+
invisible (found live: `docker stop connect-scenario` produced no ticket
|
|
7
|
+
because PLF-1950 from a month earlier still held the fingerprint).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from types import SimpleNamespace
|
|
15
|
+
|
|
16
|
+
from wup import planfile_reporter as reporter_mod
|
|
17
|
+
from wup.models.config import PlanfileConfig
|
|
18
|
+
from wup.planfile_reporter import PlanfileReporter
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _reporter(tmp_path: Path) -> PlanfileReporter:
|
|
22
|
+
cfg = PlanfileConfig(enabled=True, command="planfile")
|
|
23
|
+
return PlanfileReporter(tmp_path, cfg)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _seed_dedupe(rep: PlanfileReporter, fingerprint_kwargs: dict, ticket_id: str) -> None:
|
|
27
|
+
fingerprint = rep._fingerprint(**fingerprint_kwargs)
|
|
28
|
+
rep.dedupe_path.parent.mkdir(parents=True, exist_ok=True)
|
|
29
|
+
rep.dedupe_path.write_text(
|
|
30
|
+
json.dumps({fingerprint: {"ticket_id": ticket_id, "service": fingerprint_kwargs["service"], "stage": fingerprint_kwargs["stage"]}}),
|
|
31
|
+
encoding="utf-8",
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
FAIL = {"service": "svc", "status": "down", "stage": "probe", "message": "connection refused"}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_open_ticket_still_mutes_recurrence(tmp_path, monkeypatch):
|
|
39
|
+
rep = _reporter(tmp_path)
|
|
40
|
+
_seed_dedupe(rep, FAIL, "PLF-1")
|
|
41
|
+
|
|
42
|
+
def fake_run(cmd, **kwargs):
|
|
43
|
+
assert "show" in cmd
|
|
44
|
+
return SimpleNamespace(returncode=0, stdout=json.dumps({"id": "PLF-1", "status": "open"}), stderr="")
|
|
45
|
+
|
|
46
|
+
monkeypatch.setattr(reporter_mod.subprocess, "run", fake_run)
|
|
47
|
+
assert rep.report_failure(**FAIL) == "PLF-1"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_closed_ticket_refiles_fresh_ticket(tmp_path, monkeypatch):
|
|
51
|
+
rep = _reporter(tmp_path)
|
|
52
|
+
_seed_dedupe(rep, FAIL, "PLF-1")
|
|
53
|
+
calls = []
|
|
54
|
+
|
|
55
|
+
def fake_run(cmd, **kwargs):
|
|
56
|
+
calls.append(list(cmd))
|
|
57
|
+
if "show" in cmd:
|
|
58
|
+
return SimpleNamespace(returncode=0, stdout=json.dumps({"id": "PLF-1", "status": "done"}), stderr="")
|
|
59
|
+
return SimpleNamespace(returncode=0, stdout="✓ Created PLF-2: [AUTO-DIAG] wup-svc probe down", stderr="")
|
|
60
|
+
|
|
61
|
+
monkeypatch.setattr(reporter_mod.subprocess, "run", fake_run)
|
|
62
|
+
monkeypatch.setattr(rep, "_wait_for_planfile_store_ready", lambda timeout_s=30.0: True)
|
|
63
|
+
|
|
64
|
+
assert rep.report_failure(**FAIL) == "PLF-2"
|
|
65
|
+
# registry now points at the fresh ticket
|
|
66
|
+
dedupe = json.loads(rep.dedupe_path.read_text(encoding="utf-8"))
|
|
67
|
+
assert [v["ticket_id"] for v in dedupe.values()] == ["PLF-2"]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_show_error_keeps_muting_conservatively(tmp_path, monkeypatch):
|
|
71
|
+
rep = _reporter(tmp_path)
|
|
72
|
+
_seed_dedupe(rep, FAIL, "PLF-1")
|
|
73
|
+
|
|
74
|
+
def fake_run(cmd, **kwargs):
|
|
75
|
+
return SimpleNamespace(returncode=1, stdout="", stderr="boom")
|
|
76
|
+
|
|
77
|
+
monkeypatch.setattr(reporter_mod.subprocess, "run", fake_run)
|
|
78
|
+
assert rep.report_failure(**FAIL) == "PLF-1"
|
|
@@ -246,7 +246,10 @@ def test_planfile_reporter_creates_deduped_ticket(monkeypatch):
|
|
|
246
246
|
|
|
247
247
|
assert first == "PLF-999"
|
|
248
248
|
assert second == "PLF-999"
|
|
249
|
-
|
|
249
|
+
# Recurrence check may `ticket show` the deduped id, but only ONE
|
|
250
|
+
# ticket may ever be created for the same open fingerprint.
|
|
251
|
+
create_calls = [c for c in calls if c[:3] == ["planfile", "ticket", "create"]]
|
|
252
|
+
assert len(create_calls) == 1
|
|
250
253
|
assert calls[0][:3] == ["planfile", "ticket", "create"]
|
|
251
254
|
assert "--label" in calls[0]
|
|
252
255
|
assert "llm-ready" in calls[0]
|
|
@@ -442,7 +445,13 @@ def test_visual_differ_disabled_by_default():
|
|
|
442
445
|
# Differ is created but flagged disabled — run_for_service() must be a no-op
|
|
443
446
|
assert watcher.visual_differ is not None
|
|
444
447
|
assert watcher.visual_differ.cfg.enabled is False
|
|
445
|
-
|
|
448
|
+
from wup.models.target import ServiceTestTarget
|
|
449
|
+
|
|
450
|
+
results = asyncio.run(
|
|
451
|
+
watcher.visual_differ.run_for_service(
|
|
452
|
+
ServiceTestTarget(service="svc", endpoints=["/x"])
|
|
453
|
+
)
|
|
454
|
+
)
|
|
446
455
|
assert results == []
|
|
447
456
|
|
|
448
457
|
|
|
@@ -532,8 +541,8 @@ def test_quick_pass_actions_prefer_config_endpoints_for_visual_diff():
|
|
|
532
541
|
self.cfg = VisualDiffConfig(enabled=True)
|
|
533
542
|
self.calls = []
|
|
534
543
|
|
|
535
|
-
async def run_for_service(self,
|
|
536
|
-
self.calls.append((service, list(endpoints)))
|
|
544
|
+
async def run_for_service(self, target):
|
|
545
|
+
self.calls.append((target.service, list(target.endpoints)))
|
|
537
546
|
return []
|
|
538
547
|
|
|
539
548
|
differ = RecordingDiffer()
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Regression: a watcher that never watched must not exit as a clean run.
|
|
2
|
+
|
|
3
|
+
`start_watching` used to bare-`return` when every configured watch path was
|
|
4
|
+
missing — under systemd (Restart=on-failure) the clean exit 0 meant the unit
|
|
5
|
+
silently died and never came back. It now returns False and the CLI turns
|
|
6
|
+
that into exit code 1.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from wup.core import WupWatcher
|
|
12
|
+
from wup.models.config import ProjectConfig, WatchConfig, WupConfig
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _watcher_with_paths(paths: list[str]) -> WupWatcher:
|
|
16
|
+
cfg = WupConfig(
|
|
17
|
+
project=ProjectConfig(name="t"),
|
|
18
|
+
watch=WatchConfig(paths=paths),
|
|
19
|
+
)
|
|
20
|
+
return WupWatcher(project_root="/nonexistent-wup-test-root", config=cfg)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_start_watching_returns_false_when_no_valid_paths() -> None:
|
|
24
|
+
watcher = _watcher_with_paths(["does-not-exist/**"])
|
|
25
|
+
assert watcher.start_watching() is False
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_start_watching_returns_false_for_explicit_missing_paths() -> None:
|
|
29
|
+
watcher = _watcher_with_paths([])
|
|
30
|
+
assert watcher.start_watching(["/nonexistent-wup-test-root/a"]) is False
|
|
@@ -11,6 +11,7 @@ import pytest
|
|
|
11
11
|
from wup.config import load_config, save_config, get_default_config
|
|
12
12
|
from wup.core import WupWatcher
|
|
13
13
|
from wup.dependency_mapper import DependencyMapper
|
|
14
|
+
from wup.models.target import ServiceTestTarget
|
|
14
15
|
from wup.models.config import (
|
|
15
16
|
WupConfig,
|
|
16
17
|
WatchConfig,
|
|
@@ -1056,7 +1057,7 @@ class TestVisualDiffer:
|
|
|
1056
1057
|
pages_from_endpoints=False,
|
|
1057
1058
|
)
|
|
1058
1059
|
differ = VisualDiffer(tmpdir, cfg)
|
|
1059
|
-
pages = differ._pages_for_service("users", ["/api/users"])
|
|
1060
|
+
pages = differ._pages_for_service(ServiceTestTarget(service="users", endpoints=["/api/users"]))
|
|
1060
1061
|
assert pages == ["http://localhost:8080/dashboard"]
|
|
1061
1062
|
|
|
1062
1063
|
def test_pages_for_service_from_endpoints(self):
|
|
@@ -1066,7 +1067,9 @@ class TestVisualDiffer:
|
|
|
1066
1067
|
pages_from_endpoints=True,
|
|
1067
1068
|
)
|
|
1068
1069
|
differ = VisualDiffer(tmpdir, cfg)
|
|
1069
|
-
pages = differ._pages_for_service(
|
|
1070
|
+
pages = differ._pages_for_service(
|
|
1071
|
+
ServiceTestTarget(service="users", endpoints=["/connect-users", "/connect-users/1"])
|
|
1072
|
+
)
|
|
1070
1073
|
assert "http://localhost:8080/connect-users" in pages
|
|
1071
1074
|
assert "http://localhost:8080/connect-users/1" in pages
|
|
1072
1075
|
|
|
@@ -1083,8 +1086,10 @@ class TestVisualDiffer:
|
|
|
1083
1086
|
)
|
|
1084
1087
|
differ = VisualDiffer(tmpdir, cfg)
|
|
1085
1088
|
pages = differ._pages_for_service(
|
|
1086
|
-
|
|
1087
|
-
|
|
1089
|
+
ServiceTestTarget(
|
|
1090
|
+
service="backend",
|
|
1091
|
+
endpoints=["/api/v3/health", "/connect-config", "http://localhost:8080/connect-data"],
|
|
1092
|
+
)
|
|
1088
1093
|
)
|
|
1089
1094
|
assert "http://localhost:8080/api/v3/health" not in pages
|
|
1090
1095
|
assert "http://localhost:8080/connect-config" in pages
|
|
@@ -1097,7 +1102,7 @@ class TestVisualDiffer:
|
|
|
1097
1102
|
pages_from_endpoints=False,
|
|
1098
1103
|
)
|
|
1099
1104
|
differ = VisualDiffer(tmpdir, cfg)
|
|
1100
|
-
pages = differ._pages_for_service("users"
|
|
1105
|
+
pages = differ._pages_for_service(ServiceTestTarget(service="users"))
|
|
1101
1106
|
assert pages == ["http://localhost:8080/users"]
|
|
1102
1107
|
|
|
1103
1108
|
def test_pages_for_service_absolute_url_passthrough(self):
|
|
@@ -1108,7 +1113,7 @@ class TestVisualDiffer:
|
|
|
1108
1113
|
pages_from_endpoints=False,
|
|
1109
1114
|
)
|
|
1110
1115
|
differ = VisualDiffer(tmpdir, cfg)
|
|
1111
|
-
pages = differ._pages_for_service("svc"
|
|
1116
|
+
pages = differ._pages_for_service(ServiceTestTarget(service="svc"))
|
|
1112
1117
|
assert pages == ["https://example.com/connect-dashboard"]
|
|
1113
1118
|
|
|
1114
1119
|
def test_diff_snapshots_baseline(self):
|
|
@@ -1144,7 +1149,9 @@ class TestVisualDiffer:
|
|
|
1144
1149
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
1145
1150
|
cfg = VisualDiffConfig(enabled=False)
|
|
1146
1151
|
differ = VisualDiffer(tmpdir, cfg)
|
|
1147
|
-
results = asyncio.run(
|
|
1152
|
+
results = asyncio.run(
|
|
1153
|
+
differ.run_for_service(ServiceTestTarget(service="svc", endpoints=["/x"]))
|
|
1154
|
+
)
|
|
1148
1155
|
assert results == []
|
|
1149
1156
|
|
|
1150
1157
|
def test_run_for_service_summarizes_fetch_errors(self, monkeypatch):
|
|
@@ -1177,7 +1184,9 @@ class TestVisualDiffer:
|
|
|
1177
1184
|
monkeypatch.setattr(visual_diff_module.console, "print", lambda message: printed.append(str(message)))
|
|
1178
1185
|
|
|
1179
1186
|
results = asyncio.run(
|
|
1180
|
-
differ.run_for_service(
|
|
1187
|
+
differ.run_for_service(
|
|
1188
|
+
ServiceTestTarget(service="frontend", endpoints=["/a", "/b", "/c", "/d"])
|
|
1189
|
+
)
|
|
1181
1190
|
)
|
|
1182
1191
|
|
|
1183
1192
|
assert len(results) == 4
|
|
@@ -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.70"
|
|
11
11
|
__author__ = "Tom Sapletta"
|
|
12
12
|
|
|
13
13
|
from .config import load_config, save_config, get_default_config
|