wup 0.2.73__tar.gz → 0.2.75__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 (88) hide show
  1. {wup-0.2.73/wup.egg-info → wup-0.2.75}/PKG-INFO +6 -6
  2. {wup-0.2.73 → wup-0.2.75}/README.md +5 -5
  3. {wup-0.2.73 → wup-0.2.75}/pyproject.toml +1 -1
  4. wup-0.2.75/tests/test_aql.py +120 -0
  5. wup-0.2.75/tests/test_discovery_adapters.py +118 -0
  6. {wup-0.2.73 → wup-0.2.75}/tests/test_genericity.py +25 -0
  7. wup-0.2.75/tests/test_oql.py +104 -0
  8. {wup-0.2.73 → wup-0.2.75}/tests/test_testql_monitor.py +7 -2
  9. {wup-0.2.73 → wup-0.2.75}/wup/__init__.py +1 -1
  10. wup-0.2.75/wup/aql.py +306 -0
  11. {wup-0.2.73 → wup-0.2.75}/wup/cli.py +105 -0
  12. {wup-0.2.73 → wup-0.2.75}/wup/config.py +2 -0
  13. wup-0.2.75/wup/dependency_mapper.py +177 -0
  14. {wup-0.2.73 → wup-0.2.75}/wup/discovery.py +11 -7
  15. {wup-0.2.73 → wup-0.2.75}/wup/models/config.py +4 -0
  16. wup-0.2.75/wup/oql.py +267 -0
  17. {wup-0.2.73 → wup-0.2.75}/wup/testql_monitor.py +40 -33
  18. {wup-0.2.73 → wup-0.2.75/wup.egg-info}/PKG-INFO +6 -6
  19. {wup-0.2.73 → wup-0.2.75}/wup.egg-info/SOURCES.txt +5 -0
  20. wup-0.2.73/wup/dependency_mapper.py +0 -301
  21. {wup-0.2.73 → wup-0.2.75}/LICENSE +0 -0
  22. {wup-0.2.73 → wup-0.2.75}/setup.cfg +0 -0
  23. {wup-0.2.73 → wup-0.2.75}/tests/test_assistant.py +0 -0
  24. {wup-0.2.73 → wup-0.2.75}/tests/test_auto_detection.py +0 -0
  25. {wup-0.2.73 → wup-0.2.75}/tests/test_cli_bridge.py +0 -0
  26. {wup-0.2.73 → wup-0.2.75}/tests/test_cli_filtering.py +0 -0
  27. {wup-0.2.73 → wup-0.2.75}/tests/test_control.py +0 -0
  28. {wup-0.2.73 → wup-0.2.75}/tests/test_e2e.py +0 -0
  29. {wup-0.2.73 → wup-0.2.75}/tests/test_endpoints_init_cli.py +0 -0
  30. {wup-0.2.73 → wup-0.2.75}/tests/test_health_summary_passed.py +0 -0
  31. {wup-0.2.73 → wup-0.2.75}/tests/test_monitoring_manifest.py +0 -0
  32. {wup-0.2.73 → wup-0.2.75}/tests/test_multi_project.py +0 -0
  33. {wup-0.2.73 → wup-0.2.75}/tests/test_planfile_reporter_dedupe.py +0 -0
  34. {wup-0.2.73 → wup-0.2.75}/tests/test_probe_mutex.py +0 -0
  35. {wup-0.2.73 → wup-0.2.75}/tests/test_service_inference.py +0 -0
  36. {wup-0.2.73 → wup-0.2.75}/tests/test_status_data.py +0 -0
  37. {wup-0.2.73 → wup-0.2.75}/tests/test_sync.py +0 -0
  38. {wup-0.2.73 → wup-0.2.75}/tests/test_testql_watcher.py +0 -0
  39. {wup-0.2.73 → wup-0.2.75}/tests/test_visual_diff_periodic_skip.py +0 -0
  40. {wup-0.2.73 → wup-0.2.75}/tests/test_visual_diff_progress.py +0 -0
  41. {wup-0.2.73 → wup-0.2.75}/tests/test_watch_exclude.py +0 -0
  42. {wup-0.2.73 → wup-0.2.75}/tests/test_watch_no_paths_exit.py +0 -0
  43. {wup-0.2.73 → wup-0.2.75}/tests/test_web_client.py +0 -0
  44. {wup-0.2.73 → wup-0.2.75}/tests/test_wup.py +0 -0
  45. {wup-0.2.73 → wup-0.2.75}/tests/test_wup_generate.py +0 -0
  46. {wup-0.2.73 → wup-0.2.75}/wup/_ast_detector.py +0 -0
  47. {wup-0.2.73 → wup-0.2.75}/wup/_base_detector.py +0 -0
  48. {wup-0.2.73 → wup-0.2.75}/wup/_hash_detector.py +0 -0
  49. {wup-0.2.73 → wup-0.2.75}/wup/_yaml_detector.py +0 -0
  50. {wup-0.2.73 → wup-0.2.75}/wup/anomaly_detector.py +0 -0
  51. {wup-0.2.73 → wup-0.2.75}/wup/anomaly_models.py +0 -0
  52. {wup-0.2.73 → wup-0.2.75}/wup/assistant.py +0 -0
  53. {wup-0.2.73 → wup-0.2.75}/wup/assistant_discovery.py +0 -0
  54. {wup-0.2.73 → wup-0.2.75}/wup/assistant_validator.py +0 -0
  55. {wup-0.2.73 → wup-0.2.75}/wup/bus.py +0 -0
  56. {wup-0.2.73 → wup-0.2.75}/wup/cli_bridge.py +0 -0
  57. {wup-0.2.73 → wup-0.2.75}/wup/cli_config_generator.py +0 -0
  58. {wup-0.2.73 → wup-0.2.75}/wup/cli_scanner.py +0 -0
  59. {wup-0.2.73 → wup-0.2.75}/wup/control.py +0 -0
  60. {wup-0.2.73 → wup-0.2.75}/wup/core.py +0 -0
  61. {wup-0.2.73 → wup-0.2.75}/wup/endpoints.py +0 -0
  62. {wup-0.2.73 → wup-0.2.75}/wup/event_store.py +0 -0
  63. {wup-0.2.73 → wup-0.2.75}/wup/file_watcher/events/file_events.py +0 -0
  64. {wup-0.2.73 → wup-0.2.75}/wup/generate.py +0 -0
  65. {wup-0.2.73 → wup-0.2.75}/wup/init_cli.py +0 -0
  66. {wup-0.2.73 → wup-0.2.75}/wup/models/__init__.py +0 -0
  67. {wup-0.2.73 → wup-0.2.75}/wup/models/target.py +0 -0
  68. {wup-0.2.73 → wup-0.2.75}/wup/monitoring_manifest.py +0 -0
  69. {wup-0.2.73 → wup-0.2.75}/wup/multi.py +0 -0
  70. {wup-0.2.73 → wup-0.2.75}/wup/paths.py +0 -0
  71. {wup-0.2.73 → wup-0.2.75}/wup/planfile_reporter.py +0 -0
  72. {wup-0.2.73 → wup-0.2.75}/wup/status_data.py +0 -0
  73. {wup-0.2.73 → wup-0.2.75}/wup/sync.py +0 -0
  74. {wup-0.2.73 → wup-0.2.75}/wup/testing/events/health_events.py +0 -0
  75. {wup-0.2.73 → wup-0.2.75}/wup/testing/events/test_results.py +0 -0
  76. {wup-0.2.73 → wup-0.2.75}/wup/testing/handlers/event_handlers.py +0 -0
  77. {wup-0.2.73 → wup-0.2.75}/wup/testing/handlers/health_handlers.py +0 -0
  78. {wup-0.2.73 → wup-0.2.75}/wup/testing/queries/health_queries.py +0 -0
  79. {wup-0.2.73 → wup-0.2.75}/wup/testql_cli_generator.py +0 -0
  80. {wup-0.2.73 → wup-0.2.75}/wup/testql_discovery.py +0 -0
  81. {wup-0.2.73 → wup-0.2.75}/wup/testql_watcher.py +0 -0
  82. {wup-0.2.73 → wup-0.2.75}/wup/validate.py +0 -0
  83. {wup-0.2.73 → wup-0.2.75}/wup/visual_diff.py +0 -0
  84. {wup-0.2.73 → wup-0.2.75}/wup/web_client.py +0 -0
  85. {wup-0.2.73 → wup-0.2.75}/wup.egg-info/dependency_links.txt +0 -0
  86. {wup-0.2.73 → wup-0.2.75}/wup.egg-info/entry_points.txt +0 -0
  87. {wup-0.2.73 → wup-0.2.75}/wup.egg-info/requires.txt +0 -0
  88. {wup-0.2.73 → wup-0.2.75}/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.73
3
+ Version: 0.2.75
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.73-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.24-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-52.4h-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.75-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.97-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-52.9h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
47
47
 
48
- - 🤖 **LLM usage:** $7.2381 (101 commits)
49
- - 👤 **Human dev:** ~$5241 (52.4h @ $100/h, 30min dedup)
48
+ - 🤖 **LLM usage:** $6.9692 (104 commits)
49
+ - 👤 **Human dev:** ~$5288 (52.9h @ $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.73-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.75-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.73-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.24-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-52.4h-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.75-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.97-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-52.9h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
8
8
 
9
- - 🤖 **LLM usage:** $7.2381 (101 commits)
10
- - 👤 **Human dev:** ~$5241 (52.4h @ $100/h, 30min dedup)
9
+ - 🤖 **LLM usage:** $6.9692 (104 commits)
10
+ - 👤 **Human dev:** ~$5288 (52.9h @ $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.73-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.75-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.73"
7
+ version = "0.2.75"
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,120 @@
1
+ """Tests for AQL — the Assertion Query Language."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+
8
+ import pytest
9
+
10
+ from wup.aql import AQLEngine, AQLError, CheckAQL, parse_rule, register_aql
11
+ from wup.bus import EventBus
12
+
13
+
14
+ def _sample(tmp_path: Path) -> Path:
15
+ f = tmp_path / "sample.json"
16
+ f.write_text(json.dumps({
17
+ "name": "subactor",
18
+ "version": "1.2.0",
19
+ "services": ["a", "b"],
20
+ "testql": {"probe_interval_s": 60},
21
+ "flag": True,
22
+ "empty": None,
23
+ }))
24
+ return f
25
+
26
+
27
+ def _check(tmp_path: Path, rule: str) -> bool:
28
+ """True when the rule passes (no violations)."""
29
+ return not AQLEngine(tmp_path).check_file(_sample(tmp_path), [rule])
30
+
31
+
32
+ # --- parsing --------------------------------------------------------------
33
+
34
+ def test_parse_exists() -> None:
35
+ r = parse_rule("json .version exists")
36
+ assert (r.selector, r.path, r.op) == ("json", ".version", "exists")
37
+
38
+
39
+ def test_parse_length_and_severity() -> None:
40
+ r = parse_rule("json .services length > 0 severity high")
41
+ assert (r.op, r.length_op, r.value, r.severity) == ("length", ">", "0", "high")
42
+
43
+
44
+ @pytest.mark.parametrize("bad", [
45
+ "", "widget .x exists", "json", "json .x", "json .x bogus",
46
+ "json .x length foo", "json .x type banana", "json .x = ",
47
+ "json .x severity nope",
48
+ ])
49
+ def test_parse_errors(bad: str) -> None:
50
+ with pytest.raises(AQLError):
51
+ parse_rule(bad)
52
+
53
+
54
+ # --- evaluation -----------------------------------------------------------
55
+
56
+ @pytest.mark.parametrize("rule", [
57
+ "json .version exists",
58
+ "json .missing missing",
59
+ "json .services length > 0",
60
+ "json .services length = 2",
61
+ "json .testql.probe_interval_s >= 60",
62
+ "json .name = subactor",
63
+ "json .name ~ sub",
64
+ "json .name !~ zzz",
65
+ "json .version matches ^\\d+\\.\\d+",
66
+ "json .services type array",
67
+ "json .testql type object",
68
+ "json .flag type bool",
69
+ "json .name type string",
70
+ "json .testql.probe_interval_s type number",
71
+ "keys .testql ~ probe_interval_s",
72
+ "keys .testql length = 1",
73
+ "text ~ subactor",
74
+ ])
75
+ def test_passing_rules(tmp_path: Path, rule: str) -> None:
76
+ assert _check(tmp_path, rule)
77
+
78
+
79
+ @pytest.mark.parametrize("rule", [
80
+ "json .missing exists",
81
+ "json .version missing",
82
+ "json .services length > 5",
83
+ "json .name = other",
84
+ "json .name ~ zzz",
85
+ "json .version matches ^zzz",
86
+ "json .services type object",
87
+ "json .missing type string",
88
+ "keys .testql ~ nope",
89
+ "text ~ nonexistent-token",
90
+ ])
91
+ def test_failing_rules(tmp_path: Path, rule: str) -> None:
92
+ assert not _check(tmp_path, rule)
93
+
94
+
95
+ def test_violation_carries_severity(tmp_path: Path) -> None:
96
+ v = AQLEngine(tmp_path).check_file(_sample(tmp_path), ["json .missing exists severity critical"])
97
+ assert len(v) == 1 and v[0].severity == "critical" and v[0].detector == "aql"
98
+
99
+
100
+ def test_nested_and_indexed_paths(tmp_path: Path) -> None:
101
+ assert _check(tmp_path, "json .services[0] = a")
102
+ assert _check(tmp_path, "json .services[1] = b")
103
+
104
+
105
+ def test_missing_file(tmp_path: Path) -> None:
106
+ v = AQLEngine(tmp_path).check_file(tmp_path / "nope.json", ["json .x exists"])
107
+ assert len(v) == 1 and v[0].anomaly_type == "error"
108
+
109
+
110
+ def test_yaml_file(tmp_path: Path) -> None:
111
+ y = tmp_path / "wup.yaml"
112
+ y.write_text("project:\n name: demo\ntestql:\n probe_interval_s: 0\n")
113
+ assert not AQLEngine(tmp_path).check_file(y, ["yaml .project.name = demo", "yaml .testql.probe_interval_s >= 0"])
114
+
115
+
116
+ def test_bus_integration(tmp_path: Path) -> None:
117
+ bus = EventBus()
118
+ register_aql(bus, tmp_path)
119
+ result = bus.query(CheckAQL(file=str(_sample(tmp_path)), rules=["json .version exists"]))
120
+ assert result == [] # no violations
@@ -0,0 +1,118 @@
1
+ """Tests for the pluggable endpoint-discovery adapters."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ import pytest
8
+
9
+ from wup.dependency_mapper import DependencyMapper
10
+ from wup.discovery import SourceIndex, detect_frameworks, discover_endpoints
11
+
12
+ # (framework, filename, source) fixtures — one per adapter.
13
+ FIXTURES = {
14
+ "fastapi": (
15
+ "api.py",
16
+ "from fastapi import APIRouter\nrouter = APIRouter()\n"
17
+ '@router.get("/users")\ndef u(): ...\n@router.post("/users")\ndef c(): ...\n',
18
+ {"/users"},
19
+ ),
20
+ "flask": (
21
+ "views.py",
22
+ "from flask import Blueprint\nbp = Blueprint('a', __name__)\n"
23
+ '@bp.route("/login")\ndef login(): ...\n',
24
+ {"/login"},
25
+ ),
26
+ "django": (
27
+ "urls.py",
28
+ 'from django.urls import path\nurlpatterns = [path("admin/", v), path("home/", v)]\n',
29
+ {"admin/", "home/"},
30
+ ),
31
+ "nestjs": (
32
+ "users.controller.ts",
33
+ "import { Controller, Get } from '@nestjs/common';\n"
34
+ "@Controller('users')\nclass C { @Get('/list') l(){} @Post('/create') c(){} }\n",
35
+ {"/list", "/create"},
36
+ ),
37
+ "express": (
38
+ "routes.js",
39
+ "const express = require('express')\nconst router = express.Router()\n"
40
+ "router.get('/api/users', h)\nrouter.post('/api/users', h)\n",
41
+ {"/api/users"},
42
+ ),
43
+ "fastify": (
44
+ "server.ts",
45
+ "import Fastify from 'fastify'\nconst f = Fastify()\n"
46
+ "f.get('/health', h)\nf.post('/items', h)\n",
47
+ {"/health", "/items"},
48
+ ),
49
+ "hono": (
50
+ "app.ts",
51
+ "import { Hono } from 'hono'\nconst app = new Hono()\n"
52
+ "app.get('/ping', c)\napp.delete('/items/:id', c)\n",
53
+ {"/ping", "/items/:id"},
54
+ ),
55
+ "go": (
56
+ "main.go",
57
+ 'package main\nimport "github.com/gin-gonic/gin"\n'
58
+ 'func main(){ r := gin.Default(); r.GET("/ping", h); r.POST("/users", h) }\n',
59
+ {"/ping", "/users"},
60
+ ),
61
+ "openapi": (
62
+ "openapi.yaml",
63
+ "openapi: 3.0.0\npaths:\n /pets:\n get: {}\n post: {}\n /pets/{id}:\n delete: {}\n",
64
+ {"/pets", "/pets/{id}"},
65
+ ),
66
+ }
67
+
68
+
69
+ def _write(tmp_path: Path, filename: str, source: str) -> Path:
70
+ svc = tmp_path / "services" / "svc"
71
+ svc.mkdir(parents=True)
72
+ (svc / filename).write_text(source)
73
+ return tmp_path
74
+
75
+
76
+ @pytest.mark.parametrize("framework", list(FIXTURES))
77
+ def test_adapter_detects_and_discovers(tmp_path: Path, framework: str) -> None:
78
+ filename, source, expected_paths = FIXTURES[framework]
79
+ root = _write(tmp_path, filename, source)
80
+
81
+ assert framework in detect_frameworks(SourceIndex(root))
82
+
83
+ paths = {ep.path for ep in discover_endpoints(root, framework)}
84
+ assert expected_paths <= paths
85
+
86
+
87
+ @pytest.mark.parametrize("framework", list(FIXTURES))
88
+ def test_mapper_builds_nonempty_deps(tmp_path: Path, framework: str) -> None:
89
+ filename, source, expected_paths = FIXTURES[framework]
90
+ root = _write(tmp_path, filename, source)
91
+
92
+ result = DependencyMapper(str(root)).build_from_codebase()
93
+ all_endpoints = {ep for svc in result["services"].values() for ep in svc["endpoints"]}
94
+ assert expected_paths <= all_endpoints
95
+
96
+
97
+ def test_auto_mode_prefers_specific_framework(tmp_path: Path) -> None:
98
+ """A Hono app using app.get() attributes to hono, not express."""
99
+ root = _write(tmp_path, "app.ts", "import { Hono } from 'hono'\nconst app = new Hono()\napp.get('/x', c)\n")
100
+ assert DependencyMapper(str(root))._detect_framework() == "hono"
101
+
102
+
103
+ def test_no_endpoints_for_plain_project(tmp_path: Path) -> None:
104
+ (tmp_path / "readme.md").write_text("# hello")
105
+ result = DependencyMapper(str(tmp_path)).build_from_codebase()
106
+ assert result == {"services": {}, "files": {}}
107
+
108
+
109
+ def test_endpoints_deduplicated(tmp_path: Path) -> None:
110
+ """The same path via GET and POST collapses to a single endpoint entry."""
111
+ root = _write(
112
+ tmp_path, "api.py",
113
+ "from fastapi import APIRouter\nrouter = APIRouter()\n"
114
+ '@router.get("/x")\ndef a(): ...\n@router.post("/x")\ndef b(): ...\n',
115
+ )
116
+ result = DependencyMapper(str(root)).build_from_codebase()
117
+ eps = result["services"]["services/svc"]["endpoints"]
118
+ assert eps.count("/x") == 1
@@ -71,6 +71,31 @@ def test_docker_map_generic_token_match() -> None:
71
71
  assert _map_docker_to_wup_service(_dc("payments-api"), services) == "payments"
72
72
 
73
73
 
74
+ def test_reject_prefixes_generic_by_default() -> None:
75
+ from wup.testql_monitor import ProbeTarget, is_monitoring_probe, reject_prefixes_for_config
76
+
77
+ cfg = validate_config({"project": {"name": "p"}})
78
+ assert reject_prefixes_for_config(cfg) == ()
79
+ # A generic /api/... health path is a valid probe (not fleet-rejected).
80
+ assert is_monitoring_probe(ProbeTarget(url="http://x:8100/api/id/health"), reject_prefixes_for_config(cfg))
81
+
82
+
83
+ def test_reject_prefixes_connect_profile_opt_in() -> None:
84
+ from wup.testql_monitor import ProbeTarget, is_monitoring_probe, reject_prefixes_for_config
85
+
86
+ cfg = validate_config({"project": {"name": "p"}, "testql": {"service_map_profile": "connect"}})
87
+ prefixes = reject_prefixes_for_config(cfg)
88
+ assert "/api/id" in prefixes
89
+ assert not is_monitoring_probe(ProbeTarget(url="http://x:8100/api/id/health"), prefixes)
90
+
91
+
92
+ def test_reject_prefixes_explicit_override() -> None:
93
+ from wup.testql_monitor import reject_prefixes_for_config
94
+
95
+ cfg = validate_config({"project": {"name": "p"}, "testql": {"monitoring_reject_prefixes": ["/internal"]}})
96
+ assert reject_prefixes_for_config(cfg) == ("/internal",)
97
+
98
+
74
99
  def test_config_roundtrips_docker_service_map() -> None:
75
100
  raw = {
76
101
  "project": {"name": "p"},
@@ -0,0 +1,104 @@
1
+ """Tests for OQL — the Observability Query Language."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import time
7
+ from pathlib import Path
8
+
9
+ import pytest
10
+
11
+ from wup.bus import EventBus
12
+ from wup.oql import Condition, OQLEngine, OQLError, RunOQL, parse, register_oql
13
+
14
+
15
+ def _project(tmp_path: Path) -> Path:
16
+ now = int(time.time())
17
+ wup = tmp_path / ".wup"
18
+ wup.mkdir()
19
+ (wup / "service-health.json").write_text(json.dumps({
20
+ "api": {"status": "down", "updated_at": now, "stage": "probe", "message": "500 error"},
21
+ "web": {"status": "up", "updated_at": now, "stage": "probe", "message": ""},
22
+ "worker": {"status": "degraded", "updated_at": now - 500, "stage": "quick", "message": "slow"},
23
+ }))
24
+ (wup / "service-health-events.jsonl").write_text(
25
+ json.dumps({"service": "api", "status": "down", "timestamp": now}) + "\n"
26
+ + json.dumps({"service": "web", "status": "up", "timestamp": now - 4000}) + "\n"
27
+ )
28
+ return tmp_path
29
+
30
+
31
+ # --- parsing --------------------------------------------------------------
32
+
33
+ def test_parse_minimal() -> None:
34
+ q = parse("services")
35
+ assert q.source == "services" and q.conditions == [] and q.limit is None
36
+
37
+
38
+ def test_parse_full() -> None:
39
+ q = parse("events where service = api and status != up since 1h limit 5")
40
+ assert q.source == "events"
41
+ assert q.conditions == [Condition("service", "=", "api"), Condition("status", "!=", "up")]
42
+ assert q.since_seconds == 3600
43
+ assert q.limit == 5
44
+
45
+
46
+ def test_parse_operator_without_spaces() -> None:
47
+ q = parse("services where status=down")
48
+ assert q.conditions == [Condition("status", "=", "down")]
49
+
50
+
51
+ @pytest.mark.parametrize("bad", [
52
+ "", "widgets", "services where", "services where status", "services since",
53
+ "services limit x", "services since 5x",
54
+ ])
55
+ def test_parse_errors(bad: str) -> None:
56
+ with pytest.raises(OQLError):
57
+ parse(bad)
58
+
59
+
60
+ # --- execution ------------------------------------------------------------
61
+
62
+ def test_filter_equals(tmp_path: Path) -> None:
63
+ engine = OQLEngine(_project(tmp_path))
64
+ rows = engine.execute("services where status = down")
65
+ assert [r["name"] for r in rows] == ["api"]
66
+
67
+
68
+ def test_filter_not_equals(tmp_path: Path) -> None:
69
+ engine = OQLEngine(_project(tmp_path))
70
+ assert {r["name"] for r in engine.execute("services where status != up")} == {"api", "worker"}
71
+
72
+
73
+ def test_contains_operator(tmp_path: Path) -> None:
74
+ engine = OQLEngine(_project(tmp_path))
75
+ assert [r["name"] for r in engine.execute("services where message ~ error")] == ["api"]
76
+
77
+
78
+ def test_since_filters_old_events(tmp_path: Path) -> None:
79
+ engine = OQLEngine(_project(tmp_path))
80
+ rows = engine.execute("events since 10m")
81
+ assert [r["service"] for r in rows] == ["api"] # web event (4000s old) excluded
82
+
83
+
84
+ def test_limit(tmp_path: Path) -> None:
85
+ engine = OQLEngine(_project(tmp_path))
86
+ assert len(engine.execute("services limit 1")) == 1
87
+
88
+
89
+ def test_numeric_comparison(tmp_path: Path) -> None:
90
+ engine = OQLEngine(_project(tmp_path))
91
+ now = int(time.time())
92
+ rows = engine.execute(f"services where updated_at > {now - 100}")
93
+ assert {r["name"] for r in rows} == {"api", "web"} # worker is 500s old
94
+
95
+
96
+ def test_missing_files_return_empty(tmp_path: Path) -> None:
97
+ assert OQLEngine(tmp_path).execute("services") == []
98
+
99
+
100
+ def test_bus_integration(tmp_path: Path) -> None:
101
+ bus = EventBus()
102
+ register_oql(bus, _project(tmp_path))
103
+ rows = bus.query(RunOQL(query="services where status = down"))
104
+ assert [r["name"] for r in rows] == ["api"]
@@ -78,9 +78,14 @@ def test_firmware_plugin_health_catalog_not_periodic_live_probe():
78
78
  )
79
79
 
80
80
 
81
- def test_connect_api_paths_on_8100_are_not_monitoring_probes():
81
+ def test_connect_api_paths_rejection_is_opt_in():
82
+ from wup.testql_monitor import _BUILTIN_REJECT_PREFIXES
83
+
82
84
  probe = ProbeTarget(url="http://localhost:8100/api/id/health")
83
- assert not is_monitoring_probe(probe)
85
+ # Generic default: nothing is rejected — /api/id/health is a valid probe.
86
+ assert is_monitoring_probe(probe)
87
+ # With the connect profile's reject-prefixes, it is excluded.
88
+ assert not is_monitoring_probe(probe, _BUILTIN_REJECT_PREFIXES["connect"])
84
89
  assert assign_probe_to_service(
85
90
  probe,
86
91
  [ServiceConfig(name="backend", paths=["backend/**", "api/**"])],
@@ -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.73"
10
+ __version__ = "0.2.75"
11
11
  __author__ = "Tom Sapletta"
12
12
 
13
13
  from .config import load_config, save_config, get_default_config