feat(gateway): dual-mount legacy routers on /api and /api/v1 (Stage 1 PR5)

Strip /api prefix from 13 legacy router APIRouter() declarations and dual-mount
each on prefix="/api" (backward compat) and prefix="/api/v1" (versioned surface)
in app.py. Auth, service-accounts, api-keys, assistants-compat remain single-mount.
Update 10 test files to pass prefix="/api" when directly including stripped routers.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1445043649
2026-06-28 19:49:05 +08:00
parent 3f0d5c8c96
commit 2461a923be
25 changed files with 135 additions and 68 deletions
+51
View File
@@ -0,0 +1,51 @@
"""Dual-mount /api + /api/v1 tests (Stage 1 PR5).
Every migrated legacy router must answer on BOTH /api/<x> and /api/v1/<x>.
Asserts route presence on the OpenAPI schema (independent of per-route auth).
"""
from __future__ import annotations
from app.gateway.app import create_app
def _paths():
return set(create_app().openapi()["paths"].keys())
def test_models_dual_mounted():
paths = _paths()
assert "/api/models" in paths
assert "/api/v1/models" in paths
def test_runs_dual_mounted():
paths = _paths()
assert "/api/runs/stream" in paths
assert "/api/v1/runs/stream" in paths
def test_threads_dual_mounted():
paths = _paths()
# at least one threads sub-path must exist on both surfaces
assert any(p.startswith("/api/threads/") for p in paths)
assert any(p.startswith("/api/v1/threads/") for p in paths)
def test_uploads_dual_mounted():
paths = _paths()
assert any(p.startswith("/api/threads/") and "/uploads" in p for p in paths)
assert any(p.startswith("/api/v1/threads/") and "/uploads" in p for p in paths)
def test_auth_only_v1_not_dual():
# auth stays v1-only — must NOT acquire an /api/auth twin.
paths = _paths()
assert "/api/v1/auth/me" in paths
assert "/api/auth/me" not in paths
def test_no_v1_langgraph_twins():
paths = _paths()
assert not any(p.startswith("/api/v1/langgraph") for p in paths)
assert not any(p.startswith("/api/v1/assistants") for p in paths)
+2 -2
View File
@@ -76,7 +76,7 @@ def test_get_artifact_download_false_does_not_force_attachment(tmp_path, monkeyp
monkeypatch.setattr(artifacts_router, "resolve_thread_virtual_path", lambda _thread_id, _path: artifact_path)
app = make_authed_test_app()
app.include_router(artifacts_router.router)
app.include_router(artifacts_router.router, prefix="/api")
with TestClient(app) as client:
response = client.get("/api/threads/thread-1/artifacts/mnt/user-data/outputs/note.txt?download=false")
@@ -94,7 +94,7 @@ def test_get_artifact_download_true_forces_attachment_for_skill_archive(tmp_path
monkeypatch.setattr(artifacts_router, "resolve_thread_virtual_path", lambda _thread_id, _path: skill_path)
app = make_authed_test_app()
app.include_router(artifacts_router.router)
app.include_router(artifacts_router.router, prefix="/api")
with TestClient(app) as client:
response = client.get("/api/threads/thread-1/artifacts/mnt/user-data/outputs/sample.skill/notes.txt?download=true")
+1 -1
View File
@@ -382,7 +382,7 @@ def _make_test_app(tmp_path: Path):
from app.gateway.routers.agents import router
app = FastAPI()
app.include_router(router)
app.include_router(router, prefix="/api")
return app
+12 -12
View File
@@ -26,7 +26,7 @@ def _sample_memory(facts: list[dict] | None = None) -> dict:
def test_export_memory_route_returns_current_memory() -> None:
app = FastAPI()
app.include_router(memory.router)
app.include_router(memory.router, prefix="/api")
exported_memory = _sample_memory(
facts=[
{
@@ -50,7 +50,7 @@ def test_export_memory_route_returns_current_memory() -> None:
def test_import_memory_route_returns_imported_memory() -> None:
app = FastAPI()
app.include_router(memory.router)
app.include_router(memory.router, prefix="/api")
imported_memory = _sample_memory(
facts=[
{
@@ -74,7 +74,7 @@ def test_import_memory_route_returns_imported_memory() -> None:
def test_export_memory_route_preserves_source_error() -> None:
app = FastAPI()
app.include_router(memory.router)
app.include_router(memory.router, prefix="/api")
exported_memory = _sample_memory(
facts=[
{
@@ -99,7 +99,7 @@ def test_export_memory_route_preserves_source_error() -> None:
def test_import_memory_route_preserves_source_error() -> None:
app = FastAPI()
app.include_router(memory.router)
app.include_router(memory.router, prefix="/api")
imported_memory = _sample_memory(
facts=[
{
@@ -124,7 +124,7 @@ def test_import_memory_route_preserves_source_error() -> None:
def test_clear_memory_route_returns_cleared_memory() -> None:
app = FastAPI()
app.include_router(memory.router)
app.include_router(memory.router, prefix="/api")
with patch("app.gateway.routers.memory.clear_memory_data", return_value=_sample_memory()):
with TestClient(app) as client:
@@ -136,7 +136,7 @@ def test_clear_memory_route_returns_cleared_memory() -> None:
def test_create_memory_fact_route_returns_updated_memory() -> None:
app = FastAPI()
app.include_router(memory.router)
app.include_router(memory.router, prefix="/api")
updated_memory = _sample_memory(
facts=[
{
@@ -167,7 +167,7 @@ def test_create_memory_fact_route_returns_updated_memory() -> None:
def test_delete_memory_fact_route_returns_updated_memory() -> None:
app = FastAPI()
app.include_router(memory.router)
app.include_router(memory.router, prefix="/api")
updated_memory = _sample_memory(
facts=[
{
@@ -191,7 +191,7 @@ def test_delete_memory_fact_route_returns_updated_memory() -> None:
def test_delete_memory_fact_route_returns_404_for_missing_fact() -> None:
app = FastAPI()
app.include_router(memory.router)
app.include_router(memory.router, prefix="/api")
with patch("app.gateway.routers.memory.delete_memory_fact", side_effect=KeyError("fact_missing")):
with TestClient(app) as client:
@@ -203,7 +203,7 @@ def test_delete_memory_fact_route_returns_404_for_missing_fact() -> None:
def test_update_memory_fact_route_returns_updated_memory() -> None:
app = FastAPI()
app.include_router(memory.router)
app.include_router(memory.router, prefix="/api")
updated_memory = _sample_memory(
facts=[
{
@@ -234,7 +234,7 @@ def test_update_memory_fact_route_returns_updated_memory() -> None:
def test_update_memory_fact_route_preserves_omitted_fields() -> None:
app = FastAPI()
app.include_router(memory.router)
app.include_router(memory.router, prefix="/api")
updated_memory = _sample_memory(
facts=[
{
@@ -270,7 +270,7 @@ def test_update_memory_fact_route_preserves_omitted_fields() -> None:
def test_update_memory_fact_route_returns_404_for_missing_fact() -> None:
app = FastAPI()
app.include_router(memory.router)
app.include_router(memory.router, prefix="/api")
with patch("app.gateway.routers.memory.update_memory_fact", side_effect=KeyError("fact_missing")):
with TestClient(app) as client:
@@ -289,7 +289,7 @@ def test_update_memory_fact_route_returns_404_for_missing_fact() -> None:
def test_update_memory_fact_route_returns_specific_error_for_invalid_confidence() -> None:
app = FastAPI()
app.include_router(memory.router)
app.include_router(memory.router, prefix="/api")
with patch("app.gateway.routers.memory.update_memory_fact", side_effect=ValueError("confidence")):
with TestClient(app) as client:
+1 -1
View File
@@ -17,7 +17,7 @@ from app.gateway.routers import runs
def _make_app(run_store=None, event_store=None, feedback_repo=None):
"""Build a test FastAPI app with stub auth and mocked state."""
app = make_authed_test_app()
app.include_router(runs.router)
app.include_router(runs.router, prefix="/api")
if run_store is not None:
app.state.run_store = run_store
+1 -1
View File
@@ -39,7 +39,7 @@ def _make_skill(name: str, *, enabled: bool) -> Skill:
def _make_test_app(config) -> FastAPI:
app = FastAPI()
app.state.config = config
app.include_router(skills_router.router)
app.include_router(skills_router.router, prefix="/api")
return app
@@ -17,7 +17,7 @@ from app.gateway.routers import thread_runs
def _make_app(event_store=None):
"""Build a test FastAPI app with stub auth and mocked state."""
app = make_authed_test_app()
app.include_router(thread_runs.router)
app.include_router(thread_runs.router, prefix="/api")
if event_store is not None:
app.state.run_event_store = event_store
+1 -1
View File
@@ -37,7 +37,7 @@ def _build_app(workspace_id: str):
app.state.store = store
app.state.checkpointer = checkpointer
app.state.thread_store = MemoryThreadMetaStore(store)
app.include_router(threads.router)
app.include_router(threads.router, prefix="/api")
return app, store
+4 -4
View File
@@ -59,7 +59,7 @@ def _build_thread_app() -> tuple[FastAPI, InMemoryStore, InMemorySaver]:
app.state.store = store
app.state.checkpointer = checkpointer
app.state.thread_store = _PermissiveThreadMetaStore(store)
app.include_router(threads.router)
app.include_router(threads.router, prefix="/api")
return app, store, checkpointer
@@ -113,7 +113,7 @@ def test_delete_thread_route_cleans_thread_directory(tmp_path):
(paths.sandbox_work_dir("thread-route", user_id=user_id) / "notes.txt").write_text("hello", encoding="utf-8")
app = make_authed_test_app()
app.include_router(threads.router)
app.include_router(threads.router, prefix="/api")
with patch("app.gateway.routers.threads.get_paths", return_value=paths):
with TestClient(app) as client:
@@ -128,7 +128,7 @@ def test_delete_thread_route_rejects_invalid_thread_id(tmp_path):
paths = Paths(tmp_path)
app = make_authed_test_app()
app.include_router(threads.router)
app.include_router(threads.router, prefix="/api")
with patch("app.gateway.routers.threads.get_paths", return_value=paths):
with TestClient(app) as client:
@@ -141,7 +141,7 @@ def test_delete_thread_route_returns_422_for_route_safe_invalid_id(tmp_path):
paths = Paths(tmp_path)
app = make_authed_test_app()
app.include_router(threads.router)
app.include_router(threads.router, prefix="/api")
with patch("app.gateway.routers.threads.get_paths", return_value=paths):
with TestClient(app) as client:
+1 -1
View File
@@ -598,7 +598,7 @@ def test_upload_limits_endpoint_requires_thread_access():
cfg.uploads = {}
app = make_authed_test_app(owner_check_passes=False)
app.state.config = cfg
app.include_router(uploads.router)
app.include_router(uploads.router, prefix="/api")
with TestClient(app) as client:
response = client.get("/api/threads/thread-local/uploads/limits")
@@ -80,7 +80,7 @@ def _build_app(*, user_id: str, workspace_id: str):
app.state.store = store
app.state.checkpointer = InMemorySaver()
app.state.thread_store = MemoryThreadMetaStore(store)
app.include_router(threads.router)
app.include_router(threads.router, prefix="/api")
return app, store