Skip to content

Commit 2e23b89

Browse files
fix(tests): update docs
Statistics: 17 files changed, 1185 insertions, 205 deletions Summary: - Dirs: wup=8, tests=5, .=3, docs=1 - Exts: .py=13, .md=3, .pb=1 - A/M/D: 5/12/0 - Added: tests/test_aql.py, tests/test_discovery_adapters.py, tests/test_oql.py, wup/aql.py, wup/oql.py - Symbols: _load, test_nested_and_indexed_paths, test_bus_integration, test_missing_file, test_limit Added files: - tests/test_aql.py (+120/-0) - tests/test_discovery_adapters.py (+118/-0) - tests/test_oql.py (+104/-0) - wup/aql.py (+306/-0) - wup/oql.py (+267/-0) Modified files: - CHANGELOG.md (+23/-0) - README.md (+3/-3) - app.doql.events.pb (+0/-0) - docs/GENERICITY_AUDIT.md (+25/-11) - tests/test_genericity.py (+25/-0) - tests/test_testql_monitor.py (+7/-2) - wup/cli.py (+105/-0) - wup/config.py (+2/-0) - wup/dependency_mapper.py (+25/-149) - wup/discovery.py (+11/-7) - wup/models/config.py (+4/-0) - wup/testql_monitor.py (+40/-33) Implementation notes (heuristics): - Type inferred from file paths + diff keywords + add/delete ratio - Scope prefers 'goal' when goal/* is touched; otherwise based on top-level dirs - For <=6 files: generate short per-file notes from added lines (defs/classes/click options/headings) - A/M/D derived from git name-status; per-file +X/-X from git numstat Co-authored-by: Koru Agent <agent@coru.dev>
1 parent b58ab70 commit 2e23b89

21 files changed

Lines changed: 1216 additions & 211 deletions

CHANGELOG.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,34 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88
## [Unreleased]
99

1010
### Added
11+
- **Pluggable endpoint-discovery adapters (`wup/discovery.py`).** `deps.json` is
12+
now built by per-ecosystem adapters — FastAPI, Flask, Django, NestJS, Express,
13+
Fastify, Hono, Go (gin/echo/net-http) and OpenAPI/Swagger — selected by repo
14+
markers instead of a hardcoded four-framework switch. Adding a framework means
15+
adding an adapter, not editing the mapper. Endpoints are de-duplicated across
16+
adapters and HTTP methods.
17+
- **OQL — Observability Query Language (`wup/oql.py`, `wup oql`).** A small
18+
declarative language over observed state (service health + events):
19+
`wup oql "services where status = down"`, `wup oql "events since 10m limit 5"`.
20+
Supports `= != > < >= <= ~ !~`, `since <dur>`, `limit`, `--json` output, and a
21+
`RunOQL` query on the CQRS event bus so agents can read state programmatically.
22+
- **AQL — Assertion Query Language (`wup/aql.py`, `wup aql`).** Declarative
23+
assertions about a file's data (JSON/YAML/text) that emit `AnomalyResult`
24+
violations: `wup aql wup.yaml "yaml .project.name exists" "json .services
25+
length > 0"`. Supports paths with `[index]`, predicates `exists/missing`,
26+
`= != > < >= <=`, `~ !~`, `matches <regex>`, `length <op> <n>`, `type <t>`,
27+
per-rule `severity`, `--json` output (non-zero exit on failure for CI), and a
28+
`CheckAQL` bus query. Together TestQL + OQL + AQL give AI agents one declarative
29+
surface to test behaviour, read state, and assert invariants.
1130
- **Config-driven docker→service mapping.** `testql.docker_service_map`
1231
(`{substring: service}`) and `testql.service_map_profile` let a project map its
1332
compose services to WUP services without any names hardcoded in WUP. The old
1433
maskservice/c2004 "connect" fleet rules are now an opt-in built-in profile
1534
(`service_map_profile: connect`) instead of always-on logic.
35+
- **Config-driven probe rejection.** `testql.monitoring_reject_prefixes` (and the
36+
`connect` profile) control which URL path prefixes are excluded from health
37+
probes. The default rejects nothing, so a generic project's `/api/*` health
38+
endpoints are no longer wrongly filtered out by hardcoded "connect" fleet paths.
1639
- `docs/GENERICITY_AUDIT.md` — inventory of how `wup.yaml`/`deps.json` are
1740
generated, where the tool is hardwired to one project, and a TestQL+OQL+AQL
1841
roadmap toward a language-agnostic, AI-open design.
@@ -56,6 +79,31 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
5679
and `_select_scenarios_for_service` (testql_watcher). Also removed a dead
5780
web-service branch in scenario selection.
5881

82+
## [0.2.74] - 2026-07-17
83+
84+
### Docs
85+
- Update CHANGELOG.md
86+
- Update README.md
87+
- Update docs/GENERICITY_AUDIT.md
88+
89+
### Test
90+
- Update tests/test_aql.py
91+
- Update tests/test_discovery_adapters.py
92+
- Update tests/test_genericity.py
93+
- Update tests/test_oql.py
94+
- Update tests/test_testql_monitor.py
95+
96+
### Other
97+
- Update app.doql.events.pb
98+
- Update wup/aql.py
99+
- Update wup/cli.py
100+
- Update wup/config.py
101+
- Update wup/dependency_mapper.py
102+
- Update wup/discovery.py
103+
- Update wup/models/config.py
104+
- Update wup/oql.py
105+
- Update wup/testql_monitor.py
106+
59107
## [0.2.73] - 2026-07-17
60108

61109
### Docs

README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,17 @@
33

44
## AI Cost Tracking
55

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.74-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.19-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)
88

9-
- 🤖 **LLM usage:** $7.2381 (101 commits)
10-
- 👤 **Human dev:** ~$5241 (52.4h @ $100/h, 30min dedup)
9+
- 🤖 **LLM usage:** $7.1945 (102 commits)
10+
- 👤 **Human dev:** ~$5242 (52.4h @ $100/h, 30min dedup)
1111

1212
Generated on 2026-07-17 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
1313

1414
---
1515

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.74-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
1717

1818
**WUP (What's Up)** - Intelligent file watcher for regression testing in large projects.
1919

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.2.73
1+
0.2.74

app.doql.events.pb

34.1 KB
Binary file not shown.

docs/GENERICITY_AUDIT.md

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -106,17 +106,31 @@ and `wup.testing.queries.GetServiceHealth` — a ready foundation for OQL.
106106

107107
**Roadmap (cheapest first):**
108108

109-
1. **Move the "connect" fleet out of code into config.** A `service_mapping:` /
110-
`probe_topology:` section in `wup.yaml`; default empty ⇒ zero knowledge of any
111-
other project. Unblocks every non-fleet project without touching code.
112-
2. **Discovery as pluggable adapters** instead of four built-in frameworks:
113-
`python-ast`, `node-package-json`, `openapi`, `testql-scenarios` (exists).
114-
Selected by repo markers, not by guessing. Fixes the empty `deps.json`.
115-
3. **Formalize OQL over `wup.bus`** — declarative queries
116-
(`services where health=down`, `events since 5m`) as `Query` classes.
117-
4. **AQL = declarative assertions** — expose the `anomaly_detector` methods
118-
(`hash`/`structure`/`keys`/`ast`/`text`) as config rules so an AI can *generate*
119-
assertions instead of reading Python.
109+
1.**Move the "connect" fleet out of code into config.** Done:
110+
`testql.docker_service_map` + opt-in `testql.service_map_profile: connect`.
111+
Default carries no project-specific service names.
112+
2.**Discovery as pluggable adapters** — done in `wup/discovery.py`: FastAPI,
113+
Flask, Django, NestJS, Express, Fastify, Hono, Go and OpenAPI adapters selected
114+
by repo markers. Fixes the empty/Python-only `deps.json`.
115+
3.**Formalize OQL over `wup.bus`** — done in `wup/oql.py` + `wup oql`:
116+
declarative queries (`services where status = down`, `events since 5m`) with a
117+
`RunOQL` bus query.
118+
4.**AQL = declarative assertions** — done in `wup/aql.py` + `wup aql`:
119+
assertions about a file's data (`json .services length > 0`,
120+
`yaml .project.name exists`) that emit `AnomalyResult` violations, with a
121+
`CheckAQL` bus query. AI can now generate checks instead of reading Python.
122+
123+
### Still hardcoded (follow-up)
124+
125+
-`testql_monitor.py` probe rejection (`_CONNECT_API_PREFIXES`) is now
126+
config-driven: generic default rejects nothing; the connect prefixes are a
127+
built-in profile (`service_map_profile: connect`) or set explicitly via
128+
`testql.monitoring_reject_prefixes`. This was the one that actively *broke*
129+
other projects (rejecting their valid `/api/*` health probes).
130+
- ⏳ Lower-harm remainders (only trigger on specific ports/paths, else fall through
131+
to generic matching): the port `8202`/`8100``firmware` assignment and
132+
hardware-USB service names in `testql_monitor.py`, and `core.py:152`'s
133+
`^(connect|backend|…)[-_]` service regex. Same treatment (profile/config) applies.
120134

121135
"AI-open" = all three layers declarative (YAML/query) under one schema, so an agent
122136
can read state (OQL), write scenarios (TestQL) and define assertions (AQL) without

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "wup"
7-
version = "0.2.73"
7+
version = "0.2.74"
88
description = "WUP (What's Up) - Intelligent file watcher for regression testing in large projects"
99
readme = "README.md"
1010
requires-python = ">=3.10"

tests/test_aql.py

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

tests/test_discovery_adapters.py

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

0 commit comments

Comments
 (0)