wup 0.2.67__tar.gz → 0.2.68__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 (75) hide show
  1. {wup-0.2.67/wup.egg-info → wup-0.2.68}/PKG-INFO +7 -5
  2. {wup-0.2.67 → wup-0.2.68}/README.md +6 -4
  3. {wup-0.2.67 → wup-0.2.68}/pyproject.toml +1 -1
  4. {wup-0.2.67 → wup-0.2.68}/tests/test_testql_watcher.py +28 -0
  5. {wup-0.2.67 → wup-0.2.68}/tests/test_wup.py +81 -0
  6. {wup-0.2.67 → wup-0.2.68}/wup/__init__.py +1 -1
  7. {wup-0.2.67 → wup-0.2.68}/wup/config.py +6 -0
  8. {wup-0.2.67 → wup-0.2.68}/wup/models/config.py +3 -0
  9. {wup-0.2.67 → wup-0.2.68}/wup/planfile_reporter.py +20 -0
  10. {wup-0.2.67 → wup-0.2.68}/wup/testql_watcher.py +11 -5
  11. {wup-0.2.67 → wup-0.2.68}/wup/visual_diff.py +54 -1
  12. {wup-0.2.67 → wup-0.2.68/wup.egg-info}/PKG-INFO +7 -5
  13. {wup-0.2.67 → wup-0.2.68}/LICENSE +0 -0
  14. {wup-0.2.67 → wup-0.2.68}/setup.cfg +0 -0
  15. {wup-0.2.67 → wup-0.2.68}/tests/test_assistant.py +0 -0
  16. {wup-0.2.67 → wup-0.2.68}/tests/test_auto_detection.py +0 -0
  17. {wup-0.2.67 → wup-0.2.68}/tests/test_cli_bridge.py +0 -0
  18. {wup-0.2.67 → wup-0.2.68}/tests/test_cli_filtering.py +0 -0
  19. {wup-0.2.67 → wup-0.2.68}/tests/test_control.py +0 -0
  20. {wup-0.2.67 → wup-0.2.68}/tests/test_e2e.py +0 -0
  21. {wup-0.2.67 → wup-0.2.68}/tests/test_endpoints_init_cli.py +0 -0
  22. {wup-0.2.67 → wup-0.2.68}/tests/test_health_summary_passed.py +0 -0
  23. {wup-0.2.67 → wup-0.2.68}/tests/test_monitoring_manifest.py +0 -0
  24. {wup-0.2.67 → wup-0.2.68}/tests/test_probe_mutex.py +0 -0
  25. {wup-0.2.67 → wup-0.2.68}/tests/test_service_inference.py +0 -0
  26. {wup-0.2.67 → wup-0.2.68}/tests/test_status_data.py +0 -0
  27. {wup-0.2.67 → wup-0.2.68}/tests/test_sync.py +0 -0
  28. {wup-0.2.67 → wup-0.2.68}/tests/test_testql_monitor.py +0 -0
  29. {wup-0.2.67 → wup-0.2.68}/tests/test_visual_diff_periodic_skip.py +0 -0
  30. {wup-0.2.67 → wup-0.2.68}/tests/test_visual_diff_progress.py +0 -0
  31. {wup-0.2.67 → wup-0.2.68}/tests/test_watch_exclude.py +0 -0
  32. {wup-0.2.67 → wup-0.2.68}/tests/test_web_client.py +0 -0
  33. {wup-0.2.67 → wup-0.2.68}/tests/test_wup_generate.py +0 -0
  34. {wup-0.2.67 → wup-0.2.68}/wup/_ast_detector.py +0 -0
  35. {wup-0.2.67 → wup-0.2.68}/wup/_base_detector.py +0 -0
  36. {wup-0.2.67 → wup-0.2.68}/wup/_hash_detector.py +0 -0
  37. {wup-0.2.67 → wup-0.2.68}/wup/_yaml_detector.py +0 -0
  38. {wup-0.2.67 → wup-0.2.68}/wup/anomaly_detector.py +0 -0
  39. {wup-0.2.67 → wup-0.2.68}/wup/anomaly_models.py +0 -0
  40. {wup-0.2.67 → wup-0.2.68}/wup/assistant.py +0 -0
  41. {wup-0.2.67 → wup-0.2.68}/wup/assistant_discovery.py +0 -0
  42. {wup-0.2.67 → wup-0.2.68}/wup/assistant_validator.py +0 -0
  43. {wup-0.2.67 → wup-0.2.68}/wup/bus.py +0 -0
  44. {wup-0.2.67 → wup-0.2.68}/wup/cli.py +0 -0
  45. {wup-0.2.67 → wup-0.2.68}/wup/cli_bridge.py +0 -0
  46. {wup-0.2.67 → wup-0.2.68}/wup/cli_config_generator.py +0 -0
  47. {wup-0.2.67 → wup-0.2.68}/wup/cli_scanner.py +0 -0
  48. {wup-0.2.67 → wup-0.2.68}/wup/control.py +0 -0
  49. {wup-0.2.67 → wup-0.2.68}/wup/core.py +0 -0
  50. {wup-0.2.67 → wup-0.2.68}/wup/dependency_mapper.py +0 -0
  51. {wup-0.2.67 → wup-0.2.68}/wup/endpoints.py +0 -0
  52. {wup-0.2.67 → wup-0.2.68}/wup/event_store.py +0 -0
  53. {wup-0.2.67 → wup-0.2.68}/wup/file_watcher/events/file_events.py +0 -0
  54. {wup-0.2.67 → wup-0.2.68}/wup/generate.py +0 -0
  55. {wup-0.2.67 → wup-0.2.68}/wup/init_cli.py +0 -0
  56. {wup-0.2.67 → wup-0.2.68}/wup/models/__init__.py +0 -0
  57. {wup-0.2.67 → wup-0.2.68}/wup/monitoring_manifest.py +0 -0
  58. {wup-0.2.67 → wup-0.2.68}/wup/paths.py +0 -0
  59. {wup-0.2.67 → wup-0.2.68}/wup/status_data.py +0 -0
  60. {wup-0.2.67 → wup-0.2.68}/wup/sync.py +0 -0
  61. {wup-0.2.67 → wup-0.2.68}/wup/testing/events/health_events.py +0 -0
  62. {wup-0.2.67 → wup-0.2.68}/wup/testing/events/test_results.py +0 -0
  63. {wup-0.2.67 → wup-0.2.68}/wup/testing/handlers/event_handlers.py +0 -0
  64. {wup-0.2.67 → wup-0.2.68}/wup/testing/handlers/health_handlers.py +0 -0
  65. {wup-0.2.67 → wup-0.2.68}/wup/testing/queries/health_queries.py +0 -0
  66. {wup-0.2.67 → wup-0.2.68}/wup/testql_cli_generator.py +0 -0
  67. {wup-0.2.67 → wup-0.2.68}/wup/testql_discovery.py +0 -0
  68. {wup-0.2.67 → wup-0.2.68}/wup/testql_monitor.py +0 -0
  69. {wup-0.2.67 → wup-0.2.68}/wup/validate.py +0 -0
  70. {wup-0.2.67 → wup-0.2.68}/wup/web_client.py +0 -0
  71. {wup-0.2.67 → wup-0.2.68}/wup.egg-info/SOURCES.txt +0 -0
  72. {wup-0.2.67 → wup-0.2.68}/wup.egg-info/dependency_links.txt +0 -0
  73. {wup-0.2.67 → wup-0.2.68}/wup.egg-info/entry_points.txt +0 -0
  74. {wup-0.2.67 → wup-0.2.68}/wup.egg-info/requires.txt +0 -0
  75. {wup-0.2.67 → wup-0.2.68}/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.67
3
+ Version: 0.2.68
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
@@ -42,15 +42,17 @@ Dynamic: license-file
42
42
 
43
43
  ## AI Cost Tracking
44
44
 
45
- ![AI Cost](https://img.shields.io/badge/AI%20Cost-$7.50-yellow) ![AI Model](https://img.shields.io/badge/AI%20Model-openrouter%2Fqwen%2Fqwen3-coder-next-lightgrey)
45
+ ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.68-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
46
+ ![AI Cost](https://img.shields.io/badge/AI%20Cost-$3.88-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-39.7h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
46
47
 
47
- This project uses AI-generated code. Total cost: **$7.5000** with **80** AI commits.
48
+ - 🤖 **LLM usage:** $3.8816 (82 commits)
49
+ - 👤 **Human dev:** ~$3968 (39.7h @ $100/h, 30min dedup)
48
50
 
49
- Generated on 2026-06-29 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/models/openrouter/qwen/qwen3-coder-next)
51
+ Generated on 2026-07-03 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
50
52
 
51
53
  ---
52
54
 
53
- ![PyPI](https://img.shields.io/badge/pypi-wup-blue) ![Version](https://img.shields.io/badge/version-0.2.67-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
55
+ ![PyPI](https://img.shields.io/badge/pypi-wup-blue) ![Version](https://img.shields.io/badge/version-0.2.68-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
54
56
 
55
57
  **WUP (What's Up)** - Intelligent file watcher for regression testing in large projects.
56
58
 
@@ -3,15 +3,17 @@
3
3
 
4
4
  ## AI Cost Tracking
5
5
 
6
- ![AI Cost](https://img.shields.io/badge/AI%20Cost-$7.50-yellow) ![AI Model](https://img.shields.io/badge/AI%20Model-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.68-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.88-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-39.7h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
7
8
 
8
- This project uses AI-generated code. Total cost: **$7.5000** with **80** AI commits.
9
+ - 🤖 **LLM usage:** $3.8816 (82 commits)
10
+ - 👤 **Human dev:** ~$3968 (39.7h @ $100/h, 30min dedup)
9
11
 
10
- Generated on 2026-06-29 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/models/openrouter/qwen/qwen3-coder-next)
12
+ Generated on 2026-07-03 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
11
13
 
12
14
  ---
13
15
 
14
- ![PyPI](https://img.shields.io/badge/pypi-wup-blue) ![Version](https://img.shields.io/badge/version-0.2.67-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.68-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
15
17
 
16
18
  **WUP (What's Up)** - Intelligent file watcher for regression testing in large projects.
17
19
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "wup"
7
- version = "0.2.67"
7
+ version = "0.2.68"
8
8
  description = "WUP (What's Up) - Intelligent file watcher for regression testing in large projects"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -283,6 +283,34 @@ def test_planfile_reporter_clears_dedupe_after_recovery(monkeypatch):
283
283
 
284
284
  assert first == "PLF-1000"
285
285
  assert second == "PLF-1001"
286
+
287
+
288
+ def test_planfile_reporter_retries_without_files_for_old_planfile_cli(monkeypatch):
289
+ with tempfile.TemporaryDirectory() as tmpdir:
290
+ root = Path(tmpdir)
291
+ calls = []
292
+
293
+ def fake_run(cmd, **kwargs):
294
+ calls.append(cmd)
295
+ if "--files" in cmd:
296
+ return CompletedProcess(cmd, returncode=2, stdout="", stderr="Error: No such option: --files")
297
+ return CompletedProcess(cmd, returncode=0, stdout="Created PLF-998\n", stderr="")
298
+
299
+ monkeypatch.setattr("wup.planfile_reporter.subprocess.run", fake_run)
300
+ reporter = PlanfileReporter(root, PlanfileConfig(enabled=True))
301
+
302
+ ticket_id = reporter.report_failure(
303
+ service="firmware",
304
+ status="failed",
305
+ stage="detail",
306
+ message="detail failed",
307
+ track_file=".wup/tracks/firmware_detail.json",
308
+ )
309
+
310
+ assert ticket_id == "PLF-998"
311
+ assert len(calls) == 2
312
+ assert "--files" in calls[0]
313
+ assert "--files" not in calls[1]
286
314
  assert len(calls) == 2
287
315
 
288
316
 
@@ -26,6 +26,7 @@ from wup.models.config import (
26
26
  from wup.testql_watcher import TestQLWatcher
27
27
  from wup.visual_diff import (
28
28
  VisualDiffer,
29
+ _chromium_launch_options,
29
30
  _diff_snapshots,
30
31
  _looks_like_visual_page,
31
32
  _page_slug,
@@ -994,6 +995,9 @@ class TestConfigModels:
994
995
  assert cfg.pages == []
995
996
  assert cfg.pages_from_endpoints is True
996
997
  assert cfg.headless is True
998
+ assert cfg.page_settle_ms == 750
999
+ assert cfg.issue_retry_count == 0
1000
+ assert cfg.issue_retry_delay_seconds == 2.0
997
1001
 
998
1002
  def test_visual_diff_config_custom(self):
999
1003
  """Test VisualDiffConfig with custom values."""
@@ -1002,11 +1006,17 @@ class TestConfigModels:
1002
1006
  base_url="http://localhost:9000",
1003
1007
  pages=["/dashboard", "/users"],
1004
1008
  threshold_added=10,
1009
+ page_settle_ms=1500,
1010
+ issue_retry_count=2,
1011
+ issue_retry_delay_seconds=3.0,
1005
1012
  )
1006
1013
  assert cfg.enabled is True
1007
1014
  assert cfg.base_url == "http://localhost:9000"
1008
1015
  assert cfg.pages == ["/dashboard", "/users"]
1009
1016
  assert cfg.threshold_added == 10
1017
+ assert cfg.page_settle_ms == 1500
1018
+ assert cfg.issue_retry_count == 2
1019
+ assert cfg.issue_retry_delay_seconds == 3.0
1010
1020
 
1011
1021
 
1012
1022
  class TestVisualDiffer:
@@ -1026,6 +1036,14 @@ class TestVisualDiffer:
1026
1036
  monkeypatch.delenv("WUP_VD_NONE", raising=False)
1027
1037
  assert _resolve_base_url(cfg) == ""
1028
1038
 
1039
+ def test_chromium_launch_options_uses_env_executable(self, tmp_path, monkeypatch):
1040
+ chrome = tmp_path / "chrome"
1041
+ chrome.write_text("#!/bin/sh\n", encoding="utf-8")
1042
+ monkeypatch.setenv("WUP_CHROMIUM_EXECUTABLE_PATH", str(chrome))
1043
+ options = _chromium_launch_options(True)
1044
+ assert options["headless"] is True
1045
+ assert options["executable_path"] == str(chrome)
1046
+
1029
1047
  def test_page_slug(self):
1030
1048
  assert _page_slug("http://x/api/v1/users") == "api_v1_users"
1031
1049
  assert _page_slug("http://x/") == "root"
@@ -1168,6 +1186,57 @@ class TestVisualDiffer:
1168
1186
  assert "/a, /b, /c (+1 more)" in printed[0]
1169
1187
  assert "4x BrowserType.launch: Executable doesn't exist at /tmp/chrome" in printed[0]
1170
1188
 
1189
+ def test_check_page_retries_transient_visual_issue(self, monkeypatch):
1190
+ """A transient Vite overlay/tiny shell should be retried before reporting an issue."""
1191
+ import asyncio
1192
+ from wup import visual_diff as visual_diff_module
1193
+
1194
+ bad_snapshot = {
1195
+ "tag": "HTML",
1196
+ "meta": {
1197
+ "text_length": 33,
1198
+ "dom_nodes": 8,
1199
+ "matched_error_selectors": ["vite-error-overlay"],
1200
+ },
1201
+ }
1202
+ good_snapshot = {
1203
+ "tag": "HTML",
1204
+ "children": [{"tag": "BODY", "children": [{"tag": "MAIN"}]}],
1205
+ "meta": {
1206
+ "text_length": 500,
1207
+ "dom_nodes": 50,
1208
+ "matched_error_selectors": [],
1209
+ },
1210
+ }
1211
+ snapshots = [bad_snapshot, good_snapshot]
1212
+ calls = []
1213
+
1214
+ async def fake_fetch(*args):
1215
+ calls.append(args)
1216
+ return snapshots.pop(0), None
1217
+
1218
+ async def fake_sleep(_seconds):
1219
+ return None
1220
+
1221
+ with tempfile.TemporaryDirectory() as tmpdir:
1222
+ cfg = VisualDiffConfig(
1223
+ enabled=True,
1224
+ issue_retry_count=1,
1225
+ issue_retry_delay_seconds=0,
1226
+ page_settle_ms=1234,
1227
+ )
1228
+ differ = VisualDiffer(tmpdir, cfg)
1229
+
1230
+ monkeypatch.setattr(visual_diff_module, "_fetch_dom_snapshot", fake_fetch)
1231
+ monkeypatch.setattr(visual_diff_module.asyncio, "sleep", fake_sleep)
1232
+
1233
+ result = asyncio.run(differ._check_page("frontend", "http://localhost/page"))
1234
+
1235
+ assert len(calls) == 2
1236
+ assert calls[0][-1] == 1234
1237
+ assert result["diff"]["status"] in {"new", "ok", "changed"}
1238
+ assert "issues" not in result["diff"]
1239
+
1171
1240
  def test_get_recent_diffs_empty(self):
1172
1241
  with tempfile.TemporaryDirectory() as tmpdir:
1173
1242
  cfg = VisualDiffConfig()
@@ -1376,6 +1445,9 @@ testql:
1376
1445
  threshold_added=7,
1377
1446
  threshold_removed=4,
1378
1447
  threshold_changed=8,
1448
+ page_settle_ms=1600,
1449
+ issue_retry_count=3,
1450
+ issue_retry_delay_seconds=4.5,
1379
1451
  headless=False,
1380
1452
  ),
1381
1453
  )
@@ -1392,6 +1464,9 @@ testql:
1392
1464
  assert vd.threshold_added == 7
1393
1465
  assert vd.threshold_removed == 4
1394
1466
  assert vd.threshold_changed == 8
1467
+ assert vd.page_settle_ms == 1600
1468
+ assert vd.issue_retry_count == 3
1469
+ assert vd.issue_retry_delay_seconds == 4.5
1395
1470
  assert vd.headless is False
1396
1471
 
1397
1472
  def test_load_config_visual_diff_from_yaml(self):
@@ -1415,6 +1490,9 @@ visual_diff:
1415
1490
  threshold_added: 5
1416
1491
  threshold_removed: 2
1417
1492
  threshold_changed: 9
1493
+ page_settle_ms: 1500
1494
+ issue_retry_count: 2
1495
+ issue_retry_delay_seconds: 3
1418
1496
  headless: false
1419
1497
  """
1420
1498
  config_path = Path(tmpdir) / "wup.yaml"
@@ -1433,6 +1511,9 @@ visual_diff:
1433
1511
  assert vd.threshold_added == 5
1434
1512
  assert vd.threshold_removed == 2
1435
1513
  assert vd.threshold_changed == 9
1514
+ assert vd.page_settle_ms == 1500
1515
+ assert vd.issue_retry_count == 2
1516
+ assert vd.issue_retry_delay_seconds == 3.0
1436
1517
  assert vd.headless is False
1437
1518
 
1438
1519
  def test_load_config_visual_diff_defaults_when_section_absent(self):
@@ -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.67"
10
+ __version__ = "0.2.68"
11
11
  __author__ = "Tom Sapletta"
12
12
 
13
13
  from .config import load_config, save_config, get_default_config
@@ -292,6 +292,9 @@ def _parse_visual_diff_config(raw: dict) -> VisualDiffConfig:
292
292
  threshold_changed=int(vd_raw.get("threshold_changed", 5)),
293
293
  min_text_length=int(vd_raw.get("min_text_length", 200)),
294
294
  min_dom_nodes=int(vd_raw.get("min_dom_nodes", 20)),
295
+ page_settle_ms=int(vd_raw.get("page_settle_ms", 750)),
296
+ issue_retry_count=int(vd_raw.get("issue_retry_count", 0)),
297
+ issue_retry_delay_seconds=float(vd_raw.get("issue_retry_delay_seconds", 2.0)),
295
298
  error_selectors=vd_raw.get("error_selectors", [
296
299
  "#error-container",
297
300
  ".error-container",
@@ -494,6 +497,9 @@ def save_config(config: WupConfig, output_path: Path):
494
497
  "threshold_changed": config.visual_diff.threshold_changed,
495
498
  "min_text_length": config.visual_diff.min_text_length,
496
499
  "min_dom_nodes": config.visual_diff.min_dom_nodes,
500
+ "page_settle_ms": config.visual_diff.page_settle_ms,
501
+ "issue_retry_count": config.visual_diff.issue_retry_count,
502
+ "issue_retry_delay_seconds": config.visual_diff.issue_retry_delay_seconds,
497
503
  "error_selectors": config.visual_diff.error_selectors,
498
504
  "headless": config.visual_diff.headless,
499
505
  },
@@ -97,6 +97,9 @@ class VisualDiffConfig:
97
97
  threshold_changed: int = 5 # min changed attrs to report
98
98
  min_text_length: int = 200 # anomaly if rendered text is too short
99
99
  min_dom_nodes: int = 20 # anomaly if DOM is suspiciously tiny
100
+ page_settle_ms: int = 750 # wait after networkidle before snapshotting SPA DOM
101
+ issue_retry_count: int = 0 # retry transient Vite/HMR/page-shell issues
102
+ issue_retry_delay_seconds: float = 2.0
100
103
  error_selectors: List[str] = field(default_factory=lambda: [
101
104
  "#error-container",
102
105
  ".error-container",
@@ -123,6 +123,21 @@ class PlanfileReporter:
123
123
 
124
124
  stdout = (result.stdout or "").strip()
125
125
  stderr = (result.stderr or "").strip()
126
+ if result.returncode != 0 and track_file and self._files_option_unsupported(stderr or stdout):
127
+ cmd = [part for index, part in enumerate(cmd) if not (part == "--files" or (index > 0 and cmd[index - 1] == "--files"))]
128
+ try:
129
+ result = subprocess.run(
130
+ cmd,
131
+ cwd=str(self.project_root),
132
+ capture_output=True,
133
+ text=True,
134
+ timeout=30,
135
+ )
136
+ except (OSError, subprocess.TimeoutExpired) as exc:
137
+ self.console.print(f"[yellow]planfile ticket creation skipped: {exc}[/yellow]")
138
+ return None
139
+ stdout = (result.stdout or "").strip()
140
+ stderr = (result.stderr or "").strip()
126
141
  if result.returncode != 0:
127
142
  detail = stderr or stdout or f"rc={result.returncode}"
128
143
  self.console.print(f"[yellow]planfile ticket creation failed: {detail}[/yellow]")
@@ -187,6 +202,11 @@ class PlanfileReporter:
187
202
  match = re.search(r"\bPLF-\d+\b", text)
188
203
  return match.group(0) if match else None
189
204
 
205
+ @staticmethod
206
+ def _files_option_unsupported(text: str) -> bool:
207
+ lowered = text.lower()
208
+ return "--files" in lowered and ("no such option" in lowered or "unknown option" in lowered)
209
+
190
210
  @staticmethod
191
211
  def _ticket_name(*, service: str, stage: str, status: str) -> str:
192
212
  return f"[AUTO-DIAG] wup-{service} {stage} {status}"
@@ -361,16 +361,22 @@ class TestQLWatcher(WupWatcher):
361
361
  passed, total, failed = (int(match.group(i)) for i in range(1, 4))
362
362
  return failed == 0 and passed >= total and total > 0
363
363
 
364
- def _select_scenarios_for_service(self, service: str) -> List[Path]:
364
+ def _select_scenarios_for_service(self, service: str, *, stage: str = "quick") -> List[Path]:
365
365
  all_scenarios = self._discover_scenarios()
366
366
  if not all_scenarios:
367
367
  return []
368
368
 
369
369
  svc_config = self.get_service_config(service)
370
- limit = (svc_config.quick_tests.max_endpoints
371
- if svc_config and svc_config.quick_tests else self.quick_limit)
370
+ if stage == "detail":
371
+ test_cfg = svc_config.detail_tests if svc_config else None
372
+ default_limit = 10
373
+ else:
374
+ test_cfg = svc_config.quick_tests if svc_config else None
375
+ default_limit = self.quick_limit
372
376
 
373
- pinned = svc_config.quick_tests.scenario if svc_config and svc_config.quick_tests else ""
377
+ limit = test_cfg.max_endpoints if test_cfg else default_limit
378
+
379
+ pinned = test_cfg.scenario if test_cfg else ""
374
380
  if pinned:
375
381
  resolved = self._resolve_scenario_path(pinned)
376
382
  if resolved:
@@ -870,7 +876,7 @@ class TestQLWatcher(WupWatcher):
870
876
  if configured_endpoint not in merged_endpoints:
871
877
  merged_endpoints.append(configured_endpoint)
872
878
 
873
- scenarios = self._select_scenarios_for_service(service)
879
+ scenarios = self._select_scenarios_for_service(service, stage="detail")
874
880
  results = {
875
881
  "service": service,
876
882
  "total_scenarios": len(scenarios),
@@ -17,6 +17,7 @@ from __future__ import annotations
17
17
  import asyncio
18
18
  import json
19
19
  import os
20
+ import shutil
20
21
  import time
21
22
  from collections import Counter
22
23
  from pathlib import Path
@@ -97,11 +98,30 @@ _DOM_SNAPSHOT_JS = """
97
98
  """
98
99
 
99
100
 
101
+ def _chromium_launch_options(headless: bool) -> Dict[str, Any]:
102
+ options: Dict[str, Any] = {"headless": headless}
103
+ executable = os.environ.get("WUP_CHROMIUM_EXECUTABLE_PATH") or os.environ.get("CHROME_BIN")
104
+ if not executable:
105
+ for candidate in (
106
+ "google-chrome-stable",
107
+ "google-chrome",
108
+ "chromium",
109
+ "chromium-browser",
110
+ ):
111
+ executable = shutil.which(candidate)
112
+ if executable:
113
+ break
114
+ if executable and Path(executable).exists():
115
+ options["executable_path"] = executable
116
+ return options
117
+
118
+
100
119
  async def _fetch_dom_snapshot(
101
120
  url: str,
102
121
  max_depth: int,
103
122
  headless: bool,
104
123
  error_selectors: List[str],
124
+ page_settle_ms: int = 750,
105
125
  ) -> Tuple[Optional[Dict], Optional[str]]:
106
126
  """Return a DOM structure dict for *url* using Playwright."""
107
127
  if not _playwright_available():
@@ -110,10 +130,12 @@ async def _fetch_dom_snapshot(
110
130
  try:
111
131
  from playwright.async_api import async_playwright
112
132
  async with async_playwright() as pw:
113
- browser = await pw.chromium.launch(headless=headless)
133
+ browser = await pw.chromium.launch(**_chromium_launch_options(headless))
114
134
  page = await browser.new_page()
115
135
  try:
116
136
  await page.goto(url, wait_until="networkidle", timeout=15_000)
137
+ if page_settle_ms > 0:
138
+ await page.wait_for_timeout(page_settle_ms)
117
139
  snapshot = await page.evaluate(_DOM_SNAPSHOT_JS, max_depth)
118
140
  text_length = await page.evaluate("() => (document.body?.innerText || '').trim().length")
119
141
  dom_nodes = await page.evaluate("() => document.querySelectorAll('*').length")
@@ -522,6 +544,7 @@ class VisualDiffer:
522
544
  self.cfg.max_depth,
523
545
  self.cfg.headless,
524
546
  self.cfg.error_selectors,
547
+ self.cfg.page_settle_ms,
525
548
  )
526
549
 
527
550
  if new_snapshot is None:
@@ -543,6 +566,36 @@ class VisualDiffer:
543
566
  )
544
567
 
545
568
  issues = _detect_content_issues(new_snapshot, self.cfg)
569
+ retries_left = max(0, int(getattr(self.cfg, "issue_retry_count", 0) or 0))
570
+ while issues and retries_left > 0:
571
+ await asyncio.sleep(max(0.0, float(getattr(self.cfg, "issue_retry_delay_seconds", 2.0) or 0.0)))
572
+ retry_snapshot, retry_error = await _fetch_dom_snapshot(
573
+ url,
574
+ self.cfg.max_depth,
575
+ self.cfg.headless,
576
+ self.cfg.error_selectors,
577
+ self.cfg.page_settle_ms,
578
+ )
579
+ if retry_snapshot is None:
580
+ fetch_error = retry_error
581
+ retries_left -= 1
582
+ continue
583
+ retry_issues = _detect_content_issues(retry_snapshot, self.cfg)
584
+ if not retry_issues:
585
+ new_snapshot = retry_snapshot
586
+ issues = []
587
+ diff = _diff_snapshots(
588
+ old_snapshot,
589
+ new_snapshot,
590
+ self.cfg.max_depth,
591
+ self.cfg.threshold_added,
592
+ self.cfg.threshold_removed,
593
+ self.cfg.threshold_changed,
594
+ )
595
+ break
596
+ new_snapshot = retry_snapshot
597
+ issues = retry_issues
598
+ retries_left -= 1
546
599
  if issues:
547
600
  diff["status"] = "issue"
548
601
  diff["issues"] = issues
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wup
3
- Version: 0.2.67
3
+ Version: 0.2.68
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
@@ -42,15 +42,17 @@ Dynamic: license-file
42
42
 
43
43
  ## AI Cost Tracking
44
44
 
45
- ![AI Cost](https://img.shields.io/badge/AI%20Cost-$7.50-yellow) ![AI Model](https://img.shields.io/badge/AI%20Model-openrouter%2Fqwen%2Fqwen3-coder-next-lightgrey)
45
+ ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.68-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
46
+ ![AI Cost](https://img.shields.io/badge/AI%20Cost-$3.88-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-39.7h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
46
47
 
47
- This project uses AI-generated code. Total cost: **$7.5000** with **80** AI commits.
48
+ - 🤖 **LLM usage:** $3.8816 (82 commits)
49
+ - 👤 **Human dev:** ~$3968 (39.7h @ $100/h, 30min dedup)
48
50
 
49
- Generated on 2026-06-29 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/models/openrouter/qwen/qwen3-coder-next)
51
+ Generated on 2026-07-03 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
50
52
 
51
53
  ---
52
54
 
53
- ![PyPI](https://img.shields.io/badge/pypi-wup-blue) ![Version](https://img.shields.io/badge/version-0.2.67-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
55
+ ![PyPI](https://img.shields.io/badge/pypi-wup-blue) ![Version](https://img.shields.io/badge/version-0.2.68-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
54
56
 
55
57
  **WUP (What's Up)** - Intelligent file watcher for regression testing in large projects.
56
58
 
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
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