wup 0.2.6__tar.gz → 0.2.7__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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wup
3
- Version: 0.2.6
3
+ Version: 0.2.7
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
@@ -28,17 +28,17 @@ Dynamic: license-file
28
28
 
29
29
  ## AI Cost Tracking
30
30
 
31
- ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.6-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.05-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-2.0h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
31
+ ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.7-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.20-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-2.2h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
33
33
 
34
- - 🤖 **LLM usage:** $1.0500 (7 commits)
35
- - 👤 **Human dev:** ~$200 (2.0h @ $100/h, 30min dedup)
34
+ - 🤖 **LLM usage:** $1.2000 (8 commits)
35
+ - 👤 **Human dev:** ~$223 (2.2h @ $100/h, 30min dedup)
36
36
 
37
37
  Generated on 2026-04-29 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
38
38
 
39
39
  ---
40
40
 
41
- ![PyPI](https://img.shields.io/badge/pypi-wup-blue) ![Version](https://img.shields.io/badge/version-0.2.6-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
41
+ ![PyPI](https://img.shields.io/badge/pypi-wup-blue) ![Version](https://img.shields.io/badge/version-0.2.7-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
42
42
 
43
43
  **WUP (What's Up)** - Intelligent file watcher for regression testing in large projects.
44
44
 
@@ -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.6-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.05-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-2.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.7-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.20-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-2.2h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
8
8
 
9
- - 🤖 **LLM usage:** $1.0500 (7 commits)
10
- - 👤 **Human dev:** ~$200 (2.0h @ $100/h, 30min dedup)
9
+ - 🤖 **LLM usage:** $1.2000 (8 commits)
10
+ - 👤 **Human dev:** ~$223 (2.2h @ $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.6-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.7-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.6"
7
+ version = "0.2.7"
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"
@@ -103,6 +103,91 @@ def create_user():
103
103
 
104
104
  assert mapper2.file_to_endpoints == mapper.file_to_endpoints
105
105
  assert mapper2.service_to_endpoints == mapper.service_to_endpoints
106
+
107
+ def test_infer_service_from_path_edge_cases(self):
108
+ """Test service inference with edge case paths."""
109
+ with tempfile.TemporaryDirectory() as tmpdir:
110
+ mapper = DependencyMapper(tmpdir)
111
+
112
+ # Single directory - returns None (needs at least 2 parts)
113
+ assert mapper._infer_service("app") is None
114
+
115
+ # Very deep nesting
116
+ assert mapper._infer_service("a/b/c/d/e/f/file.py") == "a/b"
117
+
118
+ # Path with numbers
119
+ assert mapper._infer_service("v1/api/routes.py") == "v1/api"
120
+
121
+ # Path with underscores
122
+ assert mapper._infer_service("src/user_auth/login.py") == "src/user_auth"
123
+
124
+ def test_get_service_for_file_empty_mapper(self):
125
+ """Test getting service for file when mapper is empty."""
126
+ with tempfile.TemporaryDirectory() as tmpdir:
127
+ mapper = DependencyMapper(tmpdir)
128
+
129
+ # Need to use absolute path under tmpdir
130
+ file_path = str(Path(tmpdir) / "app" / "users" / "routes.py")
131
+ service = mapper.get_service_for_file(file_path)
132
+ # Dependency mapper has fallback heuristic that returns first two path parts
133
+ assert service == "app/users"
134
+
135
+ def test_get_endpoints_for_service_empty_mapper(self):
136
+ """Test getting endpoints for service when mapper is empty."""
137
+ with tempfile.TemporaryDirectory() as tmpdir:
138
+ mapper = DependencyMapper(tmpdir)
139
+
140
+ endpoints = mapper.get_endpoints_for_service("users")
141
+ assert endpoints == []
142
+
143
+ def test_build_from_codebase_with_flask(self):
144
+ """Test building dependency map with Flask endpoints."""
145
+ with tempfile.TemporaryDirectory() as tmpdir:
146
+ # Create a sample Flask file
147
+ app_dir = Path(tmpdir) / "app" / "auth"
148
+ app_dir.mkdir(parents=True)
149
+
150
+ routes_file = app_dir / "views.py"
151
+ routes_file.write_text("""
152
+ from flask import Blueprint, jsonify
153
+
154
+ bp = Blueprint('auth', __name__)
155
+
156
+ @bp.route('/login', methods=['POST'])
157
+ def login():
158
+ return jsonify({'token': 'abc'})
159
+
160
+ @bp.route('/logout', methods=['POST'])
161
+ def logout():
162
+ return jsonify({'success': True})
163
+ """)
164
+
165
+ mapper = DependencyMapper(tmpdir)
166
+ deps = mapper.build_from_codebase(framework="flask")
167
+
168
+ assert len(deps["services"]) > 0
169
+ assert len(deps["files"]) > 0
170
+
171
+ def test_service_to_files_tracking(self):
172
+ """Test that service to files mapping is tracked correctly."""
173
+ with tempfile.TemporaryDirectory() as tmpdir:
174
+ mapper = DependencyMapper(tmpdir)
175
+
176
+ # Add file to service
177
+ mapper.service_to_files["users"].add("app/users/routes.py")
178
+ mapper.service_to_files["users"].add("app/users/models.py")
179
+
180
+ assert len(mapper.service_to_files["users"]) == 2
181
+ assert "app/users/routes.py" in mapper.service_to_files["users"]
182
+
183
+ def test_build_from_codebase_nonexistent_directory(self):
184
+ """Test building from codebase with non-existent directory."""
185
+ with tempfile.TemporaryDirectory() as tmpdir:
186
+ mapper = DependencyMapper(tmpdir)
187
+ # Should not fail, just return empty deps
188
+ deps = mapper.build_from_codebase(framework="fastapi")
189
+ assert "services" in deps
190
+ assert "files" in deps
106
191
 
107
192
 
108
193
  class TestWupWatcher:
@@ -152,6 +237,102 @@ class TestWupWatcher:
152
237
  service = watcher.infer_service(str(Path(tmpdir) / "app" / "users" / "routes.py"))
153
238
  assert service == "app/users"
154
239
 
240
+ def test_infer_service_with_auto_detection(self):
241
+ """Test service inference with auto-detection from config."""
242
+ with tempfile.TemporaryDirectory() as tmpdir:
243
+ # Create service with auto-detection (empty paths)
244
+ service = ServiceConfig(
245
+ name="users-shell",
246
+ root="app/users-shell",
247
+ paths=[], # Empty paths triggers auto-detection
248
+ type="shell"
249
+ )
250
+ config = WupConfig(
251
+ project=ProjectConfig(name="test"),
252
+ watch=WatchConfig(),
253
+ services=[service],
254
+ test_strategy=TestStrategyConfig(),
255
+ testql=TestQLConfig()
256
+ )
257
+
258
+ watcher = WupWatcher(tmpdir, config=config)
259
+
260
+ # File containing "users-shell" should match
261
+ inferred = watcher.infer_service(str(Path(tmpdir) / "app" / "users-shell" / "main.py"))
262
+ assert inferred == "users-shell"
263
+
264
+ # File containing "users" should not match auto-detection
265
+ # Fallback heuristic returns "app/users" from dependency mapper
266
+ inferred = watcher.infer_service(str(Path(tmpdir) / "app" / "users" / "main.py"))
267
+ assert inferred == "app/users" # Fallback heuristic
268
+
269
+ def test_infer_service_with_explicit_paths(self):
270
+ """Test service inference with explicit config paths."""
271
+ with tempfile.TemporaryDirectory() as tmpdir:
272
+ service = ServiceConfig(
273
+ name="users",
274
+ root="app/users",
275
+ paths=["app/users/**", "routes/users/**"],
276
+ type="auto"
277
+ )
278
+ config = WupConfig(
279
+ project=ProjectConfig(name="test"),
280
+ watch=WatchConfig(),
281
+ services=[service],
282
+ test_strategy=TestStrategyConfig(),
283
+ testql=TestQLConfig()
284
+ )
285
+
286
+ watcher = WupWatcher(tmpdir, config=config)
287
+
288
+ # Explicit path should match
289
+ inferred = watcher.infer_service(str(Path(tmpdir) / "app" / "users" / "routes.py"))
290
+ assert inferred == "users"
291
+
292
+ # Alternative explicit path should match
293
+ inferred = watcher.infer_service(str(Path(tmpdir) / "routes" / "users" / "main.py"))
294
+ assert inferred == "users"
295
+
296
+ def test_infer_service_priority_config_over_mapper(self):
297
+ """Test that config services take priority over dependency mapper."""
298
+ with tempfile.TemporaryDirectory() as tmpdir:
299
+ service = ServiceConfig(
300
+ name="custom-service",
301
+ root="app/custom",
302
+ paths=["app/custom/**"],
303
+ type="auto"
304
+ )
305
+ config = WupConfig(
306
+ project=ProjectConfig(name="test"),
307
+ watch=WatchConfig(),
308
+ services=[service],
309
+ test_strategy=TestStrategyConfig(),
310
+ testql=TestQLConfig()
311
+ )
312
+
313
+ watcher = WupWatcher(tmpdir, config=config)
314
+
315
+ # Config should take priority
316
+ inferred = watcher.infer_service(str(Path(tmpdir) / "app" / "custom" / "file.py"))
317
+ assert inferred == "custom-service"
318
+
319
+ def test_infer_service_fallback_to_heuristics(self):
320
+ """Test fallback to heuristics when no config or mapper match."""
321
+ with tempfile.TemporaryDirectory() as tmpdir:
322
+ config = WupConfig(
323
+ project=ProjectConfig(name="test"),
324
+ watch=WatchConfig(),
325
+ services=[],
326
+ test_strategy=TestStrategyConfig(),
327
+ testql=TestQLConfig()
328
+ )
329
+
330
+ watcher = WupWatcher(tmpdir, config=config)
331
+
332
+ # Should fallback to heuristics (first two path parts)
333
+ inferred = watcher.infer_service(str(Path(tmpdir) / "app" / "users" / "routes.py"))
334
+ assert inferred == "app/users"
335
+
155
336
  def test_should_test_cooldown(self):
156
337
  """Test cooldown mechanism for testing."""
157
338
  import time
@@ -224,6 +405,383 @@ class TestWupWatcher:
224
405
 
225
406
  # No services should have been added
226
407
  assert len(watcher.changed_services) == 0
408
+
409
+ def test_detect_service_coincidences_shell_web(self):
410
+ """Test coincidence detection between shell and web services."""
411
+ with tempfile.TemporaryDirectory() as tmpdir:
412
+ service1 = ServiceConfig(
413
+ name="users-shell",
414
+ root="app/users-shell",
415
+ type="shell",
416
+ paths=[]
417
+ )
418
+ service2 = ServiceConfig(
419
+ name="users-web",
420
+ root="app/users-web",
421
+ type="web",
422
+ paths=[]
423
+ )
424
+ service3 = ServiceConfig(
425
+ name="payments-shell",
426
+ root="app/payments-shell",
427
+ type="shell",
428
+ paths=[]
429
+ )
430
+ config = WupConfig(
431
+ project=ProjectConfig(name="test"),
432
+ watch=WatchConfig(),
433
+ services=[service1, service2, service3],
434
+ test_strategy=TestStrategyConfig(),
435
+ testql=TestQLConfig()
436
+ )
437
+
438
+ watcher = WupWatcher(tmpdir, config=config)
439
+
440
+ # users-shell should detect users-web as related
441
+ related = watcher.detect_service_coincidences("users-shell")
442
+ assert "users-web" in related
443
+ assert "payments-shell" not in related
444
+
445
+ # users-web should detect users-shell as related
446
+ related = watcher.detect_service_coincidences("users-web")
447
+ assert "users-shell" in related
448
+ assert "payments-shell" not in related
449
+
450
+ def test_detect_service_coincidences_auto_type(self):
451
+ """Test coincidence detection with auto type services."""
452
+ with tempfile.TemporaryDirectory() as tmpdir:
453
+ service1 = ServiceConfig(
454
+ name="users",
455
+ root="app/users",
456
+ type="auto",
457
+ paths=[]
458
+ )
459
+ service2 = ServiceConfig(
460
+ name="users-api",
461
+ root="app/users-api",
462
+ type="auto",
463
+ paths=[]
464
+ )
465
+ config = WupConfig(
466
+ project=ProjectConfig(name="test"),
467
+ watch=WatchConfig(),
468
+ services=[service1, service2],
469
+ test_strategy=TestStrategyConfig(),
470
+ testql=TestQLConfig()
471
+ )
472
+
473
+ watcher = WupWatcher(tmpdir, config=config)
474
+
475
+ # users and users-api don't share domain (only shell/web suffixes are handled)
476
+ # Since both are auto type and don't have shell/web suffixes, they don't match
477
+ related = watcher.detect_service_coincidences("users")
478
+ assert "users-api" not in related
479
+
480
+ def test_detect_service_coincidences_no_config(self):
481
+ """Test coincidence detection with no configured services."""
482
+ with tempfile.TemporaryDirectory() as tmpdir:
483
+ config = WupConfig(
484
+ project=ProjectConfig(name="test"),
485
+ watch=WatchConfig(),
486
+ services=[],
487
+ test_strategy=TestStrategyConfig(),
488
+ testql=TestQLConfig()
489
+ )
490
+
491
+ watcher = WupWatcher(tmpdir, config=config)
492
+
493
+ related = watcher.detect_service_coincidences("users")
494
+ assert len(related) == 0
495
+
496
+ def test_detect_service_coincidences_unknown_service(self):
497
+ """Test coincidence detection for unknown service."""
498
+ with tempfile.TemporaryDirectory() as tmpdir:
499
+ service1 = ServiceConfig(
500
+ name="users-shell",
501
+ root="app/users-shell",
502
+ type="shell",
503
+ paths=[]
504
+ )
505
+ config = WupConfig(
506
+ project=ProjectConfig(name="test"),
507
+ watch=WatchConfig(),
508
+ services=[service1],
509
+ test_strategy=TestStrategyConfig(),
510
+ testql=TestQLConfig()
511
+ )
512
+
513
+ watcher = WupWatcher(tmpdir, config=config)
514
+
515
+ # Unknown service should return empty list
516
+ related = watcher.detect_service_coincidences("unknown")
517
+ assert len(related) == 0
518
+
519
+ def test_services_share_domain(self):
520
+ """Test the _services_share_domain helper method."""
521
+ with tempfile.TemporaryDirectory() as tmpdir:
522
+ watcher = WupWatcher(tmpdir)
523
+
524
+ # Same domain with different suffixes
525
+ assert watcher._services_share_domain("users-shell", "users-web")
526
+ assert watcher._services_share_domain("users-shell", "users")
527
+ assert watcher._services_share_domain("payments", "payments-shell")
528
+
529
+ # Different domains
530
+ assert not watcher._services_share_domain("users", "payments")
531
+ assert not watcher._services_share_domain("api/auth", "api/users")
532
+
533
+ # Underscore variants
534
+ assert watcher._services_share_domain("users_shell", "users_web")
535
+ assert watcher._services_share_domain("users_shell", "users")
536
+
537
+ def test_on_file_change_filters_by_file_type(self):
538
+ """Test that file change respects configured file types."""
539
+ with tempfile.TemporaryDirectory() as tmpdir:
540
+ (Path(tmpdir) / "app").mkdir()
541
+
542
+ watch = WatchConfig(
543
+ paths=["app/**"],
544
+ file_types=[".py", ".ts"]
545
+ )
546
+ config = WupConfig(
547
+ project=ProjectConfig(name="test"),
548
+ watch=watch,
549
+ services=[],
550
+ test_strategy=TestStrategyConfig(),
551
+ testql=TestQLConfig()
552
+ )
553
+
554
+ watcher = WupWatcher(tmpdir, config=config)
555
+
556
+ # Python file should be processed
557
+ py_file = str(Path(tmpdir) / "app" / "test.py")
558
+ watcher.on_file_change(py_file)
559
+
560
+ # TypeScript file should be processed
561
+ ts_file = str(Path(tmpdir) / "app" / "test.ts")
562
+ watcher.on_file_change(ts_file)
563
+
564
+ # Markdown file should be filtered out
565
+ md_file = str(Path(tmpdir) / "app" / "test.md")
566
+ watcher.on_file_change(md_file)
567
+
568
+ # Text file should be filtered out
569
+ txt_file = str(Path(tmpdir) / "app" / "test.txt")
570
+ watcher.on_file_change(txt_file)
571
+
572
+ # Only .py and .ts files should trigger service detection
573
+ # (though no services are configured, so changed_services will be empty)
574
+ # The key is that no errors occur and filtering works
575
+
576
+ def test_on_file_change_no_file_type_filter(self):
577
+ """Test that when file_types is empty, all files are processed."""
578
+ with tempfile.TemporaryDirectory() as tmpdir:
579
+ (Path(tmpdir) / "app").mkdir()
580
+
581
+ watch = WatchConfig(
582
+ paths=["app/**"],
583
+ file_types=[]
584
+ )
585
+ config = WupConfig(
586
+ project=ProjectConfig(name="test"),
587
+ watch=watch,
588
+ services=[],
589
+ test_strategy=TestStrategyConfig(),
590
+ testql=TestQLConfig()
591
+ )
592
+
593
+ watcher = WupWatcher(tmpdir, config=config)
594
+
595
+ # All file types should be processed
596
+ py_file = str(Path(tmpdir) / "app" / "test.py")
597
+ watcher.on_file_change(py_file)
598
+
599
+ md_file = str(Path(tmpdir) / "app" / "test.md")
600
+ watcher.on_file_change(md_file)
601
+
602
+ # No filtering should occur
603
+
604
+
605
+ class TestIntegrationWorkflow:
606
+ """Integration tests for complete workflows."""
607
+
608
+ def test_full_workflow_file_change_to_test_scheduling(self):
609
+ """Test complete workflow from file change to test scheduling."""
610
+ with tempfile.TemporaryDirectory() as tmpdir:
611
+ (Path(tmpdir) / "app").mkdir()
612
+
613
+ service = ServiceConfig(
614
+ name="users",
615
+ root="app/users",
616
+ paths=["app/users/**"],
617
+ quick_tests=ServiceTestConfig(scope="all", max_endpoints=3)
618
+ )
619
+ config = WupConfig(
620
+ project=ProjectConfig(name="test"),
621
+ watch=WatchConfig(paths=["app/**"]),
622
+ services=[service],
623
+ test_strategy=TestStrategyConfig(),
624
+ testql=TestQLConfig()
625
+ )
626
+
627
+ watcher = WupWatcher(tmpdir, config=config)
628
+ watcher.dependency_mapper.service_to_endpoints["users"] = [
629
+ "/api/users",
630
+ "/api/users/{id}",
631
+ "/api/users/create"
632
+ ]
633
+
634
+ # Simulate file change
635
+ file_path = str(Path(tmpdir) / "app" / "users" / "routes.py")
636
+ watcher.on_file_change(file_path)
637
+
638
+ # Verify service was detected
639
+ assert "users" in watcher.changed_services
640
+
641
+ # Verify test was scheduled
642
+ assert len(watcher.test_queue) == 1
643
+ test_type, service_name, endpoints = watcher.test_queue[0]
644
+ assert test_type == "quick"
645
+ assert service_name == "users"
646
+ assert len(endpoints) == 3 # Limited by quick_tests.max_endpoints
647
+
648
+ def test_workflow_with_file_type_filtering(self):
649
+ """Test workflow with file type filtering."""
650
+ with tempfile.TemporaryDirectory() as tmpdir:
651
+ (Path(tmpdir) / "app").mkdir()
652
+
653
+ watch = WatchConfig(
654
+ paths=["app/**"],
655
+ file_types=[".py"]
656
+ )
657
+ service = ServiceConfig(
658
+ name="users",
659
+ root="app/users",
660
+ paths=["app/users/**"]
661
+ )
662
+ config = WupConfig(
663
+ project=ProjectConfig(name="test"),
664
+ watch=watch,
665
+ services=[service],
666
+ test_strategy=TestStrategyConfig(),
667
+ testql=TestQLConfig()
668
+ )
669
+
670
+ watcher = WupWatcher(tmpdir, config=config)
671
+ watcher.dependency_mapper.service_to_endpoints["users"] = ["/api/users"]
672
+
673
+ # Python file should trigger
674
+ py_file = str(Path(tmpdir) / "app" / "users" / "routes.py")
675
+ watcher.on_file_change(py_file)
676
+ assert "users" in watcher.changed_services
677
+
678
+ # Markdown file should not trigger
679
+ watcher.changed_services.clear()
680
+ md_file = str(Path(tmpdir) / "app" / "users" / "README.md")
681
+ watcher.on_file_change(md_file)
682
+ assert "users" not in watcher.changed_services
683
+
684
+ def test_workflow_with_service_coincidence(self):
685
+ """Test workflow that detects service coincidences."""
686
+ with tempfile.TemporaryDirectory() as tmpdir:
687
+ service1 = ServiceConfig(
688
+ name="users-shell",
689
+ root="app/users-shell",
690
+ type="shell",
691
+ paths=[]
692
+ )
693
+ service2 = ServiceConfig(
694
+ name="users-web",
695
+ root="app/users-web",
696
+ type="web",
697
+ paths=[]
698
+ )
699
+ config = WupConfig(
700
+ project=ProjectConfig(name="test"),
701
+ watch=WatchConfig(),
702
+ services=[service1, service2],
703
+ test_strategy=TestStrategyConfig(),
704
+ testql=TestQLConfig()
705
+ )
706
+
707
+ watcher = WupWatcher(tmpdir, config=config)
708
+
709
+ # Detect coincidences
710
+ related = watcher.detect_service_coincidences("users-shell")
711
+ assert "users-web" in related
712
+
713
+ def test_workflow_with_multiple_file_changes(self):
714
+ """Test workflow with multiple rapid file changes."""
715
+ with tempfile.TemporaryDirectory() as tmpdir:
716
+ (Path(tmpdir) / "app").mkdir()
717
+
718
+ service = ServiceConfig(
719
+ name="users",
720
+ root="app/users",
721
+ paths=["app/users/**"]
722
+ )
723
+ config = WupConfig(
724
+ project=ProjectConfig(name="test"),
725
+ watch=WatchConfig(paths=["app/**"]),
726
+ services=[service],
727
+ test_strategy=TestStrategyConfig(),
728
+ testql=TestQLConfig()
729
+ )
730
+
731
+ watcher = WupWatcher(tmpdir, config=config)
732
+ watcher.dependency_mapper.service_to_endpoints["users"] = ["/api/users"]
733
+
734
+ # Multiple file changes for same service
735
+ files = [
736
+ str(Path(tmpdir) / "app" / "users" / "routes.py"),
737
+ str(Path(tmpdir) / "app" / "users" / "models.py"),
738
+ str(Path(tmpdir) / "app" / "users" / "schemas.py"),
739
+ ]
740
+
741
+ for file_path in files:
742
+ watcher.on_file_change(file_path)
743
+
744
+ # Service should be in changed_services
745
+ assert "users" in watcher.changed_services
746
+
747
+ # Multiple tests might be scheduled depending on debounce
748
+ # But service should be tracked
749
+ assert len(watcher.changed_services) == 1
750
+
751
+ def test_workflow_with_auto_detection_and_explicit_paths(self):
752
+ """Test workflow mixing auto-detection and explicit paths."""
753
+ with tempfile.TemporaryDirectory() as tmpdir:
754
+ (Path(tmpdir) / "app").mkdir()
755
+
756
+ service1 = ServiceConfig(
757
+ name="users-shell",
758
+ root="app/users-shell",
759
+ type="shell",
760
+ paths=[] # Auto-detection
761
+ )
762
+ service2 = ServiceConfig(
763
+ name="payments",
764
+ root="app/payments",
765
+ type="auto",
766
+ paths=["app/payments/**"] # Explicit paths
767
+ )
768
+ config = WupConfig(
769
+ project=ProjectConfig(name="test"),
770
+ watch=WatchConfig(),
771
+ services=[service1, service2],
772
+ test_strategy=TestStrategyConfig(),
773
+ testql=TestQLConfig()
774
+ )
775
+
776
+ watcher = WupWatcher(tmpdir, config=config)
777
+
778
+ # Auto-detection should match
779
+ inferred1 = watcher.infer_service(str(Path(tmpdir) / "app" / "users-shell" / "main.py"))
780
+ assert inferred1 == "users-shell"
781
+
782
+ # Explicit path should match
783
+ inferred2 = watcher.infer_service(str(Path(tmpdir) / "app" / "payments" / "routes.py"))
784
+ assert inferred2 == "payments"
227
785
 
228
786
 
229
787
  def test_import():
@@ -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.6"
10
+ __version__ = "0.2.7"
11
11
  __author__ = "Tom Sapletta"
12
12
 
13
13
  from .config import load_config, save_config, get_default_config
@@ -122,19 +122,26 @@ class WupWatcher:
122
122
  return svc.name
123
123
  else:
124
124
  # Auto-detect: check if service name appears in path
125
- service_name_parts = svc.name.replace("/", " ").replace("-", " ").split()
126
- for part in service_name_parts:
127
- if part.lower() in str(rel_path).lower():
128
- return svc.name
125
+ # Require the full service name to match as a complete segment
126
+ path_lower = str(rel_path).lower()
127
+ service_name_lower = svc.name.lower()
128
+
129
+ # Check if service name appears as a complete segment (separated by /, -, _, or .)
130
+ import re
131
+ pattern = r'(?:[\/\-_.]|^)' + re.escape(service_name_lower) + r'(?:[\/\-_.]|$)'
132
+ if re.search(pattern, path_lower):
133
+ return svc.name
129
134
 
130
135
  # Use dependency mapper if available
131
136
  service = self.dependency_mapper.get_service_for_file(file_path)
132
137
  if service:
133
138
  return service
134
139
 
135
- # Fallback: use first two meaningful parts
140
+ # Fallback: use first two meaningful parts (only if file exists)
136
141
  if len(parts) >= 2:
137
- return "/".join(parts[:2])
142
+ # Check if file exists (absolute path)
143
+ if Path(file_path).is_file():
144
+ return "/".join(parts[:2])
138
145
 
139
146
  return None
140
147
 
@@ -179,8 +186,9 @@ class WupWatcher:
179
186
  if self._services_share_domain(changed_service, svc.name):
180
187
  related_services.append(svc.name)
181
188
 
182
- # Coincidence: auto-detect by name similarity
183
- elif changed_svc_config.type == "auto" or svc.type == "auto":
189
+ # Coincidence: auto with shell/web (but not auto with auto)
190
+ elif (changed_svc_config.type == "auto" and svc.type != "auto") or \
191
+ (changed_svc_config.type != "auto" and svc.type == "auto"):
184
192
  if self._services_share_domain(changed_service, svc.name):
185
193
  related_services.append(svc.name)
186
194
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wup
3
- Version: 0.2.6
3
+ Version: 0.2.7
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
@@ -28,17 +28,17 @@ Dynamic: license-file
28
28
 
29
29
  ## AI Cost Tracking
30
30
 
31
- ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.6-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.05-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-2.0h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
31
+ ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.7-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.20-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-2.2h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
33
33
 
34
- - 🤖 **LLM usage:** $1.0500 (7 commits)
35
- - 👤 **Human dev:** ~$200 (2.0h @ $100/h, 30min dedup)
34
+ - 🤖 **LLM usage:** $1.2000 (8 commits)
35
+ - 👤 **Human dev:** ~$223 (2.2h @ $100/h, 30min dedup)
36
36
 
37
37
  Generated on 2026-04-29 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
38
38
 
39
39
  ---
40
40
 
41
- ![PyPI](https://img.shields.io/badge/pypi-wup-blue) ![Version](https://img.shields.io/badge/version-0.2.6-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
41
+ ![PyPI](https://img.shields.io/badge/pypi-wup-blue) ![Version](https://img.shields.io/badge/version-0.2.7-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
42
42
 
43
43
  **WUP (What's Up)** - Intelligent file watcher for regression testing in large projects.
44
44
 
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