wup 0.2.48__tar.gz → 0.2.61__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 (60) hide show
  1. {wup-0.2.48/wup.egg-info → wup-0.2.61}/PKG-INFO +27 -7
  2. {wup-0.2.48 → wup-0.2.61}/README.md +26 -6
  3. {wup-0.2.48 → wup-0.2.61}/pyproject.toml +1 -1
  4. wup-0.2.61/tests/test_assistant.py +180 -0
  5. wup-0.2.61/tests/test_health_summary_passed.py +53 -0
  6. wup-0.2.61/tests/test_probe_mutex.py +38 -0
  7. {wup-0.2.48 → wup-0.2.61}/tests/test_visual_diff_progress.py +17 -0
  8. wup-0.2.61/tests/test_watch_exclude.py +34 -0
  9. {wup-0.2.48 → wup-0.2.61}/wup/__init__.py +1 -1
  10. {wup-0.2.48 → wup-0.2.61}/wup/assistant.py +16 -116
  11. wup-0.2.61/wup/assistant_discovery.py +99 -0
  12. wup-0.2.61/wup/assistant_validator.py +57 -0
  13. {wup-0.2.48 → wup-0.2.61}/wup/cli.py +249 -169
  14. {wup-0.2.48 → wup-0.2.61}/wup/config.py +132 -71
  15. {wup-0.2.48 → wup-0.2.61}/wup/core.py +16 -4
  16. {wup-0.2.48 → wup-0.2.61}/wup/models/config.py +19 -0
  17. {wup-0.2.48 → wup-0.2.61}/wup/monitoring_manifest.py +81 -18
  18. {wup-0.2.48 → wup-0.2.61}/wup/testql_watcher.py +93 -41
  19. {wup-0.2.48 → wup-0.2.61}/wup/visual_diff.py +50 -25
  20. {wup-0.2.48 → wup-0.2.61/wup.egg-info}/PKG-INFO +27 -7
  21. {wup-0.2.48 → wup-0.2.61}/wup.egg-info/SOURCES.txt +6 -0
  22. {wup-0.2.48 → wup-0.2.61}/LICENSE +0 -0
  23. {wup-0.2.48 → wup-0.2.61}/setup.cfg +0 -0
  24. {wup-0.2.48 → wup-0.2.61}/tests/test_auto_detection.py +0 -0
  25. {wup-0.2.48 → wup-0.2.61}/tests/test_cli_filtering.py +0 -0
  26. {wup-0.2.48 → wup-0.2.61}/tests/test_e2e.py +0 -0
  27. {wup-0.2.48 → wup-0.2.61}/tests/test_monitoring_manifest.py +0 -0
  28. {wup-0.2.48 → wup-0.2.61}/tests/test_service_inference.py +0 -0
  29. {wup-0.2.48 → wup-0.2.61}/tests/test_testql_monitor.py +0 -0
  30. {wup-0.2.48 → wup-0.2.61}/tests/test_testql_watcher.py +0 -0
  31. {wup-0.2.48 → wup-0.2.61}/tests/test_visual_diff_periodic_skip.py +0 -0
  32. {wup-0.2.48 → wup-0.2.61}/tests/test_web_client.py +0 -0
  33. {wup-0.2.48 → wup-0.2.61}/tests/test_wup.py +0 -0
  34. {wup-0.2.48 → wup-0.2.61}/wup/_ast_detector.py +0 -0
  35. {wup-0.2.48 → wup-0.2.61}/wup/_base_detector.py +0 -0
  36. {wup-0.2.48 → wup-0.2.61}/wup/_hash_detector.py +0 -0
  37. {wup-0.2.48 → wup-0.2.61}/wup/_yaml_detector.py +0 -0
  38. {wup-0.2.48 → wup-0.2.61}/wup/anomaly_detector.py +0 -0
  39. {wup-0.2.48 → wup-0.2.61}/wup/anomaly_models.py +0 -0
  40. {wup-0.2.48 → wup-0.2.61}/wup/bus.py +0 -0
  41. {wup-0.2.48 → wup-0.2.61}/wup/cli_config_generator.py +0 -0
  42. {wup-0.2.48 → wup-0.2.61}/wup/cli_scanner.py +0 -0
  43. {wup-0.2.48 → wup-0.2.61}/wup/dependency_mapper.py +0 -0
  44. {wup-0.2.48 → wup-0.2.61}/wup/event_store.py +0 -0
  45. {wup-0.2.48 → wup-0.2.61}/wup/file_watcher/events/file_events.py +0 -0
  46. {wup-0.2.48 → wup-0.2.61}/wup/models/__init__.py +0 -0
  47. {wup-0.2.48 → wup-0.2.61}/wup/planfile_reporter.py +0 -0
  48. {wup-0.2.48 → wup-0.2.61}/wup/testing/events/health_events.py +0 -0
  49. {wup-0.2.48 → wup-0.2.61}/wup/testing/events/test_results.py +0 -0
  50. {wup-0.2.48 → wup-0.2.61}/wup/testing/handlers/event_handlers.py +0 -0
  51. {wup-0.2.48 → wup-0.2.61}/wup/testing/handlers/health_handlers.py +0 -0
  52. {wup-0.2.48 → wup-0.2.61}/wup/testing/queries/health_queries.py +0 -0
  53. {wup-0.2.48 → wup-0.2.61}/wup/testql_cli_generator.py +0 -0
  54. {wup-0.2.48 → wup-0.2.61}/wup/testql_discovery.py +0 -0
  55. {wup-0.2.48 → wup-0.2.61}/wup/testql_monitor.py +0 -0
  56. {wup-0.2.48 → wup-0.2.61}/wup/web_client.py +0 -0
  57. {wup-0.2.48 → wup-0.2.61}/wup.egg-info/dependency_links.txt +0 -0
  58. {wup-0.2.48 → wup-0.2.61}/wup.egg-info/entry_points.txt +0 -0
  59. {wup-0.2.48 → wup-0.2.61}/wup.egg-info/requires.txt +0 -0
  60. {wup-0.2.48 → wup-0.2.61}/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.61
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.61-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.49-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-30.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.4944 (74 commits)
38
+ - 👤 **Human dev:** ~$3068 (30.7h @ $100/h, 30min dedup)
39
39
 
40
- Generated on 2026-05-24 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
40
+ Generated on 2026-05-27 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.61-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.61-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.49-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-30.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.4944 (74 commits)
10
+ - 👤 **Human dev:** ~$3068 (30.7h @ $100/h, 30min dedup)
11
11
 
12
- Generated on 2026-05-24 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
12
+ Generated on 2026-05-27 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.61-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.61"
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"
@@ -0,0 +1,180 @@
1
+ """Unit tests for WupAssistant."""
2
+ import tempfile
3
+ from pathlib import Path
4
+ import pytest
5
+
6
+ from wup.assistant import WupAssistant
7
+ from wup.models.config import WupConfig, ProjectConfig, ServiceConfig
8
+
9
+
10
+ def test_framework_detection_fastapi():
11
+ """Test auto-detecting FastAPI project framework."""
12
+ with tempfile.TemporaryDirectory() as tmpdir:
13
+ root = Path(tmpdir)
14
+
15
+ # Create FastAPI characteristic files and content
16
+ main_py = root / "main.py"
17
+ main_py.write_text("from fastapi import FastAPI\napp = FastAPI()", encoding="utf-8")
18
+
19
+ assistant = WupAssistant(str(root))
20
+ detected = assistant._detect_framework()
21
+ assert detected == "fastapi"
22
+
23
+
24
+ def test_framework_detection_flask():
25
+ """Test auto-detecting Flask project framework."""
26
+ with tempfile.TemporaryDirectory() as tmpdir:
27
+ root = Path(tmpdir)
28
+
29
+ # Create Flask characteristic files and content
30
+ app_py = root / "app.py"
31
+ app_py.write_text("from flask import Flask\napp = Flask(__name__)", encoding="utf-8")
32
+
33
+ assistant = WupAssistant(str(root))
34
+ detected = assistant._detect_framework()
35
+ assert detected == "flask"
36
+
37
+
38
+ def test_framework_detection_none():
39
+ """Test framework detection returns None when no markers match."""
40
+ with tempfile.TemporaryDirectory() as tmpdir:
41
+ root = Path(tmpdir)
42
+ assistant = WupAssistant(str(root))
43
+ detected = assistant._detect_framework()
44
+ assert detected is None
45
+
46
+
47
+ def test_auto_detect_services_fastapi():
48
+ """Test auto-detecting services for a FastAPI project."""
49
+ with tempfile.TemporaryDirectory() as tmpdir:
50
+ root = Path(tmpdir)
51
+
52
+ # Create a router folder under app
53
+ router_dir = root / "app" / "routers"
54
+ router_dir.mkdir(parents=True)
55
+ (router_dir / "users.py").write_text("# user routes", encoding="utf-8")
56
+
57
+ assistant = WupAssistant(str(root))
58
+ services = assistant._auto_detect_services("fastapi")
59
+
60
+ assert len(services) == 1
61
+ assert services[0].name == "users"
62
+ assert services[0].type == "auto"
63
+ assert services[0].paths[0].endswith("app/routers/users.py")
64
+
65
+
66
+ def test_detect_service_type():
67
+ """Test service type detection based on name and path."""
68
+ with tempfile.TemporaryDirectory() as tmpdir:
69
+ root = Path(tmpdir)
70
+ assistant = WupAssistant(str(root))
71
+
72
+ # Web service detection
73
+ assert assistant._detect_service_type("my-api", root) == "web"
74
+ assert assistant._detect_service_type("http-server", root) == "web"
75
+
76
+ # Shell service detection
77
+ assert assistant._detect_service_type("cli-tool", root) == "shell"
78
+ assert assistant._detect_service_type("run-command", root) == "shell"
79
+
80
+ # Default fallback
81
+ assert assistant._detect_service_type("unknown", root) == "auto"
82
+
83
+
84
+ def test_validate_config_success():
85
+ """Test config validation passes on a valid config."""
86
+ with tempfile.TemporaryDirectory() as tmpdir:
87
+ root = Path(tmpdir)
88
+
89
+ # Create a watch path to satisfy validator
90
+ src_dir = root / "src"
91
+ src_dir.mkdir()
92
+
93
+ # Create scenario dir to satisfy validator
94
+ scenario_dir = root / "scenarios"
95
+ scenario_dir.mkdir()
96
+
97
+ assistant = WupAssistant(str(root))
98
+ assistant.config = WupConfig(
99
+ project=ProjectConfig(
100
+ name="MyProj",
101
+ description="Valid project"
102
+ ),
103
+ services=[
104
+ ServiceConfig(name="web-service", type="web", paths=["src"])
105
+ ]
106
+ )
107
+ assistant.config.watch.paths = ["src/**"]
108
+ assistant.config.testql.scenario_dir = "scenarios"
109
+
110
+ issues = assistant._validate_config()
111
+ assert len(issues) == 0
112
+
113
+
114
+ def test_validate_config_issues():
115
+ """Test config validation detects common errors."""
116
+ with tempfile.TemporaryDirectory() as tmpdir:
117
+ root = Path(tmpdir)
118
+ assistant = WupAssistant(str(root))
119
+
120
+ # 1. Empty/Missing project name
121
+ assistant.config.project.name = ""
122
+ # 2. No services configured
123
+ assistant.config.services = []
124
+ # 3. Non-existent watch path
125
+ assistant.config.watch.paths = ["non_existent_dir/**"]
126
+ # 4. Non-existent scenario directory
127
+ assistant.config.testql.scenario_dir = "missing_scenarios"
128
+
129
+ issues = assistant._validate_config()
130
+ assert len(issues) == 4
131
+ assert "Project name is required" in issues
132
+ assert "No services configured" in issues
133
+ assert "Watch path does not exist" in issues[2]
134
+ assert "TestQL scenario directory not found" in issues[3]
135
+
136
+
137
+ def test_generate_suggestions():
138
+ """Test assistant suggestions generation."""
139
+ with tempfile.TemporaryDirectory() as tmpdir:
140
+ root = Path(tmpdir)
141
+ assistant = WupAssistant(str(root))
142
+
143
+ # Empty watch file types, single service, web dashboard disabled
144
+ assistant.config.services = [ServiceConfig(name="single", type="web")]
145
+ assistant.config.watch.file_types = []
146
+ assistant.config.web.enabled = False
147
+ assistant.config.testql.scenario_dir = "scenarios"
148
+ assistant.config.testql.smoke_scenario = ""
149
+
150
+ suggestions = assistant._generate_suggestions()
151
+ assert len(suggestions) == 4
152
+ assert "Consider splitting into multiple services" in suggestions[0]
153
+ assert "Specify file types to avoid watching" in suggestions[1]
154
+ assert "Enable web dashboard for real-time monitoring" in suggestions[2]
155
+ assert "Set a smoke test scenario for quick health checks" in suggestions[3]
156
+
157
+
158
+ def test_quick_setup():
159
+ """Test quick non-interactive setup."""
160
+ with tempfile.TemporaryDirectory() as tmpdir:
161
+ root = Path(tmpdir)
162
+
163
+ # Create FastAPI marker to let quick setup detect it
164
+ main_py = root / "main.py"
165
+ main_py.write_text("from fastapi import FastAPI", encoding="utf-8")
166
+
167
+ assistant = WupAssistant(str(root))
168
+ assistant.run(quick=True)
169
+
170
+ # Should save wup.yaml
171
+ config_path = root / "wup.yaml"
172
+ assert config_path.exists()
173
+
174
+ # Verify content of saved config
175
+ import yaml
176
+ saved_data = yaml.safe_load(config_path.read_text(encoding="utf-8"))
177
+ assert saved_data["project"]["name"] == root.name
178
+ assert "fastapi" in saved_data["project"]["description"].lower()
179
+ assert len(saved_data["services"]) > 0
180
+ assert saved_data["web"]["enabled"] is True
@@ -0,0 +1,53 @@
1
+ """Regression: fleet health treats all-pass summaries as success in non-strict mode."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import json
7
+ import tempfile
8
+ from pathlib import Path
9
+ from subprocess import CompletedProcess
10
+
11
+ from wup.models.config import ProjectConfig, TestQLConfig, WatchConfig, WupConfig
12
+ from wup.testql_watcher import TestQLWatcher
13
+
14
+
15
+ def test_health_summary_all_passed_parser() -> None:
16
+ assert TestQLWatcher._health_summary_all_passed("17/17 passed, 0 failed")
17
+ assert not TestQLWatcher._health_summary_all_passed("16/17 passed, 1 failed")
18
+
19
+
20
+ def test_fleet_health_nonzero_exit_all_passed_counts_as_up() -> None:
21
+ with tempfile.TemporaryDirectory() as tmpdir:
22
+ root = Path(tmpdir)
23
+ scenario_dir = root / "testql-scenarios"
24
+ scenario_dir.mkdir(parents=True, exist_ok=True)
25
+ (scenario_dir / "fleet.testql.toon.yaml").write_text("name: fleet\n", encoding="utf-8")
26
+ (root / ".wup").mkdir(parents=True, exist_ok=True)
27
+
28
+ cfg = WupConfig(
29
+ project=ProjectConfig(name="demo"),
30
+ watch=WatchConfig(),
31
+ testql=TestQLConfig(
32
+ scenario_dir="testql-scenarios",
33
+ health_scenario="fleet.testql.toon.yaml",
34
+ health_scenario_strict=False,
35
+ ),
36
+ )
37
+ watcher = TestQLWatcher(
38
+ project_root=str(root),
39
+ deps_file=str(root / "deps.json"),
40
+ scenarios_dir="testql-scenarios",
41
+ track_dir=".wup/tracks",
42
+ config=cfg,
43
+ )
44
+ watcher._run_testql = lambda args, timeout: CompletedProcess( # type: ignore[method-assign]
45
+ args=args,
46
+ returncode=1,
47
+ stdout='{"passed": 17, "failed": 0}',
48
+ stderr="",
49
+ )
50
+ assert asyncio.run(watcher._run_fleet_health_scenario()) is True
51
+ state = json.loads((root / ".wup" / "service-health.json").read_text(encoding="utf-8"))
52
+ assert state["demo"]["status"] == "up"
53
+ assert state["demo"]["stage"] == "health_scenario"
@@ -0,0 +1,38 @@
1
+ """Regression: periodic probes do not overlap file-change quick/visual cycles."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import io
6
+ import threading
7
+ from unittest.mock import patch
8
+
9
+ from rich.console import Console
10
+
11
+ from wup.models.config import ProjectConfig, ServiceConfig, WupConfig
12
+ from wup.testql_watcher import TestQLWatcher
13
+
14
+
15
+ def _minimal_watcher() -> TestQLWatcher:
16
+ watcher = TestQLWatcher.__new__(TestQLWatcher)
17
+ watcher.console = Console(file=io.StringIO(), width=120)
18
+ watcher.config = WupConfig(
19
+ project=ProjectConfig(name="t"),
20
+ services=[ServiceConfig(name="frontend", paths=["frontend"])],
21
+ )
22
+ watcher._watch_work_lock = threading.Lock()
23
+ watcher._periodic_probe_in_progress = False
24
+ return watcher
25
+
26
+
27
+ def test_periodic_probe_skipped_when_watch_lock_held() -> None:
28
+ watcher = _minimal_watcher()
29
+ watcher._watch_work_lock.acquire()
30
+ try:
31
+ with patch("asyncio.run"):
32
+ watcher._run_periodic_probes_once()
33
+ finally:
34
+ watcher._watch_work_lock.release()
35
+
36
+ output = watcher.console.file.getvalue()
37
+ assert "skipped" in output.lower()
38
+ assert watcher._periodic_probe_in_progress is False
@@ -2,8 +2,11 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import io
5
6
  import os
6
7
 
8
+ from rich.console import Console
9
+
7
10
  from wup.models.config import VisualDiffConfig
8
11
  from wup.visual_diff import VisualDiffer
9
12
 
@@ -37,3 +40,17 @@ def test_progress_can_be_disabled_via_env(tmp_path, monkeypatch) -> None:
37
40
  monkeypatch.setenv("WUP_VISUAL_DIFF_PROGRESS", "0")
38
41
  differ = _make_differ(tmp_path)
39
42
  assert differ._build_progress("frontend", total=50) is None
43
+
44
+
45
+ def test_progress_uses_injected_console(tmp_path) -> None:
46
+ custom = Console(file=io.StringIO(), width=80)
47
+ cfg = VisualDiffConfig(
48
+ enabled=True,
49
+ base_url="http://localhost:8100",
50
+ snapshot_dir=str(tmp_path / "snap"),
51
+ diff_dir=str(tmp_path / "diff"),
52
+ )
53
+ differ = VisualDiffer(str(tmp_path), cfg, console=custom)
54
+ progress = differ._build_progress("frontend", total=10)
55
+ assert progress is not None
56
+ assert progress.console is custom
@@ -0,0 +1,34 @@
1
+ """Regression: nested test directories are ignored by the file watcher."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ from wup.core import WupWatcher
8
+ from wup.models.config import ProjectConfig, WatchConfig, WupConfig
9
+
10
+
11
+ def _watcher() -> WupWatcher:
12
+ cfg = WupConfig(
13
+ project=ProjectConfig(name="t"),
14
+ watch=WatchConfig(
15
+ exclude_patterns=["**/tests/**", "*.md"],
16
+ file_types=[".ts", ".py"],
17
+ ),
18
+ )
19
+ return WupWatcher(project_root=".", config=cfg)
20
+
21
+
22
+ def test_nested_tests_directory_ignored() -> None:
23
+ watcher = _watcher()
24
+ assert watcher._is_file_ignored(Path("frontend/src/tests/setup.ts"))
25
+
26
+
27
+ def test_src_file_not_ignored() -> None:
28
+ watcher = _watcher()
29
+ assert not watcher._is_file_ignored(Path("frontend/src/services/foo.ts"))
30
+
31
+
32
+ def test_glob_exclude_pattern() -> None:
33
+ watcher = _watcher()
34
+ assert watcher._path_matches_exclude_pattern(Path("docs/readme.md"), "*.md")
@@ -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.61"
11
11
  __author__ = "Tom Sapletta"
12
12
 
13
13
  from .config import load_config, save_config, get_default_config
@@ -39,6 +39,16 @@ from .models.config import (
39
39
  WebConfig,
40
40
  WupConfig,
41
41
  )
42
+ from .assistant_discovery import (
43
+ FRAMEWORK_PATTERNS,
44
+ detect_framework,
45
+ auto_detect_services,
46
+ detect_service_type,
47
+ )
48
+ from .assistant_validator import (
49
+ validate_config,
50
+ generate_suggestions,
51
+ )
42
52
 
43
53
  # Import ServiceType for type checking
44
54
  if False: # TYPE_CHECKING
@@ -50,33 +60,7 @@ console = Console()
50
60
  class WupAssistant:
51
61
  """Interactive configuration assistant."""
52
62
 
53
- # Framework detection patterns
54
- FRAMEWORK_PATTERNS = {
55
- 'fastapi': {
56
- 'files': ['main.py', 'app/main.py'],
57
- 'content': ['FastAPI', 'from fastapi', 'app = FastAPI'],
58
- 'services': ['app/routers/*', 'app/routes/*', 'routes/*'],
59
- 'default_services': ['web', 'api'],
60
- },
61
- 'flask': {
62
- 'files': ['app.py', 'wsgi.py', 'application.py'],
63
- 'content': ['Flask', 'from flask', 'app = Flask'],
64
- 'services': ['app/*/__init__.py', 'blueprints/*'],
65
- 'default_services': ['web', 'admin'],
66
- },
67
- 'django': {
68
- 'files': ['manage.py', 'settings.py'],
69
- 'content': ['Django', 'from django', 'INSTALLED_APPS'],
70
- 'services': ['*/apps.py', '*/models.py'],
71
- 'default_services': ['models', 'views', 'tasks'],
72
- },
73
- 'express': {
74
- 'files': ['server.js', 'app.js'],
75
- 'content': ['express', 'require("express")', "require('express')"],
76
- 'services': ['routes/*', 'controllers/*'],
77
- 'default_services': ['api', 'web'],
78
- },
79
- }
63
+ FRAMEWORK_PATTERNS = FRAMEWORK_PATTERNS
80
64
 
81
65
  def __init__(self, project_root: str = "."):
82
66
  self.project_root = Path(project_root).resolve()
@@ -192,59 +176,15 @@ class WupAssistant:
192
176
 
193
177
  def _detect_framework(self) -> Optional[str]:
194
178
  """Auto-detect project framework."""
195
- for framework, patterns in self.FRAMEWORK_PATTERNS.items():
196
- # Check for characteristic files
197
- for file in patterns['files']:
198
- if (self.project_root / file).exists():
199
- # Verify content
200
- content = (self.project_root / file).read_text()
201
- if any(marker in content for marker in patterns['content']):
202
- return framework
203
- return None
179
+ return detect_framework(self.project_root)
204
180
 
205
181
  def _auto_detect_services(self, framework: str) -> List[ServiceConfig]:
206
182
  """Auto-detect services based on framework patterns."""
207
- services = []
208
- patterns = self.FRAMEWORK_PATTERNS.get(framework, {})
209
-
210
- for pattern in patterns.get('services', []):
211
- for path in self.project_root.rglob(pattern):
212
- if path.is_dir() or path.is_file():
213
- service_name = path.parent.name if path.name == '__init__.py' else path.stem
214
-
215
- # Detect service type
216
- svc_type = self._detect_service_type(service_name, path)
217
-
218
- services.append(ServiceConfig(
219
- name=service_name,
220
- type=svc_type,
221
- paths=[str(path.parent if path.name == '__init__.py' else path)],
222
- ))
223
-
224
- return services
183
+ return auto_detect_services(self.project_root, framework)
225
184
 
226
185
  def _detect_service_type(self, name: str, path: Path) -> ServiceType:
227
186
  """Detect service type from name and path."""
228
- name_lower = name.lower()
229
-
230
- # Web indicators
231
- if any(x in name_lower for x in ['web', 'api', 'http', 'rest', 'router', 'route']):
232
- return 'web'
233
-
234
- # Shell indicators
235
- if any(x in name_lower for x in ['shell', 'cli', 'cmd', 'command']):
236
- return 'shell'
237
-
238
- # Check directory contents
239
- if path.is_dir():
240
- files = list(path.iterdir())
241
- has_html = any(f.suffix in ['.html', '.htm'] for f in files)
242
- has_routes = any('route' in f.name.lower() for f in files)
243
-
244
- if has_html or has_routes:
245
- return 'web'
246
-
247
- return 'auto'
187
+ return detect_service_type(name, path)
248
188
 
249
189
  def _configure_services(self):
250
190
  """Interactive service configuration."""
@@ -549,51 +489,11 @@ class WupAssistant:
549
489
 
550
490
  def _validate_config(self) -> List[str]:
551
491
  """Validate current configuration."""
552
- issues = []
553
-
554
- # Check project name
555
- if not self.config.project.name:
556
- issues.append("Project name is required")
557
-
558
- # Check services
559
- if not self.config.services:
560
- issues.append("No services configured")
561
-
562
- for svc in self.config.services:
563
- if not svc.name:
564
- issues.append("Service with empty name found")
565
-
566
- # Check watch paths exist
567
- for path in self.config.watch.paths:
568
- resolved = self.project_root / path.replace('/**', '').replace('/*', '')
569
- if not resolved.exists():
570
- issues.append(f"Watch path does not exist: {path}")
571
-
572
- # Check TestQL
573
- if self.config.testql.scenario_dir:
574
- scenario_path = self.project_root / self.config.testql.scenario_dir
575
- if not scenario_path.exists():
576
- issues.append(f"TestQL scenario directory not found: {self.config.testql.scenario_dir}")
577
-
578
- return issues
492
+ return validate_config(self.config, self.project_root)
579
493
 
580
494
  def _generate_suggestions(self) -> List[str]:
581
495
  """Generate helpful suggestions."""
582
- suggestions = []
583
-
584
- if len(self.config.services) == 1:
585
- suggestions.append("Consider splitting into multiple services for better granularity")
586
-
587
- if not self.config.watch.file_types:
588
- suggestions.append("Specify file types to avoid watching unnecessary files")
589
-
590
- if not self.config.web.enabled:
591
- suggestions.append("Enable web dashboard for real-time monitoring and notifications")
592
-
593
- if self.config.testql.scenario_dir and not self.config.testql.smoke_scenario:
594
- suggestions.append("Set a smoke test scenario for quick health checks")
595
-
596
- return suggestions
496
+ return generate_suggestions(self.config)
597
497
 
598
498
  def _save_configuration(self):
599
499
  """Save configuration to wup.yaml."""