wup 0.2.70__tar.gz → 0.2.71__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 (81) hide show
  1. {wup-0.2.70/wup.egg-info → wup-0.2.71}/PKG-INFO +6 -6
  2. {wup-0.2.70 → wup-0.2.71}/README.md +5 -5
  3. {wup-0.2.70 → wup-0.2.71}/pyproject.toml +1 -1
  4. wup-0.2.71/tests/test_genericity.py +84 -0
  5. {wup-0.2.70 → wup-0.2.71}/wup/__init__.py +1 -1
  6. {wup-0.2.70 → wup-0.2.71}/wup/config.py +4 -0
  7. {wup-0.2.70 → wup-0.2.71}/wup/dependency_mapper.py +56 -39
  8. {wup-0.2.70 → wup-0.2.71}/wup/models/config.py +6 -0
  9. {wup-0.2.70 → wup-0.2.71}/wup/monitoring_manifest.py +57 -15
  10. {wup-0.2.70 → wup-0.2.71/wup.egg-info}/PKG-INFO +6 -6
  11. {wup-0.2.70 → wup-0.2.71}/wup.egg-info/SOURCES.txt +1 -0
  12. {wup-0.2.70 → wup-0.2.71}/LICENSE +0 -0
  13. {wup-0.2.70 → wup-0.2.71}/setup.cfg +0 -0
  14. {wup-0.2.70 → wup-0.2.71}/tests/test_assistant.py +0 -0
  15. {wup-0.2.70 → wup-0.2.71}/tests/test_auto_detection.py +0 -0
  16. {wup-0.2.70 → wup-0.2.71}/tests/test_cli_bridge.py +0 -0
  17. {wup-0.2.70 → wup-0.2.71}/tests/test_cli_filtering.py +0 -0
  18. {wup-0.2.70 → wup-0.2.71}/tests/test_control.py +0 -0
  19. {wup-0.2.70 → wup-0.2.71}/tests/test_e2e.py +0 -0
  20. {wup-0.2.70 → wup-0.2.71}/tests/test_endpoints_init_cli.py +0 -0
  21. {wup-0.2.70 → wup-0.2.71}/tests/test_health_summary_passed.py +0 -0
  22. {wup-0.2.70 → wup-0.2.71}/tests/test_monitoring_manifest.py +0 -0
  23. {wup-0.2.70 → wup-0.2.71}/tests/test_multi_project.py +0 -0
  24. {wup-0.2.70 → wup-0.2.71}/tests/test_planfile_reporter_dedupe.py +0 -0
  25. {wup-0.2.70 → wup-0.2.71}/tests/test_probe_mutex.py +0 -0
  26. {wup-0.2.70 → wup-0.2.71}/tests/test_service_inference.py +0 -0
  27. {wup-0.2.70 → wup-0.2.71}/tests/test_status_data.py +0 -0
  28. {wup-0.2.70 → wup-0.2.71}/tests/test_sync.py +0 -0
  29. {wup-0.2.70 → wup-0.2.71}/tests/test_testql_monitor.py +0 -0
  30. {wup-0.2.70 → wup-0.2.71}/tests/test_testql_watcher.py +0 -0
  31. {wup-0.2.70 → wup-0.2.71}/tests/test_visual_diff_periodic_skip.py +0 -0
  32. {wup-0.2.70 → wup-0.2.71}/tests/test_visual_diff_progress.py +0 -0
  33. {wup-0.2.70 → wup-0.2.71}/tests/test_watch_exclude.py +0 -0
  34. {wup-0.2.70 → wup-0.2.71}/tests/test_watch_no_paths_exit.py +0 -0
  35. {wup-0.2.70 → wup-0.2.71}/tests/test_web_client.py +0 -0
  36. {wup-0.2.70 → wup-0.2.71}/tests/test_wup.py +0 -0
  37. {wup-0.2.70 → wup-0.2.71}/tests/test_wup_generate.py +0 -0
  38. {wup-0.2.70 → wup-0.2.71}/wup/_ast_detector.py +0 -0
  39. {wup-0.2.70 → wup-0.2.71}/wup/_base_detector.py +0 -0
  40. {wup-0.2.70 → wup-0.2.71}/wup/_hash_detector.py +0 -0
  41. {wup-0.2.70 → wup-0.2.71}/wup/_yaml_detector.py +0 -0
  42. {wup-0.2.70 → wup-0.2.71}/wup/anomaly_detector.py +0 -0
  43. {wup-0.2.70 → wup-0.2.71}/wup/anomaly_models.py +0 -0
  44. {wup-0.2.70 → wup-0.2.71}/wup/assistant.py +0 -0
  45. {wup-0.2.70 → wup-0.2.71}/wup/assistant_discovery.py +0 -0
  46. {wup-0.2.70 → wup-0.2.71}/wup/assistant_validator.py +0 -0
  47. {wup-0.2.70 → wup-0.2.71}/wup/bus.py +0 -0
  48. {wup-0.2.70 → wup-0.2.71}/wup/cli.py +0 -0
  49. {wup-0.2.70 → wup-0.2.71}/wup/cli_bridge.py +0 -0
  50. {wup-0.2.70 → wup-0.2.71}/wup/cli_config_generator.py +0 -0
  51. {wup-0.2.70 → wup-0.2.71}/wup/cli_scanner.py +0 -0
  52. {wup-0.2.70 → wup-0.2.71}/wup/control.py +0 -0
  53. {wup-0.2.70 → wup-0.2.71}/wup/core.py +0 -0
  54. {wup-0.2.70 → wup-0.2.71}/wup/endpoints.py +0 -0
  55. {wup-0.2.70 → wup-0.2.71}/wup/event_store.py +0 -0
  56. {wup-0.2.70 → wup-0.2.71}/wup/file_watcher/events/file_events.py +0 -0
  57. {wup-0.2.70 → wup-0.2.71}/wup/generate.py +0 -0
  58. {wup-0.2.70 → wup-0.2.71}/wup/init_cli.py +0 -0
  59. {wup-0.2.70 → wup-0.2.71}/wup/models/__init__.py +0 -0
  60. {wup-0.2.70 → wup-0.2.71}/wup/models/target.py +0 -0
  61. {wup-0.2.70 → wup-0.2.71}/wup/multi.py +0 -0
  62. {wup-0.2.70 → wup-0.2.71}/wup/paths.py +0 -0
  63. {wup-0.2.70 → wup-0.2.71}/wup/planfile_reporter.py +0 -0
  64. {wup-0.2.70 → wup-0.2.71}/wup/status_data.py +0 -0
  65. {wup-0.2.70 → wup-0.2.71}/wup/sync.py +0 -0
  66. {wup-0.2.70 → wup-0.2.71}/wup/testing/events/health_events.py +0 -0
  67. {wup-0.2.70 → wup-0.2.71}/wup/testing/events/test_results.py +0 -0
  68. {wup-0.2.70 → wup-0.2.71}/wup/testing/handlers/event_handlers.py +0 -0
  69. {wup-0.2.70 → wup-0.2.71}/wup/testing/handlers/health_handlers.py +0 -0
  70. {wup-0.2.70 → wup-0.2.71}/wup/testing/queries/health_queries.py +0 -0
  71. {wup-0.2.70 → wup-0.2.71}/wup/testql_cli_generator.py +0 -0
  72. {wup-0.2.70 → wup-0.2.71}/wup/testql_discovery.py +0 -0
  73. {wup-0.2.70 → wup-0.2.71}/wup/testql_monitor.py +0 -0
  74. {wup-0.2.70 → wup-0.2.71}/wup/testql_watcher.py +0 -0
  75. {wup-0.2.70 → wup-0.2.71}/wup/validate.py +0 -0
  76. {wup-0.2.70 → wup-0.2.71}/wup/visual_diff.py +0 -0
  77. {wup-0.2.70 → wup-0.2.71}/wup/web_client.py +0 -0
  78. {wup-0.2.70 → wup-0.2.71}/wup.egg-info/dependency_links.txt +0 -0
  79. {wup-0.2.70 → wup-0.2.71}/wup.egg-info/entry_points.txt +0 -0
  80. {wup-0.2.70 → wup-0.2.71}/wup.egg-info/requires.txt +0 -0
  81. {wup-0.2.70 → wup-0.2.71}/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.70
3
+ Version: 0.2.71
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,17 +42,17 @@ Dynamic: license-file
42
42
 
43
43
  ## AI Cost Tracking
44
44
 
45
- ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.70-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-$6.52-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-51.1h-blue) ![Model](https://img.shields.io/badge/Model-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.71-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-$7.22-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-52.1h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
47
47
 
48
- - 🤖 **LLM usage:** $6.5152 (98 commits)
49
- - 👤 **Human dev:** ~$5115 (51.1h @ $100/h, 30min dedup)
48
+ - 🤖 **LLM usage:** $7.2227 (99 commits)
49
+ - 👤 **Human dev:** ~$5215 (52.1h @ $100/h, 30min dedup)
50
50
 
51
51
  Generated on 2026-07-17 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
52
52
 
53
53
  ---
54
54
 
55
- ![PyPI](https://img.shields.io/badge/pypi-wup-blue) ![Version](https://img.shields.io/badge/version-0.2.70-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.71-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
56
56
 
57
57
  **WUP (What's Up)** - Intelligent file watcher for regression testing in large projects.
58
58
 
@@ -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.70-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-$6.52-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-51.1h-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.71-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-$7.22-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-52.1h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
8
8
 
9
- - 🤖 **LLM usage:** $6.5152 (98 commits)
10
- - 👤 **Human dev:** ~$5115 (51.1h @ $100/h, 30min dedup)
9
+ - 🤖 **LLM usage:** $7.2227 (99 commits)
10
+ - 👤 **Human dev:** ~$5215 (52.1h @ $100/h, 30min dedup)
11
11
 
12
12
  Generated on 2026-07-17 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.70-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.71-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.70"
7
+ version = "0.2.71"
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"
@@ -0,0 +1,84 @@
1
+ """Tests for language-agnostic deps.json discovery and config-driven docker mapping."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ from wup.config import validate_config
8
+ from wup.dependency_mapper import DependencyMapper
9
+ from wup.monitoring_manifest import DockerComposeService, _map_docker_to_wup_service
10
+
11
+
12
+ def _dc(compose: str, container: str = "") -> DockerComposeService:
13
+ return DockerComposeService(compose_service=compose, container_name=container)
14
+
15
+
16
+ # --- deps.json genericity -------------------------------------------------
17
+
18
+ def test_express_ts_endpoints_discovered(tmp_path: Path) -> None:
19
+ """A TypeScript Express project yields a non-empty deps map (regression: rglob brace bug)."""
20
+ svc = tmp_path / "services" / "users"
21
+ svc.mkdir(parents=True)
22
+ (svc / "routes.ts").write_text('router.get("/api/users", h)\nrouter.post("/api/users", h)\n')
23
+
24
+ mapper = DependencyMapper(str(tmp_path))
25
+ result = mapper.build_from_codebase()
26
+
27
+ assert mapper._detect_framework() == "express"
28
+ assert "services/users" in result["services"]
29
+ assert "/api/users" in result["services"]["services/users"]["endpoints"]
30
+
31
+
32
+ def test_infer_service_uses_services_dir(tmp_path: Path) -> None:
33
+ mapper = DependencyMapper(str(tmp_path))
34
+ assert mapper._infer_service("services/payments/handler.ts") == "services/payments"
35
+
36
+
37
+ def test_to_dict_handles_service_without_endpoints(tmp_path: Path) -> None:
38
+ """Services with files but no endpoints must not corrupt the map (regression: fragile zip)."""
39
+ mapper = DependencyMapper(str(tmp_path))
40
+ mapper.service_to_files["orphan"].add("a.py")
41
+ mapper.service_to_endpoints["web"].append("/x")
42
+
43
+ d = mapper.to_dict()
44
+ assert d["services"]["orphan"]["files"] == ["a.py"]
45
+ assert d["services"]["orphan"]["endpoints"] == []
46
+ assert d["services"]["web"]["endpoints"] == ["/x"]
47
+
48
+
49
+ # --- config-driven docker mapping ----------------------------------------
50
+
51
+ def test_docker_map_default_is_generic() -> None:
52
+ """Without a profile, project-specific fleet literals do not auto-map."""
53
+ services = ["backend", "frontend", "payments"]
54
+ # 'connect-scenario' carries no generic token for these services -> unmapped.
55
+ assert _map_docker_to_wup_service(_dc("connect-scenario"), services) is None
56
+
57
+
58
+ def test_docker_map_connect_profile_opt_in() -> None:
59
+ services = ["backend", "frontend"]
60
+ assert _map_docker_to_wup_service(_dc("connect-scenario"), services, profile="connect") == "backend"
61
+
62
+
63
+ def test_docker_map_user_rules() -> None:
64
+ services = ["payments", "web"]
65
+ got = _map_docker_to_wup_service(_dc("stripe-worker"), services, user_map={"stripe": "payments"})
66
+ assert got == "payments"
67
+
68
+
69
+ def test_docker_map_generic_token_match() -> None:
70
+ services = ["payments", "web"]
71
+ assert _map_docker_to_wup_service(_dc("payments-api"), services) == "payments"
72
+
73
+
74
+ def test_config_roundtrips_docker_service_map() -> None:
75
+ raw = {
76
+ "project": {"name": "p"},
77
+ "testql": {
78
+ "docker_service_map": {"stripe": "payments"},
79
+ "service_map_profile": "connect",
80
+ },
81
+ }
82
+ cfg = validate_config(raw)
83
+ assert cfg.testql.docker_service_map == {"stripe": "payments"}
84
+ assert cfg.testql.service_map_profile == "connect"
@@ -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.70"
10
+ __version__ = "0.2.71"
11
11
  __author__ = "Tom Sapletta"
12
12
 
13
13
  from .config import load_config, save_config, get_default_config
@@ -230,6 +230,8 @@ def _parse_testql_config(raw: dict) -> TestQLConfig:
230
230
  health_scenario_strict=bool(testql_raw.get("health_scenario_strict", False)),
231
231
  quick_smoke_only=bool(testql_raw.get("quick_smoke_only", False)),
232
232
  service_map_globs=testql_raw.get("service_map_globs", []),
233
+ docker_service_map=testql_raw.get("docker_service_map", {}),
234
+ service_map_profile=testql_raw.get("service_map_profile", ""),
233
235
  base_url=testql_raw.get("base_url", ""),
234
236
  api_base_url=testql_raw.get("api_base_url", ""),
235
237
  base_url_env=testql_raw.get("base_url_env", "WUP_BASE_URL"),
@@ -493,6 +495,8 @@ def save_config(config: WupConfig, output_path: Path):
493
495
  "health_scenario": config.testql.health_scenario,
494
496
  "health_scenario_strict": config.testql.health_scenario_strict,
495
497
  "service_map_globs": config.testql.service_map_globs,
498
+ "docker_service_map": config.testql.docker_service_map,
499
+ "service_map_profile": config.testql.service_map_profile,
496
500
  "base_url": config.testql.base_url,
497
501
  "api_base_url": config.testql.api_base_url,
498
502
  "base_url_env": config.testql.base_url_env,
@@ -11,11 +11,24 @@ This enables intelligent testing by only testing related services when files cha
11
11
 
12
12
  import json
13
13
  from pathlib import Path
14
- from typing import Dict, List, Set, Optional
14
+ from typing import Dict, Iterable, Iterator, List, Set, Optional
15
15
  from collections import defaultdict
16
16
  import re
17
17
  from .testql_discovery import TestQLEndpointDiscovery
18
18
 
19
+ # Source file extensions scanned when discovering endpoints / detecting the
20
+ # framework. Kept broad so JS/TS projects are covered, not just Python.
21
+ PY_EXTENSIONS = (".py",)
22
+ JS_EXTENSIONS = (".js", ".ts", ".jsx", ".tsx", ".mjs", ".cjs")
23
+ SOURCE_EXTENSIONS = PY_EXTENSIONS + JS_EXTENSIONS
24
+
25
+ # Directory names that commonly sit directly above a service/module. Used by
26
+ # _infer_service to derive a service name from a file path in a language-agnostic
27
+ # way (Python `app/`, Node `services/`, monorepo `packages/`, …).
28
+ SERVICE_ROOT_DIRS = (
29
+ "app", "src", "services", "packages", "lib", "modules", "apps", "cmd", "internal", "pkg",
30
+ )
31
+
19
32
 
20
33
  class DependencyMapper:
21
34
  """Maps project dependencies for intelligent testing."""
@@ -66,27 +79,35 @@ class DependencyMapper:
66
79
 
67
80
  def _detect_framework(self) -> str:
68
81
  """Detect the web framework used in the project."""
69
- # Check for common framework indicators
82
+ # Indicators are language-scoped so Python patterns are only matched in
83
+ # Python files and JS patterns only in JS/TS files — otherwise a frontend
84
+ # file that merely mentions e.g. "Blueprint" would false-positive as Flask.
70
85
  indicators = {
71
- "fastapi": ["FastAPI", "APIRouter", "from fastapi"],
72
- "flask": ["Flask", "Blueprint", "from flask"],
73
- "django": ["urlpatterns", "path(", "from django.urls"],
74
- "express": ["express()", "app.get(", "router."],
86
+ "fastapi": (["FastAPI", "APIRouter", "from fastapi"], PY_EXTENSIONS),
87
+ "flask": (["Flask", "Blueprint", "from flask"], PY_EXTENSIONS),
88
+ "django": (["urlpatterns", "from django.urls"], PY_EXTENSIONS),
89
+ "express": (["express()", "app.get(", "router.get(", "router.post("], JS_EXTENSIONS),
75
90
  }
76
-
77
- for framework, patterns in indicators.items():
91
+
92
+ for framework, (patterns, extensions) in indicators.items():
78
93
  for pattern in patterns:
79
- if self._search_codebase(pattern):
94
+ if self._search_codebase(pattern, extensions):
80
95
  return framework
81
-
96
+
82
97
  return "generic"
83
98
 
84
- def _search_codebase(self, pattern: str) -> bool:
85
- """Search for a pattern in the codebase."""
86
- for py_file in self.project_root.rglob("*.py"):
99
+ def _iter_source_files(self, extensions: Iterable[str]) -> Iterator[Path]:
100
+ """Yield source files with any of the given extensions (recursively)."""
101
+ wanted = tuple(extensions)
102
+ for path in self.project_root.rglob("*"):
103
+ if path.is_file() and path.suffix in wanted:
104
+ yield path
105
+
106
+ def _search_codebase(self, pattern: str, extensions: Iterable[str] = SOURCE_EXTENSIONS) -> bool:
107
+ """Search for a pattern across source files of the given extensions."""
108
+ for source_file in self._iter_source_files(extensions):
87
109
  try:
88
- content = py_file.read_text()
89
- if pattern in content:
110
+ if pattern in source_file.read_text():
90
111
  return True
91
112
  except (UnicodeDecodeError, PermissionError):
92
113
  continue
@@ -106,8 +127,8 @@ class DependencyMapper:
106
127
  def _scan_python_endpoints(self, framework: str) -> List[Dict]:
107
128
  """Scan Python files for endpoint definitions."""
108
129
  endpoints = []
109
-
110
- for py_file in self.project_root.rglob("*.py"):
130
+
131
+ for py_file in self._iter_source_files(PY_EXTENSIONS):
111
132
  try:
112
133
  content = py_file.read_text()
113
134
  rel_path = str(py_file.relative_to(self.project_root))
@@ -154,8 +175,9 @@ class DependencyMapper:
154
175
  def _scan_js_endpoints(self) -> List[Dict]:
155
176
  """Scan JavaScript/TypeScript files for endpoint definitions."""
156
177
  endpoints = []
157
-
158
- for js_file in self.project_root.rglob("*.{js,ts,jsx,tsx}"):
178
+
179
+ # NOTE: pathlib.rglob does not brace-expand, so we iterate real extensions.
180
+ for js_file in self._iter_source_files(JS_EXTENSIONS):
159
181
  try:
160
182
  content = js_file.read_text()
161
183
  rel_path = str(js_file.relative_to(self.project_root))
@@ -185,22 +207,19 @@ class DependencyMapper:
185
207
  src/components/auth.ts → "components/auth"
186
208
  """
187
209
  parts = Path(file_path).parts
188
-
189
- # Common patterns
190
- if "app" in parts:
191
- idx = parts.index("app")
192
- if idx + 1 < len(parts):
193
- return "/".join(parts[idx:idx+2])
194
-
195
- if "src" in parts:
196
- idx = parts.index("src")
197
- if idx + 1 < len(parts):
198
- return "/".join(parts[idx:idx+2])
199
-
210
+
211
+ # Derive the service from the segment following a known service-root dir
212
+ # (app/, src/, services/, packages/, …) — language-agnostic.
213
+ for root in SERVICE_ROOT_DIRS:
214
+ if root in parts:
215
+ idx = parts.index(root)
216
+ if idx + 1 < len(parts):
217
+ return "/".join(parts[idx:idx + 2])
218
+
200
219
  # Fallback: use first two meaningful parts
201
220
  if len(parts) >= 2:
202
221
  return "/".join(parts[:2])
203
-
222
+
204
223
  return None
205
224
 
206
225
  def get_endpoints_for_file(self, file_path: str) -> List[str]:
@@ -228,18 +247,16 @@ class DependencyMapper:
228
247
 
229
248
  def to_dict(self) -> Dict:
230
249
  """Convert the dependency map to a dictionary."""
250
+ services = sorted(set(self.service_to_endpoints) | set(self.service_to_files))
231
251
  return {
232
252
  "services": {
233
253
  service: {
234
- "endpoints": endpoints,
235
- "files": list(files)
254
+ "endpoints": self.service_to_endpoints.get(service, []),
255
+ "files": sorted(self.service_to_files.get(service, set())),
236
256
  }
237
- for service, (endpoints, files) in zip(
238
- self.service_to_endpoints.keys(),
239
- zip(self.service_to_endpoints.values(), self.service_to_files.values())
240
- )
257
+ for service in services
241
258
  },
242
- "files": dict(self.file_to_endpoints)
259
+ "files": dict(self.file_to_endpoints),
243
260
  }
244
261
 
245
262
  def save(self, output_path: str = "deps.json"):
@@ -69,6 +69,12 @@ class TestQLConfig:
69
69
  health_scenario_strict: bool = False # If false, fleet scenario failure is logged but does not block per-service probes
70
70
  quick_smoke_only: bool = False # Quick TestQL uses smoke_scenario only (skip auto-api scoring)
71
71
  service_map_globs: List[str] = field(default_factory=list) # e.g. testql-testing/service-map/*.yaml
72
+ # docker-compose service/container substring → WUP service name. Lets a project
73
+ # map its containers to services without hardcoding names in WUP itself.
74
+ docker_service_map: Dict[str, str] = field(default_factory=dict)
75
+ # Optional built-in mapping profile (e.g. "connect") applied after
76
+ # docker_service_map and before generic token matching. Empty = generic only.
77
+ service_map_profile: str = ""
72
78
  base_url: str = ""
73
79
  api_base_url: str = "" # Core API (c2004: http://localhost:8101) — used for backend probes
74
80
  base_url_env: str = "WUP_BASE_URL"
@@ -124,13 +124,13 @@ def _host_port_from_mapping(mapping: str) -> Optional[int]:
124
124
  return None
125
125
 
126
126
 
127
- def _map_docker_to_wup_service(
128
- docker: DockerComposeService,
129
- wup_services: Sequence[str],
130
- ) -> Optional[str]:
131
- name = docker.compose_service.lower()
132
- container = docker.container_name.lower()
127
+ def _connect_profile_rules(name: str, container: str, wup_services: Sequence[str]) -> List[Any]:
128
+ """
129
+ Built-in "connect" mapping profile (maskservice / c2004 fleet).
133
130
 
131
+ Opt-in via ``testql.service_map_profile: connect``. Kept out of the default
132
+ path so WUP carries no project-specific service names unless asked.
133
+ """
134
134
  def _connect_backend_target() -> Optional[str]:
135
135
  if not (name.endswith("-backend") and "connect" in name):
136
136
  return None
@@ -141,20 +141,53 @@ def _map_docker_to_wup_service(
141
141
  return "backend"
142
142
  return None
143
143
 
144
- # Each rule returns a candidate WUP service name (or None); the first
145
- # candidate that names a configured service wins.
146
- rules = [
144
+ return [
147
145
  lambda: "firmware" if ("firmware" in name or "firmware" in container) else None,
148
146
  lambda: "frontend" if (name == "frontend" or "frontend" in container) else None,
149
147
  lambda: "backend" if (name == "backend" or container == "identification-backend") else None,
150
148
  lambda: "backend" if ("connect-scenario" in name and "backend" not in name) else None,
151
149
  _connect_backend_target,
152
150
  ]
153
- for rule in rules:
154
- candidate = rule()
155
- if candidate and candidate in wup_services:
156
- return candidate
157
151
 
152
+
153
+ _BUILTIN_SERVICE_MAP_PROFILES = {
154
+ "connect": _connect_profile_rules,
155
+ }
156
+
157
+
158
+ def _map_docker_to_wup_service(
159
+ docker: DockerComposeService,
160
+ wup_services: Sequence[str],
161
+ *,
162
+ user_map: Optional[Dict[str, str]] = None,
163
+ profile: str = "",
164
+ ) -> Optional[str]:
165
+ """
166
+ Map a docker-compose service to a configured WUP service.
167
+
168
+ Resolution order (first configured match wins):
169
+ 1. user rules from ``testql.docker_service_map`` (substring → service),
170
+ 2. an opt-in built-in ``profile`` (e.g. "connect"),
171
+ 3. generic token matching (service name appears in compose/container name).
172
+ """
173
+ name = docker.compose_service.lower()
174
+ container = docker.container_name.lower()
175
+
176
+ # 1. User-defined substring rules (fully project-agnostic).
177
+ for needle, target in (user_map or {}).items():
178
+ if needle.lower() in name or needle.lower() in container:
179
+ if target in wup_services:
180
+ return target
181
+
182
+ # 2. Opt-in built-in profile rules.
183
+ profile_factory = _BUILTIN_SERVICE_MAP_PROFILES.get(profile.lower()) if profile else None
184
+ if profile_factory:
185
+ for rule in profile_factory(name, container, wup_services):
186
+ candidate = rule()
187
+ if candidate and candidate in wup_services:
188
+ return candidate
189
+
190
+ # 3. Generic token matching.
158
191
  for svc in wup_services:
159
192
  token = svc.lower().replace("_", "-")
160
193
  if token in name or token in container:
@@ -188,6 +221,9 @@ def _build_docker_rows(
188
221
  docker_all: List[DockerComposeService],
189
222
  wup_names: List[str],
190
223
  by_wup: Dict[str, Dict[str, Any]],
224
+ *,
225
+ user_map: Optional[Dict[str, str]] = None,
226
+ profile: str = "",
191
227
  ) -> List[Dict[str, Any]]:
192
228
  """Group docker-compose rows under WUP services; return unmapped leftovers."""
193
229
  unmapped: List[Dict[str, Any]] = []
@@ -202,7 +238,7 @@ def _build_docker_rows(
202
238
  if d.healthcheck_test:
203
239
  row["compose_healthcheck"] = d.healthcheck_test
204
240
 
205
- mapped = _map_docker_to_wup_service(d, wup_names)
241
+ mapped = _map_docker_to_wup_service(d, wup_names, user_map=user_map, profile=profile)
206
242
  if mapped:
207
243
  by_wup[mapped]["docker"].append(row)
208
244
  else:
@@ -288,7 +324,13 @@ def build_monitoring_manifest(project_root: Path, config: WupConfig) -> Dict[str
288
324
  docker_all = discover_docker_compose_services(project_root)
289
325
 
290
326
  by_wup = _build_wup_service_dicts(config)
291
- unmapped_docker = _build_docker_rows(docker_all, wup_names, by_wup)
327
+ unmapped_docker = _build_docker_rows(
328
+ docker_all,
329
+ wup_names,
330
+ by_wup,
331
+ user_map=config.testql.docker_service_map,
332
+ profile=config.testql.service_map_profile,
333
+ )
292
334
 
293
335
  # Live HTTP probes (exactly what WUP will call)
294
336
  for svc_name in wup_names:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wup
3
- Version: 0.2.70
3
+ Version: 0.2.71
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,17 +42,17 @@ Dynamic: license-file
42
42
 
43
43
  ## AI Cost Tracking
44
44
 
45
- ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.70-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-$6.52-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-51.1h-blue) ![Model](https://img.shields.io/badge/Model-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.71-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-$7.22-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-52.1h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
47
47
 
48
- - 🤖 **LLM usage:** $6.5152 (98 commits)
49
- - 👤 **Human dev:** ~$5115 (51.1h @ $100/h, 30min dedup)
48
+ - 🤖 **LLM usage:** $7.2227 (99 commits)
49
+ - 👤 **Human dev:** ~$5215 (52.1h @ $100/h, 30min dedup)
50
50
 
51
51
  Generated on 2026-07-17 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
52
52
 
53
53
  ---
54
54
 
55
- ![PyPI](https://img.shields.io/badge/pypi-wup-blue) ![Version](https://img.shields.io/badge/version-0.2.70-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.71-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
56
56
 
57
57
  **WUP (What's Up)** - Intelligent file watcher for regression testing in large projects.
58
58
 
@@ -8,6 +8,7 @@ tests/test_cli_filtering.py
8
8
  tests/test_control.py
9
9
  tests/test_e2e.py
10
10
  tests/test_endpoints_init_cli.py
11
+ tests/test_genericity.py
11
12
  tests/test_health_summary_passed.py
12
13
  tests/test_monitoring_manifest.py
13
14
  tests/test_multi_project.py
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
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