"""What M2 removed stays removed, and what it must not break stays working. A subtractive milestone needs tests that fail if the surface grows back. These are cheap, blunt, and deliberately not clever: they assert against the running app's route table, the shipped configuration files, and the settings API. python -m pytest tests/test_local_only_surface.py -v """ import re from pathlib import Path import pytest from fastapi import Depends from fastapi.testclient import TestClient from app import auth, models from app.database import Base, SessionLocal, engine, get_db from app.main import app from app.providers.openai_compatible import ( CONNECT_TIMEOUT, DEFAULT_READ_TIMEOUT, OpenAICompatibleProvider, ) REPO = Path(__file__).resolve().parents[2] def _paths() -> set[str]: """Every path the app serves, including those inside included routers.""" found = set() def walk(routes): for r in routes: path = getattr(r, "path", None) if path: found.add(path) walk(getattr(r, "routes", []) or []) walk(app.routes) return found @pytest.fixture() def client(): Base.metadata.create_all(bind=engine) setup = SessionLocal() user = models.User(is_guest=False) setup.add(user) setup.flush() setup.add(models.Settings(user_id=user.id, model="test-model")) setup.commit() user_id = user.id setup.close() def _current_user(db=Depends(get_db)): return db.get(models.User, user_id) app.dependency_overrides[auth.get_current_user] = _current_user c = TestClient(app) try: yield c finally: app.dependency_overrides.clear() Base.metadata.drop_all(bind=engine) # --- the removed surfaces ------------------------------------------------ @pytest.mark.parametrize("prefix", ["/api/auth", "/api/analytics", "/api/scripts"]) def test_no_route_serves_a_removed_subsystem(prefix): """Accounts, the visitor dashboard, and campaign scripting are gone as routes, not merely hidden behind a flag.""" assert not [p for p in _paths() if p.startswith(prefix)], prefix @pytest.mark.parametrize("path", [ "/api/auth/me", "/api/auth/login", "/api/auth/register", "/api/auth/logout", "/api/analytics/summary", "/api/analytics/collect", "/api/analytics/access", "/api/scripts", "/api/adventures/1/scripts", "/api/adventures/1/script-state", ]) def test_a_removed_endpoint_answers_404(client, path): assert client.get(path).status_code == 404, path def test_the_application_has_no_scripting_engine(): with pytest.raises(ImportError): __import__("app.scripting") def test_no_module_imports_quickjs(): """The dependency is gone from requirements; this catches an import that would put it back.""" for py in (REPO / "backend" / "app").rglob("*.py"): assert "import quickjs" not in py.read_text(), py def test_requirements_carry_no_hosted_dependencies(): text = (REPO / "backend" / "requirements.txt").read_text() for gone in ("quickjs", "psycopg", "cryptography"): assert gone not in text, gone def test_no_render_deployment_config(): assert not (REPO / "render.yaml").exists() # --- no cloud provider, no key ------------------------------------------ def test_settings_expose_no_api_key_field(client): body = client.get("/api/settings").json() assert "api_key" not in body assert "has_api_key" not in body def test_an_api_key_cannot_be_set_through_the_api(client): """Pydantic ignores unknown fields, so this asserts the value does not land rather than that the request is refused.""" client.put("/api/settings", json={"api_key": "sk-should-not-stick"}) db = SessionLocal() try: assert db.query(models.Settings).first().api_key == "" finally: db.close() def test_the_provider_sends_no_authorization_header(): provider = OpenAICompatibleProvider("http://127.0.0.1:11434/v1", "m") assert "Authorization" not in provider._headers() # --- the model timeout --------------------------------------------------- def test_the_default_timeout_is_generous_but_finite(): """M1 measured a cold model load exceeding the inherited hardcoded 120s on a CPU-only host. It must be longer than that, and it must be a number.""" assert DEFAULT_READ_TIMEOUT > 120 assert DEFAULT_READ_TIMEOUT <= 3600 def test_connect_stays_short_while_reading_stays_patient(): """A wrong address should fail in seconds; a loading model should not.""" provider = OpenAICompatibleProvider("http://127.0.0.1:11434/v1", "m") timeout = provider._timeout() assert timeout.connect == CONNECT_TIMEOUT <= 30 assert timeout.read == DEFAULT_READ_TIMEOUT def test_the_timeout_is_configurable(client): r = client.put("/api/settings", json={"model_timeout_seconds": 900}) assert r.status_code == 200, r.text assert client.get("/api/settings").json()["model_timeout_seconds"] == 900 @pytest.mark.parametrize("value", [0, 29, 3601, -1]) def test_an_unusable_timeout_is_refused(client, value): """Not zero, not negative, and not "wait forever" spelled as a big number.""" assert client.put( "/api/settings", json={"model_timeout_seconds": value} ).status_code == 422 def test_the_provider_honours_the_configured_timeout(): provider = OpenAICompatibleProvider("http://127.0.0.1:11434/v1", "m", read_timeout=45) assert provider._timeout().read == 45 # --- the storyteller stays on loopback ----------------------------------- def test_the_native_start_scripts_bind_loopback(): for script in ("start.sh", "start.ps1"): text = (REPO / script).read_text(errors="ignore") assert "--host 127.0.0.1" in text, script assert "--host 0.0.0.0" not in text, script def test_compose_publishes_to_loopback_only(): """The container listens on 0.0.0.0 because a published port cannot reach anything else. What must stay loopback is the *host* side of the mapping.""" text = (REPO / "docker-compose.yml").read_text() published = re.findall(r'^\s*-\s*"([^"]+)"', text, re.M) assert published, "no published ports found — has the file moved?" for mapping in published: assert mapping.startswith("127.0.0.1:"), mapping