wup 0.2.32__tar.gz → 0.2.34__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.32/wup.egg-info → wup-0.2.34}/PKG-INFO +6 -6
- {wup-0.2.32 → wup-0.2.34}/README.md +5 -5
- {wup-0.2.32 → wup-0.2.34}/pyproject.toml +1 -1
- {wup-0.2.32 → wup-0.2.34}/tests/test_testql_watcher.py +112 -0
- {wup-0.2.32 → wup-0.2.34}/tests/test_wup.py +90 -0
- {wup-0.2.32 → wup-0.2.34}/wup/__init__.py +1 -1
- {wup-0.2.32 → wup-0.2.34}/wup/cli.py +115 -53
- {wup-0.2.32 → wup-0.2.34}/wup/config.py +31 -0
- {wup-0.2.32 → wup-0.2.34}/wup/core.py +47 -14
- {wup-0.2.32 → wup-0.2.34}/wup/models/config.py +18 -0
- {wup-0.2.32 → wup-0.2.34}/wup/monitoring_manifest.py +93 -59
- wup-0.2.34/wup/planfile_reporter.py +166 -0
- {wup-0.2.32 → wup-0.2.34}/wup/testql_monitor.py +126 -67
- {wup-0.2.32 → wup-0.2.34}/wup/testql_watcher.py +49 -19
- {wup-0.2.32 → wup-0.2.34}/wup/visual_diff.py +71 -48
- {wup-0.2.32 → wup-0.2.34/wup.egg-info}/PKG-INFO +6 -6
- {wup-0.2.32 → wup-0.2.34}/wup.egg-info/SOURCES.txt +1 -0
- {wup-0.2.32 → wup-0.2.34}/LICENSE +0 -0
- {wup-0.2.32 → wup-0.2.34}/setup.cfg +0 -0
- {wup-0.2.32 → wup-0.2.34}/tests/test_e2e.py +0 -0
- {wup-0.2.32 → wup-0.2.34}/tests/test_monitoring_manifest.py +0 -0
- {wup-0.2.32 → wup-0.2.34}/tests/test_testql_monitor.py +0 -0
- {wup-0.2.32 → wup-0.2.34}/tests/test_web_client.py +0 -0
- {wup-0.2.32 → wup-0.2.34}/wup/_ast_detector.py +0 -0
- {wup-0.2.32 → wup-0.2.34}/wup/_hash_detector.py +0 -0
- {wup-0.2.32 → wup-0.2.34}/wup/_yaml_detector.py +0 -0
- {wup-0.2.32 → wup-0.2.34}/wup/anomaly_detector.py +0 -0
- {wup-0.2.32 → wup-0.2.34}/wup/anomaly_models.py +0 -0
- {wup-0.2.32 → wup-0.2.34}/wup/assistant.py +0 -0
- {wup-0.2.32 → wup-0.2.34}/wup/dependency_mapper.py +0 -0
- {wup-0.2.32 → wup-0.2.34}/wup/models/__init__.py +0 -0
- {wup-0.2.32 → wup-0.2.34}/wup/testql_discovery.py +0 -0
- {wup-0.2.32 → wup-0.2.34}/wup/web_client.py +0 -0
- {wup-0.2.32 → wup-0.2.34}/wup.egg-info/dependency_links.txt +0 -0
- {wup-0.2.32 → wup-0.2.34}/wup.egg-info/entry_points.txt +0 -0
- {wup-0.2.32 → wup-0.2.34}/wup.egg-info/requires.txt +0 -0
- {wup-0.2.32 → wup-0.2.34}/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.34
|
|
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
|
-
    
|
|
35
|
+
  
|
|
36
36
|
|
|
37
|
-
- 🤖 **LLM usage:** $2.
|
|
38
|
-
- 👤 **Human dev:** ~$
|
|
37
|
+
- 🤖 **LLM usage:** $2.6840 (44 commits)
|
|
38
|
+
- 👤 **Human dev:** ~$1867 (18.7h @ $100/h, 30min dedup)
|
|
39
39
|
|
|
40
40
|
Generated on 2026-05-21 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
|
|
41
41
|
|
|
42
42
|
---
|
|
43
43
|
|
|
44
|
-
    
|
|
45
45
|
|
|
46
46
|
**WUP (What's Up)** - Intelligent file watcher for regression testing in large projects.
|
|
47
47
|
|
|
@@ -3,17 +3,17 @@
|
|
|
3
3
|
|
|
4
4
|
## AI Cost Tracking
|
|
5
5
|
|
|
6
|
-
    
|
|
7
|
+
  
|
|
8
8
|
|
|
9
|
-
- 🤖 **LLM usage:** $2.
|
|
10
|
-
- 👤 **Human dev:** ~$
|
|
9
|
+
- 🤖 **LLM usage:** $2.6840 (44 commits)
|
|
10
|
+
- 👤 **Human dev:** ~$1867 (18.7h @ $100/h, 30min dedup)
|
|
11
11
|
|
|
12
12
|
Generated on 2026-05-21 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
|
|
|
@@ -4,9 +4,11 @@ import os
|
|
|
4
4
|
import tempfile
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
from subprocess import CompletedProcess
|
|
7
|
+
from unittest.mock import Mock
|
|
7
8
|
|
|
8
9
|
from wup.testql_watcher import TestQLWatcher
|
|
9
10
|
from wup.models.config import (
|
|
11
|
+
PlanfileConfig,
|
|
10
12
|
ProjectConfig,
|
|
11
13
|
ServiceConfig,
|
|
12
14
|
TestStrategyConfig,
|
|
@@ -15,6 +17,7 @@ from wup.models.config import (
|
|
|
15
17
|
WatchConfig,
|
|
16
18
|
WupConfig,
|
|
17
19
|
)
|
|
20
|
+
from wup.planfile_reporter import PlanfileReporter
|
|
18
21
|
|
|
19
22
|
|
|
20
23
|
def test_process_changed_file_creates_track_on_failure():
|
|
@@ -215,6 +218,115 @@ def test_service_health_transitions_are_persisted():
|
|
|
215
218
|
assert "up" in statuses
|
|
216
219
|
|
|
217
220
|
|
|
221
|
+
def test_planfile_reporter_creates_deduped_ticket(monkeypatch):
|
|
222
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
223
|
+
root = Path(tmpdir)
|
|
224
|
+
calls = []
|
|
225
|
+
|
|
226
|
+
def fake_run(cmd, **kwargs):
|
|
227
|
+
calls.append(cmd)
|
|
228
|
+
return CompletedProcess(cmd, returncode=0, stdout="Created PLF-999\n", stderr="")
|
|
229
|
+
|
|
230
|
+
monkeypatch.setattr("wup.planfile_reporter.subprocess.run", fake_run)
|
|
231
|
+
reporter = PlanfileReporter(
|
|
232
|
+
root,
|
|
233
|
+
PlanfileConfig(enabled=True, labels=["koru", "llm-ready", "wup"]),
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
first = reporter.report_failure(
|
|
237
|
+
service="frontend",
|
|
238
|
+
status="down",
|
|
239
|
+
stage="quick",
|
|
240
|
+
message="broken page",
|
|
241
|
+
track_file=".wup/tracks/one.json",
|
|
242
|
+
)
|
|
243
|
+
second = reporter.report_failure(
|
|
244
|
+
service="frontend",
|
|
245
|
+
status="down",
|
|
246
|
+
stage="quick",
|
|
247
|
+
message="broken page",
|
|
248
|
+
track_file=".wup/tracks/one.json",
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
assert first == "PLF-999"
|
|
252
|
+
assert second == "PLF-999"
|
|
253
|
+
assert len(calls) == 1
|
|
254
|
+
assert calls[0][:3] == ["planfile", "ticket", "create"]
|
|
255
|
+
assert "--label" in calls[0]
|
|
256
|
+
assert "llm-ready" in calls[0]
|
|
257
|
+
assert "--files" in calls[0]
|
|
258
|
+
assert ".wup/tracks/one.json" in calls[0]
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def test_planfile_reporter_clears_dedupe_after_recovery(monkeypatch):
|
|
262
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
263
|
+
root = Path(tmpdir)
|
|
264
|
+
calls = []
|
|
265
|
+
|
|
266
|
+
def fake_run(cmd, **kwargs):
|
|
267
|
+
calls.append(cmd)
|
|
268
|
+
ticket_id = f"PLF-{999 + len(calls)}"
|
|
269
|
+
return CompletedProcess(cmd, returncode=0, stdout=f"Created {ticket_id}\n", stderr="")
|
|
270
|
+
|
|
271
|
+
monkeypatch.setattr("wup.planfile_reporter.subprocess.run", fake_run)
|
|
272
|
+
reporter = PlanfileReporter(root, PlanfileConfig(enabled=True))
|
|
273
|
+
|
|
274
|
+
first = reporter.report_failure(
|
|
275
|
+
service="frontend",
|
|
276
|
+
status="down",
|
|
277
|
+
stage="quick",
|
|
278
|
+
message="broken page",
|
|
279
|
+
)
|
|
280
|
+
reporter.clear_service_stage(service="frontend", stage="quick")
|
|
281
|
+
second = reporter.report_failure(
|
|
282
|
+
service="frontend",
|
|
283
|
+
status="down",
|
|
284
|
+
stage="quick",
|
|
285
|
+
message="broken page",
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
assert first == "PLF-1000"
|
|
289
|
+
assert second == "PLF-1001"
|
|
290
|
+
assert len(calls) == 2
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def test_health_transition_creates_planfile_ticket(monkeypatch):
|
|
294
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
295
|
+
root = Path(tmpdir)
|
|
296
|
+
cfg = WupConfig(
|
|
297
|
+
project=ProjectConfig(name="demo"),
|
|
298
|
+
services=[ServiceConfig(name="frontend", paths=["frontend/**"])],
|
|
299
|
+
watch=WatchConfig(),
|
|
300
|
+
testql=TestQLConfig(),
|
|
301
|
+
planfile=PlanfileConfig(enabled=True),
|
|
302
|
+
)
|
|
303
|
+
watcher = TestQLWatcher(
|
|
304
|
+
project_root=str(root),
|
|
305
|
+
deps_file=str(root / "deps.json"),
|
|
306
|
+
scenarios_dir="testql-scenarios",
|
|
307
|
+
track_dir=".wup/tracks",
|
|
308
|
+
config=cfg,
|
|
309
|
+
)
|
|
310
|
+
report_failure = Mock(return_value="PLF-100")
|
|
311
|
+
watcher.planfile_reporter.report_failure = report_failure
|
|
312
|
+
|
|
313
|
+
watcher._record_health_transition(
|
|
314
|
+
service="frontend",
|
|
315
|
+
status="down",
|
|
316
|
+
stage="visual",
|
|
317
|
+
message="error_container_detected:.error-container",
|
|
318
|
+
track_file=".wup/visual-diffs/frontend.jsonl",
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
report_failure.assert_called_once_with(
|
|
322
|
+
service="frontend",
|
|
323
|
+
status="down",
|
|
324
|
+
stage="visual",
|
|
325
|
+
message="error_container_detected:.error-container",
|
|
326
|
+
track_file=".wup/visual-diffs/frontend.jsonl",
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
|
|
218
330
|
def test_normalize_fleet_health_entry_down_to_degraded():
|
|
219
331
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
220
332
|
root = Path(tmpdir)
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"""Tests for WUP (What's Up) - Intelligent file watcher for regression testing."""
|
|
2
2
|
|
|
3
|
+
import errno
|
|
3
4
|
import json
|
|
4
5
|
import tempfile
|
|
6
|
+
from unittest.mock import MagicMock, patch
|
|
5
7
|
from pathlib import Path
|
|
6
8
|
|
|
7
9
|
import pytest
|
|
@@ -18,6 +20,7 @@ from wup.models.config import (
|
|
|
18
20
|
NotifyConfig,
|
|
19
21
|
ServiceTestConfig,
|
|
20
22
|
ProjectConfig,
|
|
23
|
+
PlanfileConfig,
|
|
21
24
|
VisualDiffConfig,
|
|
22
25
|
)
|
|
23
26
|
from wup.testql_watcher import TestQLWatcher
|
|
@@ -609,6 +612,50 @@ class TestWupWatcher:
|
|
|
609
612
|
|
|
610
613
|
# No filtering should occur
|
|
611
614
|
|
|
615
|
+
def test_create_and_start_observer_fallback_on_enospc(self):
|
|
616
|
+
"""Fallback to PollingObserver when inotify watch limit is reached."""
|
|
617
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
618
|
+
watcher = WupWatcher(tmpdir)
|
|
619
|
+
event_handler = MagicMock()
|
|
620
|
+
fake_observer = MagicMock()
|
|
621
|
+
fake_observer.start.side_effect = OSError(errno.ENOSPC, "inotify watch limit reached")
|
|
622
|
+
|
|
623
|
+
with patch("wup.core.Observer", return_value=fake_observer):
|
|
624
|
+
with patch("wup.core.PollingObserver") as mock_polling:
|
|
625
|
+
fake_polling = MagicMock()
|
|
626
|
+
mock_polling.return_value = fake_polling
|
|
627
|
+
result = watcher._create_and_start_observer(event_handler, [tmpdir])
|
|
628
|
+
assert result is fake_polling
|
|
629
|
+
fake_polling.start.assert_called_once()
|
|
630
|
+
|
|
631
|
+
def test_create_and_start_observer_fallback_on_emfile(self):
|
|
632
|
+
"""Fallback to PollingObserver when inotify instance limit is reached."""
|
|
633
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
634
|
+
watcher = WupWatcher(tmpdir)
|
|
635
|
+
event_handler = MagicMock()
|
|
636
|
+
fake_observer = MagicMock()
|
|
637
|
+
fake_observer.start.side_effect = OSError(errno.EMFILE, "inotify instance limit reached")
|
|
638
|
+
|
|
639
|
+
with patch("wup.core.Observer", return_value=fake_observer):
|
|
640
|
+
with patch("wup.core.PollingObserver") as mock_polling:
|
|
641
|
+
fake_polling = MagicMock()
|
|
642
|
+
mock_polling.return_value = fake_polling
|
|
643
|
+
result = watcher._create_and_start_observer(event_handler, [tmpdir])
|
|
644
|
+
assert result is fake_polling
|
|
645
|
+
fake_polling.start.assert_called_once()
|
|
646
|
+
|
|
647
|
+
def test_create_and_start_observer_reraises_other_oserror(self):
|
|
648
|
+
"""Re-raise OSError that is not ENOSPC or EMFILE."""
|
|
649
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
650
|
+
watcher = WupWatcher(tmpdir)
|
|
651
|
+
event_handler = MagicMock()
|
|
652
|
+
fake_observer = MagicMock()
|
|
653
|
+
fake_observer.start.side_effect = OSError(errno.EACCES, "permission denied")
|
|
654
|
+
|
|
655
|
+
with patch("wup.core.Observer", return_value=fake_observer):
|
|
656
|
+
with pytest.raises(OSError, match="permission denied"):
|
|
657
|
+
watcher._create_and_start_observer(event_handler, [tmpdir])
|
|
658
|
+
|
|
612
659
|
|
|
613
660
|
class TestIntegrationWorkflow:
|
|
614
661
|
"""Integration tests for complete workflows."""
|
|
@@ -1403,6 +1450,49 @@ visual_diff:
|
|
|
1403
1450
|
assert vd.pages_from_endpoints is True
|
|
1404
1451
|
assert vd.max_pages == 200
|
|
1405
1452
|
|
|
1453
|
+
def test_save_and_load_planfile_config(self):
|
|
1454
|
+
"""Test that planfile section is correctly saved and reloaded."""
|
|
1455
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
1456
|
+
config = WupConfig(
|
|
1457
|
+
project=ProjectConfig(name="pf-test"),
|
|
1458
|
+
planfile=PlanfileConfig(
|
|
1459
|
+
enabled=True,
|
|
1460
|
+
command=".venv/bin/planfile",
|
|
1461
|
+
sprint="runtime",
|
|
1462
|
+
priority="high",
|
|
1463
|
+
source="wup-watch",
|
|
1464
|
+
dedupe_file=".wup/pf.json",
|
|
1465
|
+
labels=["koru", "llm-ready", "wup", "visual"],
|
|
1466
|
+
),
|
|
1467
|
+
)
|
|
1468
|
+
config_path = Path(tmpdir) / "wup.yaml"
|
|
1469
|
+
save_config(config, config_path)
|
|
1470
|
+
|
|
1471
|
+
loaded = load_config(Path(tmpdir), config_path)
|
|
1472
|
+
pf = loaded.planfile
|
|
1473
|
+
assert pf.enabled is True
|
|
1474
|
+
assert pf.command == ".venv/bin/planfile"
|
|
1475
|
+
assert pf.sprint == "runtime"
|
|
1476
|
+
assert pf.priority == "high"
|
|
1477
|
+
assert pf.source == "wup-watch"
|
|
1478
|
+
assert pf.dedupe_file == ".wup/pf.json"
|
|
1479
|
+
assert pf.labels == ["koru", "llm-ready", "wup", "visual"]
|
|
1480
|
+
|
|
1481
|
+
def test_load_config_planfile_env_override(self, monkeypatch):
|
|
1482
|
+
"""Env can enable planfile ticket creation without editing wup.yaml."""
|
|
1483
|
+
monkeypatch.setenv("WUP_PLANFILE_ENABLED", "true")
|
|
1484
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
1485
|
+
config_path = Path(tmpdir) / "wup.yaml"
|
|
1486
|
+
config_path.write_text(
|
|
1487
|
+
"project:\n"
|
|
1488
|
+
" name: x\n"
|
|
1489
|
+
"planfile:\n"
|
|
1490
|
+
" enabled: false\n",
|
|
1491
|
+
encoding="utf-8",
|
|
1492
|
+
)
|
|
1493
|
+
config = load_config(Path(tmpdir), config_path)
|
|
1494
|
+
assert config.planfile.enabled is True
|
|
1495
|
+
|
|
1406
1496
|
def test_load_dotenv_sets_env_var(self):
|
|
1407
1497
|
"""_load_dotenv should load .wup.env into os.environ."""
|
|
1408
1498
|
import os
|
|
@@ -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.34"
|
|
11
11
|
__author__ = "Tom Sapletta"
|
|
12
12
|
|
|
13
13
|
from .config import load_config, save_config, get_default_config
|
|
@@ -23,6 +23,100 @@ app = typer.Typer(
|
|
|
23
23
|
console = Console()
|
|
24
24
|
|
|
25
25
|
|
|
26
|
+
def _load_watch_config(
|
|
27
|
+
project_path: Path,
|
|
28
|
+
config_path: Optional[Path],
|
|
29
|
+
probe_interval: Optional[int],
|
|
30
|
+
mode: str,
|
|
31
|
+
) -> WupConfig:
|
|
32
|
+
"""Load wup.yaml config and apply CLI probe_interval override."""
|
|
33
|
+
wup_config = load_config(project_path, config_path)
|
|
34
|
+
if probe_interval is not None:
|
|
35
|
+
wup_config.testql.probe_interval_s = int(probe_interval)
|
|
36
|
+
elif mode.lower() == "testql" and not wup_config.testql.probe_interval_s:
|
|
37
|
+
wup_config.testql.probe_interval_s = 60
|
|
38
|
+
return wup_config
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _print_watch_header(
|
|
42
|
+
wup_config: WupConfig,
|
|
43
|
+
cpu_throttle: float,
|
|
44
|
+
debounce: int,
|
|
45
|
+
cooldown: int,
|
|
46
|
+
config_path: Optional[Path],
|
|
47
|
+
) -> None:
|
|
48
|
+
"""Print watcher startup banner."""
|
|
49
|
+
console.print(f"[bold cyan]🚀 WUP Watcher[/bold cyan]")
|
|
50
|
+
console.print(f"[dim]Project: {wup_config.project.name}[/dim]")
|
|
51
|
+
console.print(f"[dim]Description: {wup_config.project.description}[/dim]")
|
|
52
|
+
console.print(f"[dim]CPU Throttle: {cpu_throttle * 100}%[/dim]")
|
|
53
|
+
console.print(f"[dim]Debounce: {debounce}s[/dim]")
|
|
54
|
+
console.print(f"[dim]Cooldown: {cooldown}s[/dim]")
|
|
55
|
+
if wup_config.testql.probe_interval_s:
|
|
56
|
+
console.print(f"[dim]Live probes: every {wup_config.testql.probe_interval_s}s[/dim]")
|
|
57
|
+
console.print(f"[dim]Config: {config_path or 'auto-detected'}[/dim]")
|
|
58
|
+
console.print()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _refresh_monitoring_manifest(
|
|
62
|
+
project_path: Path,
|
|
63
|
+
wup_config: WupConfig,
|
|
64
|
+
cfg_path: Optional[Path],
|
|
65
|
+
) -> None:
|
|
66
|
+
"""Rebuild and patch monitoring manifest into wup.yaml when possible."""
|
|
67
|
+
if not cfg_path:
|
|
68
|
+
return
|
|
69
|
+
from .monitoring_manifest import build_monitoring_manifest, patch_wup_yaml_monitoring
|
|
70
|
+
try:
|
|
71
|
+
manifest = build_monitoring_manifest(project_path, wup_config)
|
|
72
|
+
patch_wup_yaml_monitoring(cfg_path, manifest)
|
|
73
|
+
console.print("[dim]Refreshed monitoring manifest in wup.yaml[/dim]")
|
|
74
|
+
except OSError as exc:
|
|
75
|
+
console.print(f"[yellow]Could not refresh monitoring manifest: {exc}[/yellow]")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _create_watcher(
|
|
79
|
+
mode: str,
|
|
80
|
+
project_path: Path,
|
|
81
|
+
deps_file: str,
|
|
82
|
+
cpu_throttle: float,
|
|
83
|
+
debounce: int,
|
|
84
|
+
cooldown: int,
|
|
85
|
+
scenarios_dir: Optional[str],
|
|
86
|
+
testql_bin: str,
|
|
87
|
+
browser_service_url: Optional[str],
|
|
88
|
+
track_dir: str,
|
|
89
|
+
quick_limit: int,
|
|
90
|
+
config: WupConfig,
|
|
91
|
+
) -> WupWatcher:
|
|
92
|
+
"""Instantiate the correct watcher class for the chosen mode."""
|
|
93
|
+
if mode.lower() == "testql":
|
|
94
|
+
watcher = TestQLWatcher(
|
|
95
|
+
project_root=str(project_path),
|
|
96
|
+
deps_file=deps_file,
|
|
97
|
+
cpu_throttle=cpu_throttle,
|
|
98
|
+
debounce_seconds=debounce,
|
|
99
|
+
test_cooldown_seconds=cooldown,
|
|
100
|
+
scenarios_dir=scenarios_dir,
|
|
101
|
+
testql_bin=testql_bin,
|
|
102
|
+
browser_service_url=browser_service_url,
|
|
103
|
+
track_dir=track_dir,
|
|
104
|
+
quick_limit=quick_limit,
|
|
105
|
+
config=config,
|
|
106
|
+
)
|
|
107
|
+
console.print("[green]TestQL mode enabled[/green]")
|
|
108
|
+
return watcher
|
|
109
|
+
|
|
110
|
+
return WupWatcher(
|
|
111
|
+
project_root=str(project_path),
|
|
112
|
+
deps_file=deps_file,
|
|
113
|
+
cpu_throttle=cpu_throttle,
|
|
114
|
+
debounce_seconds=debounce,
|
|
115
|
+
test_cooldown_seconds=cooldown,
|
|
116
|
+
config=config,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
|
|
26
120
|
@app.command()
|
|
27
121
|
def watch(
|
|
28
122
|
project: str = typer.Argument(".", help="Path to the project root directory"),
|
|
@@ -60,67 +154,35 @@ def watch(
|
|
|
60
154
|
``--mode default`` for the legacy HTTP-only watcher without TestQL.
|
|
61
155
|
"""
|
|
62
156
|
project_path = Path(project).resolve()
|
|
63
|
-
|
|
157
|
+
|
|
64
158
|
if not project_path.exists():
|
|
65
159
|
console.print(f"[red]Error: Project path '{project}' does not exist[/red]")
|
|
66
160
|
raise typer.Exit(1)
|
|
67
|
-
|
|
68
|
-
# Load configuration
|
|
69
|
-
config_path = Path(config) if config else None
|
|
70
|
-
wup_config = load_config(project_path, config_path)
|
|
71
|
-
if probe_interval is not None:
|
|
72
|
-
wup_config.testql.probe_interval_s = int(probe_interval)
|
|
73
|
-
elif mode.lower() == "testql" and not wup_config.testql.probe_interval_s:
|
|
74
|
-
wup_config.testql.probe_interval_s = 60
|
|
75
161
|
|
|
162
|
+
config_path = Path(config) if config else None
|
|
163
|
+
wup_config = _load_watch_config(project_path, config_path, probe_interval, mode)
|
|
76
164
|
effective_scenarios_dir = scenarios_dir or wup_config.testql.scenario_dir
|
|
77
165
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
console.print(f"[dim]Description: {wup_config.project.description}[/dim]")
|
|
81
|
-
console.print(f"[dim]CPU Throttle: {cpu_throttle * 100}%[/dim]")
|
|
82
|
-
console.print(f"[dim]Debounce: {debounce}s[/dim]")
|
|
83
|
-
console.print(f"[dim]Cooldown: {cooldown}s[/dim]")
|
|
84
|
-
if wup_config.testql.probe_interval_s:
|
|
85
|
-
console.print(f"[dim]Live probes: every {wup_config.testql.probe_interval_s}s[/dim]")
|
|
86
|
-
console.print(f"[dim]Config: {config_path or 'auto-detected'}[/dim]")
|
|
87
|
-
console.print()
|
|
88
|
-
|
|
166
|
+
_print_watch_header(wup_config, cpu_throttle, debounce, cooldown, config_path)
|
|
167
|
+
|
|
89
168
|
cfg_path = config_path if config_path and config_path.exists() else find_config_file(project_path)
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
169
|
+
_refresh_monitoring_manifest(project_path, wup_config, cfg_path)
|
|
170
|
+
|
|
171
|
+
watcher = _create_watcher(
|
|
172
|
+
mode=mode,
|
|
173
|
+
project_path=project_path,
|
|
174
|
+
deps_file=deps_file,
|
|
175
|
+
cpu_throttle=cpu_throttle,
|
|
176
|
+
debounce=debounce,
|
|
177
|
+
cooldown=cooldown,
|
|
178
|
+
scenarios_dir=effective_scenarios_dir,
|
|
179
|
+
testql_bin=testql_bin,
|
|
180
|
+
browser_service_url=browser_service_url,
|
|
181
|
+
track_dir=track_dir,
|
|
182
|
+
quick_limit=quick_limit,
|
|
183
|
+
config=wup_config,
|
|
184
|
+
)
|
|
98
185
|
|
|
99
|
-
if mode.lower() == "testql":
|
|
100
|
-
watcher = TestQLWatcher(
|
|
101
|
-
project_root=str(project_path),
|
|
102
|
-
deps_file=deps_file,
|
|
103
|
-
cpu_throttle=cpu_throttle,
|
|
104
|
-
debounce_seconds=debounce,
|
|
105
|
-
test_cooldown_seconds=cooldown,
|
|
106
|
-
scenarios_dir=effective_scenarios_dir,
|
|
107
|
-
testql_bin=testql_bin,
|
|
108
|
-
browser_service_url=browser_service_url,
|
|
109
|
-
track_dir=track_dir,
|
|
110
|
-
quick_limit=quick_limit,
|
|
111
|
-
config=wup_config,
|
|
112
|
-
)
|
|
113
|
-
console.print("[green]TestQL mode enabled[/green]")
|
|
114
|
-
else:
|
|
115
|
-
watcher = WupWatcher(
|
|
116
|
-
project_root=str(project_path),
|
|
117
|
-
deps_file=deps_file,
|
|
118
|
-
cpu_throttle=cpu_throttle,
|
|
119
|
-
debounce_seconds=debounce,
|
|
120
|
-
test_cooldown_seconds=cooldown,
|
|
121
|
-
config=wup_config,
|
|
122
|
-
)
|
|
123
|
-
|
|
124
186
|
if dashboard:
|
|
125
187
|
console.print("[green]Starting watcher with live dashboard...[/green]")
|
|
126
188
|
asyncio.run(watcher.run_with_dashboard())
|
|
@@ -18,6 +18,7 @@ from .models.config import (
|
|
|
18
18
|
TestQLConfig,
|
|
19
19
|
ProjectConfig,
|
|
20
20
|
NotifyConfig,
|
|
21
|
+
PlanfileConfig,
|
|
21
22
|
ServiceTestConfig,
|
|
22
23
|
VisualDiffConfig,
|
|
23
24
|
WebConfig,
|
|
@@ -258,6 +259,26 @@ def validate_config(raw: dict) -> WupConfig:
|
|
|
258
259
|
api_key=web_raw.get("api_key", ""),
|
|
259
260
|
)
|
|
260
261
|
|
|
262
|
+
# Parse planfile config (ticket sink for Koru/Planfile workflows)
|
|
263
|
+
planfile_raw = raw.get("planfile", {})
|
|
264
|
+
env_planfile_enabled = os.environ.get("WUP_PLANFILE_ENABLED")
|
|
265
|
+
if env_planfile_enabled is None:
|
|
266
|
+
planfile_enabled = bool(planfile_raw.get("enabled", False))
|
|
267
|
+
else:
|
|
268
|
+
planfile_enabled = env_planfile_enabled.strip().lower() in {"1", "true", "yes", "on"}
|
|
269
|
+
|
|
270
|
+
labels_raw = planfile_raw.get("labels", ["koru", "llm-ready", "wup", "auto-diag"])
|
|
271
|
+
labels = [str(label) for label in labels_raw] if isinstance(labels_raw, list) else []
|
|
272
|
+
planfile = PlanfileConfig(
|
|
273
|
+
enabled=planfile_enabled,
|
|
274
|
+
command=planfile_raw.get("command", "planfile"),
|
|
275
|
+
sprint=planfile_raw.get("sprint", "current"),
|
|
276
|
+
priority=planfile_raw.get("priority", "normal"),
|
|
277
|
+
source=planfile_raw.get("source", "wup"),
|
|
278
|
+
dedupe_file=planfile_raw.get("dedupe_file", ".wup/planfile-tickets.json"),
|
|
279
|
+
labels=labels or ["koru", "llm-ready", "wup", "auto-diag"],
|
|
280
|
+
)
|
|
281
|
+
|
|
261
282
|
return WupConfig(
|
|
262
283
|
project=project,
|
|
263
284
|
watch=watch,
|
|
@@ -266,6 +287,7 @@ def validate_config(raw: dict) -> WupConfig:
|
|
|
266
287
|
testql=testql,
|
|
267
288
|
visual_diff=visual_diff,
|
|
268
289
|
web=web,
|
|
290
|
+
planfile=planfile,
|
|
269
291
|
)
|
|
270
292
|
|
|
271
293
|
|
|
@@ -385,6 +407,15 @@ def save_config(config: WupConfig, output_path: Path):
|
|
|
385
407
|
"endpoint_env": config.web.endpoint_env,
|
|
386
408
|
"timeout_s": config.web.timeout_s,
|
|
387
409
|
"api_key": config.web.api_key,
|
|
410
|
+
},
|
|
411
|
+
"planfile": {
|
|
412
|
+
"enabled": config.planfile.enabled,
|
|
413
|
+
"command": config.planfile.command,
|
|
414
|
+
"sprint": config.planfile.sprint,
|
|
415
|
+
"priority": config.planfile.priority,
|
|
416
|
+
"source": config.planfile.source,
|
|
417
|
+
"dedupe_file": config.planfile.dedupe_file,
|
|
418
|
+
"labels": config.planfile.labels,
|
|
388
419
|
}
|
|
389
420
|
}
|
|
390
421
|
|