Spaces:
Sleeping
Sleeping
| """ | |
| Guardrail: the two deployment options must not break each other. | |
| This project ships in exactly two deployment shapes, differentiated by | |
| environment configuration over the SAME codebase: | |
| 1) Hosted free web (HF Spaces / Render / β¦) | |
| OLLAMA_ENABLED=false, HOSTED=1, NOMINATIM_URL=<self-hosted/3rd-party> | |
| - Ollama is never used (no local model server on a shared host) β | |
| requests fall back to Claude (BYOK) / the free chatbot path. | |
| - Bulk geocoding against PUBLIC OSM is refused (would get the IP banned). | |
| 2) Local Docker (localhost) | |
| OLLAMA_ENABLED=true, HOSTED unset, public OSM Nominatim | |
| - The Ollama provider is honored. | |
| - Bulk geocoding against public OSM is allowed (single user, ~1 req/s). | |
| These two columns are the contract. A code change that breaks either column | |
| fails this test β that's the guardrail. The behaviour is encoded in two pure | |
| helpers so it stays testable without a server or the network: | |
| web.app.resolve_provider(...) β provider/model selection | |
| pipeline.geocode.public_bulk_blocked β geocoder hosted guard | |
| Run: python3 tests/test_deployment_modes.py | |
| """ | |
| import sys | |
| from pathlib import Path | |
| sys.path.insert(0, str(Path(__file__).parent.parent)) | |
| import xml.etree.ElementTree as ET | |
| from pipeline.chatbot import get_prompt | |
| from pipeline.export import build_kml | |
| from pipeline.extract import DEFAULT_MODEL, DEFAULT_OLLAMA_MODEL | |
| from pipeline.geocode import public_bulk_blocked | |
| from web.app import resolve_provider, safe_err, valid_latlng | |
| PUBLIC = "https://nominatim.openstreetmap.org" | |
| SELF_HOSTED = "http://nominatim:8080" | |
| failures: list[str] = [] | |
| def check(label: str, got, expected) -> None: | |
| if got != expected: | |
| failures.append(f"{label}: expected {expected!r}, got {got!r}") | |
| # ββ Deployment 1: Hosted free web (OLLAMA_ENABLED=false, HOSTED=1) βββββββββββββ | |
| # Ollama must NEVER be selected on a hosted deploy β fall back to Claude. | |
| check("hosted: ollama request falls back to Claude", | |
| resolve_provider("ollama", DEFAULT_MODEL, "qwen2.5", ollama_enabled=False), | |
| ("anthropic", DEFAULT_MODEL)) | |
| check("hosted: explicit Claude model honored", | |
| resolve_provider("anthropic", "claude-haiku-4-5", DEFAULT_OLLAMA_MODEL, ollama_enabled=False), | |
| ("anthropic", "claude-haiku-4-5")) | |
| check("hosted: unknown Claude model defaults", | |
| resolve_provider("anthropic", "made-up-model", DEFAULT_OLLAMA_MODEL, ollama_enabled=False), | |
| ("anthropic", DEFAULT_MODEL)) | |
| # Bulk geocoding against public OSM must be refused; a self-hosted endpoint is OK. | |
| check("hosted: public OSM bulk geocode blocked", | |
| public_bulk_blocked("1", PUBLIC), True) | |
| check("hosted: 'true'/'yes' variants also block", | |
| public_bulk_blocked("true", PUBLIC) and public_bulk_blocked("yes", PUBLIC), True) | |
| check("hosted: self-hosted endpoint is allowed even when HOSTED=1", | |
| public_bulk_blocked("1", SELF_HOSTED), False) | |
| # ββ Deployment 2: Local Docker (OLLAMA_ENABLED=true, HOSTED unset) βββββββββββββ | |
| # The Ollama provider is honored locally. | |
| check("local: ollama request honored", | |
| resolve_provider("ollama", DEFAULT_MODEL, "qwen2.5", ollama_enabled=True), | |
| ("ollama", "qwen2.5")) | |
| check("local: unknown ollama model defaults", | |
| resolve_provider("ollama", DEFAULT_MODEL, "made-up-model", ollama_enabled=True), | |
| ("ollama", DEFAULT_OLLAMA_MODEL)) | |
| check("local: anthropic still selectable", | |
| resolve_provider("anthropic", "claude-opus-4-8", DEFAULT_OLLAMA_MODEL, ollama_enabled=True), | |
| ("anthropic", "claude-opus-4-8")) | |
| # Geocoding against public OSM is allowed when HOSTED is unset. | |
| check("local: public OSM geocode allowed (HOSTED unset)", | |
| public_bulk_blocked(None, PUBLIC), False) | |
| check("local: public OSM geocode allowed (HOSTED empty string)", | |
| public_bulk_blocked("", PUBLIC), False) | |
| # ββ Client-side geocoding contract (hosted mode, option B) ββββββββββββββββββββ | |
| # Hosted mode geocodes in the browser: the server is "geocode-blocked", which is | |
| # exactly what flips web.app.CLIENT_GEOCODE on. Same predicate, asserted as the | |
| # client-geocode trigger. | |
| check("client-geocode ON when server bulk-blocked (hosted)", | |
| public_bulk_blocked("1", PUBLIC), True) | |
| check("client-geocode OFF for local Docker (server geocodes)", | |
| public_bulk_blocked(None, PUBLIC), False) | |
| # /set-coords must only accept well-formed, in-range coordinates from the browser. | |
| check("set-coords accepts valid lat/lng", | |
| valid_latlng("37.7749", "-122.4194"), (37.7749, -122.4194)) | |
| check("set-coords rejects non-numeric", valid_latlng("abc", "1"), None) | |
| check("set-coords rejects out-of-range lat", valid_latlng("91", "0"), None) | |
| check("set-coords rejects out-of-range lng", valid_latlng("0", "181"), None) | |
| # Errors surfaced to the client must not leak an API key (BYOK). | |
| check("safe_err redacts an Anthropic key", | |
| "sk-ant-" in safe_err(Exception("boom sk-ant-abc123DEF_key tail")) and | |
| "abc123DEF_key" not in safe_err(Exception("boom sk-ant-abc123DEF_key tail")), | |
| True) | |
| # ββ Free chatbot path (primary no-key hosted provider) ββββββββββββββββββββββββ | |
| # get_prompt must not crash on the literal JSON braces in the template | |
| # (regression: it used str.format() and KeyError'd) and must NOT hardcode a post | |
| # count (regression: the prefiltered total mismatched chunked files). | |
| try: | |
| _prompt = get_prompt(217) | |
| check("chatbot get_prompt renders a string", isinstance(_prompt, str) and len(_prompt) > 100, True) | |
| check("chatbot get_prompt keeps JSON example", '"post_number"' in _prompt, True) | |
| check("chatbot get_prompt has no leftover {post_count} token", "{post_count}" not in _prompt, True) | |
| check("chatbot get_prompt embeds no fixed count", "217" not in _prompt, True) | |
| check("chatbot get_prompt refers to the attached file", "attached file" in _prompt.lower(), True) | |
| except Exception as exc: # noqa: BLE001 | |
| failures.append(f"chatbot get_prompt raised: {exc!r}") | |
| # ββ KML export must stay valid XML (Google My Maps rejects invalid XML) ββββββββ | |
| # Caption/name data can carry control chars that escape() doesn't strip; the KML | |
| # must still parse. (Regression: "invalid or unsupported data" on My Maps import.) | |
| try: | |
| _kml = build_kml([{ | |
| "name": "Bad\x07CafΓ©", "city": "Lisbon", "country": "Portugal", | |
| "category": "Cafe", "lat": "38.71", "lng": "-9.14", | |
| "instagram_url": "https://instagram.com/reel/AB?x=1&y=2", | |
| }]) | |
| ET.fromstring(_kml) # raises if not well-formed | |
| check("KML is valid XML even with control chars", "\x07" not in _kml, True) | |
| except Exception as exc: # noqa: BLE001 | |
| failures.append(f"build_kml produced invalid XML: {exc!r}") | |
| # ββ Report ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| print("Deployment-mode guardrail\n") | |
| if failures: | |
| print(f"FAIL β {len(failures)} contract violation(s):") | |
| for f in failures: | |
| print(f" β {f}") | |
| print("\nA change broke one deployment mode's contract. See the docstring " | |
| "and DEPLOY.md for the two-column invariant.") | |
| sys.exit(1) | |
| print("PASS β both deployment modes honor their contract:") | |
| print(" β Hosted free web: Ollama hidden β Claude; public-OSM bulk geocode refused") | |
| print(" β Local Docker: Ollama honored; public-OSM geocode allowed") | |
| sys.exit(0) | |