wup 0.2.47__tar.gz → 0.2.48__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.47/wup.egg-info → wup-0.2.48}/PKG-INFO +6 -6
  2. {wup-0.2.47 → wup-0.2.48}/README.md +5 -5
  3. {wup-0.2.47 → wup-0.2.48}/pyproject.toml +1 -1
  4. wup-0.2.48/tests/test_visual_diff_periodic_skip.py +40 -0
  5. wup-0.2.48/tests/test_visual_diff_progress.py +39 -0
  6. {wup-0.2.47 → wup-0.2.48}/wup/__init__.py +1 -1
  7. {wup-0.2.47 → wup-0.2.48}/wup/models/config.py +3 -0
  8. {wup-0.2.47 → wup-0.2.48}/wup/testing/handlers/event_handlers.py +7 -1
  9. {wup-0.2.47 → wup-0.2.48}/wup/testql_watcher.py +39 -12
  10. {wup-0.2.47 → wup-0.2.48}/wup/visual_diff.py +42 -4
  11. {wup-0.2.47 → wup-0.2.48/wup.egg-info}/PKG-INFO +6 -6
  12. {wup-0.2.47 → wup-0.2.48}/wup.egg-info/SOURCES.txt +2 -0
  13. {wup-0.2.47 → wup-0.2.48}/LICENSE +0 -0
  14. {wup-0.2.47 → wup-0.2.48}/setup.cfg +0 -0
  15. {wup-0.2.47 → wup-0.2.48}/tests/test_auto_detection.py +0 -0
  16. {wup-0.2.47 → wup-0.2.48}/tests/test_cli_filtering.py +0 -0
  17. {wup-0.2.47 → wup-0.2.48}/tests/test_e2e.py +0 -0
  18. {wup-0.2.47 → wup-0.2.48}/tests/test_monitoring_manifest.py +0 -0
  19. {wup-0.2.47 → wup-0.2.48}/tests/test_service_inference.py +0 -0
  20. {wup-0.2.47 → wup-0.2.48}/tests/test_testql_monitor.py +0 -0
  21. {wup-0.2.47 → wup-0.2.48}/tests/test_testql_watcher.py +0 -0
  22. {wup-0.2.47 → wup-0.2.48}/tests/test_web_client.py +0 -0
  23. {wup-0.2.47 → wup-0.2.48}/tests/test_wup.py +0 -0
  24. {wup-0.2.47 → wup-0.2.48}/wup/_ast_detector.py +0 -0
  25. {wup-0.2.47 → wup-0.2.48}/wup/_base_detector.py +0 -0
  26. {wup-0.2.47 → wup-0.2.48}/wup/_hash_detector.py +0 -0
  27. {wup-0.2.47 → wup-0.2.48}/wup/_yaml_detector.py +0 -0
  28. {wup-0.2.47 → wup-0.2.48}/wup/anomaly_detector.py +0 -0
  29. {wup-0.2.47 → wup-0.2.48}/wup/anomaly_models.py +0 -0
  30. {wup-0.2.47 → wup-0.2.48}/wup/assistant.py +0 -0
  31. {wup-0.2.47 → wup-0.2.48}/wup/bus.py +0 -0
  32. {wup-0.2.47 → wup-0.2.48}/wup/cli.py +0 -0
  33. {wup-0.2.47 → wup-0.2.48}/wup/cli_config_generator.py +0 -0
  34. {wup-0.2.47 → wup-0.2.48}/wup/cli_scanner.py +0 -0
  35. {wup-0.2.47 → wup-0.2.48}/wup/config.py +0 -0
  36. {wup-0.2.47 → wup-0.2.48}/wup/core.py +0 -0
  37. {wup-0.2.47 → wup-0.2.48}/wup/dependency_mapper.py +0 -0
  38. {wup-0.2.47 → wup-0.2.48}/wup/event_store.py +0 -0
  39. {wup-0.2.47 → wup-0.2.48}/wup/file_watcher/events/file_events.py +0 -0
  40. {wup-0.2.47 → wup-0.2.48}/wup/models/__init__.py +0 -0
  41. {wup-0.2.47 → wup-0.2.48}/wup/monitoring_manifest.py +0 -0
  42. {wup-0.2.47 → wup-0.2.48}/wup/planfile_reporter.py +0 -0
  43. {wup-0.2.47 → wup-0.2.48}/wup/testing/events/health_events.py +0 -0
  44. {wup-0.2.47 → wup-0.2.48}/wup/testing/events/test_results.py +0 -0
  45. {wup-0.2.47 → wup-0.2.48}/wup/testing/handlers/health_handlers.py +0 -0
  46. {wup-0.2.47 → wup-0.2.48}/wup/testing/queries/health_queries.py +0 -0
  47. {wup-0.2.47 → wup-0.2.48}/wup/testql_cli_generator.py +0 -0
  48. {wup-0.2.47 → wup-0.2.48}/wup/testql_discovery.py +0 -0
  49. {wup-0.2.47 → wup-0.2.48}/wup/testql_monitor.py +0 -0
  50. {wup-0.2.47 → wup-0.2.48}/wup/web_client.py +0 -0
  51. {wup-0.2.47 → wup-0.2.48}/wup.egg-info/dependency_links.txt +0 -0
  52. {wup-0.2.47 → wup-0.2.48}/wup.egg-info/entry_points.txt +0 -0
  53. {wup-0.2.47 → wup-0.2.48}/wup.egg-info/requires.txt +0 -0
  54. {wup-0.2.47 → wup-0.2.48}/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.47
3
+ Version: 0.2.48
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.47-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.47-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-23.0h-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.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)
36
36
 
37
- - 🤖 **LLM usage:** $3.4708 (57 commits)
38
- - 👤 **Human dev:** ~$2304 (23.0h @ $100/h, 30min dedup)
37
+ - 🤖 **LLM usage:** $3.3508 (60 commits)
38
+ - 👤 **Human dev:** ~$2573 (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.47-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.48-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
 
@@ -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.47-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.47-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-23.0h-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.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)
8
8
 
9
- - 🤖 **LLM usage:** $3.4708 (57 commits)
10
- - 👤 **Human dev:** ~$2304 (23.0h @ $100/h, 30min dedup)
9
+ - 🤖 **LLM usage:** $3.3508 (60 commits)
10
+ - 👤 **Human dev:** ~$2573 (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.47-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.48-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
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "wup"
7
- version = "0.2.47"
7
+ version = "0.2.48"
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,40 @@
1
+ """Regression: visual_diff is skipped during periodic probe cycles by default."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from unittest.mock import MagicMock
6
+
7
+ from wup.models.config import VisualDiffConfig
8
+ from wup.testql_watcher import TestQLWatcher
9
+
10
+
11
+ def _make_watcher(tmp_path, *, run_on_periodic_probe: bool) -> TestQLWatcher:
12
+ watcher = TestQLWatcher.__new__(TestQLWatcher)
13
+ differ = MagicMock()
14
+ differ.cfg = VisualDiffConfig(enabled=True, run_on_periodic_probe=run_on_periodic_probe)
15
+ watcher.visual_differ = differ
16
+ watcher._periodic_probe_in_progress = False
17
+ return watcher
18
+
19
+
20
+ def test_visual_diff_runs_on_file_change_cycles(tmp_path) -> None:
21
+ watcher = _make_watcher(tmp_path, run_on_periodic_probe=False)
22
+ assert watcher._should_run_visual_diff() is True
23
+
24
+
25
+ def test_visual_diff_skipped_on_periodic_probe_by_default(tmp_path) -> None:
26
+ watcher = _make_watcher(tmp_path, run_on_periodic_probe=False)
27
+ watcher._periodic_probe_in_progress = True
28
+ assert watcher._should_run_visual_diff() is False
29
+
30
+
31
+ def test_visual_diff_runs_on_periodic_probe_when_opted_in(tmp_path) -> None:
32
+ watcher = _make_watcher(tmp_path, run_on_periodic_probe=True)
33
+ watcher._periodic_probe_in_progress = True
34
+ assert watcher._should_run_visual_diff() is True
35
+
36
+
37
+ def test_visual_diff_skipped_when_disabled(tmp_path) -> None:
38
+ watcher = _make_watcher(tmp_path, run_on_periodic_probe=True)
39
+ watcher.visual_differ.cfg.enabled = False
40
+ assert watcher._should_run_visual_diff() is False
@@ -0,0 +1,39 @@
1
+ """Regression: visual_diff shows a progress bar for large scans."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+
7
+ from wup.models.config import VisualDiffConfig
8
+ from wup.visual_diff import VisualDiffer
9
+
10
+
11
+ def _make_differ(tmp_path) -> VisualDiffer:
12
+ cfg = VisualDiffConfig(
13
+ enabled=True,
14
+ base_url="http://localhost:8100",
15
+ snapshot_dir=str(tmp_path / "snap"),
16
+ diff_dir=str(tmp_path / "diff"),
17
+ pages_from_endpoints=True,
18
+ max_pages=200,
19
+ )
20
+ return VisualDiffer(str(tmp_path), cfg)
21
+
22
+
23
+ def test_progress_returned_for_big_scans(tmp_path, monkeypatch) -> None:
24
+ monkeypatch.delenv("WUP_VISUAL_DIFF_PROGRESS", raising=False)
25
+ differ = _make_differ(tmp_path)
26
+ progress = differ._build_progress("frontend", total=20)
27
+ assert progress is not None
28
+ assert progress.live.transient is True
29
+
30
+
31
+ def test_progress_skipped_for_small_scans(tmp_path) -> None:
32
+ differ = _make_differ(tmp_path)
33
+ assert differ._build_progress("frontend", total=2) is None
34
+
35
+
36
+ def test_progress_can_be_disabled_via_env(tmp_path, monkeypatch) -> None:
37
+ monkeypatch.setenv("WUP_VISUAL_DIFF_PROGRESS", "0")
38
+ differ = _make_differ(tmp_path)
39
+ assert differ._build_progress("frontend", total=50) is None
@@ -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.47"
10
+ __version__ = "0.2.48"
11
11
  __author__ = "Tom Sapletta"
12
12
 
13
13
  from .config import load_config, save_config, get_default_config
@@ -100,6 +100,9 @@ class VisualDiffConfig:
100
100
  "[class*='error'][class*='container']",
101
101
  ])
102
102
  headless: bool = True
103
+ # Run visual_diff during periodic probe cycles too. Default false: visual_diff
104
+ # only runs when something on disk actually changed (or on first cycle).
105
+ run_on_periodic_probe: bool = False
103
106
 
104
107
 
105
108
  @dataclass
@@ -35,7 +35,13 @@ class TestResultEventHandler:
35
35
  )
36
36
  )
37
37
 
38
- self.console.print(f"[red]✗ {event.stage.capitalize()} failed: {event.scenario.name} | track: {event.track_file}[/red]")
38
+ reason = (event.reason or "TestQL failed").strip().splitlines()[-1]
39
+ if len(reason) > 160:
40
+ reason = reason[:157] + "..."
41
+ self.console.print(
42
+ f"[red]✗ {event.stage.capitalize()} failed: {event.scenario.name} — {reason}[/red]\n"
43
+ f"[dim] track: {event.track_file}[/dim]"
44
+ )
39
45
 
40
46
  def handle_test_passed(self, event: ScenarioPassed) -> None:
41
47
  """Handle scenario pass."""
@@ -117,6 +117,7 @@ class TestQLWatcher(WupWatcher):
117
117
  )
118
118
 
119
119
  self._probe_thread = None
120
+ self._periodic_probe_in_progress = False
120
121
  self._normalize_fleet_health_entry()
121
122
 
122
123
  def _normalize_fleet_health_entry(self) -> None:
@@ -419,8 +420,10 @@ class TestQLWatcher(WupWatcher):
419
420
  ts = int(time.time())
420
421
  safe_service = service.replace("/", "_").replace("\\", "_")
421
422
  scenario_name = scenario.name if scenario else "unknown"
422
- stderr_line = (result.stderr or "").strip().splitlines()[:1]
423
- stdout_line = (result.stdout or "").strip().splitlines()[:1]
423
+ stderr_lines = (result.stderr or "").strip().splitlines()
424
+ stdout_lines = (result.stdout or "").strip().splitlines()
425
+ stderr_tail = stderr_lines[-3:] if stderr_lines else []
426
+ stdout_tail = stdout_lines[-5:] if stdout_lines else []
424
427
 
425
428
  payload = {
426
429
  "service": service,
@@ -428,8 +431,11 @@ class TestQLWatcher(WupWatcher):
428
431
  "scenario": str(scenario) if scenario else None,
429
432
  "command": result.args,
430
433
  "returncode": result.returncode,
431
- "stderr_head": stderr_line[0] if stderr_line else "",
432
- "stdout_head": stdout_line[0] if stdout_line else "",
434
+ "stderr_head": stderr_lines[0] if stderr_lines else "",
435
+ "stdout_head": stdout_lines[0] if stdout_lines else "",
436
+ "stderr_tail": stderr_tail,
437
+ "stdout_tail": stdout_tail,
438
+ "failure_summary": self._summarize_testql_failure(result),
433
439
  "track": {
434
440
  "file": str(scenario) if scenario else "",
435
441
  "line": 1,
@@ -478,7 +484,7 @@ class TestQLWatcher(WupWatcher):
478
484
  if self._is_interrupted_result(result):
479
485
  raise KeyboardInterrupt
480
486
 
481
- reason = result.stderr.strip() or result.stdout.strip() or "Quick TestQL failed"
487
+ reason = self._summarize_testql_failure(result)
482
488
  track_path = self._write_track(service=service, stage="quick",
483
489
  scenario=scenario, result=result)
484
490
 
@@ -497,6 +503,13 @@ class TestQLWatcher(WupWatcher):
497
503
 
498
504
  return False
499
505
 
506
+ def _should_run_visual_diff(self) -> bool:
507
+ if not (self.visual_differ and self.visual_differ.cfg.enabled):
508
+ return False
509
+ if not getattr(self, "_periodic_probe_in_progress", False):
510
+ return True
511
+ return bool(getattr(self.visual_differ.cfg, "run_on_periodic_probe", False))
512
+
500
513
  async def _quick_pass_actions(self, service: str, merged_endpoints: List[str]) -> None:
501
514
  """Actions to perform after all quick scenarios pass."""
502
515
  self._record_health_transition(service=service, status="up", stage="quick",
@@ -504,7 +517,7 @@ class TestQLWatcher(WupWatcher):
504
517
  if self.web_client.is_active:
505
518
  await self.web_client.send_pass(service=service, stage="quick")
506
519
  self.console.print(f"[green]✓ Quick TestQL passed for {service}[/green]")
507
- if self.visual_differ and self.visual_differ.cfg.enabled:
520
+ if self._should_run_visual_diff():
508
521
  visual_endpoints = self._get_config_endpoints_for_service(service) or merged_endpoints
509
522
  visual_results = await self.visual_differ.run_for_service(service, visual_endpoints)
510
523
  visual_issues = [
@@ -605,6 +618,16 @@ class TestQLWatcher(WupWatcher):
605
618
  return stripped
606
619
  return None
607
620
 
621
+ @staticmethod
622
+ def _summarize_testql_failure(result: subprocess.CompletedProcess) -> str:
623
+ """Short failure line for tracks and console (last lines of testql output)."""
624
+ if int(result.returncode) == 124:
625
+ return "TestQL subprocess timed out"
626
+ summary = TestQLWatcher._summarize_health_scenario_failure(result)
627
+ if summary != "health_scenario failed":
628
+ return summary
629
+ return "TestQL command failed"
630
+
608
631
  @staticmethod
609
632
  def _summarize_health_scenario_failure(result: subprocess.CompletedProcess) -> str:
610
633
  """Extract a short human summary from TestQL --output json (avoid trailing '}')."""
@@ -846,15 +869,19 @@ class TestQLWatcher(WupWatcher):
846
869
  if not self.config.services:
847
870
  return
848
871
  self.console.print("[cyan]⟳ Periodic live probe cycle[/cyan]")
872
+ self._periodic_probe_in_progress = True
849
873
  try:
850
- asyncio.run(self._run_fleet_health_scenario())
851
- except Exception as exc: # noqa: BLE001
852
- self.console.print(f"[red]Fleet health scenario error: {exc}[/red]")
853
- for svc in self.config.services:
854
874
  try:
855
- asyncio.run(self.run_quick_test(svc.name, []))
875
+ asyncio.run(self._run_fleet_health_scenario())
856
876
  except Exception as exc: # noqa: BLE001
857
- self.console.print(f"[red]Probe error for {svc.name}: {exc}[/red]")
877
+ self.console.print(f"[red]Fleet health scenario error: {exc}[/red]")
878
+ for svc in self.config.services:
879
+ try:
880
+ asyncio.run(self.run_quick_test(svc.name, []))
881
+ except Exception as exc: # noqa: BLE001
882
+ self.console.print(f"[red]Probe error for {svc.name}: {exc}[/red]")
883
+ finally:
884
+ self._periodic_probe_in_progress = False
858
885
 
859
886
  def _start_periodic_probe_thread(self) -> None:
860
887
  import threading
@@ -24,6 +24,14 @@ from typing import Any, Dict, List, Optional, Tuple
24
24
  from urllib.parse import urlparse
25
25
 
26
26
  from rich.console import Console
27
+ from rich.progress import (
28
+ BarColumn,
29
+ MofNCompleteColumn,
30
+ Progress,
31
+ TextColumn,
32
+ TimeElapsedColumn,
33
+ TimeRemainingColumn,
34
+ )
27
35
 
28
36
  from .models.config import VisualDiffConfig
29
37
 
@@ -442,14 +450,44 @@ class VisualDiffer:
442
450
  new_urls: List[str] = []
443
451
  error_results: List[Tuple[str, str]] = []
444
452
 
445
- for url in pages:
446
- result = await self._check_page(service, url)
447
- results.append(result)
448
- self._categorize_page_result(service, url, result, ok_urls, new_urls, error_results)
453
+ progress = self._build_progress(service, len(pages))
454
+ if progress is None:
455
+ for url in pages:
456
+ result = await self._check_page(service, url)
457
+ results.append(result)
458
+ self._categorize_page_result(service, url, result, ok_urls, new_urls, error_results)
459
+ else:
460
+ with progress:
461
+ task_id = progress.add_task(
462
+ f"[cyan]🔍 Visual diff {service}", total=len(pages), url=""
463
+ )
464
+ for url in pages:
465
+ progress.update(task_id, url=_short_url(url))
466
+ result = await self._check_page(service, url)
467
+ results.append(result)
468
+ self._categorize_page_result(service, url, result, ok_urls, new_urls, error_results)
469
+ progress.advance(task_id)
449
470
 
450
471
  self._print_scan_summary(service, ok_urls, new_urls, error_results)
451
472
  return results
452
473
 
474
+ def _build_progress(self, service: str, total: int) -> Optional[Progress]:
475
+ """Return a rich Progress for big scans; None for tiny ones (avoid noise)."""
476
+ if total < 5 or os.environ.get("WUP_VISUAL_DIFF_PROGRESS", "1") == "0":
477
+ return None
478
+ return Progress(
479
+ TextColumn("[bold blue]{task.description}"),
480
+ BarColumn(bar_width=None),
481
+ MofNCompleteColumn(),
482
+ TextColumn("[dim]{task.fields[url]}"),
483
+ TimeElapsedColumn(),
484
+ TextColumn("eta"),
485
+ TimeRemainingColumn(),
486
+ console=console,
487
+ transient=True,
488
+ refresh_per_second=4,
489
+ )
490
+
453
491
  async def _check_page(self, service: str, url: str) -> Dict[str, Any]:
454
492
  snap_path = _snapshot_path(self.snapshot_dir, service, url)
455
493
  old_snapshot = _load_snapshot(snap_path)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wup
3
- Version: 0.2.47
3
+ Version: 0.2.48
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.47-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.47-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-23.0h-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.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)
36
36
 
37
- - 🤖 **LLM usage:** $3.4708 (57 commits)
38
- - 👤 **Human dev:** ~$2304 (23.0h @ $100/h, 30min dedup)
37
+ - 🤖 **LLM usage:** $3.3508 (60 commits)
38
+ - 👤 **Human dev:** ~$2573 (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.47-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.48-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
 
@@ -8,6 +8,8 @@ tests/test_monitoring_manifest.py
8
8
  tests/test_service_inference.py
9
9
  tests/test_testql_monitor.py
10
10
  tests/test_testql_watcher.py
11
+ tests/test_visual_diff_periodic_skip.py
12
+ tests/test_visual_diff_progress.py
11
13
  tests/test_web_client.py
12
14
  tests/test_wup.py
13
15
  wup/__init__.py
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