fix(infra): fullstack api dirs include package name (#261)
* fix(infra): fullstack _api_dirs_for includes package name in path * test(infra): fullstack thin-routes check coverage for issue #259 * fix(ci): reformat assert in test_project_status for ruff --------- Co-authored-by: opencode-agent <agent@opencode.local>
This commit is contained in:
parent
8b05fa74f7
commit
06899b3c01
2 changed files with 153 additions and 2 deletions
|
|
@ -621,7 +621,13 @@ def _has_route_decorator(decorators: list[ast.expr], route_methods: set[str]) ->
|
||||||
|
|
||||||
|
|
||||||
def _api_dirs_for(ptype: ProjectType, ctx: RepoCtx) -> list[Path]:
|
def _api_dirs_for(ptype: ProjectType, ctx: RepoCtx) -> list[Path]:
|
||||||
"""Return list of api/v1 dirs to scan for routes, based on project type."""
|
"""Return list of api/v1 dirs to scan for routes, based on project type.
|
||||||
|
|
||||||
|
Both BACKEND and FULLSTACK use the nested ``src/<package>/api/v1`` layout
|
||||||
|
(issue #241). The package name is resolved from ``[project].name`` in
|
||||||
|
``pyproject.toml`` (normalized via ``_normalize_package_name``). FULLSTACK
|
||||||
|
additionally prefixes the backend root with ``backend/``.
|
||||||
|
"""
|
||||||
if ptype == ProjectType.BACKEND:
|
if ptype == ProjectType.BACKEND:
|
||||||
pkg = _resolve_package_name(ctx)
|
pkg = _resolve_package_name(ctx)
|
||||||
if pkg is None:
|
if pkg is None:
|
||||||
|
|
@ -629,7 +635,13 @@ def _api_dirs_for(ptype: ProjectType, ctx: RepoCtx) -> list[Path]:
|
||||||
root = ctx.root / "src" / pkg / "api" / "v1"
|
root = ctx.root / "src" / pkg / "api" / "v1"
|
||||||
return [root] if root.exists() else []
|
return [root] if root.exists() else []
|
||||||
if ptype == ProjectType.FULLSTACK:
|
if ptype == ProjectType.FULLSTACK:
|
||||||
root = ctx.root / "backend" / "src" / "api" / "v1"
|
# pyproject.toml lives under ``backend/`` in the fullstack template,
|
||||||
|
# so resolve the package name against a backend-rooted context.
|
||||||
|
backend_ctx = RepoCtx(root=ctx.root / "backend", config=ctx.config)
|
||||||
|
pkg = _resolve_package_name(backend_ctx)
|
||||||
|
if pkg is None:
|
||||||
|
return []
|
||||||
|
root = ctx.root / "backend" / "src" / pkg / "api" / "v1"
|
||||||
return [root] if root.exists() else []
|
return [root] if root.exists() else []
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -205,6 +205,40 @@ def _make_backend_repo(tmp_path: Path) -> None:
|
||||||
(tmp_path / ".pre-commit-config.yaml").write_text("repos: []\n")
|
(tmp_path / ".pre-commit-config.yaml").write_text("repos: []\n")
|
||||||
|
|
||||||
|
|
||||||
|
def _make_fullstack_repo(tmp_path: Path) -> None:
|
||||||
|
"""Create a minimal fullstack repo skeleton in tmp_path.
|
||||||
|
|
||||||
|
Mirrors the cookiecutter fullstack template (issue #241 nested layout):
|
||||||
|
backend ``pyproject.toml`` lives under ``backend/``, the package under
|
||||||
|
``backend/src/<package>/`` with ``api/v1`` nested. The package name is
|
||||||
|
``test_repo`` (normalized from the default ``[project].name``
|
||||||
|
``test-repo``). Frontend is a stub ``package.json``.
|
||||||
|
"""
|
||||||
|
pkg = "test_repo"
|
||||||
|
for rel in [
|
||||||
|
f"backend/src/{pkg}/api/v1",
|
||||||
|
f"backend/src/{pkg}/db/models",
|
||||||
|
f"backend/src/{pkg}/schemas",
|
||||||
|
f"backend/src/{pkg}/services",
|
||||||
|
f"backend/src/{pkg}/config",
|
||||||
|
"backend/tests",
|
||||||
|
"frontend",
|
||||||
|
]:
|
||||||
|
(tmp_path / rel).mkdir(parents=True, exist_ok=True)
|
||||||
|
(tmp_path / "backend" / "src" / pkg / "__init__.py").write_text("")
|
||||||
|
(tmp_path / "backend" / f"src/{pkg}/config/settings.py").write_text("settings = {}\n")
|
||||||
|
(tmp_path / "backend" / "main.py").write_text(
|
||||||
|
"from contextlib import asynccontextmanager\n"
|
||||||
|
"@asynccontextmanager\nasync def lifespan(app): yield\n"
|
||||||
|
)
|
||||||
|
(tmp_path / "backend" / f"src/{pkg}/api/v1/users.py").write_text(
|
||||||
|
"from fastapi import APIRouter\nrouter = APIRouter()\n"
|
||||||
|
"@router.get('/users')\nasync def list_users():\n return []\n"
|
||||||
|
)
|
||||||
|
_write_pyproject(tmp_path / "backend", deps=["fastapi", "uvicorn"], cov_source=["src"])
|
||||||
|
(tmp_path / "frontend" / "package.json").write_text('{"name": "test-frontend"}\n')
|
||||||
|
|
||||||
|
|
||||||
# ── parse_remote_url ─────────────────────────────────────────────────────────
|
# ── parse_remote_url ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -536,6 +570,111 @@ def test_thin_routes_import_ok_no_fail_on_over_limit(tmp_path, ctx):
|
||||||
assert any(c.name == "route imports" and c.status == ps.CheckStatus.OK for c in group.checks)
|
assert any(c.name == "route imports" and c.status == ps.CheckStatus.OK for c in group.checks)
|
||||||
|
|
||||||
|
|
||||||
|
# ── check_thin_routes: FULLSTACK (issue #259) ──────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
def test_thin_routes_fullstack_ok(tmp_path, ctx):
|
||||||
|
"""Fullstack nested ``backend/src/<pkg>/api/v1/`` → thin-routes OK (new)."""
|
||||||
|
_make_fullstack_repo(tmp_path)
|
||||||
|
group = ps.check_thin_routes(ps.ProjectType.FULLSTACK, ctx)
|
||||||
|
assert group.overall() == ps.CheckStatus.OK, (
|
||||||
|
f"fullstack thin-routes check should work now, got {group.checks}"
|
||||||
|
)
|
||||||
|
assert any(c.name == "route imports" and c.status == ps.CheckStatus.OK for c in group.checks)
|
||||||
|
|
||||||
|
|
||||||
|
def test_thin_routes_fullstack_no_pkg_dir_skip(tmp_path, ctx):
|
||||||
|
"""Fullstack without ``backend/src/<pkg>/`` (flat) → WARN skip (no api dir)."""
|
||||||
|
(tmp_path / "backend").mkdir()
|
||||||
|
(tmp_path / "frontend").mkdir()
|
||||||
|
group = ps.check_thin_routes(ps.ProjectType.FULLSTACK, ctx)
|
||||||
|
assert group.overall() == ps.CheckStatus.WARN
|
||||||
|
assert any(c.name == "src/api/v1/" and "не найдена" in c.detail for c in group.checks)
|
||||||
|
|
||||||
|
|
||||||
|
def test_thin_routes_fullstack_no_backend_pyproject_skip(tmp_path, ctx):
|
||||||
|
"""Fullstack without ``backend/pyproject.toml`` → cannot resolve pkg → skip."""
|
||||||
|
pkg = "test_repo"
|
||||||
|
for rel in [
|
||||||
|
f"backend/src/{pkg}/api/v1",
|
||||||
|
f"backend/src/{pkg}/db/models",
|
||||||
|
f"backend/src/{pkg}/schemas",
|
||||||
|
f"backend/src/{pkg}/services",
|
||||||
|
f"backend/src/{pkg}/config",
|
||||||
|
"frontend",
|
||||||
|
]:
|
||||||
|
(tmp_path / rel).mkdir(parents=True, exist_ok=True)
|
||||||
|
(tmp_path / "backend" / "src" / pkg / "__init__.py").write_text("")
|
||||||
|
(tmp_path / "backend" / f"src/{pkg}/api/v1/users.py").write_text(
|
||||||
|
"from fastapi import APIRouter\nrouter = APIRouter()\n"
|
||||||
|
"@router.get('/users')\nasync def list_users():\n return []\n"
|
||||||
|
)
|
||||||
|
group = ps.check_thin_routes(ps.ProjectType.FULLSTACK, ctx)
|
||||||
|
assert group.overall() == ps.CheckStatus.WARN
|
||||||
|
assert any(c.name == "src/api/v1/" and "не найдена" in c.detail for c in group.checks), (
|
||||||
|
f"expected WARN skip when backend pyproject missing, got {group.checks}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_thin_routes_fullstack_import_src_db_models_fail(tmp_path, ctx):
|
||||||
|
"""Fullstack route importing ``src.db.models`` → FAIL (same rule as backend)."""
|
||||||
|
_make_fullstack_repo(tmp_path)
|
||||||
|
(tmp_path / "backend" / "src/test_repo/api/v1/users.py").write_text(
|
||||||
|
"from fastapi import APIRouter\nfrom src.db.models import User\n"
|
||||||
|
"router = APIRouter()\n"
|
||||||
|
"@router.get('/users')\nasync def list_users():\n return []\n"
|
||||||
|
)
|
||||||
|
group = ps.check_thin_routes(ps.ProjectType.FULLSTACK, ctx)
|
||||||
|
assert any(
|
||||||
|
c.name == "route imports" and c.status == ps.CheckStatus.FAIL for c in group.checks
|
||||||
|
), f"expected FAIL on fullstack route imports, got: {group.checks}"
|
||||||
|
|
||||||
|
|
||||||
|
def test_thin_routes_fullstack_import_tortoise_fail(tmp_path, ctx):
|
||||||
|
"""Fullstack route importing ``tortoise`` → FAIL (same rule as backend)."""
|
||||||
|
_make_fullstack_repo(tmp_path)
|
||||||
|
(tmp_path / "backend" / "src/test_repo/api/v1/users.py").write_text(
|
||||||
|
"from fastapi import APIRouter\nfrom tortoise import fields\n"
|
||||||
|
"router = APIRouter()\n"
|
||||||
|
"@router.get('/users')\nasync def list_users():\n return []\n"
|
||||||
|
)
|
||||||
|
group = ps.check_thin_routes(ps.ProjectType.FULLSTACK, ctx)
|
||||||
|
assert any(
|
||||||
|
c.name == "route imports" and c.status == ps.CheckStatus.FAIL for c in group.checks
|
||||||
|
), f"expected FAIL on fullstack route imports, got: {group.checks}"
|
||||||
|
|
||||||
|
|
||||||
|
def test_thin_routes_fullstack_import_services_ok(tmp_path, ctx):
|
||||||
|
"""Fullstack route importing ``<pkg>.services`` → OK (not forbidden)."""
|
||||||
|
_make_fullstack_repo(tmp_path)
|
||||||
|
(tmp_path / "backend" / "src/test_repo/api/v1/users.py").write_text(
|
||||||
|
"from fastapi import APIRouter\nfrom test_repo.services import user_service\n"
|
||||||
|
"router = APIRouter()\n"
|
||||||
|
"@router.get('/users')\nasync def list_users():\n return user_service.list_all()\n"
|
||||||
|
)
|
||||||
|
group = ps.check_thin_routes(ps.ProjectType.FULLSTACK, ctx)
|
||||||
|
assert all(c.status != ps.CheckStatus.FAIL for c in group.checks), (
|
||||||
|
f"fullstack services import should be OK, got: {group.checks}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_thin_routes_fullstack_over_limit_warn(tmp_path, ctx):
|
||||||
|
"""Fullstack route over line limit → WARN (no FAIL) when imports clean."""
|
||||||
|
_make_fullstack_repo(tmp_path)
|
||||||
|
body = "\n x = 1\n" * 60
|
||||||
|
(tmp_path / "backend" / "src/test_repo/api/v1/users.py").write_text(
|
||||||
|
"from fastapi import APIRouter\nfrom test_repo.services import user_service\n"
|
||||||
|
"router = APIRouter()\n"
|
||||||
|
"@router.get('/users')\nasync def list_users():"
|
||||||
|
f"{body} return []\n"
|
||||||
|
)
|
||||||
|
group = ps.check_thin_routes(ps.ProjectType.FULLSTACK, ctx)
|
||||||
|
assert group.overall() == ps.CheckStatus.WARN, (
|
||||||
|
f"over-limit but no forbidden import → WARN, got {group.checks}"
|
||||||
|
)
|
||||||
|
assert any(c.name == "route imports" and c.status == ps.CheckStatus.OK for c in group.checks)
|
||||||
|
|
||||||
|
|
||||||
# ── check_quality ────────────────────────────────────────────────────────────
|
# ── check_quality ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue