wup 0.2.8__tar.gz → 0.2.11__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.
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wup
3
- Version: 0.2.8
3
+ Version: 0.2.11
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
- License-Expression: Apache-2.0
6
+ License: Apache-2.0
7
7
  Project-URL: Homepage, https://github.com/semcod/wup
8
8
  Project-URL: Repository, https://github.com/semcod/wup
9
9
  Keywords: wup,watcher,testing,regression,file-monitoring
@@ -21,6 +21,7 @@ Requires-Dist: watchdog>=4.0.0
21
21
  Requires-Dist: psutil>=5.9.0
22
22
  Requires-Dist: rich>=13.0.0
23
23
  Requires-Dist: typer>=0.9.0
24
+ Requires-Dist: pyyaml>=6.0
24
25
  Dynamic: license-file
25
26
 
26
27
  # WUP (What's Up)
@@ -28,17 +29,17 @@ Dynamic: license-file
28
29
 
29
30
  ## AI Cost Tracking
30
31
 
31
- ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.8-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
32
- ![AI Cost](https://img.shields.io/badge/AI%20Cost-$1.35-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-2.3h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
32
+ ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.11-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
33
+ ![AI Cost](https://img.shields.io/badge/AI%20Cost-$1.80-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-2.9h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
33
34
 
34
- - 🤖 **LLM usage:** $1.3500 (9 commits)
35
- - 👤 **Human dev:** ~$228 (2.3h @ $100/h, 30min dedup)
35
+ - 🤖 **LLM usage:** $1.8000 (12 commits)
36
+ - 👤 **Human dev:** ~$290 (2.9h @ $100/h, 30min dedup)
36
37
 
37
38
  Generated on 2026-04-29 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
38
39
 
39
40
  ---
40
41
 
41
- ![PyPI](https://img.shields.io/badge/pypi-wup-blue) ![Version](https://img.shields.io/badge/version-0.2.8-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
42
+ ![PyPI](https://img.shields.io/badge/pypi-wup-blue) ![Version](https://img.shields.io/badge/version-0.2.11-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
42
43
 
43
44
  **WUP (What's Up)** - Intelligent file watcher for regression testing in large projects.
44
45
 
@@ -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.8-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-$1.35-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-2.3h-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.11-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-$1.80-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-2.9h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
8
8
 
9
- - 🤖 **LLM usage:** $1.3500 (9 commits)
10
- - 👤 **Human dev:** ~$228 (2.3h @ $100/h, 30min dedup)
9
+ - 🤖 **LLM usage:** $1.8000 (12 commits)
10
+ - 👤 **Human dev:** ~$290 (2.9h @ $100/h, 30min dedup)
11
11
 
12
12
  Generated on 2026-04-29 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.8-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.11-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,11 +4,10 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "wup"
7
- version = "0.2.8"
7
+ version = "0.2.11"
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"
11
- license = "Apache-2.0"
12
11
  authors = [
13
12
  {name = "Tom Sapletta", email = "tom@sapletta.com"},
14
13
  ]
@@ -18,6 +17,7 @@ dependencies = [
18
17
  "psutil>=5.9.0",
19
18
  "rich>=13.0.0",
20
19
  "typer>=0.9.0",
20
+ "pyyaml>=6.0",
21
21
  ]
22
22
  classifiers = [
23
23
  "Development Status :: 3 - Alpha",
@@ -29,6 +29,11 @@ classifiers = [
29
29
  "Programming Language :: Python :: 3.12",
30
30
  ]
31
31
 
32
+ [project.license]
33
+ text = "Apache-2.0"
34
+ [tool.setuptools.packages.find]
35
+ include = ["wup*"]
36
+
32
37
  [project.scripts]
33
38
  wup = "wup.cli:app"
34
39
 
@@ -1,6 +1,7 @@
1
1
  """End-to-end tests for WUP CLI and workflows."""
2
2
 
3
3
  import json
4
+ import os
4
5
  import subprocess
5
6
  import sys
6
7
  import tempfile
@@ -11,13 +12,29 @@ from typing import List
11
12
  import pytest
12
13
 
13
14
 
15
+ def run_wup_command(args, cwd=None, timeout=30, capture_output=True, text=True):
16
+ """Helper to run WUP commands with PYTHONPATH set."""
17
+ env = os.environ.copy()
18
+ # Add project root to PYTHONPATH so subprocess can find wup module
19
+ project_root = Path(__file__).parent.parent
20
+ env["PYTHONPATH"] = str(project_root) + ":" + env.get("PYTHONPATH", "")
21
+ return subprocess.run(
22
+ args,
23
+ cwd=cwd,
24
+ capture_output=capture_output,
25
+ text=text,
26
+ timeout=timeout,
27
+ env=env
28
+ )
29
+
30
+
14
31
  class TestE2ECLI:
15
32
  """End-to-end tests for CLI commands."""
16
33
 
17
34
  def test_cli_init_creates_config_file(self):
18
35
  """Test that wup init creates a wup.yaml configuration file."""
19
36
  with tempfile.TemporaryDirectory() as tmpdir:
20
- result = subprocess.run(
37
+ result = run_wup_command(
21
38
  [sys.executable, "-m", "wup.cli", "init", "--output", str(Path(tmpdir) / "wup.yaml")],
22
39
  cwd=tmpdir,
23
40
  capture_output=True,
@@ -37,7 +54,7 @@ class TestE2ECLI:
37
54
  def test_cli_init_default_location(self):
38
55
  """Test that wup init creates wup.yaml in current directory by default."""
39
56
  with tempfile.TemporaryDirectory() as tmpdir:
40
- result = subprocess.run(
57
+ result = run_wup_command(
41
58
  [sys.executable, "-m", "wup.cli", "init"],
42
59
  cwd=tmpdir,
43
60
  capture_output=True,
@@ -67,7 +84,7 @@ def get_users():
67
84
  return []
68
85
  """)
69
86
 
70
- result = subprocess.run(
87
+ result = run_wup_command(
71
88
  [sys.executable, "-m", "wup.cli", "map-deps", tmpdir, "--framework", "fastapi"],
72
89
  cwd=tmpdir,
73
90
  capture_output=True,
@@ -98,7 +115,7 @@ def get_users():
98
115
  "files": {"app/users/routes.py": ["/users"]}
99
116
  }))
100
117
 
101
- result = subprocess.run(
118
+ result = run_wup_command(
102
119
  [sys.executable, "-m", "wup.cli", "status", "--deps", str(deps_file)],
103
120
  cwd=tmpdir,
104
121
  capture_output=True,
@@ -118,25 +135,23 @@ class TestE2EWorkflow:
118
135
  """Test complete workflow from config to file watching."""
119
136
  with tempfile.TemporaryDirectory() as tmpdir:
120
137
  # Initialize config
121
- subprocess.run(
138
+ run_wup_command(
122
139
  [sys.executable, "-m", "wup.cli", "init"],
123
140
  cwd=tmpdir,
124
- capture_output=True,
125
141
  timeout=10
126
142
  )
127
-
143
+
128
144
  # Create project structure
129
145
  app_dir = Path(tmpdir) / "app" / "users"
130
146
  app_dir.mkdir(parents=True)
131
-
147
+
132
148
  routes_file = app_dir / "routes.py"
133
149
  routes_file.write_text("def handler(): pass\n")
134
-
150
+
135
151
  # Build dependencies
136
- subprocess.run(
152
+ run_wup_command(
137
153
  [sys.executable, "-m", "wup.cli", "map-deps", tmpdir, "--framework", "fastapi"],
138
154
  cwd=tmpdir,
139
- capture_output=True,
140
155
  timeout=30
141
156
  )
142
157
 
@@ -186,19 +201,10 @@ test_strategy:
186
201
  routes_file.write_text("def handler(): pass\n")
187
202
 
188
203
  # Build dependencies
189
- import os
190
- env = os.environ.copy()
191
- # Add project root to PYTHONPATH so subprocess can find wup module
192
- project_root = Path(__file__).parent.parent
193
- env["PYTHONPATH"] = str(project_root) + ":" + env.get("PYTHONPATH", "")
194
-
195
- result = subprocess.run(
204
+ result = run_wup_command(
196
205
  [sys.executable, "-m", "wup.cli", "map-deps", tmpdir],
197
206
  cwd=tmpdir,
198
- capture_output=True,
199
- text=True,
200
- timeout=30,
201
- env=env
207
+ timeout=30
202
208
  )
203
209
 
204
210
  assert result.returncode == 0
@@ -241,7 +247,7 @@ services:
241
247
  md_file.write_text("# API\n")
242
248
 
243
249
  # Build dependencies
244
- result = subprocess.run(
250
+ result = run_wup_command(
245
251
  [sys.executable, "-m", "wup.cli", "map-deps", tmpdir],
246
252
  cwd=tmpdir,
247
253
  capture_output=True,
@@ -312,7 +318,7 @@ def login():
312
318
  """)
313
319
 
314
320
  # Build dependencies for FastAPI
315
- result = subprocess.run(
321
+ result = run_wup_command(
316
322
  [sys.executable, "-m", "wup.cli", "map-deps", tmpdir, "--framework", "fastapi"],
317
323
  cwd=tmpdir,
318
324
  capture_output=True,
@@ -338,7 +344,7 @@ class TestE2EErrorHandling:
338
344
  config_file = Path(tmpdir) / "wup.yaml"
339
345
  config_file.write_text("invalid: yaml: content: [")
340
346
 
341
- result = subprocess.run(
347
+ result = run_wup_command(
342
348
  [sys.executable, "-m", "wup.cli", "status"],
343
349
  cwd=tmpdir,
344
350
  capture_output=True,
@@ -351,7 +357,7 @@ class TestE2EErrorHandling:
351
357
 
352
358
  def test_cli_handles_missing_project(self):
353
359
  """Test that CLI handles missing project directory."""
354
- result = subprocess.run(
360
+ result = run_wup_command(
355
361
  [sys.executable, "-m", "wup.cli", "map-deps", "/nonexistent/path"],
356
362
  capture_output=True,
357
363
  text=True,
@@ -364,7 +370,7 @@ class TestE2EErrorHandling:
364
370
  def test_cli_handles_empty_project(self):
365
371
  """Test that CLI handles empty project directory."""
366
372
  with tempfile.TemporaryDirectory() as tmpdir:
367
- result = subprocess.run(
373
+ result = run_wup_command(
368
374
  [sys.executable, "-m", "wup.cli", "map-deps", tmpdir],
369
375
  cwd=tmpdir,
370
376
  capture_output=True,
@@ -394,7 +400,7 @@ class TestE2EPerformance:
394
400
  routes_file.write_text("def handler(): pass\n")
395
401
 
396
402
  start_time = time.time()
397
- result = subprocess.run(
403
+ result = run_wup_command(
398
404
  [sys.executable, "-m", "wup.cli", "map-deps", tmpdir],
399
405
  cwd=tmpdir,
400
406
  capture_output=True,
@@ -409,7 +415,7 @@ class TestE2EPerformance:
409
415
  """Test init command performance."""
410
416
  with tempfile.TemporaryDirectory() as tmpdir:
411
417
  start_time = time.time()
412
- result = subprocess.run(
418
+ result = run_wup_command(
413
419
  [sys.executable, "-m", "wup.cli", "init"],
414
420
  cwd=tmpdir,
415
421
  capture_output=True,
@@ -461,7 +467,7 @@ services:
461
467
  routes_file.write_text("def handler(): pass\n")
462
468
 
463
469
  # Build dependencies
464
- result = subprocess.run(
470
+ result = run_wup_command(
465
471
  [sys.executable, "-m", "wup.cli", "map-deps", tmpdir],
466
472
  cwd=tmpdir,
467
473
  capture_output=True,
@@ -500,7 +506,7 @@ services:
500
506
  routes_file.write_text("def handler(): pass\n")
501
507
 
502
508
  # Build dependencies
503
- result = subprocess.run(
509
+ result = run_wup_command(
504
510
  [sys.executable, "-m", "wup.cli", "map-deps", tmpdir],
505
511
  cwd=tmpdir,
506
512
  capture_output=True,
@@ -0,0 +1,201 @@
1
+ import asyncio
2
+ import json
3
+ import os
4
+ import tempfile
5
+ from pathlib import Path
6
+ from subprocess import CompletedProcess
7
+
8
+ from wup.testql_watcher import TestQLWatcher
9
+ from wup.models.config import WupConfig, ProjectConfig, TestQLConfig
10
+
11
+
12
+ def test_process_changed_file_creates_track_on_failure():
13
+ with tempfile.TemporaryDirectory() as tmpdir:
14
+ root = Path(tmpdir)
15
+ app_file = root / "app" / "users" / "routes.py"
16
+ app_file.parent.mkdir(parents=True, exist_ok=True)
17
+ app_file.write_text("print('x')\n", encoding="utf-8")
18
+
19
+ scenario_dir = root / "testql-scenarios"
20
+ scenario_dir.mkdir(parents=True, exist_ok=True)
21
+ failing_scenario = scenario_dir / "app-users.testql.toon.yaml"
22
+ failing_scenario.write_text("name: failing\n", encoding="utf-8")
23
+
24
+ # Pass empty config to prevent loading from temp dir
25
+ from wup.models.config import TestQLConfig, WatchConfig
26
+ empty_config = WupConfig(
27
+ project=ProjectConfig(name="test"),
28
+ services=[],
29
+ test_strategy=None,
30
+ watch=WatchConfig(), # Add watch config to avoid file filtering issues
31
+ testql=TestQLConfig(scenario_dir="testql-scenarios")
32
+ )
33
+ watcher = TestQLWatcher(
34
+ project_root=str(root),
35
+ deps_file=str(root / "deps.json"),
36
+ scenarios_dir="testql-scenarios",
37
+ track_dir=".wup/tracks",
38
+ config=empty_config,
39
+ )
40
+
41
+ watcher.dependency_mapper.service_to_endpoints["app/users"] = ["/api/v1/users"]
42
+
43
+ def fake_run_testql(args, timeout):
44
+ if "--dry-run" in args:
45
+ return CompletedProcess(args=args, returncode=1, stdout="", stderr="intentional failure")
46
+ return CompletedProcess(args=args, returncode=0, stdout="{}", stderr="")
47
+
48
+ watcher._run_testql = fake_run_testql # type: ignore[method-assign]
49
+
50
+ result = asyncio.run(watcher.process_changed_file_once(str(app_file)))
51
+
52
+ assert result["processed_items"] >= 1
53
+ assert result["last_track_path"] is not None
54
+
55
+ track_path = Path(result["last_track_path"])
56
+ assert track_path.exists()
57
+
58
+ track_payload = json.loads(track_path.read_text(encoding="utf-8"))
59
+ assert track_payload["service"] == "app/users"
60
+ assert track_payload["stage"] == "quick"
61
+ assert "intentional failure" in track_payload["stderr_head"]
62
+
63
+
64
+ def test_browser_event_file_is_written_without_service_url():
65
+ with tempfile.TemporaryDirectory() as tmpdir:
66
+ root = Path(tmpdir)
67
+ scenario_dir = root / "testql-scenarios"
68
+ scenario_dir.mkdir(parents=True, exist_ok=True)
69
+ scenario_file = scenario_dir / "api-users-smoke.testql.toon.yaml"
70
+ scenario_file.write_text("name: smoke\n", encoding="utf-8")
71
+
72
+ watcher = TestQLWatcher(
73
+ project_root=str(root),
74
+ deps_file=str(root / "deps.json"),
75
+ scenarios_dir="testql-scenarios",
76
+ track_dir=".wup/tracks",
77
+ )
78
+
79
+ result = CompletedProcess(args=["testql", "run"], returncode=1, stdout="", stderr="boom")
80
+ track_path = watcher._write_track(
81
+ service="app/users",
82
+ stage="quick",
83
+ scenario=scenario_file,
84
+ result=result,
85
+ )
86
+
87
+ assert track_path.exists()
88
+ event_file = root / ".wup" / "browser-events" / "latest.json"
89
+ assert event_file.exists()
90
+ event_payload = json.loads(event_file.read_text(encoding="utf-8"))
91
+ assert event_payload["type"] == "wup_testql_error"
92
+ assert event_payload["service"] == "app/users"
93
+
94
+
95
+ def test_config_endpoints_use_base_url_from_yaml_config():
96
+ with tempfile.TemporaryDirectory() as tmpdir:
97
+ root = Path(tmpdir)
98
+ cfg = WupConfig(
99
+ project=ProjectConfig(name="demo"),
100
+ testql=TestQLConfig(
101
+ base_url="http://localhost:8100",
102
+ explicit_endpoints=["/connect-config"],
103
+ endpoints_by_service={"connect-config": ["/connect-config-sitemap"]},
104
+ ),
105
+ )
106
+
107
+ watcher = TestQLWatcher(
108
+ project_root=str(root),
109
+ deps_file=str(root / "deps.json"),
110
+ scenarios_dir="testql-scenarios",
111
+ track_dir=".wup/tracks",
112
+ config=cfg,
113
+ )
114
+
115
+ endpoints = watcher._get_config_endpoints_for_service("connect-config")
116
+ assert "http://localhost:8100/connect-config" in endpoints
117
+ assert "http://localhost:8100/connect-config-sitemap" in endpoints
118
+
119
+
120
+ def test_config_endpoints_use_base_url_from_env_when_yaml_missing():
121
+ with tempfile.TemporaryDirectory() as tmpdir:
122
+ root = Path(tmpdir)
123
+ cfg = WupConfig(
124
+ project=ProjectConfig(name="demo"),
125
+ testql=TestQLConfig(
126
+ base_url="",
127
+ base_url_env="WUP_BASE_URL",
128
+ explicit_endpoints=["/connect-data"],
129
+ ),
130
+ )
131
+
132
+ old_value = os.environ.get("WUP_BASE_URL")
133
+ os.environ["WUP_BASE_URL"] = "http://localhost:8100"
134
+ try:
135
+ watcher = TestQLWatcher(
136
+ project_root=str(root),
137
+ deps_file=str(root / "deps.json"),
138
+ scenarios_dir="testql-scenarios",
139
+ track_dir=".wup/tracks",
140
+ config=cfg,
141
+ )
142
+
143
+ endpoints = watcher._get_config_endpoints_for_service("connect-data")
144
+ assert "http://localhost:8100/connect-data" in endpoints
145
+ finally:
146
+ if old_value is None:
147
+ os.environ.pop("WUP_BASE_URL", None)
148
+ else:
149
+ os.environ["WUP_BASE_URL"] = old_value
150
+
151
+
152
+ def test_service_health_transitions_are_persisted():
153
+ with tempfile.TemporaryDirectory() as tmpdir:
154
+ root = Path(tmpdir)
155
+ scenario_dir = root / "testql-scenarios"
156
+ scenario_dir.mkdir(parents=True, exist_ok=True)
157
+ scenario_file = scenario_dir / "connect-config-smoke.testql.toon.yaml"
158
+ scenario_file.write_text("name: smoke\n", encoding="utf-8")
159
+
160
+ watcher = TestQLWatcher(
161
+ project_root=str(root),
162
+ deps_file=str(root / "deps.json"),
163
+ scenarios_dir="testql-scenarios",
164
+ track_dir=".wup/tracks",
165
+ )
166
+
167
+ # 1) First quick run fails -> service goes down
168
+ def failing_run(args, timeout):
169
+ return CompletedProcess(args=args, returncode=1, stdout="", stderr="down")
170
+
171
+ watcher._run_testql = failing_run # type: ignore[method-assign]
172
+ failed = asyncio.run(watcher.run_quick_test("connect-config", []))
173
+ assert failed is False
174
+
175
+ health_state_path = root / ".wup" / "service-health.json"
176
+ health_events_path = root / ".wup" / "service-health-events.jsonl"
177
+ assert health_state_path.exists()
178
+ assert health_events_path.exists()
179
+
180
+ state = json.loads(health_state_path.read_text(encoding="utf-8"))
181
+ assert state["connect-config"]["status"] == "down"
182
+
183
+ # 2) Next quick run succeeds -> service goes up
184
+ def passing_run(args, timeout):
185
+ return CompletedProcess(args=args, returncode=0, stdout="ok", stderr="")
186
+
187
+ watcher._run_testql = passing_run # type: ignore[method-assign]
188
+ passed = asyncio.run(watcher.run_quick_test("connect-config", []))
189
+ assert passed is True
190
+
191
+ state = json.loads(health_state_path.read_text(encoding="utf-8"))
192
+ assert state["connect-config"]["status"] == "up"
193
+
194
+ events = []
195
+ with health_events_path.open("r", encoding="utf-8") as handle:
196
+ for line in handle:
197
+ events.append(json.loads(line))
198
+
199
+ statuses = [event.get("status") for event in events if event.get("service") == "connect-config"]
200
+ assert "down" in statuses
201
+ assert "up" in statuses
@@ -790,6 +790,47 @@ def test_import():
790
790
  from wup import WupWatcher, DependencyMapper # noqa: F401
791
791
 
792
792
 
793
+ class TestFileFiltering:
794
+ """Tests for file type filtering."""
795
+
796
+ def test_should_watch_file_with_config(self):
797
+ """Test file filtering with configured file types."""
798
+ from wup.models.config import WupConfig, ProjectConfig, WatchConfig
799
+
800
+ with tempfile.TemporaryDirectory() as tmpdir:
801
+ config = WupConfig(
802
+ project=ProjectConfig(name="test"),
803
+ watch=WatchConfig(file_types=[".py", ".ts", ".tsx", ".js"])
804
+ )
805
+ watcher = WupWatcher(tmpdir, config=config)
806
+
807
+ # Should watch allowed types
808
+ assert watcher.should_watch_file(str(Path(tmpdir) / "app.py"))
809
+ assert watcher.should_watch_file(str(Path(tmpdir) / "component.ts"))
810
+ assert watcher.should_watch_file(str(Path(tmpdir) / "app.tsx"))
811
+ assert watcher.should_watch_file(str(Path(tmpdir) / "main.js"))
812
+
813
+ # Should skip disallowed types
814
+ assert not watcher.should_watch_file(str(Path(tmpdir) / "README.md"))
815
+ assert not watcher.should_watch_file(str(Path(tmpdir) / "config.yaml"))
816
+
817
+ def test_should_watch_file_without_config(self):
818
+ """Test file filtering without configured file types (watch all)."""
819
+ from wup.models.config import WupConfig, ProjectConfig, WatchConfig
820
+
821
+ with tempfile.TemporaryDirectory() as tmpdir:
822
+ config = WupConfig(
823
+ project=ProjectConfig(name="test"),
824
+ watch=WatchConfig(file_types=[])
825
+ )
826
+ watcher = WupWatcher(tmpdir, config=config)
827
+
828
+ # Should watch all files when no filter configured
829
+ assert watcher.should_watch_file(str(Path(tmpdir) / "app.py"))
830
+ assert watcher.should_watch_file(str(Path(tmpdir) / "README.md"))
831
+ assert watcher.should_watch_file(str(Path(tmpdir) / "config.yaml"))
832
+
833
+
793
834
  class TestConfigModels:
794
835
  """Tests for configuration dataclasses."""
795
836
 
@@ -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.8"
10
+ __version__ = "0.2.11"
11
11
  __author__ = "Tom Sapletta"
12
12
 
13
13
  from .config import load_config, save_config, get_default_config
@@ -171,6 +171,8 @@ def map_deps(
171
171
  def status(
172
172
  deps_file: str = typer.Option("deps.json", "--deps", "-d", help="Path to dependency map file"),
173
173
  config: Optional[str] = typer.Option(None, "--config", "-C", help="Path to wup.yaml config file"),
174
+ delta_seconds: int = typer.Option(0, "--delta-seconds", help="Show only service health transitions from last N seconds"),
175
+ failed_only: bool = typer.Option(False, "--failed-only", help="Show only currently failing services"),
174
176
  ):
175
177
  """
176
178
  Show dependency map status and configuration.
@@ -243,6 +245,81 @@ def status(
243
245
  if endpoints:
244
246
  console.print(f" Sample endpoints: {', '.join(endpoints[:3])}")
245
247
 
248
+ # Show service health state and recent transitions (TestQL watcher)
249
+ health_state_path = project_path / ".wup" / "service-health.json"
250
+ health_events_path = project_path / ".wup" / "service-health-events.jsonl"
251
+
252
+ health_state = {}
253
+ if health_state_path.exists():
254
+ import json
255
+
256
+ try:
257
+ payload = json.loads(health_state_path.read_text(encoding="utf-8"))
258
+ if isinstance(payload, dict):
259
+ health_state = payload
260
+ except json.JSONDecodeError:
261
+ health_state = {}
262
+
263
+ if failed_only:
264
+ failing = [
265
+ (svc, data)
266
+ for svc, data in sorted(health_state.items())
267
+ if isinstance(data, dict) and data.get("status") == "down"
268
+ ]
269
+ console.print()
270
+ console.print("[bold]Currently failing services:[/bold]")
271
+ if not failing:
272
+ console.print(" [green]None[/green]")
273
+ else:
274
+ for svc, data in failing:
275
+ updated_at = data.get("updated_at", 0)
276
+ track_file = data.get("track_file", "")
277
+ stage = data.get("stage", "")
278
+ message = data.get("message", "")
279
+ console.print(f" [red]{svc}[/red] stage={stage} updated_at={updated_at}")
280
+ if track_file:
281
+ console.print(f" track: {track_file}")
282
+ if message:
283
+ console.print(f" message: {message}")
284
+
285
+ if delta_seconds > 0:
286
+ import json
287
+ import time
288
+
289
+ cutoff = int(time.time()) - delta_seconds
290
+ recent_events = []
291
+ if health_events_path.exists():
292
+ with health_events_path.open("r", encoding="utf-8") as handle:
293
+ for line in handle:
294
+ line = line.strip()
295
+ if not line:
296
+ continue
297
+ try:
298
+ event = json.loads(line)
299
+ except json.JSONDecodeError:
300
+ continue
301
+ if int(event.get("timestamp", 0)) >= cutoff:
302
+ recent_events.append(event)
303
+
304
+ console.print()
305
+ console.print(f"[bold]Service health delta (last {delta_seconds}s):[/bold]")
306
+ if not recent_events:
307
+ console.print(" [yellow]No health transitions in selected window[/yellow]")
308
+ else:
309
+ recent_events.sort(key=lambda item: int(item.get("timestamp", 0)), reverse=True)
310
+ for event in recent_events:
311
+ svc = event.get("service", "unknown")
312
+ prev = event.get("previous_status", "unknown")
313
+ curr = event.get("status", "unknown")
314
+ stage = event.get("stage", "")
315
+ message = event.get("message", "")
316
+ track_file = event.get("track_file", "")
317
+ console.print(f" [cyan]{svc}[/cyan]: {prev} -> {curr} ({stage})")
318
+ if message:
319
+ console.print(f" message: {message}")
320
+ if track_file:
321
+ console.print(f" track: {track_file}")
322
+
246
323
 
247
324
  @app.command()
248
325
  def init(
@@ -150,7 +150,11 @@ def validate_config(raw: dict) -> WupConfig:
150
150
  smoke_scenario=testql_raw.get("smoke_scenario", "smoke.testql.toon.yaml"),
151
151
  output_format=testql_raw.get("output_format", "json"),
152
152
  extra_args=testql_raw.get("extra_args", ["--timeout 10s"]),
153
- endpoint_discovery=testql_raw.get("endpoint_discovery", True)
153
+ endpoint_discovery=testql_raw.get("endpoint_discovery", True),
154
+ base_url=testql_raw.get("base_url", ""),
155
+ base_url_env=testql_raw.get("base_url_env", "WUP_BASE_URL"),
156
+ explicit_endpoints=testql_raw.get("explicit_endpoints", []),
157
+ endpoints_by_service=testql_raw.get("endpoints_by_service", {})
154
158
  )
155
159
 
156
160
  return WupConfig(
@@ -215,7 +219,12 @@ def save_config(config: WupConfig, output_path: Path):
215
219
  "scenario_dir": config.testql.scenario_dir,
216
220
  "smoke_scenario": config.testql.smoke_scenario,
217
221
  "output_format": config.testql.output_format,
218
- "extra_args": config.testql.extra_args
222
+ "extra_args": config.testql.extra_args,
223
+ "endpoint_discovery": config.testql.endpoint_discovery,
224
+ "base_url": config.testql.base_url,
225
+ "base_url_env": config.testql.base_url_env,
226
+ "explicit_endpoints": config.testql.explicit_endpoints,
227
+ "endpoints_by_service": config.testql.endpoints_by_service,
219
228
  }
220
229
  }
221
230
 
@@ -132,11 +132,19 @@ class WupWatcher:
132
132
  if re.search(pattern, path_lower):
133
133
  return svc.name
134
134
 
135
+ # Heuristic: if top-level directory matches known prefix patterns (e.g. connect-*)
136
+ # use it directly as the service name — takes priority over stale deps.json
137
+ if parts:
138
+ top = parts[0]
139
+ import re as _re
140
+ if _re.match(r'^(connect|backend|frontend|api|app|worker|service)[-_]', top):
141
+ return top
142
+
135
143
  # Use dependency mapper if available
136
144
  service = self.dependency_mapper.get_service_for_file(file_path)
137
145
  if service:
138
146
  return service
139
-
147
+
140
148
  # Fallback: use first two meaningful parts (only if file exists)
141
149
  if len(parts) >= 2:
142
150
  # Check if file exists (absolute path)
@@ -375,6 +383,26 @@ class WupWatcher:
375
383
  await self.process_test_queue_once()
376
384
  await asyncio.sleep(self.debounce_seconds)
377
385
 
386
+ def should_watch_file(self, file_path: str) -> bool:
387
+ """
388
+ Check if a file should be watched based on configured file types.
389
+
390
+ Args:
391
+ file_path: Path to the file
392
+
393
+ Returns:
394
+ True if file should be watched, False otherwise
395
+ """
396
+ normalized = str(file_path).lower()
397
+ if normalized.endswith(".testql.toon.yaml"):
398
+ return True
399
+
400
+ if not self.config.watch.file_types:
401
+ return True
402
+
403
+ file_suffix = Path(file_path).suffix.lower()
404
+ return file_suffix in self.config.watch.file_types
405
+
378
406
  def on_file_change(self, file_path: str):
379
407
  """
380
408
  Handle file change event.
@@ -382,6 +410,10 @@ class WupWatcher:
382
410
  Args:
383
411
  file_path: Path to the changed file
384
412
  """
413
+ # Check file type filter
414
+ if not self.should_watch_file(file_path):
415
+ return
416
+
385
417
  # Only watch relevant directories
386
418
  rel_path = self._to_relative_path(file_path)
387
419
  parts = rel_path.parts
@@ -59,6 +59,10 @@ class TestQLConfig:
59
59
  output_format: str = "json"
60
60
  extra_args: List[str] = field(default_factory=lambda: ["--timeout 10s"])
61
61
  endpoint_discovery: bool = True # Enable automatic endpoint discovery from scenarios
62
+ base_url: str = ""
63
+ base_url_env: str = "WUP_BASE_URL"
64
+ explicit_endpoints: List[str] = field(default_factory=list)
65
+ endpoints_by_service: Dict[str, List[str]] = field(default_factory=dict)
62
66
 
63
67
 
64
68
  @dataclass
@@ -4,6 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  import asyncio
6
6
  import json
7
+ import os
7
8
  import re
8
9
  import subprocess
9
10
  import time
@@ -50,10 +51,12 @@ class TestQLWatcher(WupWatcher):
50
51
 
51
52
  __test__ = False
52
53
 
54
+ _UNSET = object()
55
+
53
56
  def __init__(
54
57
  self,
55
58
  project_root: str,
56
- scenarios_dir: str = "testql-scenarios",
59
+ scenarios_dir=_UNSET,
57
60
  testql_bin: str = "testql",
58
61
  track_dir: str = ".wup/tracks",
59
62
  browser_service_url: Optional[str] = None,
@@ -68,16 +71,15 @@ class TestQLWatcher(WupWatcher):
68
71
  # Pass config to parent class
69
72
  super().__init__(project_root=project_root, config=config, **kwargs)
70
73
 
71
- # Use config values if available, otherwise use parameters
72
- if config.testql:
74
+ # Explicit constructor arg wins; otherwise use config; final fallback default
75
+ if scenarios_dir is not self._UNSET:
76
+ self.scenarios_dir = self.project_root / scenarios_dir
77
+ elif config and config.testql and config.testql.scenario_dir:
73
78
  self.scenarios_dir = self.project_root / config.testql.scenario_dir
74
- self.testql_bin = testql_bin # CLI parameter takes precedence
75
- # Use extra_args from config if needed
76
- self.testql_extra_args = config.testql.extra_args
77
79
  else:
78
- self.scenarios_dir = self.project_root / scenarios_dir
79
- self.testql_bin = testql_bin
80
- self.testql_extra_args = []
80
+ self.scenarios_dir = self.project_root / "testql-scenarios"
81
+ self.testql_bin = testql_bin
82
+ self.testql_extra_args = config.testql.extra_args if config and config.testql else []
81
83
 
82
84
  self.quick_limit = quick_limit
83
85
  self.track_dir = self.project_root / track_dir
@@ -87,12 +89,119 @@ class TestQLWatcher(WupWatcher):
87
89
  events_file=self.project_root / ".wup" / "browser-events" / "latest.json",
88
90
  )
89
91
  self.last_track_path: Optional[Path] = None
92
+ self.health_state_path = self.project_root / ".wup" / "service-health.json"
93
+ self.health_events_path = self.project_root / ".wup" / "service-health-events.jsonl"
94
+ self.health_state_path.parent.mkdir(parents=True, exist_ok=True)
95
+ self.service_health = self._load_service_health()
90
96
  self.config = config
91
97
 
98
+ def _load_service_health(self) -> Dict[str, Dict]:
99
+ if not self.health_state_path.exists():
100
+ return {}
101
+ try:
102
+ payload = json.loads(self.health_state_path.read_text(encoding="utf-8"))
103
+ if isinstance(payload, dict):
104
+ return payload
105
+ except (json.JSONDecodeError, OSError):
106
+ return {}
107
+ return {}
108
+
109
+ def _save_service_health(self) -> None:
110
+ self.health_state_path.write_text(
111
+ json.dumps(self.service_health, indent=2),
112
+ encoding="utf-8",
113
+ )
114
+
115
+ def _record_health_transition(
116
+ self,
117
+ *,
118
+ service: str,
119
+ status: str,
120
+ stage: str,
121
+ message: str = "",
122
+ track_file: Optional[str] = None,
123
+ ) -> None:
124
+ now = int(time.time())
125
+ previous = self.service_health.get(service, {})
126
+ previous_status = previous.get("status", "unknown")
127
+
128
+ self.service_health[service] = {
129
+ "status": status,
130
+ "updated_at": now,
131
+ "stage": stage,
132
+ "message": message,
133
+ "track_file": track_file or "",
134
+ }
135
+ self._save_service_health()
136
+
137
+ changed = previous_status != status
138
+ if not changed:
139
+ return
140
+
141
+ event = {
142
+ "timestamp": now,
143
+ "service": service,
144
+ "status": status,
145
+ "previous_status": previous_status,
146
+ "stage": stage,
147
+ "message": message,
148
+ "track_file": track_file or "",
149
+ }
150
+ with self.health_events_path.open("a", encoding="utf-8") as handle:
151
+ handle.write(json.dumps(event) + "\n")
152
+
153
+ self.browser_notifier.notify(
154
+ {
155
+ "type": "wup_service_health_change",
156
+ "service": service,
157
+ "status": status,
158
+ "previous_status": previous_status,
159
+ "stage": stage,
160
+ "message": message,
161
+ "track_file": track_file,
162
+ }
163
+ )
164
+
92
165
  def _tokenize_service(self, service: str) -> List[str]:
93
166
  raw_tokens = re.split(r"[^a-zA-Z0-9]+", service.lower())
94
167
  return [token for token in raw_tokens if len(token) >= 3]
95
168
 
169
+ def _get_config_endpoints_for_service(self, service: str) -> List[str]:
170
+ by_service = self.config.testql.endpoints_by_service or {}
171
+ explicit = self.config.testql.explicit_endpoints or []
172
+
173
+ service_specific = by_service.get(service, [])
174
+ merged: List[str] = []
175
+ for endpoint in [*service_specific, *explicit]:
176
+ endpoint_url = self._to_full_url(endpoint)
177
+ if endpoint_url not in merged:
178
+ merged.append(endpoint_url)
179
+ return merged
180
+
181
+ def _resolve_base_url(self) -> str:
182
+ base_url = (self.config.testql.base_url or "").strip()
183
+ if base_url:
184
+ return base_url.rstrip("/")
185
+
186
+ env_key = (self.config.testql.base_url_env or "WUP_BASE_URL").strip()
187
+ env_url = os.getenv(env_key, "").strip()
188
+ if env_url:
189
+ return env_url.rstrip("/")
190
+
191
+ return ""
192
+
193
+ def _to_full_url(self, endpoint: str) -> str:
194
+ if endpoint.startswith("http://") or endpoint.startswith("https://"):
195
+ return endpoint
196
+
197
+ base_url = self._resolve_base_url()
198
+ if not base_url:
199
+ return endpoint
200
+
201
+ if endpoint.startswith("/"):
202
+ return f"{base_url}{endpoint}"
203
+ return f"{base_url}/{endpoint}"
204
+
96
205
  def _discover_scenarios(self) -> List[Path]:
97
206
  if not self.scenarios_dir.exists():
98
207
  return []
@@ -210,6 +319,11 @@ class TestQLWatcher(WupWatcher):
210
319
  return track_path
211
320
 
212
321
  async def run_quick_test(self, service: str, endpoints: List[str]) -> bool:
322
+ merged_endpoints = list(endpoints)
323
+ for configured_endpoint in self._get_config_endpoints_for_service(service):
324
+ if configured_endpoint not in merged_endpoints:
325
+ merged_endpoints.append(configured_endpoint)
326
+
213
327
  scenarios = self._select_scenarios_for_service(service)
214
328
 
215
329
  # Apply service-specific quick limit
@@ -224,7 +338,7 @@ class TestQLWatcher(WupWatcher):
224
338
  return True
225
339
 
226
340
  self.console.print(
227
- f"[cyan]🧪 Quick TestQL for {service} ({len(scenarios)} scenarios / {len(endpoints)} endpoints)[/cyan]"
341
+ f"[cyan]🧪 Quick TestQL for {service} ({len(scenarios)} scenarios / {len(merged_endpoints)} endpoints)[/cyan]"
228
342
  )
229
343
 
230
344
  for scenario in scenarios:
@@ -246,19 +360,38 @@ class TestQLWatcher(WupWatcher):
246
360
  scenario=scenario,
247
361
  result=result,
248
362
  )
363
+ self._record_health_transition(
364
+ service=service,
365
+ status="down",
366
+ stage="quick",
367
+ message=result.stderr.strip() or result.stdout.strip() or "Quick TestQL failed",
368
+ track_file=str(track_path),
369
+ )
249
370
  self.console.print(
250
371
  f"[red]✗ Quick failed: {scenario.name} | track: {track_path}[/red]"
251
372
  )
252
373
  return False
253
374
 
375
+ self._record_health_transition(
376
+ service=service,
377
+ status="up",
378
+ stage="quick",
379
+ message="Quick TestQL passed",
380
+ )
254
381
  self.console.print(f"[green]✓ Quick TestQL passed for {service}[/green]")
255
382
  return True
256
383
 
257
384
  async def run_detail_test(self, service: str, endpoints: List[str]) -> Dict:
385
+ merged_endpoints = list(endpoints)
386
+ for configured_endpoint in self._get_config_endpoints_for_service(service):
387
+ if configured_endpoint not in merged_endpoints:
388
+ merged_endpoints.append(configured_endpoint)
389
+
258
390
  scenarios = self._select_scenarios_for_service(service)
259
391
  results = {
260
392
  "service": service,
261
393
  "total_scenarios": len(scenarios),
394
+ "total_endpoints": len(merged_endpoints),
262
395
  "passed": 0,
263
396
  "failed": 0,
264
397
  "failed_scenarios": [],
@@ -266,7 +399,7 @@ class TestQLWatcher(WupWatcher):
266
399
  }
267
400
 
268
401
  self.console.print(
269
- f"[cyan]🔍 Detail TestQL for {service} ({len(scenarios)} scenarios / {len(endpoints)} endpoints)[/cyan]"
402
+ f"[cyan]🔍 Detail TestQL for {service} ({len(scenarios)} scenarios / {len(merged_endpoints)} endpoints)[/cyan]"
270
403
  )
271
404
 
272
405
  for scenario in scenarios:
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wup
3
- Version: 0.2.8
3
+ Version: 0.2.11
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
- License-Expression: Apache-2.0
6
+ License: Apache-2.0
7
7
  Project-URL: Homepage, https://github.com/semcod/wup
8
8
  Project-URL: Repository, https://github.com/semcod/wup
9
9
  Keywords: wup,watcher,testing,regression,file-monitoring
@@ -21,6 +21,7 @@ Requires-Dist: watchdog>=4.0.0
21
21
  Requires-Dist: psutil>=5.9.0
22
22
  Requires-Dist: rich>=13.0.0
23
23
  Requires-Dist: typer>=0.9.0
24
+ Requires-Dist: pyyaml>=6.0
24
25
  Dynamic: license-file
25
26
 
26
27
  # WUP (What's Up)
@@ -28,17 +29,17 @@ Dynamic: license-file
28
29
 
29
30
  ## AI Cost Tracking
30
31
 
31
- ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.8-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
32
- ![AI Cost](https://img.shields.io/badge/AI%20Cost-$1.35-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-2.3h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
32
+ ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.11-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
33
+ ![AI Cost](https://img.shields.io/badge/AI%20Cost-$1.80-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-2.9h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
33
34
 
34
- - 🤖 **LLM usage:** $1.3500 (9 commits)
35
- - 👤 **Human dev:** ~$228 (2.3h @ $100/h, 30min dedup)
35
+ - 🤖 **LLM usage:** $1.8000 (12 commits)
36
+ - 👤 **Human dev:** ~$290 (2.9h @ $100/h, 30min dedup)
36
37
 
37
38
  Generated on 2026-04-29 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
38
39
 
39
40
  ---
40
41
 
41
- ![PyPI](https://img.shields.io/badge/pypi-wup-blue) ![Version](https://img.shields.io/badge/version-0.2.8-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
42
+ ![PyPI](https://img.shields.io/badge/pypi-wup-blue) ![Version](https://img.shields.io/badge/version-0.2.11-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
42
43
 
43
44
  **WUP (What's Up)** - Intelligent file watcher for regression testing in large projects.
44
45
 
@@ -2,3 +2,4 @@ watchdog>=4.0.0
2
2
  psutil>=5.9.0
3
3
  rich>=13.0.0
4
4
  typer>=0.9.0
5
+ pyyaml>=6.0
@@ -1,89 +0,0 @@
1
- import asyncio
2
- import json
3
- import tempfile
4
- from pathlib import Path
5
- from subprocess import CompletedProcess
6
-
7
- from wup.testql_watcher import TestQLWatcher
8
- from wup.models.config import WupConfig, ProjectConfig
9
-
10
-
11
- def test_process_changed_file_creates_track_on_failure():
12
- with tempfile.TemporaryDirectory() as tmpdir:
13
- root = Path(tmpdir)
14
- app_file = root / "app" / "users" / "routes.py"
15
- app_file.parent.mkdir(parents=True, exist_ok=True)
16
- app_file.write_text("print('x')\n", encoding="utf-8")
17
-
18
- scenario_dir = root / "testql-scenarios"
19
- scenario_dir.mkdir(parents=True, exist_ok=True)
20
- failing_scenario = scenario_dir / "api-users-failing.testql.toon.yaml"
21
- failing_scenario.write_text("name: failing\n", encoding="utf-8")
22
-
23
- # Pass empty config to prevent loading from temp dir
24
- empty_config = WupConfig(
25
- project=ProjectConfig(name="test"),
26
- services=[],
27
- test_strategy=None,
28
- testql=None
29
- )
30
- watcher = TestQLWatcher(
31
- project_root=str(root),
32
- deps_file=str(root / "deps.json"),
33
- scenarios_dir="testql-scenarios",
34
- track_dir=".wup/tracks",
35
- config=empty_config,
36
- )
37
-
38
- watcher.dependency_mapper.service_to_endpoints["app/users"] = ["/api/v1/users"]
39
-
40
- def fake_run_testql(args, timeout):
41
- if "--dry-run" in args:
42
- return CompletedProcess(args=args, returncode=1, stdout="", stderr="intentional failure")
43
- return CompletedProcess(args=args, returncode=0, stdout="{}", stderr="")
44
-
45
- watcher._run_testql = fake_run_testql # type: ignore[method-assign]
46
-
47
- result = asyncio.run(watcher.process_changed_file_once(str(app_file)))
48
-
49
- assert result["processed_items"] >= 1
50
- assert result["last_track_path"] is not None
51
-
52
- track_path = Path(result["last_track_path"])
53
- assert track_path.exists()
54
-
55
- track_payload = json.loads(track_path.read_text(encoding="utf-8"))
56
- assert track_payload["service"] == "app/users"
57
- assert track_payload["stage"] == "quick"
58
- assert "intentional failure" in track_payload["stderr_head"]
59
-
60
-
61
- def test_browser_event_file_is_written_without_service_url():
62
- with tempfile.TemporaryDirectory() as tmpdir:
63
- root = Path(tmpdir)
64
- scenario_dir = root / "testql-scenarios"
65
- scenario_dir.mkdir(parents=True, exist_ok=True)
66
- scenario_file = scenario_dir / "api-users-smoke.testql.toon.yaml"
67
- scenario_file.write_text("name: smoke\n", encoding="utf-8")
68
-
69
- watcher = TestQLWatcher(
70
- project_root=str(root),
71
- deps_file=str(root / "deps.json"),
72
- scenarios_dir="testql-scenarios",
73
- track_dir=".wup/tracks",
74
- )
75
-
76
- result = CompletedProcess(args=["testql", "run"], returncode=1, stdout="", stderr="boom")
77
- track_path = watcher._write_track(
78
- service="app/users",
79
- stage="quick",
80
- scenario=scenario_file,
81
- result=result,
82
- )
83
-
84
- assert track_path.exists()
85
- event_file = root / ".wup" / "browser-events" / "latest.json"
86
- assert event_file.exists()
87
- event_payload = json.loads(event_file.read_text(encoding="utf-8"))
88
- assert event_payload["type"] == "wup_testql_error"
89
- assert event_payload["service"] == "app/users"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes