wup 0.2.78__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.
Files changed (91) hide show
  1. {wup-0.2.78 → wup-0.2.80}/PKG-INFO +60 -7
  2. {wup-0.2.78 → wup-0.2.80}/README.md +59 -6
  3. {wup-0.2.78 → wup-0.2.80}/pyproject.toml +1 -1
  4. wup-0.2.80/tests/test_intent_monitor.py +208 -0
  5. {wup-0.2.78 → wup-0.2.80}/tests/test_multi_project.py +42 -0
  6. {wup-0.2.78 → wup-0.2.80}/wup/__init__.py +1 -1
  7. {wup-0.2.78 → wup-0.2.80}/wup/aql.py +88 -50
  8. {wup-0.2.78 → wup-0.2.80}/wup/config.py +84 -2
  9. {wup-0.2.78 → wup-0.2.80}/wup/core.py +29 -2
  10. wup-0.2.80/wup/intent_monitor.py +260 -0
  11. {wup-0.2.78 → wup-0.2.80}/wup/models/config.py +28 -0
  12. {wup-0.2.78 → wup-0.2.80}/wup/testql_watcher.py +23 -0
  13. {wup-0.2.78 → wup-0.2.80}/wup.egg-info/PKG-INFO +60 -7
  14. {wup-0.2.78 → wup-0.2.80}/wup.egg-info/SOURCES.txt +2 -0
  15. {wup-0.2.78 → wup-0.2.80}/LICENSE +0 -0
  16. {wup-0.2.78 → wup-0.2.80}/setup.cfg +0 -0
  17. {wup-0.2.78 → wup-0.2.80}/tests/test_aql.py +0 -0
  18. {wup-0.2.78 → wup-0.2.80}/tests/test_assistant.py +0 -0
  19. {wup-0.2.78 → wup-0.2.80}/tests/test_auto_detection.py +0 -0
  20. {wup-0.2.78 → wup-0.2.80}/tests/test_bootstrap.py +0 -0
  21. {wup-0.2.78 → wup-0.2.80}/tests/test_cli_bridge.py +0 -0
  22. {wup-0.2.78 → wup-0.2.80}/tests/test_cli_filtering.py +0 -0
  23. {wup-0.2.78 → wup-0.2.80}/tests/test_control.py +0 -0
  24. {wup-0.2.78 → wup-0.2.80}/tests/test_discovery_adapters.py +0 -0
  25. {wup-0.2.78 → wup-0.2.80}/tests/test_e2e.py +0 -0
  26. {wup-0.2.78 → wup-0.2.80}/tests/test_endpoints_init_cli.py +0 -0
  27. {wup-0.2.78 → wup-0.2.80}/tests/test_genericity.py +0 -0
  28. {wup-0.2.78 → wup-0.2.80}/tests/test_health_summary_passed.py +0 -0
  29. {wup-0.2.78 → wup-0.2.80}/tests/test_monitoring_manifest.py +0 -0
  30. {wup-0.2.78 → wup-0.2.80}/tests/test_oql.py +0 -0
  31. {wup-0.2.78 → wup-0.2.80}/tests/test_planfile_reporter_dedupe.py +0 -0
  32. {wup-0.2.78 → wup-0.2.80}/tests/test_probe_mutex.py +0 -0
  33. {wup-0.2.78 → wup-0.2.80}/tests/test_service_inference.py +0 -0
  34. {wup-0.2.78 → wup-0.2.80}/tests/test_status_data.py +0 -0
  35. {wup-0.2.78 → wup-0.2.80}/tests/test_sync.py +0 -0
  36. {wup-0.2.78 → wup-0.2.80}/tests/test_testql_monitor.py +0 -0
  37. {wup-0.2.78 → wup-0.2.80}/tests/test_testql_watcher.py +0 -0
  38. {wup-0.2.78 → wup-0.2.80}/tests/test_visual_diff_periodic_skip.py +0 -0
  39. {wup-0.2.78 → wup-0.2.80}/tests/test_visual_diff_progress.py +0 -0
  40. {wup-0.2.78 → wup-0.2.80}/tests/test_watch_exclude.py +0 -0
  41. {wup-0.2.78 → wup-0.2.80}/tests/test_watch_no_paths_exit.py +0 -0
  42. {wup-0.2.78 → wup-0.2.80}/tests/test_web_client.py +0 -0
  43. {wup-0.2.78 → wup-0.2.80}/tests/test_wup.py +0 -0
  44. {wup-0.2.78 → wup-0.2.80}/tests/test_wup_generate.py +0 -0
  45. {wup-0.2.78 → wup-0.2.80}/wup/_ast_detector.py +0 -0
  46. {wup-0.2.78 → wup-0.2.80}/wup/_base_detector.py +0 -0
  47. {wup-0.2.78 → wup-0.2.80}/wup/_hash_detector.py +0 -0
  48. {wup-0.2.78 → wup-0.2.80}/wup/_yaml_detector.py +0 -0
  49. {wup-0.2.78 → wup-0.2.80}/wup/anomaly_detector.py +0 -0
  50. {wup-0.2.78 → wup-0.2.80}/wup/anomaly_models.py +0 -0
  51. {wup-0.2.78 → wup-0.2.80}/wup/assistant.py +0 -0
  52. {wup-0.2.78 → wup-0.2.80}/wup/assistant_discovery.py +0 -0
  53. {wup-0.2.78 → wup-0.2.80}/wup/assistant_validator.py +0 -0
  54. {wup-0.2.78 → wup-0.2.80}/wup/bootstrap.py +0 -0
  55. {wup-0.2.78 → wup-0.2.80}/wup/bus.py +0 -0
  56. {wup-0.2.78 → wup-0.2.80}/wup/cli.py +0 -0
  57. {wup-0.2.78 → wup-0.2.80}/wup/cli_bridge.py +0 -0
  58. {wup-0.2.78 → wup-0.2.80}/wup/cli_config_generator.py +0 -0
  59. {wup-0.2.78 → wup-0.2.80}/wup/cli_scanner.py +0 -0
  60. {wup-0.2.78 → wup-0.2.80}/wup/control.py +0 -0
  61. {wup-0.2.78 → wup-0.2.80}/wup/dependency_mapper.py +0 -0
  62. {wup-0.2.78 → wup-0.2.80}/wup/discovery.py +0 -0
  63. {wup-0.2.78 → wup-0.2.80}/wup/endpoints.py +0 -0
  64. {wup-0.2.78 → wup-0.2.80}/wup/event_store.py +0 -0
  65. {wup-0.2.78 → wup-0.2.80}/wup/file_watcher/events/file_events.py +0 -0
  66. {wup-0.2.78 → wup-0.2.80}/wup/generate.py +0 -0
  67. {wup-0.2.78 → wup-0.2.80}/wup/init_cli.py +0 -0
  68. {wup-0.2.78 → wup-0.2.80}/wup/models/__init__.py +0 -0
  69. {wup-0.2.78 → wup-0.2.80}/wup/models/target.py +0 -0
  70. {wup-0.2.78 → wup-0.2.80}/wup/monitoring_manifest.py +0 -0
  71. {wup-0.2.78 → wup-0.2.80}/wup/multi.py +0 -0
  72. {wup-0.2.78 → wup-0.2.80}/wup/oql.py +0 -0
  73. {wup-0.2.78 → wup-0.2.80}/wup/paths.py +0 -0
  74. {wup-0.2.78 → wup-0.2.80}/wup/planfile_reporter.py +0 -0
  75. {wup-0.2.78 → wup-0.2.80}/wup/status_data.py +0 -0
  76. {wup-0.2.78 → wup-0.2.80}/wup/sync.py +0 -0
  77. {wup-0.2.78 → wup-0.2.80}/wup/testing/events/health_events.py +0 -0
  78. {wup-0.2.78 → wup-0.2.80}/wup/testing/events/test_results.py +0 -0
  79. {wup-0.2.78 → wup-0.2.80}/wup/testing/handlers/event_handlers.py +0 -0
  80. {wup-0.2.78 → wup-0.2.80}/wup/testing/handlers/health_handlers.py +0 -0
  81. {wup-0.2.78 → wup-0.2.80}/wup/testing/queries/health_queries.py +0 -0
  82. {wup-0.2.78 → wup-0.2.80}/wup/testql_cli_generator.py +0 -0
  83. {wup-0.2.78 → wup-0.2.80}/wup/testql_discovery.py +0 -0
  84. {wup-0.2.78 → wup-0.2.80}/wup/testql_monitor.py +0 -0
  85. {wup-0.2.78 → wup-0.2.80}/wup/validate.py +0 -0
  86. {wup-0.2.78 → wup-0.2.80}/wup/visual_diff.py +0 -0
  87. {wup-0.2.78 → wup-0.2.80}/wup/web_client.py +0 -0
  88. {wup-0.2.78 → wup-0.2.80}/wup.egg-info/dependency_links.txt +0 -0
  89. {wup-0.2.78 → wup-0.2.80}/wup.egg-info/entry_points.txt +0 -0
  90. {wup-0.2.78 → wup-0.2.80}/wup.egg-info/requires.txt +0 -0
  91. {wup-0.2.78 → 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.78
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
- ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.78-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
51
- ![AI Cost](https://img.shields.io/badge/AI%20Cost-$7.12-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-54.5h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
50
+ ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.80-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
51
+ ![AI Cost](https://img.shields.io/badge/AI%20Cost-$7.74-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-55.7h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
52
52
 
53
- - 🤖 **LLM usage:** $7.1247 (108 commits)
54
- - 👤 **Human dev:** ~$5451 (54.5h @ $100/h, 30min dedup)
53
+ - 🤖 **LLM usage:** $7.7365 (111 commits)
54
+ - 👤 **Human dev:** ~$5566 (55.7h @ $100/h, 30min dedup)
55
55
 
56
- Generated on 2026-07-29 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
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
- ![PyPI](https://img.shields.io/badge/pypi-wup-blue) ![Version](https://img.shields.io/badge/version-0.2.78-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
60
+ ![PyPI](https://img.shields.io/badge/pypi-wup-blue) ![Version](https://img.shields.io/badge/version-0.2.80-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
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
- ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.78-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
7
- ![AI Cost](https://img.shields.io/badge/AI%20Cost-$7.12-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-54.5h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
6
+ ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.80-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
7
+ ![AI Cost](https://img.shields.io/badge/AI%20Cost-$7.74-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-55.7h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
8
8
 
9
- - 🤖 **LLM usage:** $7.1247 (108 commits)
10
- - 👤 **Human dev:** ~$5451 (54.5h @ $100/h, 30min dedup)
9
+ - 🤖 **LLM usage:** $7.7365 (111 commits)
10
+ - 👤 **Human dev:** ~$5566 (55.7h @ $100/h, 30min dedup)
11
11
 
12
- Generated on 2026-07-29 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
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
- ![PyPI](https://img.shields.io/badge/pypi-wup-blue) ![Version](https://img.shields.io/badge/version-0.2.78-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
16
+ ![PyPI](https://img.shields.io/badge/pypi-wup-blue) ![Version](https://img.shields.io/badge/version-0.2.80-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
17
17
 
18
18
  **WUP (What's Up)** - Intelligent file watcher for regression testing in large projects.
19
19
 
@@ -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
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "wup"
7
- version = "0.2.78"
7
+ version = "0.2.80"
8
8
  description = "WUP (What's Up) - Intelligent file watcher for regression testing in large projects"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -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.78"
10
+ __version__ = "0.2.80"
11
11
  __author__ = "Tom Sapletta"
12
12
 
13
13
  from .config import load_config, save_config, get_default_config