wup 0.2.48__tar.gz → 0.2.49__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 (54) hide show
  1. {wup-0.2.48/wup.egg-info → wup-0.2.49}/PKG-INFO +26 -6
  2. {wup-0.2.48 → wup-0.2.49}/README.md +25 -5
  3. {wup-0.2.48 → wup-0.2.49}/pyproject.toml +1 -1
  4. {wup-0.2.48 → wup-0.2.49}/wup/__init__.py +1 -1
  5. {wup-0.2.48 → wup-0.2.49}/wup/config.py +51 -42
  6. {wup-0.2.48 → wup-0.2.49}/wup/testql_watcher.py +30 -27
  7. {wup-0.2.48 → wup-0.2.49/wup.egg-info}/PKG-INFO +26 -6
  8. {wup-0.2.48 → wup-0.2.49}/LICENSE +0 -0
  9. {wup-0.2.48 → wup-0.2.49}/setup.cfg +0 -0
  10. {wup-0.2.48 → wup-0.2.49}/tests/test_auto_detection.py +0 -0
  11. {wup-0.2.48 → wup-0.2.49}/tests/test_cli_filtering.py +0 -0
  12. {wup-0.2.48 → wup-0.2.49}/tests/test_e2e.py +0 -0
  13. {wup-0.2.48 → wup-0.2.49}/tests/test_monitoring_manifest.py +0 -0
  14. {wup-0.2.48 → wup-0.2.49}/tests/test_service_inference.py +0 -0
  15. {wup-0.2.48 → wup-0.2.49}/tests/test_testql_monitor.py +0 -0
  16. {wup-0.2.48 → wup-0.2.49}/tests/test_testql_watcher.py +0 -0
  17. {wup-0.2.48 → wup-0.2.49}/tests/test_visual_diff_periodic_skip.py +0 -0
  18. {wup-0.2.48 → wup-0.2.49}/tests/test_visual_diff_progress.py +0 -0
  19. {wup-0.2.48 → wup-0.2.49}/tests/test_web_client.py +0 -0
  20. {wup-0.2.48 → wup-0.2.49}/tests/test_wup.py +0 -0
  21. {wup-0.2.48 → wup-0.2.49}/wup/_ast_detector.py +0 -0
  22. {wup-0.2.48 → wup-0.2.49}/wup/_base_detector.py +0 -0
  23. {wup-0.2.48 → wup-0.2.49}/wup/_hash_detector.py +0 -0
  24. {wup-0.2.48 → wup-0.2.49}/wup/_yaml_detector.py +0 -0
  25. {wup-0.2.48 → wup-0.2.49}/wup/anomaly_detector.py +0 -0
  26. {wup-0.2.48 → wup-0.2.49}/wup/anomaly_models.py +0 -0
  27. {wup-0.2.48 → wup-0.2.49}/wup/assistant.py +0 -0
  28. {wup-0.2.48 → wup-0.2.49}/wup/bus.py +0 -0
  29. {wup-0.2.48 → wup-0.2.49}/wup/cli.py +0 -0
  30. {wup-0.2.48 → wup-0.2.49}/wup/cli_config_generator.py +0 -0
  31. {wup-0.2.48 → wup-0.2.49}/wup/cli_scanner.py +0 -0
  32. {wup-0.2.48 → wup-0.2.49}/wup/core.py +0 -0
  33. {wup-0.2.48 → wup-0.2.49}/wup/dependency_mapper.py +0 -0
  34. {wup-0.2.48 → wup-0.2.49}/wup/event_store.py +0 -0
  35. {wup-0.2.48 → wup-0.2.49}/wup/file_watcher/events/file_events.py +0 -0
  36. {wup-0.2.48 → wup-0.2.49}/wup/models/__init__.py +0 -0
  37. {wup-0.2.48 → wup-0.2.49}/wup/models/config.py +0 -0
  38. {wup-0.2.48 → wup-0.2.49}/wup/monitoring_manifest.py +0 -0
  39. {wup-0.2.48 → wup-0.2.49}/wup/planfile_reporter.py +0 -0
  40. {wup-0.2.48 → wup-0.2.49}/wup/testing/events/health_events.py +0 -0
  41. {wup-0.2.48 → wup-0.2.49}/wup/testing/events/test_results.py +0 -0
  42. {wup-0.2.48 → wup-0.2.49}/wup/testing/handlers/event_handlers.py +0 -0
  43. {wup-0.2.48 → wup-0.2.49}/wup/testing/handlers/health_handlers.py +0 -0
  44. {wup-0.2.48 → wup-0.2.49}/wup/testing/queries/health_queries.py +0 -0
  45. {wup-0.2.48 → wup-0.2.49}/wup/testql_cli_generator.py +0 -0
  46. {wup-0.2.48 → wup-0.2.49}/wup/testql_discovery.py +0 -0
  47. {wup-0.2.48 → wup-0.2.49}/wup/testql_monitor.py +0 -0
  48. {wup-0.2.48 → wup-0.2.49}/wup/visual_diff.py +0 -0
  49. {wup-0.2.48 → wup-0.2.49}/wup/web_client.py +0 -0
  50. {wup-0.2.48 → wup-0.2.49}/wup.egg-info/SOURCES.txt +0 -0
  51. {wup-0.2.48 → wup-0.2.49}/wup.egg-info/dependency_links.txt +0 -0
  52. {wup-0.2.48 → wup-0.2.49}/wup.egg-info/entry_points.txt +0 -0
  53. {wup-0.2.48 → wup-0.2.49}/wup.egg-info/requires.txt +0 -0
  54. {wup-0.2.48 → wup-0.2.49}/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.48
3
+ Version: 0.2.49
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
@@ -31,17 +31,17 @@ Dynamic: license-file
31
31
 
32
32
  ## AI Cost Tracking
33
33
 
34
- ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.48-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
35
- ![AI Cost](https://img.shields.io/badge/AI%20Cost-$3.35-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-25.7h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
34
+ ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.49-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
35
+ ![AI Cost](https://img.shields.io/badge/AI%20Cost-$3.12-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-25.7h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
36
36
 
37
- - 🤖 **LLM usage:** $3.3508 (60 commits)
38
- - 👤 **Human dev:** ~$2573 (25.7h @ $100/h, 30min dedup)
37
+ - 🤖 **LLM usage:** $3.1175 (62 commits)
38
+ - 👤 **Human dev:** ~$2574 (25.7h @ $100/h, 30min dedup)
39
39
 
40
40
  Generated on 2026-05-24 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
41
41
 
42
42
  ---
43
43
 
44
- ![PyPI](https://img.shields.io/badge/pypi-wup-blue) ![Version](https://img.shields.io/badge/version-0.2.48-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
44
+ ![PyPI](https://img.shields.io/badge/pypi-wup-blue) ![Version](https://img.shields.io/badge/version-0.2.49-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
45
45
 
46
46
  **WUP (What's Up)** - Intelligent file watcher for regression testing in large projects.
47
47
 
@@ -512,6 +512,26 @@ python3 -m pytest tests/test_testql_watcher.py -v
512
512
  python3 -m pytest tests/ --cov=wup
513
513
  ```
514
514
 
515
+ ### Goal wrapper (local `.venv`)
516
+
517
+ When `goal` is installed globally, it may inherit another project's `VIRTUAL_ENV`.
518
+ Use the local wrapper to force `wup/.venv` before running `goal` commands:
519
+
520
+ ```bash
521
+ # Default: runs goal -a
522
+ bash scripts/goal-local
523
+
524
+ # Explicit arguments
525
+ bash scripts/goal-local -a
526
+ bash scripts/goal-local --dry-run
527
+ ```
528
+
529
+ If needed, point to a specific `goal` binary:
530
+
531
+ ```bash
532
+ GOAL_BIN=/home/tom/.local/bin/goal bash scripts/goal-local -a
533
+ ```
534
+
515
535
  ### Examples
516
536
 
517
537
  ```bash
@@ -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.48-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-$3.35-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-25.7h-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.49-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-$3.12-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-25.7h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
8
8
 
9
- - 🤖 **LLM usage:** $3.3508 (60 commits)
10
- - 👤 **Human dev:** ~$2573 (25.7h @ $100/h, 30min dedup)
9
+ - 🤖 **LLM usage:** $3.1175 (62 commits)
10
+ - 👤 **Human dev:** ~$2574 (25.7h @ $100/h, 30min dedup)
11
11
 
12
12
  Generated on 2026-05-24 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.48-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.49-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
 
@@ -484,6 +484,26 @@ python3 -m pytest tests/test_testql_watcher.py -v
484
484
  python3 -m pytest tests/ --cov=wup
485
485
  ```
486
486
 
487
+ ### Goal wrapper (local `.venv`)
488
+
489
+ When `goal` is installed globally, it may inherit another project's `VIRTUAL_ENV`.
490
+ Use the local wrapper to force `wup/.venv` before running `goal` commands:
491
+
492
+ ```bash
493
+ # Default: runs goal -a
494
+ bash scripts/goal-local
495
+
496
+ # Explicit arguments
497
+ bash scripts/goal-local -a
498
+ bash scripts/goal-local --dry-run
499
+ ```
500
+
501
+ If needed, point to a specific `goal` binary:
502
+
503
+ ```bash
504
+ GOAL_BIN=/home/tom/.local/bin/goal bash scripts/goal-local -a
505
+ ```
506
+
487
507
  ### Examples
488
508
 
489
509
  ```bash
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "wup"
7
- version = "0.2.48"
7
+ version = "0.2.49"
8
8
  description = "WUP (What's Up) - Intelligent file watcher for regression testing in large projects"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -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.48"
10
+ __version__ = "0.2.49"
11
11
  __author__ = "Tom Sapletta"
12
12
 
13
13
  from .config import load_config, save_config, get_default_config
@@ -6,7 +6,7 @@ Handles loading and validation of wup.yaml configuration files.
6
6
 
7
7
  import os
8
8
  from pathlib import Path
9
- from typing import Optional
9
+ from typing import Optional, List
10
10
 
11
11
  import yaml
12
12
 
@@ -98,38 +98,27 @@ def load_config(project_root: Path, config_path: Optional[Path] = None) -> WupCo
98
98
  return validate_config(raw_config)
99
99
 
100
100
 
101
- def validate_config(raw: dict) -> WupConfig:
102
- """
103
- Validate raw config dict and convert to WupConfig object.
104
-
105
- Args:
106
- raw: Raw configuration dictionary from YAML
107
-
108
- Returns:
109
- Validated WupConfig object
110
-
111
- Raises:
112
- ValueError: If config is invalid
113
- """
114
- # Parse project config
101
+ def _parse_project_config(raw: dict) -> ProjectConfig:
115
102
  project_raw = raw.get("project", {})
116
103
  if not project_raw.get("name"):
117
104
  raise ValueError("Config must contain project.name")
118
105
 
119
- project = ProjectConfig(
106
+ return ProjectConfig(
120
107
  name=project_raw["name"],
121
108
  description=project_raw.get("description", "")
122
109
  )
123
-
124
- # Parse watch config
110
+
111
+
112
+ def _parse_watch_config(raw: dict) -> WatchConfig:
125
113
  watch_raw = raw.get("watch", {})
126
- watch = WatchConfig(
114
+ return WatchConfig(
127
115
  paths=watch_raw.get("paths", []),
128
116
  exclude_patterns=watch_raw.get("exclude_patterns", ["*.md", "*.txt"]),
129
117
  file_types=watch_raw.get("file_types", [])
130
118
  )
131
-
132
- # Parse services
119
+
120
+
121
+ def _parse_services_config(raw: dict) -> List[ServiceConfig]:
133
122
  services_raw = raw.get("services", [])
134
123
  services = []
135
124
  for svc_raw in services_raw:
@@ -161,18 +150,20 @@ def validate_config(raw: dict) -> WupConfig:
161
150
  )
162
151
  )
163
152
  services.append(service)
164
-
165
- # Parse test strategy
153
+ return services
154
+
155
+
156
+ def _parse_strategy_config(raw: dict) -> TestStrategyConfig:
166
157
  strategy_raw = raw.get("test_strategy", {})
167
- test_strategy = TestStrategyConfig(
158
+ return TestStrategyConfig(
168
159
  quick=strategy_raw.get("quick", {"debounce_s": 2, "max_queue": 5, "timeout_s": 10}),
169
160
  detail=strategy_raw.get("detail", {"debounce_s": 10, "max_queue": 1, "timeout_s": 30})
170
161
  )
171
-
172
- # Parse testql config
162
+
163
+
164
+ def _parse_testql_config(raw: dict) -> TestQLConfig:
173
165
  testql_raw = raw.get("testql", {})
174
166
  extra_args_raw = testql_raw.get("extra_args", ["--timeout", "10"])
175
- extra_args = []
176
167
  if isinstance(extra_args_raw, str):
177
168
  extra_args_raw = extra_args_raw.split()
178
169
  elif isinstance(extra_args_raw, list):
@@ -215,7 +206,7 @@ def validate_config(raw: dict) -> WupConfig:
215
206
  normalized_extra_args.append(arg)
216
207
  i += 1
217
208
 
218
- testql = TestQLConfig(
209
+ return TestQLConfig(
219
210
  scenario_dir=testql_raw.get("scenario_dir", "scenarios/tests"),
220
211
  smoke_scenario=testql_raw.get("smoke_scenario", "smoke.testql.toon.yaml"),
221
212
  output_format=testql_raw.get("output_format", "json"),
@@ -233,7 +224,8 @@ def validate_config(raw: dict) -> WupConfig:
233
224
  endpoints_by_service=testql_raw.get("endpoints_by_service", {})
234
225
  )
235
226
 
236
- # Parse visual_diff config
227
+
228
+ def _parse_visual_diff_config(raw: dict) -> VisualDiffConfig:
237
229
  vd_raw = raw.get("visual_diff", {})
238
230
  env_visual_enabled = os.environ.get("WUP_VISUAL_DIFF_ENABLED")
239
231
  env_visual_delay = os.environ.get("WUP_VISUAL_DIFF_DELAY_SECONDS")
@@ -268,7 +260,7 @@ def validate_config(raw: dict) -> WupConfig:
268
260
  env_visual_pages_from_endpoints.strip().lower() in {"1", "true", "yes", "on"}
269
261
  )
270
262
 
271
- visual_diff = VisualDiffConfig(
263
+ return VisualDiffConfig(
272
264
  enabled=visual_enabled,
273
265
  base_url=vd_raw.get("base_url", ""),
274
266
  base_url_env=vd_raw.get("base_url_env", "WUP_BASE_URL"),
@@ -291,11 +283,13 @@ def validate_config(raw: dict) -> WupConfig:
291
283
  "[class*='error'][class*='container']",
292
284
  ]),
293
285
  headless=vd_raw.get("headless", True),
286
+ run_on_periodic_probe=bool(vd_raw.get("run_on_periodic_probe", False)),
294
287
  )
295
288
 
296
- # Parse web config (event sink)
289
+
290
+ def _parse_web_config(raw: dict) -> WebConfig:
297
291
  web_raw = raw.get("web", {})
298
- web = WebConfig(
292
+ return WebConfig(
299
293
  enabled=web_raw.get("enabled", False),
300
294
  endpoint=web_raw.get("endpoint", ""),
301
295
  endpoint_env=web_raw.get("endpoint_env", "WUPBRO_ENDPOINT"),
@@ -303,7 +297,8 @@ def validate_config(raw: dict) -> WupConfig:
303
297
  api_key=web_raw.get("api_key", ""),
304
298
  )
305
299
 
306
- # Parse planfile config (ticket sink for Koru/Planfile workflows)
300
+
301
+ def _parse_planfile_config(raw: dict) -> PlanfileConfig:
307
302
  planfile_raw = raw.get("planfile", {})
308
303
  env_planfile_enabled = os.environ.get("WUP_PLANFILE_ENABLED")
309
304
  if env_planfile_enabled is None:
@@ -313,7 +308,7 @@ def validate_config(raw: dict) -> WupConfig:
313
308
 
314
309
  labels_raw = planfile_raw.get("labels", ["koru", "llm-ready", "wup", "auto-diag"])
315
310
  labels = [str(label) for label in labels_raw] if isinstance(labels_raw, list) else []
316
- planfile = PlanfileConfig(
311
+ return PlanfileConfig(
317
312
  enabled=planfile_enabled,
318
313
  command=planfile_raw.get("command", "planfile"),
319
314
  sprint=planfile_raw.get("sprint", "current"),
@@ -323,15 +318,29 @@ def validate_config(raw: dict) -> WupConfig:
323
318
  labels=labels or ["koru", "llm-ready", "wup", "auto-diag"],
324
319
  )
325
320
 
321
+
322
+ def validate_config(raw: dict) -> WupConfig:
323
+ """
324
+ Validate raw config dict and convert to WupConfig object.
325
+
326
+ Args:
327
+ raw: Raw configuration dictionary from YAML
328
+
329
+ Returns:
330
+ Validated WupConfig object
331
+
332
+ Raises:
333
+ ValueError: If config is invalid
334
+ """
326
335
  return WupConfig(
327
- project=project,
328
- watch=watch,
329
- services=services,
330
- test_strategy=test_strategy,
331
- testql=testql,
332
- visual_diff=visual_diff,
333
- web=web,
334
- planfile=planfile,
336
+ project=_parse_project_config(raw),
337
+ watch=_parse_watch_config(raw),
338
+ services=_parse_services_config(raw),
339
+ test_strategy=_parse_strategy_config(raw),
340
+ testql=_parse_testql_config(raw),
341
+ visual_diff=_parse_visual_diff_config(raw),
342
+ web=_parse_web_config(raw),
343
+ planfile=_parse_planfile_config(raw),
335
344
  )
336
345
 
337
346
 
@@ -696,6 +696,35 @@ class TestQLWatcher(WupWatcher):
696
696
  )
697
697
  return True
698
698
 
699
+ async def _run_quick_test_no_scenarios(self, service: str, merged_endpoints: List[str]) -> bool:
700
+ self.console.print(
701
+ f"[yellow]⚠ No TestQL scenarios found for {service} — running visual diff only[/yellow]"
702
+ )
703
+ self._record_health_transition(service=service, status="up", stage="quick", message="Probes passed (no scenarios)")
704
+ if self.web_client.is_active:
705
+ await self.web_client.send_pass(service=service, stage="quick")
706
+ if self.visual_differ and self.visual_differ.cfg.enabled:
707
+ visual_endpoints = self._get_config_endpoints_for_service(service) or merged_endpoints
708
+ visual_results = await self.visual_differ.run_for_service(service, visual_endpoints)
709
+ visual_issues = [
710
+ item for item in visual_results
711
+ if item.get("diff", {}).get("status") == "issue"
712
+ ]
713
+ if visual_issues:
714
+ issue_text = "; ".join(
715
+ ", ".join(item.get("diff", {}).get("issues", []) or ["visual page issue"])
716
+ for item in visual_issues
717
+ )
718
+ self._record_health_transition(
719
+ service=service,
720
+ status="down",
721
+ stage="visual",
722
+ message=issue_text or "visual page issue",
723
+ track_file="",
724
+ )
725
+ await self._publish_visual_events(service, visual_results)
726
+ return True
727
+
699
728
  async def run_quick_test(self, service: str, endpoints: List[str]) -> bool:
700
729
  merged_endpoints = self._merge_endpoints(service, endpoints)
701
730
 
@@ -709,33 +738,7 @@ class TestQLWatcher(WupWatcher):
709
738
  scenarios = scenarios[:limit]
710
739
 
711
740
  if not scenarios:
712
- self.console.print(
713
- f"[yellow]⚠ No TestQL scenarios found for {service} — running visual diff only[/yellow]"
714
- )
715
- self._record_health_transition(service=service, status="up", stage="quick", message="Probes passed (no scenarios)")
716
- if self.web_client.is_active:
717
- await self.web_client.send_pass(service=service, stage="quick")
718
- if self.visual_differ and self.visual_differ.cfg.enabled:
719
- visual_endpoints = self._get_config_endpoints_for_service(service) or merged_endpoints
720
- visual_results = await self.visual_differ.run_for_service(service, visual_endpoints)
721
- visual_issues = [
722
- item for item in visual_results
723
- if item.get("diff", {}).get("status") == "issue"
724
- ]
725
- if visual_issues:
726
- issue_text = "; ".join(
727
- ", ".join(item.get("diff", {}).get("issues", []) or ["visual page issue"])
728
- for item in visual_issues
729
- )
730
- self._record_health_transition(
731
- service=service,
732
- status="down",
733
- stage="visual",
734
- message=issue_text or "visual page issue",
735
- track_file="",
736
- )
737
- await self._publish_visual_events(service, visual_results)
738
- return True
741
+ return await self._run_quick_test_no_scenarios(service, merged_endpoints)
739
742
 
740
743
  self.console.print(
741
744
  f"[cyan]🧪 Quick TestQL for {service} "
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wup
3
- Version: 0.2.48
3
+ Version: 0.2.49
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
@@ -31,17 +31,17 @@ Dynamic: license-file
31
31
 
32
32
  ## AI Cost Tracking
33
33
 
34
- ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.48-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
35
- ![AI Cost](https://img.shields.io/badge/AI%20Cost-$3.35-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-25.7h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
34
+ ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.49-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
35
+ ![AI Cost](https://img.shields.io/badge/AI%20Cost-$3.12-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-25.7h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
36
36
 
37
- - 🤖 **LLM usage:** $3.3508 (60 commits)
38
- - 👤 **Human dev:** ~$2573 (25.7h @ $100/h, 30min dedup)
37
+ - 🤖 **LLM usage:** $3.1175 (62 commits)
38
+ - 👤 **Human dev:** ~$2574 (25.7h @ $100/h, 30min dedup)
39
39
 
40
40
  Generated on 2026-05-24 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
41
41
 
42
42
  ---
43
43
 
44
- ![PyPI](https://img.shields.io/badge/pypi-wup-blue) ![Version](https://img.shields.io/badge/version-0.2.48-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
44
+ ![PyPI](https://img.shields.io/badge/pypi-wup-blue) ![Version](https://img.shields.io/badge/version-0.2.49-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
45
45
 
46
46
  **WUP (What's Up)** - Intelligent file watcher for regression testing in large projects.
47
47
 
@@ -512,6 +512,26 @@ python3 -m pytest tests/test_testql_watcher.py -v
512
512
  python3 -m pytest tests/ --cov=wup
513
513
  ```
514
514
 
515
+ ### Goal wrapper (local `.venv`)
516
+
517
+ When `goal` is installed globally, it may inherit another project's `VIRTUAL_ENV`.
518
+ Use the local wrapper to force `wup/.venv` before running `goal` commands:
519
+
520
+ ```bash
521
+ # Default: runs goal -a
522
+ bash scripts/goal-local
523
+
524
+ # Explicit arguments
525
+ bash scripts/goal-local -a
526
+ bash scripts/goal-local --dry-run
527
+ ```
528
+
529
+ If needed, point to a specific `goal` binary:
530
+
531
+ ```bash
532
+ GOAL_BIN=/home/tom/.local/bin/goal bash scripts/goal-local -a
533
+ ```
534
+
515
535
  ### Examples
516
536
 
517
537
  ```bash
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes