wup 0.2.79__tar.gz → 0.2.80__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.79 → wup-0.2.80}/PKG-INFO +60 -7
- {wup-0.2.79 → wup-0.2.80}/README.md +59 -6
- {wup-0.2.79 → wup-0.2.80}/pyproject.toml +1 -1
- wup-0.2.80/tests/test_intent_monitor.py +208 -0
- {wup-0.2.79 → wup-0.2.80}/tests/test_multi_project.py +42 -0
- {wup-0.2.79 → wup-0.2.80}/wup/__init__.py +1 -1
- {wup-0.2.79 → wup-0.2.80}/wup/config.py +84 -2
- {wup-0.2.79 → wup-0.2.80}/wup/core.py +29 -2
- wup-0.2.80/wup/intent_monitor.py +260 -0
- {wup-0.2.79 → wup-0.2.80}/wup/models/config.py +28 -0
- {wup-0.2.79 → wup-0.2.80}/wup/testql_watcher.py +23 -0
- {wup-0.2.79 → wup-0.2.80}/wup.egg-info/PKG-INFO +60 -7
- {wup-0.2.79 → wup-0.2.80}/wup.egg-info/SOURCES.txt +2 -0
- {wup-0.2.79 → wup-0.2.80}/LICENSE +0 -0
- {wup-0.2.79 → wup-0.2.80}/setup.cfg +0 -0
- {wup-0.2.79 → wup-0.2.80}/tests/test_aql.py +0 -0
- {wup-0.2.79 → wup-0.2.80}/tests/test_assistant.py +0 -0
- {wup-0.2.79 → wup-0.2.80}/tests/test_auto_detection.py +0 -0
- {wup-0.2.79 → wup-0.2.80}/tests/test_bootstrap.py +0 -0
- {wup-0.2.79 → wup-0.2.80}/tests/test_cli_bridge.py +0 -0
- {wup-0.2.79 → wup-0.2.80}/tests/test_cli_filtering.py +0 -0
- {wup-0.2.79 → wup-0.2.80}/tests/test_control.py +0 -0
- {wup-0.2.79 → wup-0.2.80}/tests/test_discovery_adapters.py +0 -0
- {wup-0.2.79 → wup-0.2.80}/tests/test_e2e.py +0 -0
- {wup-0.2.79 → wup-0.2.80}/tests/test_endpoints_init_cli.py +0 -0
- {wup-0.2.79 → wup-0.2.80}/tests/test_genericity.py +0 -0
- {wup-0.2.79 → wup-0.2.80}/tests/test_health_summary_passed.py +0 -0
- {wup-0.2.79 → wup-0.2.80}/tests/test_monitoring_manifest.py +0 -0
- {wup-0.2.79 → wup-0.2.80}/tests/test_oql.py +0 -0
- {wup-0.2.79 → wup-0.2.80}/tests/test_planfile_reporter_dedupe.py +0 -0
- {wup-0.2.79 → wup-0.2.80}/tests/test_probe_mutex.py +0 -0
- {wup-0.2.79 → wup-0.2.80}/tests/test_service_inference.py +0 -0
- {wup-0.2.79 → wup-0.2.80}/tests/test_status_data.py +0 -0
- {wup-0.2.79 → wup-0.2.80}/tests/test_sync.py +0 -0
- {wup-0.2.79 → wup-0.2.80}/tests/test_testql_monitor.py +0 -0
- {wup-0.2.79 → wup-0.2.80}/tests/test_testql_watcher.py +0 -0
- {wup-0.2.79 → wup-0.2.80}/tests/test_visual_diff_periodic_skip.py +0 -0
- {wup-0.2.79 → wup-0.2.80}/tests/test_visual_diff_progress.py +0 -0
- {wup-0.2.79 → wup-0.2.80}/tests/test_watch_exclude.py +0 -0
- {wup-0.2.79 → wup-0.2.80}/tests/test_watch_no_paths_exit.py +0 -0
- {wup-0.2.79 → wup-0.2.80}/tests/test_web_client.py +0 -0
- {wup-0.2.79 → wup-0.2.80}/tests/test_wup.py +0 -0
- {wup-0.2.79 → wup-0.2.80}/tests/test_wup_generate.py +0 -0
- {wup-0.2.79 → wup-0.2.80}/wup/_ast_detector.py +0 -0
- {wup-0.2.79 → wup-0.2.80}/wup/_base_detector.py +0 -0
- {wup-0.2.79 → wup-0.2.80}/wup/_hash_detector.py +0 -0
- {wup-0.2.79 → wup-0.2.80}/wup/_yaml_detector.py +0 -0
- {wup-0.2.79 → wup-0.2.80}/wup/anomaly_detector.py +0 -0
- {wup-0.2.79 → wup-0.2.80}/wup/anomaly_models.py +0 -0
- {wup-0.2.79 → wup-0.2.80}/wup/aql.py +0 -0
- {wup-0.2.79 → wup-0.2.80}/wup/assistant.py +0 -0
- {wup-0.2.79 → wup-0.2.80}/wup/assistant_discovery.py +0 -0
- {wup-0.2.79 → wup-0.2.80}/wup/assistant_validator.py +0 -0
- {wup-0.2.79 → wup-0.2.80}/wup/bootstrap.py +0 -0
- {wup-0.2.79 → wup-0.2.80}/wup/bus.py +0 -0
- {wup-0.2.79 → wup-0.2.80}/wup/cli.py +0 -0
- {wup-0.2.79 → wup-0.2.80}/wup/cli_bridge.py +0 -0
- {wup-0.2.79 → wup-0.2.80}/wup/cli_config_generator.py +0 -0
- {wup-0.2.79 → wup-0.2.80}/wup/cli_scanner.py +0 -0
- {wup-0.2.79 → wup-0.2.80}/wup/control.py +0 -0
- {wup-0.2.79 → wup-0.2.80}/wup/dependency_mapper.py +0 -0
- {wup-0.2.79 → wup-0.2.80}/wup/discovery.py +0 -0
- {wup-0.2.79 → wup-0.2.80}/wup/endpoints.py +0 -0
- {wup-0.2.79 → wup-0.2.80}/wup/event_store.py +0 -0
- {wup-0.2.79 → wup-0.2.80}/wup/file_watcher/events/file_events.py +0 -0
- {wup-0.2.79 → wup-0.2.80}/wup/generate.py +0 -0
- {wup-0.2.79 → wup-0.2.80}/wup/init_cli.py +0 -0
- {wup-0.2.79 → wup-0.2.80}/wup/models/__init__.py +0 -0
- {wup-0.2.79 → wup-0.2.80}/wup/models/target.py +0 -0
- {wup-0.2.79 → wup-0.2.80}/wup/monitoring_manifest.py +0 -0
- {wup-0.2.79 → wup-0.2.80}/wup/multi.py +0 -0
- {wup-0.2.79 → wup-0.2.80}/wup/oql.py +0 -0
- {wup-0.2.79 → wup-0.2.80}/wup/paths.py +0 -0
- {wup-0.2.79 → wup-0.2.80}/wup/planfile_reporter.py +0 -0
- {wup-0.2.79 → wup-0.2.80}/wup/status_data.py +0 -0
- {wup-0.2.79 → wup-0.2.80}/wup/sync.py +0 -0
- {wup-0.2.79 → wup-0.2.80}/wup/testing/events/health_events.py +0 -0
- {wup-0.2.79 → wup-0.2.80}/wup/testing/events/test_results.py +0 -0
- {wup-0.2.79 → wup-0.2.80}/wup/testing/handlers/event_handlers.py +0 -0
- {wup-0.2.79 → wup-0.2.80}/wup/testing/handlers/health_handlers.py +0 -0
- {wup-0.2.79 → wup-0.2.80}/wup/testing/queries/health_queries.py +0 -0
- {wup-0.2.79 → wup-0.2.80}/wup/testql_cli_generator.py +0 -0
- {wup-0.2.79 → wup-0.2.80}/wup/testql_discovery.py +0 -0
- {wup-0.2.79 → wup-0.2.80}/wup/testql_monitor.py +0 -0
- {wup-0.2.79 → wup-0.2.80}/wup/validate.py +0 -0
- {wup-0.2.79 → wup-0.2.80}/wup/visual_diff.py +0 -0
- {wup-0.2.79 → wup-0.2.80}/wup/web_client.py +0 -0
- {wup-0.2.79 → wup-0.2.80}/wup.egg-info/dependency_links.txt +0 -0
- {wup-0.2.79 → wup-0.2.80}/wup.egg-info/entry_points.txt +0 -0
- {wup-0.2.79 → wup-0.2.80}/wup.egg-info/requires.txt +0 -0
- {wup-0.2.79 → wup-0.2.80}/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.80
|
|
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
|
|
@@ -47,17 +47,17 @@ Dynamic: license-file
|
|
|
47
47
|
|
|
48
48
|
## AI Cost Tracking
|
|
49
49
|
|
|
50
|
-
    
|
|
51
|
+
  
|
|
52
52
|
|
|
53
|
-
- 🤖 **LLM usage:** $7.
|
|
54
|
-
- 👤 **Human dev:** ~$
|
|
53
|
+
- 🤖 **LLM usage:** $7.7365 (111 commits)
|
|
54
|
+
- 👤 **Human dev:** ~$5566 (55.7h @ $100/h, 30min dedup)
|
|
55
55
|
|
|
56
|
-
Generated on 2026-
|
|
56
|
+
Generated on 2026-08-01 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
|
|
57
57
|
|
|
58
58
|
---
|
|
59
59
|
|
|
60
|
-
    
|
|
61
61
|
|
|
62
62
|
**WUP (What's Up)** - Intelligent file watcher for regression testing in large projects.
|
|
63
63
|
|
|
@@ -157,6 +157,13 @@ wup testql-endpoints /path/to/scenarios --output testql-deps.json
|
|
|
157
157
|
Pass several project roots to watch them all at once in a single process. Each
|
|
158
158
|
project keeps its own `wup.yaml`, dependency map, file observer and test queue.
|
|
159
159
|
|
|
160
|
+
When the current directory is a workspace containing projects in immediate
|
|
161
|
+
subfolders, plain `wup watch .` auto-detects their common source directories,
|
|
162
|
+
for example `api/src`, `worker/services` and `frontend/app`. This also repairs
|
|
163
|
+
the runtime behaviour of older auto-generated configs that still point only to
|
|
164
|
+
missing root-level `app`, `src` and `routes` directories. The config file is not
|
|
165
|
+
rewritten by this fallback.
|
|
166
|
+
|
|
160
167
|
```bash
|
|
161
168
|
# Watch several projects at the same time
|
|
162
169
|
wup watch ./service-a ./service-b ./service-c
|
|
@@ -176,6 +183,50 @@ path is resolved per project, so each project uses its own `deps.json`.
|
|
|
176
183
|
> The live `--dashboard` and single `--config` options apply to single-project
|
|
177
184
|
> runs only; when watching multiple projects each uses its own `wup.yaml`.
|
|
178
185
|
|
|
186
|
+
### Continuous Intent Monitoring with todo2code
|
|
187
|
+
|
|
188
|
+
WUP can run todo2code on startup, periodically and after debounced source-file
|
|
189
|
+
changes. Diagnostics are projected into the regular service-health stream as
|
|
190
|
+
`<project>:intent`: blocking findings produce `down`, review-required findings
|
|
191
|
+
produce `degraded`, and a clean audit produces `up`.
|
|
192
|
+
|
|
193
|
+
The integration is opt-in and deterministic by default, so merely installing
|
|
194
|
+
WUP never starts paid LLM requests:
|
|
195
|
+
|
|
196
|
+
```yaml
|
|
197
|
+
intent_monitoring:
|
|
198
|
+
enabled: true
|
|
199
|
+
runner: cli
|
|
200
|
+
command:
|
|
201
|
+
- node
|
|
202
|
+
- /path/to/todo2code/dist/src/cli.js
|
|
203
|
+
interval_s: 300
|
|
204
|
+
debounce_s: 10
|
|
205
|
+
run_on_start: true
|
|
206
|
+
run_on_change: true
|
|
207
|
+
mode: deterministic
|
|
208
|
+
task_file: TASK.md
|
|
209
|
+
todo_file: TODO.md
|
|
210
|
+
changelog_file: CHANGELOG.md
|
|
211
|
+
docs:
|
|
212
|
+
- README.md
|
|
213
|
+
- docs/**/*.md
|
|
214
|
+
output_dir: .wup/intent
|
|
215
|
+
fail_severities:
|
|
216
|
+
- blocking
|
|
217
|
+
- review_required
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Set `runner: python` to use the dependency-free `todo2code-sdk` bridge. Install
|
|
221
|
+
it from todo2code's `sdk/python` directory and set `cli_path` to the built
|
|
222
|
+
`dist/src/cli.js`. The Python package still delegates semantic processing to
|
|
223
|
+
the canonical Node/TypeScript runtime.
|
|
224
|
+
|
|
225
|
+
`mode: prefer-llm` or `mode: require-llm` enables LLM enrichment for NL and
|
|
226
|
+
TODO/CHANGELOG. Documentation and final-summary LLM calls remain separately
|
|
227
|
+
controlled by `docs_llm` and `summary_llm`. Use these modes deliberately: an
|
|
228
|
+
audit may run after every debounce interval and therefore incur provider cost.
|
|
229
|
+
|
|
179
230
|
### Initialize Configuration
|
|
180
231
|
|
|
181
232
|
```bash
|
|
@@ -294,6 +345,8 @@ The generated `wup.yaml` includes:
|
|
|
294
345
|
- **Metadata header**: Version, generation date, documentation links
|
|
295
346
|
- **Dependencies info**: WUP version and optional wupbro dashboard
|
|
296
347
|
- **Quick start guide**: Common commands to get started
|
|
348
|
+
- **Detected watch paths**: Existing common source directories at the project
|
|
349
|
+
root and one level below it
|
|
297
350
|
|
|
298
351
|
Example `wup.yaml`:
|
|
299
352
|
|
|
@@ -3,17 +3,17 @@
|
|
|
3
3
|
|
|
4
4
|
## AI Cost Tracking
|
|
5
5
|
|
|
6
|
-
    
|
|
7
|
+
  
|
|
8
8
|
|
|
9
|
-
- 🤖 **LLM usage:** $7.
|
|
10
|
-
- 👤 **Human dev:** ~$
|
|
9
|
+
- 🤖 **LLM usage:** $7.7365 (111 commits)
|
|
10
|
+
- 👤 **Human dev:** ~$5566 (55.7h @ $100/h, 30min dedup)
|
|
11
11
|
|
|
12
|
-
Generated on 2026-
|
|
12
|
+
Generated on 2026-08-01 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
|
|
|
@@ -113,6 +113,13 @@ wup testql-endpoints /path/to/scenarios --output testql-deps.json
|
|
|
113
113
|
Pass several project roots to watch them all at once in a single process. Each
|
|
114
114
|
project keeps its own `wup.yaml`, dependency map, file observer and test queue.
|
|
115
115
|
|
|
116
|
+
When the current directory is a workspace containing projects in immediate
|
|
117
|
+
subfolders, plain `wup watch .` auto-detects their common source directories,
|
|
118
|
+
for example `api/src`, `worker/services` and `frontend/app`. This also repairs
|
|
119
|
+
the runtime behaviour of older auto-generated configs that still point only to
|
|
120
|
+
missing root-level `app`, `src` and `routes` directories. The config file is not
|
|
121
|
+
rewritten by this fallback.
|
|
122
|
+
|
|
116
123
|
```bash
|
|
117
124
|
# Watch several projects at the same time
|
|
118
125
|
wup watch ./service-a ./service-b ./service-c
|
|
@@ -132,6 +139,50 @@ path is resolved per project, so each project uses its own `deps.json`.
|
|
|
132
139
|
> The live `--dashboard` and single `--config` options apply to single-project
|
|
133
140
|
> runs only; when watching multiple projects each uses its own `wup.yaml`.
|
|
134
141
|
|
|
142
|
+
### Continuous Intent Monitoring with todo2code
|
|
143
|
+
|
|
144
|
+
WUP can run todo2code on startup, periodically and after debounced source-file
|
|
145
|
+
changes. Diagnostics are projected into the regular service-health stream as
|
|
146
|
+
`<project>:intent`: blocking findings produce `down`, review-required findings
|
|
147
|
+
produce `degraded`, and a clean audit produces `up`.
|
|
148
|
+
|
|
149
|
+
The integration is opt-in and deterministic by default, so merely installing
|
|
150
|
+
WUP never starts paid LLM requests:
|
|
151
|
+
|
|
152
|
+
```yaml
|
|
153
|
+
intent_monitoring:
|
|
154
|
+
enabled: true
|
|
155
|
+
runner: cli
|
|
156
|
+
command:
|
|
157
|
+
- node
|
|
158
|
+
- /path/to/todo2code/dist/src/cli.js
|
|
159
|
+
interval_s: 300
|
|
160
|
+
debounce_s: 10
|
|
161
|
+
run_on_start: true
|
|
162
|
+
run_on_change: true
|
|
163
|
+
mode: deterministic
|
|
164
|
+
task_file: TASK.md
|
|
165
|
+
todo_file: TODO.md
|
|
166
|
+
changelog_file: CHANGELOG.md
|
|
167
|
+
docs:
|
|
168
|
+
- README.md
|
|
169
|
+
- docs/**/*.md
|
|
170
|
+
output_dir: .wup/intent
|
|
171
|
+
fail_severities:
|
|
172
|
+
- blocking
|
|
173
|
+
- review_required
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Set `runner: python` to use the dependency-free `todo2code-sdk` bridge. Install
|
|
177
|
+
it from todo2code's `sdk/python` directory and set `cli_path` to the built
|
|
178
|
+
`dist/src/cli.js`. The Python package still delegates semantic processing to
|
|
179
|
+
the canonical Node/TypeScript runtime.
|
|
180
|
+
|
|
181
|
+
`mode: prefer-llm` or `mode: require-llm` enables LLM enrichment for NL and
|
|
182
|
+
TODO/CHANGELOG. Documentation and final-summary LLM calls remain separately
|
|
183
|
+
controlled by `docs_llm` and `summary_llm`. Use these modes deliberately: an
|
|
184
|
+
audit may run after every debounce interval and therefore incur provider cost.
|
|
185
|
+
|
|
135
186
|
### Initialize Configuration
|
|
136
187
|
|
|
137
188
|
```bash
|
|
@@ -250,6 +301,8 @@ The generated `wup.yaml` includes:
|
|
|
250
301
|
- **Metadata header**: Version, generation date, documentation links
|
|
251
302
|
- **Dependencies info**: WUP version and optional wupbro dashboard
|
|
252
303
|
- **Quick start guide**: Common commands to get started
|
|
304
|
+
- **Detected watch paths**: Existing common source directories at the project
|
|
305
|
+
root and one level below it
|
|
253
306
|
|
|
254
307
|
Example `wup.yaml`:
|
|
255
308
|
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"""Tests for continuous todo2code Intent-vs-Reality monitoring."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import sys
|
|
7
|
+
import types
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from types import SimpleNamespace
|
|
10
|
+
|
|
11
|
+
from wup.config import load_config, save_config
|
|
12
|
+
from wup.intent_monitor import IntentAuditResult, Todo2CodeIntentMonitor
|
|
13
|
+
from wup.models.config import IntentMonitoringConfig, ProjectConfig, WupConfig
|
|
14
|
+
from wup.testql_watcher import TestQLWatcher
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _diagnostics(tmp_path: Path, items: list[dict]) -> Path:
|
|
18
|
+
path = tmp_path / "diagnostics.json"
|
|
19
|
+
path.write_text(json.dumps({"diagnostics": items}), encoding="utf-8")
|
|
20
|
+
return path
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_config_round_trip_preserves_intent_monitoring(tmp_path: Path) -> None:
|
|
24
|
+
config = WupConfig(
|
|
25
|
+
project=ProjectConfig(name="demo"),
|
|
26
|
+
intent_monitoring=IntentMonitoringConfig(
|
|
27
|
+
enabled=True,
|
|
28
|
+
runner="python",
|
|
29
|
+
cli_path="/opt/todo2code/dist/src/cli.js",
|
|
30
|
+
interval_s=120,
|
|
31
|
+
mode="require-llm",
|
|
32
|
+
docs_llm=True,
|
|
33
|
+
fail_codes=["PLANNED_NOT_IMPLEMENTED"],
|
|
34
|
+
),
|
|
35
|
+
)
|
|
36
|
+
save_config(config, tmp_path / "wup.yaml")
|
|
37
|
+
|
|
38
|
+
loaded = load_config(tmp_path)
|
|
39
|
+
|
|
40
|
+
assert loaded.intent_monitoring.enabled is True
|
|
41
|
+
assert loaded.intent_monitoring.runner == "python"
|
|
42
|
+
assert loaded.intent_monitoring.interval_s == 120
|
|
43
|
+
assert loaded.intent_monitoring.mode == "require-llm"
|
|
44
|
+
assert loaded.intent_monitoring.docs_llm is True
|
|
45
|
+
assert loaded.intent_monitoring.fail_codes == ["PLANNED_NOT_IMPLEMENTED"]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_cli_monitor_maps_blocking_diagnostics_to_down(tmp_path: Path, monkeypatch) -> None:
|
|
49
|
+
path = _diagnostics(
|
|
50
|
+
tmp_path,
|
|
51
|
+
[{"severity": "blocking", "code": "PLANNED_NOT_IMPLEMENTED"}],
|
|
52
|
+
)
|
|
53
|
+
calls = []
|
|
54
|
+
|
|
55
|
+
def fake_run(command, **kwargs):
|
|
56
|
+
calls.append((command, kwargs))
|
|
57
|
+
payload = {"diagnosticsPath": str(path), "runDirectory": str(tmp_path / "run")}
|
|
58
|
+
return SimpleNamespace(returncode=0, stdout=json.dumps(payload), stderr="")
|
|
59
|
+
|
|
60
|
+
monkeypatch.setattr("wup.intent_monitor.subprocess.run", fake_run)
|
|
61
|
+
results = []
|
|
62
|
+
config = IntentMonitoringConfig(
|
|
63
|
+
enabled=True,
|
|
64
|
+
command=["node", "/opt/todo2code/cli.js"],
|
|
65
|
+
mode="deterministic",
|
|
66
|
+
)
|
|
67
|
+
monitor = Todo2CodeIntentMonitor(tmp_path, config, results.append)
|
|
68
|
+
|
|
69
|
+
result = monitor.run_once()
|
|
70
|
+
|
|
71
|
+
assert result.status == "down"
|
|
72
|
+
assert len(result.diagnostics) == 1
|
|
73
|
+
assert results == [result]
|
|
74
|
+
command, kwargs = calls[0]
|
|
75
|
+
assert command[:3] == ["node", "/opt/todo2code/cli.js", "pipeline"]
|
|
76
|
+
assert "--no-docs-llm" in command
|
|
77
|
+
assert "--no-summary-llm" in command
|
|
78
|
+
assert kwargs.get("shell", False) is False
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_cli_monitor_resolves_single_javascript_launcher(tmp_path: Path, monkeypatch) -> None:
|
|
82
|
+
path = _diagnostics(tmp_path, [])
|
|
83
|
+
cli_path = tmp_path / "cli.js"
|
|
84
|
+
cli_path.write_text("", encoding="utf-8")
|
|
85
|
+
observed = {}
|
|
86
|
+
|
|
87
|
+
monkeypatch.setattr("wup.intent_monitor.shutil.which", lambda command: str(cli_path))
|
|
88
|
+
|
|
89
|
+
def fake_run(command, **kwargs):
|
|
90
|
+
observed["command"] = command
|
|
91
|
+
return SimpleNamespace(
|
|
92
|
+
returncode=0,
|
|
93
|
+
stdout=json.dumps({"diagnosticsPath": str(path)}),
|
|
94
|
+
stderr="",
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
monkeypatch.setattr("wup.intent_monitor.subprocess.run", fake_run)
|
|
98
|
+
monitor = Todo2CodeIntentMonitor(
|
|
99
|
+
tmp_path,
|
|
100
|
+
IntentMonitoringConfig(enabled=True, command=["t2c"]),
|
|
101
|
+
lambda result: None,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
result = monitor.run_once()
|
|
105
|
+
|
|
106
|
+
assert result.status == "up"
|
|
107
|
+
assert observed["command"][:2] == ["node", str(cli_path.resolve())]
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def test_monitor_filters_codes_and_maps_review_required_to_degraded(tmp_path: Path) -> None:
|
|
111
|
+
path = _diagnostics(
|
|
112
|
+
tmp_path,
|
|
113
|
+
[
|
|
114
|
+
{"severity": "review_required", "code": "CHANGELOG_WITHOUT_IMPLEMENTATION"},
|
|
115
|
+
{"severity": "blocking", "code": "UNRELATED"},
|
|
116
|
+
],
|
|
117
|
+
)
|
|
118
|
+
config = IntentMonitoringConfig(
|
|
119
|
+
fail_codes=["CHANGELOG_WITHOUT_IMPLEMENTATION"]
|
|
120
|
+
)
|
|
121
|
+
monitor = Todo2CodeIntentMonitor(tmp_path, config, lambda result: None)
|
|
122
|
+
|
|
123
|
+
result = monitor._result_from_payload({"diagnosticsPath": str(path)})
|
|
124
|
+
|
|
125
|
+
assert result.status == "degraded"
|
|
126
|
+
assert [item["code"] for item in result.diagnostics] == [
|
|
127
|
+
"CHANGELOG_WITHOUT_IMPLEMENTATION"
|
|
128
|
+
]
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def test_monitor_reports_runner_failure_as_down(tmp_path: Path, monkeypatch) -> None:
|
|
132
|
+
monkeypatch.setattr(
|
|
133
|
+
"wup.intent_monitor.subprocess.run",
|
|
134
|
+
lambda *args, **kwargs: SimpleNamespace(
|
|
135
|
+
returncode=2, stdout="", stderr="structured output invalid"
|
|
136
|
+
),
|
|
137
|
+
)
|
|
138
|
+
results = []
|
|
139
|
+
monitor = Todo2CodeIntentMonitor(
|
|
140
|
+
tmp_path, IntentMonitoringConfig(enabled=True), results.append
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
result = monitor.run_once()
|
|
144
|
+
|
|
145
|
+
assert result.status == "down"
|
|
146
|
+
assert "structured output invalid" in result.message
|
|
147
|
+
assert results == [result]
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def test_python_runner_uses_todo2code_sdk_bridge(tmp_path: Path, monkeypatch) -> None:
|
|
151
|
+
path = _diagnostics(tmp_path, [])
|
|
152
|
+
observed = {}
|
|
153
|
+
|
|
154
|
+
class FakeRuntime:
|
|
155
|
+
def __init__(self, root, **kwargs):
|
|
156
|
+
observed["root"] = root
|
|
157
|
+
observed["init"] = kwargs
|
|
158
|
+
|
|
159
|
+
def invoke(self, arguments):
|
|
160
|
+
observed["arguments"] = arguments
|
|
161
|
+
payload = {"diagnosticsPath": str(path), "runDirectory": "python-run"}
|
|
162
|
+
return SimpleNamespace(stdout=json.dumps(payload))
|
|
163
|
+
|
|
164
|
+
module = types.ModuleType("todo2code")
|
|
165
|
+
module.TypeScriptRuntime = FakeRuntime
|
|
166
|
+
monkeypatch.setitem(sys.modules, "todo2code", module)
|
|
167
|
+
config = IntentMonitoringConfig(
|
|
168
|
+
enabled=True,
|
|
169
|
+
runner="python",
|
|
170
|
+
cli_path="/opt/todo2code/dist/src/cli.js",
|
|
171
|
+
mode="prefer-llm",
|
|
172
|
+
docs_llm=True,
|
|
173
|
+
)
|
|
174
|
+
monitor = Todo2CodeIntentMonitor(tmp_path, config, lambda result: None)
|
|
175
|
+
|
|
176
|
+
result = monitor.run_once()
|
|
177
|
+
|
|
178
|
+
assert result.status == "up"
|
|
179
|
+
assert observed["root"] == tmp_path.resolve()
|
|
180
|
+
assert observed["init"]["cli_path"] == config.cli_path
|
|
181
|
+
assert observed["arguments"][:2] == ["pipeline", str(tmp_path.resolve())]
|
|
182
|
+
assert observed["arguments"][observed["arguments"].index("--markdown-mode") + 1] == "prefer-llm"
|
|
183
|
+
assert "--no-docs-llm" not in observed["arguments"]
|
|
184
|
+
assert "--no-communication" in observed["arguments"]
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def test_testql_watcher_projects_intent_result_as_project_health() -> None:
|
|
188
|
+
watcher = object.__new__(TestQLWatcher)
|
|
189
|
+
watcher.config = SimpleNamespace(project=SimpleNamespace(name="workspace"))
|
|
190
|
+
transitions = []
|
|
191
|
+
watcher._record_health_transition = lambda **values: transitions.append(values)
|
|
192
|
+
result = IntentAuditResult(
|
|
193
|
+
status="down",
|
|
194
|
+
message="todo2code found 3 intent issue(s), 1 blocking",
|
|
195
|
+
diagnostics_path="/tmp/diagnostics.json",
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
watcher._record_intent_audit(result)
|
|
199
|
+
|
|
200
|
+
assert transitions == [
|
|
201
|
+
{
|
|
202
|
+
"service": "workspace:intent",
|
|
203
|
+
"status": "down",
|
|
204
|
+
"stage": "intent",
|
|
205
|
+
"message": result.message,
|
|
206
|
+
"track_file": result.diagnostics_path,
|
|
207
|
+
}
|
|
208
|
+
]
|
|
@@ -58,6 +58,29 @@ def test_detect_watch_paths_backend_frontend(tmp_path: Path) -> None:
|
|
|
58
58
|
assert "app/**" not in paths
|
|
59
59
|
|
|
60
60
|
|
|
61
|
+
def test_detect_watch_paths_finds_source_dirs_in_project_subfolders(
|
|
62
|
+
tmp_path: Path,
|
|
63
|
+
) -> None:
|
|
64
|
+
(tmp_path / "api" / "src").mkdir(parents=True)
|
|
65
|
+
(tmp_path / "worker" / "services").mkdir(parents=True)
|
|
66
|
+
|
|
67
|
+
assert detect_watch_paths(tmp_path) == ["api/src/**", "worker/services/**"]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_detect_watch_paths_combines_root_and_project_subfolders(tmp_path: Path) -> None:
|
|
71
|
+
(tmp_path / "src").mkdir()
|
|
72
|
+
(tmp_path / "worker" / "src").mkdir(parents=True)
|
|
73
|
+
|
|
74
|
+
assert detect_watch_paths(tmp_path) == ["src/**", "worker/src/**"]
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_detect_watch_paths_skips_nested_vendor_and_hidden_dirs(tmp_path: Path) -> None:
|
|
78
|
+
(tmp_path / "node_modules" / "package" / "src").mkdir(parents=True)
|
|
79
|
+
(tmp_path / ".cache" / "src").mkdir(parents=True)
|
|
80
|
+
|
|
81
|
+
assert detect_watch_paths(tmp_path) == ["app/**", "src/**", "routes/**"]
|
|
82
|
+
|
|
83
|
+
|
|
61
84
|
def test_default_config_watches_only_real_dirs(tmp_path: Path) -> None:
|
|
62
85
|
(tmp_path / "services").mkdir()
|
|
63
86
|
cfg = get_default_config(tmp_path)
|
|
@@ -122,6 +145,25 @@ def test_prepare_observer_none_when_no_valid_paths(tmp_path: Path) -> None:
|
|
|
122
145
|
assert watcher.prepare_observer() is None
|
|
123
146
|
|
|
124
147
|
|
|
148
|
+
def test_watcher_falls_back_to_source_dirs_in_project_subfolders(
|
|
149
|
+
tmp_path: Path,
|
|
150
|
+
) -> None:
|
|
151
|
+
nested_src = tmp_path / "api" / "src"
|
|
152
|
+
nested_services = tmp_path / "worker" / "services"
|
|
153
|
+
nested_src.mkdir(parents=True)
|
|
154
|
+
nested_services.mkdir(parents=True)
|
|
155
|
+
watcher = _watcher(tmp_path, paths=["app/**", "src/**", "routes/**"])
|
|
156
|
+
|
|
157
|
+
assert watcher.build_watched_paths() == [str(nested_src), str(nested_services)]
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def test_watcher_does_not_replace_invalid_custom_paths(tmp_path: Path) -> None:
|
|
161
|
+
(tmp_path / "src").mkdir()
|
|
162
|
+
watcher = _watcher(tmp_path, paths=["misspelled-source/**"])
|
|
163
|
+
|
|
164
|
+
assert watcher.build_watched_paths() == []
|
|
165
|
+
|
|
166
|
+
|
|
125
167
|
def test_multi_watcher_returns_false_when_all_invalid(tmp_path: Path) -> None:
|
|
126
168
|
w1 = _watcher(tmp_path, paths=["missing-a/**"])
|
|
127
169
|
w2 = _watcher(tmp_path, paths=["missing-b/**"])
|
|
@@ -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.80"
|
|
11
11
|
__author__ = "Tom Sapletta"
|
|
12
12
|
|
|
13
13
|
from .config import load_config, save_config, get_default_config
|
|
@@ -13,6 +13,7 @@ import yaml
|
|
|
13
13
|
|
|
14
14
|
from .models.config import (
|
|
15
15
|
AnomalyDetectionConfig,
|
|
16
|
+
IntentMonitoringConfig,
|
|
16
17
|
NotifyConfig,
|
|
17
18
|
PlanfileConfig,
|
|
18
19
|
ProjectConfig,
|
|
@@ -438,6 +439,52 @@ def _parse_semcod_tools_config(raw: dict) -> SemcodToolsConfig:
|
|
|
438
439
|
)
|
|
439
440
|
|
|
440
441
|
|
|
442
|
+
def _parse_intent_monitoring_config(raw: dict) -> IntentMonitoringConfig:
|
|
443
|
+
intent_raw = raw.get("intent_monitoring", {})
|
|
444
|
+
if not isinstance(intent_raw, dict):
|
|
445
|
+
return IntentMonitoringConfig()
|
|
446
|
+
|
|
447
|
+
defaults = IntentMonitoringConfig()
|
|
448
|
+
command_raw = intent_raw.get("command", defaults.command)
|
|
449
|
+
if isinstance(command_raw, str):
|
|
450
|
+
command = [command_raw]
|
|
451
|
+
elif isinstance(command_raw, list):
|
|
452
|
+
command = [str(part) for part in command_raw if str(part)]
|
|
453
|
+
else:
|
|
454
|
+
command = defaults.command
|
|
455
|
+
|
|
456
|
+
docs_raw = intent_raw.get("docs", defaults.docs)
|
|
457
|
+
severities_raw = intent_raw.get("fail_severities", defaults.fail_severities)
|
|
458
|
+
codes_raw = intent_raw.get("fail_codes", defaults.fail_codes)
|
|
459
|
+
return IntentMonitoringConfig(
|
|
460
|
+
enabled=bool(intent_raw.get("enabled", defaults.enabled)),
|
|
461
|
+
runner=str(intent_raw.get("runner", defaults.runner)),
|
|
462
|
+
command=command or defaults.command,
|
|
463
|
+
cli_path=str(intent_raw.get("cli_path", defaults.cli_path)),
|
|
464
|
+
interval_s=max(0, int(intent_raw.get("interval_s", defaults.interval_s))),
|
|
465
|
+
debounce_s=max(0, int(intent_raw.get("debounce_s", defaults.debounce_s))),
|
|
466
|
+
timeout_s=max(1, int(intent_raw.get("timeout_s", defaults.timeout_s))),
|
|
467
|
+
run_on_start=bool(intent_raw.get("run_on_start", defaults.run_on_start)),
|
|
468
|
+
run_on_change=bool(intent_raw.get("run_on_change", defaults.run_on_change)),
|
|
469
|
+
mode=str(intent_raw.get("mode", defaults.mode)),
|
|
470
|
+
docs_llm=bool(intent_raw.get("docs_llm", defaults.docs_llm)),
|
|
471
|
+
summary_llm=bool(intent_raw.get("summary_llm", defaults.summary_llm)),
|
|
472
|
+
task_file=str(intent_raw.get("task_file", defaults.task_file)),
|
|
473
|
+
todo_file=str(intent_raw.get("todo_file", defaults.todo_file)),
|
|
474
|
+
changelog_file=str(intent_raw.get("changelog_file", defaults.changelog_file)),
|
|
475
|
+
docs=[str(item) for item in docs_raw]
|
|
476
|
+
if isinstance(docs_raw, list)
|
|
477
|
+
else defaults.docs,
|
|
478
|
+
output_dir=str(intent_raw.get("output_dir", defaults.output_dir)),
|
|
479
|
+
fail_severities=[str(item) for item in severities_raw]
|
|
480
|
+
if isinstance(severities_raw, list)
|
|
481
|
+
else defaults.fail_severities,
|
|
482
|
+
fail_codes=[str(item) for item in codes_raw]
|
|
483
|
+
if isinstance(codes_raw, list)
|
|
484
|
+
else defaults.fail_codes,
|
|
485
|
+
)
|
|
486
|
+
|
|
487
|
+
|
|
441
488
|
def validate_config(
|
|
442
489
|
raw: dict, *, environ: Mapping[str, str] | None = None
|
|
443
490
|
) -> WupConfig:
|
|
@@ -465,6 +512,7 @@ def validate_config(
|
|
|
465
512
|
planfile=_parse_planfile_config(raw, effective_environ),
|
|
466
513
|
anomaly_detection=_parse_anomaly_detection_config(raw),
|
|
467
514
|
semcod_tools=_parse_semcod_tools_config(raw),
|
|
515
|
+
intent_monitoring=_parse_intent_monitoring_config(raw),
|
|
468
516
|
)
|
|
469
517
|
|
|
470
518
|
|
|
@@ -485,18 +533,52 @@ _DEFAULT_SOURCE_DIRS = [
|
|
|
485
533
|
"cmd",
|
|
486
534
|
]
|
|
487
535
|
|
|
536
|
+
_WATCH_DISCOVERY_SKIP_DIRS = {
|
|
537
|
+
".git",
|
|
538
|
+
".venv",
|
|
539
|
+
"venv",
|
|
540
|
+
"node_modules",
|
|
541
|
+
"__pycache__",
|
|
542
|
+
"dist",
|
|
543
|
+
"build",
|
|
544
|
+
}
|
|
545
|
+
|
|
488
546
|
|
|
489
547
|
def detect_watch_paths(project_root: Path) -> list:
|
|
490
548
|
"""
|
|
491
549
|
Pick watch-path globs for a project by probing for common source directories.
|
|
492
550
|
|
|
493
551
|
Returns ``["<dir>/**", ...]`` for every well-known source directory that
|
|
494
|
-
exists under ``project_root``.
|
|
495
|
-
|
|
552
|
+
exists directly under ``project_root``. For a directory containing several
|
|
553
|
+
projects, the same probe is also performed one level down (for example,
|
|
554
|
+
``api/src/**`` and ``worker/app/**``). Hidden and vendor/build directories
|
|
555
|
+
are skipped. Falls back to the classic app/src/routes guess only when none
|
|
556
|
+
are found, so a brand-new project still gets a config.
|
|
496
557
|
"""
|
|
497
558
|
found = [
|
|
498
559
|
f"{name}/**" for name in _DEFAULT_SOURCE_DIRS if (project_root / name).is_dir()
|
|
499
560
|
]
|
|
561
|
+
|
|
562
|
+
# A common workspace layout is ``root/project/src``. WUP used to only
|
|
563
|
+
# inspect ``root/src``, producing an unusable app/src/routes config for such
|
|
564
|
+
# workspaces. Limit discovery to one project level so we do not accidentally
|
|
565
|
+
# select vendored source trees deep inside dependencies.
|
|
566
|
+
try:
|
|
567
|
+
children = sorted(project_root.iterdir(), key=lambda path: path.name)
|
|
568
|
+
except OSError:
|
|
569
|
+
children = []
|
|
570
|
+
|
|
571
|
+
for child in children:
|
|
572
|
+
if (
|
|
573
|
+
not child.is_dir()
|
|
574
|
+
or child.name.startswith(".")
|
|
575
|
+
or child.name in _WATCH_DISCOVERY_SKIP_DIRS
|
|
576
|
+
):
|
|
577
|
+
continue
|
|
578
|
+
for name in _DEFAULT_SOURCE_DIRS:
|
|
579
|
+
if (child / name).is_dir():
|
|
580
|
+
found.append(f"{child.name}/{name}/**")
|
|
581
|
+
|
|
500
582
|
return found or ["app/**", "src/**", "routes/**"]
|
|
501
583
|
|
|
502
584
|
|
|
@@ -22,7 +22,7 @@ from watchdog.events import FileSystemEventHandler
|
|
|
22
22
|
from watchdog.observers import Observer
|
|
23
23
|
from watchdog.observers.polling import PollingObserver
|
|
24
24
|
|
|
25
|
-
from .config import load_config
|
|
25
|
+
from .config import detect_watch_paths, load_config
|
|
26
26
|
from .dependency_mapper import DependencyMapper
|
|
27
27
|
from .models.config import WupConfig, ServiceConfig
|
|
28
28
|
from .models.target import ServiceTestTarget
|
|
@@ -534,10 +534,12 @@ class WupWatcher:
|
|
|
534
534
|
if self.config.watch.paths:
|
|
535
535
|
# Use paths from config
|
|
536
536
|
watch_paths = []
|
|
537
|
+
configured_paths = []
|
|
537
538
|
for pattern in self.config.watch.paths:
|
|
538
539
|
# Handle exclusion patterns (starting with !)
|
|
539
540
|
if pattern.startswith("!"):
|
|
540
541
|
continue
|
|
542
|
+
configured_paths.append(pattern.rstrip("/"))
|
|
541
543
|
# Convert to absolute path
|
|
542
544
|
if "**" in pattern:
|
|
543
545
|
base_path = pattern.replace("**", "")
|
|
@@ -547,7 +549,32 @@ class WupWatcher:
|
|
|
547
549
|
abs_path = str(self.project_root / base_path)
|
|
548
550
|
if Path(abs_path).exists():
|
|
549
551
|
watch_paths.append(abs_path)
|
|
550
|
-
|
|
552
|
+
if watch_paths:
|
|
553
|
+
return watch_paths
|
|
554
|
+
|
|
555
|
+
# Older auto-generated configs always contained app/src/routes.
|
|
556
|
+
# When the supplied root is a workspace and its projects live one
|
|
557
|
+
# level down, all three paths are invalid. Re-detect at runtime so
|
|
558
|
+
# ``wup watch .`` starts successfully without rewriting user config.
|
|
559
|
+
# Preserve the configuration error for deliberate custom paths;
|
|
560
|
+
# silently replacing a misspelled path with an unrelated detected
|
|
561
|
+
# directory would hide the user's mistake.
|
|
562
|
+
if set(configured_paths) != {"app/**", "src/**", "routes/**"}:
|
|
563
|
+
return []
|
|
564
|
+
|
|
565
|
+
detected_paths = []
|
|
566
|
+
for pattern in detect_watch_paths(self.project_root):
|
|
567
|
+
base_path = pattern.replace("**", "")
|
|
568
|
+
abs_path = self.project_root / base_path
|
|
569
|
+
if abs_path.exists():
|
|
570
|
+
detected_paths.append(str(abs_path))
|
|
571
|
+
if detected_paths:
|
|
572
|
+
self.console.print(
|
|
573
|
+
"[yellow]Configured watch paths do not exist; "
|
|
574
|
+
"using auto-detected project subfolders.[/yellow]"
|
|
575
|
+
)
|
|
576
|
+
return detected_paths
|
|
577
|
+
return []
|
|
551
578
|
|
|
552
579
|
# Fallback to default paths
|
|
553
580
|
return [
|