Lands the 15-bug catalogue from the 2026-05-19 creative-QA pass. API layer (wp_api/): - New _validators.py centralises meta + identifier + body-size checks so modules and bundles share the same shape rules - SPA catch-all now returns JSON 404 for unknown /wp/api/* paths instead of the SPA shell (debugging hostile previously) - create/update/import_from_workflow validate meta (name/desc/tags/ category_id) before hitting the repo; tag list now rejects non-str entries (was silently stored, breaking the SPA type contract) - IntegrityError on dangling category_id surfaces as 400, not 500 - Optional If-Match header on PUT enables opt-in optimistic concurrency — stale version → 409 instead of silent overwrite - Bundle name collisions auto-suffixed with (copy)/(copy N) mirroring the modules-side forkModule rule - Bundle children deduped by id at create + update time - Deleting a module flags meta.orphaned=true on every bundle child whose snapshot referenced it so the SPA can render a stale badge Engine layer (engine/modules/*_handler.py): - wildcard: reject empty + __dunder var_binding, cap at 64 chars, reject duplicate option ids - combine: reject empty template, cap template at 8000 chars, cap output_var at 64, reject __dunder output_var - fixed_values: cap value name length, reject __dunder names, reject duplicate names across rows (was silent last-write-wins) - derivation: reject duplicate rule ids (was nondeterministic for disabled_rule_ids / rule_order_override lookups) Tests: - New test_qa_hardening.py exercises every endpoint-level fix - Extended test_payload_validation.py parametrised rejection matrix - New test_spa_fallback.py case for /wp/api/* JSON 404 655 pytest pass, 1665 vitest pass, ruff clean.
52 lines
1.8 KiB
Python
52 lines
1.8 KiB
Python
"""/wp + /wp/{path:.*} — static asset server with SPA fallback.
|
|
|
|
Looks up the requested path inside ``WEB_DIR``. If a file exists, serves it.
|
|
Otherwise serves ``index.html`` so the SPA router can take over (any
|
|
``/wp/<route>`` path resolves to the SPA shell).
|
|
|
|
Exception: paths under ``/wp/api/`` that fell through every registered
|
|
API route are returned as JSON 404, not the SPA shell. Without this
|
|
guard a client typo like ``/wp/api/moduels`` returned 200 ``text/html``
|
|
(the SPA index), making API debugging hostile — the JSON.parse failure
|
|
on the client masked the real "no such endpoint" error.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
from aiohttp import web
|
|
|
|
WEB_DIR = Path(__file__).parent.parent / "web"
|
|
|
|
|
|
async def _serve(path: str) -> web.StreamResponse:
|
|
if path:
|
|
candidate = (WEB_DIR / path).resolve()
|
|
try:
|
|
candidate.relative_to(WEB_DIR.resolve())
|
|
except ValueError:
|
|
return web.Response(status=403)
|
|
if candidate.is_file():
|
|
return web.FileResponse(candidate)
|
|
return web.FileResponse(WEB_DIR / "index.html")
|
|
|
|
|
|
async def serve_root(request: web.Request) -> web.StreamResponse:
|
|
return await _serve("")
|
|
|
|
|
|
async def serve_path(request: web.Request) -> web.StreamResponse:
|
|
path = request.match_info["path"]
|
|
# `/wp/api/...` is reserved for the JSON API. Any request reaching
|
|
# this handler under that prefix means no specific API route
|
|
# matched — return a real 404 instead of the SPA shell so the
|
|
# client gets a useful error.
|
|
if path.startswith("api/") or path == "api":
|
|
return web.json_response({"error": f"no such API route: /wp/{path}"}, status=404)
|
|
return await _serve(path)
|
|
|
|
|
|
def register(router) -> None:
|
|
router.add_get("/wp", serve_root)
|
|
router.add_get("/wp/{path:.*}", serve_path)
|