diff --git a/routstr/core/main.py b/routstr/core/main.py index 368202cd..cd4cab90 100644 --- a/routstr/core/main.py +++ b/routstr/core/main.py @@ -340,6 +340,21 @@ async def providers() -> RedirectResponse: UI_DIST_PATH = Path(__file__).parent.parent.parent / "ui_out" +# Every `ui/app/**/page.tsx` route needs an entry, or a direct load 404s. +UI_PAGES = ( + "dashboard", + "login", + "model", + "providers", + "providers/certification", + "settings", + "transactions", + "balances", + "logs", + "usage", + "unauthorized", +) + if UI_DIST_PATH.exists() and UI_DIST_PATH.is_dir(): logger.info(f"Serving static UI from {UI_DIST_PATH}") @@ -362,18 +377,6 @@ if UI_DIST_PATH.exists() and UI_DIST_PATH.is_dir(): # with a slash (e.g. `/login/`). The proxy router catches `/{path:path}` # before FastAPI's `redirect_slashes` logic can normalize the URL, so we # must register both the with-slash and without-slash variants here. - UI_PAGES = ( - "dashboard", - "login", - "model", - "providers", - "settings", - "transactions", - "balances", - "logs", - "usage", - "unauthorized", - ) def _register_ui_page(name: str) -> None: page_dir = UI_DIST_PATH / name diff --git a/routstr/upstream/certification.py b/routstr/upstream/certification.py index fc345f33..180ab239 100644 --- a/routstr/upstream/certification.py +++ b/routstr/upstream/certification.py @@ -17,6 +17,7 @@ import argparse import asyncio import json import math +import os import sys import time from collections.abc import Callable @@ -1245,7 +1246,14 @@ def main(argv: list[str] | None = None) -> int: required=True, help="Upstream base URL (repeatable), e.g. https://api.example.com/v1", ) - parser.add_argument("--key", default="", help="Bearer API key for the upstream") + parser.add_argument( + "--key", + default=os.environ.get("ROUTSTR_CERTIFY_KEY", ""), + help=( + "Bearer API key for the upstream (defaults to $ROUTSTR_CERTIFY_KEY; " + "prefer the env var so the key stays out of shell history and ps)" + ), + ) parser.add_argument( "--model", default=None, diff --git a/tests/unit/test_certification_cli_key.py b/tests/unit/test_certification_cli_key.py new file mode 100644 index 00000000..c9355445 --- /dev/null +++ b/tests/unit/test_certification_cli_key.py @@ -0,0 +1,46 @@ +"""The certification CLI reads the upstream key from the environment.""" + +from __future__ import annotations + +from typing import Any + +import pytest + +from routstr.upstream import certification + + +@pytest.fixture +def captured_keys(monkeypatch: pytest.MonkeyPatch) -> list[str]: + keys: list[str] = [] + + async def fake_certify(url: str, *, api_key: str, **_: Any) -> dict[str, Any]: + keys.append(api_key) + return {"url": url, "rows": []} + + monkeypatch.setattr(certification, "certify_upstream_url", fake_certify) + monkeypatch.setattr(certification, "render_checklist", lambda _result: "") + return keys + + +def test_key_defaults_to_env_var( + monkeypatch: pytest.MonkeyPatch, captured_keys: list[str] +) -> None: + monkeypatch.setenv("ROUTSTR_CERTIFY_KEY", "sk-from-env") + assert certification.main(["--url", "http://localhost:1/v1"]) == 0 + assert captured_keys == ["sk-from-env"] + + +def test_key_flag_overrides_env_var( + monkeypatch: pytest.MonkeyPatch, captured_keys: list[str] +) -> None: + monkeypatch.setenv("ROUTSTR_CERTIFY_KEY", "sk-from-env") + certification.main(["--url", "http://localhost:1/v1", "--key", "sk-flag"]) + assert captured_keys == ["sk-flag"] + + +def test_key_is_empty_without_flag_or_env( + monkeypatch: pytest.MonkeyPatch, captured_keys: list[str] +) -> None: + monkeypatch.delenv("ROUTSTR_CERTIFY_KEY", raising=False) + certification.main(["--url", "http://localhost:1/v1"]) + assert captured_keys == [""] diff --git a/tests/unit/test_ui_pages_registered.py b/tests/unit/test_ui_pages_registered.py new file mode 100644 index 00000000..3ccdd3a8 --- /dev/null +++ b/tests/unit/test_ui_pages_registered.py @@ -0,0 +1,19 @@ +"""Every static UI page must be served on a direct load, not the proxy 404.""" + +from __future__ import annotations + +from pathlib import Path + +from routstr.core import main as core_main + +UI_APP_DIR = Path(__file__).resolve().parents[2] / "ui" / "app" + + +def test_every_ui_app_page_is_in_ui_pages() -> None: + routes = { + page.parent.relative_to(UI_APP_DIR).as_posix() + for page in UI_APP_DIR.rglob("page.tsx") + if page.parent != UI_APP_DIR + } + assert routes, "no ui/app pages found" + assert routes - set(core_main.UI_PAGES) == set() diff --git a/ui/components/provider-certification-setup.tsx b/ui/components/provider-certification-setup.tsx index 6c2f0a98..d8ada6bb 100644 --- a/ui/components/provider-certification-setup.tsx +++ b/ui/components/provider-certification-setup.tsx @@ -272,7 +272,8 @@ export function ProviderCertificationSetupPanel({ )} {mode === 'all' && (

- All {paths.length} paths will run in parallel. + All {paths.length} paths will run one after another, each + with its own probe calls.

)}