wup 0.2.79__tar.gz → 0.2.81__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.81}/PKG-INFO +60 -7
- {wup-0.2.79 → wup-0.2.81}/README.md +59 -6
- {wup-0.2.79 → wup-0.2.81}/pyproject.toml +1 -1
- wup-0.2.81/tests/test_intent_monitor.py +208 -0
- {wup-0.2.79 → wup-0.2.81}/tests/test_multi_project.py +42 -0
- {wup-0.2.79 → wup-0.2.81}/tests/test_planfile_reporter_dedupe.py +7 -0
- {wup-0.2.79 → wup-0.2.81}/tests/test_testql_watcher.py +6 -3
- {wup-0.2.79 → wup-0.2.81}/wup/__init__.py +1 -1
- {wup-0.2.79 → wup-0.2.81}/wup/config.py +84 -2
- {wup-0.2.79 → wup-0.2.81}/wup/core.py +29 -2
- wup-0.2.81/wup/intent_monitor.py +260 -0
- {wup-0.2.79 → wup-0.2.81}/wup/models/config.py +28 -0
- {wup-0.2.79 → wup-0.2.81}/wup/planfile_reporter.py +19 -2
- {wup-0.2.79 → wup-0.2.81}/wup/testql_watcher.py +23 -0
- {wup-0.2.79 → wup-0.2.81}/wup.egg-info/PKG-INFO +60 -7
- {wup-0.2.79 → wup-0.2.81}/wup.egg-info/SOURCES.txt +2 -0
- {wup-0.2.79 → wup-0.2.81}/LICENSE +0 -0
- {wup-0.2.79 → wup-0.2.81}/setup.cfg +0 -0
- {wup-0.2.79 → wup-0.2.81}/tests/test_aql.py +0 -0
- {wup-0.2.79 → wup-0.2.81}/tests/test_assistant.py +0 -0
- {wup-0.2.79 → wup-0.2.81}/tests/test_auto_detection.py +0 -0
- {wup-0.2.79 → wup-0.2.81}/tests/test_bootstrap.py +0 -0
- {wup-0.2.79 → wup-0.2.81}/tests/test_cli_bridge.py +0 -0
- {wup-0.2.79 → wup-0.2.81}/tests/test_cli_filtering.py +0 -0
- {wup-0.2.79 → wup-0.2.81}/tests/test_control.py +0 -0
- {wup-0.2.79 → wup-0.2.81}/tests/test_discovery_adapters.py +0 -0
- {wup-0.2.79 → wup-0.2.81}/tests/test_e2e.py +0 -0
- {wup-0.2.79 → wup-0.2.81}/tests/test_endpoints_init_cli.py +0 -0
- {wup-0.2.79 → wup-0.2.81}/tests/test_genericity.py +0 -0
- {wup-0.2.79 → wup-0.2.81}/tests/test_health_summary_passed.py +0 -0
- {wup-0.2.79 → wup-0.2.81}/tests/test_monitoring_manifest.py +0 -0
- {wup-0.2.79 → wup-0.2.81}/tests/test_oql.py +0 -0
- {wup-0.2.79 → wup-0.2.81}/tests/test_probe_mutex.py +0 -0
- {wup-0.2.79 → wup-0.2.81}/tests/test_service_inference.py +0 -0
- {wup-0.2.79 → wup-0.2.81}/tests/test_status_data.py +0 -0
- {wup-0.2.79 → wup-0.2.81}/tests/test_sync.py +0 -0
- {wup-0.2.79 → wup-0.2.81}/tests/test_testql_monitor.py +0 -0
- {wup-0.2.79 → wup-0.2.81}/tests/test_visual_diff_periodic_skip.py +0 -0
- {wup-0.2.79 → wup-0.2.81}/tests/test_visual_diff_progress.py +0 -0
- {wup-0.2.79 → wup-0.2.81}/tests/test_watch_exclude.py +0 -0
- {wup-0.2.79 → wup-0.2.81}/tests/test_watch_no_paths_exit.py +0 -0
- {wup-0.2.79 → wup-0.2.81}/tests/test_web_client.py +0 -0
- {wup-0.2.79 → wup-0.2.81}/tests/test_wup.py +0 -0
- {wup-0.2.79 → wup-0.2.81}/tests/test_wup_generate.py +0 -0
- {wup-0.2.79 → wup-0.2.81}/wup/_ast_detector.py +0 -0
- {wup-0.2.79 → wup-0.2.81}/wup/_base_detector.py +0 -0
- {wup-0.2.79 → wup-0.2.81}/wup/_hash_detector.py +0 -0
- {wup-0.2.79 → wup-0.2.81}/wup/_yaml_detector.py +0 -0
- {wup-0.2.79 → wup-0.2.81}/wup/anomaly_detector.py +0 -0
- {wup-0.2.79 → wup-0.2.81}/wup/anomaly_models.py +0 -0
- {wup-0.2.79 → wup-0.2.81}/wup/aql.py +0 -0
- {wup-0.2.79 → wup-0.2.81}/wup/assistant.py +0 -0
- {wup-0.2.79 → wup-0.2.81}/wup/assistant_discovery.py +0 -0
- {wup-0.2.79 → wup-0.2.81}/wup/assistant_validator.py +0 -0
- {wup-0.2.79 → wup-0.2.81}/wup/bootstrap.py +0 -0
- {wup-0.2.79 → wup-0.2.81}/wup/bus.py +0 -0
- {wup-0.2.79 → wup-0.2.81}/wup/cli.py +0 -0
- {wup-0.2.79 → wup-0.2.81}/wup/cli_bridge.py +0 -0
- {wup-0.2.79 → wup-0.2.81}/wup/cli_config_generator.py +0 -0
- {wup-0.2.79 → wup-0.2.81}/wup/cli_scanner.py +0 -0
- {wup-0.2.79 → wup-0.2.81}/wup/control.py +0 -0
- {wup-0.2.79 → wup-0.2.81}/wup/dependency_mapper.py +0 -0
- {wup-0.2.79 → wup-0.2.81}/wup/discovery.py +0 -0
- {wup-0.2.79 → wup-0.2.81}/wup/endpoints.py +0 -0
- {wup-0.2.79 → wup-0.2.81}/wup/event_store.py +0 -0
- {wup-0.2.79 → wup-0.2.81}/wup/file_watcher/events/file_events.py +0 -0
- {wup-0.2.79 → wup-0.2.81}/wup/generate.py +0 -0
- {wup-0.2.79 → wup-0.2.81}/wup/init_cli.py +0 -0
- {wup-0.2.79 → wup-0.2.81}/wup/models/__init__.py +0 -0
- {wup-0.2.79 → wup-0.2.81}/wup/models/target.py +0 -0
- {wup-0.2.79 → wup-0.2.81}/wup/monitoring_manifest.py +0 -0
- {wup-0.2.79 → wup-0.2.81}/wup/multi.py +0 -0
- {wup-0.2.79 → wup-0.2.81}/wup/oql.py +0 -0
- {wup-0.2.79 → wup-0.2.81}/wup/paths.py +0 -0
- {wup-0.2.79 → wup-0.2.81}/wup/status_data.py +0 -0
- {wup-0.2.79 → wup-0.2.81}/wup/sync.py +0 -0
- {wup-0.2.79 → wup-0.2.81}/wup/testing/events/health_events.py +0 -0
- {wup-0.2.79 → wup-0.2.81}/wup/testing/events/test_results.py +0 -0
- {wup-0.2.79 → wup-0.2.81}/wup/testing/handlers/event_handlers.py +0 -0
- {wup-0.2.79 → wup-0.2.81}/wup/testing/handlers/health_handlers.py +0 -0
- {wup-0.2.79 → wup-0.2.81}/wup/testing/queries/health_queries.py +0 -0
- {wup-0.2.79 → wup-0.2.81}/wup/testql_cli_generator.py +0 -0
- {wup-0.2.79 → wup-0.2.81}/wup/testql_discovery.py +0 -0
- {wup-0.2.79 → wup-0.2.81}/wup/testql_monitor.py +0 -0
- {wup-0.2.79 → wup-0.2.81}/wup/validate.py +0 -0
- {wup-0.2.79 → wup-0.2.81}/wup/visual_diff.py +0 -0
- {wup-0.2.79 → wup-0.2.81}/wup/web_client.py +0 -0
- {wup-0.2.79 → wup-0.2.81}/wup.egg-info/dependency_links.txt +0 -0
- {wup-0.2.79 → wup-0.2.81}/wup.egg-info/entry_points.txt +0 -0
- {wup-0.2.79 → wup-0.2.81}/wup.egg-info/requires.txt +0 -0
- {wup-0.2.79 → wup-0.2.81}/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.81
|
|
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.7367 (112 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.7367 (112 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/**"])
|
|
@@ -35,6 +35,13 @@ def _seed_dedupe(rep: PlanfileReporter, fingerprint_kwargs: dict, ticket_id: str
|
|
|
35
35
|
FAIL = {"service": "svc", "status": "down", "stage": "probe", "message": "connection refused"}
|
|
36
36
|
|
|
37
37
|
|
|
38
|
+
def test_parse_ticket_id_supports_project_specific_prefixes(tmp_path):
|
|
39
|
+
rep = _reporter(tmp_path)
|
|
40
|
+
|
|
41
|
+
assert rep._parse_ticket_id("✓ Created STARTER-074: intent failure") == "STARTER-074"
|
|
42
|
+
assert rep._parse_ticket_id("Created PLF-2") == "PLF-2"
|
|
43
|
+
|
|
44
|
+
|
|
38
45
|
def test_open_ticket_still_mutes_recurrence(tmp_path, monkeypatch):
|
|
39
46
|
rep = _reporter(tmp_path)
|
|
40
47
|
_seed_dedupe(rep, FAIL, "PLF-1")
|
|
@@ -253,8 +253,11 @@ def test_planfile_reporter_creates_deduped_ticket(monkeypatch):
|
|
|
253
253
|
assert calls[0][:3] == ["planfile", "ticket", "create"]
|
|
254
254
|
assert "--label" in calls[0]
|
|
255
255
|
assert "llm-ready" in calls[0]
|
|
256
|
-
|
|
257
|
-
|
|
256
|
+
# Runtime evidence remains linked in the description but must not
|
|
257
|
+
# become Koru's source-edit scope in Planfile ``files``.
|
|
258
|
+
assert "--files" not in calls[0]
|
|
259
|
+
description = calls[0][calls[0].index("--description") + 1]
|
|
260
|
+
assert ".wup/tracks/one.json" in description
|
|
258
261
|
|
|
259
262
|
|
|
260
263
|
def test_planfile_reporter_clears_dedupe_after_recovery(monkeypatch):
|
|
@@ -307,7 +310,7 @@ def test_planfile_reporter_retries_without_files_for_old_planfile_cli(monkeypatc
|
|
|
307
310
|
status="failed",
|
|
308
311
|
stage="detail",
|
|
309
312
|
message="detail failed",
|
|
310
|
-
track_file="
|
|
313
|
+
track_file="src/firmware.py",
|
|
311
314
|
)
|
|
312
315
|
|
|
313
316
|
assert ticket_id == "PLF-998"
|
|
@@ -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.81"
|
|
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
|
|